rohannsinghal commited on
Commit
396bcd4
·
1 Parent(s): d1a7dac

removed existing pipeline and created new retriever and chain

Browse files
Files changed (2) hide show
  1. app/main_api.py +70 -203
  2. requirements.txt +1 -1
app/main_api.py CHANGED
@@ -180,197 +180,81 @@ class LangChainDocumentProcessor:
180
  logger.info(f"Processed {len(chunks)} chunks into {len(langchain_docs)} LangChain documents using {split_strategy} strategy")
181
  return langchain_docs
182
 
183
- # --- ADVANCED RETRIEVER WITH LANGCHAIN ---
184
- class AdvancedLangChainRetriever:
185
- """Advanced retriever using LangChain's sophisticated retrieval methods"""
 
 
186
 
187
  def __init__(self, vectorstore: Chroma, llm: GroqLLM):
188
  self.vectorstore = vectorstore
189
  self.llm = llm
 
190
 
191
- # Base retrievers
192
- self.similarity_retriever = vectorstore.as_retriever(
193
- search_type="similarity",
194
- search_kwargs={"k": 8}
195
- )
196
-
197
- self.mmr_retriever = vectorstore.as_retriever(
198
- search_type="mmr", # Maximum Marginal Relevance
199
- search_kwargs={"k": 8, "fetch_k": 20}
200
- )
201
 
202
- # Multi-query retriever for query expansion
203
- self.multi_query_retriever = MultiQueryRetriever.from_llm(
204
- retriever=self.similarity_retriever,
205
- llm=llm
206
  )
207
 
208
- # Ensemble retriever combining multiple strategies
209
- self.ensemble_retriever = EnsembleRetriever(
210
- retrievers=[self.similarity_retriever, self.mmr_retriever],
211
- weights=[0.6, 0.4] # Weight similarity higher than MMR
212
- )
213
 
214
- # Contextual compression retriever
215
- self.compression_retriever = ContextualCompressionRetriever(
216
- base_compressor=LLMChainFilter.from_llm(llm),
217
- base_retriever=self.ensemble_retriever
 
218
  )
219
 
220
- logger.info("Advanced LangChain retriever initialized with multiple retrieval strategies")
221
-
222
- async def retrieve_documents(self, query: str, strategy: str = "ensemble", k: int = 10) -> List[LangChainDocument]:
223
- """Retrieve documents using specified strategy"""
224
- try:
225
- if strategy == "similarity":
226
- docs = await asyncio.to_thread(
227
- self.similarity_retriever.get_relevant_documents, query
228
- )
229
- elif strategy == "mmr":
230
- docs = await asyncio.to_thread(
231
- self.mmr_retriever.get_relevant_documents, query
232
- )
233
- elif strategy == "multi_query":
234
- docs = await asyncio.to_thread(
235
- self.multi_query_retriever.get_relevant_documents, query
236
- )
237
- elif strategy == "ensemble":
238
- docs = await asyncio.to_thread(
239
- self.ensemble_retriever.get_relevant_documents, query
240
- )
241
- elif strategy == "compression":
242
- docs = await asyncio.to_thread(
243
- self.compression_retriever.get_relevant_documents, query
244
- )
245
- else:
246
- docs = await asyncio.to_thread(
247
- self.ensemble_retriever.get_relevant_documents, query
248
- )
249
-
250
- # Limit results
251
- docs = docs[:k]
252
-
253
- logger.info(f"Retrieved {len(docs)} documents using {strategy} strategy for query: '{query}'")
254
- return docs
255
-
256
- except Exception as e:
257
- logger.error(f"Document retrieval failed with strategy {strategy}: {e}")
258
- # Fallback to simple similarity search
259
- try:
260
- docs = await asyncio.to_thread(
261
- self.similarity_retriever.get_relevant_documents, query
262
- )
263
- return docs[:k]
264
- except Exception as e2:
265
- logger.error(f"Fallback retrieval also failed: {e2}")
266
- return []
267
-
268
- # --- INTELLIGENT QA CHAIN WITH LANGCHAIN ---
269
- class IntelligentQAChain:
270
- """Intelligent QA chain with LangChain prompt engineering"""
271
-
272
- def __init__(self, retriever: AdvancedLangChainRetriever, llm: GroqLLM):
273
- self.retriever = retriever
274
- self.llm = llm
275
-
276
- # Custom prompt template for insurance policy questions
277
- self.prompt_template = PromptTemplate(
278
  input_variables=["context", "question"],
279
- template="""You are an expert insurance policy analyst with deep understanding of policy documents and insurance terminology.
 
 
280
 
281
- Context Information (ranked by relevance):
282
  {context}
283
 
284
  Question: {question}
285
 
286
- Instructions:
287
- 1. Analyze the provided context carefully, paying attention to the most relevant sections
288
- 2. Provide a direct, precise answer based ONLY on the context provided
289
- 3. Include specific details like:
290
- - Exact timeframes (days, months, years)
291
- - Specific amounts or percentages
292
- - Conditions and requirements
293
- - Exclusions or limitations
294
- - Special circumstances or exceptions
295
-
296
- 4. If the information is not available in the context, clearly state: "The information is not available in the provided policy document."
297
-
298
- 5. For complex questions, structure your answer clearly with relevant details
299
- 6. Use professional insurance terminology where appropriate
300
- 7. Ensure your answer is complete and addresses all parts of the question
301
-
302
  Answer:"""
303
  )
 
 
 
 
 
 
 
 
 
