rohannsinghal commited on
Commit
0c4f4d8
Β·
1 Parent(s): f479732

made changes to main_api.py

Browse files
Files changed (2) hide show
  1. app/main_api.py +298 -57
  2. run.py +6 -4
app/main_api.py CHANGED
@@ -1,4 +1,4 @@
1
- # --- FIXED main_api.py ---
2
 
3
  import psutil
4
  import os
@@ -10,13 +10,15 @@ from typing import List, Dict, Any, Optional
10
  import logging
11
  import asyncio
12
  from collections import defaultdict
 
 
13
 
14
  # FastAPI and core dependencies
15
  from fastapi import FastAPI, Body, HTTPException, Request
16
  from fastapi.middleware.cors import CORSMiddleware
17
  from pydantic import BaseModel
18
 
19
- # LangChain imports
20
  from langchain_community.embeddings import HuggingFaceEmbeddings
21
  from langchain_community.vectorstores import Chroma
22
  from langchain.chains import RetrievalQA
@@ -25,11 +27,18 @@ from langchain.llms.base import LLM
25
  from langchain.callbacks.manager import CallbackManagerForLLMRun
26
  from langchain.schema.document import Document as LangChainDocument
27
 
 
 
 
 
 
 
 
 
28
  # LLM Integration
29
  import groq
30
 
31
- # Document processing and environment
32
- from .parser import FastDocumentParserService # Fixed import
33
  import httpx
34
  from dotenv import load_dotenv
35
 
@@ -38,7 +47,7 @@ load_dotenv()
38
  logging.basicConfig(level=logging.INFO)
39
  logger = logging.getLogger(__name__)
40
 
41
- app = FastAPI(title="Fixed RAG System", version="1.0.0")
42
 
43
  # CORS Middleware
44
  app.add_middleware(
@@ -46,7 +55,245 @@ app.add_middleware(
46
  allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
47
  )
48
 
49
- # --- CUSTOM GROQ LLM FOR LANGCHAIN ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  class GroqLLM(LLM):
51
  """Custom Groq LLM wrapper for LangChain"""
52
  groq_client: Any
@@ -66,8 +313,8 @@ class GroqLLM(LLM):
66
  response = self.groq_client.chat.completions.create(
67
  model="llama-3.3-70b-versatile",
68
  messages=[{"role": "user", "content": prompt}],
69
- temperature=0.1, # Slightly higher for more flexible responses
70
- max_tokens=800, # Increased token limit
71
  top_p=0.9,
72
  stop=stop
73
  )
@@ -75,10 +322,10 @@ class GroqLLM(LLM):
75
  except Exception as e:
76
  logger.error(f"Groq LLM call failed: {e}")
77
  return "Error generating response"
78
-
79
- # --- IMPROVED RAG PIPELINE ---
80
  class ImprovedRAGPipeline:
81
- """Improved RAG pipeline with better debugging and retrieval."""
82
 
83
  def __init__(self, collection_name: str, request: Request):
84
  self.collection_name = collection_name
@@ -90,38 +337,35 @@ class ImprovedRAGPipeline:
90
  persist_directory=CHROMA_PERSIST_DIR
91
  )
92
  self.qa_chain = None
93
- logger.info(f"βœ… Improved RAG pipeline initialized for collection: {collection_name}")
94
 
95
  def add_documents(self, chunks: List[Dict[str, Any]]):
96
- """Adds documents to the vectorstore and creates the QA chain."""
97
  if not chunks:
98
- logger.error("❌ No chunks provided to add_documents!")
99
  return
100
 
101
  logger.info(f"πŸ“š Adding {len(chunks)} chunks to vectorstore...")
102
 
103
- # Debug: Log first few chunks
104
  for i, chunk in enumerate(chunks[:3]):
105
- logger.info(f"Chunk {i}: {chunk['content'][:200]}...")
106
 
107
  langchain_docs = [
108
- LangChainDocument(
109
- page_content=chunk['content'],
110
- metadata=chunk['metadata']
111
- )
112
  for chunk in chunks
113
  ]
114
 
115
  self.vectorstore.add_documents(langchain_docs)
116
  logger.info(f"βœ… Added {len(langchain_docs)} documents to vectorstore")
