File size: 8,909 Bytes
ccf254d
eb9ec17
9a1a8d3
 
1f2b26b
9a1a8d3
 
6824985
8ca2283
ccf254d
9a1a8d3
 
8ca2283
c6f2a8c
 
ccf254d
6824985
8ca2283
9a1a8d3
ccf254d
 
6824985
 
5225cff
8ca2283
 
 
ccf254d
8ca2283
cdbf2f9
8ca2283
0fb8c62
 
 
0b0de3e
6e0eba1
0fb8c62
 
 
d128292
0fb8c62
 
ccf254d
0fb8c62
ccf254d
9a1a8d3
 
ccf254d
9a1a8d3
 
 
 
ccf254d
6824985
e616961
 
7ab6a0e
8ca2283
 
6824985
 
ccf254d
 
d50d870
9ab78f8
7f7ca2c
ccf254d
 
 
 
 
 
8ca2283
ccf254d
ee2203f
55f4347
9ce6e11
cb0dae8
ccf254d
 
 
 
cdbf2f9
7f7ca2c
 
 
 
cdbf2f9
7f7ca2c
cdbf2f9
7f7ca2c
cdbf2f9
7f7ca2c
 
 
cdbf2f9
7f7ca2c
 
cdbf2f9
7f7ca2c
 
3c767f2
7f7ca2c
 
8ca2283
cdbf2f9
6824985
57b098a
8ca2283
57b098a
 
c6f2a8c
57b098a
c6f2a8c
ccf254d
 
c6f2a8c
 
ccf254d
c6f2a8c
557efda
c6f2a8c
 
 
 
 
 
 
ccf254d
 
6824985
c6f2a8c
ccf254d
6824985
 
8ca2283
 
 
 
 
 
 
ccf254d
8ca2283
 
 
 
ccf254d
6824985
8ca2283
ccf254d
c6f2a8c
8ca2283
 
ce4334f
ccf254d
c6f2a8c
 
8ca2283
55f4347
8ca2283
 
ccf254d
 
 
 
 
 
9ab78f8
ccf254d
 
 
55f4347
9ce6e11
ccf254d
8ca2283
ccf254d
 
 
 
 
 
 
 
 
 
 
 
 
 
cdbf2f9
5cc8abb
 
 
 
 
 
ccf254d
 
c6f2a8c
ce4334f
c6f2a8c
 
8ca2283
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205

import os
import json
import re
import asyncio
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from operator import itemgetter

# import gradio as gr
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 concurrent.futures import ThreadPoolExecutor
from langchain_community.vectorstores import FAISS
from langchain.schema import Document
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_huggingface import HuggingFaceEmbeddings 
# from langchain_chroma import Chroma
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker 
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.prompts import ChatPromptTemplate

import os
from sentence_transformers import SentenceTransformer


MODEL_DIR = os.path.join("/tmp", "e5-large-v2")

if not os.path.exists(MODEL_DIR):
    print("πŸ“¦ Downloading SentenceTransformer model...")
    model = SentenceTransformer("intfloat/e5-large-v2")
    model.save(MODEL_DIR)
    print("βœ… Model saved at", MODEL_DIR)

    
# Load environment variables
load_dotenv()


