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