philip11 commited on
Commit
5268e6c
Β·
verified Β·
1 Parent(s): 6bd1e95

Update old.py

Browse files
Files changed (1) hide show
  1. old.py +493 -493
old.py CHANGED
@@ -1,577 +1,577 @@
1
- import streamlit as st
2
- import os
3
- import json
4
- import re
5
- from pathlib import Path
6
- from datetime import datetime
7
- from typing import Dict, List, Any, Optional
8
- import fitz # PyMuPDF
9
- from docx import Document
10
- from docx.shared import Inches
11
- import tempfile
12
- from groq import Groq
13
 
14
- # Configure Streamlit page
15
- st.set_page_config(
16
- page_title="GLR Pipeline Automation",
17
- page_icon="πŸ“‹",
18
- layout="wide",
19
- initial_sidebar_state="expanded"
20
- )
21
 
22
- class GLRProcessor:
23
- """Main class for processing GLR documents using Groq API"""
24
 
25
- def __init__(self):
26
- self.output_dir = Path("output")
27
- self.output_dir.mkdir(exist_ok=True)
28
- # Get API key from Hugging Face secrets
29
- self.api_key = os.getenv("Key") # Get from HF secrets named "Key"
30
- self.groq_client = None
31
- self.initialize_groq()
32
 
33
- def initialize_groq(self):
34
- """Initialize Groq client with API key from HF secrets"""
35
- try:
36
- if not self.api_key:
37
- st.error("❌ API key not found in Hugging Face secrets. Please set the 'Key' secret in your Space settings.")
38
- return
39
 
40
- self.groq_client = Groq(api_key=self.api_key)
41
- st.success("βœ… Groq API initialized successfully!")
42
 
43
- except Exception as e:
44
- st.error(f"Failed to initialize Groq: {str(e)}")
45
- self.groq_client = None
46
 
47
- def extract_text_from_pdf(self, pdf_file) -> str:
48
- """Extract text from uploaded PDF file"""
49
- try:
50
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
51
- tmp_file.write(pdf_file.read())
52
- tmp_file_path = tmp_file.name
53
 
54
- doc = fitz.open(tmp_file_path)
55
- text = ""
56
- for page in doc:
57
- text += page.get_text()
58
- doc.close()
59
 
60
- os.unlink(tmp_file_path)
61
- return text.strip()
62
- except Exception as e:
63
- st.error(f"Error extracting text from PDF: {str(e)}")
64
- return ""
65
 
66
- def extract_placeholders_from_docx(self, docx_file) -> List[str]:
67
- """Extract placeholders from DOCX template"""
68
- try:
69
- with tempfile.NamedTemporaryFile(delete=False, suffix='.docx') as tmp_file:
70
- tmp_file.write(docx_file.read())
71
- tmp_file_path = tmp_file.name
72
 
73
- doc = Document(tmp_file_path)
74
- placeholders = set()
75
 
76
- for paragraph in doc.paragraphs:
77
- matches = re.findall(r'\[([A-Z_]+)\]', paragraph.text)
78
- placeholders.update(matches)
79
 
80
- for table in doc.tables:
81
- for row in table.rows:
82
- for cell in row.cells:
83
- matches = re.findall(r'\[([A-Z_]+)\]', cell.text)
84
- placeholders.update(matches)
85
 
86
- os.unlink(tmp_file_path)
87
- return list(placeholders)
88
- except Exception as e:
89
- st.error(f"Error extracting placeholders from DOCX: {str(e)}")
90
- return []
91
 
92
- def extract_data_with_groq(self, pdf_texts: List[str], placeholders: List[str]) -> Dict[str, str]:
93
- """Use Groq API to extract data from PDF texts with enhanced prompting"""
94
- if self.groq_client is None:
95
- st.warning("⚠️ Groq API not available. Using enhanced fallback extraction...")
96
- return self._enhanced_fallback_extraction("\n\n".join(pdf_texts), placeholders)
97
 
98
- try:
99
- combined_text = "\n\n".join(pdf_texts)
100
 
101
- # Truncate if too long but keep more context
102
- if len(combined_text) > 12000:
103
- combined_text = combined_text[:12000] + "..."
104
 
105
- # Create more specific field descriptions
106
- field_descriptions = {
107
- "DATE_LOSS": "Date when the loss/damage occurred (format: MM/DD/YYYY)",
108
- "DATE_RECEIVED": "Date when the claim was received (format: MM/DD/YYYY)",
109
- "DATE_INSPECTED": "Date when the property was inspected (format: MM/DD/YYYY)",
110
- "INSURED_NAME": "Full name of the insured person/entity",
111
- "STREET": "Street address of the property (number + street name + suffix like St, Ave, Rd)",
112
- "H_STREET": "Home street address (number + street name + suffix)",
113
- "INSURED_H_STREET": "Insured's home street address (number + street name + suffix like St, Ave, Rd)",
114
- "CITY": "City name",
115
- "H_CITY": "Home city name",
116
- "INSURED_H_CITY": "Insured's home city name",
117
- "STATE": "State abbreviation (2 letters like CA, NY, TX)",
118
- "H_STATE": "Home state abbreviation",
119
- "INSURED_H_STATE": "Insured's home state abbreviation",
120
- "ZIP": "ZIP code (5 or 9 digits)",
121
- "H_ZIP": "Home ZIP code",
122
- "INSURED_H_ZIP": "Insured's home ZIP code",
123
- "MORTGAGEE": "Mortgage company or lender name",
124
- "MORTGAGE": "Mortgage information",
125
- "MORTGAGE_CO": "Mortgage company name",
126
- "TOL_CODE": "Type of Loss code (alphanumeric code like F1, W2, H3, or similar insurance peril codes)",
127
- "CLAIM_NUMBER": "Insurance claim number",
128
- "POLICY_NUMBER": "Insurance policy number",
129
- "PHONE": "Phone number",
130
- "EMAIL": "Email address"
131
- }
132
 
133
- # Build detailed extraction instructions
134
- extraction_instructions = []
135
- for field in placeholders:
136
- description = field_descriptions.get(field, f"Extract the {field.lower().replace('_', ' ')}")
137
- extraction_instructions.append(f"- {field}: {description}")
138
 
