Wire the guarded conversational RAG answer layer end-to-end

This commit is contained in:
2026-08-05 14:33:13 +07:00
parent 834d9e51b0
commit ef08b4929e
127 changed files with 37921 additions and 169 deletions
+57
View File
@@ -0,0 +1,57 @@
from __future__ import annotations
from typing import Protocol
from .models import ParentDocument, SearchHit
class QueryEmbeddingUnavailable(RuntimeError):
"""The similarity route's embedding provider could not be reached.
Raised by an adapter and caught by the domain, which abstains. It exists so
a provider outage refuses to answer instead of returning a 500: an
unreachable embedder means the question was never actually searched, and an
error page hides that from the caller just as effectively as a wrong answer
would. The domain catches this without importing any SDK.
"""
class AnswerGenerationUnavailable(RuntimeError):
"""The answer generator could not be reached.
Same contract as `QueryEmbeddingUnavailable`: the adapter translates its
SDK's failure into this, and the domain degrades to the extractive answer
rather than returning an error. Generation is a presentation improvement
over quoting the source; losing it must never lose the answer.
"""
class AnswerGenerator(Protocol):
"""Rewrites retrieved evidence into prose. Never a source of facts.
Whatever it returns is checked by `rag.grounding.verify` before a caller
sees it, so this port carries no trust: an implementation that fabricates
a dose produces a discarded generation, not a wrong answer.
"""
def generate(self, system: str, user: str, schema: dict) -> str: ...
class Retriever(Protocol):
def search(self, query: str, drug_id: str, limit: int) -> list[SearchHit]: ...
class SectionRetriever(Protocol):
"""Exact retrieval of one whole section, with no similarity involved.
Separate from `Retriever` so a store that cannot filter by payload is still
a valid `Retriever` (interface segregation). `find_by_section` must return
**every** part of the section: a partial contraindication list reads as a
complete one, which is worse than returning nothing.
"""
def find_by_section(self, drug_id: str, section_key: str) -> list[SearchHit]: ...
class ParentStore(Protocol):
def get(self, parent_id: str) -> ParentDocument | None: ...