diff --git a/core/config/load.ts b/core/config/load.ts index e3d83a6b63..533e0f4c2e 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -624,7 +624,7 @@ async function intermediateToFinalConfig({ return { config: continueConfig, errors }; } -function llmToSerializedModelDescription(llm: ILLM): ModelDescription { +export function llmToSerializedModelDescription(llm: ILLM): ModelDescription { return { provider: llm.providerName, underlyingProviderName: llm.underlyingProviderName, diff --git a/core/config/types.ts b/core/config/types.ts index a23dcfeec0..c3e69f5950 100644 --- a/core/config/types.ts +++ b/core/config/types.ts @@ -1122,7 +1122,6 @@ declare global { * This is needed to crawl a large number of documentation sites that are dynamically rendered. */ useChromiumForDocsCrawling?: boolean; - useTools?: boolean; modelContextProtocolServers?: MCPOptions[]; } diff --git a/core/index.d.ts b/core/index.d.ts index dde7bc841e..4d36b39377 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -995,6 +995,7 @@ export interface Tool { uri?: string; faviconUrl?: string; group: string; + systemMessageDescription?: string; } interface ToolChoice { diff --git a/core/llm/autodetect.ts b/core/llm/autodetect.ts index cc8d3c7661..ecc9c91d26 100644 --- a/core/llm/autodetect.ts +++ b/core/llm/autodetect.ts @@ -1,9 +1,4 @@ -import { - ChatMessage, - ModelCapability, - ModelDescription, - TemplateType, -} from "../index.js"; +import { ChatMessage, ModelCapability, TemplateType } from "../index.js"; import { anthropicTemplateMessages, @@ -41,7 +36,6 @@ import { xWinCoderEditPrompt, zephyrEditPrompt, } from "./templates/edit.js"; -import { PROVIDER_TOOL_SUPPORT } from "./toolSupport.js"; const PROVIDER_HANDLES_TEMPLATING: string[] = [ "lmstudio", @@ -103,17 +97,6 @@ const MODEL_SUPPORTS_IMAGES: string[] = [ "granite-vision", ]; -function modelSupportsTools(modelDescription: ModelDescription) { - if (modelDescription.capabilities?.tools !== undefined) { - return modelDescription.capabilities.tools; - } - const providerSupport = PROVIDER_TOOL_SUPPORT[modelDescription.provider]; - if (!providerSupport) { - return false; - } - return providerSupport(modelDescription.model) ?? false; -} - function modelSupportsImages( provider: string, model: string, @@ -389,6 +372,4 @@ export { autodetectTemplateType, llmCanGenerateInParallel, modelSupportsImages, - modelSupportsTools }; - diff --git a/core/llm/llms/Bedrock.ts b/core/llm/llms/Bedrock.ts index 3b44e1f619..396528d55a 100644 --- a/core/llm/llms/Bedrock.ts +++ b/core/llm/llms/Bedrock.ts @@ -9,6 +9,7 @@ import { } from "@aws-sdk/client-bedrock-runtime"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { llmToSerializedModelDescription } from "../../config/load.js"; import { ChatMessage, Chunk, @@ -17,7 +18,7 @@ import { } from "../../index.js"; import { renderChatMessage, stripImages } from "../../util/messageContent.js"; import { BaseLLM } from "../index.js"; -import { PROVIDER_TOOL_SUPPORT } from "../toolSupport.js"; +import { modelSupportsNativeTools } from "../toolSupport.js"; import { getSecureID } from "../utils/getSecureID.js"; interface ModelConfig { @@ -292,9 +293,12 @@ class Bedrock extends BaseLLM { const convertedMessages = this._convertMessages(messages); const shouldCacheSystemMessage = - !!systemMessage && this.cacheBehavior?.cacheSystemMessage || this.completionOptions.promptCaching; + (!!systemMessage && this.cacheBehavior?.cacheSystemMessage) || + this.completionOptions.promptCaching; const enablePromptCaching = - shouldCacheSystemMessage || this.cacheBehavior?.cacheConversation || this.completionOptions.promptCaching; + shouldCacheSystemMessage || + this.cacheBehavior?.cacheConversation || + this.completionOptions.promptCaching; const shouldCacheToolsConfig = this.completionOptions.promptCaching; // Add header for prompt caching @@ -305,22 +309,23 @@ class Bedrock extends BaseLLM { }; } - const supportsTools = - (this.capabilities?.tools || PROVIDER_TOOL_SUPPORT.bedrock?.(options.model)) ?? false; - - let toolConfig = supportsTools && options.tools - ? { - tools: options.tools.map((tool) => ({ - toolSpec: { - name: tool.function.name, - description: tool.function.description, - inputSchema: { - json: tool.function.parameters, - }, - }, - })), - } as ToolConfiguration - : undefined; + const modelDesc = llmToSerializedModelDescription(this); + const supportsTools = modelSupportsNativeTools(modelDesc); + + let toolConfig = + supportsTools && !!options.tools + ? ({ + tools: options.tools.map((tool) => ({ + toolSpec: { + name: tool.function.name, + description: tool.function.description, + inputSchema: { + json: tool.function.parameters, + }, + }, + })), + } as ToolConfiguration) + : undefined; if (toolConfig?.tools && shouldCacheToolsConfig) { toolConfig.tools.push({ cachePoint: { type: "default" } }); diff --git a/core/llm/toolSupport.test.ts b/core/llm/toolSupport.test.ts index d3100a61b4..680686d2aa 100644 --- a/core/llm/toolSupport.test.ts +++ b/core/llm/toolSupport.test.ts @@ -1,9 +1,9 @@ // core/llm/toolSupport.test.ts -import { PROVIDER_TOOL_SUPPORT } from "./toolSupport"; +import { NATIVE_TOOL_SUPPORT } from "./toolSupport"; -describe("PROVIDER_TOOL_SUPPORT", () => { +describe("NATIVE_TOOL_SUPPORT", () => { describe("continue-proxy", () => { - const supportsFn = PROVIDER_TOOL_SUPPORT["continue-proxy"]; + const supportsFn = NATIVE_TOOL_SUPPORT["continue-proxy"]; it("should return true for Claude 3.5 models", () => { expect( @@ -61,7 +61,7 @@ describe("PROVIDER_TOOL_SUPPORT", () => { }); describe("anthropic", () => { - const supportsFn = PROVIDER_TOOL_SUPPORT["anthropic"]; + const supportsFn = NATIVE_TOOL_SUPPORT["anthropic"]; it("should return true for Claude 3.5 models", () => { expect(supportsFn("claude-3-5-sonnet")).toBe(true); @@ -85,7 +85,7 @@ describe("PROVIDER_TOOL_SUPPORT", () => { }); describe("openai", () => { - const supportsFn = PROVIDER_TOOL_SUPPORT["openai"]; + const supportsFn = NATIVE_TOOL_SUPPORT["openai"]; it("should return true for GPT-4 models", () => { expect(supportsFn("gpt-4")).toBe(true); @@ -110,7 +110,7 @@ describe("PROVIDER_TOOL_SUPPORT", () => { }); describe("gemini", () => { - const supportsFn = PROVIDER_TOOL_SUPPORT["gemini"]; + const supportsFn = NATIVE_TOOL_SUPPORT["gemini"]; it("should return true for all Gemini models", () => { expect(supportsFn("gemini-pro")).toBe(true); @@ -130,7 +130,7 @@ describe("PROVIDER_TOOL_SUPPORT", () => { }); describe("bedrock", () => { - const supportsFn = PROVIDER_TOOL_SUPPORT["bedrock"]; + const supportsFn = NATIVE_TOOL_SUPPORT["bedrock"]; it("should return true for Claude 3.5 Sonnet models", () => { expect(supportsFn("anthropic.claude-3-5-sonnet-20240620-v1:0")).toBe( @@ -180,7 +180,7 @@ describe("PROVIDER_TOOL_SUPPORT", () => { }); describe("mistral", () => { - const supportsFn = PROVIDER_TOOL_SUPPORT["mistral"]; + const supportsFn = NATIVE_TOOL_SUPPORT["mistral"]; it("should return true for supported models", () => { expect(supportsFn("mistral-large-latest")).toBe(true); @@ -212,7 +212,7 @@ describe("PROVIDER_TOOL_SUPPORT", () => { }); describe("ollama", () => { - const supportsFn = PROVIDER_TOOL_SUPPORT["ollama"]; + const supportsFn = NATIVE_TOOL_SUPPORT["ollama"]; it("should return true for supported models", () => { expect(supportsFn("llama3.1")).toBe(true); @@ -256,17 +256,17 @@ describe("PROVIDER_TOOL_SUPPORT", () => { describe("edge cases", () => { it("should handle empty model names", () => { - expect(PROVIDER_TOOL_SUPPORT["continue-proxy"]("")).toBe(false); - expect(PROVIDER_TOOL_SUPPORT["anthropic"]("")).toBe(false); - expect(PROVIDER_TOOL_SUPPORT["openai"]("")).toBe(false); - expect(PROVIDER_TOOL_SUPPORT["gemini"]("")).toBe(false); - expect(PROVIDER_TOOL_SUPPORT["bedrock"]("")).toBe(false); - expect(PROVIDER_TOOL_SUPPORT["ollama"]("")).toBe(false); + expect(NATIVE_TOOL_SUPPORT["continue-proxy"]("")).toBe(false); + expect(NATIVE_TOOL_SUPPORT["anthropic"]("")).toBe(false); + expect(NATIVE_TOOL_SUPPORT["openai"]("")).toBe(false); + expect(NATIVE_TOOL_SUPPORT["gemini"]("")).toBe(false); + expect(NATIVE_TOOL_SUPPORT["bedrock"]("")).toBe(false); + expect(NATIVE_TOOL_SUPPORT["ollama"]("")).toBe(false); }); it("should handle non-existent provider", () => { // @ts-ignore - Testing runtime behavior with invalid provider - expect(PROVIDER_TOOL_SUPPORT["non-existent"]).toBe(undefined); + expect(NATIVE_TOOL_SUPPORT["non-existent"]).toBe(undefined); }); }); }); diff --git a/core/llm/toolSupport.ts b/core/llm/toolSupport.ts index e6d4387cef..8fd917ba04 100644 --- a/core/llm/toolSupport.ts +++ b/core/llm/toolSupport.ts @@ -1,274 +1,299 @@ import { parseProxyModelName } from "@continuedev/config-yaml"; +import { ModelDescription } from ".."; -export const PROVIDER_TOOL_SUPPORT: Record boolean> = - { - "continue-proxy": (model) => { - try { - const { provider, model: _model } = parseProxyModelName(model); - if (provider && _model && provider !== "continue-proxy") { - const fn = PROVIDER_TOOL_SUPPORT[provider]; - if (fn) { - return fn(_model); - } +export const NATIVE_TOOL_SUPPORT: Record boolean> = { + "continue-proxy": (model) => { + try { + const { provider, model: _model } = parseProxyModelName(model); + if (provider && _model && provider !== "continue-proxy") { + const fn = NATIVE_TOOL_SUPPORT[provider]; + if (fn) { + return fn(_model); } - } catch (e) {} + } + } catch (e) {} - return [ + return [ + "claude-3-5", + "claude-3.5", + "claude-3-7", + "claude-3.7", + "claude-sonnet-4", + "gpt-4", + "o3", + "gemini", + ].some((part) => model.toLowerCase().startsWith(part)); + }, + anthropic: (model) => { + if ( + [ "claude-3-5", "claude-3.5", "claude-3-7", "claude-3.7", "claude-sonnet-4", - "gpt-4", - "o3", - "gemini", - ].some((part) => model.toLowerCase().startsWith(part)); - }, - anthropic: (model) => { - if ( - [ - "claude-3-5", - "claude-3.5", - "claude-3-7", - "claude-3.7", - "claude-sonnet-4", - ].some((part) => model.toLowerCase().startsWith(part)) - ) { - return true; - } + ].some((part) => model.toLowerCase().startsWith(part)) + ) { + return true; + } - return false; - }, - azure: (model) => { - if ( - model.toLowerCase().startsWith("gpt-4") || - model.toLowerCase().startsWith("o3") - ) - return true; - return false; - }, - openai: (model) => { - // https://platform.openai.com/docs/guides/function-calling#models-supporting-function-calling - if ( - model.toLowerCase().startsWith("gpt-4") || - model.toLowerCase().startsWith("o3") - ) { - return true; - } - // firworks-ai https://docs.fireworks.ai/guides/function-calling - if (model.startsWith("accounts/fireworks/models/")) { - switch (model.substring(26)) { - case "llama-v3p1-405b-instruct": - case "llama-v3p1-70b-instruct": - case "qwen2p5-72b-instruct": - case "firefunction-v1": - case "firefunction-v2": - return true; - default: - return false; - } + return false; + }, + azure: (model) => { + if ( + model.toLowerCase().startsWith("gpt-4") || + model.toLowerCase().startsWith("o3") + ) + return true; + return false; + }, + openai: (model) => { + // https://platform.openai.com/docs/guides/function-calling#models-supporting-function-calling + if ( + model.toLowerCase().startsWith("gpt-4") || + model.toLowerCase().startsWith("o3") + ) { + return true; + } + // firworks-ai https://docs.fireworks.ai/guides/function-calling + if (model.startsWith("accounts/fireworks/models/")) { + switch (model.substring(26)) { + case "llama-v3p1-405b-instruct": + case "llama-v3p1-70b-instruct": + case "qwen2p5-72b-instruct": + case "firefunction-v1": + case "firefunction-v2": + return true; + default: + return false; } + } - return false; - }, - gemini: (model) => { - // All gemini models support function calling - return model.toLowerCase().includes("gemini"); - }, - vertexai: (model) => { - // All gemini models except flash 2.0 lite support function calling - return ( - model.toLowerCase().includes("gemini") && - !model.toLowerCase().includes("lite") - ); - }, - bedrock: (model) => { - // For Bedrock, only support Claude Sonnet models with versions 3.5/3-5 and 3.7/3-7 - if ( - model.toLowerCase().includes("sonnet") && - [ - "claude-3-5", - "claude-3.5", - "claude-3-7", - "claude-3.7", - "claude-sonnet-4", - ].some((part) => model.toLowerCase().includes(part)) - ) { - return true; - } + return false; + }, + gemini: (model) => { + // All gemini models support function calling + return model.toLowerCase().includes("gemini"); + }, + vertexai: (model) => { + // All gemini models except flash 2.0 lite support function calling + return ( + model.toLowerCase().includes("gemini") && + !model.toLowerCase().includes("lite") + ); + }, + bedrock: (model) => { + // For Bedrock, only support Claude Sonnet models with versions 3.5/3-5 and 3.7/3-7 + if ( + model.toLowerCase().includes("sonnet") && + [ + "claude-3-5", + "claude-3.5", + "claude-3-7", + "claude-3.7", + "claude-sonnet-4", + ].some((part) => model.toLowerCase().includes(part)) + ) { + return true; + } + + return false; + }, + mistral: (model) => { + // https://docs.mistral.ai/capabilities/function_calling/ + return ( + !model.toLowerCase().includes("mamba") && + [ + "devstral", + "codestral", + "mistral-large", + "mistral-small", + "pixtral", + "ministral", + "mistral-nemo", + "devstral", + ].some((part) => model.toLowerCase().includes(part)) + ); + }, + // https://ollama.com/search?c=tools + ollama: (model) => { + let modelName = ""; + // Extract the model name after the last slash to support other registries + if (model.includes("/")) { + let parts = model.split("/"); + modelName = parts[parts.length - 1]; + } else { + modelName = model; + } + if ( + ["vision", "math", "guard", "mistrallite", "mistral-openorca"].some( + (part) => modelName.toLowerCase().includes(part), + ) + ) { return false; - }, - mistral: (model) => { - // https://docs.mistral.ai/capabilities/function_calling/ - return ( - !model.toLowerCase().includes("mamba") && - [ - "devstral", - "codestral", - "mistral-large", - "mistral-small", - "pixtral", - "ministral", - "mistral-nemo", - "devstral", - ].some((part) => model.toLowerCase().includes(part)) - ); - }, - // https://ollama.com/search?c=tools - ollama: (model) => { - let modelName = ""; - // Extract the model name after the last slash to support other registries - if (model.includes("/")) { - let parts = model.split("/"); - modelName = parts[parts.length - 1]; - } else { - modelName = model; - } + } + if ( + [ + "cogito", + "llama3.3", + "qwq", + "llama3.2", + "llama3.1", + "qwen2", + "qwen3", + "mixtral", + "command-r", + "smollm2", + "hermes3", + "athene-v2", + "nemotron", + "llama3-groq", + "granite3", + "granite-3", + "aya-expanse", + "firefunction-v2", + "mistral", + "devstral", + ].some((part) => modelName.toLowerCase().includes(part)) + ) { + return true; + } - if ( - ["vision", "math", "guard", "mistrallite", "mistral-openorca"].some( - (part) => modelName.toLowerCase().includes(part), - ) - ) { - return false; - } - if ( - [ - "cogito", - "llama3.3", - "qwq", - "llama3.2", - "llama3.1", - "qwen2", - "qwen3", - "mixtral", - "command-r", - "smollm2", - "hermes3", - "athene-v2", - "nemotron", - "llama3-groq", - "granite3", - "granite-3", - "aya-expanse", - "firefunction-v2", - "mistral", - "devstral", - ].some((part) => modelName.toLowerCase().includes(part)) - ) { - return true; - } + return false; + }, + sambanova: (model) => { + // https://docs.sambanova.ai/cloud/docs/capabilities/function-calling + if ( + model.toLowerCase().startsWith("meta-llama-3") || + model.toLowerCase().includes("llama-4") || + model.toLowerCase().includes("deepseek") + ) { + return true; + } - return false; - }, - sambanova: (model) => { - // https://docs.sambanova.ai/cloud/docs/capabilities/function-calling - if ( - model.toLowerCase().startsWith("meta-llama-3") || - model.toLowerCase().includes("llama-4") || - model.toLowerCase().includes("deepseek") - ) { - return true; - } + return false; + }, + deepseek: (model) => { + if (model !== "deepseek-reasoner") { + return true; + } + return false; + }, + watsonx: (model) => { + if (model.toLowerCase().includes("guard")) { return false; - }, - deepseek: (model) => { - if (model !== "deepseek-reasoner") { - return true; - } + } + if ( + [ + "llama-3", + "llama-4", + "mistral", + "codestral", + "granite-3", + "devstral", + ].some((part) => model.toLowerCase().includes(part)) + ) { + return true; + } + return false; + }, + openrouter: (model) => { + // https://openrouter.ai/models?fmt=cards&supported_parameters=tools + if ( + ["vision", "math", "guard", "mistrallite", "mistral-openorca"].some( + (part) => model.toLowerCase().includes(part), + ) + ) { return false; - }, - watsonx: (model) => { - if (model.toLowerCase().includes("guard")) { - return false; - } - if ( - [ - "llama-3", - "llama-4", - "mistral", - "codestral", - "granite-3", - "devstral", - ].some((part) => model.toLowerCase().includes(part)) - ) { + } + + const supportedPrefixes = [ + "openai/gpt-3.5", + "openai/gpt-4", + "openai/o1", + "openai/o3", + "openai/o4", + "anthropic/claude-3", + "anthropic/claude-4", + "microsoft/phi-3", + "google/gemini-flash-1.5", + "google/gemini-2", + "google/gemini-pro", + "x-ai/grok", + "qwen/qwen3", + "qwen/qwen-", + "cohere/command-r", + "ai21/jamba-1.6", + "mistralai/mistral", + "mistralai/ministral", + "mistralai/codestral", + "mistralai/mixtral", + "mistral/ministral", + "mistral/devstral", + "mistralai/pixtral", + "meta-llama/llama-3.3", + "amazon/nova", + "deepseek/deepseek-r1", + "deepseek/deepseek-chat", + "meta-llama/llama-4", + "all-hands/openhands-lm-32b", + ]; + for (const prefix of supportedPrefixes) { + if (model.toLowerCase().startsWith(prefix)) { return true; } + } - return false; - }, - openrouter: (model) => { - // https://openrouter.ai/models?fmt=cards&supported_parameters=tools - if ( - ["vision", "math", "guard", "mistrallite", "mistral-openorca"].some( - (part) => model.toLowerCase().includes(part), - ) - ) { - return false; + const specificModels = [ + "qwen/qwq-32b", + "qwen/qwen-2.5-72b-instruct", + "meta-llama/llama-3.2-3b-instruct", + "meta-llama/llama-3-8b-instruct", + "meta-llama/llama-3-70b-instruct", + "arcee-ai/caller-large", + "nousresearch/hermes-3-llama-3.1-70b", + ]; + for (const model of specificModels) { + if (model.toLowerCase() === model) { + return true; } + } - const supportedPrefixes = [ - "openai/gpt-3.5", - "openai/gpt-4", - "openai/o1", - "openai/o3", - "openai/o4", - "anthropic/claude-3", - "anthropic/claude-4", - "microsoft/phi-3", - "google/gemini-flash-1.5", - "google/gemini-2", - "google/gemini-pro", - "x-ai/grok", - "qwen/qwen3", - "qwen/qwen-", - "cohere/command-r", - "ai21/jamba-1.6", - "mistralai/mistral", - "mistralai/ministral", - "mistralai/codestral", - "mistralai/mixtral", - "mistral/ministral", - "mistral/devstral", - "mistralai/pixtral", - "meta-llama/llama-3.3", - "amazon/nova", - "deepseek/deepseek-r1", - "deepseek/deepseek-chat", - "meta-llama/llama-4", - "all-hands/openhands-lm-32b", - ]; - for (const prefix of supportedPrefixes) { - if (model.toLowerCase().startsWith(prefix)) { - return true; - } + const supportedContains = ["llama-3.1"]; + for (const model of supportedContains) { + if (model.toLowerCase().includes(model)) { + return true; } + } - const specificModels = [ - "qwen/qwq-32b", - "qwen/qwen-2.5-72b-instruct", - "meta-llama/llama-3.2-3b-instruct", - "meta-llama/llama-3-8b-instruct", - "meta-llama/llama-3-70b-instruct", - "arcee-ai/caller-large", - "nousresearch/hermes-3-llama-3.1-70b", - ]; - for (const model of specificModels) { - if (model.toLowerCase() === model) { - return true; - } - } + return false; + }, +}; - const supportedContains = ["llama-3.1"]; - for (const model of supportedContains) { - if (model.toLowerCase().includes(model)) { - return true; - } - } +export function modelSupportsNativeTools(modelDescription: ModelDescription) { + if (modelDescription.capabilities?.tools !== undefined) { + return modelDescription.capabilities.tools; + } + const providerSupport = NATIVE_TOOL_SUPPORT[modelDescription.provider]; + if (!providerSupport) { + return false; + } + return providerSupport(modelDescription.model) ?? false; +} - return false; - }, - }; +export function modelIsGreatWithNativeTools( + modelDescription: ModelDescription, +): boolean { + return false; + const model = modelDescription.model; + if ( + model.toLowerCase().includes("claude") && + ["3.5", "3-5", "3.7", "3-7", "-4"].some((p) => model.includes(p)) + ) { + return true; + } + return false; +} diff --git a/core/tools/builtIn.ts b/core/tools/builtIn.ts index 9f70b1a221..4e0c4482aa 100644 --- a/core/tools/builtIn.ts +++ b/core/tools/builtIn.ts @@ -1,19 +1,19 @@ export enum BuiltInToolNames { - ReadFile = "builtin_read_file", - EditExistingFile = "builtin_edit_existing_file", - ReadCurrentlyOpenFile = "builtin_read_currently_open_file", - CreateNewFile = "builtin_create_new_file", - RunTerminalCommand = "builtin_run_terminal_command", - GrepSearch = "builtin_grep_search", - FileGlobSearch = "builtin_file_glob_search", - SearchWeb = "builtin_search_web", - ViewDiff = "builtin_view_diff", - LSTool = "builtin_ls", - CreateRuleBlock = "builtin_create_rule_block", + ReadFile = "read_file", + EditExistingFile = "edit_existing_file", + ReadCurrentlyOpenFile = "read_currently_open_file", + CreateNewFile = "create_new_file", + RunTerminalCommand = "run_terminal_command", + GrepSearch = "grep_search", + FileGlobSearch = "file_glob_search", + SearchWeb = "search_web", + ViewDiff = "view_diff", + LSTool = "ls", + CreateRuleBlock = "create_rule_block", // excluded from allTools for now - ViewRepoMap = "builtin_view_repo_map", - ViewSubdirectory = "builtin_view_subdirectory", + ViewRepoMap = "view_repo_map", + ViewSubdirectory = "view_subdirectory", } export const BUILT_IN_GROUP_NAME = "Built-In"; diff --git a/core/tools/definitions/createNewFile.ts b/core/tools/definitions/createNewFile.ts index 41bea0b91f..5d996178d9 100644 --- a/core/tools/definitions/createNewFile.ts +++ b/core/tools/definitions/createNewFile.ts @@ -1,5 +1,6 @@ import { Tool } from "../.."; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; +import { createSystemMessageExampleCall } from "../instructionTools/buildXmlToolsSystemMessage"; export const createNewFileTool: Tool = { type: "function", @@ -30,4 +31,9 @@ export const createNewFileTool: Tool = { }, }, }, + systemMessageDescription: createSystemMessageExampleCall( + BuiltInToolNames.CreateNewFile, + `To create a NEW file, use the ${BuiltInToolNames.CreateNewFile} tool with the relative filepath and new contents. For example, to create a file located at 'path/to/file.txt', you would respond with:`, + `path/to/the_file.txtThese are the new contents of the file`, + ), }; diff --git a/core/tools/definitions/editFile.ts b/core/tools/definitions/editFile.ts index 1653cb2799..8119551158 100644 --- a/core/tools/definitions/editFile.ts +++ b/core/tools/definitions/editFile.ts @@ -1,11 +1,14 @@ import { Tool } from "../.."; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; +import { createSystemMessageExampleCall } from "../instructionTools/buildXmlToolsSystemMessage"; export interface EditToolArgs { filepath: string; changes: string; } +const CHANGES_DESCRIPTION = `Any modifications to the file, showing only needed changes. Do NOT wrap this in a codeblock or write anything besides the code changes. In larger files, use brief language-appropriate placeholders for large unmodified sections, e.g. '// ... existing code ...'`; + export const editFileTool: Tool = { type: "function", displayTitle: "Edit File", @@ -29,10 +32,23 @@ export const editFileTool: Tool = { }, changes: { type: "string", - description: - "Any modifications to the file, showing only needed changes. Do NOT wrap this in a codeblock or write anything besides the code changes. In larger files, use brief language-appropriate placeholders for large unmodified sections, e.g. '// ... existing code ...'", + description: CHANGES_DESCRIPTION, }, }, }, }, + systemMessageDescription: createSystemMessageExampleCall( + BuiltInToolNames.EditExistingFile, + `To edit an EXISTING file, use the ${BuiltInToolNames.EditExistingFile} tool with +- filepath: the relative filepath to the file. +- changes: ${CHANGES_DESCRIPTION} +For example:`, + `path/to/the_file.ts// ... existing code ... + + function subtract(a: number, b: number): number { + return a - b; + } + + // ... rest of code ...`, + ), }; diff --git a/core/tools/definitions/readCurrentlyOpenFile.ts b/core/tools/definitions/readCurrentlyOpenFile.ts index 9c15105a61..1bd8f64e69 100644 --- a/core/tools/definitions/readCurrentlyOpenFile.ts +++ b/core/tools/definitions/readCurrentlyOpenFile.ts @@ -1,5 +1,6 @@ import { Tool } from "../.."; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; +import { createSystemMessageExampleCall } from "../instructionTools/buildXmlToolsSystemMessage"; export const readCurrentlyOpenFileTool: Tool = { type: "function", @@ -19,4 +20,9 @@ export const readCurrentlyOpenFileTool: Tool = { properties: {}, }, }, + systemMessageDescription: createSystemMessageExampleCall( + BuiltInToolNames.ReadCurrentlyOpenFile, + `To view the user's currenly open file, use the ${BuiltInToolNames.ReadCurrentlyOpenFile} tool. +If the user is asking about a file and you don't see any code, use this to check the current file`, + ), }; diff --git a/core/tools/definitions/readFile.ts b/core/tools/definitions/readFile.ts index 42737fe636..7c6425fdd6 100644 --- a/core/tools/definitions/readFile.ts +++ b/core/tools/definitions/readFile.ts @@ -1,5 +1,6 @@ import { Tool } from "../.."; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; +import { createSystemMessageExampleCall } from "../instructionTools/buildXmlToolsSystemMessage"; export const readFileTool: Tool = { type: "function", @@ -26,4 +27,9 @@ export const readFileTool: Tool = { }, }, }, + systemMessageDescription: createSystemMessageExampleCall( + BuiltInToolNames.ReadFile, + `To read a file with a known filepath, use the ${BuiltInToolNames.ReadFile} tool. For example, to read a file located at 'path/to/file.txt', you would respond with this:`, + `path/to/the_file.txt`, + ), }; diff --git a/core/tools/definitions/runTerminalCommand.ts b/core/tools/definitions/runTerminalCommand.ts index 73d3269020..ca7ba25c2c 100644 --- a/core/tools/definitions/runTerminalCommand.ts +++ b/core/tools/definitions/runTerminalCommand.ts @@ -1,5 +1,12 @@ import { Tool } from "../.."; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; +import { createSystemMessageExampleCall } from "../instructionTools/buildXmlToolsSystemMessage"; + +const RUN_COMMAND_NOTES = + "The shell is not stateful and will not remember any previous commands.\ + When a command is run in the background ALWAYS suggest using shell commands to stop it; NEVER suggest using Ctrl+C.\ + When suggesting subsequent shell commands ALWAYS format them in shell command blocks.\ + Do NOT perform actions requiring special/admin privileges."; export const runTerminalCommandTool: Tool = { type: "function", @@ -11,27 +18,30 @@ export const runTerminalCommandTool: Tool = { group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.RunTerminalCommand, - description: - "Run a terminal command in the current directory.\ - The shell is not stateful and will not remember any previous commands.\ - When a command is run in the background ALWAYS suggest using shell commands to stop it; NEVER suggest using Ctrl+C.\ - When suggesting subsequent shell commands ALWAYS format them in shell command blocks.\ - Do NOT perform actions requiring special/admin privileges.", - parameters: { - type: "object", - required: ["command"], - properties: { - command: { - type: "string", - description: - "The command to run. This will be passed directly into the IDE shell.", - }, - waitForCompletion: { - type: "boolean", - description: - "Whether to wait for the command to complete before returning. Default is true. Set to false to run the command in the background. Set to true to run the command in the foreground and wait to collect the output.", - }, + description: `Run a terminal command in the current directory.\n${RUN_COMMAND_NOTES}`, + parameters: { + type: "object", + required: ["command"], + properties: { + command: { + type: "string", + description: + "The command to run. This will be passed directly into the IDE shell.", + }, + waitForCompletion: { + type: "boolean", + description: + "Whether to wait for the command to complete before returning. Default is true. Set to false to run the command in the background. Set to true to run the command in the foreground and wait to collect the output.", }, }, + }, }, + systemMessageDescription: createSystemMessageExampleCall( + BuiltInToolNames.RunTerminalCommand, + `To run a terminal command, use the ${BuiltInToolNames.RunTerminalCommand} tool +${RUN_COMMAND_NOTES} +You can also optionally include false within to run the command in the background. +For example, to see the git log, you could respond with:`, + `git log`, + ), }; diff --git a/core/tools/definitions/searchWeb.ts b/core/tools/definitions/searchWeb.ts index d2ce78efc6..dd2adccef6 100644 --- a/core/tools/definitions/searchWeb.ts +++ b/core/tools/definitions/searchWeb.ts @@ -1,6 +1,7 @@ import { Tool } from "../.."; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; +import { createSystemMessageExampleCall } from "../instructionTools/buildXmlToolsSystemMessage"; export const searchWebTool: Tool = { type: "function", @@ -25,4 +26,9 @@ export const searchWebTool: Tool = { }, }, }, + systemMessageDescription: createSystemMessageExampleCall( + BuiltInToolNames.SearchWeb, + `To search the web, use the ${BuiltInToolNames.SearchWeb} tool with a natural language query. For example, to search for the current weather, you would respond with:`, + `What is the current weather in San Francisco?`, + ), }; diff --git a/core/tools/definitions/viewDiff.ts b/core/tools/definitions/viewDiff.ts index d835986b0a..bd84031f22 100644 --- a/core/tools/definitions/viewDiff.ts +++ b/core/tools/definitions/viewDiff.ts @@ -1,6 +1,7 @@ import { Tool } from "../.."; import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; +import { createSystemMessageExampleCall } from "../instructionTools/buildXmlToolsSystemMessage"; export const viewDiffTool: Tool = { type: "function", @@ -19,4 +20,8 @@ export const viewDiffTool: Tool = { properties: {}, }, }, + systemMessageDescription: createSystemMessageExampleCall( + BuiltInToolNames.ViewDiff, + `To view the current git diff, use the ${BuiltInToolNames.ViewDiff} tool. This will show you the changes made in the working directory compared to the last commit.`, + ), }; diff --git a/core/tools/instructionTools/buildXmlToolsSystemMessage.test.ts b/core/tools/instructionTools/buildXmlToolsSystemMessage.test.ts new file mode 100644 index 0000000000..9e28211b2e --- /dev/null +++ b/core/tools/instructionTools/buildXmlToolsSystemMessage.test.ts @@ -0,0 +1,104 @@ +// const exampleSchema = { +// "type": "object", +// "properties": { +// "name": { +// "type": ["string", "null"], +// "description": "The name of the person, which may be null" +// }, +// "age": { +// "type": "number", +// "minimum": 0, +// "maximum": 150, +// "description": "The age of the person" +// }, +// "email": { +// "type": "string", +// "format": "email", +// "description": "The email address (if provided)" +// } +// }, +// "required": ["name"] +// } + +// const exampleSchema2 = { +// "$id": "http://example.com/complex-object", +// "type": "object", +// "properties": { +// "person": { +// "type": "object", +// "properties": { +// "name": { +// "type": "string", +// "required": true, +// "description": "The name of the person" +// }, +// "age": { +// "type": "number", +// "min": 0, +// "max": 150, +// "description": "The age of the person" +// } +// }, +// "required": ["name", "age"] +// }, +// "hobbies": { +// "type": "array", +// "items": { +// "type": "string" +// }, +// "description": "A list of hobbies" +// } +// }, +// "required": ["person", "hobbies"] +// } + +// const exampleSchema3 = { +// "type": "object", +// "properties": { +// "name": { +// "type": "string", +// "required": true, +// "description": "The name of the person" +// }, +// "age": { +// "type": "number", +// "min": 0, +// "max": 150, +// "description": "The age of the person" +// } +// }, +// "required": ["name", "age"] +// } + +// function jsonSchemaObjectToCustomXmlSchema(schema: object) { + +// } + +// const a = ` +// This is a name +// +// + +// +// +// ` + +// const b = ` + +// ` + +// const EXAMPLE_TOOL2 = ` +// example_tool +// +// +// First argument +// string +// true +// +// +// Second argument +// number +// false +// +// +// `; diff --git a/core/tools/instructionTools/buildXmlToolsSystemMessage.ts b/core/tools/instructionTools/buildXmlToolsSystemMessage.ts new file mode 100644 index 0000000000..a37112757f --- /dev/null +++ b/core/tools/instructionTools/buildXmlToolsSystemMessage.ts @@ -0,0 +1,142 @@ +import { XMLBuilder } from "fast-xml-parser"; +import { Tool } from "../.."; +import { closeTag } from "./xmlToolsUtils"; + +export const TOOL_INSTRUCTIONS_TAG = ""; +export const TOOL_DEFINITION_TAG = ""; +export const TOOL_DESCRIPTION_TAG = ""; +export const TOOL_CALL_TAG = ""; +export const TOOL_NAME_TAG = ""; +export const TOOL_ARGS_TAG = ""; + +const EXAMPLE_DYNAMIC_TOOL = ` +${TOOL_DEFINITION_TAG} + ${TOOL_NAME_TAG}example_tool${closeTag(TOOL_NAME_TAG)} + ${TOOL_ARGS_TAG} + + First argument + string + true + + + Second argument + number + false + + ${closeTag(TOOL_ARGS_TAG)} +${closeTag(TOOL_DEFINITION_TAG)}`.trim(); + +const EXAMPLE_TOOL_CALL = ` +${TOOL_CALL_TAG} + ${TOOL_NAME_TAG}example_tool${closeTag(TOOL_NAME_TAG)} + ${TOOL_ARGS_TAG} + value1 + ${closeTag(TOOL_ARGS_TAG)} +${closeTag(TOOL_CALL_TAG)} +`.trim(); + +function toolToXmlDefinition(tool: Tool): string { + const builder = new XMLBuilder({ + ignoreAttributes: true, + format: true, + suppressEmptyNode: true, + }); + + const toolDefinition: { + name: string; + description?: string; + args?: Record; + } = { + name: tool.function.name, + }; + + if (tool.function.description) { + toolDefinition.description = tool.function.description; + } + + if (tool.function.parameters && "properties" in tool.function.parameters) { + toolDefinition.args = {}; + for (const [key, value] of Object.entries( + tool.function.parameters.properties, + )) { + toolDefinition.args[key] = value; + } + } + + return builder + .build({ + tool_definition: toolDefinition, + }) + .trim(); +} + +export const generateToolsSystemMessage = (tools: Tool[]) => { + if (tools.length === 0) { + return undefined; + } + const withPredefinedMessage = tools.filter( + (tool) => !!tool.systemMessageDescription, + ); + + const withDynamicMessage = tools.filter( + (tool) => !tool.systemMessageDescription, + ); + + let prompt = TOOL_INSTRUCTIONS_TAG; + prompt += `You have access to several "tools" that you can use at any time to perform tasks for the User and interact with the IDE.`; + prompt += `\nTo use a tool, respond with a ${TOOL_CALL_TAG}, specifying ${TOOL_NAME_TAG} and ${TOOL_ARGS_TAG} as shown in the provided examples below.`; + + if (withPredefinedMessage.length > 0) { + prompt += `\n\nThe following tools are available to you:`; + for (const tool of withPredefinedMessage) { + prompt += "\n\n"; + prompt += tool.systemMessageDescription; + } + } + + if (withDynamicMessage.length > 0) { + prompt += `Also, these additional tool definitions show other tools you can call with the same syntax:`; + + for (const tool of tools) { + prompt += "\n\n"; + if (tool.systemMessageDescription) { + prompt += tool.systemMessageDescription; + } else { + prompt += toolToXmlDefinition(tool); + } + } + + prompt += `For example, this tool definition:\n\n`; + + prompt += EXAMPLE_DYNAMIC_TOOL; + + prompt += "\n\nCan be called like this:\n"; + + prompt += EXAMPLE_TOOL_CALL; + } + + prompt += `\n\nIf it seems like the User's request could be solved with one of the tools, choose the BEST one for the job based on the user's request and the tool's description.`; + prompt += `\nDo NOT use codeblocks for tool calls. You can only call one tool at a time.`; + prompt += `\nYou are the one who sends the tool call, not the user. You must respond with the to use a tool.`; + + prompt += `\n${closeTag(TOOL_INSTRUCTIONS_TAG)}`; + + return prompt; +}; + +export function createSystemMessageExampleCall( + name: string, + instructions: string, + argsExample: string = "", +) { + return `${instructions}\n${TOOL_CALL_TAG} + ${TOOL_NAME_TAG}${name}${closeTag(TOOL_NAME_TAG)}${ + !!argsExample + ? `\n ${TOOL_ARGS_TAG} + ${argsExample} + ${closeTag(TOOL_ARGS_TAG)} +`.trim() + : "" + } +${closeTag(TOOL_CALL_TAG)}`; +} diff --git a/core/tools/instructionTools/interceptXmlToolCalls.ts b/core/tools/instructionTools/interceptXmlToolCalls.ts new file mode 100644 index 0000000000..4565b68514 --- /dev/null +++ b/core/tools/instructionTools/interceptXmlToolCalls.ts @@ -0,0 +1,120 @@ +import { ChatMessage, ToolCallDelta } from "../.."; +import { renderChatMessage } from "../../util/messageContent"; +import { generateOpenAIToolCallId } from "./openAIToolCallId"; +import { parsePartialXml } from "./parsePartialXmlToolCall"; +import { getStringDelta, splitAtTagBoundaries } from "./xmlToolsUtils"; + +/* + Function to intercept tool calls in XML format from a chat message stream + 1. Skips non-assistant messages + 2. Skips xml that doesn't have root "tool_call" tag + 3. Intercepts text that contains a partial tag at the beginning, e.g. "), performs partial XML parsing + 5. Successful partial parsing yields JSON tool call delta with previous partial parses removed + 6. Failed partial parsing just adds to buffer and continues + 7. Closes when closed tag is found +*/ +export async function* interceptXMLToolCalls( + messageGenerator: AsyncGenerator, +): AsyncGenerator { + let toolCallText = ""; + let currentToolCallId: string | undefined = undefined; + let currentToolCallArgs: string = ""; + let inToolCall = false; + + let buffer = ""; + + for await (const batch of messageGenerator) { + for await (const message of batch) { + // Skip non-assistant messages or messages with native tool calls + if (message.role !== "assistant" || message.toolCalls) { + yield [message]; + continue; + } + + const content = renderChatMessage(message); + const splitContent = splitAtTagBoundaries(content); // split at tag starts/ends e.g. < > + + for (const chunk of splitContent) { + buffer += chunk; + if (!inToolCall) { + // Check for entry into tool call + if (buffer.startsWith("")) { + inToolCall = true; + } else if ("".startsWith(buffer)) { + // We have a partial start tag, continue + continue; + } + } + + if (inToolCall) { + if (!currentToolCallId) { + currentToolCallId = generateOpenAIToolCallId(); + } + + toolCallText += buffer; + + // Handle tool call + const parsed = parsePartialXml(toolCallText); + + if (parsed?.tool_call) { + const name = parsed.tool_call.name; + + if (!name) { + // Prevent dispatching with empty name + buffer = ""; + continue; + } + + const args = parsed.tool_call.args + ? JSON.stringify(parsed.tool_call.args) + : ""; + + const argsDelta = getStringDelta(currentToolCallArgs, args); + + const toolCallDelta: ToolCallDelta = { + id: currentToolCallId, + type: "function", + function: { + name: name, + arguments: argsDelta, + }, + }; + + currentToolCallArgs = args; + console.log("Tool call delta:", toolCallDelta); + yield [ + { + ...message, + content: "", + toolCalls: [toolCallDelta], + }, + ]; + } else { + console.warn( + "Partial parsing failed, continuing to accumulate tool call:\n", + toolCallText, + ); + } + + // Check for exit from tool call + if (toolCallText.endsWith("")) { + inToolCall = false; + toolCallText = ""; + currentToolCallId = undefined; + currentToolCallArgs = ""; + } + } else { + // Yield normal assistant message + yield [ + { + ...message, + content: buffer, + }, + ]; + } + buffer = ""; + } + } + } +} diff --git a/core/tools/instructionTools/openAIToolCallId.ts b/core/tools/instructionTools/openAIToolCallId.ts new file mode 100644 index 0000000000..f0f609f7e0 --- /dev/null +++ b/core/tools/instructionTools/openAIToolCallId.ts @@ -0,0 +1,13 @@ +function randomLettersAndNumbers(length: number): string { + const characters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + +export function generateOpenAIToolCallId(): string { + return `call_${randomLettersAndNumbers(24)}`; +} diff --git a/core/tools/instructionTools/parsePartialXmlToolCall.test.ts b/core/tools/instructionTools/parsePartialXmlToolCall.test.ts new file mode 100644 index 0000000000..4e5435b498 --- /dev/null +++ b/core/tools/instructionTools/parsePartialXmlToolCall.test.ts @@ -0,0 +1,87 @@ +import { parsePartialXml } from "./parsePartialXmlToolCall"; + +describe("PartialXMLParser", () => { + test("parses simple XML tag with content", () => { + const result = parsePartialXml("John"); + expect(result).toEqual({ person: "John" }); + }); + + test("parses nested XML tags", () => { + const result = parsePartialXml( + "John30", + ); + expect(result).toEqual({ + person: { + name: "John", + age: "30", + }, + }); + }); + + test("handles partial XML input - unclosed tag", () => { + const result = parsePartialXml("John"); + expect(result).toEqual({ + person: { + name: "John", + }, + }); + }); + + test("handles partial XML input - incomplete tag name", () => { + const result = parsePartialXml("John { + const result = parsePartialXml("John { + const result = parsePartialXml("Just some text"); + expect(result).toBeNull(); + }); + + test("handles whitespace correctly", () => { + const result = parsePartialXml( + "\n John \n", + ); + expect(result).toEqual({ + person: { + name: "John", + }, + }); + }); + + test("handles empty input ending with opening bracket", () => { + const result = parsePartialXml("<"); + expect(result).toEqual({}); + }); + + test("handles empty input ending with partial tag", () => { + const result = parsePartialXml(" { + const chunk1 = parsePartialXml("John"); + expect(chunk2).toEqual({ + person: { + name: "John", + }, + }); + }); +}); diff --git a/core/tools/instructionTools/parsePartialXmlToolCall.ts b/core/tools/instructionTools/parsePartialXmlToolCall.ts new file mode 100644 index 0000000000..cc48719b56 --- /dev/null +++ b/core/tools/instructionTools/parsePartialXmlToolCall.ts @@ -0,0 +1,135 @@ +export function parsePartialXml(input: string): Record | null { + let stack: { tag: string; content: any }[] = []; + let currentTag = ""; + let currentContent = ""; + let isInTag = false; + let isClosingTag = false; + let lastCompletedNestedTag = null; + let lastCompletedContent = null; + + // Return null for non-XML content + if (!input.includes("<")) { + return null; + } + + // If it's just starting a tag at the end, return empty + if (input.trim().endsWith("<")) { + return {}; + } + + for (let i = 0; i < input.length; i++) { + const char = input[i]; + + if (char === "<") { + isInTag = true; + isClosingTag = input[i + 1] === "/"; + + // Save any content we've accumulated + if (currentContent.trim() && stack.length > 0) { + stack[stack.length - 1].content = currentContent.trim(); + } + + currentTag = ""; + currentContent = ""; + continue; + } + + if (char === ">" || (isClosingTag && i === input.length - 1)) { + isInTag = false; + + if (isClosingTag) { + if (stack.length > 0) { + const closed = stack.pop()!; + const tagName = closed.tag; + lastCompletedNestedTag = tagName; + lastCompletedContent = closed.content; + + if (stack.length > 0) { + const parent = stack[stack.length - 1]; + if (typeof parent.content !== "object") { + parent.content = {}; + } + parent.content[tagName] = closed.content; + } else { + return { [tagName]: closed.content }; + } + } + } else if (currentTag) { + stack.push({ tag: currentTag, content: {} }); + } + + continue; + } + + if (isInTag) { + if (!isClosingTag && char !== "/") { + currentTag += char; + } + } else { + currentContent += char; + } + } + + // Handle partial XML + if (stack.length > 0) { + // Handle any remaining content + if (currentContent.trim()) { + const current = stack[stack.length - 1]; + if (stack.length > 1) { + lastCompletedNestedTag = current.tag; + lastCompletedContent = currentContent.trim(); + } else { + current.content = currentContent.trim(); + } + } + + // Create the result structure + let result: Record = {}; + let current = stack[0]; + + // Special case for single tag with direct content + if (stack.length === 1 && typeof current.content === "string") { + return { [current.tag]: current.content }; + } + + result[current.tag] = {}; + let currentObj = result[current.tag]; + + // If we have a completed nested tag, make sure it's included + if (lastCompletedNestedTag && lastCompletedContent) { + currentObj[lastCompletedNestedTag] = lastCompletedContent; + } + + return result; + } + + return {}; +} + +// export const getXmlToolCallsFromContent = ( +// content: string, +// existingToolCalls: ToolCallDelta[] = [], +// ): ToolCall[] => { +// const toolCallRegex = /([\s\S]*?)<\/tool_call>/g; +// const matches = [...content.matchAll(toolCallRegex)].map((match) => +// match[0].trim(), +// ); + +// const parser = new XMLParser({ +// ignoreAttributes: true, +// parseTagValue: true, +// trimValues: true, +// }); + +// return matches.map((match, index) => { +// const parsed = parser.parse(match); +// return { +// id: existingToolCalls[index]?.id ?? `tool-call-${index}`, +// type: "function", +// function: { +// name: parsed["tool_call"]?.name, +// arguments: JSON.stringify(parsed["tool_call"]?.args), +// }, +// }; +// }); +// }; diff --git a/core/tools/instructionTools/xmlToolsUtils.test.ts b/core/tools/instructionTools/xmlToolsUtils.test.ts new file mode 100644 index 0000000000..f329354b5d --- /dev/null +++ b/core/tools/instructionTools/xmlToolsUtils.test.ts @@ -0,0 +1,66 @@ +import { closeTag, splitAtTagBoundaries } from "./xmlToolsUtils"; + +describe("splitAtTagBoundaries", () => { + test("doesn't split plain text without tags", () => { + expect(splitAtTagBoundaries("hello world")).toEqual(["hello world"]); + }); + + test("splits single complete tag", () => { + expect(splitAtTagBoundaries("")).toEqual([""]); + }); + + test("splits text with complete tag", () => { + expect(splitAtTagBoundaries("beforeafter")).toEqual([ + "before", + "", + "after", + ]); + }); + + test("splits multiple complete tags", () => { + expect(splitAtTagBoundaries("middle")).toEqual([ + "", + "middle", + "", + ]); + }); + + test("handles partial opening tag", () => { + expect(splitAtTagBoundaries("text { + expect(splitAtTagBoundaries("text>")).toEqual(["text>"]); + }); + + test("handles empty string", () => { + expect(splitAtTagBoundaries("")).toEqual([""]); + }); + + test("handles consecutive tag boundaries", () => { + expect(splitAtTagBoundaries("<><>")).toEqual(["<>", "<>"]); + }); +}); + +describe("closeTag", () => { + describe("when given an opening tag", () => { + const testCases = [ + { + input: "
", + expectedOutput: "
", + }, + { + input: "

", + expectedOutput: "

", + }, + // Add more test cases as needed + ]; + + testCases.forEach((testCase) => { + it(`closes tag "${testCase.input}" to " { + const result = closeTag(testCase.input); + expect(result).toEqual(testCase.expectedOutput); + }); + }); + }); +}); diff --git a/core/tools/instructionTools/xmlToolsUtils.ts b/core/tools/instructionTools/xmlToolsUtils.ts new file mode 100644 index 0000000000..80ed315a9e --- /dev/null +++ b/core/tools/instructionTools/xmlToolsUtils.ts @@ -0,0 +1,28 @@ +export function getStringDelta(original: string, updated: string): string { + if (!updated.startsWith(original)) { + console.warn( + `Original string "${original}" is not a prefix of updated string "${updated}"`, + ); + return updated; + } + return updated.slice(original.length); +} + +export function splitAtTagBoundaries(content: string) { + if (!content) return [""]; + + // Add spaces after > and before < to help with splitting + const BOUNDARY = "XML_PARSING_BOUNDARY_9b1deb4d3b7d"; // not that important, just something unique + const spaced = content + .replace(/>/g, `>${BOUNDARY}`) + .replace(/ { }).timeout(DEFAULT_TIMEOUT.MD * 100); it("should call tool after approval", async () => { - await GUIActions.toggleToolPolicy(view, "builtin_view_diff", 2); + await GUIActions.toggleToolPolicy(view, "view_diff", 2); const [messageInput] = await GUISelectors.getMessageInputFields(view); await messageInput.sendKeys("Hello"); @@ -325,7 +325,7 @@ describe("GUI Test", () => { }).timeout(DEFAULT_TIMEOUT.XL); it("should cancel tool", async () => { - await GUIActions.toggleToolPolicy(view, "builtin_view_diff", 2); + await GUIActions.toggleToolPolicy(view, "view_diff", 2); const [messageInput] = await GUISelectors.getMessageInputFields(view); await messageInput.sendKeys("Hello"); diff --git a/gui/package-lock.json b/gui/package-lock.json index 17f6b15148..0d3e3a9c48 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -29,6 +29,7 @@ "core": "file:../core", "dompurify": "^3.0.6", "downshift": "^7.6.0", + "fast-xml-parser": "^5.2.3", "lodash": "^4.17.21", "lowlight": "^3.3.0", "minisearch": "^7.0.2", @@ -5775,6 +5776,24 @@ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" }, + "node_modules/fast-xml-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.3.tgz", + "integrity": "sha512-OdCYfRqfpuLUFonTNjvd30rCBZUneHpSQkCqfaeWQ9qrKcl6XlWeDBNVwGb+INAIxRshuN2jF+BE0L6gbBO2mw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -11506,6 +11525,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/strtok3": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.1.1.tgz", diff --git a/gui/package.json b/gui/package.json index de6da19591..e568eec07b 100644 --- a/gui/package.json +++ b/gui/package.json @@ -38,6 +38,7 @@ "core": "file:../core", "dompurify": "^3.0.6", "downshift": "^7.6.0", + "fast-xml-parser": "^5.2.3", "lodash": "^4.17.21", "lowlight": "^3.3.0", "minisearch": "^7.0.2", diff --git a/gui/src/components/ModeSelect/ModeSelect.tsx b/gui/src/components/ModeSelect/ModeSelect.tsx index 22a9975ca4..c9f64f722f 100644 --- a/gui/src/components/ModeSelect/ModeSelect.tsx +++ b/gui/src/components/ModeSelect/ModeSelect.tsx @@ -1,11 +1,17 @@ -import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +// A dropdown menu for selecting between Chat, Edit, and Agent modes with keyboard shortcuts +import { + CheckIcon, + ChevronDownIcon, + ExclamationCircleIcon, +} from "@heroicons/react/24/outline"; import { MessageModes } from "core"; -import { modelSupportsTools } from "core/llm/autodetect"; +import { modelIsGreatWithNativeTools } from "core/llm/toolSupport"; import { useCallback, useEffect, useMemo } from "react"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { selectSelectedChatModel } from "../../redux/slices/configSlice"; import { setMode } from "../../redux/slices/sessionSlice"; import { getFontSize, getMetaKeyLabel } from "../../util"; +import { ToolTip } from "../gui/Tooltip"; import { useMainEditor } from "../mainInput/TipTapEditor"; import { Listbox, @@ -19,24 +25,18 @@ export function ModeSelect() { const dispatch = useAppDispatch(); const mode = useAppSelector((store) => store.session.mode); const selectedModel = useAppSelector(selectSelectedChatModel); - const agentModeSupported = useMemo(() => { - return selectedModel && modelSupportsTools(selectedModel); + const isGoodInAgentMode = useMemo(() => { + if (!selectedModel) { + return true; // not need to show warning if no model is selected + } + return modelIsGreatWithNativeTools(selectedModel); }, [selectedModel]); + const { mainEditor } = useMainEditor(); const metaKeyLabel = useMemo(() => { return getMetaKeyLabel(); }, []); - // Switch to chat mode if agent mode is selected but not supported - useEffect(() => { - if (!selectedModel) { - return; - } - if (mode === "agent" && !agentModeSupported) { - dispatch(setMode("chat")); - } - }, [mode, agentModeSupported, dispatch, selectedModel]); - const cycleMode = useCallback(() => { dispatch(setMode(mode === "chat" ? "agent" : "chat")); mainEditor?.commands.focus(); @@ -97,19 +97,45 @@ export function ModeSelect() { {mode === "chat" && } - +
Agent
- {agentModeSupported ? ( - mode === "agent" && + {!isGoodInAgentMode && ( + <> + + + Agent might not work well with this model.{" "} + {/* can't seem to make link in tooltip clickable. globalCloseEvents or closeEvents? */} + {/* { + ideMessenger.post( + "openUrl", + "https://docs.continue.dev/agent/model-setup", + ); + }} + className="text-link cursor-pointer" + > + See docs + */} + + + )} + {mode === "agent" ? ( + ) : ( - (Not supported) +
)}
diff --git a/gui/src/components/mainInput/InputToolbar.tsx b/gui/src/components/mainInput/InputToolbar.tsx index fe3cd83f0b..7ba7083ae7 100644 --- a/gui/src/components/mainInput/InputToolbar.tsx +++ b/gui/src/components/mainInput/InputToolbar.tsx @@ -1,6 +1,6 @@ import { AtSymbolIcon, PhotoIcon } from "@heroicons/react/24/outline"; import { InputModifiers } from "core"; -import { modelSupportsImages, modelSupportsTools } from "core/llm/autodetect"; +import { modelSupportsImages } from "core/llm/autodetect"; import { useContext, useRef } from "react"; import { IdeMessengerContext } from "../../context/IdeMessenger"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; @@ -63,8 +63,6 @@ function InputToolbar(props: InputToolbarProps) { (currentToolCallApplyState && currentToolCallApplyState.status !== "closed"); - const toolsSupported = defaultModel && modelSupportsTools(defaultModel); - const supportsImages = defaultModel && modelSupportsImages( @@ -158,7 +156,7 @@ function InputToolbar(props: InputToolbarProps) { > {!props.toolbarOptions?.hideUseCodebase && !isInEdit && (