139
- prompt = f"""You are an expert insurance document analyst. Extract the following specific information from this insurance report text. Be very thorough and look for variations in how the data might be presented.
140
 
141
- REQUIRED FIELDS TO EXTRACT:
142
- {chr(10).join(extraction_instructions)}
143
 
144
- EXTRACTION GUIDELINES:
145
- 1. Search the ENTIRE document text carefully for each field
146
- 2. Look for variations like "Date of Loss", "Loss Date", "DOL" for DATE_LOSS
147
- 3. For names, extract complete full names as they appear
148
- 4. For addresses, look for complete street addresses with numbers
149
- 5. For dates, convert to MM/DD/YYYY format if in different format
150
- 6. For states, use 2-letter codes (California = CA, New York = NY, etc.)
151
- 7. Look in headers, footers, tables, and body text
152
- 8. If a field has multiple possible values, choose the most complete/relevant one
153
- 9. Only use "N/A" if the information is absolutely not present anywhere in the document
154
 
155
- DOCUMENT TEXT TO ANALYZE:
156
- {combined_text}
157
 
158
- Return ONLY a valid JSON object with the extracted data:
159
- {{
160
- "FIELD_NAME": "extracted_value"
161
- }}
162
 
163
- JSON:"""
164
 
165
- # Make API call to Groq with better parameters
166
- completion = self.groq_client.chat.completions.create(
167
- model="deepseek-r1-distill-llama-70b",
168
- messages=[
169
- {
170
- "role": "system",
171
- "content": "You are an expert at extracting structured data from insurance documents. Always return valid JSON and be thorough in your extraction."
172
- },
173
- {
174
- "role": "user",
175
- "content": prompt
176
- }
177
- ],
178
- temperature=0.0, # More deterministic
179
- max_tokens=3000,
180
- top_p=0.9,
181
- stream=False,
182
- stop=None,
183
- )
184
 
185
- response = completion.choices[0].message.content
186
 
187
- # Better JSON parsing
188
- json_text = response.strip()
189
 
190
- # Remove markdown formatting
191
- if json_text.startswith("```json"):
192
- json_text = json_text[7:]
193
- if json_text.startswith("```"):
194
- json_text = json_text[3:]
195
- if json_text.endswith("```"):
196
- json_text = json_text[:-3]
197
- json_text = json_text.strip()
198
 
199
- # Extract JSON object more carefully
200
- json_match = re.search(r'\{.*\}', json_text, re.DOTALL)
201
- if json_match:
202
- json_text = json_match.group()
203
 
204
- try:
205
- extracted_data = json.loads(json_text)
206
- except json.JSONDecodeError:
207
- # Try to fix common JSON issues
208
- json_text = json_text.replace("'", '"') # Replace single quotes
209
- json_text = re.sub(r',\s*}', '}', json_text) # Remove trailing commas
210
- extracted_data = json.loads(json_text)
211
 
212
- # Ensure all placeholders are present and post-process
213
- final_data = {}
214
- for placeholder in placeholders:
215
- if placeholder in extracted_data:
216
- value = str(extracted_data[placeholder]).strip()
217
- # Clean up the value
218
- if value and value.lower() not in ['n/a', 'null', 'none', '', 'not found']:
219
- final_data[placeholder] = value
220
- else:
221
- # Try fallback extraction for this specific field
222
- fallback_value = self._extract_single_field(combined_text, placeholder)
223
- final_data[placeholder] = fallback_value
224
- else:
225
- # Try fallback extraction for missing field
226
- fallback_value = self._extract_single_field(combined_text, placeholder)
227
- final_data[placeholder] = fallback_value
228
 
229
- st.success("βœ… Groq AI extraction completed successfully!")
230
- return final_data
231
 
232
- except Exception as e:
233
- st.warning(f"Groq API extraction failed: {str(e)}")
234
- return self._enhanced_fallback_extraction(combined_text, placeholders)
235
 
236
- def _extract_single_field(self, text: str, field: str) -> str:
237
- """Extract a single field using enhanced pattern matching"""
238
- text_lower = text.lower()
239
- text_upper = text.upper()
240
 
241
- # Date patterns
242
- if "DATE" in field:
243
- date_patterns = [
244
- r'(?:' + field.lower().replace('_', r'[\s_]*') + r')[\s:]*(\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4})',
245
- r'(?:loss|received|inspect[a-z]*|claim)[\s:]+((?:\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4})|(?:[A-Za-z]+ \d{1,2}, \d{4}))',
246
- r'\b(\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4})\b',
247
- r'\b([A-Za-z]+ \d{1,2}, \d{4})\b'
248
- ]
249
- for pattern in date_patterns:
250
- match = re.search(pattern, text, re.IGNORECASE)
251
- if match:
252
- return match.group(1)
253
 
254
- # Name patterns
255
- elif "NAME" in field:
256
- name_patterns = [
257
- r'(?:insured|name|policyholder)[\s:]+([A-Z][a-z]+ [A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)',
258
- r'\b([A-Z][a-z]+ [A-Z][a-z]+)\b',
259
- r'name[\s:]*([A-Z][A-Za-z\s]+?)(?:\n|address|phone|email)',
260
- ]
261
- for pattern in name_patterns:
262
- matches = re.findall(pattern, text, re.IGNORECASE)
263
- for match in matches:
264
- if len(match.split()) >= 2 and len(match) > 4:
265
- return match.strip()
266
 
267
- # Enhanced Address patterns for STREET
268
- elif "STREET" in field:
269
- street_patterns = [
270
- # Standard address patterns with numbers
271
- r'\b(\d+\s+[A-Z][A-Za-z\s]+(?:ST|STREET|AVE|AVENUE|RD|ROAD|DR|DRIVE|LN|LANE|CT|COURT|BLVD|BOULEVARD|PL|PLACE|WAY|CIR|CIRCLE|PKWY|PARKWAY)\.?)\b',
272
- # Address after keywords
273
- r'(?:address|street|location|property)[\s:]+(\d+\s+[A-Za-z][A-Za-z\s]+)',
274
- # Full address patterns
275
- r'\b(\d{1,5}\s+[A-Za-z][A-Za-z\s]+ (?:Street|Avenue|Road|Drive|Lane|Court|Boulevard|Place|Way|Circle|Parkway))\b',
276
- # Address in format: number + name + abbreviated suffix
277
- r'\b(\d{1,5}\s+[A-Za-z][A-Za-z\s]+\s+(?:St|Ave|Rd|Dr|Ln|Ct|Blvd|Pl|Way|Cir|Pkwy)\.?)\b',
278
- # Property address patterns
279
- r'(?:property|insured|loss)[\s\w]*address[\s:]*(\d+\s+[A-Za-z][A-Za-z\s]+)',
280
- # Address before city/state
281
- r'(\d+\s+[A-Za-z][A-Za-z\s]+?)(?:,\s*[A-Z][a-z]+\s*,?\s*[A-Z]{2})',
282
- # General number + street name pattern
283
- r'\b(\d{1,5}\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*(?:\s+(?:St|Ave|Rd|Dr|Ln|Ct|Blvd|Pl|Way|Street|Avenue|Road|Drive|Lane|Court|Boulevard|Place))?)\b'
284
- ]
285
- for pattern in street_patterns:
286
- match = re.search(pattern, text, re.IGNORECASE)
287
- if match:
288
- street = match.group(1).strip()
289
- # Validate it looks like a street address
290
- if re.match(r'\d+\s+[A-Za-z]', street) and len(street) > 5:
291
- return street
292
 
