Add unified diff patch tool

This commit is contained in:
mirivlad 2026-04-07 17:19:32 +08:00
parent aa3154e9d7
commit 1679ca6262
2 changed files with 70 additions and 2 deletions

View File

@ -28,7 +28,7 @@ Qwen OAuth + OpenAI-compatible endpoint
- хранение токенов в `~/.qwen/oauth_creds.json` - хранение токенов в `~/.qwen/oauth_creds.json`
- HTTP API сервера - HTTP API сервера
- агентный цикл с tool calling - агентный цикл с tool calling
- инструменты: `list_files`, `glob_search`, `grep_text`, `stat_path`, `read_file`, `replace_in_file`, `write_file`, `make_directory`, `exec_command` - инструменты: `list_files`, `glob_search`, `grep_text`, `stat_path`, `read_file`, `apply_unified_diff`, `replace_in_file`, `write_file`, `make_directory`, `exec_command`
- Telegram polling без внешних библиотек - Telegram polling без внешних библиотек
- JSON-хранилище сессий - JSON-хранилище сессий
- API списка и просмотра сессий - API списка и просмотра сессий

View File

@ -5,6 +5,7 @@ import json
import os import os
import re import re
import subprocess import subprocess
import tempfile
from pathlib import Path from pathlib import Path
from typing import Any, Callable from typing import Any, Callable
@ -25,6 +26,7 @@ class ToolRegistry:
"grep_text": self.grep_text, "grep_text": self.grep_text,
"stat_path": self.stat_path, "stat_path": self.stat_path,
"read_file": self.read_file, "read_file": self.read_file,
"apply_unified_diff": self.apply_unified_diff,
"replace_in_file": self.replace_in_file, "replace_in_file": self.replace_in_file,
"write_file": self.write_file, "write_file": self.write_file,
"make_directory": self.make_directory, "make_directory": self.make_directory,
@ -107,6 +109,21 @@ class ToolRegistry:
}, },
}, },
}, },
{
"type": "function",
"function": {
"name": "apply_unified_diff",
"description": "Apply a unified diff patch inside the workspace using the system patch command.",
"parameters": {
"type": "object",
"properties": {
"patch": {"type": "string"},
"strip": {"type": "integer"},
},
"required": ["patch"],
},
},
},
{ {
"type": "function", "type": "function",
"function": { "function": {
@ -201,7 +218,12 @@ class ToolRegistry:
def requires_approval(self, tool_name: str) -> bool: def requires_approval(self, tool_name: str) -> bool:
policy = self.config.tool_policy policy = self.config.tool_policy
write_tools = {"replace_in_file", "write_file", "make_directory"} write_tools = {
"apply_unified_diff",
"replace_in_file",
"write_file",
"make_directory",
}
shell_tools = {"exec_command"} shell_tools = {"exec_command"}
if policy == "ask-all": if policy == "ask-all":
return True return True
@ -351,6 +373,52 @@ class ToolRegistry:
"replacements": count, "replacements": count,
} }
def apply_unified_diff(self, arguments: dict[str, Any]) -> dict[str, Any]:
patch_text = arguments["patch"]
strip = int(arguments.get("strip", 0))
if not patch_text.strip():
raise ToolError("Patch is empty")
if "\x00" in patch_text:
raise ToolError("Patch contains NUL byte")
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
suffix=".patch",
delete=False,
) as handle:
patch_file = Path(handle.name)
handle.write(patch_text)
try:
completed = subprocess.run(
["patch", f"-p{strip}", "--forward", "--batch", "-i", str(patch_file)],
cwd=str(self.workspace_root),
capture_output=True,
text=True,
timeout=120,
)
except FileNotFoundError as exc:
raise ToolError("System command 'patch' is not available") from exc
finally:
try:
patch_file.unlink()
except OSError:
pass
if completed.returncode != 0:
raise ToolError(
"patch failed: "
+ (completed.stderr.strip() or completed.stdout.strip() or "unknown error")
)
return {
"applied": True,
"strip": strip,
"stdout": completed.stdout[-self.config.max_command_output_bytes :],
"stderr": completed.stderr[-self.config.max_command_output_bytes :],
}
def make_directory(self, arguments: dict[str, Any]) -> dict[str, Any]: def make_directory(self, arguments: dict[str, Any]) -> dict[str, Any]:
target = self._resolve(arguments["path"]) target = self._resolve(arguments["path"])
target.mkdir(parents=True, exist_ok=True) target.mkdir(parents=True, exist_ok=True)