diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index dbc3a89..b3584d3 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -7,7 +7,6 @@ import type { Conversation } from "Conversations/Conversation"; import { AIProvider, AIProviderURL } from "Enums/ApiProvider"; import { AIFunctionCall } from "AIClasses/AIFunctionCall"; import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFunctionDefinition"; -import type VaultkeeperAIPlugin from "main"; import type { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions"; import { isValidJson } from "Helpers/Helpers"; import type { ConversationContent } from "Conversations/ConversationContent"; @@ -22,7 +21,6 @@ export class Claude implements IAIClass { private readonly apiKey: string; private readonly aiPrompt: IPrompt = Resolve(Services.IPrompt); - private readonly plugin: VaultkeeperAIPlugin = Resolve(Services.VaultkeeperAIPlugin); private readonly settingsService: SettingsService = Resolve(Services.SettingsService); private readonly streamingService: StreamingService = Resolve(Services.StreamingService); private readonly aiFunctionDefinitions: AIFunctionDefinitions = Resolve(Services.AIFunctionDefinitions); diff --git a/AIClasses/Claude/ClaudeConversationNamingService.ts b/AIClasses/Claude/ClaudeConversationNamingService.ts index 0ecee79..b422c93 100644 --- a/AIClasses/Claude/ClaudeConversationNamingService.ts +++ b/AIClasses/Claude/ClaudeConversationNamingService.ts @@ -5,6 +5,7 @@ import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider"; import { Role } from "Enums/Role"; import { NamePrompt } from "AIClasses/NamePrompt"; import type { SettingsService } from "Services/SettingsService"; +import type Anthropic from '@anthropic-ai/sdk'; export class ClaudeConversationNamingService implements IConversationNamingService { @@ -43,13 +44,13 @@ export class ClaudeConversationNamingService implements IConversationNamingServi throw new Error(`Claude API error: ${response.status} ${response.statusText} - ${await response.text()}`); } - const data = await response.json(); - const generatedName = data.content?.[0]?.text; + const data = await response.json() as Anthropic.Messages.Message; + const firstContent = data.content?.[0]; - if (!generatedName) { + if (!firstContent || firstContent.type !== 'text') { throw new Error("Failed to generate conversation name"); } - return generatedName; + return firstContent.text; } } \ No newline at end of file diff --git a/AIClasses/FunctionDefinitions/AIFunctionTypes.ts b/AIClasses/FunctionDefinitions/AIFunctionTypes.ts index b0283e9..f7150a5 100644 --- a/AIClasses/FunctionDefinitions/AIFunctionTypes.ts +++ b/AIClasses/FunctionDefinitions/AIFunctionTypes.ts @@ -29,10 +29,12 @@ export interface StoredFunctionCall { /** * Stored function response format used across all AI providers * This is the format saved to conversation history when a function returns + * Note: The actual stored data includes a 'name' field (see AIFunctionResponse.toConversationString) */ export interface StoredFunctionResponse { id: string; functionResponse: { + name: string; response: unknown; }; } diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index 5185c51..96fd5c2 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -14,12 +14,12 @@ import { isValidJson } from "Helpers/Helpers"; import type { ConversationContent } from "Conversations/ConversationContent"; import type { SettingsService } from "Services/SettingsService"; import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionTypes"; -import type { GeminiStreamResponse, GeminiFunctionDeclaration, GeminiContentPart } from "./GeminiInterfaces"; +import type { Candidate, Part, FunctionDeclaration } from "@google/genai"; +import { FinishReason } from "@google/genai"; export class Gemini implements IAIClass { private readonly REQUEST_WEB_SEARCH: string = "request_web_search"; - private readonly STOP_REASON_STOP: string = "STOP"; private readonly apiKey: string; private readonly aiPrompt: IPrompt = Resolve(Services.IPrompt); @@ -98,7 +98,7 @@ export class Gemini implements IAIClass { private parseStreamChunk(chunk: string): IStreamChunk { try { - const data = JSON.parse(chunk) as GeminiStreamResponse; + const data = JSON.parse(chunk) as { candidates?: Candidate[] }; let text = ""; let functionCall: AIFunctionCall | undefined = undefined; @@ -108,8 +108,6 @@ export class Gemini implements IAIClass { // Check for text content if (candidate.content?.parts?.[0]?.text) { text = candidate.content.parts[0].text; - } else if (candidate.text) { - text = candidate.text; } // Check for function call and accumulate @@ -136,7 +134,7 @@ export class Gemini implements IAIClass { const isComplete = !!candidate?.finishReason; const finishReason = candidate?.finishReason; - const shouldContinue = isComplete && finishReason !== this.STOP_REASON_STOP; + const shouldContinue = isComplete && finishReason !== FinishReason.STOP; // If streaming is complete and we have accumulated a function call, return it if (isComplete && this.accumulatedFunctionName) { @@ -159,10 +157,10 @@ export class Gemini implements IAIClass { } } - private extractContents(conversationContent: ConversationContent[]): { role: Role, parts: GeminiContentPart[] }[] { + private extractContents(conversationContent: ConversationContent[]): { role: Role, parts: Part[] }[] { return conversationContent.filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "") .map(content => { - const parts: GeminiContentPart[] = []; + const parts: Part[] = []; const contentToExtract = content.role === Role.User ? content.promptContent : content.content; if (contentToExtract.trim() !== "") { @@ -172,7 +170,10 @@ export class Gemini implements IAIClass { const parsedContent = JSON.parse(contentToExtract) as StoredFunctionResponse; if (parsedContent.functionResponse) { parts.push({ - functionResponse: parsedContent.functionResponse + functionResponse: { + name: parsedContent.functionResponse.name, + response: parsedContent.functionResponse.response as Record + } }); } else { parts.push({ text: contentToExtract }); @@ -218,11 +219,11 @@ export class Gemini implements IAIClass { .filter(message => message.parts.length > 0); } - private mapFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]): GeminiFunctionDeclaration[] { + private mapFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]): FunctionDeclaration[] { return aiFunctionDefinitions.map((functionDefinition) => ({ name: functionDefinition.name, description: functionDefinition.description, - parameters: functionDefinition.parameters + parameters: functionDefinition.parameters as FunctionDeclaration['parameters'] })); } } \ No newline at end of file diff --git a/AIClasses/Gemini/GeminiConversationNamingService.ts b/AIClasses/Gemini/GeminiConversationNamingService.ts index 0c72f1c..a7f91d8 100644 --- a/AIClasses/Gemini/GeminiConversationNamingService.ts +++ b/AIClasses/Gemini/GeminiConversationNamingService.ts @@ -4,6 +4,7 @@ import type { IConversationNamingService } from "AIClasses/IConversationNamingSe import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider"; import { Role } from "Enums/Role"; import { NamePrompt } from "AIClasses/NamePrompt"; +import type { GenerateContentResponse } from "@google/genai"; import type { SettingsService } from "Services/SettingsService"; export class GeminiConversationNamingService implements IConversationNamingService { @@ -40,7 +41,7 @@ export class GeminiConversationNamingService implements IConversationNamingServi throw new Error(`Gemini API error: ${response.status} ${response.statusText} - ${await response.text()}`); } - const data = await response.json(); + const data = await response.json() as GenerateContentResponse; const generatedName = data.candidates?.[0]?.content?.parts?.[0]?.text; if (!generatedName) { diff --git a/AIClasses/Gemini/GeminiInterfaces.ts b/AIClasses/Gemini/GeminiInterfaces.ts deleted file mode 100644 index fd85969..0000000 --- a/AIClasses/Gemini/GeminiInterfaces.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Type definitions for Google Gemini API responses - -export interface GeminiCandidate { - content?: { - parts?: GeminiPart[]; - }; - text?: string; - finishReason?: string; -} - -export interface GeminiPart { - text?: string; - functionCall?: { - name?: string; - args?: Record; - }; -} - -export interface GeminiStreamResponse { - candidates?: GeminiCandidate[]; -} - -export interface GeminiFunctionDeclaration { - name: string; - description: string; - parameters: { - type: string; - properties: Record; - required?: string[]; - }; -} - -export interface GeminiContentPart { - text?: string; - functionCall?: { - name: string; - args: Record; - }; - functionResponse?: { - response: unknown; - }; -} diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index e53a344..614ec6f 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -13,7 +13,7 @@ import { Role } from "Enums/Role"; import { isValidJson } from "Helpers/Helpers"; import type { SettingsService } from "Services/SettingsService"; import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionTypes"; -import type { OpenAIStreamResponse, OpenAITool } from "./OpenAIInterfaces"; +import type { ChatCompletionChunk, ChatCompletionTool } from "openai/resources/chat/completions"; interface IToolCallAccumulator { id: string | null; @@ -163,7 +163,7 @@ export class OpenAI implements IAIClass { return { content: "", isComplete: true }; } - const data = JSON.parse(chunk) as OpenAIStreamResponse; + const data = JSON.parse(chunk) as ChatCompletionChunk; let text = ""; let functionCall: AIFunctionCall | undefined = undefined; @@ -249,7 +249,7 @@ export class OpenAI implements IAIClass { } } - private mapFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]): OpenAITool[] { + private mapFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]): ChatCompletionTool[] { return aiFunctionDefinitions.map((functionDefinition) => ({ type: "function", function: { diff --git a/AIClasses/OpenAI/OpenAIConversationNamingService.ts b/AIClasses/OpenAI/OpenAIConversationNamingService.ts index 14af99e..d11ab4f 100644 --- a/AIClasses/OpenAI/OpenAIConversationNamingService.ts +++ b/AIClasses/OpenAI/OpenAIConversationNamingService.ts @@ -5,6 +5,7 @@ import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider"; import { Role } from "Enums/Role"; import { NamePrompt } from "AIClasses/NamePrompt"; import type { SettingsService } from "Services/SettingsService"; +import type { ChatCompletion } from "openai/resources/chat/completions"; export class OpenAIConversationNamingService implements IConversationNamingService { @@ -46,7 +47,7 @@ export class OpenAIConversationNamingService implements IConversationNamingServi throw new Error(`OpenAI API error: ${response.status} ${response.statusText} - ${await response.text()}`); } - const data = await response.json(); + const data = await response.json() as ChatCompletion; const generatedName = data.choices?.[0]?.message?.content; if (!generatedName) { diff --git a/AIClasses/OpenAI/OpenAIInterfaces.ts b/AIClasses/OpenAI/OpenAIInterfaces.ts deleted file mode 100644 index 6cab676..0000000 --- a/AIClasses/OpenAI/OpenAIInterfaces.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Type definitions for OpenAI API responses - -export interface OpenAIStreamResponse { - choices?: OpenAIChoice[]; -} - -export interface OpenAIChoice { - delta?: OpenAIDelta; - finish_reason?: string; -} - -export interface OpenAIDelta { - content?: string; - tool_calls?: OpenAIToolCallDelta[]; -} - -export interface OpenAIToolCallDelta { - index: number; - id?: string; - function?: { - name?: string; - arguments?: string; - }; -} - -export interface OpenAITool { - type: "function"; - function: { - name: string; - description: string; - parameters: { - type: string; - properties: Record; - required?: string[]; - }; - }; -} diff --git a/__tests__/AIClasses/ClaudeConversationNamingService.test.ts b/__tests__/AIClasses/ClaudeConversationNamingService.test.ts index 4b5fdf0..29f1cf9 100644 --- a/__tests__/AIClasses/ClaudeConversationNamingService.test.ts +++ b/__tests__/AIClasses/ClaudeConversationNamingService.test.ts @@ -54,7 +54,7 @@ describe('ClaudeConversationNamingService', () => { fetchMock.mockResolvedValue({ ok: true, json: async () => ({ - content: [{ text: 'Test Conversation' }] + content: [{ type: 'text', text: 'Test Conversation' }] }) }); @@ -87,7 +87,7 @@ describe('ClaudeConversationNamingService', () => { fetchMock.mockResolvedValue({ ok: true, json: async () => ({ - content: [{ text: 'Generated Name' }] + content: [{ type: 'text', text: 'Generated Name' }] }) }); @@ -126,7 +126,7 @@ describe('ClaudeConversationNamingService', () => { fetchMock.mockResolvedValue({ ok: true, json: async () => ({ - content: [{ text: 'Name' }] + content: [{ type: 'text', text: 'Name' }] }) }); diff --git a/__tests__/AIClasses/Gemini.test.ts b/__tests__/AIClasses/Gemini.test.ts index 1e51326..0cc3347 100644 --- a/__tests__/AIClasses/Gemini.test.ts +++ b/__tests__/AIClasses/Gemini.test.ts @@ -128,7 +128,9 @@ describe('Gemini', () => { it('should parse text from candidate.text field', () => { const chunk = JSON.stringify({ candidates: [{ - text: 'Direct text' + content: { + parts: [{ text: 'Direct text' }] + } }] }); @@ -394,6 +396,7 @@ describe('Gemini', () => { expect(result).toHaveLength(1); expect(result[0].parts).toHaveLength(1); + // Gemini API requires both 'name' and 'response' fields expect(result[0].parts[0]).toEqual({ functionResponse: { name: 'search_files', diff --git a/package-lock.json b/package-lock.json index 7c82b81..c9f1b0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "highlight.js": "^11.11.1", "katex": "^0.16.25", "lowlight": "^3.3.0", + "openai": "^6.8.1", "path-browserify": "^1.0.1", "rehype-highlight": "^7.0.2", "rehype-katex": "^7.0.1", @@ -7254,6 +7255,27 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/openai": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.8.1.tgz", + "integrity": "sha512-ACifslrVgf+maMz9vqwMP4+v9qvx5Yzssydizks8n+YUJ6YwUoxj51sKRQ8HYMfR6wgKLSIlaI108ZwCk+8yig==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/package.json b/package.json index b3ae361..d82db20 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "highlight.js": "^11.11.1", "katex": "^0.16.25", "lowlight": "^3.3.0", + "openai": "^6.8.1", "path-browserify": "^1.0.1", "rehype-highlight": "^7.0.2", "rehype-katex": "^7.0.1",