"""Полный цикл запроса через Assistant: модель, инструменты памяти, откат без инструментов.""" import time from collections.abc import Callable from pathlib import Path import numpy as np import pytest from PySide6.QtCore import QCoreApplication from PySide6.QtWidgets import QApplication from agr_assistent.config import LLMConfig, ProviderConfig from agr_assistent.core.assistant import Assistant from agr_assistent.core.memory import MemoryStore from agr_assistent.core.speech import Speaker from tests.fake_llm import FakeLLMServer, Reply class _SilentEngine: sample_rate = 24000 def load(self) -> None: pass def synthesize(self, text: str) -> np.ndarray: return np.zeros(1, dtype=np.float32) class _SilentPlayer: def play(self, *args: object) -> None: pass def finish(self) -> None: pass def abort(self) -> None: pass def _wait_until(condition: Callable[[], bool], timeout: float = 5.0) -> None: deadline = time.monotonic() + timeout while not condition(): assert time.monotonic() < deadline, "условие не выполнилось вовремя" QCoreApplication.processEvents() time.sleep(0.005) @pytest.fixture def memory(tmp_path: Path) -> MemoryStore: store = MemoryStore(tmp_path / "memory.sqlite3") yield store store.close() def _assistant(server: FakeLLMServer, memory: MemoryStore, *, auto_save: bool = True) -> Assistant: config = LLMConfig( provider="fake", providers={"fake": ProviderConfig("fake", server.base_url, "key", "fake-model")}, system_prompt="Будь краток.", temperature=0.5, follow_up_seconds=0, timeout_seconds=10, ) speaker = Speaker(_SilentEngine(), _SilentPlayer(), enabled=False) return Assistant(config, speaker, None, memory=memory, memory_auto_save=auto_save) def test_remember_request_runs_tool_and_answers( qapp: QApplication, fake_llm: FakeLLMServer, memory: MemoryStore ) -> None: memory.add("Живёт в Казани", "user") fake_llm.replies += [ Reply(tool_calls=[("remember", '{"text": "Машина — Škoda Octavia", "requested_by_user": true}')]), Reply(text="Запомнил."), ] assistant = _assistant(fake_llm, memory) tools: list[tuple[str, bool]] = [] finished: list[str] = [] assistant.tool_executed.connect(lambda display, ok: tools.append((display, ok))) assistant.reply_finished.connect(finished.append) assistant.send("Запомни, что у меня Škoda Octavia") _wait_until(lambda: bool(finished)) assert [fact.text for fact in memory.facts()] == ["Живёт в Казани", "Машина — Škoda Octavia"] assert tools == [("Запомнил: Машина — Škoda Octavia", True)] assert finished == ["Запомнил."] first, second = fake_llm.requests system = first["messages"][0]["content"] assert "Живёт в Казани" in system and "requested_by_user=false" in system assert {tool["function"]["name"] for tool in first["tools"]} == { "remember", "update_memory", "forget", } # Второй запрос несёт вызов инструмента и его результат assert second["messages"][-2]["tool_calls"][0]["function"]["name"] == "remember" assert second["messages"][-1] == { "role": "tool", "tool_call_id": "call_0", "content": "Сохранено под номером 2", } def test_model_without_tools_falls_back_once( qapp: QApplication, fake_llm: FakeLLMServer, memory: MemoryStore ) -> None: fake_llm.replies += [ Reply(status=400, error="model does not support tools"), Reply(text="Привет!"), Reply(text="Снова привет!"), ] assistant = _assistant(fake_llm, memory) errors: list[str] = [] finished: list[str] = [] assistant.error_occurred.connect(errors.append) assistant.reply_finished.connect(finished.append) assistant.send("Привет") _wait_until(lambda: len(finished) == 1) assistant.send("Ещё раз привет") _wait_until(lambda: len(finished) == 2) assert finished == ["Привет!", "Снова привет!"] assert len(errors) == 1 and "не поддерживает инструменты" in errors[0] assert ["tools" in request for request in fake_llm.requests] == [True, False, False] def test_auto_save_can_be_disabled( qapp: QApplication, fake_llm: FakeLLMServer, memory: MemoryStore ) -> None: fake_llm.replies.append(Reply(text="Ок")) assistant = _assistant(fake_llm, memory, auto_save=False) finished: list[str] = [] assistant.reply_finished.connect(finished.append) assistant.send("Меня зовут Лео") _wait_until(lambda: bool(finished)) assert "requested_by_user=false" not in fake_llm.requests[0]["messages"][0]["content"]