293
- # City patterns
294
- elif "CITY" in field:
295
- city_patterns = [
296
- r'(?:city)[\s:]+([A-Z][a-z\s]+?)(?:,|\s+[A-Z]{2}\s|\n)',
297
- r',\s*([A-Z][a-z\s]+?)\s+[A-Z]{2}\s+\d{5}',
298
- r'\b([A-Z][a-z]+(?: [A-Z][a-z]+)*),\s*[A-Z]{2}\b'
299
- ]
300
- for pattern in city_patterns:
301
- match = re.search(pattern, text)
302
- if match:
303
- city = match.group(1).strip()
304
- if len(city) > 2:
305
- return city
306
 
307
- # State patterns
308
- elif "STATE" in field:
309
- state_patterns = [
310
- r'\b([A-Z]{2})\s+\d{5}',
311
- r'(?:state)[\s:]+([A-Z]{2})\b',
312
- r',\s*[A-Za-z\s]+,?\s*([A-Z]{2})\s+\d{5}'
313
- ]
314
- for pattern in state_patterns:
315
- match = re.search(pattern, text)
316
- if match:
317
- return match.group(1)
318
 
319
- # ZIP patterns
320
- elif "ZIP" in field:
321
- zip_patterns = [
322
- r'\b(\d{5}-\d{4})\b',
323
- r'\b(\d{5})\b(?!\d)'
324
- ]
325
- for pattern in zip_patterns:
326
- match = re.search(pattern, text)
327
- if match:
328
- return match.group(1)
329
 
330
- # Phone patterns
331
- elif "PHONE" in field:
332
- phone_patterns = [
333
- r'\b(\d{3}[-.\s]?\d{3}[-.\s]?\d{4})\b',
334
- r'\((\d{3})\)\s*(\d{3})[-.\s]?(\d{4})'
335
- ]
336
- for pattern in phone_patterns:
337
- match = re.search(pattern, text)
338
- if match:
339
- if len(match.groups()) == 1:
340
- return match.group(1)
341
- else:
342
- return f"({match.group(1)}) {match.group(2)}-{match.group(3)}"
343
 
344
- # Email patterns
345
- elif "EMAIL" in field:
346
- email_pattern = r'\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\b'
347
- match = re.search(email_pattern, text)
348
- if match:
349
- return match.group(1)
350
 
351
- # Policy/Claim number patterns
352
- elif "POLICY" in field or "CLAIM" in field:
353
- number_patterns = [
354
- r'(?:policy|claim)[\s#:]*([A-Z0-9\-]+)',
355
- r'\b([A-Z]{2,4}\d{6,})\b',
356
- r'\b(\d{8,})\b'
357
- ]
358
- for pattern in number_patterns:
359
- match = re.search(pattern, text, re.IGNORECASE)
360
- if match:
361
- return match.group(1)
362
 
363
- # Mortgage patterns
364
- elif "MORTGAGE" in field:
365
- mortgage_patterns = [
366
- r'(?:mortgage[e]?|lender)[\s:]+([A-Z][A-Za-z\s&]+?)(?:\n|$|address)',
367
- r'\b([A-Z][a-z]+ (?:Bank|Mortgage|Financial|Credit Union|Lending))\b'
368
- ]
369
- for pattern in mortgage_patterns:
370
- match = re.search(pattern, text, re.IGNORECASE)
371
- if match:
372
- return match.group(1).strip()
373
 
374
- # Enhanced TOL Code patterns
375
- elif "TOL" in field or "CODE" in field:
376
- code_patterns = [
377
- # Direct TOL/code patterns
378
- r'(?:tol|type\s*of\s*loss)[\s:]*code[\s:]*([A-Z0-9\-]+)',
379
- r'(?:tol|code)[\s:]*([A-Z0-9\-]+)',
380
- r'(?:type\s*of\s*loss)[\s:]*([A-Z0-9\-]+)',
381
- # Loss type codes
382
- r'(?:loss\s*type|cause\s*of\s*loss)[\s:]*([A-Z0-9\-]+)',
383
- # Peril codes
384
- r'(?:peril|coverage)[\s:]*code[\s:]*([A-Z0-9\-]+)',
385
- # General code patterns in insurance context
386
- r'(?:claim|loss|damage)[\s\w]*code[\s:]*([A-Z0-9\-]+)',
387
- # Alphanumeric codes (common format)
388
- r'\b([A-Z]{1,3}\d{1,4})\b',
389
- r'\b([A-Z]{2,4}-?\d{2,4})\b',
390
- # Fire, water, wind codes
391
- r'(?:fire|water|wind|storm|hail)[\s:]*([A-Z0-9\-]+)',
392
- # Coverage codes
393
- r'(?:coverage|section)[\s:]*([A-Z]\d*)',
394
- # Standalone codes that might be TOL
395
- r'\b([A-Z]\d{2,3})\b',
396
- r'\b([A-Z]{2}\d{1,2})\b'
397
- ]
398
- for pattern in code_patterns:
399
- matches = re.findall(pattern, text, re.IGNORECASE)
400
- for match in matches:
401
- # Filter out common false positives
402
- if (len(match) >= 2 and
403
- not match.lower() in ['tx', 'ca', 'ny', 'fl'] and # state codes
404
- not match.isdigit() and # pure numbers
405
- not re.match(r'^\d{5}$', match)): # zip codes
406
- return match.upper()
407
 
408
- return "N/A"
409
 
410
- def _enhanced_fallback_extraction(self, text: str, placeholders: List[str]) -> Dict[str, str]:
411
- """Enhanced fallback extraction with better pattern matching"""
412
- st.info("πŸ” Using enhanced pattern matching extraction...")
413
 
414
- fallback_data = {}
415
 
416
- for placeholder in placeholders:
417
- value = self._extract_single_field(text, placeholder)
418
- fallback_data[placeholder] = value
419
 
420
- return fallback_data
421
 
422
- def populate_docx_template(self, docx_file, extracted_data: Dict[str, str]) -> str:
423
- """Populate DOCX template with extracted data"""
424
- try:
425
- with tempfile.NamedTemporaryFile(delete=False, suffix='.docx') as tmp_file:
426
- tmp_file.write(docx_file.read())
427
- tmp_file_path = tmp_file.name
428
 
429
- doc = Document(tmp_file_path)
430
 
431
- # Track replacements made
432
- replacements_made = 0
433
 
434
- for paragraph in doc.paragraphs:
435
- for placeholder, value in extracted_data.items():
436
- if f"[{placeholder}]" in paragraph.text:
437
- paragraph.text = paragraph.text.replace(f"[{placeholder}]", str(value))
438
- replacements_made += 1
439
 
