"""Загруженные команды: файл commands.yaml, перезагрузка при изменении, инструменты для модели.""" from __future__ import annotations import logging from importlib import resources from pathlib import Path from typing import Any from PySide6.QtCore import QFileSystemWatcher, QObject, QTimer, Signal from agr_assistent.commands import actions from agr_assistent.commands.model import Command, load_commands from agr_assistent.llm.tools import Tool, ToolResult log = logging.getLogger(__name__) COMMANDS_FILE_NAME = "commands.yaml" # Редакторы сохраняют файл в несколько приёмов — перечитываем после паузы _RELOAD_DELAY_MS = 300 def default_commands_text() -> str: return ( resources.files("agr_assistent").joinpath("default_commands.yaml").read_text(encoding="utf-8") ) def command_tool(command: Command) -> Tool: def handler(arguments: dict[str, Any]) -> ToolResult: outcome = actions.execute(command, arguments) return ToolResult(outcome.ok, outcome.content, outcome.display) return Tool( name=command.name, description=command.description, parameters=command.schema(), handler=handler, confirm=command.confirm, describe=command.summary, ) class CommandCatalog(QObject): changed = Signal() def __init__(self, path: Path, parent: QObject | None = None) -> None: super().__init__(parent) self._path = path self._commands: list[Command] = [] self._errors: list[str] = [] self._reload_timer = QTimer(self) self._reload_timer.setSingleShot(True) self._reload_timer.setInterval(_RELOAD_DELAY_MS) self._reload_timer.timeout.connect(self.reload) self._watcher = QFileSystemWatcher(self) self._watcher.fileChanged.connect(lambda _path: self._reload_timer.start()) self.reload() @property def path(self) -> Path: return self._path @property def commands(self) -> list[Command]: return self._commands @property def errors(self) -> list[str]: return self._errors def ensure_file(self) -> None: """Создаёт commands.yaml с примерами, если его ещё нет.""" if not self._path.exists(): self._path.parent.mkdir(parents=True, exist_ok=True) self._path.write_text(default_commands_text(), encoding="utf-8") log.info("Создан файл команд: %s", self._path) self.reload() def tools(self) -> list[Tool]: return [command_tool(command) for command in self._commands] def reload(self) -> None: self._commands, self._errors = load_commands(self._path) # После сохранения «заменой файла» наблюдение слетает — ставим заново if self._path.exists() and str(self._path) not in self._watcher.files(): self._watcher.addPath(str(self._path)) log.info("Команды: %d загружено, %d ошибок", len(self._commands), len(self._errors)) for error in self._errors: log.warning(error) self.changed.emit()