Seth0330 commited on
Commit
dd32a84
·
verified ·
1 Parent(s): 50aaed9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -78
app.py CHANGED
@@ -5,23 +5,17 @@ import re
5
  import os
6
  import time
7
  import mimetypes
 
 
 
 
 
 
8
 
9
  st.set_page_config(page_title="PDF Tools", layout="wide")
10
 
11
- # -------- LLM Model Setup (same as before) --------
12
  MODELS = {
13
- "DeepSeek v3": {
14
- "api_url": "https://api.deepseek.com/v1/chat/completions",
15
- "model": "deepseek-chat",
16
- "key_env": "DEEPSEEK_API_KEY",
17
- "response_format": {"type": "json_object"},
18
- },
19
- "DeepSeek R1": {
20
- "api_url": "https://api.deepseek.com/v1/chat/completions",
21
- "model": "deepseek-reasoner",
22
- "key_env": "DEEPSEEK_API_KEY",
23
- "response_format": None,
24
- },
25
  "OpenAI GPT-4.1": {
26
  "api_url": "https://api.openai.com/v1/chat/completions",
27
  "model": "gpt-4-1106-preview",
@@ -29,16 +23,6 @@ MODELS = {
29
  "response_format": None,
30
  "extra_headers": {},
31
  },
32
- "Mistral Small": {
33
- "api_url": "https://openrouter.ai/api/v1/chat/completions",
34
- "model": "mistralai/ministral-8b",
35
- "key_env": "OPENROUTER_API_KEY",
36
- "response_format": {"type": "json_object"},
37
- "extra_headers": {
38
- "HTTP-Referer": "https://huggingface.co",
39
- "X-Title": "Invoice Extractor",
40
- },
41
- },
42
  }
43
 
44
  def get_api_key(model_choice):
@@ -68,10 +52,7 @@ def query_llm(model_choice, prompt):
68
  with st.spinner(f"🔍 Querying {model_choice}..."):
69
  r = requests.post(cfg["api_url"], headers=headers, json=payload, timeout=90)
70
  if r.status_code != 200:
71
- if "No instances available" in r.text or r.status_code == 503:
72
- st.error(f"{model_choice} is currently unavailable. Please try again later or select another model.")
73
- else:
74
- st.error(f"🚨 API Error {r.status_code}: {r.text}")
75
  return None
76
  content = r.json()["choices"][0]["message"]["content"]
77
  st.session_state.last_api = content
@@ -201,18 +182,6 @@ def extract_invoice_info(model_choice, text):
201
  data = clean_json_response(raw)
202
  if not data:
203
  return None
204
-
205
- if model_choice.startswith("DeepSeek"):
206
- header = {k: v for k, v in data.items() if k != "line_items"}
207
- items = data.get("line_items", [])
208
- if not isinstance(items, list):
209
- items = []
210
- for itm in items:
211
- if not isinstance(itm, dict):
212
- continue
213
- for k in ("description","quantity","unit_price","total_price"):
214
- itm.setdefault(k, None)
215
- return {"invoice_header": header, "line_items": items}
216
  hdr = data.get("invoice_header", {})
217
  if not hdr and any(k in data for k in ("invoice_number","supplier_name","customer_name")):
218
  hdr = data
@@ -230,32 +199,27 @@ def extract_invoice_info(model_choice, text):
230
  itm.setdefault(k, None)
231
  return {"invoice_header": hdr, "line_items": items}
232
 
233
- # --------- File type/content-type detection ---------
234
  def get_content_type(filename):
235
  mime, _ = mimetypes.guess_type(filename)
236
  ext = filename.lower().split('.')[-1]
237
- # Special case for PDF (Unstract quirk)
238
  if ext == "pdf":
239
  return "text/plain"
240
  if mime is None:
241
  return "application/octet-stream"
242
  return mime
243
 
244
- # --------- UNSTRACT API Multi-file PDF/Doc/Image-to-Text ---------
245
  UNSTRACT_BASE = "https://llmwhisperer-api.us-central.unstract.com/api/v2"
246
- UNSTRACT_API_KEY = os.getenv("UNSTRACT_API_KEY") # Set this in your environment!
247
 
248
  def extract_text_from_unstract(uploaded_file):
249
  filename = getattr(uploaded_file, "name", "uploaded_file")
250
  file_bytes = uploaded_file.read()
251
  content_type = get_content_type(filename)
252
-
253
  headers = {
254
  "unstract-key": UNSTRACT_API_KEY,
255
  "Content-Type": content_type,
256
  }
257
  url = f"{UNSTRACT_BASE}/whisper"
258
-
259
  with st.spinner("Uploading and processing document with Unstract..."):
260
  r = requests.post(url, headers=headers, data=file_bytes)
261
  if r.status_code != 202:
@@ -265,9 +229,8 @@ def extract_text_from_unstract(uploaded_file):
265
  if not whisper_hash:
266
  st.error("Unstract: No whisper_hash received.")
267
  return None
268
-
269
  status_url = f"{UNSTRACT_BASE}/whisper-status?whisper_hash={whisper_hash}"
270
- for i in range(30): # Wait up to 60s (2s x 30)
271
  status_r = requests.get(status_url, headers={"unstract-key": UNSTRACT_API_KEY})
272
  if status_r.status_code != 200:
273
  st.error(f"Unstract: Error checking status: {status_r.status_code} - {status_r.text}")
@@ -280,7 +243,6 @@ def extract_text_from_unstract(uploaded_file):
280
  else:
281
  st.error("Unstract: Timeout waiting for OCR to finish.")
282
  return None
283
-
284
  retrieve_url = f"{UNSTRACT_BASE}/whisper-retrieve?whisper_hash={whisper_hash}&text_only=true"
285
  r = requests.get(retrieve_url, headers={"unstract-key": UNSTRACT_API_KEY})
286
  if r.status_code != 200:
@@ -292,11 +254,23 @@ def extract_text_from_unstract(uploaded_file):
292
  except Exception:
293
  return r.text
294
 
295
- # --------- INVOICE EXTRACTOR UI ---------
 
 
 
 
 
 
 
 
 
 
 
 
296
  st.title("Invoice/Document Extractor")
297
  mdl = st.selectbox("Model", list(MODELS.keys()), key="extract_model")
298
  inv_file = st.file_uploader(
299
- "Invoice or Document File",
300
  type=["pdf", "docx", "xlsx", "xls", "png", "jpg", "jpeg", "tiff"]
301
  )
302
  extracted_info = None
@@ -314,34 +288,75 @@ if st.button("Extract") and inv_file:
314
  st.table(extracted_info["line_items"])
315
  st.session_state["last_extracted_info"] = extracted_info # store in session
316
 
317
- # If we've already extracted info, or in this session, show further controls
318
  extracted_info = extracted_info or st.session_state.get("last_extracted_info", None)
319
- if extracted_info:
320
- st.markdown("---")
321
- st.subheader("📝 Fine-tune Extracted Data with Your Own Prompt")
322
- user_prompt = st.text_area(
323
- "Enter your prompt for further processing or transformation (the extracted JSON will be available as context).",
324
- height=120,
325
- key="custom_prompt"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  )
327
- model_2 = st.selectbox("Model for Fine-Tuning Prompt", list(MODELS.keys()), key="refine_model")
328
- if st.button("Run Custom Prompt"):
329
- refine_input = (
330
- "Here is an extracted invoice in JSON format:\n"
331
- f"{json.dumps(extracted_info, indent=2)}\n"
332
- "Follow this instruction and return the result as a JSON object only (no explanation):\n"
333
- f"{user_prompt}"
334
- )
335
- result = query_llm(model_2, refine_input)
336
- refined_json = clean_json_response(result)
337
- st.subheader("Fine-Tuned Output")
338
- if refined_json:
339
- st.json(refined_json)
340
- else:
341
- st.error("Could not parse a valid JSON output from the model.")
342
- st.caption("The prompt is run on the above-extracted fields as JSON. Try instructions like: 'Add a new field for net_amount (amount minus tax) to each line item', or 'Summarize the total quantity ordered', etc.")
343
 
344
- if "last_api" in st.session_state:
345
- with st.expander("Debug"):
346
- st.code(st.session_state.last_api)
347
- st.code(st.session_state.last_raw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import os
6
  import time
7
  import mimetypes
8
+ import pandas as pd
9
+
10
+ # NEW: LangGraph & LangChain imports
11
+ from langchain_community.chat_models import ChatOpenAI
12
+ from langgraph.graph import StateGraph, END
13
+ from langgraph.prebuilt import create_react_agent
14
 
15
  st.set_page_config(page_title="PDF Tools", layout="wide")
16
 
17
+ # -------- LLM Model Setup (your unchanged code) --------
18
  MODELS = {
 
 
 
 
 
 
 
 
 
 
 
 
19
  "OpenAI GPT-4.1": {
20
  "api_url": "https://api.openai.com/v1/chat/completions",
21
  "model": "gpt-4-1106-preview",
 
23
  "response_format": None,
24
  "extra_headers": {},
25
  },
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
  def get_api_key(model_choice):
 
52
  with st.spinner(f"🔍 Querying {model_choice}..."):
53
  r = requests.post(cfg["api_url"], headers=headers, json=payload, timeout=90)
54
  if r.status_code != 200:
55
+ st.error(f"🚨 API Error {r.status_code}: {r.text}")
 
 
 
56
  return None
57
  content = r.json()["choices"][0]["message"]["content"]
58
  st.session_state.last_api = content
 
182
  data = clean_json_response(raw)
183
  if not data:
184
  return None
 
 
 
 
 
 
 
 
 
 
 
 
185
  hdr = data.get("invoice_header", {})
186
  if not hdr and any(k in data for k in ("invoice_number","supplier_name","customer_name")):
187
  hdr = data
 
199
  itm.setdefault(k, None)
200
  return {"invoice_header": hdr, "line_items": items}
201
 
 
202
  def get_content_type(filename):
203
  mime, _ = mimetypes.guess_type(filename)
204
  ext = filename.lower().split('.')[-1]
 
205
  if ext == "pdf":
206
  return "text/plain"
207
  if mime is None:
208
  return "application/octet-stream"
209
  return mime
210
 
 
211
  UNSTRACT_BASE = "https://llmwhisperer-api.us-central.unstract.com/api/v2"
212
+ UNSTRACT_API_KEY = os.getenv("UNSTRACT_API_KEY")
213
 
214
  def extract_text_from_unstract(uploaded_file):
215
  filename = getattr(uploaded_file, "name", "uploaded_file")
216
  file_bytes = uploaded_file.read()
217
  content_type = get_content_type(filename)
 
218
  headers = {
219
  "unstract-key": UNSTRACT_API_KEY,
220
  "Content-Type": content_type,
221
  }
222
  url = f"{UNSTRACT_BASE}/whisper"
 
223
  with st.spinner("Uploading and processing document with Unstract..."):
224
  r = requests.post(url, headers=headers, data=file_bytes)
225
  if r.status_code != 202:
 
229
  if not whisper_hash:
230
  st.error("Unstract: No whisper_hash received.")
231
  return None
 
232
  status_url = f"{UNSTRACT_BASE}/whisper-status?whisper_hash={whisper_hash}"
233
+ for i in range(30):
234
  status_r = requests.get(status_url, headers={"unstract-key": UNSTRACT_API_KEY})
235
  if status_r.status_code != 200:
236
  st.error(f"Unstract: Error checking status: {status_r.status_code} - {status_r.text}")
 
243
  else:
244
  st.error("Unstract: Timeout waiting for OCR to finish.")
245
  return None
 
246
  retrieve_url = f"{UNSTRACT_BASE}/whisper-retrieve?whisper_hash={whisper_hash}&text_only=true"
247
  r = requests.get(retrieve_url, headers={"unstract-key": UNSTRACT_API_KEY})
248
  if r.status_code != 200:
 
254
  except Exception:
255
  return r.text
256
 
257
+ # --------- NEW: UPLOAD PO CSV ---------
258
+ st.sidebar.header("Step 1: Upload Active Purchase Orders (POs)")
259
+ po_file = st.sidebar.file_uploader(
260
+ "Upload POs CSV (must include PO number, Supplier, Items, etc.)",
261
+ type=["csv"],
262
+ key="po_csv"
263
+ )
264
+ po_df = None
265
+ if po_file:
266
+ po_df = pd.read_csv(po_file)
267
+ st.sidebar.success(f"Loaded {len(po_df)} Purchase Orders.")
268
+ st.sidebar.dataframe(po_df.head())
269
+
270
  st.title("Invoice/Document Extractor")
271
  mdl = st.selectbox("Model", list(MODELS.keys()), key="extract_model")
272
  inv_file = st.file_uploader(
273
+ "Step 2: Upload Invoice or Document File",
274
  type=["pdf", "docx", "xlsx", "xls", "png", "jpg", "jpeg", "tiff"]
275
  )
276
  extracted_info = None
 
288
  st.table(extracted_info["line_items"])
289
  st.session_state["last_extracted_info"] = extracted_info # store in session
290
 
 
291
  extracted_info = extracted_info or st.session_state.get("last_extracted_info", None)
292
+
293
+ # -------------------------------
294
+ # LANGGRAPH ReAct DECISION AGENT
295
+ # -------------------------------
296
+
297
+ def po_match_tool(query: str, context: dict):
298
+ invoice = context['invoice']
299
+ po_df = context['po_df']
300
+ inv_hdr = invoice["invoice_header"]
301
+ inv_po_number = inv_hdr.get("purchase_order_number") or inv_hdr.get("order_number") or inv_hdr.get("our_order_number")
302
+ inv_supplier = inv_hdr.get("supplier_name")
303
+ explanation = ""
304
+ matched_po = None
305
+ if inv_po_number:
306
+ for idx, row in po_df.iterrows():
307
+ if (
308
+ str(row.get("PO Number", "")).lower().replace(" ", "") == str(inv_po_number).lower().replace(" ", "")
309
+ ):
310
+ matched_po = row
311
+ explanation += f"Matched on PO Number: {inv_po_number}\n"
312
+ break
313
+ if matched_po is None and inv_supplier:
314
+ potential_matches = po_df[po_df["Supplier Name"].str.lower().str.strip() == inv_supplier.lower().strip()]
315
+ if not potential_matches.empty:
316
+ matched_po = potential_matches.iloc[0]
317
+ explanation += f"Matched on Supplier Name: {inv_supplier}\n"
318
+ if matched_po is not None:
319
+ return f"PO matched: {matched_po.to_dict()}"
320
+ return "No matching PO found."
321
+
322
+ def build_decision_agent():
323
+ openai_api_key = os.getenv("OPENAI_API_KEY")
324
+ llm = ChatOpenAI(
325
+ openai_api_key=openai_api_key,
326
+ model="gpt-4-1106-preview",
327
+ temperature=0,
328
+ streaming=False,
329
  )
330
+ tools = [
331
+ {
332
+ "name": "po_match_tool",
333
+ "description": "Looks up a PO for a given invoice context.",
334
+ "func": po_match_tool,
335
+ }
336
+ ]
337
+ agent = create_react_agent(llm, tools)
338
+ graph_builder = StateGraph(agent)
339
+ def finish_decision(state, context):
340
+ return END, state
341
+ graph_builder.add_node("finish", finish_decision)
342
+ graph_builder.set_entry_point(agent)
343
+ graph_builder.add_edge(agent, END)
344
+ return graph_builder.compile()
 
345
 
346
+ if extracted_info and po_df is not None:
347
+ if st.button("Make a decision (AI Agent)"):
348
+ with st.spinner("Reasoning and making a decision with LangGraph agent..."):
349
+ agent_graph = build_decision_agent()
350
+ task = (
351
+ "Here is an invoice JSON and a list of active POs in context. "
352
+ "Step by step, reason whether the invoice matches an active PO and can be approved. "
353
+ "If there is a match, state the matched PO, otherwise explain why not. "
354
+ "Give a clear final decision: APPROVED or REJECTED."
355
+ )
356
+ context = {
357
+ "invoice": extracted_info,
358
+ "po_df": po_df,
359
+ }
360
+ out = agent_graph.invoke(task, context=context)
361
+ st.subheader("AI Decision")
362
+ st.write(out)