From 8bc149ccd80214dcce882267c183e25da0187ac2 Mon Sep 17 00:00:00 2001 From: yetone Date: Sat, 26 Jul 2025 16:06:56 +0800 Subject: [PATCH] fix: react prompts (#2537) --- crates/avante-templates/src/lib.rs | 2 + lua/avante/libs/ReAct_parser.lua | 11 - lua/avante/libs/ReAct_parser2.lua | 261 ++++++++++++++++++++ lua/avante/llm.lua | 23 +- lua/avante/llm_tools/edit_file.lua | 4 + lua/avante/llm_tools/init.lua | 2 +- lua/avante/llm_tools/update_todo_status.lua | 2 +- lua/avante/llm_tools/write_to_file.lua | 2 + lua/avante/providers/gemini.lua | 16 +- lua/avante/providers/openai.lua | 34 +-- lua/avante/sidebar.lua | 6 +- lua/avante/templates/agentic.avanterules | 76 ++++++ lua/avante/utils/init.lua | 10 +- lua/avante/utils/prompts.lua | 172 +++++-------- 14 files changed, 448 insertions(+), 173 deletions(-) create mode 100644 lua/avante/libs/ReAct_parser2.lua diff --git a/crates/avante-templates/src/lib.rs b/crates/avante-templates/src/lib.rs index ecf7beb..c77879d 100644 --- a/crates/avante-templates/src/lib.rs +++ b/crates/avante-templates/src/lib.rs @@ -44,6 +44,7 @@ struct TemplateContext { model_name: Option, memory: Option, todos: Option, + enable_fastapply: Option, } // Given the file name registered after add, the context table in Lua, resulted in a formatted @@ -72,6 +73,7 @@ fn render(state: &State, template: &str, context: TemplateContext) -> LuaResult< model_name => context.model_name, memory => context.memory, todos => context.todos, + enable_fastapply => context.enable_fastapply, }) .map_err(LuaError::external) .unwrap()) diff --git a/lua/avante/libs/ReAct_parser.lua b/lua/avante/libs/ReAct_parser.lua index a8316c8..aebda7d 100644 --- a/lua/avante/libs/ReAct_parser.lua +++ b/lua/avante/libs/ReAct_parser.lua @@ -1,14 +1,3 @@ ----@class avante.TextContent ----@field type "text" ----@field text string ----@field partial boolean ---- ----@class avante.ToolUseContent ----@field type "tool_use" ----@field tool_name string ----@field tool_input table ----@field partial boolean - local M = {} -- Helper function to parse a parameter tag like value diff --git a/lua/avante/libs/ReAct_parser2.lua b/lua/avante/libs/ReAct_parser2.lua new file mode 100644 index 0000000..00c7dc6 --- /dev/null +++ b/lua/avante/libs/ReAct_parser2.lua @@ -0,0 +1,261 @@ +local JsonParser = require("avante.libs.jsonparser") + +---@class avante.TextContent +---@field type "text" +---@field text string +---@field partial boolean +--- +---@class avante.ToolUseContent +---@field type "tool_use" +---@field tool_name string +---@field tool_input table +---@field partial boolean + +local M = {} + +--- Parse the text into a list of TextContent and ToolUseContent +--- The text is a string. +--- For example: +--- parse([[Hello, world!]]) +--- returns +--- { +--- { +--- type = "text", +--- text = "Hello, world!", +--- partial = false, +--- }, +--- } +--- +--- parse([[Hello, world! I am a tool.{"name": "write", "input": {"path": "path/to/file.txt", "content": "foo"}}]]) +--- returns +--- { +--- { +--- type = "text", +--- text = "Hello, world! I am a tool.", +--- partial = false, +--- }, +--- { +--- type = "tool_use", +--- tool_name = "write", +--- tool_input = { +--- path = "path/to/file.txt", +--- content = "foo", +--- }, +--- partial = false, +--- }, +--- } +--- +--- parse([[Hello, world! I am a tool.{"name": "write", "input": {"path": "path/to/file.txt", "content": "foo"}}I am another tool.{"name": "write", "input": {"path": "path/to/file.txt", "content": "bar"}}hello]]) +--- returns +--- { +--- { +--- type = "text", +--- text = "Hello, world! I am a tool.", +--- partial = false, +--- }, +--- { +--- type = "tool_use", +--- tool_name = "write", +--- tool_input = { +--- path = "path/to/file.txt", +--- content = "foo", +--- }, +--- partial = false, +--- }, +--- { +--- type = "text", +--- text = "I am another tool.", +--- partial = false, +--- }, +--- { +--- type = "tool_use", +--- tool_name = "write", +--- tool_input = { +--- path = "path/to/file.txt", +--- content = "bar", +--- }, +--- partial = false, +--- }, +--- { +--- type = "text", +--- text = "hello", +--- partial = false, +--- }, +--- } +--- +--- parse([[Hello, world! I am a tool.{"name"]]) +--- returns +--- { +--- { +--- type = "text", +--- text = "Hello, world! I am a tool.", +--- partial = false, +--- } +--- } +--- +--- parse([[Hello, world! I am a tool.{"name": "write"]]) +--- returns +--- { +--- { +--- type = "text", +--- text = "Hello, world! I am a tool.", +--- partial = false, +--- }, +--- { +--- type = "tool_use", +--- tool_name = "write", +--- tool_input = {}, +--- partial = true, +--- }, +--- } +--- +--- parse([[Hello, world! I am a tool.{"name": "write", "input": {"path": "path/to/file.txt"]]) +--- returns +--- { +--- { +--- type = "text", +--- text = "Hello, world! I am a tool.", +--- partial = false, +--- }, +--- { +--- type = "tool_use", +--- tool_name = "write", +--- tool_input = { +--- path = "path/to/file.txt", +--- }, +--- partial = true, +--- }, +--- } +--- +--- parse([[Hello, world! I am a tool.{"name": "write", "input": {"path": "path/to/file.txt", "content": "foo bar]]) +--- returns +--- { +--- { +--- type = "text", +--- text = "Hello, world! I am a tool.", +--- partial = false, +--- }, +--- { +--- type = "tool_use", +--- tool_name = "write", +--- tool_input = { +--- path = "path/to/file.txt", +--- content = "foo bar", +--- }, +--- partial = true, +--- }, +--- } +--- +--- parse([[Hello, world! I am a tool.{"name": "write", "input": {"path": "path/to/file.txt", "content": foo bar]]) +--- returns +--- { +--- { +--- type = "text", +--- text = [[Hello, world! I am a tool.{"name": "write", "input": {"path": "path/to/file.txt", "content": foo bar]], +--- partial = false, +--- } +--- } +--- +---@param text string +---@return (avante.TextContent|avante.ToolUseContent)[] +function M.parse(text) + local result = {} + local pos = 1 + local len = #text + + while pos <= len do + local tool_start = text:find("", pos, true) + + if not tool_start then + -- No more tool_use tags, add remaining text if any + if pos <= len then + local remaining_text = text:sub(pos) + if remaining_text ~= "" then + table.insert(result, { + type = "text", + text = remaining_text, + partial = false, + }) + end + end + break + end + + -- Add text before tool_use tag if any + if tool_start > pos then + local text_content = text:sub(pos, tool_start - 1) + if text_content ~= "" then + table.insert(result, { + type = "text", + text = text_content, + partial = false, + }) + end + end + + -- Find the closing tag + local json_start = tool_start + 10 -- length of "" + local tool_end = text:find("", json_start, true) + + if not tool_end then + -- No closing tag found, treat as partial tool_use + local json_text = text:sub(json_start) + + json_text = json_text:gsub("^\n+", "") + json_text = json_text:gsub("\n+$", "") + json_text = json_text:gsub("^%s+", "") + json_text = json_text:gsub("%s+$", "") + + -- Try to parse complete JSON first + local success, json_data = pcall(function() return vim.json.decode(json_text) end) + + if success and json_data and json_data.name then + table.insert(result, { + type = "tool_use", + tool_name = json_data.name, + tool_input = json_data.input or {}, + partial = true, + }) + else + local jsn = JsonParser.parse(json_text) + + if jsn and jsn.name then + table.insert(result, { + type = "tool_use", + tool_name = jsn.name, + tool_input = jsn.input or {}, + partial = true, + }) + end + end + break + end + + -- Extract JSON content + local json_text = text:sub(json_start, tool_end - 1) + local success, json_data = pcall(function() return vim.json.decode(json_text) end) + + if success and json_data and json_data.name then + table.insert(result, { + type = "tool_use", + tool_name = json_data.name, + tool_input = json_data.input or {}, + partial = false, + }) + pos = tool_end + 11 -- length of "" + else + -- Invalid JSON, treat the whole thing as text + local invalid_text = text:sub(tool_start, tool_end + 10) + table.insert(result, { + type = "text", + text = invalid_text, + partial = false, + }) + pos = tool_end + 11 + end + end + + return result +end + +return M diff --git a/lua/avante/llm.lua b/lua/avante/llm.lua index a2c408d..c555d51 100644 --- a/lua/avante/llm.lua +++ b/lua/avante/llm.lua @@ -307,6 +307,7 @@ function M.generate_prompts(opts) system_info = system_info, model_name = provider.model or "unknown", memory = opts.memory, + enable_fastapply = Config.behaviour.enable_fastapply, } -- Removed the original todos processing logic, now handled in context_messages @@ -804,7 +805,7 @@ function M._stream(opts) return end end - M._stream(new_opts) + if not streaming_tool_use then M._stream(new_opts) end return end local partial_tool_use = tool_uses[tool_use_index] @@ -849,15 +850,14 @@ function M._stream(opts) streaming = partial_tool_use.state == "generating", on_complete = function() end, } - if partial_tool_use.state == "generating" and not is_edit_tool_use and not support_streaming then return end if partial_tool_use.state == "generating" then + if not is_edit_tool_use and not support_streaming then return end if type(partial_tool_use.input) == "table" then LLMTools.process_tool_use(prompt_opts.tools, partial_tool_use, tool_use_opts) end return - else - if streaming_tool_use then return end end + if streaming_tool_use then return end partial_tool_use_message.is_calling = true if opts.on_messages_add then opts.on_messages_add({ partial_tool_use_message }) end -- Either on_complete handles the tool result asynchronously or we receive the result and error synchronously when either is not nil @@ -887,12 +887,11 @@ function M._stream(opts) local completed_attempt_completion_tool_use = nil for idx = #history_messages, 1, -1 do local message = history_messages[idx] - if not message.is_user_submission then - local use = History.Helpers.get_tool_use_data(message) - if use and use.name == "attempt_completion" then - completed_attempt_completion_tool_use = message - break - end + if message.is_user_submission then break end + local use = History.Helpers.get_tool_use_data(message) + if use and use.name == "attempt_completion" then + completed_attempt_completion_tool_use = message + break end end local unfinished_todos = {} @@ -915,7 +914,7 @@ function M._stream(opts) if #unfinished_todos > 0 then message = History.Message:new( "user", - "You should use tool calls to answer the question, for example, use update_todo_status if the task step is done or cancelled.", + "You should use tool calls to answer the question, for example, use update_todo_status if the task step is done or cancelled.", { visible = false, } @@ -923,7 +922,7 @@ function M._stream(opts) else message = History.Message:new( "user", - "You should use tool calls to answer the question, for example, use attempt_completion if the job is done.", + "You should use tool calls to answer the question, for example, use attempt_completion if the job is done.", { visible = false, } diff --git a/lua/avante/llm_tools/edit_file.lua b/lua/avante/llm_tools/edit_file.lua index 728a25e..3e34b2a 100644 --- a/lua/avante/llm_tools/edit_file.lua +++ b/lua/avante/llm_tools/edit_file.lua @@ -53,6 +53,10 @@ M.returns = { ---@type AvanteLLMToolFunc<{ path: string, instructions: string, code_edit: string }> M.func = vim.schedule_wrap(function(input, opts) + if opts.streaming then return false, "streaming not supported" end + if not input.path then return false, "path not provided" end + if not input.instructions then input.instructions = "" end + if not input.code_edit then return false, "code_edit not provided" end local on_complete = opts.on_complete if not on_complete then return false, "on_complete not provided" end local provider = Providers["morph"] diff --git a/lua/avante/llm_tools/init.lua b/lua/avante/llm_tools/init.lua index 12f11d0..872908e 100644 --- a/lua/avante/llm_tools/init.lua +++ b/lua/avante/llm_tools/init.lua @@ -1274,7 +1274,7 @@ function M.process_tool_use(tools, tool_use, opts) else ---@type AvanteLLMTool? local tool = vim.iter(tools):find(function(tool) return tool.name == tool_use.name end) ---@param tool AvanteLLMTool - if tool == nil then return nil, "This tool is not provided: " .. tool_use.name end + if tool == nil then return nil, "This tool is not provided: " .. vim.inspect(tool_use.name) end func = tool.func or M[tool.name] end local input_json = tool_use.input diff --git a/lua/avante/llm_tools/update_todo_status.lua b/lua/avante/llm_tools/update_todo_status.lua index 84dc61f..6f3e39b 100644 --- a/lua/avante/llm_tools/update_todo_status.lua +++ b/lua/avante/llm_tools/update_todo_status.lua @@ -50,7 +50,7 @@ function M.func(input, opts) local todos = sidebar.chat_history.todos if not todos or #todos == 0 then return false, "No todos found" end for _, todo in ipairs(todos) do - if todo.id == input.id then + if tostring(todo.id) == tostring(input.id) then todo.status = input.status break end diff --git a/lua/avante/llm_tools/write_to_file.lua b/lua/avante/llm_tools/write_to_file.lua index 62e57e4..09d59cd 100644 --- a/lua/avante/llm_tools/write_to_file.lua +++ b/lua/avante/llm_tools/write_to_file.lua @@ -10,6 +10,8 @@ M.name = "write_to_file" M.description = "Request to write content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file." +M.support_streaming = false + function M.enabled() return require("avante.config").mode == "agentic" and not require("avante.config").behaviour.enable_fastapply end diff --git a/lua/avante/providers/gemini.lua b/lua/avante/providers/gemini.lua index de195ac..5f30b1e 100644 --- a/lua/avante/providers/gemini.lua +++ b/lua/avante/providers/gemini.lua @@ -113,29 +113,25 @@ function M:parse_messages(opts) end if not provider_conf.disable_tools and use_ReAct_prompt then if content_items[1].type == "tool_result" then - local tool_use = nil + local tool_use_msg = nil for _, msg_ in ipairs(opts.messages) do if type(msg_.content) == "table" and #msg_.content > 0 then if msg_.content[1].type == "tool_use" and msg_.content[1].id == content_items[1].tool_use_id then - tool_use = msg_ + tool_use_msg = msg_ break end end end - if tool_use then + if tool_use_msg then table.insert(contents, { role = "model", parts = { - { text = Utils.tool_use_to_xml(tool_use.content[1]) }, + { text = Utils.tool_use_to_xml(tool_use_msg.content[1]) }, }, }) role = "user" table.insert(parts, { - text = "[" - .. tool_use.content[1].name - .. " for '" - .. (tool_use.content[1].input.path or tool_use.content[1].input.rel_path or "") - .. "'] Result:", + text = "The result of tool use " .. Utils.tool_use_to_xml(tool_use_msg.content[1]) .. " is:\n", }) table.insert(parts, { text = content_items[1].content, @@ -189,6 +185,8 @@ function M.prepare_request_body(provider_instance, prompt_opts, provider_conf, r local use_ReAct_prompt = provider_conf.use_ReAct_prompt == true + if use_ReAct_prompt then request_body.generationConfig.stopSequences = { "" } end + local disable_tools = provider_conf.disable_tools or false if not use_ReAct_prompt and not disable_tools and prompt_opts.tools then diff --git a/lua/avante/providers/openai.lua b/lua/avante/providers/openai.lua index 5b4d183..8184d36 100644 --- a/lua/avante/providers/openai.lua +++ b/lua/avante/providers/openai.lua @@ -3,7 +3,7 @@ local Config = require("avante.config") local Clipboard = require("avante.clipboard") local Providers = require("avante.providers") local HistoryMessage = require("avante.history.message") -local ReActParser = require("avante.libs.ReAct_parser") +local ReActParser = require("avante.libs.ReAct_parser2") local JsonParser = require("avante.libs.jsonparser") local Prompts = require("avante.utils.prompts") local LlmTools = require("avante.llm_tools") @@ -130,24 +130,20 @@ function M:parse_messages(opts) end if not provider_conf.disable_tools and use_ReAct_prompt then if msg.content[1].type == "tool_result" then - local tool_use = nil + local tool_use_msg = nil for _, msg_ in ipairs(opts.messages) do if type(msg_.content) == "table" and #msg_.content > 0 then if msg_.content[1].type == "tool_use" and msg_.content[1].id == msg.content[1].tool_use_id then - tool_use = msg_ + tool_use_msg = msg_ break end end end - if tool_use then + if tool_use_msg then msg.role = "user" table.insert(content, { type = "text", - text = "[" - .. tool_use.content[1].name - .. " for '" - .. (tool_use.content[1].input.path or tool_use.content[1].input.rel_path or "") - .. "'] Result:", + text = "The result of tool use " .. Utils.tool_use_to_xml(tool_use_msg.content[1]) .. " is:\n", }) table.insert(content, { type = "text", @@ -258,7 +254,6 @@ function M:add_text_message(ctx, text, state, opts) end local cleaned_xml_content = table.concat(cleaned_xml_lines, "\n") local xml = ReActParser.parse(cleaned_xml_content) - local has_tool_use = false if xml and #xml > 0 then local new_content_list = {} local xml_md_openned = false @@ -293,42 +288,45 @@ function M:add_text_message(ctx, text, state, opts) end end if next(input) ~= nil then - has_tool_use = true local msg_uuid = ctx.content_uuid .. "-" .. idx local tool_use_id = msg_uuid + local tool_message_state = item.partial and "generating" or "generated" local msg_ = HistoryMessage:new("assistant", { type = "tool_use", name = item.tool_name, id = tool_use_id, input = input, }, { - state = state, + state = tool_message_state, uuid = msg_uuid, turn_id = ctx.turn_id, }) msgs[#msgs + 1] = msg_ ctx.tool_use_list = ctx.tool_use_list or {} + local input_json = type(input) == "string" and input or vim.json.encode(input) local exists = false for _, tool_use in ipairs(ctx.tool_use_list) do if tool_use.id == tool_use_id then - tool_use.input_json = input + tool_use.input_json = input_json exists = true end end if not exists then ctx.tool_use_list[#ctx.tool_use_list + 1] = { + uuid = tool_use_id, id = tool_use_id, name = item.tool_name, - input_json = input, + input_json = input_json, + state = "generating", } end + opts.on_stop({ reason = "tool_use", streaming_tool_use = item.partial }) end ::continue:: end - msg.message.content = table.concat(new_content_list, "\n") + msg.message.content = table.concat(new_content_list, "\n"):gsub("\n+$", "\n") end if opts.on_messages_add then opts.on_messages_add(msgs) end - if has_tool_use and state == "generating" then opts.on_stop({ reason = "tool_use", streaming_tool_use = true }) end end function M:add_thinking_message(ctx, text, state, opts) @@ -537,6 +535,9 @@ function M:parse_curl_args(prompt_opts) Utils.debug("endpoint", provider_conf.endpoint) Utils.debug("model", provider_conf.model) + local stop = nil + if use_ReAct_prompt then stop = { "" } end + return { url = Utils.url_join(provider_conf.endpoint, "/chat/completions"), proxy = provider_conf.proxy, @@ -545,6 +546,7 @@ function M:parse_curl_args(prompt_opts) body = vim.tbl_deep_extend("force", { model = provider_conf.model, messages = self:parse_messages(prompt_opts), + stop = stop, stream = true, stream_options = not M.is_mistral(provider_conf.endpoint) and { include_usage = true, diff --git a/lua/avante/sidebar.lua b/lua/avante/sidebar.lua index 4919ff7..6ec233a 100644 --- a/lua/avante/sidebar.lua +++ b/lua/avante/sidebar.lua @@ -3147,6 +3147,10 @@ function Sidebar:create_todos_container() local total_count = #history.todos local focused_idx = 1 local todos_content_lines = {} + if type(history.todos) ~= "table" then + Utils.debug("Invalid todos type", history.todos) + history.todos = {} + end for idx, todo in ipairs(history.todos) do local status_content = "[ ]" if todo.status == "done" then @@ -3163,7 +3167,7 @@ function Sidebar:create_todos_container() local todos_buf = api.nvim_win_get_buf(self.containers.todos.winid) Utils.unlock_buf(todos_buf) api.nvim_buf_set_lines(todos_buf, 0, -1, false, todos_content_lines) - api.nvim_win_set_cursor(self.containers.todos.winid, { focused_idx, 0 }) + pcall(function() api.nvim_win_set_cursor(self.containers.todos.winid, { focused_idx, 0 }) end) Utils.lock_buf(todos_buf) self:render_header( self.containers.todos.winid, diff --git a/lua/avante/templates/agentic.avanterules b/lua/avante/templates/agentic.avanterules index 45ae4c5..8f654f8 100644 --- a/lua/avante/templates/agentic.avanterules +++ b/lua/avante/templates/agentic.avanterules @@ -10,6 +10,82 @@ {%- include "_task-guidelines.avanterules" %} +{% if not enable_fastapply -%} +==== + +EDITING FILES + +You have access to two tools for working with files: **write_to_file** and **replace_in_file**. Understanding their roles and selecting the right one for the job will help ensure efficient and accurate modifications. + +# write_to_file + +## Purpose + +- Create a new file, or overwrite the entire contents of an existing file. + +## When to Use + +- Initial file creation, such as when scaffolding a new project. +- Overwriting large boilerplate files where you want to replace the entire content at once. +- When the complexity or number of changes would make replace_in_file unwieldy or error-prone. +- When you need to completely restructure a file's content or change its fundamental organization. + +## Important Considerations + +- Using write_to_file requires providing the file's complete final content. +- If you only need to make small changes to an existing file, consider using replace_in_file instead to avoid unnecessarily rewriting the entire file. +- While write_to_file should not be your default choice, don't hesitate to use it when the situation truly calls for it. + +# replace_in_file + +## Purpose + +- Make targeted edits to specific parts of an existing file without overwriting the entire file. + +## When to Use + +- Small, localized changes like updating a few lines, function implementations, changing variable names, modifying a section of text, etc. +- Targeted improvements where only specific portions of the file's content needs to be altered. +- Especially useful for long files where much of the file will remain unchanged. + +## Advantages + +- More efficient for minor edits, since you don't need to supply the entire file content. +- Reduces the chance of errors that can occur when overwriting large files. + +# Choosing the Appropriate Tool + +- **Default to replace_in_file** for most changes. It's the safer, more precise option that minimizes potential issues. +- **Use write_to_file** when: + - Creating new files + - The changes are so extensive that using replace_in_file would be more complex or risky + - You need to completely reorganize or restructure a file + - The file is relatively small and the changes affect most of its content + - You're generating boilerplate or template files + +# Auto-formatting Considerations + +- After using either write_to_file or replace_in_file, the user's editor may automatically format the file +- This auto-formatting may modify the file contents, for example: + - Breaking single lines into multiple lines + - Adjusting indentation to match project style (e.g. 2 spaces vs 4 spaces vs tabs) + - Converting single quotes to double quotes (or vice versa based on project preferences) + - Organizing imports (e.g. sorting, grouping by type) + - Adding/removing trailing commas in objects and arrays + - Enforcing consistent brace style (e.g. same-line vs new-line) + - Standardizing semicolon usage (adding or removing based on style) +- The write_to_file and replace_in_file tool responses will include the final state of the file after any auto-formatting +- Use this final state as your reference point for any subsequent edits. This is ESPECIALLY important when crafting SEARCH blocks for replace_in_file which require the content to match what's in the file exactly. + +# Workflow Tips + +1. Before editing, assess the scope of your changes and decide which tool to use. +2. For targeted edits, apply replace_in_file with carefully crafted SEARCH/REPLACE blocks. If you need multiple changes, you can stack multiple SEARCH/REPLACE blocks within a single replace_in_file call. +3. For major overhauls or initial file creation, rely on write_to_file. +4. Once the file has been edited with either write_to_file or replace_in_file, the system will provide you with the final state of the modified file. Use this updated content as the reference point for any subsequent SEARCH/REPLACE operations, since it reflects any auto-formatting or user-applied changes. +By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient. + +{% endif %} ==== RULES diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index a98e1f2..b06effd 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -1579,11 +1579,11 @@ end ---@param tool_use AvanteLLMToolUse function M.tool_use_to_xml(tool_use) - local xml = string.format("\n<%s>\n", tool_use.name) - for k, v in pairs(tool_use.input or {}) do - xml = xml .. string.format("<%s>%s\n", k, tostring(v), k) - end - xml = xml .. "\n" + local tool_use_json = vim.json.encode({ + name = tool_use.name, + input = tool_use.input, + }) + local xml = string.format("%s", tool_use_json) return xml end diff --git a/lua/avante/utils/prompts.lua b/lua/avante/utils/prompts.lua index 75ab487..6101804 100644 --- a/lua/avante/utils/prompts.lua +++ b/lua/avante/utils/prompts.lua @@ -1,3 +1,4 @@ +local Config = require("avante.config") local M = {} ---@param provider_conf AvanteDefaultBaseProvider @@ -16,35 +17,37 @@ You have access to a set of tools that are executed upon the user's approval. Yo # Tool Use Formatting -Tool use is formatted using XML-style tags. Each tool use is wrapped in a tag. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure: +Tool use is formatted using XML-style tags. Each tool use is wrapped in a tag. The tool use content is a valid JSON with tool name and tool input. Here's the structure: - -value1 -value2 -... - +{ + "name": "tool_name", + "input": { + "parameter1_name": "value1", + "parameter2_name": "value2", + ... + } +} For example: - - -I have completed the task... - - +{"name": "attempt_completion", "input": {"result": "I have completed the task..."}} + - -./src -npm run dev - +{"name": "bash", "input": {"path": "./src", "command": "npm run dev"}} ALWAYS ADHERE TO this format for the tool use to ensure proper parsing and execution. +## RULES +- When outputting the JSON for tool_use, you MUST first output the "name" field and then the "input" field. +- The value of "input" MUST be VALID JSON. +- If the "input" JSON object contains a "path" field, you MUST output the "path" field before any other fields. + ## OUTPUT FORMAT Please remember you are not allowed to use any format related to function calling or fc or tool_code. @@ -71,15 +74,23 @@ Parameters: field.get_description and field.get_description() or (field.description or "") ) end + if field.choices then + tool_prompt = tool_prompt .. " - Choices: " + for i, choice in ipairs(field.choices) do + tool_prompt = tool_prompt .. string.format("%s", choice) + if i ~= #field.choices then tool_prompt = tool_prompt .. ", " end + end + tool_prompt = tool_prompt .. "\n" + end end if tool.param.usage then - tool_prompt = tool_prompt - .. ("Usage:\n\n<{{name}}>\n"):gsub("{{([%w_]+)}}", function(name) return tool[name] end) - for k, v in pairs(tool.param.usage) do - tool_prompt = tool_prompt .. "<" .. k .. ">" .. tostring(v) .. "\n" - end - tool_prompt = tool_prompt - .. ("\n\n"):gsub("{{([%w_]+)}}", function(name) return tool[name] end) + tool_prompt = tool_prompt .. "Usage:\n" + local tool_use = { + name = tool.name, + input = tool.param.usage, + } + local tool_use_json = vim.json.encode(tool_use) + tool_prompt = tool_prompt .. tool_use_json .. "\n" end tools_prompts = tools_prompts .. tool_prompt .. "\n" end @@ -92,119 +103,46 @@ Parameters: ## Example 1: Requesting to execute a command - - -./src -npm run dev - - +{"name": "bash", "input": {"path": "./src", "command": "npm run dev"}} +]] + if Config.behaviour.enable_fastapply then + system_prompt = system_prompt + .. [[ ## Example 2: Requesting to create a new file - - -src/frontend-config.json - -{ - "apiEndpoint": "https://api.example.com", - "theme": { - "primaryColor": "#007bff", - "secondaryColor": "#6c757d", - "fontFamily": "Arial, sans-serif" - }, - "features": { - "darkMode": true, - "notifications": true, - "analytics": false - }, - "version": "1.0.0" -} - - - +{"name": "edit_file", "input": {"path": "src/frontend-config.json", "instructions": "write the following content to the file", "code_edit": "// ... existing code ...\nFIRST_EDIT\n// ... existing code ...\nSECOND_EDIT\n// ... existing code ...\nTHIRD_EDIT\n// ... existing code ...\n\n"}} ## Example 3: Requesting to make targeted edits to a file - - -src/components/App.tsx - -------- SEARCH -import React from 'react'; -======= -import React, { useState } from 'react'; -+++++++ REPLACE +{"name": "edit_file", "input": {"path": "src/frontend-config.json", "instructions": "write the following content to the file", "code_edit": "// ... existing code ...\nFIRST_EDIT\n// ... existing code ...\nSECOND_EDIT\n// ... existing code ...\nTHIRD_EDIT\n// ... existing code ...\n\n"}} +]] + else + system_prompt = system_prompt + .. [[ +## Example 2: Requesting to create a new file -------- SEARCH -function handleSubmit() { - saveData(); - setLoading(false); -} +{"name": "write_to_file", "input": {"path": "src/frontend-config.json", "the_content": "{\n \"apiEndpoint\": \"https://api.example.com\",\n \"theme\": {\n \"primaryColor\": \"#007bff\",\n \"secondaryColor\": \"#6c757d\",\n \"fontFamily\": \"Arial, sans-serif\"\n },\n \"features\": {\n \"darkMode\": true,\n \"notifications\": true,\n \"analytics\": false\n },\n \"version\": \"1.0.0\"\n}"}} -======= -+++++++ REPLACE +## Example 3: Requesting to make targeted edits to a file -------- SEARCH -return ( -
-======= -function handleSubmit() { - saveData(); - setLoading(false); -} - -return ( -
-+++++++ REPLACE - - - +{"name": "replace_in_file", "input": {"path": "src/components/App.tsx", "the_diff": "------- SEARCH\nimport React from 'react';\n=======\nimport React, { useState } from 'react';\n+++++++ REPLACE\n\n------- SEARCH\nfunction handleSubmit() {\n saveData();\n setLoading(false);\n}\n\n=======\n+++++++ REPLACE\n\n------- SEARCH\nreturn (\n
\n=======\nfunction handleSubmit() {\n saveData();\n setLoading(false);\n}\n\nreturn (\n
\n+++++++ REPLACE\n"}}} +]] + end + system_prompt = system_prompt + .. [[ ## Example 4: Complete current task - - - -I've successfully created the requested React component with the following features: -- Responsive layout -- Dark/light mode toggle -- Form validation -- API integration - - - +{"name": "attempt_completion", "input": {"result": "I've successfully created the requested React component with the following features:\n- Responsive layout\n- Dark/light mode toggle\n- Form validation\n- API integration"}} ## Example 5: Add todos - - - -[ - { - "id": "1", - "content": "Implement a responsive layout", - "status": "todo", - "priority": "low" - }, - { - "id": "2", - "content": "Add dark/light mode toggle", - "status": "todo", - "priority": "medium" - }, -] - - - +{"name": "add_todos", "input": {"todos": [{"id": "1", "content": "Implement a responsive layout", "status": "todo", "priority": "low"}, {"id": "2", "content": "Add dark/light mode toggle", "status": "todo", "priority": "medium"}]}} ## Example 6: Update todo status - - -1 -done - - +{"name": "update_todo_status", "input": {"id": "1", "status": "done"}} ]] end return system_prompt