117
 
118
- # Create retriever with more chunks and lower threshold
119
  retriever = self.vectorstore.as_retriever(
120
  search_type="similarity",
121
- search_kwargs={"k": 10} # Increased from 6 to 10
122
  )
123
 
124
- # Improved prompt template - less restrictive
125
  prompt_template = PromptTemplate(
126
  input_variables=["context", "question"],
127
  template="""You are an expert insurance policy analyst. Use the following policy document context to answer the question.
@@ -134,6 +378,7 @@ Question: {question}
134
  Instructions:
135
  - Provide a clear, direct answer based on the policy document context above
136
  - If you find relevant information, provide specific details including numbers, percentages, time periods, etc.
 
137
  - If the exact answer is not in the context but related information exists, provide what you can find
138
  - Only say "information not available" if absolutely no relevant information exists in the context
139
 
@@ -145,36 +390,36 @@ Answer:"""
145
  chain_type="stuff",
146
  retriever=retriever,
147
  chain_type_kwargs={"prompt": prompt_template},
148
- return_source_documents=True # This helps with debugging
149
  )
150
- logger.info(f"βœ… QA Chain is ready with improved retrieval")
151
 
152
  async def answer_question(self, question: str) -> str:
153
  if not self.qa_chain:
154
  return "Error: QA chain not initialized. Please add documents first."
155
 
156
- logger.info(f"πŸ€” Answering question: {question}")
157
  try:
158
- # First, let's test retrieval directly
159
  retriever = self.vectorstore.as_retriever(search_kwargs={"k": 5})
160
  retrieved_docs = retriever.get_relevant_documents(question)
161
 
162
- logger.info(f"πŸ” Retrieved {len(retrieved_docs)} documents for question")
163
  for i, doc in enumerate(retrieved_docs):
164
- logger.info(f"Retrieved Doc {i}: {doc.page_content[:150]}...")
165
 
166
- # Now run the QA chain
167
  result = await asyncio.to_thread(self.qa_chain, {"query": question})
168
  answer = result.get("result", "Failed to get an answer.")
169
 
170
- logger.info(f"βœ… Generated answer: {answer[:200]}...")
171
  return answer
172
 
173
  except Exception as e:
174
- logger.error(f"❌ Error during QA chain execution: {e}")
175
  return "An error occurred while processing the question."
176
 
177
- # --- GROQ API KEY MANAGER (unchanged) ---
178
  class GroqAPIKeyManager:
179
  def __init__(self, api_keys: List[str]):
180
  self.api_keys = [key.strip() for key in api_keys if key.strip()]
@@ -182,7 +427,7 @@ class GroqAPIKeyManager:
182
  self.key_last_used = defaultdict(float)
183
  self.current_key_index = 0
184
  self.max_requests_per_key = 45
185
- logger.info(f"πŸ”‘ API Key Manager initialized with {len(self.api_keys)} keys")
186
 
187
  def get_next_api_key(self):
188
  current_time = time.time()
@@ -196,14 +441,11 @@ class GroqAPIKeyManager:
196
  self.key_usage_count[best_key] += 1
197
  self.key_last_used[best_key] = current_time
198
  return best_key
199
-
200
- def get_key_stats(self):
201
- return {f"...{key[-4:]}": {"usage_count": self.key_usage_count[key], "last_used": self.key_last_used[key]} for key in self.api_keys}
202
 
203
- # --- APP STARTUP & CONFIG ---
204
  GROQ_API_KEYS = os.getenv("GROQ_API_KEYS", "").split(',')
205
  EMBEDDING_MODEL = "BAAI/bge-small-en-v1.5"
206
- CHROMA_PERSIST_DIR = "./chroma_db" # Simplified path
207
  UPLOAD_DIR = "/tmp/docs"
208
 
209
  @app.on_event("startup")
@@ -219,13 +461,13 @@ async def startup_event():
219
  first_key = app.state.api_key_manager.get_next_api_key()
220
  app.state.groq_client = groq.Groq(api_key=first_key)
221
  app.state.groq_llm = GroqLLM(groq_client=app.state.groq_client, api_key_manager=app.state.api_key_manager)
222
- app.state.parsing_service = FastDocumentParserService()
223
- logger.info("βœ… All services initialized successfully!")
224
  except Exception as e:
