fix: refine tools (#2229)

This commit is contained in:
yetone
2025-06-16 01:27:39 +08:00
committed by GitHub
parent b8aa182c3f
commit fcf457ed7f
15 changed files with 247 additions and 169 deletions

View File

@@ -989,8 +989,8 @@ In case you want to ban some tools to avoid its usage (like Claude 3.7 overusing
Tool list
> rag_search, python, git_diff, git_commit, list_files, search_files, search_keyword, read_file_toplevel_symbols,
> read_file, create_file, rename_file, delete_file, create_dir, rename_dir, delete_dir, bash, web_search, fetch
> rag_search, python, git_diff, git_commit, glob, search_keyword, read_file_toplevel_symbols,
> read_file, create_file, move_path, copy_path, delete_path, create_dir, bash, web_search, fetch
## Custom Tools

View File

@@ -847,8 +847,8 @@ Avante 默认启用工具,但某些 LLM 模型不支持工具。您可以通
工具列表
> rag_search, python, git_diff, git_commit, list_files, search_files, search_keyword, read_file_toplevel_symbols,
> read_file, create_file, rename_file, delete_file, create_dir, rename_dir, delete_dir, bash, web_search, fetch
> rag_search, python, git_diff, git_commit, glob, search_keyword, read_file_toplevel_symbols,
> read_file, create_file, move_path, copy_path, delete_path, create_dir, bash, web_search, fetch
## 自定义工具

View File

@@ -48,6 +48,7 @@ local Highlights = {
AVANTE_STATE_SPINNER_THINKING = { name = "AvanteStateSpinnerThinking", fg = "#1e222a", bg = "#c678dd" },
AVANTE_STATE_SPINNER_COMPACTING = { name = "AvanteStateSpinnerCompacting", fg = "#1e222a", bg = "#c678dd" },
AVANTE_TASK_COMPLETED = { name = "AvanteTaskCompleted", fg = "#98c379", bg_link = "Normal" },
AVANTE_THINKING = { name = "AvanteThinking", fg = "#1e222a", bg = "#c678dd" },
}
Highlights.conflict = {

View File

@@ -735,10 +735,10 @@ function M._stream(opts)
return handle_next_tool_use(partial_tool_use_list, tool_use_index + 1, tool_results)
end
local is_edit_tool_use = Utils.is_edit_func_call_tool_use(partial_tool_use)
local is_attempt_completion_tool_use = partial_tool_use.name == "attempt_completion"
if partial_tool_use.state == "generating" and not is_edit_tool_use and not is_attempt_completion_tool_use then
return
end
local support_streaming = false
local llm_tool = vim.iter(prompt_opts.tools):find(function(tool) return tool.name == partial_tool_use.name end)
if llm_tool then support_streaming = llm_tool.support_streaming == true end
if partial_tool_use.state == "generating" and not is_edit_tool_use and not support_streaming then return end
if type(partial_tool_use.input) == "table" then partial_tool_use.input.tool_use_id = partial_tool_use.id end
if partial_tool_use.state == "generating" then
if type(partial_tool_use.input) == "table" then

View File

@@ -15,6 +15,8 @@ After each tool use, the user will respond with the result of that tool use, i.e
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
]]
M.support_streaming = true
M.enabled = function() return Config.mode == "agentic" end
---@type AvanteLLMToolParam
@@ -54,7 +56,7 @@ M.returns = {
},
}
---@type AvanteLLMToolOnRender<AttemptCompletionInput>
---@type avante.LLMToolOnRender<AttemptCompletionInput>
function M.on_render(opts)
local lines = {}
table.insert(lines, Line:new({ { "✓ Task Completed", Highlights.AVANTE_TASK_COMPLETED } }))

View File

@@ -219,6 +219,7 @@ function M.func(opts, on_log, on_complete, session_ctx)
local abs_path = Helpers.get_abs_path(opts.path)
if not Helpers.has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "Path not found: " .. abs_path end
if not opts.command then return false, "Command is required" end
if on_log then on_log("command: " .. opts.command) end
---change cwd to abs_path
---@param output string

View File

