legco_ai_assistant/backend/app/routers/query.py

78 lines
2.8 KiB
Python

"""Query router for RAG pipeline."""
import logging
from fastapi import APIRouter, HTTPException
from app.core.config import get_settings
from app.models.ingest import QueryRequest, QueryResponse, SourceMetadata
from app.services.llm_client import LLMClient
from app.services.query_decomposer import QueryDecomposer
from app.services.relevance_filter import RelevanceFilter
from app.services.rag import RAGService
logger = logging.getLogger(__name__)
router = APIRouter(tags=["query"])
NO_RESULTS_ANSWER = "I could not find any relevant information to answer your question."
@router.post("/query", response_model=QueryResponse)
async def query(request: QueryRequest):
"""Execute the 3-step RAG query pipeline.
Pipeline:
1. QueryDecomposer: Extract keywords from question
2. RAGService.retrieve: Get relevant chunks from ChromaDB
3. RelevanceFilter: Score and filter chunks by relevance
4. RAGService.generate_response: Generate bullet-point answer
"""
if not request.question or not request.question.strip():
raise HTTPException(status_code=400, detail="Question is required")
settings = get_settings()
try:
llm_client = LLMClient(settings)
logger.info("Query: %s", request.question)
decomposer = QueryDecomposer(llm_client)
keywords = decomposer.decompose(request.question)
logger.info("Keywords: %s", keywords)
rag = RAGService(llm_client=llm_client)
chunks = rag.retrieve(keywords, n_results=10)
if not chunks:
return QueryResponse(keywords=keywords, answer=NO_RESULTS_ANSWER, sources=[])
chunks_for_filter = [(text, meta) for text, meta, _dist in chunks]
relevance_filter = RelevanceFilter(llm_client)
filtered = relevance_filter.filter(request.question, chunks_for_filter, threshold=7.0)
if not filtered:
return QueryResponse(keywords=keywords, answer=NO_RESULTS_ANSWER, sources=[])
chunk_texts = [chunk for chunk, _meta in filtered]
chunk_metadata = [meta for _chunk, meta in filtered]
answer = rag.generate_response(request.question, chunk_texts, chunk_metadata)
logger.info("Answer generated: %d chars, %d sources", len(answer), len(filtered))
sources = [
SourceMetadata(
filename=meta.get("filename", "unknown"),
upload_date=meta.get("upload_date", ""),
content_summary=meta.get("content_summary", ""),
chunk_index=meta.get("chunk_index", 0),
)
for meta in chunk_metadata
]
return QueryResponse(keywords=keywords, answer=answer, sources=sources)
except HTTPException:
raise
except Exception as e:
logger.error("Query failed: %s", str(e))
raise HTTPException(status_code=500, detail=f"Query failed: {str(e)}")