"""Model-provider interfaces — the seam the AI assistant and match ranking plug into. LLM (text) and embeddings are *separate* abstractions: Anthropic offers no embeddings endpoint, so the two are configured independently (twelve-factor, CLAUDE.md #7) and a deployment may run one without the other. These providers are read-only text/vector producers. They MUST NOT mutate tree data — the assistant's writes go through a ChangeProposal a human approves (CLAUDE.md #1). Nothing here touches the database. """ from abc import ABC, abstractmethod class LLMProvider(ABC): """Text in, text out. Implementations wrap a chat/completion model.""" @abstractmethod async def complete(self, *, prompt: str, system: str | None = None) -> str: """Return the model's text response to a single user prompt.""" ... class EmbeddingProvider(ABC): """Text in, vectors out — for pgvector-backed match ranking.""" #: Dimensionality of the returned vectors (for the pgvector column). dimensions: int @abstractmethod async def embed(self, texts: list[str]) -> list[list[float]]: """Return one embedding vector per input text, in order.""" ... class ModelProviderNotConfigured(RuntimeError): """Raised when an AI capability is used but no provider is configured."""