File size: 7,864 Bytes
ce4334f
eb9ec17
9a1a8d3
 
1f2b26b
9a1a8d3
 
 
 
3e691b4
9a1a8d3
 
 
c6f2a8c
 
 
62da1fe
556f955
da7bc60
9a1a8d3
c6f2a8c
556f955
9a1a8d3
 
 
c6f2a8c
9a1a8d3
ce4334f
9a1a8d3
 
 
 
c6f2a8c
 
9a1a8d3
 
 
 
c6f2a8c
 
e616961
 
 
 
 
 
 
eb9ec17
57b098a
 
7a995b2
 
57b098a
 
 
c6f2a8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57b098a
 
 
 
 
c6f2a8c
57b098a
c6f2a8c
 
 
 
 
 
 
557efda
c6f2a8c
 
 
 
7a995b2
 
c6f2a8c
 
 
ce4334f
c6f2a8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce4334f
c6f2a8c
 
 
0b810e6
 
 
c6f2a8c
da7bc60
 
62da1fe
556f955
0b810e6
62da1fe
 
da7bc60
 
 
 
c6f2a8c
0b810e6
62da1fe
da7bc60
 
eb9ec17
c6f2a8c
 
 
 
 
 
 
 
 
 
eb9ec17
1f2b26b
 
 
 
 
 
 
 
 
 
 
 
 
c6f2a8c
 
ce4334f
c6f2a8c
 
df5d943
 
 
5ec7fa4
 
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

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

# Make sure you have these files in a 'utils' folder
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 # Correct new import
# from langchain_chroma import Chroma
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.prompts import PromptTemplate


# Load environment variables
load_dotenv()

# --- 1. Lifespan Event Handler (The New, Correct Way) ---
# This dictionary will hold our loaded models
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")
        # 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=5)
        ml_models["llm"] = ChatGoogleGenerativeAI(model="gemini-1.5-flash", api_key=GOOGLE_API_KEY)
        ml_models["prompt_template"] = PromptTemplate.from_template(
         """
        You are an expert decision maker Assistant in the domain such as insurance, legal compliance, human resources, and contract management.

The customer has submitted a query. If the query has details about age , gender ,procedure ,location , policy duration, then parse it and understand the query properly
If not, the raw question is provided instead:
- Query: {full_query}

We retrieved the following policy clauses and rules relevant to this case:
{context}

### Task:
If the details about age , gender , procedure , location , policy duration are available, do all of the following:
1. Decide whether the procedure is covered.
2. Estimate the claimable amount.
3. Justify with the relevant clause.
and answer the query  precisely as insurance agent.

Otherwise, answer the question concisely and clearly using the retrieved context.

### Output format:
If query involes age , gender , procedure , location , policy duration answer like below:
{{
  "decision": "approved / rejected",
  "amount": "INR amount or null",
  "justification": "Refer to specific clause"
  Make it a perfect and concise.
}}
Else:
{{
  "response": "Concise natural language answer"
  Make it a perfect and concise.
}}

##NOTE : Do  not mention document id or its page number just mention clauses if applicable .
        """
    )
        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]
    # print(token)
    # print(TEAM_API_KEY)
    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)

        
    ####code for parallel####################################################################################

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

    dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
    

    # Create retrievers using the pre-loaded models from our ml_models dictionary
    keyword_retriever = BM25Retriever.from_documents(chunks)
    keyword_retriever.k = 3
    # dense_retriever = Chroma.from_documents(documents=chunks, embedding=ml_models["embedder"]).as_retriever()
    ensemble_retriever = EnsembleRetriever(retrievers=[keyword_retriever, dense_retriever], weights=[0.4, 0.65])
    
    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"]
    )
    
    # answers = []
    # for q in req.questions:
    #     try:
    #         result = await hybrid_rag_chain.ainvoke({"full_query": q})
    #         parsed = parse_llm_response(result.content)
    #         answers.append(parsed)
    #     except Exception as e:
    #         return JSONResponse({"error": str(e)}, status_code=500)
    tasks = [hybrid_rag_chain.ainvoke({"full_query": q}) for q in req.questions]
    results = await asyncio.gather(*tasks)
    
    # Extract the content from each result and parse it
    answers = [parse_llm_response(result.content) for result in results]

    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."}


# def dummy_gradio(): return "✅ API running!"
# gr.Interface(fn=dummy_gradio, inputs=[], outputs="text").launch(server_name="0.0.0.0", server_port=7860)