Seth0330 commited on
Commit
d2967d8
·
verified ·
1 Parent(s): 1541e64

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -31
app.py CHANGED
@@ -10,6 +10,8 @@ import pandas as pd
10
  from langchain_community.chat_models import ChatOpenAI
11
  from langchain.agents import initialize_agent, Tool, AgentType
12
 
 
 
13
  st.set_page_config(page_title="Accounts Payable AI Agent", layout="wide")
14
 
15
  # -------- LLM Model Setup --------
@@ -256,38 +258,70 @@ def po_match_tool_func(input_text):
256
  inv_hdr = invoice["invoice_header"]
257
  inv_po_number = inv_hdr.get("purchase_order_number") or inv_hdr.get("order_number") or inv_hdr.get("our_order_number")
258
  inv_supplier = inv_hdr.get("supplier_name")
 
259
  inv_line_items = invoice.get("line_items", [])
260
 
261
- # Try to match PO by number or supplier name
262
- matched_po = None
263
- explanation = ""
 
264
  for idx, row in po_df.iterrows():
265
- po_number = str(row.get("PO Number", "")).lower().replace(" ", "")
266
- supplier = str(row.get("Supplier Name", "")).lower().strip()
267
- if inv_po_number and po_number == str(inv_po_number).lower().replace(" ", ""):
268
- matched_po = row
269
- explanation += f"Matched on PO Number: {inv_po_number}. "
 
 
 
 
 
 
 
 
270
  break
271
- elif inv_supplier and supplier == inv_supplier.lower().strip():
272
- matched_po = row
273
- explanation += f"Matched on Supplier Name: {inv_supplier}. "
 
 
 
 
 
 
 
274
  break
275
 
276
- # If no direct match, try to match by line items
277
- if matched_po is None and len(inv_line_items) > 0:
278
  for idx, row in po_df.iterrows():
279
  po_desc = str(row.get("Description", "")).lower()
 
 
280
  for line in inv_line_items:
281
- if line.get("description") and line["description"].lower() in po_desc:
282
- matched_po = row
283
- explanation += f"Matched on line item description: '{line['description']}'. "
 
 
 
 
284
  break
285
- if matched_po is not None:
 
 
286
  break
287
 
288
- if matched_po is not None:
289
- return f"PO matched: {matched_po.to_dict()}. {explanation}"
290
- return "No matching PO found based on PO Number, Supplier, or Line Items."
 
 
 
 
 
 
 
291
 
292
  if po_df is not None:
293
  st.session_state["po_df"] = po_df
@@ -306,7 +340,7 @@ if extracted_info is not None and po_df is not None:
306
  Tool(
307
  name="po_match_tool",
308
  func=po_match_tool_func,
309
- description="Use this tool to check if the invoice matches any PO in the current PO list, including by line items.",
310
  )
311
  ]
312
  decision_llm = ChatOpenAI(
@@ -322,16 +356,13 @@ if extracted_info is not None and po_df is not None:
322
  verbose=True,
323
  )
324
  prompt = (
325
- "You are an expert accounts payable decision agent.\n"
326
- "You are given an extracted invoice in JSON and have access to a tool called po_match_tool, which can check for matches with all available POs (including matching line items/descriptions between invoice and PO).\n"
327
- "To approve an invoice, you must verify at least one of the following:\n"
328
- "- The PO number matches a PO\n"
329
- "- The supplier name matches a PO\n"
330
- "- At least one line item description or quantity/price matches with a PO's item\n"
331
- "If you can't match on PO number or supplier, do your best to match using the invoice's line items (description/quantity/unit price/etc) and the PO data, and explain your reasoning step by step."
332
- "In your reasoning, list all fields and line items that matched, or say if nothing matched (be specific about what was compared)."
333
- "At the end, respond in this JSON format ONLY:\n"
334
- '{"decision": "APPROVED or REJECTED", "reason": "<detailed step-by-step explanation for your decision, showing what matched and what did not, including line item checks>"}\n'
335
  f"Invoice JSON:\n{json.dumps(extracted_info, indent=2)}"
336
  )
337
  with st.spinner("AI is reasoning and making a decision..."):
 
10
  from langchain_community.chat_models import ChatOpenAI
11
  from langchain.agents import initialize_agent, Tool, AgentType
12
 
13
+ from fuzzywuzzy import fuzz
14
+
15
  st.set_page_config(page_title="Accounts Payable AI Agent", layout="wide")
16
 
17
  # -------- LLM Model Setup --------
 
258
  inv_hdr = invoice["invoice_header"]
259
  inv_po_number = inv_hdr.get("purchase_order_number") or inv_hdr.get("order_number") or inv_hdr.get("our_order_number")
