From ac1dcf7f83b86b78ea44e58b1bc18f58e12b1afe Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Fri, 10 Apr 2026 10:47:23 +0100 Subject: [PATCH] feat: add toggleable web search with UI control and provider-specific implementations Implement web search toggle functionality across all AI providers (Claude, Gemini, Mistral, OpenAI) with conditional tool inclusion based on settings. Add globe button to chat input for toggling web search on/off. Change formatBinaryFiles visibility from public to protected in BaseAIClass and IAIClass interface. Extract tool configuration into private getTools() methods for each provider. Update tests to verify web search toggle behavior and formatBinaryFiles access level changes. --- AIClasses/BaseAIClass.ts | 2 +- AIClasses/Claude/Claude.ts | 25 +++++---- AIClasses/Gemini/Gemini.ts | 37 +++++++------ AIClasses/IAIClass.ts | 2 - AIClasses/Mistral/Mistral.ts | 44 +++++++++------- AIClasses/OpenAI/OpenAI.ts | 15 ++++-- Components/ChatInput.svelte | 81 ++++++++++++++++++++++------- Enums/Copy.ts | 3 ++ Services/SettingsService.ts | 6 ++- __tests__/AIClasses/Claude.test.ts | 26 ++++----- __tests__/AIClasses/Gemini.test.ts | 53 ++++++++++++++----- __tests__/AIClasses/Mistral.test.ts | 14 ++--- __tests__/AIClasses/OpenAI.test.ts | 27 +++++----- 13 files changed, 219 insertions(+), 116 deletions(-) diff --git a/AIClasses/BaseAIClass.ts b/AIClasses/BaseAIClass.ts index 5f79d92..10e103f 100644 --- a/AIClasses/BaseAIClass.ts +++ b/AIClasses/BaseAIClass.ts @@ -88,7 +88,7 @@ export abstract class BaseAIClass implements IAIClass { public abstract streamRequest(conversation: Conversation): AsyncGenerator; - public abstract formatBinaryFiles(attachments: Attachment[]): string; + protected abstract formatBinaryFiles(attachments: Attachment[]): string; protected abstract parseStreamChunk(chunk: string): IStreamChunk; protected abstract extractContents(conversationContent: ConversationContent[]): unknown; diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index 5bf69a0..5657cc2 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -63,14 +63,7 @@ export class Claude extends BaseAIClass { const messages = this.addCacheControlToMessages( await this.extractContents(conversation.contents)); - const webSearchTool: WebSearchTool20250305 = { - type: "web_search_20250305", - name: "web_search", - max_uses: 5 - }; - - const tools: ToolUnion[] = this.addCacheControlToTools([ - webSearchTool, ...this.mapFunctionDefinitions(this.aiToolDefinitions)]); + const tools = this.getTools(); const requestBody = { model: this.model(), @@ -288,7 +281,7 @@ export class Claude extends BaseAIClass { })); } - public formatBinaryFiles(attachments: Attachment[]): string { + protected formatBinaryFiles(attachments: Attachment[]): string { const contentBlocks = attachments.flatMap(attachment => { const fileID = attachment.getFileID(this.provider); if (!fileID) { @@ -375,6 +368,20 @@ export class Claude extends BaseAIClass { return cachedMessages; } + private getTools(): ToolUnion[] { + if (this.settingsService.settings.enableWebSearch) { + const webSearchTool: WebSearchTool20250305 = { + type: "web_search_20250305", + name: "web_search", + max_uses: 5 + }; + + return this.addCacheControlToTools([ + webSearchTool, ...this.mapFunctionDefinitions(this.aiToolDefinitions)]); + } + return this.addCacheControlToTools(this.mapFunctionDefinitions(this.aiToolDefinitions)); + } + private buildClaudeToolChoice(): { type: string } { // If no tools defined, fall back to auto if (this.aiToolDefinitions.length === 0) { diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index 837866a..7f1de82 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -5,7 +5,7 @@ import type { Attachment } from "Conversations/Attachment"; import { Role } from "Enums/Role"; import { AIProvider, AIProviderURL } from "Enums/ApiProvider"; import { AIToolCall } from "AIClasses/AIToolCall"; -import { fromString as aiToolFromString } from "Enums/AITool"; +import { AITool, fromString as aiToolFromString } from "Enums/AITool"; import type { IAIToolDefinition } from "AIClasses/ToolDefinitions/IAIToolDefinition"; import type { ConversationContent } from "Conversations/ConversationContent"; import type { Candidate, Part, FunctionDeclaration } from "@google/genai"; @@ -22,7 +22,6 @@ import { Copy, replaceCopy } from "Enums/Copy"; export class Gemini extends BaseAIClass { - private readonly REQUEST_WEB_SEARCH: string = "request_web_search"; private readonly SUPPORTED_MIMETYPES = [ // Common Text @@ -83,7 +82,7 @@ export class Gemini extends BaseAIClass { public async* streamRequest(conversation: Conversation): AsyncGenerator { // next request should use web search only (gemini api doesn't support custom tooling and grounding at the same time) - const requestWebSearch = this.accumulatedFunctionName == this.REQUEST_WEB_SEARCH; + const requestWebSearch = this.accumulatedFunctionName == AITool.RequestWebSearch; this.accumulatedFunctionName = null; this.accumulatedFunctionArgs = {}; @@ -96,18 +95,7 @@ export class Gemini extends BaseAIClass { const contents = await this.extractContents(conversation.contents); - const tools = requestWebSearch ? { google_search: {} } : - { - functionDeclarations: [ - { - name: "request_web_search", - description: `Use this function when you need to search the web for current - information, recent events, news, or facts that may have changed. - After calling this, you will be able to perform web searches.`, - }, - ...this.mapFunctionDefinitions(this.aiToolDefinitions), - ] - } + const tools = requestWebSearch ? { google_search: {} } : this.getTools() const requestBody: Record = { system_instruction: { @@ -326,7 +314,7 @@ export class Gemini extends BaseAIClass { })); } - public formatBinaryFiles(attachments: Attachment[]): string { + protected formatBinaryFiles(attachments: Attachment[]): string { const parts: unknown[] = []; for (const attachment of attachments) { @@ -361,6 +349,23 @@ export class Gemini extends BaseAIClass { return JSON.stringify(parts); } + private getTools(): { functionDeclarations: FunctionDeclaration[] } { + if (this.settingsService.settings.enableWebSearch) { + return { + functionDeclarations: [ + { + name: AITool.RequestWebSearch, + description: `Use this function when you need to search the web for current + information, recent events, news, or facts that may have changed. + After calling this, you will be able to perform web searches.`, + }, + ...this.mapFunctionDefinitions(this.aiToolDefinitions), + ] + }; + } + return { functionDeclarations: this.mapFunctionDefinitions(this.aiToolDefinitions) }; + } + private buildGeminiToolConfig(): { function_calling_config: { mode: string } } { // If no tools defined, fall back to auto if (this.aiToolDefinitions.length === 0) { diff --git a/AIClasses/IAIClass.ts b/AIClasses/IAIClass.ts index 6c01dc6..52b8d60 100644 --- a/AIClasses/IAIClass.ts +++ b/AIClasses/IAIClass.ts @@ -1,6 +1,5 @@ import type { IStreamChunk } from "Services/StreamingService"; import type { Conversation } from "Conversations/Conversation"; -import type { Attachment } from "Conversations/Attachment"; import type { IAIToolDefinition } from "./ToolDefinitions/IAIToolDefinition"; import type { AIProvider } from "Enums/ApiProvider"; import type { AgentType } from "Enums/AgentType"; @@ -17,7 +16,6 @@ export interface IAIClass { set aiToolUsageMode(mode: AIToolUsageMode); streamRequest(conversation: Conversation): AsyncGenerator; - formatBinaryFiles(attachments: Attachment[]): string; // Optional provider-level tool call handler. Called before AIToolService. resolveToolCall?(toolCall: AIToolCall): Promise; diff --git a/AIClasses/Mistral/Mistral.ts b/AIClasses/Mistral/Mistral.ts index d658f73..6cc7474 100644 --- a/AIClasses/Mistral/Mistral.ts +++ b/AIClasses/Mistral/Mistral.ts @@ -46,24 +46,7 @@ export class Mistral extends BaseAIClass { public async* streamRequest(conversation: Conversation): AsyncGenerator { const messages = await this.buildMessages(conversation); - - const tools = [ - { - type: "function" as const, - function: { - name: AITool.RequestWebSearch, - description: `Use this function when you need to search the web for current information, recent events, news, or facts that may have changed.`, - parameters: { - type: "object" as const, - properties: { - query: { type: "string", description: "The search query to look up on the web." } - }, - required: ["query"] - } - } - }, - ...this.mapFunctionDefinitions(this.aiToolDefinitions) - ]; + const tools = this.getTools(); const requestBody: Record = { model: this.model(), @@ -357,7 +340,7 @@ export class Mistral extends BaseAIClass { })); } - public formatBinaryFiles(attachments: Attachment[]): string { + protected formatBinaryFiles(attachments: Attachment[]): string { const contentParts: MistralContentPart[] = []; const fileService = this.aiFileService as MistralFileService; @@ -420,6 +403,29 @@ export class Mistral extends BaseAIClass { return JSON.stringify(contentParts); } + private getTools(): MistralToolDefinition[] { + if (this.settingsService.settings.enableWebSearch) { + return [ + { + type: "function" as const, + function: { + name: AITool.RequestWebSearch, + description: `Use this function when you need to search the web for current information, recent events, news, or facts that may have changed.`, + parameters: { + type: "object" as const, + properties: { + query: { type: "string", description: "The search query to look up on the web." } + }, + required: ["query"] + } + } + }, + ...this.mapFunctionDefinitions(this.aiToolDefinitions) + ]; + } + return this.mapFunctionDefinitions(this.aiToolDefinitions); + } + private buildMistralToolChoice(): string { if (this.aiToolDefinitions.length === 0) { return "auto"; diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index 3995bda..e5352e0 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -42,9 +42,7 @@ export class OpenAI extends BaseAIClass { const input = await this.extractContents(conversation.contents); - const tools = [{ - type: "web_search" - }, ...this.mapFunctionDefinitions(this.aiToolDefinitions)]; + const tools = this.getTools(); const requestBody = { model: this.model(), @@ -329,7 +327,7 @@ export class OpenAI extends BaseAIClass { })); } - public formatBinaryFiles(attachments: Attachment[]): string { + protected formatBinaryFiles(attachments: Attachment[]): string { const contentBlocks: unknown[] = []; for (const attachment of attachments) { @@ -367,6 +365,15 @@ export class OpenAI extends BaseAIClass { }]); } + private getTools(): (OpenAIToolTool | { type: string })[] { + if (this.settingsService.settings.enableWebSearch) { + return [{ + type: "web_search" + }, ...this.mapFunctionDefinitions(this.aiToolDefinitions)]; + } + return this.mapFunctionDefinitions(this.aiToolDefinitions); + } + private buildOpenAIToolChoice(): string { // If no tools defined, fall back to auto if (this.aiToolDefinitions.length === 0) { diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index 9a62a21..d8a84f2 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -21,6 +21,7 @@ import { Copy, replaceCopy } from "Enums/Copy"; import { HelpModal } from "Modals/HelpModal"; import type { IPrompt } from "AIPrompts/IPrompt"; + import type { SettingsService } from "Services/SettingsService"; export let attachments: Attachment[] = []; @@ -32,6 +33,7 @@ export let onStop: () => void; const inputService: InputService = Resolve(Services.InputService); + const settingsService: SettingsService = Resolve(Services.SettingsService); const userInputService: UserInputService = Resolve(Services.UserInputService); const searchStateStore: SearchStateStore = Resolve(Services.SearchStateStore); const diffService: DiffService = Resolve(Services.DiffService); @@ -43,6 +45,7 @@ let inputDisplay: InputDisplay; let textareaElement: HTMLDivElement; let userInstructionButton: HTMLButtonElement; + let webSearchButton: HTMLButtonElement; let submitButton: HTMLButtonElement; let editModeButton: HTMLButtonElement; let planningModeButton: HTMLButtonElement; @@ -177,6 +180,10 @@ setIcon(userInstructionButton, "user-round-pen"); } + $: if (webSearchButton) { + setIcon(webSearchButton, "globe"); + } + $: userInstructionAreaActive, (() => { tick().then(async () => { userInstructionActive = (await aiPrompt.userInstruction()).trim() !== ""; @@ -278,6 +285,11 @@ searchStateStore.resetSearch(); } + function toggleWebSearch() { + settingsService.settings.enableWebSearch = !settingsService.settings.enableWebSearch; + settingsService.saveSettings(); + } + function toggleEditMode() { if (planningModeActive) { planningModeActive = false @@ -515,7 +527,15 @@ class:instruction-active={userInstructionActive} bind:this={userInstructionButton} on:click={toggleUserInstructionArea} - aria-label="User Instruction"> + aria-label={Copy.ButtonUserInstruction}> + + +
diff --git a/Enums/Copy.ts b/Enums/Copy.ts index cee2d37..9dc1898 100644 --- a/Enums/Copy.ts +++ b/Enums/Copy.ts @@ -107,6 +107,9 @@ export enum Copy { ButtonTurnOnAgentMode = "Turn on Agent Mode", ButtonTurnOffPlanningMode = "Turn off Planning Mode", ButtonTurnOnPlanningMode = "Turn on Planning Mode", + ButtonTurnOffWebSearch = "Turn off Web Search", + ButtonTurnOnWebSearch = "Turn on Web Search", + ButtonUserInstruction = "User Instruction", // Agent file message AttachedFile = `The user has attached the file {fileName}. The contents of the file are included below. diff --git a/Services/SettingsService.ts b/Services/SettingsService.ts index 390ff19..2063cb1 100644 --- a/Services/SettingsService.ts +++ b/Services/SettingsService.ts @@ -21,7 +21,9 @@ const DEFAULT_SETTINGS: IVaultkeeperAISettings = { snippetSizeLimit: 100, enableMemories: false, - allowUpdatingMemories: true + allowUpdatingMemories: true, + + enableWebSearch: true } export interface IVaultkeeperAISettings { @@ -43,6 +45,8 @@ export interface IVaultkeeperAISettings { enableMemories: boolean; allowUpdatingMemories: boolean; + + enableWebSearch: boolean; } export class SettingsService { diff --git a/__tests__/AIClasses/Claude.test.ts b/__tests__/AIClasses/Claude.test.ts index ff602bf..c3edd1d 100644 --- a/__tests__/AIClasses/Claude.test.ts +++ b/__tests__/AIClasses/Claude.test.ts @@ -1236,7 +1236,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -1264,7 +1264,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -1292,7 +1292,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -1316,7 +1316,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -1340,7 +1340,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -1364,7 +1364,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1405,7 +1405,7 @@ describe('Claude', () => { } ]; - const result = claude.formatBinaryFiles(attachments as any); + const result = (claude as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(6); @@ -1472,7 +1472,7 @@ describe('Claude', () => { } ]; - const result = claude.formatBinaryFiles(attachments as any); + const result = (claude as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(5); @@ -1500,7 +1500,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); // Should return empty array when no file ID is available @@ -1529,7 +1529,7 @@ describe('Claude', () => { } ]; - const result = claude.formatBinaryFiles(attachments as any); + const result = (claude as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(4); @@ -1542,7 +1542,7 @@ describe('Claude', () => { it('should handle empty files array', () => { const attachments: any[] = []; - const result = claude.formatBinaryFiles(attachments); + const result = (claude as any).formatBinaryFiles(attachments); const parsed = JSON.parse(result); expect(parsed).toHaveLength(0); @@ -1559,7 +1559,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed[0].text).toBe(replaceCopy(Copy.AttachedFile, ["report (final) v2.pdf"])); @@ -1576,7 +1576,7 @@ describe('Claude', () => { deleteFileID: vi.fn() }; - const result = claude.formatBinaryFiles([attachment as any]); + const result = (claude as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed[1]).toEqual({ diff --git a/__tests__/AIClasses/Gemini.test.ts b/__tests__/AIClasses/Gemini.test.ts index d32e080..e82cca1 100644 --- a/__tests__/AIClasses/Gemini.test.ts +++ b/__tests__/AIClasses/Gemini.test.ts @@ -337,7 +337,35 @@ describe('Gemini', () => { }); describe('Web Search Toggle', () => { - it('should use custom tools by default', async () => { + it('should use custom tools by default (no request_web_search when disabled)', async () => { + mockSettingsService.settings.enableWebSearch = false; + + const conversation = new Conversation(); + conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test' })); + + mockStreamingService.streamRequest.mockImplementation(async function* () { + yield { content: 'done', isComplete: true }; + }); + + const generator = gemini.streamRequest(conversation); + for await (const chunk of generator) {} + + const callArgs = mockStreamingService.streamRequest.mock.calls[0]; + const requestBody = callArgs[1]; + + expect(requestBody.tools[0]).toHaveProperty('functionDeclarations'); + expect(requestBody.tools[0].functionDeclarations).toBeInstanceOf(Array); + + // Should NOT include request_web_search when web search is disabled + const hasWebSearchFunc = requestBody.tools[0].functionDeclarations.some( + (f: any) => f.name === 'request_web_search' + ); + expect(hasWebSearchFunc).toBe(false); + }); + + it('should include request_web_search function when web search is enabled', async () => { + mockSettingsService.settings.enableWebSearch = true; + const conversation = new Conversation(); conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test' })); @@ -355,7 +383,6 @@ describe('Gemini', () => { expect(requestBody.tools[0].functionDeclarations).toBeInstanceOf(Array); expect(requestBody.tools[0].functionDeclarations.length).toBeGreaterThan(0); - // Should include request_web_search function const hasWebSearchFunc = requestBody.tools[0].functionDeclarations.some( (f: any) => f.name === 'request_web_search' ); @@ -1131,7 +1158,7 @@ describe('Gemini', () => { deleteFileID: vi.fn() }; - const result = gemini.formatBinaryFiles([attachment as any]); + const result = (gemini as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -1157,7 +1184,7 @@ describe('Gemini', () => { deleteFileID: vi.fn() }; - const result = gemini.formatBinaryFiles([attachment as any]); + const result = (gemini as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -1183,7 +1210,7 @@ describe('Gemini', () => { deleteFileID: vi.fn() }; - const result = gemini.formatBinaryFiles([attachment as any]); + const result = (gemini as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -1206,7 +1233,7 @@ describe('Gemini', () => { deleteFileID: vi.fn() }; - const result = gemini.formatBinaryFiles([attachment as any]); + const result = (gemini as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1226,7 +1253,7 @@ describe('Gemini', () => { deleteFileID: vi.fn() }; - const result = gemini.formatBinaryFiles([attachment as any]); + const result = (gemini as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1266,7 +1293,7 @@ describe('Gemini', () => { } ]; - const result = gemini.formatBinaryFiles(attachments as any); + const result = (gemini as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(6); @@ -1330,7 +1357,7 @@ describe('Gemini', () => { } ]; - const result = gemini.formatBinaryFiles(attachments as any); + const result = (gemini as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(5); @@ -1366,7 +1393,7 @@ describe('Gemini', () => { } ]; - const result = gemini.formatBinaryFiles(attachments as any); + const result = (gemini as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); // Only successful upload @@ -1380,7 +1407,7 @@ describe('Gemini', () => { }); it('should handle empty attachments array', () => { - const result = gemini.formatBinaryFiles([]); + const result = (gemini as any).formatBinaryFiles([]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(0); @@ -1397,7 +1424,7 @@ describe('Gemini', () => { deleteFileID: vi.fn() }; - const result = gemini.formatBinaryFiles([attachment as any]); + const result = (gemini as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed[0].text).toBe(replaceCopy(Copy.AttachedFile, ["report (final) v2.pdf"])); @@ -1414,7 +1441,7 @@ describe('Gemini', () => { deleteFileID: vi.fn() }; - const result = gemini.formatBinaryFiles([attachment as any]); + const result = (gemini as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed[1]).toEqual({ diff --git a/__tests__/AIClasses/Mistral.test.ts b/__tests__/AIClasses/Mistral.test.ts index 7d35b8a..8874441 100644 --- a/__tests__/AIClasses/Mistral.test.ts +++ b/__tests__/AIClasses/Mistral.test.ts @@ -700,7 +700,7 @@ describe('Mistral', () => { deleteFileID: vi.fn() }; - const result = mistral.formatBinaryFiles([attachment as any]); + const result = (mistral as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -725,7 +725,7 @@ describe('Mistral', () => { deleteFileID: vi.fn() }; - const result = mistral.formatBinaryFiles([attachment as any]); + const result = (mistral as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -750,7 +750,7 @@ describe('Mistral', () => { deleteFileID: vi.fn() }; - const result = mistral.formatBinaryFiles([attachment as any]); + const result = (mistral as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); @@ -771,7 +771,7 @@ describe('Mistral', () => { deleteFileID: vi.fn() }; - const result = mistral.formatBinaryFiles([attachment as any]); + const result = (mistral as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -803,7 +803,7 @@ describe('Mistral', () => { } ]; - const result = mistral.formatBinaryFiles(attachments as any); + const result = (mistral as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(4); @@ -834,7 +834,7 @@ describe('Mistral', () => { deleteFileID: vi.fn() }; - const result = mistral.formatBinaryFiles([attachment as any]); + const result = (mistral as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); // Should return empty array when no file ID is available @@ -844,7 +844,7 @@ describe('Mistral', () => { it('should handle empty files array', () => { const attachments: any[] = []; - const result = mistral.formatBinaryFiles(attachments); + const result = (mistral as any).formatBinaryFiles(attachments); const parsed = JSON.parse(result); expect(parsed).toHaveLength(0); diff --git a/__tests__/AIClasses/OpenAI.test.ts b/__tests__/AIClasses/OpenAI.test.ts index ff682c0..c7ec113 100644 --- a/__tests__/AIClasses/OpenAI.test.ts +++ b/__tests__/AIClasses/OpenAI.test.ts @@ -976,7 +976,9 @@ describe('OpenAI', () => { ); }); - it('should include name field in web_search tool', async () => { + it('should include web_search tool when web search is enabled', async () => { + mockSettingsService.settings.enableWebSearch = true; + const conversation = new Conversation(); conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test message' })); @@ -993,7 +995,6 @@ describe('OpenAI', () => { expect(webSearchTool).toBeDefined(); expect(webSearchTool.type).toBe('web_search'); - expect(webSearchTool.name).toBe(undefined); }); }); @@ -1009,7 +1010,7 @@ describe('OpenAI', () => { deleteFileID: vi.fn() }; - const result = openai.formatBinaryFiles([attachment as any]); + const result = (openai as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1036,7 +1037,7 @@ describe('OpenAI', () => { deleteFileID: vi.fn() }; - const result = openai.formatBinaryFiles([attachment as any]); + const result = (openai as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1063,7 +1064,7 @@ describe('OpenAI', () => { deleteFileID: vi.fn() }; - const result = openai.formatBinaryFiles([attachment as any]); + const result = (openai as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1089,7 +1090,7 @@ describe('OpenAI', () => { deleteFileID: vi.fn() }; - const result = openai.formatBinaryFiles([attachment as any]); + const result = (openai as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1115,7 +1116,7 @@ describe('OpenAI', () => { deleteFileID: vi.fn() }; - const result = openai.formatBinaryFiles([attachment as any]); + const result = (openai as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1137,7 +1138,7 @@ describe('OpenAI', () => { deleteFileID: vi.fn() }; - const result = openai.formatBinaryFiles([attachment as any]); + const result = (openai as any).formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1176,7 +1177,7 @@ describe('OpenAI', () => { } ]; - const result = openai.formatBinaryFiles(attachments as any); + const result = (openai as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1242,7 +1243,7 @@ describe('OpenAI', () => { } ]; - const result = openai.formatBinaryFiles(attachments as any); + const result = (openai as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1286,7 +1287,7 @@ describe('OpenAI', () => { } ]; - const result = openai.formatBinaryFiles(attachments as any); + const result = (openai as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1302,7 +1303,7 @@ describe('OpenAI', () => { }); it('should handle empty attachments array', () => { - const result = openai.formatBinaryFiles([]); + const result = (openai as any).formatBinaryFiles([]); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1); @@ -1332,7 +1333,7 @@ describe('OpenAI', () => { } ]; - const result = openai.formatBinaryFiles(attachments as any); + const result = (openai as any).formatBinaryFiles(attachments as any); const parsed = JSON.parse(result); expect(parsed).toHaveLength(1);