440
- for table in doc.tables:
441
- for row in table.rows:
442
- for cell in row.cells:
443
- for paragraph in cell.paragraphs:
444
- for placeholder, value in extracted_data.items():
445
- if f"[{placeholder}]" in paragraph.text:
446
- paragraph.text = paragraph.text.replace(f"[{placeholder}]", str(value))
447
- replacements_made += 1
448
 
449
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
450
- output_filename = f"GLR_Report_{timestamp}.docx"
451
- output_path = self.output_dir / output_filename
452
 
453
- doc.save(str(output_path))
454
- os.unlink(tmp_file_path)
455
 
456
- st.info(f"Made {replacements_made} field replacements in the template")
457
- return str(output_path)
458
 
459
- except Exception as e:
460
- st.error(f"Error populating template: {str(e)}")
461
- return None
462
 
463
- def main():
464
- """Main Streamlit application"""
465
- st.title("πŸ“‹ GLR Pipeline Automation (Enhanced)")
466
- st.markdown("**Automate General Loss Report generation with improved data extraction**")
467
 
468
- # Initialize processor
469
- processor = GLRProcessor()
470
 
471
- col1, col2 = st.columns([1, 1])
472
 
473
- with col1:
474
- st.header("πŸ“„ Upload Template")
475
- template_file = st.file_uploader("Upload GLR Template (.docx)", type=['docx'])
476
 
477
- with col2:
478
- st.header("πŸ“‹ Upload Reports")
479
- photo_reports = st.file_uploader("Upload PDF Reports", type=['pdf'], accept_multiple_files=True)
480
 
481
- if template_file and photo_reports:
482
- st.header("πŸ”„ Processing")
483
 
484
- with st.spinner("Processing..."):
485
- placeholders = processor.extract_placeholders_from_docx(template_file)
486
 
487
- if placeholders:
488
- st.success(f"Found {len(placeholders)} placeholders: {', '.join(placeholders)}")
489
 
490
- pdf_texts = []
491
- for pdf_file in photo_reports:
492
- text = processor.extract_text_from_pdf(pdf_file)
493
- if text:
494
- pdf_texts.append(text)
495
- st.success(f"βœ… Processed {pdf_file.name} ({len(text)} characters)")
496
 
497
- if pdf_texts:
498
- extracted_data = processor.extract_data_with_groq(pdf_texts, placeholders)
499
 
500
- st.subheader("πŸ“Š Extracted Data")
501
 
502
- # Show extraction statistics
503
- non_na_count = sum(1 for v in extracted_data.values() if v != "N/A")
504
- extraction_rate = (non_na_count / len(extracted_data)) * 100
505
- st.metric("Extraction Success Rate", f"{extraction_rate:.1f}%", f"{non_na_count}/{len(extracted_data)} fields")
506
 
507
- # Create form for editing data
508
- with st.form("data_form"):
509
- edited_data = {}
510
- for placeholder, value in extracted_data.items():
511
- # Color code based on whether value was found
512
- if value != "N/A":
513
- st.markdown(f"**{placeholder}** βœ…")
514
- else:
515
- st.markdown(f"**{placeholder}** ❌")
516
- edited_data[placeholder] = st.text_input(f"", value=value, key=placeholder)
517
 
518
- generate_report = st.form_submit_button("Generate Report", type="primary")
519
 
520
- # Handle report generation outside the form
521
- if generate_report:
522
- template_file.seek(0)
523
- output_path = processor.populate_docx_template(template_file, edited_data)
524
 
525
- if output_path:
526
- st.success("βœ… Report generated successfully!")
527
 
528
- # Store the file data in session state for download
529
- with open(output_path, 'rb') as file:
530
- st.session_state['report_data'] = file.read()
531
- st.session_state['report_filename'] = os.path.basename(output_path)
532
 
533
- # Download button outside the form
534
- if 'report_data' in st.session_state:
535
- st.download_button(
536
- label="πŸ“₯ Download Generated Report",
537
- data=st.session_state['report_data'],
538
- file_name=st.session_state['report_filename'],
539
- mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
540
- type="primary"
541
- )
542
- else:
543
- st.error("No text extracted from PDFs")
544
- else:
545
- st.error("No placeholders found in template")
546
 
547
- # Instructions and setup for Hugging Face Spaces
548
- st.header("πŸ“– Hugging Face Spaces Setup")
549
- st.markdown("""
550
- **This app is configured for Hugging Face Spaces deployment:**
551
 
552
- **Step 1: Set up your Space**
553
- - Create a new Space on Hugging Face with Streamlit SDK
554
- - Upload the app files (app.py, requirements.txt, packages.txt)
555
 
556
- **Step 2: Configure API Key**
557
- - Go to your Space Settings β†’ Repository secrets
558
- - Add a new secret named `Key` with your Groq API key value
559
- - Get your free API key from [console.groq.com](https://console.groq.com/)
560
 
561
- **Step 3: Use the App**
562
- 1. Upload a DOCX template with placeholders like `[DATE_LOSS]`, `[INSURED_NAME]`
563
- 2. Upload PDF reports containing the data to extract
564
- 3. Review and edit the AI-extracted data
565
- 4. Generate and download the completed report
566
- """)
567
 
568
- # Show API key status
569
- if not processor.api_key:
570
- st.error("⚠️ **API Key Not Found** - Please set the 'Key' secret in your Hugging Face Space settings.")
571
- elif processor.groq_client:
572
- st.success("βœ… **API Key Configured** - Ready to process documents!")
573
- else:
574
- st.error("❌ **API Key Error** - Please check your API key configuration in Space secrets")
575
 
576
- if __name__ == "__main__":
577
- main()
 
1
+ # import streamlit as st
2
+ # import os
3
+ # import json
4
+ # import re
5
+ # from pathlib import Path
6
+ # from datetime import datetime
7
+ # from typing import Dict, List, Any, Optional
8
+ # import fitz # PyMuPDF
9
+ # from docx import Document
10
+ # from docx.shared import Inches
11
+ # import tempfile
12
+ # from groq import Groq
13
 
14
+ # # Configure Streamlit page
15
+ # st.set_page_config(
16
+ # page_title="GLR Pipeline Automation",
17
+ # page_icon="πŸ“‹",
18
+ # layout="wide",
19
+ # initial_sidebar_state="expanded"
20
+ # )
21
 
22
+ # class GLRProcessor:
23
+ # """Main class for processing GLR documents using Groq API"""
24
 
25
+ # def __init__(self):
26
+ # self.output_dir = Path("output")
27
+ # self.output_dir.mkdir(exist_ok=True)
28
+ # # Get API key from Hugging Face secrets
29
+ # self.api_key = os.getenv("Key") # Get from HF secrets named "Key"
30
+ # self.groq_client = None
31
+ # self.initialize_groq()
32
 
