Vineetiitg
feat(backend): integrate Redis workers, persistent model caching, Cohere V2 fallback, and 5x TTFT fast-path RAG
d816f3a | import asyncio | |
| import os | |
| import uuid | |
| from pathlib import Path | |
| from fastapi import Depends, FastAPI, File, Request, UploadFile | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse, StreamingResponse | |
| from pydantic import BaseModel | |
| from guardrails import Guard | |
| from langchain_core.prompts import PromptTemplate | |
| from langchain_openai import ChatOpenAI | |
| from app.auth.models import Token, UserContext | |
| from app.auth.security import require_admin, resolve_user, create_access_token, verify_password, USERS | |
| from fastapi.security import OAuth2PasswordRequestForm | |
| from datetime import timedelta | |
| from app.core.config import settings | |
| from app.core.dependencies import check_openrouter, check_qdrant | |
| from app.core.errors import CopilotError | |
| from app.core.logging import configure_logging, logger, request_id_var | |
| from app.core.queue import get_redis_client | |
| from app.engine.document_registry import load_registry | |
| from app.engine.ingestion import delete_indexed_document, ingest_documents, reset_index | |
| from app.engine.context_builder import build_context, format_sources | |
| import csv | |
| from app.engine.memory import add_session_message, get_session_history, list_user_sessions, delete_session, get_session_summary, list_all_sessions | |
| from app.engine.query_transform import condense_query | |
| from app.engine.retriever import retrieve_documents | |
| from app.engine.semantic_cache import get_cached_answer, set_cached_answer | |
| from app.graph.workflow import compile_workflow | |
| from app.guardrails.input import async_enforce_rate_limit, enforce_rate_limit, validate_query | |
| from app.guardrails.output import redact_sensitive_data | |
| from app.guardrails.validators import DetectPromptInjection | |
| from app.observability.metrics import RequestMetrics, log_request_metrics, timed_stage | |
| from app.tests.eval_rag import run_local_evaluation, REPORT_PATH | |
| configure_logging() | |
| if settings.LANGCHAIN_TRACING_V2 and settings.LANGCHAIN_API_KEY: | |
| os.environ["LANGCHAIN_TRACING_V2"] = "true" | |
| os.environ["LANGCHAIN_ENDPOINT"] = settings.LANGCHAIN_ENDPOINT | |
| os.environ["LANGCHAIN_API_KEY"] = settings.LANGCHAIN_API_KEY | |
| os.environ["LANGCHAIN_PROJECT"] = settings.LANGCHAIN_PROJECT | |
| logger.info(f"LangSmith tracing enabled for project: {settings.LANGCHAIN_PROJECT}") | |
| app = FastAPI(title=settings.PROJECT_NAME) | |
| async def startup_faq_prewarming(): | |
| try: | |
| csv_path = Path("datasets/golden_qa.csv") | |
| if csv_path.exists(): | |
| logger.info("PRE-WARMING SEMANTIC CACHE: Seeding FAQ entries from golden_qa.csv...") | |
| with open(csv_path, mode="r", encoding="utf-8") as f: | |
| reader = csv.DictReader(f) | |
| count = 0 | |
| for row in reader: | |
| question = row.get("question", "").strip() | |
| answer = row.get("expected_answer", "").strip() | |
| sources_raw = row.get("expected_sources", "").strip() | |
| if question and answer: | |
| sources = [{"doc_id": sources_raw, "source": sources_raw, "snippet": answer}] if sources_raw else [] | |
| await set_cached_answer(question, answer, sources, confidence=0.99) | |
| count += 1 | |
| logger.info(f"PRE-WARMING COMPLETE: Successfully seeded {count} FAQ entries into Redis vector cache.") | |
| except Exception as e: | |
| logger.warning(f"FAQ pre-warming failed or skipped: {e}") | |
| async def resolve_query_speculative(query: str, chat_history: list, summary: str): | |
| speculative_docs = [] | |
| if chat_history: | |
| condense_task = asyncio.create_task(condense_query(query, chat_history, summary=summary)) | |
| retrieval_task = asyncio.create_task(retrieve_documents(query, chat_history)) | |
| results = await asyncio.gather(condense_task, retrieval_task, return_exceptions=True) | |
| standalone_query = query if isinstance(results[0], Exception) else results[0] | |
| raw_docs = [] if isinstance(results[1], Exception) else results[1] | |
| if raw_docs: | |
| top_sim = max([d.metadata.get("similarity_score", 0.0) for d in raw_docs] + [0.0]) | |
| if top_sim >= 0.85: | |
| logger.info(f"SPECULATIVE RETRIEVAL HIT: Raw query '{query}' matched with top similarity {top_sim:.4f} >= 0.85!") | |
| speculative_docs = raw_docs | |
| else: | |
| standalone_query = await condense_query(query, chat_history, summary=summary) | |
| return standalone_query, speculative_docs | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def request_id_middleware(request: Request, call_next): | |
| request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())) | |
| token = request_id_var.set(request_id) | |
| try: | |
| response = await call_next(request) | |
| response.headers["X-Request-ID"] = request_id | |
| return response | |
| finally: | |
| request_id_var.reset(token) | |
| async def copilot_error_handler(request: Request, exc: CopilotError): | |
| return JSONResponse( | |
| status_code=exc.status_code, | |
| content={"detail": exc.message}, | |
| ) | |
| rag_agent = compile_workflow() | |
| input_guard = Guard().use(DetectPromptInjection, on_fail="exception") | |
| class ChatRequest(BaseModel): | |
| query: str | |
| chat_history: list[dict] = [] | |
| session_id: str | None = None | |
| class SourceCitation(BaseModel): | |
| source: str | |
| page: int | None = None | |
| chunk_id: str | None = None | |
| doc_id: str | None = None | |
| snippet: str | |
| class ChatResponse(BaseModel): | |
| query: str | |
| answer: str | |
| sources: list[SourceCitation] = [] | |
| confidence: float = 0.0 | |
| session_id: str = "default" | |
| class IngestionRequest(BaseModel): | |
| data_dir: str = "data/docs" | |
| force: bool = False | |
| class FeedbackRequest(BaseModel): | |
| query: str | |
| answer: str | |
| is_positive: bool = True | |
| comments: str | None = None | |
| class InterveneRequest(BaseModel): | |
| message: str | |
| role: str = "assistant" | |
| async def health_endpoint(): | |
| return {"status": "ok", "project": settings.PROJECT_NAME} | |
| async def ready_endpoint(): | |
| openrouter = check_openrouter() | |
| qdrant = check_qdrant() | |
| return { | |
| "ready": bool(openrouter.get("ok") and qdrant.get("ok")), | |
| "openrouter": openrouter, | |
| "qdrant": qdrant, | |
| } | |
| async def login_endpoint(form_data: OAuth2PasswordRequestForm = Depends()): | |
| user = USERS.get(form_data.username) | |
| if not user or not verify_password(form_data.password, user["password_hash"]): | |
| raise CopilotError("Incorrect username or password", status_code=401) | |
| access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) | |
| access_token = create_access_token( | |
| data={"sub": form_data.username, "role": user["role"]}, expires_delta=access_token_expires | |
| ) | |
| return {"access_token": access_token, "token_type": "bearer", "role": user["role"]} | |
| async def documents_endpoint(user: UserContext = Depends(resolve_user)): | |
| return {"documents": list(load_registry().values()), "role": user.role} | |
| async def admin_ingest_endpoint(request: IngestionRequest, user: UserContext = Depends(resolve_user)): | |
| require_admin(user) | |
| try: | |
| from app.core.queue import get_arq_pool | |
| pool = await get_arq_pool() | |
| job = await pool.enqueue_job("async_ingest_documents", data_dir=request.data_dir, force=request.force) | |
| return {"status": "ok", "message": "Ingestion task queued.", "job_id": job.job_id} | |
| except Exception as e: | |
| logger.warning(f"Arq enqueue failed ({e}), falling back to synchronous ingestion.") | |
| ingest_documents(data_dir=request.data_dir, force=request.force) | |
| return {"status": "ok", "message": "Ingestion completed synchronously."} | |
| async def admin_upload_endpoint(files: list[UploadFile] = File(...), user: UserContext = Depends(resolve_user)): | |
| require_admin(user) | |
| target_dir = Path(settings.DATA_DIR) | |
| target_dir.mkdir(parents=True, exist_ok=True) | |
| saved_files = [] | |
| for uploaded_file in files: | |
| filename = Path(uploaded_file.filename or "uploaded.txt").name | |
| target_path = target_dir / filename | |
| target_path.write_bytes(await uploaded_file.read()) | |
| saved_files.append(filename) | |
| return {"status": "ok", "saved_files": saved_files, "data_dir": str(target_dir)} | |
| async def admin_delete_document_endpoint(doc_id: str, user: UserContext = Depends(resolve_user)): | |
| require_admin(user) | |
| delete_indexed_document(doc_id) | |
| return {"status": "ok", "doc_id": doc_id} | |
| async def admin_reset_endpoint(user: UserContext = Depends(resolve_user)): | |
| require_admin(user) | |
| reset_index() | |
| return {"status": "ok", "message": "Index reset."} | |
| async def get_eval_endpoint(user: UserContext = Depends(resolve_user)): | |
| if REPORT_PATH.exists(): | |
| return {"status": "ok", "report": REPORT_PATH.read_text(encoding="utf-8")} | |
| return {"status": "missing", "report": "No evaluation report found yet. Click 'Run Evaluation Now' below to generate one."} | |
| async def post_eval_endpoint(user: UserContext = Depends(resolve_user)): | |
| require_admin(user) | |
| try: | |
| from app.core.queue import get_arq_pool | |
| pool = await get_arq_pool() | |
| job = await pool.enqueue_job("async_run_ragas_eval") | |
| return {"status": "ok", "message": "RAGAS evaluation task queued.", "job_id": job.job_id} | |
| except Exception as e: | |
| logger.warning(f"Arq enqueue failed ({e}), falling back to synchronous evaluation.") | |
| summary = await run_local_evaluation() | |
| report_content = REPORT_PATH.read_text(encoding="utf-8") if REPORT_PATH.exists() else "Report generated." | |
| return {"status": "ok", "summary": summary, "report": report_content} | |
| async def get_task_status_endpoint(job_id: str, user: UserContext = Depends(resolve_user)): | |
| try: | |
| from app.core.queue import get_arq_pool | |
| from arq.jobs import Job | |
| pool = await get_arq_pool() | |
| job = Job(job_id, redis=pool) | |
| status = await job.status() | |
| info = await job.info() if status else None | |
| return { | |
| "job_id": job_id, | |
| "status": status.value if status else "unknown", | |
| "result": getattr(info, "result", None) if info else None | |
| } | |
| except Exception as e: | |
| return {"job_id": job_id, "status": "error", "error": str(e)} | |
| async def chat_endpoint(request: ChatRequest, http_request: Request, user: UserContext = Depends(resolve_user)): | |
| metrics = RequestMetrics() | |
| await async_enforce_rate_limit(http_request.client.host if http_request.client else user.user_id) | |
| validate_query(request.query) | |
| if settings.ENABLE_GUARDRAILS: | |
| try: | |
| input_guard.validate(request.query) | |
| except Exception as e: | |
| raise CopilotError(str(getattr(e, "message", e)), status_code=400) | |
| session_id = request.session_id or str(uuid.uuid4()) | |
| chat_history = request.chat_history | |
| if not chat_history: | |
| chat_history = await get_session_history(user.user_id, session_id, limit=6) | |
| summary = await get_session_summary(user.user_id, session_id) | |
| standalone_query, speculative_docs = await resolve_query_speculative(request.query, chat_history, summary=summary) | |
| cached = await get_cached_answer(standalone_query) | |
| if cached: | |
| log_request_metrics(metrics, route="/chat (cache hit)", sources=len(cached.get("sources", [])), model="semantic_cache") | |
| await add_session_message(user.user_id, session_id, "user", request.query) | |
| await add_session_message(user.user_id, session_id, "assistant", cached["answer"], cached.get("sources", []), cached.get("confidence", 0.99)) | |
| return ChatResponse(query=request.query, answer=cached["answer"], sources=cached.get("sources", []), confidence=cached.get("confidence", 0.99), session_id=session_id) | |
| initial_state = {"question": standalone_query, "chat_history": chat_history, "summary": summary, "run_count": 0, "documents": speculative_docs} | |
| try: | |
| with timed_stage(metrics, "rag_workflow"): | |
| final_state = await rag_agent.ainvoke(initial_state) | |
| answer = redact_sensitive_data(final_state.get("generation", "Unable to compile answer.")) | |
| sources = final_state.get("sources", []) | |
| confidence = final_state.get("confidence_score", 0.0) | |
| if answer and sources: | |
| await set_cached_answer(standalone_query, answer, sources, confidence) | |
| await add_session_message(user.user_id, session_id, "user", request.query) | |
| await add_session_message(user.user_id, session_id, "assistant", answer, sources, confidence) | |
| except Exception as e: | |
| raise CopilotError(str(e), status_code=500) | |
| log_request_metrics(metrics, route="/chat", sources=len(sources), model=settings.LLM_MODEL) | |
| return ChatResponse(query=request.query, answer=answer, sources=sources, confidence=confidence, session_id=session_id) | |
| async def chat_feedback_endpoint(request: FeedbackRequest, user: UserContext = Depends(resolve_user)): | |
| logger.info("Feedback received", extra={"feedback": request.dict(), "user": user.user_id}) | |
| return {"status": "ok", "message": "Feedback recorded."} | |
| async def chat_stream_endpoint(request: ChatRequest, http_request: Request, user: UserContext = Depends(resolve_user)): | |
| await async_enforce_rate_limit(http_request.client.host if http_request.client else user.user_id) | |
| validate_query(request.query) | |
| if settings.ENABLE_GUARDRAILS: | |
| try: | |
| input_guard.validate(request.query) | |
| except Exception as e: | |
| raise CopilotError(str(getattr(e, "message", e)), status_code=400) | |
| session_id = request.session_id or str(uuid.uuid4()) | |
| chat_history = request.chat_history | |
| if not chat_history: | |
| chat_history = await get_session_history(user.user_id, session_id, limit=6) | |
| summary = await get_session_summary(user.user_id, session_id) | |
| standalone_query, speculative_docs = await resolve_query_speculative(request.query, chat_history, summary=summary) | |
| async def token_generator(): | |
| try: | |
| metrics = RequestMetrics() | |
| cached = await get_cached_answer(standalone_query) | |
| if cached: | |
| log_request_metrics(metrics, route="/chat/stream (cache hit)", sources=len(cached.get("sources", [])), model="semantic_cache") | |
| await add_session_message(user.user_id, session_id, "user", request.query) | |
| await add_session_message(user.user_id, session_id, "assistant", cached["answer"], cached.get("sources", []), cached.get("confidence", 0.99)) | |
| yield cached["answer"] | |
| return | |
| initial_state = {"question": standalone_query, "chat_history": chat_history, "summary": summary, "run_count": 0, "documents": speculative_docs} | |
| documents = [] | |
| sources_text = "" | |
| grounded_result = "yes" | |
| has_streamed_tokens = False | |
| streamed_text = "" | |
| with timed_stage(metrics, "rag_workflow_stream"): | |
| redis = await get_redis_client() | |
| async for event in rag_agent.astream_events(initial_state, version="v2"): | |
| kind = event["event"] | |
| node_name = event.get("metadata", {}).get("langgraph_node", "") | |
| if kind == "on_chat_model_stream" and node_name == "generate": | |
| if await redis.exists(f"session:{session_id}:terminate"): | |
| await redis.delete(f"session:{session_id}:terminate") | |
| yield "\n\n🛑 **[TERMINATED BY USER: Generation was stopped.]**" | |
| await add_session_message(user.user_id, session_id, "user", request.query) | |
| await add_session_message(user.user_id, session_id, "assistant", streamed_text + "\n\n🛑 [TERMINATED BY USER]", documents or [], 0.0) | |
| return | |
| chunk = event["data"]["chunk"] | |
| if chunk and getattr(chunk, "content", None): | |
| has_streamed_tokens = True | |
| streamed_text += chunk.content | |
| yield redact_sensitive_data(chunk.content) | |
| await asyncio.sleep(0.005) | |
| elif kind == "on_chain_end": | |
| output = event.get("data", {}).get("output") | |
| if isinstance(output, dict): | |
| if "documents" in output: | |
| documents = output["documents"] | |
| if "sources" in output: | |
| sources_text = output["sources"] | |
| if "grounded" in output: | |
| grounded_result = output["grounded"] | |
| if not has_streamed_tokens: | |
| if not documents: | |
| yield "I am sorry, no reliable matching documentation was found." | |
| else: | |
| yield "I am sorry, I could not generate a response based on the available documentation." | |
| return | |
| if str(grounded_result).lower() == "no": | |
| yield "\n\n🚨 **[CANCELLED: This response violated safety guidelines and has been retracted.]**" | |
| return | |
| if has_streamed_tokens and documents and str(grounded_result).lower() != "no": | |
| redacted_text = redact_sensitive_data(streamed_text) | |
| await set_cached_answer(standalone_query, redacted_text, documents, 0.98) | |
| await add_session_message(user.user_id, session_id, "user", request.query) | |
| await add_session_message(user.user_id, session_id, "assistant", redacted_text, documents, 0.98) | |
| log_request_metrics(metrics, route="/chat/stream", sources=len(documents), model=settings.LLM_MODEL) | |
| except Exception as exc: | |
| logger.error(f"Streaming error: {exc}", exc_info=True) | |
| if "429" in str(exc) or "Rate limit" in str(exc) or "free-models-per-day" in str(exc): | |
| yield "\n\n⚠️ **OpenRouter Daily Limit Reached:** You have exhausted the 50 free requests/day limit on OpenRouter. To continue using free models today without rate limits, add $1 (or 10 credits) to your OpenRouter account, or try again tomorrow when the limit resets." | |
| else: | |
| yield f"\n\n⚠️ **Error generating response:** {exc}" | |
| return StreamingResponse(token_generator(), media_type="text/event-stream") | |
| async def get_user_sessions_endpoint(user: UserContext = Depends(resolve_user)): | |
| sessions = await list_user_sessions(user.user_id) | |
| return {"sessions": sessions} | |
| async def get_session_messages_endpoint(session_id: str, user: UserContext = Depends(resolve_user)): | |
| messages = await get_session_history(user.user_id, session_id, limit=50) | |
| return {"session_id": session_id, "messages": messages} | |
| async def terminate_session_endpoint(session_id: str, user: UserContext = Depends(resolve_user)): | |
| redis = await get_redis_client() | |
| await redis.setex(f"session:{session_id}:terminate", 60, "1") | |
| return {"status": "terminated", "session_id": session_id} | |
| async def delete_session_endpoint(session_id: str, user: UserContext = Depends(resolve_user)): | |
| success = await delete_session(user.user_id, session_id) | |
| return {"status": "ok" if success else "error", "session_id": session_id} | |
| async def admin_list_sessions_endpoint(user: UserContext = Depends(resolve_user)): | |
| if user.role != "admin": | |
| raise CopilotError("Admin privileges required", status_code=403) | |
| sessions = await list_all_sessions(limit=50) | |
| return {"sessions": sessions} | |
| async def admin_get_session_messages_endpoint(user_id: str, session_id: str, user: UserContext = Depends(resolve_user)): | |
| if user.role != "admin": | |
| raise CopilotError("Admin privileges required", status_code=403) | |
| messages = await get_session_history(user_id, session_id, limit=50) | |
| summary = await get_session_summary(user_id, session_id) | |
| return {"session_id": session_id, "user_id": user_id, "messages": messages, "summary": summary} | |
| async def admin_intervene_message_endpoint(user_id: str, session_id: str, request: InterveneRequest, user: UserContext = Depends(resolve_user)): | |
| if user.role != "admin": | |
| raise CopilotError("Admin privileges required", status_code=403) | |
| await add_session_message(user_id, session_id, request.role, request.message) | |
| return {"status": "ok", "message": "Intervention message injected."} | |