"""Базовый интерфейс «мозга» (LLM) + результат запроса. История диалога — простой список {"role": "user"|"assistant", "text": str}, чтобы провайдеры и будущая машина состояний не зависели от формата API. """ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import List, Optional DialogHistory = List[dict] # [{"role": "user"|"assistant", "text": str}, ...] @dataclass class LlmResult: """Ответ LLM: сырой текст + очищенный для озвучки.""" question: str raw_text: str # как прислала модель (с разметкой, если была) cleaned_text: str # после clean_for_speech() — именно это озвучиваем generation_sec: float model: str = "" def __str__(self) -> str: return (f"[{self.model}] {self.generation_sec:.2f} c, " f"{len(self.cleaned_text)} симв.: {self.cleaned_text!r}") class LlmProvider(ABC): """Интерфейс провайдера LLM (сейчас OpenRouter, при надобности — локальный ollama).""" name: str = "base" context_block: str = "" # доп. контекст (долговременная память); подставляется в system @abstractmethod def ask(self, question: str, history: Optional[DialogHistory] = None) -> LlmResult: """Задать вопрос, получить ответ. history — предыдущие реплики диалога.""" def close(self) -> None: """Освободить ресурсы (по умолчанию — ничего)."""