33
+ # def initialize_groq(self):
34
+ # """Initialize Groq client with API key from HF secrets"""
35
+ # try:
36
+ # if not self.api_key:
37
+ # st.error("❌ API key not found in Hugging Face secrets. Please set the 'Key' secret in your Space settings.")
38
+ # return
39
 
40
+ # self.groq_client = Groq(api_key=self.api_key)
41
+ # st.success("βœ… Groq API initialized successfully!")
42
 
43
+ # except Exception as e:
44
+ # st.error(f"Failed to initialize Groq: {str(e)}")
45
+ # self.groq_client = None
46
 
47
+ # def extract_text_from_pdf(self, pdf_file) -> str:
48
+ # """Extract text from uploaded PDF file"""
49
+ # try:
50
+ # with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
51
+ # tmp_file.write(pdf_file.read())
52
+ # tmp_file_path = tmp_file.name
53
 
54
+ # doc = fitz.open(tmp_file_path)
55
+ # text = ""
56
+ # for page in doc:
57
+ # text += page.get_text()
58
+ # doc.close()
59
 
60
+ # os.unlink(tmp_file_path)
61
+ # return text.strip()
62
+ # except Exception as e:
63
+ # st.error(f"Error extracting text from PDF: {str(e)}")
64
+ # return ""
65
 
66
+ # def extract_placeholders_from_docx(self, docx_file) -> List[str]:
67
+ # """Extract placeholders from DOCX template"""
68
+ # try:
69
+ # with tempfile.NamedTemporaryFile(delete=False, suffix='.docx') as tmp_file:
70
+ # tmp_file.write(docx_file.read())
71
+ # tmp_file_path = tmp_file.name
72
 
73
+ # doc = Document(tmp_file_path)
74
+ # placeholders = set()
75
 
76
+ # for paragraph in doc.paragraphs:
77
+ # matches = re.findall(r'\[([A-Z_]+)\]', paragraph.text)
78
+ # placeholders.update(matches)
79
 
80
+ # for table in doc.tables:
81
+ # for row in table.rows:
82
+ # for cell in row.cells:
83
+ # matches = re.findall(r'\[([A-Z_]+)\]', cell.text)
84
+ # placeholders.update(matches)
85
 
86
+ # os.unlink(tmp_file_path)
87
+ # return list(placeholders)
88
+ # except Exception as e:
89
+ # st.error(f"Error extracting placeholders from DOCX: {str(e)}")
90
+ # return []
91
 
92
+ # def extract_data_with_groq(self, pdf_texts: List[str], placeholders: List[str]) -> Dict[str, str]:
93
+ # """Use Groq API to extract data from PDF texts with enhanced prompting"""
94
+ # if self.groq_client is None:
95
+ # st.warning("⚠️ Groq API not available. Using enhanced fallback extraction...")
96
+ # return self._enhanced_fallback_extraction("\n\n".join(pdf_texts), placeholders)
97
 
98
+ # try:
99
+ # combined_text = "\n\n".join(pdf_texts)
100
 
101
+ # # Truncate if too long but keep more context
102
+ # if len(combined_text) > 12000:
103
+ # combined_text = combined_text[:12000] + "..."
104
 
105
+ # # Create more specific field descriptions
106
+ # field_descriptions = {
107
+ # "DATE_LOSS": "Date when the loss/damage occurred (format: MM/DD/YYYY)",
108
+ # "DATE_RECEIVED": "Date when the claim was received (format: MM/DD/YYYY)",
109
+ # "DATE_INSPECTED": "Date when the property was inspected (format: MM/DD/YYYY)",
110
+ # "INSURED_NAME": "Full name of the insured person/entity",
111
+ # "STREET": "Street address of the property (number + street name + suffix like St, Ave, Rd)",
112
+ # "H_STREET": "Home street address (number + street name + suffix)",
113
+ # "INSURED_H_STREET": "Insured's home street address (number + street name + suffix like St, Ave, Rd)",
114
+ # "CITY": "City name",
115
+ # "H_CITY": "Home city name",
116
+ # "INSURED_H_CITY": "Insured's home city name",
117
+ # "STATE": "State abbreviation (2 letters like CA, NY, TX)",
118
+ # "H_STATE": "Home state abbreviation",
119
+ # "INSURED_H_STATE": "Insured's home state abbreviation",
120
+ # "ZIP": "ZIP code (5 or 9 digits)",
121
+ # "H_ZIP": "Home ZIP code",
122
+ # "INSURED_H_ZIP": "Insured's home ZIP code",
123
+ # "MORTGAGEE": "Mortgage company or lender name",
124
+ # "MORTGAGE": "Mortgage information",
125
+ # "MORTGAGE_CO": "Mortgage company name",
126
+ # "TOL_CODE": "Type of Loss code (alphanumeric code like F1, W2, H3, or similar insurance peril codes)",
127
+ # "CLAIM_NUMBER": "Insurance claim number",
128
+ # "POLICY_NUMBER": "Insurance policy number",
129
+ # "PHONE": "Phone number",
130
+ # "EMAIL": "Email address"
131
+ # }
132
 
133
+ # # Build detailed extraction instructions
134
+ # extraction_instructions = []
135
+ # for field in placeholders:
136
+ # description = field_descriptions.get(field, f"Extract the {field.lower().replace('_', ' ')}")
137
+ # extraction_instructions.append(f"- {field}: {description}")
138
 
139
+ # prompt = f"""You are an expert insurance document analyst. Extract the following specific information from this insurance report text. Be very thorough and look for variations in how the data might be presented.
140
 
141
+ # REQUIRED FIELDS TO EXTRACT:
142
+ # {chr(10).join(extraction_instructions)}
143
 
144
+ # EXTRACTION GUIDELINES:
145
+ # 1. Search the ENTIRE document text carefully for each field
146
+ # 2. Look for variations like "Date of Loss", "Loss Date", "DOL" for DATE_LOSS
147
+ # 3. For names, extract complete full names as they appear
148
+ # 4. For addresses, look for complete street addresses with numbers
149
+ # 5. For dates, convert to MM/DD/YYYY format if in different format
150
+ # 6. For states, use 2-letter codes (California = CA, New York = NY, etc.)
151
+ # 7. Look in headers, footers, tables, and body text
152
+ # 8. If a field has multiple possible values, choose the most complete/relevant one
153
+ # 9. Only use "N/A" if the information is absolutely not present anywhere in the document
154
 
155
+ # DOCUMENT TEXT TO ANALYZE:
156
+ # {combined_text}
157
 
158
+ # Return ONLY a valid JSON object with the extracted data:
159
+ # {{
160
+ # "FIELD_NAME": "extracted_value"
161
+ # }}
162
 
163
+ # JSON:"""
164
 
