fix: ReAct parser (#2330)

This commit is contained in:
yetone
2025-06-25 16:55:10 +08:00
committed by GitHub
parent 46d27ff0fd
commit 07b703dbd5
7 changed files with 577 additions and 50 deletions

View File

@@ -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,

View File

@@ -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 <param_name>value</param_name>
-- 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 = "</" .. param_name .. ">"
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 <tool_use>
-- 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 <tool_use>
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 </tool_use>
local tool_close_tag = "</" .. tool_name .. ">"
local tool_use_close_tag = "</tool_use>"
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 </tool_use>
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 </tool_use> 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.<tool_use><write><path>path/to/file.txt</path><content>foo</content></write></tool_use>")
--- 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.<tool_use><write><path>path/to/file.txt</path><content>foo</content></write></tool_use>I am another tool.<tool_use><write><path>path/to/file.txt</path><content>bar</content></write></tool_use>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.<tool_use><write")
--- returns
--- {
--- {
--- type = "text",
--- text = "Hello, world! I am a tool.",
--- partial = false,
--- }
--- }
---
--- parse("Hello, world! I am a tool.<tool_use><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.<tool_use><write><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.<tool_use><write><path>path/to/file.txt</path><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.<write><path>path/to/file.txt</path><content>foo bar")
--- returns
--- {
--- {
--- type = "text",
--- text = "Hello, world! I am a tool.<write><path>path/to/file.txt</path><content>foo bar",
--- partial = false,
--- }
--- }
---
--- parse("Hello, world! I am a tool.<tool_use><write><path>path/to/file.txt</path><content><button>foo</button></content></write></tool_use>")
--- 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 = "<button>foo</button>",
--- },
--- partial = false,
--- },
--- }
---
--- parse("Hello, world! I am a tool.<tool_use><write><path>path/to/file.txt</path><content><button>foo")
--- returns
--- {
--- {
--- text = "Hello, world! I am a tool.",
--- partial = false,
--- },
--- {
--- type = "tool_use",
--- tool_name = "write",
--- tool_input = {
--- path = "path/to/file.txt",
--- content = "<button>foo",
--- },
--- partial = true,
--- },
--- }
---
---@param text string
---@return (avante.TextContent|avante.ToolUseContent)[]
function M.parse(text)
local result = {}
local current_text = ""
local i = 1
local len = #text
-- Helper function to add text content to result
local function add_text_content()
if current_text ~= "" then
table.insert(result, {
type = "text",
text = current_text,
partial = false,
})
current_text = ""
end
end
-- Helper function to find the next occurrence of a pattern
local function find_pattern(pattern, start_pos) return string.find(text, pattern, start_pos, true) end
while i <= len do
-- Check for <tool_use> tag
local tool_use_start = find_pattern("<tool_use>", i)
if tool_use_start and tool_use_start == i then
-- Found <tool_use> at current position
add_text_content()
i = i + 10 -- Skip "<tool_use>"
-- Parse tool use content
local tool_use_result = parse_tool_use(text, i)
if tool_use_result then
table.insert(result, tool_use_result.content)
i = tool_use_result.next_pos
else
-- Incomplete tool_use, break
break
end
else
-- Regular text character
if tool_use_start then
-- There's a <tool_use> ahead, add text up to that point
current_text = current_text .. string.sub(text, i, tool_use_start - 1)
i = tool_use_start
else
-- No more <tool_use> tags, add rest of text
current_text = current_text .. string.sub(text, i)
break
end
end
end
-- Add any remaining text
add_text_content()
return result
end
return M

View File