@@ -95,21 +95,20 @@ function M.write_global_file(opts, on_log, on_complete)
end)
end
---@type AvanteLLMToolFunc<{ path: string, new_path: string }>
function M.rename_file(opts, on_log, on_complete)
local abs_path = Helpers.get_abs_path(opts.path)
---@type AvanteLLMToolFunc<{ source_path: string, destination_path: string }>
function M.move_path(opts, on_log, on_complete)
local abs_path = Helpers.get_abs_path(opts.source_path)
if not Helpers.has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "File not found: " .. abs_path end
if not Path:new(abs_path):is_file() then return false, "Path is not a file: " .. abs_path end
local new_abs_path = Helpers.get_abs_path(opts.new_path)
if not Path:new(abs_path):exists() then return false, "The source path not found: " .. abs_path end
local new_abs_path = Helpers.get_abs_path(opts.destination_path)
if on_log then on_log(abs_path .. " -> " .. new_abs_path) end
if not Helpers.has_permission_to_access(new_abs_path) then
return false, "No permission to access path: " .. new_abs_path
end
if Path:new(new_abs_path):exists() then return false, "File already exists: " .. new_abs_path end
if Path:new(new_abs_path):exists() then return false, "The destination path already exists: " .. new_abs_path end
if not on_complete then return false, "on_complete not provided" end
Helpers.confirm(
"Are you sure you want to rename the file: " .. abs_path .. " to: " .. new_abs_path,
"Are you sure you want to move the path: " .. abs_path .. " to: " .. new_abs_path,
function(ok, reason)
if not ok then
on_complete(false, "User declined, reason: " .. (reason or "unknown"))
@@ -121,35 +120,61 @@ function M.rename_file(opts, on_log, on_complete)
)
end
---@type AvanteLLMToolFunc<{ path: string, new_path: string }>
function M.copy_file(opts, on_log)
local abs_path = Helpers.get_abs_path(opts.path)
---@type AvanteLLMToolFunc<{ source_path: string, destination_path: string }>
function M.copy_path(opts, on_log, on_complete)
local abs_path = Helpers.get_abs_path(opts.source_path)
if not Helpers.has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "File not found: " .. abs_path end
if not Path:new(abs_path):is_file() then return false, "Path is not a file: " .. abs_path end
local new_abs_path = Helpers.get_abs_path(opts.new_path)
if not Path:new(abs_path):exists() then return false, "The source path not found: " .. abs_path end
local new_abs_path = Helpers.get_abs_path(opts.destination_path)
if not Helpers.has_permission_to_access(new_abs_path) then
return false, "No permission to access path: " .. new_abs_path
end
if Path:new(new_abs_path):exists() then return false, "File already exists: " .. new_abs_path end
if on_log then on_log("Copying file: " .. abs_path .. " to " .. new_abs_path) end
Path:new(new_abs_path):write(Path:new(abs_path):read())
return true, nil
if Path:new(new_abs_path):exists() then return false, "The destination path already exists: " .. new_abs_path end
if not on_complete then return false, "on_complete not provided" end
Helpers.confirm(
"Are you sure you want to copy the path: " .. abs_path .. " to: " .. new_abs_path,
function(ok, reason)
if not ok then
on_complete(false, "User declined, reason: " .. (reason or "unknown"))
return
end
if on_log then on_log("Copying path: " .. abs_path .. " to " .. new_abs_path) end
if Path:new(abs_path):is_dir() then
Path:new(new_abs_path):mkdir({ parents = true })
for _, entry in ipairs(Path:new(abs_path):list()) do
local new_entry_path = Path:new(new_abs_path):joinpath(entry)
if entry:match("^%.") then goto continue end
if Path:new(new_entry_path):exists() then
if Path:new(new_entry_path):is_dir() then
Path:new(new_entry_path):rmdir()
else
Path:new(new_entry_path):unlink()
end
end
vim.fn.mkdir(new_entry_path, "p")
Path:new(new_entry_path):write(Path:new(abs_path):joinpath(entry):read())
::continue::
end
else
Path:new(new_abs_path):write(Path:new(abs_path):read())
end
on_complete(true, nil)
end
)
end
---@type AvanteLLMToolFunc<{ path: string }>
function M.delete_file(opts, on_log, on_complete)
function M.delete_path(opts, on_log, on_complete)
local abs_path = Helpers.get_abs_path(opts.path)
if not Helpers.has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "File not found: " .. abs_path end
if not Path:new(abs_path):is_file() then return false, "Path is not a file: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "Path not found: " .. abs_path end
if not on_complete then return false, "on_complete not provided" end
Helpers.confirm("Are you sure you want to delete the file: " .. abs_path, function(ok, reason)
Helpers.confirm("Are you sure you want to delete the path: " .. abs_path, function(ok, reason)
if not ok then
on_complete(false, "User declined, reason: " .. (reason or "unknown"))
return
end
if on_log then on_log("Deleting file: " .. abs_path) end
if on_log then on_log("Deleting path: " .. abs_path) end
os.remove(abs_path)
on_complete(true, nil)
end)
@@ -172,50 +197,6 @@ function M.create_dir(opts, on_log, on_complete)
end)
end
---@type AvanteLLMToolFunc<{ path: string, new_path: string }>
function M.rename_dir(opts, on_log, on_complete)
local abs_path = Helpers.get_abs_path(opts.path)
if not Helpers.has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "Directory not found: " .. abs_path end
if not Path:new(abs_path):is_dir() then return false, "Path is not a directory: " .. abs_path end
local new_abs_path = Helpers.get_abs_path(opts.new_path)
if not Helpers.has_permission_to_access(new_abs_path) then
return false, "No permission to access path: " .. new_abs_path
end
if Path:new(new_abs_path):exists() then return false, "Directory already exists: " .. new_abs_path end
if not on_complete then return false, "on_complete not provided" end
Helpers.confirm(
"Are you sure you want to rename directory " .. abs_path .. " to " .. new_abs_path .. "?",
function(ok, reason)
if not ok then
on_complete(false, "User declined, reason: " .. (reason or "unknown"))
return
end
if on_log then on_log("Renaming directory: " .. abs_path .. " to " .. new_abs_path) end
os.rename(abs_path, new_abs_path)
on_complete(true, nil)
end
)
end
---@type AvanteLLMToolFunc<{ path: string }>
function M.delete_dir(opts, on_log, on_complete)
local abs_path = Helpers.get_abs_path(opts.path)
if not Helpers.has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "Directory not found: " .. abs_path end
if not Path:new(abs_path):is_dir() then return false, "Path is not a directory: " .. abs_path end
if not on_complete then return false, "on_complete not provided" end
Helpers.confirm("Are you sure you want to delete the directory: " .. abs_path, function(ok, reason)
if not ok then
on_complete(false, "User declined, reason: " .. (reason or "unknown"))
return
end
if on_log then on_log("Deleting directory: " .. abs_path) end
os.remove(abs_path)
on_complete(true, nil)
end)
end
---@type AvanteLLMToolFunc<{ query: string }>
function M.web_search(opts, on_log)
local provider_type = Config.web_search_engine.provider
@@ -896,25 +877,42 @@ M._tools = {
},
},
{
name = "rename_file",
description = "Rename a file in current project scope",
name = "move_path",
description = [[Moves or rename a file or directory in the project, and returns confirmation that the move succeeded.
If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move.
This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.]],
param = {
type = "table",
fields = {
{
name = "path",
description = "Relative path to the file in current project scope",
name = "source_path",
description = [[The source path of the file or directory to move/rename.
<example>
If the project has the following files:
- directory1/a/something.txt
- directory2/a/things.txt
- directory3/a/other.txt
You can move the first file by providing a source_path of "directory1/a/something.txt"
</example>]],
type = "string",
},
{
name = "new_path",
description = "New relative path for the file",
name = "destination_path",
description = [[The destination path where the file or directory should be moved/renamed to. If the paths are the same except for the filename, then this will be a rename.
<example>
To move "directory1/a/something.txt" to "directory2/b/renamed.txt", provide a destination_path of "directory2/b/renamed.txt"
</example>]],
type = "string",
},
},
usage = {
path = "Relative path to the file in current project scope",
new_path = "New relative path for the file",
source_path = "The source path of the file or directory to move/rename",
destination_path = "The destination path where the file or directory should be moved/renamed to",
},
},
returns = {
@@ -932,19 +930,81 @@ M._tools = {
},
},
{
name = "delete_file",
description = "Delete a file in current project scope",
name = "copy_path",
description = [[Copies a file or directory from the project to a new location, and returns confirmation that the copy succeeded.
This tool should be used when it's desirable to copy a file or directory without changing its contents at all.]],
param = {
type = "table",
fields = {
{
name = "source_path",
description = [[The source path of the file or directory to copy.
<example>
If the project has the following files:
- directory1/a/something.txt
- directory2/a/things.txt
- directory3/a/other.txt
You can copy the first file by providing a source_path of "directory1/a/something.txt"
</example>]],
type = "string",
},
{
name = "destination_path",
description = [[The destination path where the file or directory should be copied to.
<example>
To copy "directory1/a/something.txt" to "directory2/b/copied.txt", provide a destination_path of "directory2/b/copied.txt"
</example>]],
type = "string",
},
},
usage = {
source_path = "The source path of the file or directory to copy",
destination_path = "The destination path where the file or directory should be copied to",
},
},
returns = {
{
name = "success",
description = "True if the file was copied successfully, false otherwise",
type = "boolean",
},
{
name = "error",
description = "Error message if the file was not copied successfully",
type = "string",
optional = true,
},
},
},
{
name = "delete_path",
description = "Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion.",
param = {
type = "table",
fields = {
{
name = "path",
description = "Relative path to the file in current project scope",
description = [[The path of the file or directory to delete.
<example>
If the project has the following files:
- directory1/a/something.txt
- directory2/a/things.txt
- directory3/a/other.txt
You can delete the first file by providing a path of "directory1/a/something.txt"
</example>
]],
type = "string",
},
},
usage = {
path = "Relative path to the file in current project scope",
path = "Relative path to the file or directory in the current project scope",
},
},
returns = {
@@ -991,72 +1051,7 @@ M._tools = {
},
},
},
{
name = "rename_dir",
description = "Rename a directory in current project scope",
param = {
type = "table",
fields = {
{
name = "path",
description = "Relative path to the project directory",
type = "string",
},
{
name = "new_path",
description = "New relative path for the directory",
type = "string",
},
},
usage = {
path = "Relative path to the project directory",
new_path = "New relative path for the directory",
},
},
returns = {
{
name = "success",
description = "True if the directory was renamed successfully, false otherwise",
type = "boolean",
},
{
name = "error",
description = "Error message if the directory was not renamed successfully",
type = "string",
optional = true,
},
},
},
{
name = "delete_dir",
description = "Delete a directory in current project scope",
param = {
type = "table",
fields = {
{
name = "path",
description = "Relative path to the project directory",
type = "string",
},
},
usage = {
path = "Relative path to the project directory",
},
},
returns = {
{
name = "success",
description = "True if the directory was deleted successfully, false otherwise",
type = "boolean",
},
{
name = "error",
description = "Error message if the directory was not deleted successfully",
type = "string",
optional = true,
},
},
},
require("avante.llm_tools.thinking"),
require("avante.llm_tools.get_diagnostics"),
require("avante.llm_tools.bash"),
require("avante.llm_tools.attempt_completion"),
@@ -1188,8 +1183,6 @@ M.run_python = M.python
---@return string | nil result
---@return string | nil error
function M.process_tool_use(tools, tool_use, on_log, on_complete, session_ctx)
-- Utils.debug("use tool", tool_use.name, tool_use.input_json)
-- Check if execution is already cancelled
if Helpers.is_cancelled then
Utils.debug("Tool execution cancelled before starting: " .. tool_use.name)

View File

@@ -16,6 +16,7 @@ M.name = "replace_in_file"
M.description =
"Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file."
M.support_streaming = true
-- function M.enabled() return Config.provider:match("ollama") == nil end
---@type AvanteLLMToolParam

View File

@@ -0,0 +1,65 @@
local Line = require("avante.ui.line")
local Base = require("avante.llm_tools.base")
local Highlights = require("avante.highlights")
local Utils = require("avante.utils")
---@class AvanteLLMTool
local M = setmetatable({}, Base)
M.name = "thinking"
M.description =
"A tool for thinking through problems, brainstorming ideas, or planning without executing any actions. Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action."
M.support_streaming = true
---@type AvanteLLMToolParam
M.param = {
type = "table",
fields = {
{
name = "content",
description = "Content to think about. This should be a description of what to think about or a problem to solve.",
type = "string",
},
},
}
---@type AvanteLLMToolReturn[]
M.returns = {
{
name = "success",
description = "Whether the task was completed successfully",
type = "string",
},
{
name = "thoughts",
description = "The thoughts that guided the solution",
type = "string",
},
}
---@class ThinkingInput
---@field content string
---@type avante.LLMToolOnRender<ThinkingInput>
function M.on_render(opts, _, state)
local lines = {}
local text = state == "generating" and "Thinking" or "Thoughts"
table.insert(lines, Line:new({ { Utils.icon("🤔 ") .. text, Highlights.AVANTE_THINKING } }))
table.insert(lines, Line:new({ { "" } }))
local content = opts.content or ""
local text_lines = vim.split(content, "\n")
for _, text_line in ipairs(text_lines) do
table.insert(lines, Line:new({ { "> " .. text_line } }))
end
return lines
end
---@type AvanteLLMToolFunc<ThinkingInput>
function M.func(opts, on_log, on_complete, session_ctx)
if not on_complete then return false, "on_complete not provided" end
on_complete(true, nil)
end
return M

View File

@@ -8,9 +8,11 @@ local M = setmetatable({}, Base)
M.name = "view"
M.description =
[[The view tool allows you to examine the contents of a file or list the contents of a directory. It can read the entire file or a specific range of lines. If the file content is already in the context, do not use this tool.
IMPORTANT NOTE: If the file content exceeds a certain size, the returned content will be truncated, and `is_truncated` will be set to true. If `is_truncated` is true, use the `start_line` parameter and `end_line` parameter to specify the range to view.
M.description = [[Reads the content of the given file in the project.
- Never attempt to read a path that hasn't been previously mentioned.
IMPORTANT NOTE: If the file content exceeds a certain size, the returned content will be truncated, and `is_truncated` will be set to true. If `is_truncated` is true, please use the `start_line` parameter and `end_line` parameter to call this `view` tool again.
]]
M.enabled = function(opts)
@@ -35,18 +37,29 @@ M.param = {
fields = {
{
name = "path",
description = "The path to the file in the current project scope",
description = [[The relative path of the file to read.
This path should never be absolute, and the first component of the path should always be a root directory in a project.
<example>
If the project has the following root directories:
- directory1
- directory2
If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`. If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
</example>]],
type = "string",
},
{
name = "start_line",
description = "The start line of the view range, 1-indexed",
description = "Optional line number to start reading on (1-based index)",
type = "integer",
optional = true,
},
{
name = "end_line",
description = "The end line of the view range, 1-indexed, and -1 for the end line means read to the end of the file",
description = "Optional line number to end reading on (1-based index, inclusive)",
type = "integer",
optional = true,
},

View File

@@ -7,6 +7,7 @@ TOOLS USAGE GUIDE
- You have access to tools, but only use them when necessary. If a tool is not required, respond as normal.
- Please DON'T be so aggressive in using tools, as many tasks can be better completed without tools.
- Files will be provided to you as context through <file> tag!
- You should make good use of the `thinking` tool, as it can help you better solve tasks, especially complex ones.
- Before using the `view` tool each time, always repeatedly check whether the file is already in the <file> tag. If it is already there, do not use the `view` tool, just read the file content directly from the <file> tag.
- If you use the `view` tool when file content is already provided in the <file> tag, you will be fired!
- If the `rag_search` tool exists, prioritize using it to do the search!

View File

@@ -1,5 +1,5 @@
Act as an expert software developer.
Always use best practices when coding.
You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
Respect and use existing conventions, libraries, etc that are already present in the code base.
Make sure code comments are in English when generating them.

View File

@@ -387,7 +387,7 @@ vim.g.avante_login = vim.g.avante_login
--- session_ctx?: table)
--- : (boolean | string | nil, string | nil)
---
--- @alias AvanteLLMToolOnRender<T> fun(input: T, logs: string[]): avante.ui.Line[]
--- @alias avante.LLMToolOnRender<T> fun(input: T, logs: string[], state: avante.HistoryMessageState | nil): avante.ui.Line[]
---
---@class AvanteLLMTool
---@field name string
@@ -397,7 +397,8 @@ vim.g.avante_login = vim.g.avante_login
---@field param AvanteLLMToolParam
---@field returns AvanteLLMToolReturn[]
---@field enabled? fun(opts: { user_input: string, history_messages: AvanteLLMMessage[] }): boolean
---@field on_render? AvanteLLMToolOnRender
---@field on_render? avante.LLMToolOnRender
---@field support_streaming? boolean
---@class AvanteLLMToolPublic : AvanteLLMTool
---@field func AvanteLLMToolFunc

View File

@@ -1624,7 +1624,7 @@ function M.message_content_item_to_lines(item, message, messages)
local hl = "AvanteStateSpinnerToolCalling"
local ok, llm_tool = pcall(require, "avante.llm_tools." .. item.name)
if ok then
if llm_tool.on_render then return llm_tool.on_render(item.input, message.tool_use_logs) end
if llm_tool.on_render then return llm_tool.on_render(item.input, message.tool_use_logs, message.state) end
end
local tool_result_message = M.get_tool_result_message(message, messages)
if tool_result_message then

View File

@@ -112,9 +112,9 @@ describe("llm_tools", function()
end)
end)
describe("delete_file", function()
describe("delete_path", function()
it("should delete existing file", function()
LlmTools.delete_file({ path = "test.txt" }, nil, function(success, err)
LlmTools.delete_path({ path = "test.txt" }, nil, function(success, err)
assert.is_nil(err)
assert.is_true(success)