"""
Tools system for Meeting Room agents.
Provides file access and web capabilities via OpenAI function calling.
"""
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) ───
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Прочитать содержимое файла. Возвращает текст с номерами строк.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Путь к файлу"},
"offset": {"type": "integer", "description": "Начать с этой строки (1-indexed)", "default": 1},
"limit": {"type": "integer", "description": "Максимум строк", "default": 100},
},
"required": ["path"],
},
},
},
{
"type": "function",
"function": {
"name": "list_files",
"description": "Показать список файлов и директорий.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Директория для просмотра"},
"pattern": {"type": "string", "description": "Фильтр по имени (glob)"},
"max_depth": {"type": "integer", "description": "Глубина обхода", "default": 3},
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "search_in_files",
"description": "Поиск текста в файлах (regex).",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Поисковый запрос (regex)"},
"path": {"type": "string", "description": "Директория для поиска"},
"file_pattern": {"type": "string", "description": "Фильтр по расширению"},
},
"required": ["query"],
},
},
},
{
"type": "function",
"function": {
"name": "web_search",
"description": "Поиск в интернете. Возвращает заголовки, сниппеты и URL.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Поисковый запрос"},
"max_results": {"type": "integer", "description": "Максимум результатов", "default": 5},
},
"required": ["query"],
},
},
},
{
"type": "function",
"function": {
"name": "web_fetch",
"description": "Скачать содержимое веб-страницы.",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL для загрузки"},
"max_length": {"type": "integer", "description": "Максимум символов", "default": 5000},
},
"required": ["url"],
},
},
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Записать содержимое в файл.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Путь к файлу"},
"content": {"type": "string", "description": "Содержимое"},
},
"required": ["path", "content"],
},
},
},
{
"type": "function",
"function": {
"name": "run_command",
"description": "Выполнить shell-команду.",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "Команда"},
"timeout": {"type": "integer", "description": "Таймаут (сек)", "default": 30},
},
"required": ["command"],
},
},
},
]
# ─── 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'\s*(.*?)\s*.*?'
r'(.*?)',
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'', '', resp.text, flags=re.DOTALL)
text = re.sub(r'', '', 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_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": [],
}
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 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}"