@@ -17,6 +17,9 @@ function StreamParser.new()
state = "ready", -- 解析状态: ready, parsing, incomplete, error
incomplete_tag = nil, -- 未完成的标签信息
last_error = nil, -- 最后的错误信息
inside_tool_use = false, -- 是否在 tool_use 标签内
tool_use_depth = 0, -- tool_use 标签嵌套深度
tool_use_stack = {}, -- tool_use 标签栈
}
setmetatable(parser, StreamParser)
return parser
@@ -33,6 +36,9 @@ function StreamParser:reset()
self.state = "ready"
self.incomplete_tag = nil
self.last_error = nil
self.inside_tool_use = false
self.tool_use_depth = 0
self.tool_use_stack = {}
end
-- 获取解析器状态信息
@@ -45,6 +51,9 @@ function StreamParser:getStatus()
incomplete_tag = self.incomplete_tag,
last_error = self.last_error,
has_incomplete = self.state == "incomplete" or self.incomplete_tag ~= nil,
inside_tool_use = self.inside_tool_use,
tool_use_depth = self.tool_use_depth,
tool_use_stack_size = #self.tool_use_stack,
}
end
@@ -90,16 +99,6 @@ local function decodeEntities(str)
return str
end
-- 检查标签是否在行首
local function isTagAtLineStart(xmlContent, tagStart)
-- 如果标签在整个内容的开始位置,认为是在行首
if tagStart == 1 then return true end
-- 检查标签前的字符,如果是换行符,则标签在行首
local charBeforeTag = xmlContent:sub(tagStart - 1, tagStart - 1)
return charBeforeTag == "\n"
end
-- 检查是否为有效的XML标签
local function isValidXmlTag(tag, xmlContent, tagStart)
-- 排除明显不是XML标签的内容比如数学表达式 < 或 >
@@ -110,8 +109,6 @@ local function isValidXmlTag(tag, xmlContent, tagStart)
if tag:match("^</[_%w]+>$") then return true end -- 结束标签
if tag:match("^<[_%w]+[^>]*/>$") then return true end -- 自闭合标签
if tag:match("^<[_%w]+[^>]*>$") then
if not isTagAtLineStart(xmlContent, tagStart) then return false end
-- 对于开始标签,进行额外的上下文检查
local tagName = tag:match("^<([_%w]+)")
@@ -163,7 +160,75 @@ function StreamParser:parseBuffer()
while self.position <= #self.buffer do
local remaining = self.buffer:sub(self.position)
-- 查找下一个标签
-- 首先检查是否有 tool_use 标签
local tool_use_start = remaining:find("<tool_use>")
local tool_use_end = remaining:find("</tool_use>")
-- 如果当前不在 tool_use 内,且找到了 tool_use 开始标签
if not self.inside_tool_use and tool_use_start then
-- 处理 tool_use 标签前的文本作为普通文本
if tool_use_start > 1 then
local precedingText = remaining:sub(1, tool_use_start - 1)
if precedingText ~= "" then
local textElement = {
_name = "_text",
_text = precedingText,
}
table.insert(self.results, textElement)
end
end
-- 进入 tool_use 模式
self.inside_tool_use = true
self.tool_use_depth = 1
table.insert(self.tool_use_stack, { start_pos = self.position + tool_use_start - 1 })
self.position = self.position + tool_use_start + 10 -- 跳过 "<tool_use>"
goto continue
end
-- 如果在 tool_use 内,检查是否遇到结束标签
if self.inside_tool_use and tool_use_end then
self.tool_use_depth = self.tool_use_depth - 1
if self.tool_use_depth == 0 then
-- 退出 tool_use 模式
self.inside_tool_use = false
table.remove(self.tool_use_stack)
self.position = self.position + tool_use_end + 11 -- 跳过 "</tool_use>"
goto continue
end
end
-- 如果不在 tool_use 内,将所有内容作为普通文本处理
if not self.inside_tool_use then
-- 查找下一个可能的 tool_use 标签
local next_tool_use = remaining:find("<tool_use>")
if next_tool_use then
-- 处理到下一个 tool_use 标签之前的文本
local text = remaining:sub(1, next_tool_use - 1)
if text ~= "" then
local textElement = {
_name = "_text",
_text = text,
}
table.insert(self.results, textElement)
end
self.position = self.position + next_tool_use - 1
else
-- 没有更多 tool_use 标签,处理剩余的所有文本
if remaining ~= "" then
local textElement = {
_name = "_text",
_text = remaining,
}
table.insert(self.results, textElement)
end
self.position = #self.buffer + 1
break
end
goto continue
end
-- 查找下一个标签(只有在 tool_use 内才进行 XML 解析)
local tagStart, tagEnd = remaining:find("</?[%w_]+>")
if not tagStart then

