rohannsinghal commited on
Commit
fdd95d0
·
1 Parent(s): 98c83b3

changes to main_api.py

Browse files
Files changed (1) hide show
  1. app/main_api.py +341 -8
app/main_api.py CHANGED
@@ -209,7 +209,7 @@ class MultiLLMManager:
209
  )
210
  return response.choices[0].message.content.strip()
211
 
212
- # --- UNIVERSAL DOCUMENT PROCESSOR (KEEPING ALL YOUR EXCELLENT FEATURES) ---
213
  class UniversalDocumentProcessor:
214
  def __init__(self):
215
  self.chunk_size = 1000
@@ -221,24 +221,30 @@ class UniversalDocumentProcessor:
221
  self.processors = {
222
  '.pdf': self.process_pdf,
223
  '.docx': self.process_docx,
 
224
  '.xlsx': self.process_excel,
 
225
  '.csv': self.process_csv,
226
  '.txt': self.process_text,
227
  '.html': self.process_html,
 
 
 
228
  '.json': self.process_json
229
  }
230
 
231
  logger.info("⚡ Universal Document Processor (No Local Models)")
232
 
233
- # ... (KEEPING ALL YOUR EXISTING DOCUMENT PROCESSING METHODS)
234
- # I'll just show the key ones for brevity
 
235
 
