From 07b703dbd5257051e0223bf902efd949f09c1064 Mon Sep 17 00:00:00 2001 From: yetone Date: Wed, 25 Jun 2025 16:55:10 +0800 Subject: [PATCH] fix: ReAct parser (#2330) --- lua/avante/config.lua | 1 + lua/avante/libs/ReAct_parser.lua | 408 +++++++++++++++++++++++ lua/avante/libs/xmlparser.lua | 91 ++++- lua/avante/llm_tools/replace_in_file.lua | 2 - lua/avante/providers/openai.lua | 66 ++-- lua/avante/utils/init.lua | 1 + lua/avante/utils/prompts.lua | 58 +++- 7 files changed, 577 insertions(+), 50 deletions(-) create mode 100644 lua/avante/libs/ReAct_parser.lua diff --git a/lua/avante/config.lua b/lua/avante/config.lua index 2a15385..4545d8b 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -297,6 +297,7 @@ M._defaults = { model = "gemini-2.0-flash", timeout = 30000, -- Timeout in milliseconds context_window = 1048576, + use_ReAct_prompt = true, extra_request_body = { generationConfig = { temperature = 0.75, diff --git a/lua/avante/libs/ReAct_parser.lua b/lua/avante/libs/ReAct_parser.lua new file mode 100644 index 0000000..a8316c8 --- /dev/null +++ b/lua/avante/libs/ReAct_parser.lua @@ -0,0 +1,408 @@ +---@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 +-- Returns {name = string, value = string, next_pos = number} or nil if incomplete +local function parse_parameter(text, start_pos) + local i = start_pos + local len = #text + + -- Skip whitespace + while i <= len and string.match(string.sub(text, i, i), "%s") do + i = i + 1 + end + + if i > len or string.sub(text, i, i) ~= "<" then return nil end + + -- Find parameter name + local param_name_start = i + 1 + local param_name_end = string.find(text, ">", param_name_start) + + if not param_name_end then + return nil -- Incomplete parameter tag + end + + local param_name = string.sub(text, param_name_start, param_name_end - 1) + i = param_name_end + 1 + + -- Find parameter value (everything until closing tag) + local param_close_tag = "" + local param_value_start = i + local param_close_pos = string.find(text, param_close_tag, i, true) + + if not param_close_pos then + -- Incomplete parameter value, return what we have + local param_value = string.sub(text, param_value_start) + return { + name = param_name, + value = param_value, + next_pos = len + 1, + } + end + + local param_value = string.sub(text, param_value_start, param_close_pos - 1) + i = param_close_pos + #param_close_tag + + return { + name = param_name, + value = param_value, + next_pos = i, + } +end + +-- Helper function to parse tool use content starting after +-- Returns {content = ToolUseContent, next_pos = number} or nil if incomplete +local function parse_tool_use(text, start_pos) + local i = start_pos + local len = #text + + -- Skip whitespace + while i <= len and string.match(string.sub(text, i, i), "%s") do + i = i + 1 + end + + if i > len then + return nil -- No content after + end + + -- Check if we have opening tag for tool name + if string.sub(text, i, i) ~= "<" then + return nil -- Invalid format + end + + -- Find tool name + local tool_name_start = i + 1 + local tool_name_end = string.find(text, ">", tool_name_start) + + if not tool_name_end then + return nil -- Incomplete tool name tag + end + + local tool_name = string.sub(text, tool_name_start, tool_name_end - 1) + i = tool_name_end + 1 + + -- Parse tool parameters + local tool_input = {} + local partial = false + + -- Look for tool closing tag or + local tool_close_tag = "" + local tool_use_close_tag = "" + + while i <= len do + -- Skip whitespace before checking for closing tags + while i <= len and string.match(string.sub(text, i, i), "%s") do + i = i + 1 + end + + if i > len then + partial = true + break + end + + -- Check for tool closing tag first + local tool_close_pos = string.find(text, tool_close_tag, i, true) + local tool_use_close_pos = string.find(text, tool_use_close_tag, i, true) + + if tool_close_pos and tool_close_pos == i then + -- Found tool closing tag + i = tool_close_pos + #tool_close_tag + + -- Skip whitespace + while i <= len and string.match(string.sub(text, i, i), "%s") do + i = i + 1 + end + + -- Check for + if i <= len and string.find(text, tool_use_close_tag, i, true) == i then + i = i + #tool_use_close_tag + partial = false + else + partial = true + end + break + elseif tool_use_close_pos and tool_use_close_pos == i then + -- Found without tool closing tag (malformed, but handle it) + i = tool_use_close_pos + #tool_use_close_tag + partial = false + break + else + -- Parse parameter tag + local param_result = parse_parameter(text, i) + if param_result then + tool_input[param_result.name] = param_result.value + i = param_result.next_pos + else + -- Incomplete parameter, mark as partial + partial = true + break + end + end + end + + -- If we reached end of text without proper closing, it's partial + if i > len then partial = true end + + return { + content = { + type = "tool_use", + tool_name = tool_name, + tool_input = tool_input, + partial = partial, + }, + next_pos = i, + } +end + +--- 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.path/to/file.txtfoo") +--- 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.path/to/file.txtfooI am another tool.path/to/file.txtbarhello") +--- 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.") +--- 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.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.path/to/file.txtfoo 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.path/to/file.txtfoo bar") +--- returns +--- { +--- { +--- type = "text", +--- text = "Hello, world! I am a tool.path/to/file.txtfoo bar", +--- partial = false, +--- } +--- } +--- +--- parse("Hello, world! I am a tool.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", +--- content = "", +--- }, +--- partial = false, +--- }, +--- } +--- +--- parse("Hello, world! I am a tool.path/to/file.txt