129 lines
5.1 KiB
Python
129 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
from qdrant_client import QdrantClient
|
|
|
|
from adapters.embedding import (
|
|
BedrockCohereQueryEmbedder,
|
|
LocalHashQueryEmbedder,
|
|
SectionOnlyQueryEmbedder,
|
|
)
|
|
from adapters.postgres import PostgresTraceRepository
|
|
from adapters.qdrant import QdrantParentStore, QdrantRetriever
|
|
from config import Settings
|
|
from rag.answer import GroundedAnswerService
|
|
from rag.artifacts import load_aliases
|
|
from rag.conversation import DeterministicSummariser, InMemoryConversationStore
|
|
from rag.conversational import ConversationalLoopService
|
|
from rag.metrics import NullMetrics
|
|
from rag.routing import CatalogDrugResolver, QueryRoutingService
|
|
from rag.sections import SectionResolver
|
|
from rag.service import EvidencePolicy, RetrievalService
|
|
|
|
|
|
def _build_metrics(settings: Settings):
|
|
"""A Prometheus exporter, or None when the package or the flag is absent.
|
|
|
|
Missing `prometheus_client` degrades to no metrics rather than to a service
|
|
that will not start: observability is not a precondition for answering.
|
|
"""
|
|
if not settings.metrics_enabled:
|
|
return None
|
|
try:
|
|
from adapters.prometheus import PrometheusMetrics
|
|
|
|
return PrometheusMetrics()
|
|
except ImportError:
|
|
return None
|
|
|
|
|
|
def _build_generator(settings: Settings):
|
|
if settings.answer_provider == "disabled":
|
|
return None
|
|
if settings.answer_provider == "stub":
|
|
from adapters.bedrock_claude import StubAnswerGenerator
|
|
|
|
return StubAnswerGenerator(
|
|
"Câu trả lời mẫu, không gọi nhà cung cấp nào. [1]"
|
|
)
|
|
if settings.answer_provider == "bedrock-claude":
|
|
from adapters.bedrock_claude import BedrockClaudeAnswerGenerator
|
|
|
|
return BedrockClaudeAnswerGenerator(
|
|
region=settings.aws_region, model_id=settings.answer_model_id
|
|
)
|
|
if settings.answer_provider == "bedrock-converse":
|
|
from adapters.bedrock_converse import BedrockConverseAnswerGenerator
|
|
|
|
return BedrockConverseAnswerGenerator(
|
|
region=settings.aws_region, model_id=settings.answer_model_id
|
|
)
|
|
raise ValueError(
|
|
"Unknown ANSWER_PROVIDER. Supported values: disabled (default), "
|
|
"stub (local, no cloud), bedrock-claude, bedrock-converse"
|
|
)
|
|
|
|
|
|
def _build_reranker(settings: Settings):
|
|
"""A Cohere reranker, or None when disabled. Only used on the similarity /
|
|
overview fallback; the section route never reranks."""
|
|
if not settings.rerank_enabled:
|
|
return None
|
|
from adapters.bedrock_converse import BedrockCohereReranker
|
|
|
|
return BedrockCohereReranker(region=settings.aws_region)
|
|
|
|
|
|
def build_runtime(settings: Settings):
|
|
metrics = _build_metrics(settings)
|
|
if settings.embedding_provider == "disabled":
|
|
return None, None, PostgresTraceRepository(settings.postgres_dsn), metrics
|
|
if settings.embedding_provider not in ("section-only", "local-smoke", "cohere-v4"):
|
|
raise ValueError(
|
|
"No production query embedder is configured. Supported values: "
|
|
"EMBEDDING_PROVIDER=section-only (default; section route only), "
|
|
"local-smoke (plumbing only) or cohere-v4"
|
|
)
|
|
|
|
client = QdrantClient(
|
|
url=settings.qdrant_url,
|
|
api_key=settings.qdrant_api_key,
|
|
timeout=30,
|
|
)
|
|
# A collection built with one model and queried with another returns hits
|
|
# and raises nothing; the results are just meaningless. Keep this in step
|
|
# with `model_id` in the collection's manifest.
|
|
if settings.embedding_provider == "cohere-v4":
|
|
embedder = BedrockCohereQueryEmbedder(
|
|
settings.embedding_dimensions, region=settings.aws_region
|
|
)
|
|
elif settings.embedding_provider == "local-smoke":
|
|
embedder = LocalHashQueryEmbedder(settings.embedding_dimensions)
|
|
else:
|
|
embedder = SectionOnlyQueryEmbedder(settings.embedding_dimensions)
|
|
section_resolver = SectionResolver()
|
|
resolver = CatalogDrugResolver(load_aliases(settings.entities_path))
|
|
retrieval = RetrievalService(
|
|
QdrantRetriever(client, settings.qdrant_collection, embedder),
|
|
QdrantParentStore(client, settings.qdrant_collection),
|
|
EvidencePolicy(minimum_score=settings.evidence_minimum_score),
|
|
section_resolver=section_resolver,
|
|
reranker=_build_reranker(settings),
|
|
)
|
|
routing = QueryRoutingService(retrieval, resolver)
|
|
answers = GroundedAnswerService(
|
|
routing, generator=_build_generator(settings), metrics=metrics or NullMetrics()
|
|
)
|
|
# The conversational layer reuses the same resolvers and the safe answer
|
|
# engine, adding only turn understanding, follow-up inheritance and the
|
|
# clarify/refine loop around it. InMemory store for now; a Postgres-backed
|
|
# store is the persistence follow-up.
|
|
conversational = ConversationalLoopService(
|
|
answers=answers,
|
|
resolver=resolver,
|
|
section_resolver=section_resolver,
|
|
store=InMemoryConversationStore(),
|
|
summariser=DeterministicSummariser(),
|
|
metrics=metrics or NullMetrics(),
|
|
)
|
|
return answers, conversational, PostgresTraceRepository(settings.postgres_dsn), metrics
|