236
  async def process_document(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
237
- """Process document (same as your existing method)"""
238
- file_hash = hashlib.md5(content).hexdigest()[:8]
239
 
240
  if file_hash in self.cache:
241
- logger.info(f"📦 Cache hit")
242
  return self.cache[file_hash]
243
 
244
  file_ext = Path(file_path).suffix.lower()
@@ -253,10 +259,337 @@ class UniversalDocumentProcessor:
253
  logger.info(f"✅ Processed {os.path.basename(file_path)}: {len(chunks)} chunks")
254
  return chunks
255
  except Exception as e:
256
- logger.error(f"❌ Processing failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  return []
258
 
259
- # ... (ALL YOUR EXISTING PROCESSING METHODS STAY THE SAME)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
  # --- LIGHTWEIGHT EMBEDDING WRAPPER (FOR CHROMA) ---
262
  class KaggleEmbeddingWrapper:
 
209
  )
210
  return response.choices[0].message.content.strip()
211
 
212
+ # --- COMPLETE UNIVERSAL DOCUMENT PROCESSOR (FROM YOUR WORKING CODE) ---
213
  class UniversalDocumentProcessor:
214
  def __init__(self):
215
  self.chunk_size = 1000
 
221
  self.processors = {
222
  '.pdf': self.process_pdf,
223
  '.docx': self.process_docx,
224
+ '.doc': self.process_doc,
225
  '.xlsx': self.process_excel,
226
+ '.xls': self.process_excel,
227
  '.csv': self.process_csv,
228
  '.txt': self.process_text,
229
  '.html': self.process_html,
230
+ '.xml': self.process_xml,
231
+ '.eml': self.process_email,
232
+ '.zip': self.process_archive,
233
  '.json': self.process_json
234
  }
235
 
236
  logger.info("⚡ Universal Document Processor (No Local Models)")
237
 
238
+ def get_file_hash(self, content: bytes) -> str:
239
+ """Generate shorter hash for caching"""
240
+ return hashlib.md5(content).hexdigest()[:8]
241
 
242
  async def process_document(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
243
+ """Process any document format with optimized caching"""
244
+ file_hash = self.get_file_hash(content)
245
 
246
  if file_hash in self.cache:
247
+ logger.info(f"📦 Cache hit for {os.path.basename(file_path)}")
248
  return self.cache[file_hash]
249
 
250
  file_ext = Path(file_path).suffix.lower()
 
259
  logger.info(f"✅ Processed {os.path.basename(file_path)}: {len(chunks)} chunks")
260
  return chunks
261
  except Exception as e:
262
+ logger.error(f"❌ Processing failed for {file_path}: {e}")
263
+ return self._emergency_text_extraction(content, file_path)
264
+
265
+ def _detect_file_type(self, content: bytes) -> str:
266
+ """Detect file type from content"""
267
+ if content.startswith(b'%PDF'):
268
+ return '.pdf'
269
+ elif content.startswith(b'PK'):
270
+ return '.docx' if b'word/' in content[:1000] else '.zip'
271
+ elif content.startswith(b'<html') or content.startswith(b'<!DOCTYPE'):
272
+ return '.html'
273
+ elif content.startswith(b'<?xml'):
274
+ return '.xml'
275
+ else:
276
+ return '.txt'
277
+
278
+ # --- PDF PROCESSING (FROM YOUR WORKING CODE) ---
279
+ async def process_pdf(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
280
+ """Enhanced PDF processing with speed optimizations"""
281
+ chunks = []
282
+ temp_path = f"/tmp/{uuid.uuid4().hex[:6]}.pdf"
283
+
284
+ with open(temp_path, 'wb') as f:
285
+ f.write(content)
286
+
287
+ try:
288
+ doc = fitz.open(temp_path)
289
+ full_text = ""
290
+
291
+ for page_num in range(min(len(doc), self.max_pages)):
292
+ page = doc[page_num]
293
+ text = page.get_text()
294
+
295
+ if text.strip():
296
+ full_text += f"\n\nPage {page_num + 1}:\n{self._clean_text(text)}"
297
+
298
+ doc.close()
299
+
300
+ # Optimized table extraction
301
+ table_text = await self._extract_pdf_tables_fast(temp_path)
302
+ if table_text:
303
+ full_text += f"\n\n=== TABLES ===\n{table_text}"
304
+
305
+ chunks = self._create_semantic_chunks(full_text, file_path, "pdf")
306
+
307
+ except Exception as e:
308
+ logger.error(f"PDF processing error: {e}")
309
+ chunks = self._emergency_text_extraction(content, file_path)
310
+
311
+ finally:
312
+ if os.path.exists(temp_path):
313
+ os.remove(temp_path)
314
+
315
+ return chunks
316
+
317
+ async def _extract_pdf_tables_fast(self, file_path: str) -> str:
318
+ """SPEED-OPTIMIZED table extraction"""
319
+ table_text = ""
320
+ try:
321
+ with pdfplumber.open(file_path) as pdf:
322
+ for page_num, page in enumerate(pdf.pages[:10]):
323
+ tables = page.find_tables()
324
+ for i, table in enumerate(tables[:1]):
325
+ try:
326
+ table_data = table.extract()
327
+ if table_data and len(table_data) > 1:
328
+ table_md = f"\n**Table {i+1} (Page {page_num+1})**\n"
329
+ for row in table_data[:12]:
330
+ if row:
331
+ clean_row = [str(cell or "").strip()[:30] for cell in row]
332
+ table_md += "| " + " | ".join(clean_row) + " |\n"
333
+ table_text += table_md + "\n"
334
+ except:
335
+ continue
336
+ except Exception as e:
337
+ logger.warning(f"Table extraction failed: {e}")
338
+
339
+ return table_text
340
+
341
+ # --- DOCX PROCESSING (FROM YOUR WORKING CODE) ---
342
+ async def process_docx(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
343
+ """Process DOCX files"""
344
+ temp_path = f"/tmp/{uuid.uuid4().hex[:6]}.docx"
345
+ with open(temp_path, 'wb') as f:
346
+ f.write(content)
347
+
348
+ try:
349
+ doc = docx.Document(temp_path)
350
+ full_text = ""
351
+
352
+ for para in doc.paragraphs:
353
+ if para.text.strip():
354
+ full_text += para.text + "\n"
355
+
356
+ for table in doc.tables:
357
+ table_text = "\n**TABLE**\n"
358
+ for row in table.rows:
359
+ row_text = []
360
+ for cell in row.cells:
361
+ row_text.append(cell.text.strip())
362
+ table_text += "| " + " | ".join(row_text) + " |\n"
363
+ full_text += table_text + "\n"
364
+
365
+ chunks = self._create_semantic_chunks(full_text, file_path, "docx")
366
+
367
+ except Exception as e:
368
+ logger.error(f"DOCX processing error: {e}")
369
+ chunks = self._emergency_text_extraction(content, file_path)
370
+
371
+ finally:
372
+ if os.path.exists(temp_path):
373
+ os.remove(temp_path)
374
+
375
+ return chunks
376
+
377
+ async def process_doc(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
378
+ """Process DOC files (fallback to text extraction)"""
379
+ return self._emergency_text_extraction(content, file_path)
380
+
381
+ # --- EXCEL PROCESSING (FROM YOUR WORKING CODE) ---
382
+ async def process_excel(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
383
+ """Process Excel files"""
384
+ temp_path = f"/tmp/{uuid.uuid4().hex[:6]}.xlsx"
385
+ with open(temp_path, 'wb') as f:
386
+ f.write(content)
387
+
388
+ try:
389
+ workbook = openpyxl.load_workbook(temp_path, read_only=True)
390
+ full_text = ""
391
+
392
+ for sheet_name in workbook.sheetnames[:3]:
393
+ sheet = workbook[sheet_name]
394
+ full_text += f"\n**Sheet: {sheet_name}**\n"
395
+
396
+ for row_num, row in enumerate(sheet.iter_rows(max_row=50, values_only=True)):
397
+ if row_num == 0 or any(cell for cell in row):
398
+ row_text = [str(cell or "").strip()[:30] for cell in row[:8]]
399
+ full_text += "| " + " | ".join(row_text) + " |\n"
400
+
401
+ workbook.close()
402
+ chunks = self._create_semantic_chunks(full_text, file_path, "excel")
403
+
404
+ except Exception as e:
405
+ logger.error(f"Excel processing error: {e}")
406
+ chunks = self._emergency_text_extraction(content, file_path)
407
+
408
+ finally:
409
+ if os.path.exists(temp_path):
410
+ os.remove(temp_path)
411
+
412
+ return chunks
413
+
414
+ # --- OTHER FORMAT PROCESSORS (FROM YOUR WORKING CODE) ---
415
+ async def process_csv(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
416
+ try:
417
+ text_content = content.decode('utf-8', errors='ignore')
418
+ lines = text_content.split('\n')
419
+
420
+ full_text = "**CSV DATA**\n"
421
+ for i, line in enumerate(lines[:100]):
422
+ if line.strip():
423
+ full_text += f"| {line} |\n"
424
+
425
+ return self._create_semantic_chunks(full_text, file_path, "csv")
426
+ except Exception as e:
427
+ logger.error(f"CSV processing error: {e}")
428
+ return []
429
+
430
+ async def process_text(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
431
+ try:
432
+ text = content.decode('utf-8', errors='ignore')
433
+ return self._create_semantic_chunks(text, file_path, "text")
434
+ except Exception as e:
435
+ logger.error(f"Text processing error: {e}")
436
+ return []
437
+
438
+ async def process_html(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
439
+ try:
440
+ soup = BeautifulSoup(content, 'html.parser')
441
+ for script in soup(["script", "style"]):
442
+ script.decompose()
443
+ text = soup.get_text()
444
+ return self._create_semantic_chunks(text, file_path, "html")
445
+ except Exception as e:
446
+ logger.error(f"HTML processing error: {e}")
447
+ return []
448
+
449
+ async def process_xml(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
450
+ try:
451
+ root = ET.fromstring(content)
452
+ def extract_text(element, level=0):
453
+ text = ""
454
+ if element.text and element.text.strip():
455
+ text += f"{' ' * level}{element.tag}: {element.text.strip()}\n"
456
+ for child in element:
457
+ text += extract_text(child, level + 1)
458
+ return text
459
+ full_text = extract_text(root)
460
+ return self._create_semantic_chunks(full_text, file_path, "xml")
461
+ except Exception as e:
462
+ logger.error(f"XML processing error: {e}")
463
+ return []
464
+
465
+ async def process_email(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
466
+ try:
467
+ msg = email.message_from_bytes(content, policy=default)
468
+ full_text = f"**EMAIL**\n"
469
+ full_text += f"From: {msg.get('From', 'Unknown')}\n"
470
+ full_text += f"Subject: {msg.get('Subject', 'No Subject')}\n\n"
471
+
472
+ if msg.is_multipart():
473
+ for part in msg.walk():
474
+ if part.get_content_type() == "text/plain":
475
+ body = part.get_content()
476
+ full_text += f"Content:\n{body}\n"
477
+ else:
478
+ body = msg.get_content()
479
+ full_text += f"Content:\n{body}\n"
480
+
481
+ return self._create_semantic_chunks(full_text, file_path, "email")
482
+ except Exception as e:
483
+ logger.error(f"Email processing error: {e}")
484
+ return []
485
+
486
+ async def process_archive(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
487
+ temp_path = f"/tmp/{uuid.uuid4().hex[:6]}.zip"
488
+ with open(temp_path, 'wb') as f:
489
+ f.write(content)
490
+
491
+ chunks = []
492
+ try:
493
+ if file_path.endswith('.zip'):
494
+ with zipfile.ZipFile(temp_path, 'r') as zip_file:
495
+ for file_info in zip_file.filelist[:5]:
496
+ try:
497
+ file_content = zip_file.read(file_info)
498
+ sub_chunks = await self.process_document(file_info.filename, file_content)
499
+ chunks.extend(sub_chunks[:15])
500
+ except:
501
+ continue
502
+ except Exception as e:
503
+ logger.error(f"Archive processing error: {e}")
504
+
505
+ finally:
506
+ if os.path.exists(temp_path):
507
+ os.remove(temp_path)
508
+
509
+ return chunks
510
+
511
+ async def process_json(self, file_path: str, content: bytes) -> List[Dict[str, Any]]:
512
+ try:
513
+ data = json.loads(content.decode('utf-8'))
514
+ full_text = json.dumps(data, indent=2, ensure_ascii=False)
515
+ return self._create_semantic_chunks(full_text, file_path, "json")
516
+ except Exception as e:
517
+ logger.error(f"JSON processing error: {e}")
518
  return []
519
 
520
+ # --- UTILITY METHODS (FROM YOUR WORKING CODE) ---
521
+ def _clean_text(self, text: str) -> str:
522
+ """Clean extracted text"""
523
+ text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text)
524
+ text = re.sub(r'\s+', ' ', text)
525
+
526
+ noise_patterns = [
527
+ r'Office of.*Insurance Ombudsman.*?\n',
528
+ r'Lalit Bhawan.*?\n',
529
+ r'^\d+\s*$'
530
+ ]
531
+
532
+ for pattern in noise_patterns:
533
+ text = re.sub(pattern, '', text, flags=re.MULTILINE)
534
+
535
+ return text.strip()
536
+
537
+ def _create_semantic_chunks(self, text: str, source: str, doc_type: str) -> List[Dict[str, Any]]:
538
+ """Create semantic chunks from text"""
539
+ text = self._clean_text(text)
540
+
541
+ if not text or len(text) < 50:
542
+ return []
543
+
544
+ sentences = re.split(r'(?<=[.!?])\s+', text)
545
+ chunks = []
546
+ current_chunk = ""
547
+
548
+ for sentence in sentences:
549
+ if len(current_chunk) + len(sentence) <= self.chunk_size:
550
+ current_chunk += sentence + " "
551
+ else:
552
+ if current_chunk.strip():
553
+ chunks.append(current_chunk.strip())
554
+ current_chunk = sentence + " "
555
+
556
+ if current_chunk.strip():
557
+ chunks.append(current_chunk.strip())
558
+
559
+ structured_chunks = []
560
+ for i, chunk_text in enumerate(chunks[:self.max_chunks]):
561
+ structured_chunks.append({
562
+ "content": chunk_text,
563
+ "metadata": {
564
+ "source": os.path.basename(source),
565
+ "chunk_index": i,
566
+ "document_type": doc_type,
567
+ "chunk_length": len(chunk_text)
568
+ },
569
+ "chunk_id": str(uuid.uuid4())
570
+ })
571
+
572
+ return structured_chunks
573
+
574
+ def _emergency_text_extraction(self, content: bytes, file_path: str) -> List[Dict[str, Any]]:
575
+ """Emergency text extraction for unsupported formats"""
576
+ try:
577
+ text = content.decode('utf-8', errors='ignore')
578
+ if len(text) > 50:
579
+ return self._create_semantic_chunks(text, file_path, "unknown")
580
+ except:
581
+ pass
582
+
583
+ return [{
584
+ "content": "Failed to extract content from document",
585
+ "metadata": {
586
+ "source": os.path.basename(file_path),
587
+ "chunk_index": 0,
588
+ "document_type": "error",
589
+ "error": True
590
+ },
591
+ "chunk_id": str(uuid.uuid4())
592
+ }]
593
 
594
  # --- LIGHTWEIGHT EMBEDDING WRAPPER (FOR CHROMA) ---
595
  class KaggleEmbeddingWrapper: