feat: refactor APIClient and ToolRegistry to classes
- api_client.py: APIClient class with ProviderConfig types, chat_stream() returns str instead of printing, module-level wrappers for backward compat - tools.py: ToolRegistry class with instance methods, pure Python list_files (os.walk) and search_in_files (re.search) replacing find/grep for Windows compat, English tool descriptions and error messages, module-level wrappers for backward compat - 74 new tests (21 api_client + 53 tools) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,32 +1,17 @@
|
||||
"""
|
||||
Tools system for Meeting Room agents.
|
||||
Provides file access and web capabilities via OpenAI function calling.
|
||||
|
||||
The ToolRegistry class encapsulates all tool logic with a configurable
|
||||
workdir. Module-level wrappers maintain backward compatibility.
|
||||
"""
|
||||
|
||||
import fnmatch
|
||||
import httpx
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import subprocess
|
||||
import httpx
|
||||
|
||||
WORKDIR = "."
|
||||
|
||||
|
||||
def set_workdir(path: str):
|
||||
global WORKDIR
|
||||
WORKDIR = os.path.abspath(path)
|
||||
|
||||
|
||||
def _resolve_path(path: str) -> str:
|
||||
if os.path.isabs(path):
|
||||
return path
|
||||
return os.path.normpath(os.path.join(WORKDIR, path))
|
||||
|
||||
|
||||
def _truncate(text: str, max_len: int = 5000) -> str:
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return text[:max_len] + f"\n... [обрезано, всего {len(text)} символов]"
|
||||
|
||||
|
||||
# ─── Tool schemas (OpenAI function calling format) ───
|
||||
@@ -36,13 +21,13 @@ TOOL_SCHEMAS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Прочитать содержимое файла. Возвращает текст с номерами строк.",
|
||||
"description": "Read file contents. Returns text with line numbers.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Путь к файлу"},
|
||||
"offset": {"type": "integer", "description": "Начать с этой строки (1-indexed)", "default": 1},
|
||||
"limit": {"type": "integer", "description": "Максимум строк", "default": 100},
|
||||
"path": {"type": "string", "description": "Path to the file"},
|
||||
"offset": {"type": "integer", "description": "Start from this line (1-indexed)", "default": 1},
|
||||
"limit": {"type": "integer", "description": "Maximum number of lines", "default": 100},
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
@@ -52,13 +37,13 @@ TOOL_SCHEMAS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_files",
|
||||
"description": "Показать список файлов и директорий.",
|
||||
"description": "List files and directories.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Директория для просмотра"},
|
||||
"pattern": {"type": "string", "description": "Фильтр по имени (glob)"},
|
||||
"max_depth": {"type": "integer", "description": "Глубина обхода", "default": 3},
|
||||
"path": {"type": "string", "description": "Directory to list"},
|
||||
"pattern": {"type": "string", "description": "Filter by name (glob)"},
|
||||
"max_depth": {"type": "integer", "description": "Traversal depth", "default": 3},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
@@ -68,13 +53,13 @@ TOOL_SCHEMAS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_in_files",
|
||||
"description": "Поиск текста в файлах (regex).",
|
||||
"description": "Search text in files (regex).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Поисковый запрос (regex)"},
|
||||
"path": {"type": "string", "description": "Директория для поиска"},
|
||||
"file_pattern": {"type": "string", "description": "Фильтр по расширению"},
|
||||
"query": {"type": "string", "description": "Search query (regex)"},
|
||||
"path": {"type": "string", "description": "Directory to search in"},
|
||||
"file_pattern": {"type": "string", "description": "Filter by extension (e.g. *.py)"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
@@ -84,12 +69,12 @@ TOOL_SCHEMAS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "Поиск в интернете. Возвращает заголовки, сниппеты и URL.",
|
||||
"description": "Search the web. Returns titles, snippets and URLs.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Поисковый запрос"},
|
||||
"max_results": {"type": "integer", "description": "Максимум результатов", "default": 5},
|
||||
"query": {"type": "string", "description": "Search query"},
|
||||
"max_results": {"type": "integer", "description": "Maximum results", "default": 5},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
@@ -99,12 +84,12 @@ TOOL_SCHEMAS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_fetch",
|
||||
"description": "Скачать содержимое веб-страницы.",
|
||||
"description": "Fetch web page content.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "URL для загрузки"},
|
||||
"max_length": {"type": "integer", "description": "Максимум символов", "default": 5000},
|
||||
"url": {"type": "string", "description": "URL to fetch"},
|
||||
"max_length": {"type": "integer", "description": "Maximum characters", "default": 5000},
|
||||
},
|
||||
"required": ["url"],
|
||||
},
|
||||
@@ -114,12 +99,12 @@ TOOL_SCHEMAS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "Записать содержимое в файл.",
|
||||
"description": "Write content to a file.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Путь к файлу"},
|
||||
"content": {"type": "string", "description": "Содержимое"},
|
||||
"path": {"type": "string", "description": "Path to the file"},
|
||||
"content": {"type": "string", "description": "Content to write"},
|
||||
},
|
||||
"required": ["path", "content"],
|
||||
},
|
||||
@@ -129,12 +114,12 @@ TOOL_SCHEMAS = [
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_command",
|
||||
"description": "Выполнить shell-команду.",
|
||||
"description": "Run a shell command.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string", "description": "Команда"},
|
||||
"timeout": {"type": "integer", "description": "Таймаут (сек)", "default": 30},
|
||||
"command": {"type": "string", "description": "Command to run"},
|
||||
"timeout": {"type": "integer", "description": "Timeout in seconds", "default": 30},
|
||||
},
|
||||
"required": ["command"],
|
||||
},
|
||||
@@ -142,170 +127,251 @@ TOOL_SCHEMAS = [
|
||||
},
|
||||
]
|
||||
|
||||
# Directories to skip during file traversal
|
||||
_SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".mypy_cache", ".pytest_cache"}
|
||||
|
||||
# ─── Implementations ───
|
||||
|
||||
def tool_read_file(path: str, offset: int = 1, limit: int = 100) -> str:
|
||||
resolved = _resolve_path(path)
|
||||
if not os.path.isfile(resolved):
|
||||
return f"Ошибка: файл не найден — {resolved}"
|
||||
try:
|
||||
with open(resolved, encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
total = len(lines)
|
||||
selected = lines[offset - 1 : offset - 1 + limit]
|
||||
result = "".join(f"{i}|{line}" for i, line in enumerate(selected, start=offset))
|
||||
return _truncate(f"[{resolved} — {total} строк]\n{result}")
|
||||
except Exception as e:
|
||||
return f"Ошибка чтения: {e}"
|
||||
|
||||
|
||||
def tool_list_files(path: str = "", pattern: str = "", max_depth: int = 3) -> str:
|
||||
base = _resolve_path(path or ".")
|
||||
if not os.path.isdir(base):
|
||||
return f"Ошибка: директория не найдена — {base}"
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["find", base, "-maxdepth", str(max_depth),
|
||||
"-not", "-path", "*/.git/*", "-not", "-path", "*/.venv/*",
|
||||
"-not", "-path", "*/node_modules/*", "-not", "-path", "*/__pycache__/*"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
lines = result.stdout.strip().split("\n")
|
||||
if pattern:
|
||||
import fnmatch
|
||||
lines = [l for l in lines if fnmatch.fnmatch(os.path.basename(l), pattern)]
|
||||
lines.sort()
|
||||
return _truncate("\n".join(lines))
|
||||
except Exception as e:
|
||||
return f"Ошибка: {e}"
|
||||
|
||||
|
||||
def tool_search_in_files(query: str, path: str = "", file_pattern: str = "") -> str:
|
||||
base = _resolve_path(path or ".")
|
||||
cmd = ["grep", "-rn", "--color=never", "-E", query, base]
|
||||
if file_pattern:
|
||||
cmd.extend(["--include", file_pattern])
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||
if result.returncode == 1:
|
||||
return "Совпадений не найдено."
|
||||
return _truncate(result.stdout.strip())
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Таймаут поиска."
|
||||
except Exception as e:
|
||||
return f"Ошибка: {e}"
|
||||
|
||||
|
||||
def tool_web_search(query: str, max_results: int = 5) -> str:
|
||||
try:
|
||||
client = httpx.Client(timeout=15, follow_redirects=True)
|
||||
resp = client.get(
|
||||
"https://html.duckduckgo.com/html/", params={"q": query},
|
||||
headers={"User-Agent": "Mozilla/5.0"},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
results = []
|
||||
for match in re.finditer(
|
||||
r'<a rel="nofollow" class="result__a" href="([^"]+)">\s*(.*?)\s*</a>.*?'
|
||||
r'<a class="result__snippet".*?>(.*?)</a>',
|
||||
resp.text, re.DOTALL,
|
||||
):
|
||||
url = match.group(1)
|
||||
title = re.sub(r'<[^>]+>', '', match.group(2)).strip()
|
||||
snippet = re.sub(r'<[^>]+>', '', match.group(3)).strip()
|
||||
results.append(f"{title}\n {url}\n {snippet}")
|
||||
if len(results) >= max_results:
|
||||
break
|
||||
if not results:
|
||||
return "Результатов не найдено."
|
||||
return _truncate("\n\n".join(f"[{i+1}] {r}" for i, r in enumerate(results)))
|
||||
except Exception as e:
|
||||
return f"Ошибка поиска: {e}"
|
||||
|
||||
|
||||
def tool_web_fetch(url: str, max_length: int = 5000) -> str:
|
||||
try:
|
||||
client = httpx.Client(timeout=20, follow_redirects=True)
|
||||
resp = client.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
resp.raise_for_status()
|
||||
content_type = resp.headers.get("content-type", "")
|
||||
if "html" in content_type:
|
||||
text = re.sub(r'<script[^>]*>.*?</script>', '', resp.text, flags=re.DOTALL)
|
||||
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
|
||||
text = re.sub(r'<[^>]+>', ' ', text)
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
else:
|
||||
text = resp.text
|
||||
return _truncate(f"[{url} — {len(resp.text)} символов]\n{text}", max_length)
|
||||
except Exception as e:
|
||||
return f"Ошибка загрузки: {e}"
|
||||
|
||||
|
||||
def tool_write_file(path: str, content: str) -> str:
|
||||
resolved = _resolve_path(path)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(resolved), exist_ok=True)
|
||||
with open(resolved, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return f"Записано {len(content)} символов в {resolved}"
|
||||
except Exception as e:
|
||||
return f"Ошибка записи: {e}"
|
||||
|
||||
|
||||
def tool_run_command(command: str, timeout: int = 30) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command, shell=True, capture_output=True, text=True,
|
||||
timeout=timeout, cwd=WORKDIR,
|
||||
)
|
||||
output = result.stdout
|
||||
if result.stderr:
|
||||
output += f"\nSTDERR:\n{result.stderr}"
|
||||
if result.returncode != 0:
|
||||
output += f"\n[exit code: {result.returncode}]"
|
||||
return _truncate(output)
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"Таймаут ({timeout}с)"
|
||||
except Exception as e:
|
||||
return f"Ошибка: {e}"
|
||||
|
||||
|
||||
# ─── Dispatcher ───
|
||||
|
||||
TOOL_FUNCTIONS = {
|
||||
"read_file": tool_read_file,
|
||||
"list_files": tool_list_files,
|
||||
"search_in_files": tool_search_in_files,
|
||||
"web_search": tool_web_search,
|
||||
"web_fetch": tool_web_fetch,
|
||||
"write_file": tool_write_file,
|
||||
"run_command": tool_run_command,
|
||||
}
|
||||
|
||||
# Tool set definitions
|
||||
TOOL_SETS = {
|
||||
"all": list(TOOL_FUNCTIONS.keys()),
|
||||
"full": list(TOOL_FUNCTIONS.keys()),
|
||||
"files": ["read_file", "list_files", "search_in_files", "write_file"],
|
||||
"readonly": ["read_file", "list_files", "search_in_files"],
|
||||
"web": ["web_search", "web_fetch"],
|
||||
"none": [],
|
||||
"all": ["read_file", "list_files", "search_in_files", "web_search", "web_fetch", "write_file", "run_command"],
|
||||
"full": ["read_file", "list_files", "search_in_files", "web_search", "web_fetch", "write_file", "run_command"],
|
||||
"files": ["read_file", "list_files", "search_in_files", "write_file"],
|
||||
"readonly": ["read_file", "list_files", "search_in_files"],
|
||||
"web": ["web_search", "web_fetch"],
|
||||
"none": [],
|
||||
}
|
||||
|
||||
|
||||
def get_tool_schemas(tool_set: str = "all") -> list[dict]:
|
||||
if isinstance(tool_set, list):
|
||||
names = tool_set
|
||||
else:
|
||||
names = TOOL_SETS.get(tool_set, TOOL_SETS["all"])
|
||||
return [s for s in TOOL_SCHEMAS if s["function"]["name"] in names]
|
||||
def _truncate(text: str, max_len: int = 5000) -> str:
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return text[:max_len] + f"\n... [truncated, {len(text)} chars total]"
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Manages tool schemas and execution for a given working directory."""
|
||||
|
||||
def __init__(self, workdir: str = ".") -> None:
|
||||
self._workdir = os.path.abspath(workdir)
|
||||
|
||||
# ── Path resolution ──
|
||||
|
||||
def _resolve_path(self, path: str) -> str:
|
||||
if os.path.isabs(path):
|
||||
return path
|
||||
return os.path.normpath(os.path.join(self._workdir, path))
|
||||
|
||||
# ── Schema / dispatch ──
|
||||
|
||||
def get_tool_schemas(self, tool_set: str | list = "all") -> list[dict]:
|
||||
if isinstance(tool_set, list):
|
||||
names = tool_set
|
||||
else:
|
||||
names = TOOL_SETS.get(tool_set, TOOL_SETS["all"])
|
||||
return [s for s in TOOL_SCHEMAS if s["function"]["name"] in names]
|
||||
|
||||
def execute_tool(self, name: str, arguments: dict) -> str:
|
||||
method_map = {
|
||||
"read_file": self.tool_read_file,
|
||||
"list_files": self.tool_list_files,
|
||||
"search_in_files": self.tool_search_in_files,
|
||||
"web_search": self.tool_web_search,
|
||||
"web_fetch": self.tool_web_fetch,
|
||||
"write_file": self.tool_write_file,
|
||||
"run_command": self.tool_run_command,
|
||||
}
|
||||
func = method_map.get(name)
|
||||
if not func:
|
||||
return f"Unknown tool: {name}"
|
||||
try:
|
||||
return func(**arguments)
|
||||
except Exception as e:
|
||||
return f"Error executing {name}: {e}"
|
||||
|
||||
# ── Tool implementations ──
|
||||
|
||||
def tool_read_file(self, path: str, offset: int = 1, limit: int = 100) -> str:
|
||||
resolved = self._resolve_path(path)
|
||||
if not os.path.isfile(resolved):
|
||||
return f"Error: file not found — {resolved}"
|
||||
try:
|
||||
with open(resolved, encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
total = len(lines)
|
||||
selected = lines[offset - 1 : offset - 1 + limit]
|
||||
result = "".join(f"{i}|{line}" for i, line in enumerate(selected, start=offset))
|
||||
return _truncate(f"[{resolved} — {total} lines]\n{result}")
|
||||
except Exception as e:
|
||||
return f"Error reading file: {e}"
|
||||
|
||||
def tool_list_files(self, path: str = "", pattern: str = "", max_depth: int = 3) -> str:
|
||||
base = self._resolve_path(path or ".")
|
||||
if not os.path.isdir(base):
|
||||
return f"Error: directory not found — {base}"
|
||||
try:
|
||||
results: list[str] = []
|
||||
base_depth = base.count(os.sep)
|
||||
for dirpath, dirnames, filenames in os.walk(base):
|
||||
# Prune skipped directories in-place so os.walk skips them
|
||||
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
||||
# Check depth
|
||||
current_depth = dirpath.count(os.sep) - base_depth
|
||||
if current_depth >= max_depth:
|
||||
# Don't descend further, but still list this dir's files
|
||||
dirnames.clear()
|
||||
# Add directory itself
|
||||
rel_dir = os.path.relpath(dirpath, base)
|
||||
if rel_dir == ".":
|
||||
results.append(dirpath)
|
||||
else:
|
||||
results.append(dirpath)
|
||||
# Add files
|
||||
for fname in filenames:
|
||||
if pattern and not fnmatch.fnmatch(fname, pattern):
|
||||
continue
|
||||
results.append(os.path.join(dirpath, fname))
|
||||
results.sort()
|
||||
return _truncate("\n".join(results))
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def tool_search_in_files(self, query: str, path: str = "", file_pattern: str = "") -> str:
|
||||
base = self._resolve_path(path or ".")
|
||||
if not os.path.isdir(base):
|
||||
return f"Error: directory not found — {base}"
|
||||
try:
|
||||
regex = re.compile(query, re.IGNORECASE)
|
||||
except re.error as e:
|
||||
return f"Error: invalid regex — {e}"
|
||||
try:
|
||||
results: list[str] = []
|
||||
for dirpath, dirnames, filenames in os.walk(base):
|
||||
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
||||
for fname in filenames:
|
||||
if file_pattern and not fnmatch.fnmatch(fname, file_pattern):
|
||||
continue
|
||||
fpath = os.path.join(dirpath, fname)
|
||||
try:
|
||||
with open(fpath, encoding="utf-8", errors="replace") as f:
|
||||
for line_num, line in enumerate(f, start=1):
|
||||
if regex.search(line):
|
||||
results.append(f"{fpath}:{line_num}:{line.rstrip()}")
|
||||
if len(results) >= 200:
|
||||
return _truncate("\n".join(results))
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
if not results:
|
||||
return "No matches found."
|
||||
return _truncate("\n".join(results))
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def tool_web_search(self, query: str, max_results: int = 5) -> str:
|
||||
try:
|
||||
client = httpx.Client(timeout=15, follow_redirects=True)
|
||||
resp = client.get(
|
||||
"https://html.duckduckgo.com/html/",
|
||||
params={"q": query},
|
||||
headers={"User-Agent": "Mozilla/5.0"},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
results = []
|
||||
for match in re.finditer(
|
||||
r'<a rel="nofollow" class="result__a" href="([^"]+)">\s*(.*?)\s*</a>.*?'
|
||||
r'<a class="result__snippet".*?>(.*?)</a>',
|
||||
resp.text, re.DOTALL,
|
||||
):
|
||||
url = match.group(1)
|
||||
title = re.sub(r'<[^>]+>', '', match.group(2)).strip()
|
||||
snippet = re.sub(r'<[^>]+>', '', match.group(3)).strip()
|
||||
results.append(f"{title}\n {url}\n {snippet}")
|
||||
if len(results) >= max_results:
|
||||
break
|
||||
if not results:
|
||||
return "No results found."
|
||||
return _truncate("\n\n".join(f"[{i+1}] {r}" for i, r in enumerate(results)))
|
||||
except Exception as e:
|
||||
return f"Search error: {e}"
|
||||
|
||||
def tool_web_fetch(self, url: str, max_length: int = 5000) -> str:
|
||||
try:
|
||||
client = httpx.Client(timeout=20, follow_redirects=True)
|
||||
resp = client.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
resp.raise_for_status()
|
||||
content_type = resp.headers.get("content-type", "")
|
||||
if "html" in content_type:
|
||||
text = re.sub(r'<script[^>]*>.*?</script>', '', resp.text, flags=re.DOTALL)
|
||||
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
|
||||
text = re.sub(r'<[^>]+>', ' ', text)
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
else:
|
||||
text = resp.text
|
||||
return _truncate(f"[{url} — {len(resp.text)} chars]\n{text}", max_length)
|
||||
except Exception as e:
|
||||
return f"Fetch error: {e}"
|
||||
|
||||
def tool_write_file(self, path: str, content: str) -> str:
|
||||
resolved = self._resolve_path(path)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(resolved) or ".", exist_ok=True)
|
||||
with open(resolved, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return f"Wrote {len(content)} chars to {resolved}"
|
||||
except Exception as e:
|
||||
return f"Write error: {e}"
|
||||
|
||||
def tool_run_command(self, command: str, timeout: int = 30) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command, shell=True, capture_output=True, text=True,
|
||||
timeout=timeout, cwd=self._workdir,
|
||||
)
|
||||
output = result.stdout
|
||||
if result.stderr:
|
||||
output += f"\nSTDERR:\n{result.stderr}"
|
||||
if result.returncode != 0:
|
||||
output += f"\n[exit code: {result.returncode}]"
|
||||
return _truncate(output)
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"Timeout ({timeout}s)"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
# ─── Module-level backward-compatible wrappers ───
|
||||
|
||||
_default_registry: ToolRegistry | None = None
|
||||
|
||||
|
||||
def set_workdir(path: str) -> None:
|
||||
"""Set the working directory for module-level tool functions."""
|
||||
global _default_registry
|
||||
_default_registry = ToolRegistry(workdir=path)
|
||||
|
||||
|
||||
def _get_registry() -> ToolRegistry:
|
||||
global _default_registry
|
||||
if _default_registry is None:
|
||||
_default_registry = ToolRegistry()
|
||||
return _default_registry
|
||||
|
||||
|
||||
def get_tool_schemas(tool_set: str | list = "all") -> list[dict]:
|
||||
"""Return tool schemas for the given set name or explicit list."""
|
||||
return _get_registry().get_tool_schemas(tool_set)
|
||||
|
||||
|
||||
def execute_tool(name: str, arguments: dict) -> str:
|
||||
func = TOOL_FUNCTIONS.get(name)
|
||||
if not func:
|
||||
return f"Неизвестный инструмент: {name}"
|
||||
try:
|
||||
return func(**arguments)
|
||||
except Exception as e:
|
||||
return f"Ошибка выполнения {name}: {e}"
|
||||
"""Execute a tool by name with the given arguments."""
|
||||
return _get_registry().execute_tool(name, arguments)
|
||||
|
||||
|
||||
# Legacy dict-based dispatch (kept for any code that imports TOOL_FUNCTIONS)
|
||||
TOOL_FUNCTIONS = {
|
||||
"read_file": lambda **kw: _get_registry().tool_read_file(**kw),
|
||||
"list_files": lambda **kw: _get_registry().tool_list_files(**kw),
|
||||
"search_in_files": lambda **kw: _get_registry().tool_search_in_files(**kw),
|
||||
"web_search": lambda **kw: _get_registry().tool_web_search(**kw),
|
||||
"web_fetch": lambda **kw: _get_registry().tool_web_fetch(**kw),
|
||||
"write_file": lambda **kw: _get_registry().tool_write_file(**kw),
|
||||
"run_command": lambda **kw: _get_registry().tool_run_command(**kw),
|
||||
}
|
||||
Reference in New Issue
Block a user