88 lines
3.3 KiB
Python
88 lines
3.3 KiB
Python
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 RerankUnavailable(RuntimeError):
|
|
"""The reranker could not be reached.
|
|
|
|
Same fail-open contract as the other provider errors, but softer: losing the
|
|
reranker only means the candidates keep their original order, so the caller
|
|
catches this and proceeds rather than abstaining. Rerank is an ordering
|
|
improvement on the fallback, never a precondition for an answer.
|
|
"""
|
|
|
|
|
|
class Reranker(Protocol):
|
|
"""Reorders candidate texts by joint relevance to the query.
|
|
|
|
Returns indices into `documents`, most relevant first. A cross-encoder pass
|
|
that recovers precision the bi-encoder embedding cannot; applied only to the
|
|
similarity/overview fallback, never to the deterministic section route.
|
|
"""
|
|
|
|
def rerank(self, query: str, documents: list[str], top_n: int | None = None) -> list[int]: ...
|
|
|
|
|
|
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: ...
|
|
|
|
|
|
class SectionListRetriever(Protocol):
|
|
"""The per-drug section checklist Feature-List #4 needs, cheap because
|
|
`drug_id`/`section_key` are already-indexed Qdrant payload fields — no
|
|
new indexing. Separate from `SectionRetriever` (interface segregation):
|
|
this returns labels only, never chunk text."""
|
|
|
|
def list_sections(self, drug_id: str) -> list[tuple[str, str]]: ...
|