225
- logger.error(f"πŸ’₯ FATAL: Could not initialize services. Error: {e}")
226
  raise e
227
 
228
- # --- API MODELS (unchanged) ---
229
  class SubmissionRequest(BaseModel):
230
  documents: List[str]
231
  questions: List[str]
@@ -237,10 +479,10 @@ class Answer(BaseModel):
237
  class SubmissionResponse(BaseModel):
238
  answers: List[Answer]
239
 
240
- # --- MAIN API ENDPOINT ---
241
  @app.post("/hackrx/run", response_model=SubmissionResponse)
242
  async def run_submission(request: Request, submission_request: SubmissionRequest = Body(...)):
243
- logger.info(f"🎯 Processing {len(submission_request.documents)} documents and {len(submission_request.questions)} questions")
244
 
245
  parsing_service = request.app.state.parsing_service
246
  session_collection_name = f"hackrx_session_{uuid.uuid4().hex}"
@@ -251,7 +493,7 @@ async def run_submission(request: Request, submission_request: SubmissionRequest
251
  async with httpx.AsyncClient(timeout=120.0) as client:
252
  for doc_idx, doc_url in enumerate(submission_request.documents):
253
  try:
254
- logger.info(f"πŸ“₯ Downloading document {doc_idx + 1}/{len(submission_request.documents)}: {doc_url}")
255
  response = await client.get(doc_url, follow_redirects=True)
256
  response.raise_for_status()
257
 
@@ -267,36 +509,35 @@ async def run_submission(request: Request, submission_request: SubmissionRequest
267
  chunk_dicts = [chunk.to_dict() for chunk in chunks]
268
  all_chunks.extend(chunk_dicts)
269
 
270
- # Clean up
271
  os.remove(temp_file_path)
272
  logger.info(f"βœ… Processed {len(chunks)} chunks from {file_name}")
273
 
274
  except Exception as e:
275
- logger.error(f"❌ Failed to process document at {doc_url}: {e}")
276
  continue
277
 
278
- logger.info(f"πŸ“Š Total chunks collected: {len(all_chunks)}")
279
 
280
  if not all_chunks:
281
- logger.error("❌ No chunks were successfully processed!")
282
  failed_answers = [Answer(question=q, answer="No valid documents could be processed.") for q in submission_request.questions]
283
  return SubmissionResponse(answers=failed_answers)
284
 
285
- # Add documents to RAG pipeline
286
  rag_pipeline.add_documents(all_chunks)
287
 
288
  # Answer questions
289
- logger.info(f"❓ Answering {len(submission_request.questions)} questions...")
290
  tasks = [rag_pipeline.answer_question(q) for q in submission_request.questions]
291
  results = await asyncio.gather(*tasks)
292
  answers = [Answer(question=q, answer=ans) for q, ans in zip(submission_request.questions, results)]
293
 
294
- logger.info(f"πŸŽ‰ Successfully processed all questions!")
295
  return SubmissionResponse(answers=answers)
296
 
297
  @app.get("/")
298
  def read_root():
299
- return {"message": "Fixed RAG System is running.", "status": "healthy"}
300
 
301
  @app.get("/health")
302
  def health_check():
@@ -305,12 +546,12 @@ def health_check():
305
  # Debug endpoint
306
  @app.post("/debug/test-chunks")
307
  async def test_chunks(request: Request, submission_request: SubmissionRequest = Body(...)):
308
- """Debug endpoint to test document chunking"""
309
  parsing_service = request.app.state.parsing_service
310
  all_chunks = []
311
 
312
  async with httpx.AsyncClient(timeout=120.0) as client:
313
- for doc_url in submission_request.documents[:1]: # Test only first document
314
  try:
315
  response = await client.get(doc_url, follow_redirects=True)
316
  response.raise_for_status()
@@ -338,6 +579,6 @@ async def test_chunks(request: Request, submission_request: SubmissionRequest =
338
  "content": chunk["content"][:300] + "...",
339
  "metadata": chunk["metadata"]
340
  }
341
- for chunk in all_chunks[:3]
342
  ]
343
  }
 
1
+ # --- STANDALONE main_api.py with embedded parser ---
2
 
