Seth0330 commited on
Commit
0ce55d2
·
verified ·
1 Parent(s): b23347b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +105 -392
app.py CHANGED
@@ -1,5 +1,4 @@
1
  import streamlit as st
2
- from main import read_pdf, extract_key_phrases, score_sentences, summarize_text
3
  import io
4
  import requests
5
  import json
@@ -7,6 +6,8 @@ import re
7
  import os
8
  from datetime import datetime
9
 
 
 
10
  # Configure Streamlit
11
  st.set_page_config(
12
  page_title="PDF Tools - Summarizer & Invoice Extractor",
@@ -50,442 +51,154 @@ MODELS = {
50
  }
51
 
52
  def get_api_key(model_choice):
53
- """Get the appropriate API key based on model choice"""
54
- api_key_env = MODELS[model_choice]["api_key_env"]
55
- api_key = os.environ.get(api_key_env)
56
  if not api_key:
57
- st.error(f"❌ `{api_key_env}` environment variable not set!")
58
  st.stop()
59
  return api_key
60
 
61
  def query_llm(model_choice, prompt):
62
- """Call the appropriate API based on model choice"""
63
  config = MODELS[model_choice]
64
  headers = {
65
  "Authorization": f"Bearer {get_api_key(model_choice)}",
66
  "Content-Type": "application/json",
67
  }
68
-
69
- if "extra_headers" in config:
70
  headers.update(config["extra_headers"])
71
-
72
  payload = {
73
  "model": config["model_name"],
74
  "messages": [{"role": "user", "content": prompt}],
75
  "temperature": 0.1,
76
  "max_tokens": 2000,
77
  }
78
-
79
- if config["response_format"]:
80
  payload["response_format"] = config["response_format"]
81
-
82
  try:
83
  with st.spinner(f"🔍 Analyzing with {model_choice}..."):
84
- response = requests.post(config["api_url"], headers=headers, json=payload, timeout=90)
85
-
86
- if response.status_code != 200:
87
- st.error(f"🚨 API Error {response.status_code}: {response.text}")
88
- return None
89
-
90
- try:
91
- content = response.json()["choices"][0]["message"]["content"]
92
- st.session_state.last_api_response = content
93
- st.session_state.last_api_response_raw = response.text
94
- return content
95
- except KeyError as e:
96
- st.error(f"KeyError in response: {e}\nFull response: {response.json()}")
97
  return None
98
-
 
 
 
99
  except requests.exceptions.RequestException as e:
100
- st.error(f"🌐 Connection Failed: {str(e)}")
101
  return None
102
 
103
- def find_json_end(text):
104
- """Find the end of a potentially incomplete JSON object"""
105
- stack = []
106
- for i, c in enumerate(text):
107
- if c == '{':
108
- stack.append(i)
109
- elif c == '}':
110
- if stack:
111
- stack.pop()
112
- if not stack:
113
- return i+1
114
- return -1
115
-
116
- def clean_json_response(text, model_choice):
117
- """Robust JSON extraction with advanced error handling"""
118
  if not text:
119
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- original_text = text # Save for error reporting
122
-
123
- # Model-specific preprocessing
124
- if model_choice == "Mistral Small":
125
- # Remove all markdown formatting
126
- text = re.sub(r'^```json|```$', '', text, flags=re.MULTILINE).strip()
127
-
128
- # Common JSON repair patterns
129
- repair_attempts = [
130
- # Try extracting JSON from markdown
131
- lambda t: re.search(r'```(?:json)?\n({.*?})\n```', t, re.DOTALL),
132
- # Try finding the outermost JSON object
133
- lambda t: {'start': t.find('{'), 'end': t.rfind('}')+1},
134
- # Try last valid JSON fragment
135
- lambda t: {'start': 0, 'end': find_json_end(t)}
136
- ]
137
-
138
- for attempt in repair_attempts:
139
- try:
140
- result = attempt(text)
141
- if not result:
142
- continue
143
-
144
- if isinstance(result, re.Match):
145
- json_str = result.group(1)
146
- else:
147
- start, end = result['start'], result['end']
148
- if start >= 0 and end > start:
149
- json_str = text[start:end]
150
- else:
151
- continue
152
-
153
- data = json.loads(json_str)
154
-
155
- # Ensure required structure exists
156
- if model_choice in ["Llama 4 Mavericks", "Mistral Small"]:
157
- if "invoice_header" not in data:
158
- data["invoice_header"] = {}
159
- if "line_items" not in data:
160
- data["line_items"] = []
161
-
162
- return data
163
-
164
- except (json.JSONDecodeError, AttributeError, KeyError) as e:
165
- continue
166
-
167
- # Final fallback - manual reconstruction for Llama
168
- if model_choice == "Llama 4 Mavericks":
169
- try:
170
- if '"invoice_header":' in text:
171
- header_part = text.split('"line_items":')[0] if '"line_items":' in text else text
172
- if not header_part.strip().endswith('}'):
173
- header_part += '}'
174
- data = json.loads(header_part + ('"line_items": []}' if '"line_items":' not in text else ''))
175
- data["line_items"] = data.get("line_items", [])
176
- return data
177
- except:
178
- pass
179
-
180
- st.error(f"Failed to parse JSON after multiple attempts for {model_choice}")
181
- st.code(f"Original response:\n{original_text}")
182
- return None
183
 
184
  def get_extraction_prompt(model_choice, text):
185
- """Return the appropriate prompt based on model choice"""
186
  if model_choice == "DeepSeek v3":
187
- return f"""Extract complete invoice information from the text below and return ONLY a valid JSON object with these fields:
188
- {{
189
- "invoice_number": "string",
190
- "invoice_date": "YYYY-MM-DD",
191
- "po_number": "string or null",
192
- "invoice_value": "string with currency symbol",
193
- "line_items": [
194
- {{
195
- "description": "string",
196
- "quantity": "number or string",
197
- "unit_price": "string with currency",
198
- "total_price": "string with currency"
199
- }}
200
- ]
201
- }}
202
- Rules:
203
- 1. Return ONLY valid JSON (no additional text or markdown)
204
- 2. Use null for missing fields
205
- 3. Include all line items found in the invoice
206
- 4. For line items, quantity can be number or string, prices should include currency
207
- 5. Do not include any explanations or notes
208
- Invoice Text:
209
- """ + text
210
-
211
  elif model_choice == "DeepSeek R1":
212
- return f"""Please extract the following information from the invoice text below and return ONLY the raw JSON without any markdown formatting or additional text:
213
- {{
214
- "invoice_number": "string or null",
215
- "invoice_date": "YYYY-MM-DD or null",
216
- "po_number": "string or null",
217
- "invoice_value": "string with currency or null",
218
- "line_items": [
219
- {{
220
- "description": "string",
221
- "quantity": "number or string",
222
- "unit_price": "string with currency",
223
- "total_price": "string with currency"
224
- }}
225
- ]
226
- }}
227
- Invoice Text:
228
- """ + text
229
-
230
- else: # For Llama 4 and Mistral
231
- return f"""Extract complete invoice information and return a VALID JSON object with these fields:
232
- {{
233
- "invoice_header": {{
234
- "invoice_number": "string",
235
- "invoice_date": "YYYY-MM-DD",
236
- "po_number": "string or null",
237
- "invoice_value": "string with currency",
238
- "supplier_name": "string or null",
239
- "customer_name": "string or null"
240
- }},
241
- "line_items": [
242
- {{
243
- "item_number": "string or null",
244
- "description": "string",
245
- "quantity": "number",
246
- "unit_price": "string with currency",
247
- "total_price": "string with currency"
248
- }}
249
- ]
250
- }}
251
- Rules:
252
- 1. Return ONLY valid JSON (no additional text or markdown)
253
- 2. Use null for missing fields
254
- 3. Date format must be YYYY-MM-DD
255
- 4. All currency values must include currency symbol or code
256
- 5. Include all line items found in the invoice
257
- 6. For line items, quantity should be a number, prices as strings with currency
258
- 7. Do not include any explanations or notes
259
- Invoice Text:
260
- """ + text
261
-
262
- def format_currency(value):
263
- """Helper function to format currency values consistently"""
264
- if not value:
265
- return "N/A"
266
- if isinstance(value, (int, float)):
267
- return f"${value:,.2f}"
268
- return value
269
-
270
- def display_line_items(line_items, model_choice="DeepSeek v3"):
271
- """Display line items in a formatted table"""
272
- if not line_items:
273
- st.info("No line items found in this invoice.")
274
- return
275
-
276
- st.subheader("📋 Line Items")
277
-
278
- if model_choice in ["Llama 4 Mavericks", "Mistral Small"]:
279
- # Display as a table for Llama/Mistral
280
- items_display = []
281
- for idx, item in enumerate(line_items, 1):
282
- items_display.append({
283
- "#": idx,
284
- "Description": item.get("description", "N/A"),
285
- "Quantity": item.get("quantity", 0),
286
- "Unit Price": item.get("unit_price", "N/A"),
287
- "Total Price": item.get("total_price", "N/A")
288
- })
289
- st.table(items_display)
290
  else:
291
- # Display in columns for DeepSeek models
292
- cols = st.columns([4, 2, 2, 2])
293
- with st.container():
294
- cols[0].write("**Description**")
295
- cols[1].write("**Qty**")
296
- cols[2].write("**Unit Price**")
297
- cols[3].write("**Total**")
298
-
299
- for item in line_items:
300
- cols = st.columns([4, 2, 2, 2])
301
- cols[0].write(item.get("description", "N/A"))
302
- cols[1].write(item.get("quantity", "N/A"))
303
- cols[2].write(format_currency(item.get("unit_price", "N/A")))
304
- cols[3].write(format_currency(item.get("total_price", "N/A")))
305
- st.divider()
306
-
307
- def display_invoice_data(model_choice, invoice_data):
308
- if not invoice_data:
309
- return
310
-
311
- if model_choice in ["Llama 4 Mavericks", "Mistral Small"]:
312
- # Display header information
313
- st.subheader("Invoice Summary")
314
- header = invoice_data.get("invoice_header", {})
315
-
316
- col1, col2, col3 = st.columns(3)
317
- with col1:
318
- st.metric("Invoice Number", header.get("invoice_number", "Not found"))
319
- st.metric("Supplier", header.get("supplier_name", "Not found"))
320
- with col2:
321
- st.metric("Invoice Date", header.get("invoice_date", "Not found"))
322
- st.metric("Customer", header.get("customer_name", "Not found"))
323
- with col3:
324
- st.metric("PO Number", header.get("po_number", "Not found"))
325
- st.metric("Total Value", header.get("invoice_value", "Not found"))
326
-
327
- # Display line items
328
- display_line_items(invoice_data.get("line_items", []), model_choice)
329
-
330
- # Calculate and display subtotal if not provided in header
331
- if not header.get("invoice_value"):
332
- try:
333
- total = sum(float(re.sub(r'[^\d.]', '', item.get("total_price", "0")))
334
- for item in invoice_data.get("line_items", []) if item.get("total_price"))
335
- st.metric("Calculated Total", f"${total:,.2f}")
336
- except:
337
- pass
338
-
339
- else:
340
- # Display for DeepSeek models
341
- st.success("Information extracted successfully!")
342
-
343
- col1, col2 = st.columns(2)
344
- with col1:
345
- st.metric("Invoice Number", invoice_data.get("invoice_number", "Not found"))
346
- st.metric("PO Number", invoice_data.get("po_number", "Not found"))
347
-
348
- with col2:
349
- st.metric("Invoice Date", invoice_data.get("invoice_date", "Not found"))
350
- st.metric("Invoice Value", format_currency(invoice_data.get("invoice_value")))
351
-
352
- # Display line items for both DeepSeek models
353
- display_line_items(invoice_data.get("line_items", []), model_choice)
354
 
355
  def extract_invoice_info(model_choice, text):
356
- """Extract structured data from pasted text"""
357
  prompt = get_extraction_prompt(model_choice, text)
358
  result = query_llm(model_choice, prompt)
359
-
360
  if not result:
361
  return None
362
-
363
- parsed_data = clean_json_response(result, model_choice)
364
- if not parsed_data:
365
- st.error("Failed to parse JSON. Raw response:")
366
- st.code(result)
367
  return None
368
-
369
- # Normalize data structure based on model
370
  if model_choice in ["Llama 4 Mavericks", "Mistral Small"]:
371
- if "invoice_header" not in parsed_data:
372
- parsed_data["invoice_header"] = {}
373
- if "line_items" not in parsed_data:
374
- parsed_data["line_items"] = []
375
-
376
- # Set default values for header if missing
377
- header_fields = ["invoice_number", "invoice_date", "po_number", "invoice_value", "supplier_name", "customer_name"]
378
- for field in header_fields:
379
- if field not in parsed_data["invoice_header"]:
380
- parsed_data["invoice_header"][field] = None
381
-
382
- # Validate line items structure
383
- for item in parsed_data["line_items"]:
384
- item_fields = ["item_number", "description", "quantity", "unit_price", "total_price"]
385
- for field in item_fields:
386
- if field not in item:
387
- item[field] = None if field != "quantity" else 0
388
- if field == "quantity" and not isinstance(item[field], (int, float)):
389
- try:
390
- item[field] = float(item[field])
391
- except (ValueError, TypeError):
392
- item[field] = 0
393
-
394
- else: # DeepSeek models
395
- # Ensure all required fields exist
396
- for field in ["invoice_number", "invoice_date", "po_number", "invoice_value"]:
397
- if field not in parsed_data:
398
- parsed_data[field] = None
399
-
400
- # Ensure line_items exists and has proper structure
401
- if "line_items" not in parsed_data:
402
- parsed_data["line_items"] = []
403
- else:
404
- for item in parsed_data["line_items"]:
405
- item_fields = ["description", "quantity", "unit_price", "total_price"]
406
- for field in item_fields:
407
- if field not in item:
408
- item[field] = None if field != "quantity" else 0
409
-
410
- return parsed_data
411
 
412
- # Create tabs for different functionalities
413
  tab1, tab2 = st.tabs(["PDF Summarizer", "Invoice Extractor"])
414
 
415
- # PDF Summarizer Tab
416
  with tab1:
417
- st.title("PDF to Bullet Point Summarizer 🗟 🔏")
418
-
419
- # File uploader for the PDF
420
- uploaded_file = st.file_uploader("Upload your PDF document", type="pdf", key="pdf_uploader")
421
-
422
- # Slider for users to select the summarization extent
423
- summary_scale = st.slider("Select the extent of summarization (%)", min_value=1, max_value=100, value=20, key="summary_scale")
424
-
425
- # Submit button
426
- submit_button = st.button("Generate Summary", key="summary_button")
427
-
428
- # Check if the submit button is pressed
429
- if submit_button and uploaded_file is not None:
430
- with st.spinner('Processing...'):
431
- # Read the PDF content
432
- text = read_pdf(io.BytesIO(uploaded_file.getvalue()))
433
-
434
- # Extract key phrases from the text
435
- key_phrases = extract_key_phrases(text)
436
-
437
- # Score sentences based on the key phrases
438
- sentence_scores = score_sentences(text, key_phrases)
439
-
440
- # Determine the number of bullet points based on the selected summarization scale
441
- total_sentences = len(list(sentence_scores.keys()))
442
- num_points = max(1, total_sentences * summary_scale // 100)
443
-
444
- # Generate the bullet-point summary
445
- summary = summarize_text(sentence_scores, num_points=num_points)
446
-
447
- # Display the summary as bullet points
448
- st.subheader("Here's the summary: ")
449
- st.markdown(summary)
450
 
451
- # Invoice Extractor Tab
452
  with tab2:
453
  st.title("📋 Invoice Extractor from PDF")
454
- st.write("Upload an invoice PDF to extract key details")
455
-
456
- # Model selection
457
- model_choice = st.selectbox(
458
- "Select AI Model",
459
- list(MODELS.keys()),
460
- index=0,
461
- help="Choose which AI model to use for extraction",
462
- key="model_choice"
463
- )
464
-
465
- # File uploader for the invoice PDF
466
- invoice_pdf = st.file_uploader("Upload Invoice PDF", type="pdf", key="invoice_pdf_uploader")
467
-
468
- if st.button("Extract Invoice Information", key="invoice_button") and invoice_pdf is not None:
469
- with st.spinner('Reading PDF...'):
470
- # Read the PDF content
471
- invoice_text = read_pdf(io.BytesIO(invoice_pdf.getvalue()))
472
-
473
- # Process in status container
474
- with st.status("Processing...", expanded=True) as status:
475
- st.write(f"🤖 Querying {model_choice} API...")
476
- invoice_data = extract_invoice_info(model_choice, invoice_text)
477
-
478
- if invoice_data:
479
- status.update(label="✅ Extraction Complete!", state="complete")
480
- display_invoice_data(model_choice, invoice_data)
481
  else:
482
- status.update(label="❌ Extraction Failed", state="error")
483
- st.error("Failed to extract information. Try simplifying the text.")
484
-
485
- # Debug information outside the status container
486
- if invoice_data and "last_api_response" in st.session_state:
487
- with st.expander("Debug Information"):
488
- st.write("API Response:")
489
- st.json(st.session_state.last_api_response)
490
- st.write("Raw API Response:")
491
- st.code(st.session_state.get("last_api_response_raw", "No response"))
 
 
 
 
 
1
  import streamlit as st
 
2
  import io
3
  import requests
4
  import json
 
6
  import os
7
  from datetime import datetime
8
 
9
+ from main import read_pdf, extract_key_phrases, score_sentences, summarize_text
10
+
11
  # Configure Streamlit
12
  st.set_page_config(
13
  page_title="PDF Tools - Summarizer & Invoice Extractor",
 
51
  }
52
 
53
  def get_api_key(model_choice):
54
+ api_key = os.environ.get(MODELS[model_choice]["api_key_env"])
 
 
55
  if not api_key:
56
+ st.error(f"❌ {MODELS[model_choice]['api_key_env']} environment variable not set!")
57
  st.stop()
58
  return api_key
59
 
60
  def query_llm(model_choice, prompt):
 
61
  config = MODELS[model_choice]
62
  headers = {
63
  "Authorization": f"Bearer {get_api_key(model_choice)}",
64
  "Content-Type": "application/json",
65
  }
66
+ if config.get("extra_headers"):
 
67
  headers.update(config["extra_headers"])
68
+
69
  payload = {
70
  "model": config["model_name"],
71
  "messages": [{"role": "user", "content": prompt}],
72
  "temperature": 0.1,
73
  "max_tokens": 2000,
74
  }
75
+ if config.get("response_format"):
 
76
  payload["response_format"] = config["response_format"]
77
+
78
  try:
79
  with st.spinner(f"🔍 Analyzing with {model_choice}..."):
80
+ resp = requests.post(config["api_url"], headers=headers, json=payload, timeout=90)
81
+ if resp.status_code != 200:
82
+ st.error(f"🚨 API Error {resp.status_code}: {resp.text}")
 
 
 
 
 
 
 
 
 
 
83
  return None
84
+ content = resp.json()["choices"][0]["message"]["content"]
85
+ st.session_state.last_api_response = content
86
+ st.session_state.last_api_response_raw = resp.text
87
+ return content
88
  except requests.exceptions.RequestException as e:
89
+ st.error(f"🌐 Connection Failed: {e}")
90
  return None
91
 
92
+ def clean_json_response(text):
93
+ """Strip code fences and extract a valid JSON segment."""
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  if not text:
95
  return None
96
+ original = text
97
+ # Remove any ``` or ```json fences
98
+ text = re.sub(r'```(?:json)?', '', text)
99
+ text = text.strip()
100
+
101
+ # Find the JSON object boundaries
102
+ start = text.find('{')
103
+ end = text.rfind('}') + 1
104
+ if start == -1 or end == 0:
105
+ st.error("Failed to locate JSON in the response.")
106
+ st.code(original)
107
+ return None
108
+ json_str = text[start:end]
109
 
110
+ try:
111
+ return json.loads(json_str)
112
+ except json.JSONDecodeError as e:
113
+ st.error(f"JSON decode error: {e}")
114
+ st.code(json_str)
115
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  def get_extraction_prompt(model_choice, text):
118
+ # (Prompts abbreviated here for readability—use your existing prompt definitions)
119
  if model_choice == "DeepSeek v3":
120
+ return "..." # your DeepSeek v3 prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  elif model_choice == "DeepSeek R1":
122
+ return "..." # your DeepSeek R1 prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  else:
124
+ return "..." # generic Llama/Mistral prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  def extract_invoice_info(model_choice, text):
 
127
  prompt = get_extraction_prompt(model_choice, text)
128
  result = query_llm(model_choice, prompt)
 
129
  if not result:
130
  return None
131
+ data = clean_json_response(result)
132
+ if not data:
 
 
 
133
  return None
134
+
135
+ # Normalize structure
136
  if model_choice in ["Llama 4 Mavericks", "Mistral Small"]:
137
+ header = data.setdefault("invoice_header", {})
138
+ for key in ["invoice_number", "invoice_date", "po_number", "invoice_value", "supplier_name", "customer_name"]:
139
+ header.setdefault(key, None)
140
+ items = data.setdefault("line_items", [])
141
+ for item in items:
142
+ for key in ["item_number", "description", "quantity", "unit_price", "total_price"]:
143
+ item.setdefault(key, None)
144
+ else:
145
+ for key in ["invoice_number", "invoice_date", "po_number", "invoice_value"]:
146
+ data.setdefault(key, None)
147
+ items = data.setdefault("line_items", [])
148
+ for item in items:
149
+ for key in ["description", "quantity", "unit_price", "total_price"]:
150
+ item.setdefault(key, None)
151
+
152
+ return data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ # ---- UI Layout ----
155
  tab1, tab2 = st.tabs(["PDF Summarizer", "Invoice Extractor"])
156
 
 
157
  with tab1:
158
+ st.title("PDF to Bullet Point Summarizer 🗟")
159
+ pdf_file = st.file_uploader("Upload PDF", type="pdf")
160
+ scale = st.slider("Summarization extent (%)", 1, 100, 20)
161
+ if st.button("Generate Summary") and pdf_file:
162
+ text = read_pdf(io.BytesIO(pdf_file.getvalue()))
163
+ phrases = extract_key_phrases(text)
164
+ scores = score_sentences(text, phrases)
165
+ count = max(1, len(scores) * scale // 100)
166
+ summary = summarize_text(scores, num_points=count)
167
+ st.subheader("Summary:")
168
+ st.markdown(summary)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
 
170
  with tab2:
171
  st.title("📋 Invoice Extractor from PDF")
172
+ model_choice = st.selectbox("Select AI Model", list(MODELS.keys()))
173
+ invoice_pdf = st.file_uploader("Upload Invoice PDF", type="pdf")
174
+ if st.button("Extract Invoice Information") and invoice_pdf:
175
+ invoice_text = read_pdf(io.BytesIO(invoice_pdf.getvalue()))
176
+ invoice_data = extract_invoice_info(model_choice, invoice_text)
177
+ if invoice_data:
178
+ st.success("Extraction Complete!")
179
+ if model_choice in ["Llama 4 Mavericks", "Mistral Small"]:
180
+ hdr = invoice_data["invoice_header"]
181
+ c1, c2, c3 = st.columns(3)
182
+ c1.metric("Invoice #", hdr.get("invoice_number"))
183
+ c1.metric("Supplier", hdr.get("supplier_name"))
184
+ c2.metric("Date", hdr.get("invoice_date"))
185
+ c2.metric("Customer", hdr.get("customer_name"))
186
+ c3.metric("PO #", hdr.get("po_number"))
187
+ c3.metric("Total", hdr.get("invoice_value"))
188
+ st.subheader("Line Items")
189
+ st.table(invoice_data["line_items"])
 
 
 
 
 
 
 
 
 
190
  else:
191
+ c1, c2 = st.columns(2)
192
+ c1.metric("Invoice #", invoice_data.get("invoice_number"))
193
+ c1.metric("PO #", invoice_data.get("po_number"))
194
+ c2.metric("Date", invoice_data.get("invoice_date"))
195
+ c2.metric("Value", invoice_data.get("invoice_value"))
196
+ st.subheader("Line Items")
197
+ st.table(invoice_data["line_items"])
198
+
199
+ if "last_api_response" in st.session_state:
200
+ with st.expander("Debug Information"):
201
+ st.write("Extracted content (raw string):")
202
+ st.code(st.session_state.last_api_response)
203
+ st.write("Full HTTP response text:")
204
+ st.code(st.session_state.get("last_api_response_raw", "No response"))