View File

@@ -133,8 +133,6 @@ function M.func(opts, on_log, on_complete, session_ctx)
local is_streaming = opts.streaming or false
if is_streaming then return end
session_ctx.prev_streaming_diff_timestamp_map = session_ctx.prev_streaming_diff_timestamp_map or {}
local current_timestamp = os.time()
if is_streaming then

View File

@@ -4,6 +4,7 @@ local Clipboard = require("avante.clipboard")
local Providers = require("avante.providers")
local HistoryMessage = require("avante.history_message")
local XMLParser = require("avante.libs.xmlparser")
local ReActParser = require("avante.libs.ReAct_parser")
local JsonParser = require("avante.libs.jsonparser")
local Prompts = require("avante.utils.prompts")
local LlmTools = require("avante.llm_tools")
@@ -226,13 +227,8 @@ function M:add_text_message(ctx, text, state, opts)
if llm_tool_names == nil then llm_tool_names = LlmTools.get_tool_names() end
if ctx.content == nil then ctx.content = "" end
ctx.content = ctx.content .. text
local content = ctx.content
:gsub("<tool_code>", "")
:gsub("</tool_code>", "")
:gsub("<tool_call>", "")
:gsub("</tool_call>", "")
:gsub("<tool_use>", "")
:gsub("</tool_use>", "")
local content =
ctx.content:gsub("<tool_code>", ""):gsub("</tool_code>", ""):gsub("<tool_call>", ""):gsub("</tool_call>", "")
ctx.content = content
local msg = HistoryMessage:new({
role = "assistant",
@@ -262,16 +258,15 @@ function M:add_text_message(ctx, text, state, opts)
::continue::
end
local cleaned_xml_content = table.concat(cleaned_xml_lines, "\n")
local stream_parser = XMLParser.createStreamParser()
stream_parser:addData(cleaned_xml_content)
local xml = stream_parser:getAllElements()
local xml = ReActParser.parse(cleaned_xml_content)
local has_tool_use = false
if xml then
local new_content_list = {}
local xml_md_openned = false
for idx, item in ipairs(xml) do
if item._name == "_text" then
if item.type == "text" then
local cleaned_lines = {}
local lines = vim.split(item._text, "\n")
local lines = vim.split(item.text, "\n")
for _, line in ipairs(lines) do
if line:match("^```xml") or line:match("^```tool_code") or line:match("^```tool_use") then
xml_md_openned = true
@@ -288,20 +283,18 @@ function M:add_text_message(ctx, text, state, opts)
table.insert(new_content_list, table.concat(cleaned_lines, "\n"))
goto continue
end
if not vim.tbl_contains(llm_tool_names, item._name) then goto continue end
local ok, input = pcall(vim.json.decode, item._text)
if not ok then input = {} end
if not ok and item.children and #item.children > 0 then
for _, item_ in ipairs(item.children) do
local ok_, input_ = pcall(vim.json.decode, item_._text)
if ok_ and input_ then
input[item_._name] = input_
else
input[item_._name] = item_._text
end
if not vim.tbl_contains(llm_tool_names, item.tool_name) then goto continue end
local input = {}
for k, v in pairs(item.tool_input or {}) do
local ok, jsn = pcall(vim.json.decode, v)
if ok and jsn then
input[k] = jsn
else
input[k] = v
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 msg_ = HistoryMessage:new({
@@ -309,7 +302,7 @@ function M:add_text_message(ctx, text, state, opts)
content = {
{
type = "tool_use",
name = item._name,
name = item.tool_name,
id = tool_use_id,
input = input,
},
@@ -321,18 +314,27 @@ function M:add_text_message(ctx, text, state, opts)
})
msgs[#msgs + 1] = msg_
ctx.tool_use_list = ctx.tool_use_list or {}
ctx.tool_use_list[#ctx.tool_use_list + 1] = {
id = tool_use_id,
name = item._name,
input_json = 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
exists = true
end
end
if not exists then
ctx.tool_use_list[#ctx.tool_use_list + 1] = {
id = tool_use_id,
name = item.tool_name,
input_json = input,
}
end
end
if #new_content_list > 0 then msg.message.content = table.concat(new_content_list, "\n") end
if #new_content_list > 0 then msg.displayed_content = table.concat(new_content_list, "\n") end
::continue::
end
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
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)
@@ -376,7 +378,7 @@ function M:add_tool_use_message(ctx, tool_use, state, opts)
tool_use.uuid = msg.uuid
tool_use.state = state
if opts.on_messages_add then opts.on_messages_add({ msg }) end
-- if state == "generating" then opts.on_stop({ reason = "tool_use", streaming_tool_use = true }) end
if state == "generating" then opts.on_stop({ reason = "tool_use", streaming_tool_use = true }) end
end
---@param usage avante.OpenAITokenUsage | nil

View File

@@ -1688,6 +1688,7 @@ end
---@param messages avante.HistoryMessage[]
---@return avante.ui.Line[]
function M.message_to_lines(message, messages)
if message.displayed_content then return M.text_to_lines(message.displayed_content) end
local content = message.message.content
if type(content) == "string" then return M.text_to_lines(content) end
if vim.islist(content) then

View File

@@ -16,21 +16,32 @@ 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. 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 <tool_use> 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>
<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
...
</tool_name>
</tool_use>
For example:
<tool_use>
<attempt_completion>
<result>
I have completed the task...
</result>
</attempt_completion>
</tool_use>
<tool_use>
<bash>
<path>./src</path>
<command>npm run dev</command>
</bash>
</tool_use>
ALWAYS ADHERE TO this format for the tool use to ensure proper parsing and execution.
@@ -60,11 +71,12 @@ Parameters:
end
if tool.param.usage then
tool_prompt = tool_prompt
.. ("Usage:\n<{{name}}>\n"):gsub("{{([%w_]+)}}", function(name) return tool[name] end)
.. ("Usage:\n<tool_use>\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) .. "</" .. k .. ">\n"
end
tool_prompt = tool_prompt .. ("</{{name}}>\n"):gsub("{{([%w_]+)}}", function(name) return tool[name] end)
tool_prompt = tool_prompt
.. ("</{{name}}>\n</tool_use>\n"):gsub("{{([%w_]+)}}", function(name) return tool[name] end)
end
tools_prompts = tools_prompts .. tool_prompt .. "\n"
end
@@ -77,13 +89,16 @@ Parameters:
## Example 1: Requesting to execute a command
<tool_use>
<bash>
<path>./src</path>
<command>npm run dev</command>
</bash>
</tool_use>
## Example 2: Requesting to create a new file
<tool_use>
<write_to_file>
<path>src/frontend-config.json</path>
<content>
@@ -103,9 +118,11 @@ Parameters:
}
</content>
</write_to_file>
</tool_use>
## Example 3: Requesting to make targeted edits to a file
<tool_use>
<replace_in_file>
<path>src/components/App.tsx</path>
<diff>
@@ -138,9 +155,11 @@ return (
+++++++ REPLACE
</diff>
</replace_in_file>
</tool_use>
## Example 4: Complete current task
<tool_use>
<attempt_completion>
<result>
I've successfully created the requested React component with the following features:
@@ -150,6 +169,39 @@ I've successfully created the requested React component with the following featu
- API integration
</result>
</attempt_completion>
</tool_use>
## Example 5: Add todos
<tool_use>
<add_todos>
<todos>
[
{
"id": "1",
"content": "Implement a responsive layout",
"status": "todo",
"priority": "low"
},
{
"id": "2",
"content": "Add dark/light mode toggle",
"status": "todo",
"priority": "medium"
},
]
</todos>
</add_todos>
</tool_use>
## Example 6: Update todo status
<tool_use>
<update_todo_status>
<id>1</id>
<status>done</status>
</update_todo_status>
</tool_use>
]]
end
return system_prompt