165
+ # # Make API call to Groq with better parameters
166
+ # completion = self.groq_client.chat.completions.create(
167
+ # model="deepseek-r1-distill-llama-70b",
168
+ # messages=[
169
+ # {
170
+ # "role": "system",
171
+ # "content": "You are an expert at extracting structured data from insurance documents. Always return valid JSON and be thorough in your extraction."
172
+ # },
173
+ # {
174
+ # "role": "user",
175
+ # "content": prompt
176
+ # }
177
+ # ],
178
+ # temperature=0.0, # More deterministic
179
+ # max_tokens=3000,
180
+ # top_p=0.9,
181
+ # stream=False,
182
+ # stop=None,
183
+ # )
184
 
185
+ # response = completion.choices[0].message.content
186
 
187
+ # # Better JSON parsing
188
+ # json_text = response.strip()
189
 
190
+ # # Remove markdown formatting
191
+ # if json_text.startswith("```json"):
192
+ # json_text = json_text[7:]
193
+ # if json_text.startswith("```"):
194
+ # json_text = json_text[3:]
195
+ # if json_text.endswith("```"):
196
+ # json_text = json_text[:-3]
197
+ # json_text = json_text.strip()
198
 
199
+ # # Extract JSON object more carefully
200
+ # json_match = re.search(r'\{.*\}', json_text, re.DOTALL)
201
+ # if json_match:
202
+ # json_text = json_match.group()
203
 
204
+ # try:
205
+ # extracted_data = json.loads(json_text)
206
+ # except json.JSONDecodeError:
207
+ # # Try to fix common JSON issues
208
+ # json_text = json_text.replace("'", '"') # Replace single quotes
209
+ # json_text = re.sub(r',\s*}', '}', json_text) # Remove trailing commas
210
+ # extracted_data = json.loads(json_text)
211
 
212
+ # # Ensure all placeholders are present and post-process
213
+ # final_data = {}
214
+ # for placeholder in placeholders:
215
+ # if placeholder in extracted_data:
216
+ # value = str(extracted_data[placeholder]).strip()
217
+ # # Clean up the value
218
+ # if value and value.lower() not in ['n/a', 'null', 'none', '', 'not found']:
219
+ # final_data[placeholder] = value
220
+ # else:
221
+ # # Try fallback extraction for this specific field
222
+ # fallback_value = self._extract_single_field(combined_text, placeholder)
223
+ # final_data[placeholder] = fallback_value
224
+ # else:
225
+ # # Try fallback extraction for missing field
226
+ # fallback_value = self._extract_single_field(combined_text, placeholder)
227
+ # final_data[placeholder] = fallback_value
228
 
229
+ # st.success("βœ… Groq AI extraction completed successfully!")
230
+ # return final_data
231
 
232
+ # except Exception as e:
233
+ # st.warning(f"Groq API extraction failed: {str(e)}")
234
+ # return self._enhanced_fallback_extraction(combined_text, placeholders)
235
 
236
+ # def _extract_single_field(self, text: str, field: str) -> str:
237
+ # """Extract a single field using enhanced pattern matching"""
238
+ # text_lower = text.lower()
239
+ # text_upper = text.upper()
240
 
241
+ # # Date patterns
242
+ # if "DATE" in field:
243
+ # date_patterns = [
244
+ # r'(?:' + field.lower().replace('_', r'[\s_]*') + r')[\s:]*(\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4})',
245
+ # r'(?:loss|received|inspect[a-z]*|claim)[\s:]+((?:\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4})|(?:[A-Za-z]+ \d{1,2}, \d{4}))',
246
+ # r'\b(\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4})\b',
247
+ # r'\b([A-Za-z]+ \d{1,2}, \d{4})\b'
248
+ # ]
249
+ # for pattern in date_patterns:
250
+ # match = re.search(pattern, text, re.IGNORECASE)
251
+ # if match:
252
+ # return match.group(1)
253
 
254
+ # # Name patterns
255
+ # elif "NAME" in field:
256
+ # name_patterns = [
257
+ # r'(?:insured|name|policyholder)[\s:]+([A-Z][a-z]+ [A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)',
258
+ # r'\b([A-Z][a-z]+ [A-Z][a-z]+)\b',
259
+ # r'name[\s:]*([A-Z][A-Za-z\s]+?)(?:\n|address|phone|email)',
260
+ # ]
261
+ # for pattern in name_patterns:
262
+ # matches = re.findall(pattern, text, re.IGNORECASE)
263
+ # for match in matches:
264
+ # if len(match.split()) >= 2 and len(match) > 4:
265
+ # return match.strip()
266
 
267
+ # # Enhanced Address patterns for STREET
268
+ # elif "STREET" in field:
269
+ # street_patterns = [
270
+ # # Standard address patterns with numbers
271
+ # r'\b(\d+\s+[A-Z][A-Za-z\s]+(?:ST|STREET|AVE|AVENUE|RD|ROAD|DR|DRIVE|LN|LANE|CT|COURT|BLVD|BOULEVARD|PL|PLACE|WAY|CIR|CIRCLE|PKWY|PARKWAY)\.?)\b',
272
+ # # Address after keywords
273
+ # r'(?:address|street|location|property)[\s:]+(\d+\s+[A-Za-z][A-Za-z\s]+)',
274
+ # # Full address patterns
275
+ # r'\b(\d{1,5}\s+[A-Za-z][A-Za-z\s]+ (?:Street|Avenue|Road|Drive|Lane|Court|Boulevard|Place|Way|Circle|Parkway))\b',
276
+ # # Address in format: number + name + abbreviated suffix
277
+ # r'\b(\d{1,5}\s+[A-Za-z][A-Za-z\s]+\s+(?:St|Ave|Rd|Dr|Ln|Ct|Blvd|Pl|Way|Cir|Pkwy)\.?)\b',
278
+ # # Property address patterns
279
+ # r'(?:property|insured|loss)[\s\w]*address[\s:]*(\d+\s+[A-Za-z][A-Za-z\s]+)',
280
+ # # Address before city/state
281
+ # r'(\d+\s+[A-Za-z][A-Za-z\s]+?)(?:,\s*[A-Z][a-z]+\s*,?\s*[A-Z]{2})',
282
+ # # General number + street name pattern
283
+ # r'\b(\d{1,5}\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*(?:\s+(?:St|Ave|Rd|Dr|Ln|Ct|Blvd|Pl|Way|Street|Avenue|Road|Drive|Lane|Court|Boulevard|Place))?)\b'
284
+ # ]
285
+ # for pattern in street_patterns:
286
+ # match = re.search(pattern, text, re.IGNORECASE)
287
+ # if match:
288
+ # street = match.group(1).strip()
289
+ # # Validate it looks like a street address
290
+ # if re.match(r'\d+\s+[A-Za-z]', street) and len(street) > 5:
291
+ # return street
292
 