3
  import psutil
4
  import os
 
10
  import logging
11
  import asyncio
12
  from collections import defaultdict
13
+ from pathlib import Path
14
+ import gc
15
 
16
  # FastAPI and core dependencies
17
  from fastapi import FastAPI, Body, HTTPException, Request
18
  from fastapi.middleware.cors import CORSMiddleware
19
  from pydantic import BaseModel
20
 
21
+ # LangChain imports (using updated non-deprecated imports)
22
  from langchain_community.embeddings import HuggingFaceEmbeddings
23
  from langchain_community.vectorstores import Chroma
24
  from langchain.chains import RetrievalQA
 
27
  from langchain.callbacks.manager import CallbackManagerForLLMRun
28
  from langchain.schema.document import Document as LangChainDocument
29
 
30
+ # Document processing imports
31
+ import fitz # PyMuPDF
32
+ import pdfplumber
33
+ import mammoth
34
+ import email
35
+ import email.policy
36
+ from bs4 import BeautifulSoup
37
+
38
  # LLM Integration
39
  import groq
40
 
41
+ # Other dependencies
 
42
  import httpx
43
  from dotenv import load_dotenv
44
 
 
47
  logging.basicConfig(level=logging.INFO)
48
  logger = logging.getLogger(__name__)
49
 
50
+ app = FastAPI(title="Standalone Fixed RAG System", version="1.0.0")
51
 
52
  # CORS Middleware
53
  app.add_middleware(
 
55
  allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
56
  )
57
 