304
 
305
- logger.info("Intelligent QA chain initialized with custom insurance prompt template")
306
-
307
  async def answer_question(self, question: str) -> str:
308
- """Answer question using advanced retrieval and LangChain QA"""
 
 
 
 
 
309
  try:
310
- # Step 1: Retrieve relevant documents using ensemble approach
311
- relevant_docs = await self.retriever.retrieve_documents(
312
- question,
313
- strategy="ensemble", # Use ensemble for balanced results
314
- k=12
315
- )
316
-
317
- if not relevant_docs:
318
- return "The information is not available in the provided policy document."
319
-
320
- # Step 2: Prepare context with relevance scoring
321
- context_parts = []
322
- for i, doc in enumerate(relevant_docs):
323
- # Add document with metadata
324
- metadata_info = ""
325
- if doc.metadata:
326
- source = doc.metadata.get('source', 'Unknown')
327
- metadata_info = f" [Source: {source}]"
328
-
329
- context_parts.append(
330
- f"--- SECTION {i+1} ---{metadata_info}\n{doc.page_content}\n"
331
- )
332
-
333
- context = "\n".join(context_parts)
334
-
335
- # Step 3: Generate answer using custom prompt
336
- prompt = self.prompt_template.format(
337
- context=context,
338
- question=question
339
  )
340
-
341
- answer = await asyncio.to_thread(self.llm._call, prompt)
342
-
343
- # Step 4: Clean and validate answer
344
- cleaned_answer = self._clean_answer(answer)
345
-
346
- logger.info(f"Generated answer for question: '{question}' (length: {len(cleaned_answer)} chars)")
347
- return cleaned_answer
348
-
349
  except Exception as e:
350
- logger.error(f"QA chain failed for question '{question}': {e}")
351
- return "Error: Could not generate an answer due to processing issues."
352
-
353
- def _clean_answer(self, answer: str) -> str:
354
- """Clean and format the generated answer"""
355
- if not answer or answer.strip() == "":
356
- return "The information is not available in the provided policy document."
357
-
358
- # Remove common artifacts
359
- cleaned = answer.strip()
360
- cleaned = re.sub(r'\n+', ' ', cleaned)
361
- cleaned = re.sub(r'\s+', ' ', cleaned)
362
- cleaned = cleaned.replace('\\n', ' ')
363
- cleaned = cleaned.replace('\\t', ' ')
364
-
365
- # Ensure proper capitalization
366
- if cleaned and len(cleaned) > 0:
367
- cleaned = cleaned[0].upper() + cleaned[1:] if len(cleaned) > 1 else cleaned.upper()
368
-
369
- # Ensure proper ending
370
- if cleaned and not cleaned.endswith(('.', '!', '?')):
371
- cleaned += '.'
372
-
373
- return cleaned
374
 
375
  # --- CONFIGURATION & MANAGERS ---
376
  class GroqAPIKeyManager:
@@ -468,59 +352,42 @@ class Answer(BaseModel):
468
  class SubmissionResponse(BaseModel):
