"""Shared test fakes (no network, deterministic).""" from __future__ import annotations from typing import Any from app.config import Settings from app.rag.llm import LLMError class FakeEmbedder: """Duck-typed stand-in for :class:`app.rag.llm.LLMClient` (see the ``Embedder`` protocol in :mod:`app.rag.importer`). Returns deterministic vectors of *dim* dimensions; records every call so tests can assert batching behaviour. ``chat`` is the deterministic ``lite``-model stand-in (phase 30): it returns ``"Summary of "`` and raises :class:`LLMError` when the content contains the sentinel word ``SUMMARY-BLOWUP`` (drives the importer's fail-soft summary path). ``content`` may be a string or a phase-122 multimodal part list (``dict[str, Any]`` messages, the ``LLMClient.chat`` shape) — the text-summary body above is string-only; a subclass handling the multimodal image description (task 03's vision mock) overrides ``chat``. """ def __init__(self, dim: int = 768) -> None: self.dim = dim self.settings = Settings(_env_file=None) # pyright: ignore[reportCallIssue] self.embed_batches = 0 self.calls: list[list[str]] = [] self.chat_calls: list[list[dict[str, str]]] = [] async def embed(self, texts: list[str]) -> list[list[float]]: self.calls.append(list(texts)) self.embed_batches += 1 return [[0.01 * (i % 97) for i in range(self.dim)] for _ in texts] async def embed_one(self, text: str) -> list[float]: """The retrieval/probe convenience path — delegates to :meth:`embed` (satisfies the phase-41 pre-sync model probe).""" (vec,) = await self.embed([text]) return vec async def chat( self, messages: list[dict[str, Any]], model: str | None = None ) -> str: self.chat_calls.append(list(messages)) user = next((m["content"] for m in messages if m.get("role") == "user"), "") if "SUMMARY-BLOWUP" in user: raise LLMError("simulated lite-model failure (SUMMARY-BLOWUP sentinel)") first = user.split() return "Summary of " + (first[0] if first else "")