58
+ # --- EMBEDDED DOCUMENT PARSER ---
59
+ class DocumentChunk:
60
+ """Simple data class for document chunks"""
61
+ def __init__(self, content: str, metadata: Dict[str, Any], chunk_id: str):
62
+ self.content = content
63
+ self.metadata = metadata
64
+ self.chunk_id = chunk_id
65
+
66
+ def to_dict(self):
67
+ return {
68
+ "content": self.content,
69
+ "metadata": self.metadata,
70
+ "chunk_id": self.chunk_id
71
+ }
72
+
73
+ class EmbeddedDocumentParser:
74
+ """Embedded document parsing service"""
75
+
76
+ def __init__(self):
77
+ self.chunk_size = 2000
78
+ self.chunk_overlap = 200
79
+ self.max_chunks = 500
80
+ self.table_row_limit = 20
81
+ logger.info("EmbeddedDocumentParser initialized")
82
+
83
+ def fast_text_split(self, text: str, source: str) -> List[str]:
84
+ """Super fast text splitting with hard limits"""
85
+ if not text or len(text) < 100:
86
+ return [text] if text else []
87
+
88
+ if len(text) <= self.chunk_size:
89
+ return [text]
90
+
91
+ chunks = []
92
+ start = 0
93
+ chunk_count = 0
94
+
95
+ while start < len(text) and chunk_count < self.max_chunks:
96
+ end = min(start + self.chunk_size, len(text))
97
+
98
+ if end < len(text):
99
+ search_start = max(start, end - 200)
100
+ period_pos = text.rfind('.', search_start, end)
101
+ if period_pos > search_start:
102
+ end = period_pos + 1
103
+
104
+ chunk = text[start:end].strip()
105
+ if chunk:
106
+ chunks.append(chunk)
107
+ chunk_count += 1
108
+
109
+ start = end - self.chunk_overlap
110
+ if start <= 0:
111
+ start = end
112
+
113
+ logger.info(f"Split {source} into {len(chunks)} chunks")
114
+ return chunks[:self.max_chunks]
115
+
116
+ def extract_tables_fast(self, file_path: str) -> str:
117
+ """Fast table extraction"""
118
+ table_text = ""
119
+ table_count = 0
120
+ max_tables = 25
121
+
122
+ try:
123
+ with pdfplumber.open(file_path) as pdf:
124
+ total_pages = len(pdf.pages)
125
+
126
+ if total_pages <= 20:
127
+ step = 1
128
+ elif total_pages <= 40:
129
+ step = 2
130
+ else:
131
+ step = 3
132
+
133
+ pages_to_process = list(range(0, min(total_pages, 50), step))
134
+ logger.info(f"πŸ“Š Processing {len(pages_to_process)} of {total_pages} pages for tables")
135
+
136
+ for page_num in pages_to_process:
137
+ if table_count >= max_tables:
138
+ break
139
+
140
+ page = pdf.pages[page_num]
141
+ tables = page.find_tables()
142
+
143
+ for table in tables:
144
+ if table_count >= max_tables:
145
+ break
146
+
147
+ try:
148
+ table_data = table.extract()
149
+ if table_data and len(table_data) >= 2 and len(table_data[0]) <= 6:
150
+ limited_data = table_data[:min(30, len(table_data))]
151
+
152
+ header = " | ".join(str(cell or "").strip()[:60] for cell in limited_data[0])
153
+ separator = " | ".join(["---"] * len(limited_data[0]))
154
+
155
+ rows = []
156
+ for row in limited_data[1:]:
157
+ padded_row = list(row) + [None] * (len(limited_data[0]) - len(row))
158
+ row_str = " | ".join(str(cell or "").strip()[:60] for cell in padded_row)
159
+ rows.append(row_str)
160
+
161
+ table_md = f"\n**TABLE {table_count + 1} - Page {page_num + 1}**\n"
162
+ table_md += f"| {header} |\n| {separator} |\n"
163
+ for row in rows:
164
+ table_md += f"| {row} |\n"
165
+ table_md += "\n"
166
+
167
+ table_text += table_md
168
+ table_count += 1
169
+
170
+ except Exception as e:
171
+ logger.warning(f"Skip table on page {page_num + 1}: {e}")
172
+
173
+ logger.info(f"🎯 Extracted {table_count} tables")
174
+
175
+ except Exception as e:
176
+ logger.error(f"❌ Table extraction failed: {e}")
177
+
178
+ return table_text
179
+
180
+ def process_pdf_ultrafast(self, file_path: str) -> List[DocumentChunk]:
181
+ """Ultra-fast PDF processing"""
182
+ logger.info(f"πŸš€ Processing PDF: {os.path.basename(file_path)}")
183
+ start_time = time.time()
184
+
185
+ chunks = []
186
+
187
+ try:
188
+ # Extract tables
189
+ logger.info("πŸ“Š Extracting tables...")
190
+ table_content = self.extract_tables_fast(file_path)
191
+
192
+ # Extract text
193
+ logger.info("πŸ“„ Extracting text...")
194
+ doc = fitz.open(file_path)
195
+
196
+ full_text = ""
197
+ total_pages = len(doc)
198
+
199
+ if total_pages > 40:
200
+ pages_to_process = list(range(0, min(total_pages, 60), 2))
201
+ logger.info(f"πŸ“‘ Processing {len(pages_to_process)} of {total_pages} pages")
202
+ else:
203
+ pages_to_process = list(range(total_pages))
204
+
205
+ for page_num in pages_to_process:
206
+ try:
207
+ page = doc[page_num]
208
+ page_text = page.get_text()
209
+
210
+ page_text = page_text.strip()
211
+ if len(page_text) > 10000:
212
+ page_text = page_text[:10000] + f"\n[Page {page_num + 1} truncated]"
213
+
214
+ full_text += f"\n\n--- Page {page_num + 1} ---\n{page_text}"
215
+
216
+ except Exception as e:
217
+ logger.warning(f"Error processing page {page_num + 1}: {e}")
218
+
219
+ doc.close()
220
+
221
+ # Append tables
222
+ if table_content:
223
+ full_text += f"\n\n{'='*50}\nEXTRACTED TABLES\n{'='*50}\n{table_content}"
224
+
225
+ # Create chunks
226
+ logger.info("πŸ“¦ Creating chunks...")
227
+ text_chunks = self.fast_text_split(full_text, os.path.basename(file_path))
228
+
229
+ for idx, chunk_text in enumerate(text_chunks):
230
+ has_tables = "**TABLE" in chunk_text or "EXTRACTED TABLES" in chunk_text
231
+
232
+ chunks.append(DocumentChunk(
233
+ content=chunk_text,
234
+ metadata={
235
+ "source": os.path.basename(file_path),
236
+ "chunk_index": idx,
237
+ "document_type": "pdf_ultrafast",
238
+ "has_tables": has_tables,
239
+ "total_pages": total_pages,
240
+ "pages_processed": len(pages_to_process)
241
+ },
242
+ chunk_id=str(uuid.uuid4())
243
+ ))
244
+
245
+ elapsed = time.time() - start_time
246
+ logger.info(f"βœ… Processing complete in {elapsed:.2f}s: {len(chunks)} chunks")
247
+
248
+ return chunks
249
+
250
+ except Exception as e:
251
+ logger.error(f"❌ Processing failed: {e}")
252
+ return self._emergency_fallback(file_path)
253
+
254
+ def _emergency_fallback(self, file_path: str) -> List[DocumentChunk]:
255
+ """Emergency fallback"""
256
+ logger.info("πŸ†˜ Emergency fallback")
257
+
258
+ try:
259
+ doc = fitz.open(file_path)
260
+ max_pages = min(10, len(doc))
261
+ text_parts = []
262
+
263
+ for page_num in range(max_pages):
264
+ page = doc[page_num]
265
+ page_text = page.get_text()
266
+ if len(page_text) > 5000:
267
+ page_text = page_text[:5000] + f"\n[Page {page_num + 1} truncated]"
268
+ text_parts.append(f"Page {page_num + 1}:\n{page_text}")
269
+
270
+ doc.close()
271
+
272
+ full_text = "\n\n".join(text_parts)
273
+ chunks = []
274
+
275
+ chunk_size = len(full_text) // 10 + 1
276
+ for i in range(0, len(full_text), chunk_size):
277
+ chunk_text = full_text[i:i + chunk_size]
278
+ chunks.append(DocumentChunk(
279
+ content=chunk_text,
280
+ metadata={
281
+ "source": os.path.basename(file_path),
282
+ "chunk_index": len(chunks),
283
+ "document_type": "pdf_emergency_fallback",
284
+ "has_tables": False,
285
+ "pages_processed": max_pages
286
+ },
287
+ chunk_id=str(uuid.uuid4())
288
+ ))
289
+
290
+ return chunks
291
+
292
+ except Exception as e:
293
+ logger.error(f"Emergency fallback failed: {e}")
294
+ raise Exception("All processing methods failed")
295
+
296
+ # --- GROQ LLM WRAPPER ---
297
  class GroqLLM(LLM):