ml_models = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    # This code runs ONCE when the application starts up
    print("πŸš€ Initializing models and prompt template...")

    try:
        GOOGLE_API_KEY = os.getenv("gemini_api_key")
        print("πŸ”‘ gemini_api_key:", "FOUND" if GOOGLE_API_KEY else "NOT FOUND")

        if not GOOGLE_API_KEY:
            raise RuntimeError("CRITICAL: Missing GOOGLE_API_KEY in environment secrets!")
    
        # Load models into the shared dictionary
        ml_models["embedder"] = HuggingFaceEmbeddings(
                                                      model_name="BAAI/bge-base-en-v1.5", #better but lil slower
                                                      # model_name="intfloat/e5-large-v2", #lil faster  but  dont know response is slow 
                                                      encode_kwargs={
                                                          "batch_size": 64,
                                                          # "normalize_embeddings": True
                                                      }
                                                    
    )
        cross_encoder_model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base")
        # cross_encoder_model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-large")
        ml_models["reranker_compressor"] = CrossEncoderReranker(model=cross_encoder_model, top_n=6)
        ml_models["llm"] = ChatGoogleGenerativeAI(
                               # model="gemini-1.5-pro",
                               model="gemini-2.0-flash",
                               api_key=GOOGLE_API_KEY,
                               temperature=0.1,
                               max_output_tokens=300
                           )
        ml_models["prompt_template"] = ChatPromptTemplate.from_template("""
**Role**: You are an expert decision maker Assistant in the domain such as insurance, legal compliance, human resources, and contract management using ONLY the provided context.
Do not use your own knowledge.
**Context**: 
{context}

**Query**: {full_query}

**Response**:

Example how to answer for thr query:
1. query:What is the grace period for premium payment under the National Parivar Mediclaim Plus Policy?
   response: A grace period of thirty days is provided for premium payment after the due date to renew or continue the policy without losing continuity benefits.

2. query: What is the waiting period for pre-existing diseases (PED) to be covered?
   response: There is a waiting period of thirty-six (36) months of continuous coverage from the first policy inception for pre-existing diseases and their direct complications to be covered.

3. query: Are the medical expenses for an organ donor covered under this policy?
   response : Yes, the policy indemnifies the medical expenses for the organ donor's hospitalization for the purpose of harvesting the organ, provided the organ is for an insured person and the donation complies with the Transplantation of Human Organs Act, 1994.

4. query: Does this policy cover maternity expenses, and what are the conditions?
   response: Yes, the policy covers maternity expenses, including childbirth and lawful medical termination of pregnancy. To be eligible, the female insured person must have been continuously covered for at least 24 months. The benefit is limited to two deliveries or terminations during the policy period.
"""
)
        print("βœ… Models and prompt loaded successfully!")
    except Exception as e:
        print("❌ Lifespan error:", str(e))
        raise e

    yield
    print("🧹 Cleaning up.")
    ml_models.clear()
# --- 2. FastAPI App Instance ---
# We pass the lifespan function to the FastAPI constructor
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:
        # Remove code fences and clean up
        content_cleaned = re.sub(r"^```json|```$", "", content.strip(), flags=re.IGNORECASE).strip()
        data = json.loads(content_cleaned)

        if isinstance(data, dict):
            if "decision" in data:
                decision = data.get("decision", "N/A").upper()
                amount = data.get("amount", "Not specified")
                justification = data.get("justification", "No justification provided.")
                return f"Decision: {decision}\nAmount: {amount}\nJustification: {justification}"

            elif "response" in data:
                return data["response"]

        return "The response was parsed but didn't match expected structure."

    except json.JSONDecodeError:
        return f"Unstructured response:\n{content.strip()}"

    except Exception as e:
        return f"An error occurred while processing the response: {str(e)}"


# --- 5. Main API Endpoint ---
@app.post("/api/v1/hackrx/run", response_model=RunResponse, dependencies=[Depends(verify_api_key)])
async def run_hackrx(req: RunRequest):
    chunks = load_and_chunk(str(req.documents))
    if not chunks:
        return JSONResponse({"error": "No documents could be processed."}, status_code=400)

    vectorstore = await FAISS.afrom_documents(
        documents=chunks, 
        embedding=ml_models["embedder"]
    )

    # dense_retriever = vectorstore.as_retriever(search_type="mmr",search_kwargs={"k": 8})
    dense_retriever = vectorstore.as_retriever(search_type="mmr",search_kwargs={"k": 8 ,"lambda_mult": 0.5})
    

    # Create retrievers using the pre-loaded models from our ml_models dictionary
    keyword_retriever = BM25Retriever.from_documents(chunks)
    keyword_retriever.k = 5
    # dense_retriever = Chroma.from_documents(documents=chunks, embedding=ml_models["embedder"]).as_retriever()
    ensemble_retriever = EnsembleRetriever(retrievers=[keyword_retriever, dense_retriever], weights=[0.35, 0.65])
    ### to make it faster we are now using our built reranker thats why commenting the code below
    compression_retriever = ContextualCompressionRetriever(
        base_retriever=ensemble_retriever, base_compressor=ml_models["reranker_compressor"]
    )

    
    # Define the RAG chain using pre-loaded components
    hybrid_rag_chain = (
        {"context": itemgetter("full_query") | compression_retriever, "full_query": itemgetter("full_query")}
        | ml_models["prompt_template"]
        | ml_models["llm"]
    )

    tasks = [hybrid_rag_chain.ainvoke({"full_query": q}) for q in req.questions]
    results = await asyncio.gather(*tasks)
    # answers = [parse_llm_response(result.content) for result in results]
    answers = []

    for msg in results:
        # Safely access the content field
        if hasattr(msg, "content"):
            answers.append(msg.content.strip())    

    return JSONResponse({"answers": answers}, status_code=200)

@app.get("/", include_in_schema=False)
def root():
    return {"message": "API is running. Go to /docs for documentation."}