Files
duocthu/apps/ai-service/bootstrap.py
T

112 lines
4.4 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
)
raise ValueError(
"Unknown ANSWER_PROVIDER. Supported values: disabled (default), "
"stub (local, no cloud), bedrock-claude"
)
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,
)
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