298
  """Custom Groq LLM wrapper for LangChain"""
299
  groq_client: Any
 
313
  response = self.groq_client.chat.completions.create(
314
  model="llama-3.3-70b-versatile",
315
  messages=[{"role": "user", "content": prompt}],
316
+ temperature=0.1,
317
+ max_tokens=800,
318
  top_p=0.9,
319
  stop=stop
320
  )
 
322
  except Exception as e:
323
  logger.error(f"Groq LLM call failed: {e}")
324
  return "Error generating response"
325
+
326
+ # --- RAG PIPELINE ---
327
  class ImprovedRAGPipeline:
328
+ """Improved RAG pipeline"""
329
 
330
  def __init__(self, collection_name: str, request: Request):
331
  self.collection_name = collection_name
 
337
  persist_directory=CHROMA_PERSIST_DIR
338
  )
339
  self.qa_chain = None
340
+ logger.info(f"βœ… RAG pipeline initialized: {collection_name}")
341
 
342
  def add_documents(self, chunks: List[Dict[str, Any]]):
343
+ """Add documents to vectorstore"""
344
  if not chunks:
345
+ logger.error("❌ No chunks provided!")
346
  return
347
 
348
  logger.info(f"πŸ“š Adding {len(chunks)} chunks to vectorstore...")
349
 
350
+ # Debug first few chunks
351
  for i, chunk in enumerate(chunks[:3]):
352
+ logger.info(f"Sample chunk {i}: {chunk['content'][:200]}...")
353
 
354
  langchain_docs = [
355
+ LangChainDocument(page_content=chunk['content'], metadata=chunk['metadata'])
 
 
 
356
  for chunk in chunks
357
  ]
358
 
359
  self.vectorstore.add_documents(langchain_docs)
360
  logger.info(f"βœ… Added {len(langchain_docs)} documents to vectorstore")
361
 
362
+ # Create retriever
363
  retriever = self.vectorstore.as_retriever(
364
  search_type="similarity",
365
+ search_kwargs={"k": 10}
366
  )
