Этап 7: долговременная память

- Факты о пользователе в SQLite, в системном промпте с номерами
- Инструменты remember / update_memory / forget, автоматическое запоминание отключается
- Цикл вызова инструментов со стримингом (до 5 кругов), проверка и приведение аргументов
- Откат без инструментов для моделей, которые их не поддерживают, с одним предупреждением
- Действия в журнале, вкладка «Память» в настройках
- Фейковый OpenAI-совместимый сервер для тестов, тесты полного цикла через Assistant

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nid
2026-09-17 04:27:16 +03:00
co-authored by Claude Opus 5
parent 542389c0f9
commit 84d8aaa848
18 changed files with 1168 additions and 37 deletions
+64
View File
@@ -0,0 +1,64 @@
from typing import Any
from agr_assistent.llm.tools import Tool, ToolError, ToolRegistry, ToolResult, validate_arguments
_SCHEMA = {
"type": "object",
"properties": {
"level": {"type": "integer"},
"ratio": {"type": "number"},
"loud": {"type": "boolean"},
"mode": {"type": "string", "enum": ["on", "off"]},
},
"required": ["level"],
}
def test_arguments_are_converted_from_strings() -> None:
arguments, errors = validate_arguments(
_SCHEMA, {"level": "30", "ratio": "0,5", "loud": "true", "mode": "on", "extra": 1}
)
assert errors == []
assert arguments == {"level": 30, "ratio": 0.5, "loud": True, "mode": "on"}
def test_invalid_arguments_are_reported() -> None:
_arguments, errors = validate_arguments(_SCHEMA, {"ratio": "много", "loud": 1, "mode": "auto"})
assert "не указан параметр level" in errors
assert any(error.startswith("ratio:") for error in errors)
assert any(error.startswith("loud:") for error in errors)
assert any("недопустимое значение 'auto'" in error for error in errors)
def _registry() -> ToolRegistry:
def handler(arguments: dict[str, Any]) -> ToolResult:
if arguments["level"] > 100:
raise ToolError("слишком громко")
if arguments["level"] < 0:
raise RuntimeError("сломалось")
return ToolResult(True, f"ok {arguments['level']}", "готово")
return ToolRegistry([Tool("volume", "Громкость", _SCHEMA, handler)])
def test_registry_executes_tool_and_exposes_schema() -> None:
registry = _registry()
assert registry.schemas()[0]["function"]["name"] == "volume"
assert registry.execute("volume", '{"level": 30}') == ToolResult(True, "ok 30", "готово")
def test_registry_turns_problems_into_failed_results() -> None:
registry = _registry()
assert not registry.execute("missing", "{}").ok
assert not registry.execute("volume", "{not json").ok
assert not registry.execute("volume", "[1]").ok
assert not registry.execute("volume", "{}").ok
assert registry.execute("volume", '{"level": 101}') == ToolResult(
False, "слишком громко", "слишком громко"
)
failed = registry.execute("volume", '{"level": -1}')
assert not failed.ok and "сломалось" in failed.content