new-qwen/serv/tools.py

154 lines
5.6 KiB
Python

from __future__ import annotations
import json
import subprocess
from pathlib import Path
from typing import Any, Callable
class ToolError(RuntimeError):
pass
class ToolRegistry:
def __init__(self, workspace_root: Path) -> None:
self.workspace_root = workspace_root.resolve()
self._handlers: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
"list_files": self.list_files,
"read_file": self.read_file,
"write_file": self.write_file,
"exec_command": self.exec_command,
}
def schemas(self) -> list[dict[str, Any]]:
return [
{
"type": "function",
"function": {
"name": "list_files",
"description": "List files in a directory inside the workspace.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string"},
},
"required": ["path"],
},
},
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read a UTF-8 text file from the workspace.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string"},
},
"required": ["path"],
},
},
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Write UTF-8 text into a file inside the workspace.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"},
},
"required": ["path", "content"],
},
},
},
{
"type": "function",
"function": {
"name": "exec_command",
"description": "Run a shell command inside the workspace and return stdout, stderr and exit code.",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string"},
"cwd": {"type": "string"},
},
"required": ["command"],
},
},
},
]
def _resolve(self, raw_path: str) -> Path:
candidate = Path(raw_path)
if not candidate.is_absolute():
candidate = self.workspace_root / candidate
resolved = candidate.resolve()
if self.workspace_root not in resolved.parents and resolved != self.workspace_root:
raise ToolError("Path escapes workspace root")
return resolved
def execute(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
handler = self._handlers.get(name)
if not handler:
raise ToolError(f"Unknown tool: {name}")
return handler(arguments)
def list_files(self, arguments: dict[str, Any]) -> dict[str, Any]:
target = self._resolve(arguments["path"])
if not target.exists():
raise ToolError("Directory does not exist")
if not target.is_dir():
raise ToolError("Path is not a directory")
items = []
for item in sorted(target.iterdir(), key=lambda value: value.name):
items.append(
{
"name": item.name,
"path": str(item.relative_to(self.workspace_root)),
"type": "dir" if item.is_dir() else "file",
}
)
return {"items": items}
def read_file(self, arguments: dict[str, Any]) -> dict[str, Any]:
target = self._resolve(arguments["path"])
if not target.exists():
raise ToolError("File does not exist")
if not target.is_file():
raise ToolError("Path is not a file")
return {"path": str(target.relative_to(self.workspace_root)), "content": target.read_text(encoding="utf-8")}
def write_file(self, arguments: dict[str, Any]) -> dict[str, Any]:
target = self._resolve(arguments["path"])
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(arguments["content"], encoding="utf-8")
return {"path": str(target.relative_to(self.workspace_root)), "bytes_written": len(arguments["content"].encode("utf-8"))}
def exec_command(self, arguments: dict[str, Any]) -> dict[str, Any]:
cwd = self._resolve(arguments.get("cwd", "."))
command = arguments["command"]
completed = subprocess.run(
command,
cwd=str(cwd),
shell=True,
capture_output=True,
text=True,
timeout=120,
)
return {
"command": command,
"cwd": str(cwd.relative_to(self.workspace_root)),
"returncode": completed.returncode,
"stdout": completed.stdout[-12000:],
"stderr": completed.stderr[-12000:],
}
@staticmethod
def encode_result(result: dict[str, Any]) -> str:
return json.dumps(result, ensure_ascii=False)