- commands.yaml с рабочими примерами: медиа, громкость, поиск, папки, блокировка; перечитывается автоматически, ошибки не ломают остальные команды - Действия run / open / http / keys; вывод и ответы возвращаются модели - Вызов через модель (tool calling) и мгновенно по точным фразам, в том числе с параметрами - Подтверждение «да/нет» для опасных команд, после голосового вопроса микрофон включается сам - Безопасность: запуск без оболочки, защита аргументов cmd/PowerShell/.bat, переменные окружения раскрываются только в шаблоне - Вкладка «Команды» в настройках - Тесты разбора, действий, фраз и полных сценариев через Assistant Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
"""Загруженные команды: файл 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()
|