293
+ # # City patterns
294
+ # elif "CITY" in field:
295
+ # city_patterns = [
296
+ # r'(?:city)[\s:]+([A-Z][a-z\s]+?)(?:,|\s+[A-Z]{2}\s|\n)',
297
+ # r',\s*([A-Z][a-z\s]+?)\s+[A-Z]{2}\s+\d{5}',
298
+ # r'\b([A-Z][a-z]+(?: [A-Z][a-z]+)*),\s*[A-Z]{2}\b'
299
+ # ]
300
+ # for pattern in city_patterns:
301
+ # match = re.search(pattern, text)
302
+ # if match:
303
+ # city = match.group(1).strip()
304
+ # if len(city) > 2:
305
+ # return city
306
 
307
+ # # State patterns
308
+ # elif "STATE" in field:
309
+ # state_patterns = [
310
+ # r'\b([A-Z]{2})\s+\d{5}',
311
+ # r'(?:state)[\s:]+([A-Z]{2})\b',
312
+ # r',\s*[A-Za-z\s]+,?\s*([A-Z]{2})\s+\d{5}'
313
+ # ]
314
+ # for pattern in state_patterns:
315
+ # match = re.search(pattern, text)
316
+ # if match:
317
+ # return match.group(1)
318
 
319
+ # # ZIP patterns
320
+ # elif "ZIP" in field:
321
+ # zip_patterns = [
322
+ # r'\b(\d{5}-\d{4})\b',
323
+ # r'\b(\d{5})\b(?!\d)'
324
+ # ]
325
+ # for pattern in zip_patterns:
326
+ # match = re.search(pattern, text)
327
+ # if match:
328
+ # return match.group(1)
329
 
330
+ # # Phone patterns
331
+ # elif "PHONE" in field:
332
+ # phone_patterns = [
333
+ # r'\b(\d{3}[-.\s]?\d{3}[-.\s]?\d{4})\b',
334
+ # r'\((\d{3})\)\s*(\d{3})[-.\s]?(\d{4})'
335
+ # ]
336
+ # for pattern in phone_patterns:
337
+ # match = re.search(pattern, text)
338
+ # if match:
339
+ # if len(match.groups()) == 1:
340
+ # return match.group(1)
341
+ # else:
342
+ # return f"({match.group(1)}) {match.group(2)}-{match.group(3)}"
343
 
344
+ # # Email patterns
345
+ # elif "EMAIL" in field:
346
+ # email_pattern = r'\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\b'
347
+ # match = re.search(email_pattern, text)
348
+ # if match:
349
+ # return match.group(1)
350
 
351
+ # # Policy/Claim number patterns
352
+ # elif "POLICY" in field or "CLAIM" in field:
353
+ # number_patterns = [
354
+ # r'(?:policy|claim)[\s#:]*([A-Z0-9\-]+)',
355
+ # r'\b([A-Z]{2,4}\d{6,})\b',
356
+ # r'\b(\d{8,})\b'
357
+ # ]
358
+ # for pattern in number_patterns:
359
+ # match = re.search(pattern, text, re.IGNORECASE)
360
+ # if match:
361
+ # return match.group(1)
362
 
363
+ # # Mortgage patterns
364
+ # elif "MORTGAGE" in field:
365
+ # mortgage_patterns = [
366
+ # r'(?:mortgage[e]?|lender)[\s:]+([A-Z][A-Za-z\s&]+?)(?:\n|$|address)',
367
+ # r'\b([A-Z][a-z]+ (?:Bank|Mortgage|Financial|Credit Union|Lending))\b'
368
+ # ]
369
+ # for pattern in mortgage_patterns:
370
+ # match = re.search(pattern, text, re.IGNORECASE)
371
+ # if match:
372
+ # return match.group(1).strip()
373
 
374
+ # # Enhanced TOL Code patterns
375
+ # elif "TOL" in field or "CODE" in field:
376
+ # code_patterns = [
377
+ # # Direct TOL/code patterns
378
+ # r'(?:tol|type\s*of\s*loss)[\s:]*code[\s:]*([A-Z0-9\-]+)',
379
+ # r'(?:tol|code)[\s:]*([A-Z0-9\-]+)',
380
+ # r'(?:type\s*of\s*loss)[\s:]*([A-Z0-9\-]+)',
381
+ # # Loss type codes
382
+ # r'(?:loss\s*type|cause\s*of\s*loss)[\s:]*([A-Z0-9\-]+)',
383
+ # # Peril codes
384
+ # r'(?:peril|coverage)[\s:]*code[\s:]*([A-Z0-9\-]+)',
385
+ # # General code patterns in insurance context
386
+ # r'(?:claim|loss|damage)[\s\w]*code[\s:]*([A-Z0-9\-]+)',
387
+ # # Alphanumeric codes (common format)
388
+ # r'\b([A-Z]{1,3}\d{1,4})\b',
389
+ # r'\b([A-Z]{2,4}-?\d{2,4})\b',
390
+ # # Fire, water, wind codes
391
+ # r'(?:fire|water|wind|storm|hail)[\s:]*([A-Z0-9\-]+)',
392
+ # # Coverage codes
393
+ # r'(?:coverage|section)[\s:]*([A-Z]\d*)',
394
+ # # Standalone codes that might be TOL
395
+ # r'\b([A-Z]\d{2,3})\b',
396
+ # r'\b([A-Z]{2}\d{1,2})\b'
397
+ # ]
398
+ # for pattern in code_patterns:
399
+ # matches = re.findall(pattern, text, re.IGNORECASE)
400
+ # for match in matches:
401
+ # # Filter out common false positives
402
+ # if (len(match) >= 2 and
403
+ # not match.lower() in ['tx', 'ca', 'ny', 'fl'] and # state codes
404
+ # not match.isdigit() and # pure numbers
405
+ # not re.match(r'^\d{5}$', match)): # zip codes
406
+ # return match.upper()
407
 
408
+ # return "N/A"
409
 
410
+ # def _enhanced_fallback_extraction(self, text: str, placeholders: List[str]) -> Dict[str, str]:
411
+ # """Enhanced fallback extraction with better pattern matching"""
412
+ # st.info("πŸ” Using enhanced pattern matching extraction...")
413
 
414
+ # fallback_data = {}
415
 
416
+ # for placeholder in placeholders:
417
+ # value = self._extract_single_field(text, placeholder)
418
+ # fallback_data[placeholder] = value
419
 
420
+ # return fallback_data
421
 
422
+ # def populate_docx_template(self, docx_file, extracted_data: Dict[str, str]) -> str:
423
+ # """Populate DOCX template with extracted data"""
424
+ # try:
425
+ # with tempfile.NamedTemporaryFile(delete=False, suffix='.docx') as tmp_file:
426
+ # tmp_file.write(docx_file.read())
427
+ # tmp_file_path = tmp_file.name
428
 
