Этап 8: настраиваемые команды
- commands.yaml с рабочими примерами: медиа, громкость, поиск, папки, блокировка; перечитывается автоматически, ошибки не ломают остальные команды - Действия run / open / http / keys; вывод и ответы возвращаются модели - Вызов через модель (tool calling) и мгновенно по точным фразам, в том числе с параметрами - Подтверждение «да/нет» для опасных команд, после голосового вопроса микрофон включается сам - Безопасность: запуск без оболочки, защита аргументов cmd/PowerShell/.bat, переменные окружения раскрываются только в шаблоне - Вкладка «Команды» в настройках - Тесты разбора, действий, фраз и полных сценариев через Assistant Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
84d8aaa848
commit
04029f3750
@@ -0,0 +1,93 @@
|
||||
"""Загруженные команды: файл 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()
|
||||
Reference in New Issue
Block a user