mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
refactor: replace custom type definitions with official SDK types
Remove custom interface definitions for Claude, OpenAI, and Gemini APIs in favor of importing types directly from official SDKs (@anthropic-ai/sdk, openai, @google/genai). Add openai package dependency and update StoredFunctionResponse interface to include name field.
This commit is contained in:
parent
15f1a01e4a
commit
96a90452f8
13 changed files with 56 additions and 105 deletions
|
|
@ -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<IPrompt>(Services.IPrompt);
|
||||
private readonly plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
private readonly settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
private readonly streamingService: StreamingService = Resolve<StreamingService>(Services.StreamingService);
|
||||
private readonly aiFunctionDefinitions: AIFunctionDefinitions = Resolve<AIFunctionDefinitions>(Services.AIFunctionDefinitions);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IPrompt>(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<string, unknown>
|
||||
}
|
||||
});
|
||||
} 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']
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GeminiStreamResponse {
|
||||
candidates?: GeminiCandidate[];
|
||||
}
|
||||
|
||||
export interface GeminiFunctionDeclaration {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
type: string;
|
||||
properties: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GeminiContentPart {
|
||||
text?: string;
|
||||
functionCall?: {
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
};
|
||||
functionResponse?: {
|
||||
response: unknown;
|
||||
};
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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' }]
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
22
package-lock.json
generated
22
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue