Spaces:
Sleeping
Sleeping
Riley
feat: Major scraper enhancements - consistent format, better extraction, per-month costs
2ae7490 | """ | |
| QA endpoint for grant analyst queries. | |
| Clean implementation using the QA service layer with validated schemas. | |
| """ | |
| import asyncio | |
| import json | |
| from typing import Dict, Any | |
| from fastapi import APIRouter, HTTPException | |
| from fastapi.responses import StreamingResponse | |
| from uuid import uuid4 | |
| from src.logging.logger import get_logger | |
| from analyzer.models import QARequest, QAChunk | |
| from analyzer.qa_service import stream_answer, answer_question as qa_answer | |
| logger = get_logger() | |
| router = APIRouter(prefix="/qa", tags=["qa"]) | |
| async def qa_endpoint(request: QARequest) -> Dict[str, Any]: | |
| """ | |
| Answer a grant-related question (non-streaming). | |
| Uses the QA service layer with validated models and prompt injection hardening. | |
| Args: | |
| request: QARequest with query and optional filters | |
| Returns: | |
| Dict with answer, citations, and metadata | |
| Raises: | |
| HTTPException: On service failures | |
| """ | |
| # Ensure session ID is set | |
| if not request.session_id: | |
| request.session_id = str(uuid4()) | |
| try: | |
| # Use service layer | |
| result = qa_answer(request) | |
| # Log interaction | |
| logger.info( | |
| f"QA request processed: session={request.session_id}, " | |
| f"latency={result['latency_ms']}ms, success={result['success']}" | |
| ) | |
| return result | |
| except Exception as e: | |
| logger.error(f"QA endpoint error: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def qa_stream_endpoint(request: QARequest) -> StreamingResponse: | |
| """ | |
| Answer a grant-related question with Server-Sent Events streaming. | |
| Streams the LLM response as it generates for better UX. | |
| Args: | |
| request: QARequest with query and optional filters | |
| Returns: | |
| StreamingResponse with SSE-formatted NDJSON stream | |
| Raises: | |
| HTTPException: On service failures | |
| """ | |
| # Ensure session ID is set | |
| if not request.session_id: | |
| request.session_id = str(uuid4()) | |
| async def event_stream(): | |
| """Generate SSE event stream.""" | |
| try: | |
| # Stream from service layer | |
| for chunk in stream_answer(request): | |
| # Serialize chunk to NDJSON | |
| chunk_json = chunk.model_dump_json() | |
| yield f"data: {chunk_json}\n\n" | |
| # Allow other tasks to run | |
| await asyncio.sleep(0) | |
| except Exception as e: | |
| logger.error(f"Stream error: {e}", exc_info=True) | |
| # Send error chunk | |
| error_chunk = QAChunk(type="error", error=str(e)) | |
| yield f"data: {error_chunk.model_dump_json()}\n\n" | |
| return StreamingResponse( | |
| event_stream(), | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-cache", | |
| "Connection": "keep-alive", | |
| "X-Accel-Buffering": "no" # Disable nginx buffering | |
| } | |
| ) | |
| async def test_qa() -> Dict[str, str]: | |
| """Test endpoint to verify QA service is running.""" | |
| return { | |
| "status": "ok", | |
| "message": "QA service is running", | |
| "endpoint": "/qa" | |
| } | |