""" 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}"