PrathameshRaut commited on
Commit
cb07461
Β·
verified Β·
1 Parent(s): a826608

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +59 -53
app.py CHANGED
@@ -44,8 +44,8 @@ HF_TOKEN = os.environ.get("HF_TOKEN", "")
44
  # Logging config β€” edit these two constants to match your HF dataset repo
45
  # ---------------------------------------------------------------------------
46
 
47
- LOG_REPO_ID = "PrathameshRaut/VisionModelLogs" # dataset repo you create on HF
48
- LOG_FILENAME = "extraction_logs.xlsx" # file inside that repo
49
 
50
  LOG_HEADERS = [
51
  "timestamp_utc",
@@ -54,9 +54,9 @@ LOG_HEADERS = [
54
  "file_type",
55
  "document_type",
56
  "parameters_requested",
57
- "model_raw_thinking", # raw markdown text from Sarvam Document Intelligence
58
- "extracted_json", # final JSON response sent to caller
59
- "status", # "success" or "error"
60
  "error_detail",
61
  ]
62
 
@@ -112,18 +112,17 @@ def _download_or_create_workbook() -> openpyxl.Workbook:
112
  ws.title = "Logs"
113
  ws.append(LOG_HEADERS)
114
  _style_header_row(ws)
115
- # Set column widths
116
  col_widths = {
117
- "A": 22, # timestamp
118
- "B": 30, # filename
119
- "C": 16, # file_size
120
- "D": 14, # file_type
121
- "E": 22, # document_type
122
- "F": 35, # parameters
123
- "G": 60, # model_raw_thinking
124
- "H": 60, # extracted_json
125
- "I": 12, # status
126
- "J": 40, # error_detail
127
  }
128
  for col, width in col_widths.items():
129
  ws.column_dimensions[col].width = width
@@ -160,7 +159,6 @@ def append_log(
160
  ):
161
  """Thread-safe: download β†’ append row β†’ upload."""
162
  if not HF_TOKEN:
163
- # Logging silently skipped if token not configured
164
  return
165
 
166
  row = [
@@ -170,7 +168,7 @@ def append_log(
170
  file_type,
171
  document_type,
172
  ", ".join(parameters),
173
- (raw_thinking or "")[:5000], # cap at 5 000 chars to keep xlsx sane
174
  json.dumps(extracted_json, ensure_ascii=False) if extracted_json else "",
175
  status,
176
  error_detail[:1000],
@@ -183,7 +181,6 @@ def append_log(
183
 
184
  ws.append(row)
185
 
186
- # Style the new data row
187
  data_font = Font(name="Arial", size=10)
188
  wrap_align = Alignment(vertical="top", wrap_text=True)
189
  row_idx = ws.max_row
@@ -191,7 +188,6 @@ def append_log(
191
  cell.font = data_font
192
  cell.alignment = wrap_align
193
 
194
- # Zebra striping: light blue on even rows
195
  if row_idx % 2 == 0:
196
  fill = PatternFill("solid", start_color="DCE6F1")
197
  for cell in ws[row_idx]:
@@ -199,7 +195,6 @@ def append_log(
199
 
200
  _upload_workbook(wb)
201
  except Exception as e:
202
- # Logging must never crash the main API
203
  print(f"[LOG WARNING] Failed to write log entry: {e}")
204
 
205
 
@@ -242,15 +237,6 @@ def prepare_file(file_bytes: bytes, content_type: str, filename: str) -> tuple[b
242
  # ---------------------------------------------------------------------------
243
 
244
  def clean_raw_text(text: str) -> str:
245
- """
246
- Remove base64-encoded image data that Sarvam Document Intelligence
247
- carries over from PDFs/images into the markdown output.
248
- These blobs are never useful for field extraction and can easily push
249
- the prompt far beyond sarvam-m's 7 192-token context window.
250
- Two patterns are handled:
251
- 1. Markdown image syntax: ![alt](data:image/png;base64,<data>)
252
- 2. Bare data URIs that leaked outside markdown syntax
253
- """
254
  # Pattern 1 β€” markdown image embed
255
  text = re.sub(
256
  r'!\[[^\]]*\]\(data:[a-zA-Z]+/[a-zA-Z+\-]+;base64,[A-Za-z0-9+/=\s]+\)',
@@ -258,8 +244,7 @@ def clean_raw_text(text: str) -> str:
258
  text,
259
  flags=re.DOTALL,
260
  )
261
- # Pattern 2 β€” bare data URI (must be at least 100 chars of base64 to avoid
262
- # accidentally removing short legitimate strings)
263
  text = re.sub(
264
  r'data:[a-zA-Z]+/[a-zA-Z+\-]+;base64,[A-Za-z0-9+/=\s]{100,}',
265
  '',
@@ -277,7 +262,11 @@ def extract_text_with_sdk(file_bytes: bytes, filename: str) -> str:
277
  if not SARVAM_API_KEY:
278
  raise HTTPException(status_code=500, detail="SARVAM_API_KEY is not configured on the server")
279
 
280
- client = SarvamAI(api_subscription_key=SARVAM_API_KEY)
 
 
 
 
281
 
282
  suffix = Path(filename).suffix.lower()
283
  with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
@@ -317,19 +306,13 @@ def extract_text_with_sdk(file_bytes: bytes, filename: str) -> str:
317
  except FileNotFoundError:
318
  pass
319
 
 
320
  # ---------------------------------------------------------------------------
321
  # Step 2: Extract structured JSON via SarvamAI SDK chat completions
322
  # ---------------------------------------------------------------------------
323
 
324
  def strip_think_tags(text: str) -> str:
325
- """
326
- Remove <think>…</think> reasoning blocks from sarvam-m output.
327
- Handles both complete tags and truncated blocks where the closing
328
- tag was cut off due to max_tokens being reached mid-response.
329
- """
330
- # Pass 1: remove complete <think>…</think> blocks
331
  text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
332
- # Pass 2: remove any unclosed/truncated <think> block that runs to end-of-string
333
  text = re.sub(r"<think>.*$", "", text, flags=re.DOTALL)
334
  return text.strip()
335
 
@@ -341,6 +324,9 @@ async def extract_json_via_chat(doc_type: str, raw_text: str, parameters: list[s
341
  description = SUPPORTED_TYPES[doc_type]
342
  param_list = "\n".join(f" - {p}" for p in parameters)
343
 
 
 
 
344
  prompt = f"""You are a document data extraction assistant. Below is the full text extracted from a {description}.
345
  --- DOCUMENT TEXT START ---
346
  {raw_text}
@@ -355,24 +341,43 @@ Rules:
355
  5. Respond everything in english language only. Also convert date into proper DD/MM/YYYY format.
356
  Respond with the JSON object only."""
357
 
358
- # Run the synchronous SDK call in a thread so we don't block the event loop
359
  def _call_sdk() -> str:
360
- client = SarvamAI(api_subscription_key=SARVAM_API_KEY)
361
- response = client.chat.completions(
362
- model="sarvam-105b",
363
- messages=[{"role": "user", "content": prompt}],
364
- temperature=0,
365
- top_p=1,
366
- max_tokens=2048,
367
  )
368
- return response.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
  try:
371
  raw = await asyncio.to_thread(_call_sdk)
 
 
372
  except Exception as exc:
373
  raise HTTPException(status_code=500, detail=f"Sarvam chat error: {exc}") from exc
374
 
375
- # Remove <think> blocks β€” handles both complete and truncated closing tags
376
  raw = strip_think_tags(raw)
377
 
378
  # Strip markdown code fences if present
@@ -388,6 +393,7 @@ Respond with the JSON object only."""
388
  except json.JSONDecodeError:
389
  return {"raw_response": raw, "parse_error": "Model did not return valid JSON"}
390
 
 
391
  # ---------------------------------------------------------------------------
392
  # Routes
393
  # ---------------------------------------------------------------------------
@@ -440,8 +446,8 @@ async def extract_document(
440
  filename = file.filename or "document"
441
  file_size = len(file_bytes)
442
 
443
- raw_text = ""
444
- extracted = {}
445
 
446
  try:
447
  processed_bytes, processed_name = prepare_file(file_bytes, content_type, filename)
@@ -463,7 +469,7 @@ async def extract_document(
463
  )
464
  raise
465
 
466
- # Strip base64 image blobs before sending to sarvam-m
467
  raw_text = clean_raw_text(raw_text)
468
 
469
  try:
 
44
  # Logging config β€” edit these two constants to match your HF dataset repo
45
  # ---------------------------------------------------------------------------
46
 
47
+ LOG_REPO_ID = "PrathameshRaut/VisionModelLogs"
48
+ LOG_FILENAME = "extraction_logs.xlsx"
49
 
50
  LOG_HEADERS = [
51
  "timestamp_utc",
 
54
  "file_type",
55
  "document_type",
56
  "parameters_requested",
57
+ "model_raw_thinking",
58
+ "extracted_json",
59
+ "status",
60
  "error_detail",
61
  ]
62
 
 
112
  ws.title = "Logs"
113
  ws.append(LOG_HEADERS)
114
  _style_header_row(ws)
 
115
  col_widths = {
116
+ "A": 22,
117
+ "B": 30,
118
+ "C": 16,
119
+ "D": 14,
120
+ "E": 22,
121
+ "F": 35,
122
+ "G": 60,
123
+ "H": 60,
124
+ "I": 12,
125
+ "J": 40,
126
  }
127
  for col, width in col_widths.items():
128
  ws.column_dimensions[col].width = width
 
159
  ):
160
  """Thread-safe: download β†’ append row β†’ upload."""
161
  if not HF_TOKEN:
 
162
  return
163
 
164
  row = [
 
168
  file_type,
169
  document_type,
170
  ", ".join(parameters),
171
+ (raw_thinking or "")[:5000],
172
  json.dumps(extracted_json, ensure_ascii=False) if extracted_json else "",
173
  status,
174
  error_detail[:1000],
 
181
 
182
  ws.append(row)
183
 
 
184
  data_font = Font(name="Arial", size=10)
185
  wrap_align = Alignment(vertical="top", wrap_text=True)
186
  row_idx = ws.max_row
 
188
  cell.font = data_font
189
  cell.alignment = wrap_align
190
 
 
191
  if row_idx % 2 == 0:
192
  fill = PatternFill("solid", start_color="DCE6F1")
193
  for cell in ws[row_idx]:
 
195
 
196
  _upload_workbook(wb)
197
  except Exception as e:
 
198
  print(f"[LOG WARNING] Failed to write log entry: {e}")
199
 
200
 
 
237
  # ---------------------------------------------------------------------------
238
 
239
  def clean_raw_text(text: str) -> str:
 
 
 
 
 
 
 
 
 
240
  # Pattern 1 β€” markdown image embed
241
  text = re.sub(
242
  r'!\[[^\]]*\]\(data:[a-zA-Z]+/[a-zA-Z+\-]+;base64,[A-Za-z0-9+/=\s]+\)',
 
244
  text,
245
  flags=re.DOTALL,
246
  )
247
+ # Pattern 2 β€” bare data URI
 
248
  text = re.sub(
249
  r'data:[a-zA-Z]+/[a-zA-Z+\-]+;base64,[A-Za-z0-9+/=\s]{100,}',
250
  '',
 
262
  if not SARVAM_API_KEY:
263
  raise HTTPException(status_code=500, detail="SARVAM_API_KEY is not configured on the server")
264
 
265
+ # FIX: increased timeout so the polling loop doesn't give up too early
266
+ client = SarvamAI(
267
+ api_subscription_key=SARVAM_API_KEY,
268
+ timeout_in_seconds=180,
269
+ )
270
 
271
  suffix = Path(filename).suffix.lower()
272
  with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
 
306
  except FileNotFoundError:
307
  pass
308
 
309
+
310
  # ---------------------------------------------------------------------------
311
  # Step 2: Extract structured JSON via SarvamAI SDK chat completions
312
  # ---------------------------------------------------------------------------
313
 
314
  def strip_think_tags(text: str) -> str:
 
 
 
 
 
 
315
  text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
 
316
  text = re.sub(r"<think>.*$", "", text, flags=re.DOTALL)
317
  return text.strip()
318
 
 
324
  description = SUPPORTED_TYPES[doc_type]
325
  param_list = "\n".join(f" - {p}" for p in parameters)
326
 
327
+ # Truncate to avoid blowing context window
328
+ raw_text = raw_text[:6000] if len(raw_text) > 6000 else raw_text
329
+
330
  prompt = f"""You are a document data extraction assistant. Below is the full text extracted from a {description}.
331
  --- DOCUMENT TEXT START ---
332
  {raw_text}
 
341
  5. Respond everything in english language only. Also convert date into proper DD/MM/YYYY format.
342
  Respond with the JSON object only."""
343
 
 
344
  def _call_sdk() -> str:
345
+ # FIX: increased timeout β€” sarvam-105b reasoning can take >60s
346
+ client = SarvamAI(
347
+ api_subscription_key=SARVAM_API_KEY,
348
+ timeout_in_seconds=180,
 
 
 
349
  )
350
+
351
+ # FIX: retry up to 3 times β€” 105b occasionally returns null content
352
+ for attempt in range(3):
353
+ response = client.chat.completions(
354
+ model="sarvam-105b",
355
+ messages=[{"role": "user", "content": prompt}],
356
+ temperature=0,
357
+ top_p=1,
358
+ max_tokens=2048,
359
+ )
360
+
361
+ # FIX: guard against null content before calling .strip()
362
+ content = response.choices[0].message.content
363
+ if content:
364
+ return content.strip()
365
+
366
+ print(
367
+ f"[WARN] Attempt {attempt + 1}/3: Sarvam returned null content "
368
+ f"(finish_reason={response.choices[0].finish_reason!r}), retrying..."
369
+ )
370
+
371
+ raise ValueError("Sarvam returned null content after 3 attempts")
372
 
373
  try:
374
  raw = await asyncio.to_thread(_call_sdk)
375
+ except ValueError as exc:
376
+ raise HTTPException(status_code=502, detail=str(exc)) from exc
377
  except Exception as exc:
378
  raise HTTPException(status_code=500, detail=f"Sarvam chat error: {exc}") from exc
379
 
380
+ # Remove <think> blocks
381
  raw = strip_think_tags(raw)
382
 
383
  # Strip markdown code fences if present
 
393
  except json.JSONDecodeError:
394
  return {"raw_response": raw, "parse_error": "Model did not return valid JSON"}
395
 
396
+
397
  # ---------------------------------------------------------------------------
398
  # Routes
399
  # ---------------------------------------------------------------------------
 
446
  filename = file.filename or "document"
447
  file_size = len(file_bytes)
448
 
449
+ raw_text = ""
450
+ extracted = {}
451
 
452
  try:
453
  processed_bytes, processed_name = prepare_file(file_bytes, content_type, filename)
 
469
  )
470
  raise
471
 
472
+ # Strip base64 image blobs before sending to chat model
473
  raw_text = clean_raw_text(raw_text)
474
 
475
  try: