Spaces:
Sleeping
Sleeping
Commit ·
396bcd4
1
Parent(s): d1a7dac
removed existing pipeline and created new retriever and chain
Browse files- app/main_api.py +70 -203
- 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 |
-
# ---
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
| 186 |
|
| 187 |
def __init__(self, vectorstore: Chroma, llm: GroqLLM):
|
| 188 |
self.vectorstore = vectorstore
|
| 189 |
self.llm = llm
|
|
|
|
| 190 |
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 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 |
-
#
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
)
|
| 207 |
|
| 208 |
-
#
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
weights=[0.6, 0.4] # Weight similarity higher than MMR
|
| 212 |
-
)
|
| 213 |
|
| 214 |
-
#
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
| 218 |
)
|
| 219 |
|
| 220 |
-
|
| 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
|
|
|
|
|
|
|
| 280 |
|
| 281 |
-
Context
|
| 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
try:
|
| 310 |
-
#
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 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"
|
| 351 |
-
return "
|
| 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 |
-
"""
|
| 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.
|
| 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 |
-
|
| 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
|
| 511 |
-
self.
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
logger.info(f"✅ Added {len(langchain_docs)} documents to LangChain vectorstore")
|
| 515 |
-
|
| 516 |
async def answer_question(self, question: str) -> str:
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 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
|