Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import re | |
| import asyncio | |
| import time | |
| import numpy as np | |
| from cachetools import TTLCache | |
| from contextlib import asynccontextmanager | |
| from dotenv import load_dotenv | |
| from operator import itemgetter | |
| from fastapi import FastAPI, Depends, HTTPException, Header | |
| from fastapi.responses import JSONResponse | |
| from utils.DocsLoader import load_and_chunk | |
| from utils.Schemas import RunRequest, RunResponse | |
| from langchain_community.vectorstores import FAISS | |
| from langchain_google_genai import ChatGoogleGenerativeAI | |
| from langchain_huggingface import HuggingFaceEmbeddings | |
| from langchain_community.retrievers import BM25Retriever | |
| from langchain.retrievers import EnsembleRetriever | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| from langchain.prompts import PromptTemplate | |
| # Load environment variables | |
| load_dotenv() | |
| # Cache setup | |
| document_cache = TTLCache(maxsize=5, ttl=300) | |
| cache_lock = asyncio.Lock() | |
| # --- 1. Lifespan Event Handler --- | |
| ml_models = {} | |
| async def lifespan(app: FastAPI): | |
| print("🚀 Initializing models and prompt template...") | |
| try: | |
| GOOGLE_API_KEY = os.getenv("gemini_api_key") | |
| if not GOOGLE_API_KEY: | |
| raise RuntimeError("CRITICAL: Missing GOOGLE_API_KEY in environment secrets!") | |
| # Optimized embedding model with batching | |
| ml_models["embedder"] = HuggingFaceEmbeddings( | |
| model_name="BAAI/bge-base-en-v1.5", | |
| encode_kwargs={ | |
| 'batch_size': 64, | |
| 'show_progress_bar': False | |
| } | |
| ) | |
| # Faster LLM with constrained output | |
| ml_models["llm"] = ChatGoogleGenerativeAI( | |
| model="gemini-1.5-pro", | |
| api_key=GOOGLE_API_KEY, | |
| temperature=0.1, | |
| max_output_tokens=300 | |
| ) | |
| # Improved prompt template | |
| ml_models["prompt_template"] = PromptTemplate.from_template(""" | |
| **Role**: Insurance Policy Expert | |
| **Context**: | |
| {context} | |
| **Query**: {full_query} | |
| **Instructions**: | |
| 1. If query contains age/gender/procedure/location/duration: | |
| - Output ONLY JSON: {{"decision":"approved/rejected","amount":"₹X","justification":"Clause reference"}} | |
| 2. Else: Provide concise answer | |
| 3. NEVER mention document sources | |
| 4. If unsure, respond: "Insufficient information" | |
| **Response**: | |
| """) | |
| print("✅ Models and prompt loaded successfully!") | |
| except Exception as e: | |
| print(f"❌ Lifespan error: {str(e)}") | |
| raise e | |
| yield | |
| print("🧹 Cleaning up.") | |
| ml_models.clear() | |
| # --- 2. FastAPI App Instance --- | |
| app = FastAPI(title="HackRX RAG Server", lifespan=lifespan) | |
| # --- 3. API Key Verification --- | |
| TEAM_API_KEY = os.getenv("TEAM_API_KEY") | |
| def verify_api_key(authorization: str = Header(...)): | |
| if not authorization.startswith("Bearer "): | |
| raise HTTPException(status_code=401, detail="Invalid Authorization header format") | |
| token = authorization.split("Bearer ")[1] | |
| if token != TEAM_API_KEY: | |
| raise HTTPException(status_code=403, detail="Invalid or missing API key") | |
| # --- 4. Parsing Helper --- | |
| def parse_llm_response(content: str) -> str: | |
| try: | |
| # Clean JSON response | |
| content_cleaned = re.sub(r"^```json|```$", "", content.strip(), flags=re.IGNORECASE).strip() | |
| data = json.loads(content_cleaned) | |
| if "decision" in data: | |
| return ( | |
| f"Decision: {data.get('decision', 'N/A').upper()}\n" | |
| f"Amount: {data.get('amount', 'Not specified')}\n" | |
| f"Justification: {data.get('justification', 'No justification provided')}" | |
| ) | |
| elif "response" in data: | |
| return data["response"] | |
| return "Response format error" | |
| except json.JSONDecodeError: | |
| return content.strip() | |
| except Exception as e: | |
| return f"Response processing error: {str(e)}" | |
| # --- 5. Retrieval Optimization --- | |
| async def get_relevant_docs(question: str, vectorstore: FAISS, keyword_retriever: BM25Retriever): | |
| # Parallel retrieval | |
| dense_docs, sparse_docs = await asyncio.gather( | |
| vectorstore.asimilarity_search(question, k=6), | |
| asyncio.get_event_loop().run_in_executor( | |
| None, | |
| keyword_retriever.get_relevant_documents, | |
| question | |
| ) | |
| ) | |
| # Combine and deduplicate | |
| all_docs = dense_docs + sparse_docs | |
| unique_docs = {doc.page_content: doc for doc in all_docs}.values() | |
| # Fast reranking | |
| query_embedding = ml_models["embedder"].embed_query(question) | |
| doc_texts = [doc.page_content for doc in unique_docs] | |
| doc_embeddings = ml_models["embedder"].embed_documents(doc_texts) | |
| similarities = cosine_similarity([query_embedding], doc_embeddings)[0] | |
| sorted_indices = np.argsort(similarities)[::-1][:5] # Top 5 | |
| return [list(unique_docs)[i] for i in sorted_indices] | |
| # --- 6. Main API Endpoint --- | |
| async def run_hackrx(req: RunRequest): | |
| start_time = time.time() | |
| # Cache document processing | |
| async with cache_lock: | |
| if req.documents in document_cache: | |
| print("♻️ Using cached document") | |
| chunks = document_cache[req.documents] | |
| else: | |
| chunks = load_and_chunk(str(req.documents)) | |
| document_cache[req.documents] = chunks | |
| if not chunks: | |
| return JSONResponse({"error": "No documents processed"}, status_code=400) | |
| # Create vector store and retrievers | |
| vectorstore = await FAISS.afrom_documents(chunks, ml_models["embedder"]) | |
| keyword_retriever = BM25Retriever.from_documents(chunks) | |
| keyword_retriever.k = 6 | |
| # Process questions in parallel | |
| async def process_question(q: str): | |
| relevant_docs = await get_relevant_docs(q, vectorstore, keyword_retriever) | |
| context = "\n".join([d.page_content for d in relevant_docs]) | |
| # Generate response | |
| prompt = ml_models["prompt_template"].format_prompt( | |
| full_query=q, | |
| context=context | |
| ) | |
| result = await ml_models["llm"].ainvoke(prompt) | |
| return parse_llm_response(result.content) | |
| answers = await asyncio.gather(*(process_question(q) for q in req.questions)) | |
| # Performance logging | |
| proc_time = time.time() - start_time | |
| print(f"⏱️ Processed {len(req.questions)} questions in {proc_time:.2f}s") | |
| return JSONResponse({"answers": answers}, status_code=200) | |
| def root(): | |
| return {"message": "HackRX API operational. Use /api/v1/hackrx/run"} |