429
+ # doc = Document(tmp_file_path)
430
 
431
+ # # Track replacements made
432
+ # replacements_made = 0
433
 
434
+ # for paragraph in doc.paragraphs:
435
+ # for placeholder, value in extracted_data.items():
436
+ # if f"[{placeholder}]" in paragraph.text:
437
+ # paragraph.text = paragraph.text.replace(f"[{placeholder}]", str(value))
438
+ # replacements_made += 1
439
 
440
+ # for table in doc.tables:
441
+ # for row in table.rows:
442
+ # for cell in row.cells:
443
+ # for paragraph in cell.paragraphs:
444
+ # for placeholder, value in extracted_data.items():
445
+ # if f"[{placeholder}]" in paragraph.text:
446
+ # paragraph.text = paragraph.text.replace(f"[{placeholder}]", str(value))
447
+ # replacements_made += 1
448
 
449
+ # timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
450
+ # output_filename = f"GLR_Report_{timestamp}.docx"
451
+ # output_path = self.output_dir / output_filename
452
 
453
+ # doc.save(str(output_path))
454
+ # os.unlink(tmp_file_path)
455
 
456
+ # st.info(f"Made {replacements_made} field replacements in the template")
457
+ # return str(output_path)
458
 
459
+ # except Exception as e:
460
+ # st.error(f"Error populating template: {str(e)}")
461
+ # return None
462
 
463
+ # def main():
464
+ # """Main Streamlit application"""
465
+ # st.title("πŸ“‹ GLR Pipeline Automation (Enhanced)")
466
+ # st.markdown("**Automate General Loss Report generation with improved data extraction**")
467
 
468
+ # # Initialize processor
469
+ # processor = GLRProcessor()
470
 
471
+ # col1, col2 = st.columns([1, 1])
472
 
473
+ # with col1:
474
+ # st.header("πŸ“„ Upload Template")
475
+ # template_file = st.file_uploader("Upload GLR Template (.docx)", type=['docx'])
476
 
477
+ # with col2:
478
+ # st.header("πŸ“‹ Upload Reports")
479
+ # photo_reports = st.file_uploader("Upload PDF Reports", type=['pdf'], accept_multiple_files=True)
480
 
481
+ # if template_file and photo_reports:
482
+ # st.header("πŸ”„ Processing")
483
 
484
+ # with st.spinner("Processing..."):
485
+ # placeholders = processor.extract_placeholders_from_docx(template_file)
486
 
487
+ # if placeholders:
488
+ # st.success(f"Found {len(placeholders)} placeholders: {', '.join(placeholders)}")
489
 
490
+ # pdf_texts = []
491
+ # for pdf_file in photo_reports:
492
+ # text = processor.extract_text_from_pdf(pdf_file)
493
+ # if text:
494
+ # pdf_texts.append(text)
495
+ # st.success(f"βœ… Processed {pdf_file.name} ({len(text)} characters)")
496
 
497
+ # if pdf_texts:
498
+ # extracted_data = processor.extract_data_with_groq(pdf_texts, placeholders)
499
 
500
+ # st.subheader("πŸ“Š Extracted Data")
501
 
502
+ # # Show extraction statistics
503
+ # non_na_count = sum(1 for v in extracted_data.values() if v != "N/A")
504
+ # extraction_rate = (non_na_count / len(extracted_data)) * 100
505
+ # st.metric("Extraction Success Rate", f"{extraction_rate:.1f}%", f"{non_na_count}/{len(extracted_data)} fields")
506
 
507
+ # # Create form for editing data
508
+ # with st.form("data_form"):
509
+ # edited_data = {}
510
+ # for placeholder, value in extracted_data.items():
511
+ # # Color code based on whether value was found
512
+ # if value != "N/A":
513
+ # st.markdown(f"**{placeholder}** βœ…")
514
+ # else:
515
+ # st.markdown(f"**{placeholder}** ❌")
516
+ # edited_data[placeholder] = st.text_input(f"", value=value, key=placeholder)
517
 
518
+ # generate_report = st.form_submit_button("Generate Report", type="primary")
519
 
520
+ # # Handle report generation outside the form
521
+ # if generate_report:
522
+ # template_file.seek(0)
523
+ # output_path = processor.populate_docx_template(template_file, edited_data)
524
 
525
+ # if output_path:
526
+ # st.success("βœ… Report generated successfully!")
527
 
528
+ # # Store the file data in session state for download
529
+ # with open(output_path, 'rb') as file:
530
+ # st.session_state['report_data'] = file.read()
531
+ # st.session_state['report_filename'] = os.path.basename(output_path)
532
 
533
+ # # Download button outside the form
534
+ # if 'report_data' in st.session_state:
535
+ # st.download_button(
536
+ # label="πŸ“₯ Download Generated Report",
537
+ # data=st.session_state['report_data'],
538
+ # file_name=st.session_state['report_filename'],
539
+ # mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
540
+ # type="primary"
541
+ # )
542
+ # else:
543
+ # st.error("No text extracted from PDFs")
544
+ # else:
545
+ # st.error("No placeholders found in template")
546
 
547
+ # # Instructions and setup for Hugging Face Spaces
548
+ # st.header("πŸ“– Hugging Face Spaces Setup")
549
+ # st.markdown("""
550
+ # **This app is configured for Hugging Face Spaces deployment:**
551
 
552
+ # **Step 1: Set up your Space**
553
+ # - Create a new Space on Hugging Face with Streamlit SDK
554
+ # - Upload the app files (app.py, requirements.txt, packages.txt)
555
 
556
+ # **Step 2: Configure API Key**
557
+ # - Go to your Space Settings β†’ Repository secrets
558
+ # - Add a new secret named `Key` with your Groq API key value
559
+ # - Get your free API key from [console.groq.com](https://console.groq.com/)
560
 
561
+ # **Step 3: Use the App**
562
+ # 1. Upload a DOCX template with placeholders like `[DATE_LOSS]`, `[INSURED_NAME]`
563
+ # 2. Upload PDF reports containing the data to extract
564
+ # 3. Review and edit the AI-extracted data
565
+ # 4. Generate and download the completed report
566
+ # """)
567
 
568
+ # # Show API key status
569
+ # if not processor.api_key:
570
+ # st.error("⚠️ **API Key Not Found** - Please set the 'Key' secret in your Hugging Face Space settings.")
571
+ # elif processor.groq_client:
572
+ # st.success("βœ… **API Key Configured** - Ready to process documents!")
573
+ # else:
574
+ # st.error("❌ **API Key Error** - Please check your API key configuration in Space secrets")
575
 
576
+ # if __name__ == "__main__":
577
+ # main()