Files
avante.nvim/lua/avante/libs/ReAct_parser2.lua
2025-07-26 16:06:56 +08:00

262 lines
6.4 KiB
Lua

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.<tool_use>{"name": "write", "input": {"path": "path/to/file.txt", "content": "foo"}}</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>{"name": "write", "input": {"path": "path/to/file.txt", "content": "foo"}}</tool_use>I am another tool.<tool_use>{"name": "write", "input": {"path": "path/to/file.txt", "content": "bar"}}</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>{"name"]])
--- returns
--- {
--- {
--- type = "text",
--- text = "Hello, world! I am a tool.",
--- partial = false,
--- }
--- }
---
--- parse([[Hello, world! I am a tool.<tool_use>{"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.<tool_use>{"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.<tool_use>{"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("<tool_use>", 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 "<tool_use>"
local tool_end = text:find("</tool_use>", 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 "</tool_use>"
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