"""LLM provider interface. Every analysis task (app/prompts/*.py) is written against this Protocol, never against a specific vendor SDK - swapping `LLM_PROVIDER` changes which class `get_llm_provider()` returns and nothing else has to change. `generate_structured` is the primary method: it always returns a validated instance of the caller's Pydantic response model, never raw text, so a malformed model response can never propagate un-typed data into the rest of the app (see `LLMResponseError` / the repair loop in anthropic_provider.py). """ from __future__ import annotations from typing import Protocol from pydantic import BaseModel class LLMResponseError(Exception): """Raised when a provider can't produce a response matching the requested schema, even after any repair attempts.""" class LLMProvider(Protocol): provider_name: str async def generate_structured[T: BaseModel]( self, system_prompt: str, user_prompt: str, response_model: type[T], ) -> T: ... async def generate_text(self, system_prompt: str, user_prompt: str) -> str: ...