367
 
368
+ # Create prompt template - less restrictive
369
  prompt_template = PromptTemplate(
370
  input_variables=["context", "question"],
371
  template="""You are an expert insurance policy analyst. Use the following policy document context to answer the question.
 
378
  Instructions:
379
  - Provide a clear, direct answer based on the policy document context above
380
  - If you find relevant information, provide specific details including numbers, percentages, time periods, etc.
381
+ - Quote exact text when possible
382
  - If the exact answer is not in the context but related information exists, provide what you can find
383
  - Only say "information not available" if absolutely no relevant information exists in the context
384
 
 
390
  chain_type="stuff",
391
  retriever=retriever,
392
  chain_type_kwargs={"prompt": prompt_template},
393
+ return_source_documents=True
394
  )
395
+ logger.info("βœ… QA Chain ready")
396
 
397
  async def answer_question(self, question: str) -> str:
398
  if not self.qa_chain:
399
  return "Error: QA chain not initialized. Please add documents first."
400
 
401
+ logger.info(f"πŸ€” Answering: {question}")
402
  try:
403
+ # Test retrieval
404
  retriever = self.vectorstore.as_retriever(search_kwargs={"k": 5})
405
  retrieved_docs = retriever.get_relevant_documents(question)
406
 
407
+ logger.info(f"πŸ” Retrieved {len(retrieved_docs)} documents")
408
  for i, doc in enumerate(retrieved_docs):
409
+ logger.info(f"Retrieved {i}: {doc.page_content[:150]}...")
410
 
411
+ # Run QA chain
412
  result = await asyncio.to_thread(self.qa_chain, {"query": question})
413
  answer = result.get("result", "Failed to get an answer.")
414
 
415
+ logger.info(f"βœ… Answer: {answer[:200]}...")
416
  return answer
417
 
418
  except Exception as e:
419
+ logger.error(f"❌ Error during QA: {e}")
420
  return "An error occurred while processing the question."
421
 
422
+ # --- API KEY MANAGER ---
423
  class GroqAPIKeyManager:
424
  def __init__(self, api_keys: List[str]):
425
  self.api_keys = [key.strip() for key in api_keys if key.strip()]
 
427
  self.key_last_used = defaultdict(float)
428
  self.current_key_index = 0
429
  self.max_requests_per_key = 45
430
+ logger.info(f"πŸ”‘ API Key Manager: {len(self.api_keys)} keys")
431
 
432
  def get_next_api_key(self):
433
  current_time = time.time()
 
441
  self.key_usage_count[best_key] += 1
442
  self.key_last_used[best_key] = current_time
443
  return best_key
 
 
 
444
 
445
+ # --- CONFIGURATION ---
446
  GROQ_API_KEYS = os.getenv("GROQ_API_KEYS", "").split(',')
447
  EMBEDDING_MODEL = "BAAI/bge-small-en-v1.5"
448
+ CHROMA_PERSIST_DIR = "./chroma_db"
449
  UPLOAD_DIR = "/tmp/docs"
450
 
451
  @app.on_event("startup")
 
461
  first_key = app.state.api_key_manager.get_next_api_key()
462
  app.state.groq_client = groq.Groq(api_key=first_key)
463
  app.state.groq_llm = GroqLLM(groq_client=app.state.groq_client, api_key_manager=app.state.api_key_manager)
464
+ app.state.parsing_service = EmbeddedDocumentParser()
465
+ logger.info("βœ… All services initialized!")
466
  except Exception as e:
467
+ logger.error(f"πŸ’₯ FATAL: {e}")
468
  raise e
469
 
470
+ # --- API MODELS ---
471
  class SubmissionRequest(BaseModel):
472
  documents: List[str]
473
  questions: List[str]
 
479
  class SubmissionResponse(BaseModel):
480
  answers: List[Answer]
481
 
482
+ # --- MAIN ENDPOINT ---
483
  @app.post("/hackrx/run", response_model=SubmissionResponse)
484
  async def run_submission(request: Request, submission_request: SubmissionRequest = Body(...)):
485
+ logger.info(f"🎯 Processing {len(submission_request.documents)} documents, {len(submission_request.questions)} questions")
486
 
487
  parsing_service = request.app.state.parsing_service
488
  session_collection_name = f"hackrx_session_{uuid.uuid4().hex}"
 
493
  async with httpx.AsyncClient(timeout=120.0) as client:
494
  for doc_idx, doc_url in enumerate(submission_request.documents):
495
  try:
496
+ logger.info(f"πŸ“₯ Downloading document {doc_idx + 1}: {doc_url}")
497
  response = await client.get(doc_url, follow_redirects=True)
498
  response.raise_for_status()
499
 
 
509
  chunk_dicts = [chunk.to_dict() for chunk in chunks]
510
  all_chunks.extend(chunk_dicts)
511
 
 
512
  os.remove(temp_file_path)
513
  logger.info(f"βœ… Processed {len(chunks)} chunks from {file_name}")
514
 
515
  except Exception as e:
516
+ logger.error(f"❌ Failed to process document: {e}")
517
  continue
518
 
519
+ logger.info(f"πŸ“Š Total chunks: {len(all_chunks)}")
520
 
521
  if not all_chunks:
522
+ logger.error("❌ No chunks processed!")
523
  failed_answers = [Answer(question=q, answer="No valid documents could be processed.") for q in submission_request.questions]
524
  return SubmissionResponse(answers=failed_answers)
525
 
526
+ # Add to RAG pipeline
527
  rag_pipeline.add_documents(all_chunks)
528
 
529
  # Answer questions
530
+ logger.info(f"❓ Answering questions...")
531
  tasks = [rag_pipeline.answer_question(q) for q in submission_request.questions]
532
  results = await asyncio.gather(*tasks)
533
  answers = [Answer(question=q, answer=ans) for q, ans in zip(submission_request.questions, results)]
534
 
535
+ logger.info("πŸŽ‰ All questions processed!")
536
  return SubmissionResponse(answers=answers)
537
 
538
  @app.get("/")
539
  def read_root():
540
+ return {"message": "Standalone Fixed RAG System", "status": "healthy"}
541
 
542
  @app.get("/health")
543
  def health_check():
 
546
  # Debug endpoint
547
  @app.post("/debug/test-chunks")
548
  async def test_chunks(request: Request, submission_request: SubmissionRequest = Body(...)):
549
+ """Debug endpoint"""
550
  parsing_service = request.app.state.parsing_service
551
  all_chunks = []
552
 
553
  async with httpx.AsyncClient(timeout=120.0) as client:
554
+ for doc_url in submission_request.documents[:1]:
555
  try:
556
  response = await client.get(doc_url, follow_redirects=True)
557
  response.raise_for_status()
 
579
  "content": chunk["content"][:300] + "...",
580
  "metadata": chunk["metadata"]
581
  }
582
+ for chunk in all_chunks[:5] # Show more samples
583
  ]
584
  }
run.py CHANGED
@@ -1,14 +1,16 @@
1
- # run.py
2
 
3
  import uvicorn
4
  import os
5
 
6
  if __name__ == "__main__":
7
- # This makes the app compatible with hosting providers like Render.
8
- # It will use the PORT environment variable if it exists, otherwise it defaults to 8000.
9
  port = int(os.environ.get("PORT", 8000))
10
 
11
  print(f"πŸš€ Starting HackRx 6.0 RAG Server on port {port}...")
12
 
13
- # Use the standard 'app.module:variable' format for Uvicorn
 
 
 
 
14
  uvicorn.run("app.main_api:app", host="0.0.0.0", port=port, reload=False)
 
1
+ # Fixed run.py
2
 
3
  import uvicorn
4
  import os
5
 
6
  if __name__ == "__main__":
 
 
7
  port = int(os.environ.get("PORT", 8000))
8
 
9
  print(f"πŸš€ Starting HackRx 6.0 RAG Server on port {port}...")
10
 
11
+ # Use the correct path - adjust based on your file structure
12
+ # If main_api.py is in the root directory:
13
+ #uvicorn.run("main_api:app", host="0.0.0.0", port=port, reload=False)
14
+
15
+ # If main_api.py is in app/ directory, use:
16
  uvicorn.run("app.main_api:app", host="0.0.0.0", port=port, reload=False)