260
  inv_supplier = inv_hdr.get("supplier_name")
261
+ inv_total = inv_hdr.get("total_due") or inv_hdr.get("invoice_value") or inv_hdr.get("total_before_tax")
262
  inv_line_items = invoice.get("line_items", [])
263
 
264
+ explanation = []
265
+ best_match = None
266
+ best_match_type = None
267
+
268
  for idx, row in po_df.iterrows():
269
+ po_number = str(row.get("PO Number", "")).strip().lower()
270
+ supplier = str(row.get("Supplier Name", "")).strip().lower()
271
+ po_total = str(row.get("Total PO Value", "")).replace(",", "").replace("$", "").strip().lower()
272
+
273
+ po_match = po_number and inv_po_number and (po_number == str(inv_po_number).strip().lower())
274
+ supplier_match = supplier and inv_supplier and (supplier == inv_supplier.strip().lower())
275
+ total_match = po_total and inv_total and (po_total == str(inv_total).replace(",", "").replace("$", "").strip().lower())
276
+
277
+ # Strong approval condition
278
+ if po_match and supplier_match and total_match:
279
+ best_match = row
280
+ best_match_type = "APPROVED"
281
+ explanation.append(f"PO Number, Supplier Name, and Total Value all matched. PO: {row.to_dict()}")
282
  break
283
+ # Partial approval
284
+ elif (po_match or supplier_match) and not total_match:
285
+ best_match = row
286
+ best_match_type = "PARTIALLY APPROVED"
287
+ fields = []
288
+ if po_match:
289
+ fields.append("PO Number matched")
290
+ if supplier_match:
291
+ fields.append("Supplier Name matched")
292
+ explanation.append(f"{' and '.join(fields)}, but Total Value did not match. PO: {row.to_dict()}")
293
  break
294
 
295
+ # If no direct match, try line item fuzzy matching
296
+ if best_match is None and len(inv_line_items) > 0:
297
  for idx, row in po_df.iterrows():
298
  po_desc = str(row.get("Description", "")).lower()
299
+ po_total = str(row.get("Total PO Value", "")).replace(",", "").replace("$", "").strip().lower()
300
+ line_item_matched = False
301
  for line in inv_line_items:
302
+ desc = line.get("description") or ""
303
+ qty = str(line.get("quantity") or "").strip()
304
+ price = str(line.get("price") or "").replace(",", "").replace("$", "").strip()
305
+ score = fuzz.token_set_ratio(desc.lower(), po_desc)
306
+ if (desc and score >= 80) or (qty and qty in po_desc) or (price and price in po_desc):
307
+ line_item_matched = True
308
+ explanation.append(f"Line item '{desc}' (qty: {qty}, price: {price}) matched PO description with score {score}. PO: {row.to_dict()}")
309
  break
310
+ if line_item_matched and po_total and inv_total and po_total == str(inv_total).replace(",", "").replace("$", "").strip().lower():
311
+ best_match = row
312
+ best_match_type = "APPROVED"
313
  break
314
 
315
+ if best_match is not None:
316
+ return json.dumps({
317
+ "decision": best_match_type,
318
+ "reason": " | ".join(explanation)
319
+ })
320
+ else:
321
+ return json.dumps({
322
+ "decision": "REJECTED",
323
+ "reason": "No match found on PO Number, Supplier Name, Total Value, or any line item (including fuzzy match)."
324
+ })
325
 
326
  if po_df is not None:
327
  st.session_state["po_df"] = po_df
 
340
  Tool(
341
  name="po_match_tool",
342
  func=po_match_tool_func,
343
+ description="Check if the invoice matches any PO (headers or fuzzy line items).",
344
  )
345
  ]
346
  decision_llm = ChatOpenAI(
 
356
  verbose=True,
357
  )
358
  prompt = (
359
+ "You are an expert accounts payable agent. "
360
+ "Use po_match_tool to check matches based on the following business rules:\n"
361
+ "- If PO Number AND Supplier Name AND Total Value all match, the invoice is APPROVED.\n"
362
+ "- If PO Number OR Supplier Name match, but Total Value does not, the invoice is PARTIALLY APPROVED.\n"
363
+ "- If neither, try matching at least one line item (by fuzzy description, quantity, or price) and require total to match for APPROVED.\n"
364
+ "- Otherwise, REJECTED.\n"
365
+ "Call the tool and return its result as-is. Do not invent or guess the answer, do not add any comments outside the JSON.\n"
 
 
 
366
  f"Invoice JSON:\n{json.dumps(extracted_info, indent=2)}"
367
  )
368
  with st.spinner("AI is reasoning and making a decision..."):