469
  answers: List[Answer]
470
 
 
 
471
  class LangChainRAGPipeline:
472
- """Complete RAG pipeline using LangChain components"""
473
-
474
  def __init__(self, collection_name: str, request: Request):
475
  self.collection_name = collection_name
476
- self.request = request
477
  self.embedding_model = request.app.state.embedding_model
478
  self.groq_llm = request.app.state.groq_llm
479
  self.doc_processor = request.app.state.doc_processor
480
-
481
- # Initialize Chroma vectorstore
482
  self.vectorstore = Chroma(
483
  collection_name=collection_name,
484
  embedding_function=self.embedding_model,
485
  persist_directory=CHROMA_PERSIST_DIR
486
  )
487
-
488
- self.retriever = None
489
- self.qa_chain = None
490
-
491
  logger.info(f"LangChain RAG pipeline initialized for collection: {collection_name}")
492
-
493
  def add_documents(self, chunks: List[Any]):
494
- """Add documents to the LangChain vectorstore"""
495
  if not chunks:
496
- logger.warning("No chunks provided to add_documents.")
497
  return
498
-
499
- logger.info(f"Processing {len(chunks)} chunks with LangChain...")
500
-
501
- # Convert to LangChain documents with enhanced splitting
502
- langchain_docs = self.doc_processor.process_chunks_to_langchain_docs(
503
- chunks,
504
- split_strategy="semantic"
505
- )
506
-
507
- # Add to vectorstore
508
  self.vectorstore.add_documents(langchain_docs)
509
-
510
- # Initialize retriever and QA chain
511
- self.retriever = AdvancedLangChainRetriever(self.vectorstore, self.groq_llm)
512
- self.qa_chain = IntelligentQAChain(self.retriever, self.groq_llm)
513
-
514
- logger.info(f"✅ Added {len(langchain_docs)} documents to LangChain vectorstore")
515
-
516
  async def answer_question(self, question: str) -> str:
517
- """Answer question using the LangChain QA chain"""
518
- if not self.qa_chain:
519
- return "Error: QA chain not initialized. Please add documents first."
520
-
521
- logger.info(f"🤔 Answering question with LangChain: {question}")
522
- answer = await self.qa_chain.answer_question(question)
523
- return answer
524
 
525
  @app.post("/hackrx/run", response_model=SubmissionResponse)
526
  async def run_submission(request: Request, submission_request: SubmissionRequest = Body(...)):
 
180
  logger.info(f"Processed {len(chunks)} chunks into {len(langchain_docs)} LangChain documents using {split_strategy} strategy")
181
  return langchain_docs
182
 
183
+ # --- NEW: EFFICIENT AND EFFECTIVE RAG PIPELINE ---
184
+ from langchain.retrievers.document_compressors import FlashrankRerank
185
+
186
+ class EfficientRAGPipeline:
187
+ """A consolidated, efficient RAG pipeline using a Retrieve-Rerank strategy."""
188
 
189
  def __init__(self, vectorstore: Chroma, llm: GroqLLM):
190
  self.vectorstore = vectorstore
191
  self.llm = llm
192
+ self.qa_chain = self._create_qa_chain()
193
 
194
+ logger.info("✅ EfficientRAGPipeline initialized with a Retrieve-Rerank strategy.")
195
+
196
+ def _create_qa_chain(self) -> RetrievalQA:
197
+ """Creates and configures the core RetrievalQA chain."""
 
 
 
 
 
 
198
 
199
+ # 1. Base Retriever: Fetches a larger pool of documents (e.g., top 15)
200
+ base_retriever = self.vectorstore.as_retriever(
201
+ search_type="similarity",
202
+ search_kwargs={"k": 15}
203
  )
204
 
205
+ # 2. Reranker: Uses a fast, local model to re-rank the top 15 docs
206
+ # and returns the top 5 most relevant ones.
207
+ reranker = FlashrankRerank(top_n=5)
 
 
208
 
209
+ # 3. Compression Retriever: Combines the base retriever and reranker.
210
+ # This is the "Retrieve-then-Rerank" step.
211
+ compression_retriever = ContextualCompressionRetriever(
212
+ base_compressor=reranker,
213
+ base_retriever=base_retriever
214
  )
215
 
216
+ # 4. Prompt Template: The same effective prompt you already had.
217
+ prompt_template = PromptTemplate(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  input_variables=["context", "question"],
219
+ template="""You are an expert insurance policy analyst. Use the following pieces of context to answer the question at the end.
220
+ Provide a direct and precise answer based ONLY on the provided context.
221
+ If the answer is not in the context, state "The information is not available in the provided policy document."
222
 
223
+ Context:
224
  {context}
225
 
226
  Question: {question}
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  Answer:"""
229
  )
230
+
231
+ # 5. Create the final QA chain using the efficient retriever.
232
+ return RetrievalQA.from_chain_type(
233
+ llm=self.llm,
234
+ chain_type="stuff",
235
+ retriever=compression_retriever, # Using our efficient reranking retriever
236
+ chain_type_kwargs={"prompt": prompt_template},
237
+ return_source_documents=False
238
+ )
239
 
 
 
240
  async def answer_question(self, question: str) -> str:
241
+ """Answers a question using the configured RAG chain."""
242
+ if not self.qa_chain:
243
+ return "Error: QA chain is not initialized."
244
+
245
+ logger.info(f"🤔 Answering question with EfficientRAGPipeline: {question}")
246
+
247
  try:
248
+ # The chain runs synchronously, so we wrap it in asyncio.to_thread
249
+ result = await asyncio.to_thread(
250
+ self.qa_chain,
251
+ {"query": question}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  )
253
+ return result.get("result", "Failed to get an answer from the chain.")
 
 
 
 
 
 
 
 
254
  except Exception as e:
255
+ logger.error(f"Error during efficient QA chain execution: {e}")
256
+ return "An error occurred while processing the question."
257
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
  # --- CONFIGURATION & MANAGERS ---
260
  class GroqAPIKeyManager:
 
352
  class SubmissionResponse(BaseModel):
353
  answers: List[Answer]
354
 
355
+ # In main_api.py, update the LangChainRAGPipeline class
356
+
357
  class LangChainRAGPipeline:
358
+ """Wrapper to manage the RAG pipeline for a session."""
359
+
360
  def __init__(self, collection_name: str, request: Request):
361
  self.collection_name = collection_name
 
362
  self.embedding_model = request.app.state.embedding_model
363
  self.groq_llm = request.app.state.groq_llm
364
  self.doc_processor = request.app.state.doc_processor
365
+
 
366
  self.vectorstore = Chroma(
367
  collection_name=collection_name,
368
  embedding_function=self.embedding_model,
369
  persist_directory=CHROMA_PERSIST_DIR
370
  )
371
+
372
+ self.rag_pipeline = None # Will be initialized after adding docs
 
 
373
  logger.info(f"LangChain RAG pipeline initialized for collection: {collection_name}")
374
+
375
  def add_documents(self, chunks: List[Any]):
 
376
  if not chunks:
 
377
  return
378
+
379
+ langchain_docs = self.doc_processor.process_chunks_to_langchain_docs(chunks, "recursive")
 
 
 
 
 
 
 
 
380
  self.vectorstore.add_documents(langchain_docs)
381
+
382
+ # Initialize our new, efficient pipeline here
383
+ self.rag_pipeline = EfficientRAGPipeline(self.vectorstore, self.groq_llm)
384
+ logger.info(f"✅ EfficientRAGPipeline is ready.")
385
+
 
 
386
  async def answer_question(self, question: str) -> str:
387
+ if not self.rag_pipeline:
388
+ return "Error: RAG pipeline not initialized. Please add documents first."
389
+
390
+ return await self.rag_pipeline.answer_question(question)
 
 
 
391
 
392
  @app.post("/hackrx/run", response_model=SubmissionResponse)
393
  async def run_submission(request: Request, submission_request: SubmissionRequest = Body(...)):
requirements.txt CHANGED
@@ -8,7 +8,7 @@ httpx==0.25.2
8
  python-dotenv==1.0.0
9
  psutil==5.9.6
10
  python-multipart
11
-
12
 
13
  # Document processing
14
  PyMuPDF==1.23.8
 
8
  python-dotenv==1.0.0
9
  psutil==5.9.6
10
  python-multipart
11
+ flashrank
12
 
13
  # Document processing
14
  PyMuPDF==1.23.8