AI Automation & Agents  

AI Agents in Practice: Automatic Invoice Dispute Resolution Agent (Prompts + Code)

Introduction

In this next article, we explore an Automatic Invoice Dispute Resolution Agent. This agent is designed to assist businesses in automatically resolving disputes related to invoices by analyzing discrepancies between invoice details and purchase orders or contract terms. The agent can flag discrepancies, request clarifications from both vendors and internal teams, and propose potential resolutions. Just like the previous agents, this system ensures that it never claims success unless actions are fully executed and receipts are provided for all resolutions.


The Use Case

Invoice disputes often arise due to errors in billing, incorrect pricing, discrepancies in quantities or services, or misinterpretations of contract terms. The agent automates the process of identifying discrepancies between the invoice and purchase order or contract, flags potential issues, sends notifications to the relevant parties, and proposes resolutions such as issuing a credit note, requesting clarification, or accepting the invoice as-is. By handling routine disputes, the agent helps streamline the accounts payable process and reduces the workload on human staff.


Prompt Contract (agent interface)

# file: contracts/invoice_dispute_v1.yaml
role: "InvoiceDisputeAgent"
scope: >
  Identify and resolve discrepancies between invoices and purchase orders or contracts. Propose resolutions and request clarifications.
  Ask once if critical fields are missing (invoice_id, amount, contract_id, purchase_order_id).
  Propose tool calls; never assert success without a receipt.
output:
  type: object
  required: [summary, decision, dispute_details, citations, next_steps, tool_proposals]
  properties:
    summary: {type: string, maxWords: 100}
    decision: {type: string, enum: ["approve", "dispute", "need_approval", "need_more_info"]}
    dispute_details:
      type: object
      required: [invoice_id, amount, issue_found, resolution_suggested]
      properties:
        invoice_id: {type: string}
        amount: {type: integer}
        issue_found: {type: string}
        resolution_suggested: {type: string}
    citations: {type: array, items: {type: string}}
    next_steps: {type: array, items: {type: string}, maxItems: 6}
    tool_proposals:
      type: array
      items:
        type: object
        required: [name, args, preconditions, idempotency_key]
        properties:
          name: {type: string, enum: [CheckInvoiceDetails, CompareWithPurchaseOrder, ResolveDiscrepancy, RequestClarification]}
          args: {type: object}
          preconditions: {type: string}
          idempotency_key: {type: string}
policy_id: "invoice_policy.v6"
citation_rule: "1–2 minimal-span claim_ids per factual sentence"
decoding:
  narrative: {top_p: 0.92, temperature: 0.72, stop: ["\n\n## "]}
  bullets:   {top_p: 0.82, temperature: 0.45}

Example claims (context provided to the model)

[
  {"claim_id":"policy:invoice:discrepancy","text":"Discrepancies between invoice amounts and purchase orders must be flagged for resolution.",
   "effective_date":"2025-01-01","source_id":"doc:invoice_policy_v6","span":"flagged for resolution"},
  {"claim_id":"policy:invoice:pricing_terms","text":"Pricing terms must match the contract agreement. Any deviations must be reviewed.",
   "effective_date":"2025-01-01","source_id":"doc:invoice_policy_v6","span":"Pricing terms must match the contract agreement"},
  {"claim_id":"policy:invoice:timely_resolution","text":"Invoice disputes must be resolved within 10 business days.",
   "effective_date":"2025-01-01","source_id":"doc:invoice_policy_v6","span":"resolved within 10 business days"}
]

Tool Interfaces (typed, with receipts)

# tools.py
from pydantic import BaseModel
from typing import Optional, List, Dict
from datetime import datetime

class CheckInvoiceDetailsArgs(BaseModel):
    invoice_id: str
    amount: int
    contract_id: str
    purchase_order_id: str

class CompareWithPurchaseOrderArgs(BaseModel):
    invoice_id: str
    amount: int
    purchase_order_id: str

class ResolveDiscrepancyArgs(BaseModel):
    invoice_id: str
    resolution_suggested: str

class RequestClarificationArgs(BaseModel):
    invoice_id: str
    clarification_needed: str
    approver_id: str

class ToolReceipt(BaseModel):
    tool: str
    ok: bool
    ref: str
    message: str = ""
    data: Optional[Dict] = None
# adapters.py  (demo logic)
from tools import *
from datetime import datetime

INVOICES = {
    "inv-001": {"amount": 500, "contract_id": "contract-001", "purchase_order_id": "PO-001", "approved": False},
    "inv-002": {"amount": 1000, "contract_id": "contract-002", "purchase_order_id": "PO-002", "approved": False}
}

def check_invoice_details(a: CheckInvoiceDetailsArgs) -> ToolReceipt:
    invoice = INVOICES.get(a.invoice_id)
    if not invoice:
        return ToolReceipt(tool="CheckInvoiceDetails", ok=False, ref="invoice-not-found", message="Invoice not found")
    if invoice["amount"] != a.amount:
        return ToolReceipt(tool="CheckInvoiceDetails", ok=False, ref="invoice-discrepancy",
                           message=f"Amount mismatch: invoice shows {invoice['amount']} but requested {a.amount}")
    return ToolReceipt(tool="CheckInvoiceDetails", ok=True, ref=f"details-{a.invoice_id}",
                       message="Invoice details verified")

def compare_with_purchase_order(a: CompareWithPurchaseOrderArgs) -> ToolReceipt:
    invoice = INVOICES.get(a.invoice_id)
    if not invoice:
        return ToolReceipt(tool="CompareWithPurchaseOrder", ok=False, ref="invoice-not-found", message="Invoice not found")
    if invoice["purchase_order_id"] != a.purchase_order_id:
        return ToolReceipt(tool="CompareWithPurchaseOrder", ok=False, ref="po-mismatch",
                           message=f"Purchase order mismatch: invoice references {invoice['purchase_order_id']} but requested {a.purchase_order_id}")
    return ToolReceipt(tool="CompareWithPurchaseOrder", ok=True, ref=f"po-ok-{a.invoice_id}",
                       message="Purchase order verified")

def resolve_discrepancy(a: ResolveDiscrepancyArgs) -> ToolReceipt:
    return ToolReceipt(tool="ResolveDiscrepancy", ok=True, ref=f"discrepancy-{a.invoice_id}",
                       message=f"Discrepancy resolved with suggested action: {a.resolution_suggested}")

def request_clarification(a: RequestClarificationArgs) -> ToolReceipt:
    return ToolReceipt(tool="RequestClarification", ok=True, ref=f"clarify-{a.invoice_id}",
                       message=f"Clarification requested for {a.invoice_id}: {a.clarification_needed}", data={"approver_id": a.approver_id})

Agent Loop (proposal → verification → execution → receipts)

# agent_invoice_dispute.py
import uuid, json
from typing import Any, Dict, List
from tools import *
from adapters import *

ALLOWED_TOOLS = {"CheckInvoiceDetails", "CompareWithPurchaseOrder", "ResolveDiscrepancy", "RequestClarification"}

def new_idem() -> str:
    return f"idem-{uuid.uuid4()}"

def verify_proposal(p: Dict[str, Any]) -> str:
    required = {"name","args","preconditions","idempotency_key"}
    if not required.issubset(p): return "Missing proposal fields"
    if p["name"] not in ALLOWED_TOOLS: return "Tool not allowed"
    return ""

def execute(p: Dict[str, Any]) -> ToolReceipt:
    n, a = p["name"], p["args"]
    if n == "CheckInvoiceDetails":       return check_invoice_details(CheckInvoiceDetailsArgs(**a))
    if n == "CompareWithPurchaseOrder":  return compare_with_purchase_order(CompareWithPurchaseOrderArgs(**a))
    if n == "ResolveDiscrepancy":        return resolve_discrepancy(ResolveDiscrepancyArgs(**a))
    if n == "RequestClarification":      return request_clarification(RequestClarificationArgs(**a))
    return ToolReceipt(tool=n, ok=False, ref="none", message="Unknown tool")

# --- Model shim returning a plan per contract (replace with your LLM call) ---
def call_model(contract_yaml: str, claims: List[Dict[str,Any]], invoice_data: Dict[str,Any]) -> Dict[str,Any]:
    decision = "approve" if invoice_data["amount"] == invoice_data["reorder_quantity"] else "dispute"
    return {
      "summary": f"Invoice {invoice_data['invoice_id']} reviewed for discrepancies.",
      "decision": decision,
      "dispute_details": {
        "invoice_id": invoice_data["invoice_id"],
        "amount": invoice_data["amount"],
        "issue_found": "Amount mismatch",
        "resolution_suggested": "Adjust the invoice amount to match the purchase order."
      },
      "citations": ["policy:invoice:discrepancy", "policy:invoice:pricing_terms"],
      "next_steps": ["Check invoice details", "Compare with purchase order", "Resolve discrepancy", "Request clarification"],
      "tool_proposals": [
        {"name":"CheckInvoiceDetails","args":{"invoice_id":invoice_data["invoice_id"],"amount":invoice_data["amount"],
                                             "contract_id":invoice_data["contract_id"],"purchase_order_id":invoice_data["purchase_order_id"]},
         "preconditions":"Check invoice details for discrepancies.","idempotency_key": new_idem()},
        {"name":"CompareWithPurchaseOrder","args":{"invoice_id":invoice_data["invoice_id"],"amount":invoice_data["amount"],
                                                   "purchase_order_id":invoice_data["purchase_order_id"]},
         "preconditions":"Verify invoice against purchase order.","idempotency_key": new_idem()},
        {"name":"ResolveDiscrepancy","args":{"invoice_id":invoice_data["invoice_id"],"resolution_suggested":"Adjust the invoice amount."},
         "preconditions":"Resolve any detected discrepancies.","idempotency_key": new_idem()},
        {"name":"RequestClarification","args":{"invoice_id":invoice_data["invoice_id"],"clarification_needed":"Amount mismatch with purchase order.",
                                               "approver_id":"M001"},
         "preconditions":"Request clarification if necessary.","idempotency_key": new_idem()}
      ]
    }

def render_response(model_json: Dict[str,Any], receipts: List[ToolReceipt]) -> str:
    idx = {r.tool:r for r in receipts}
    lines = [model_json["summary"], ""]
    lines.append(f"Decision: {model_json['decision']}")
    lines.append(f"Discrepancy Details: {model_json['dispute_details']}")
    lines.append("")
    lines.append("Next steps:")
    for s in model_json["next_steps"]:
        lines.append(f"• {s}")
    if idx.get("ResolveDiscrepancy") and idx["ResolveDiscrepancy"].ok:
        lines.append(f"\nDiscrepancy resolved: {idx['ResolveDiscrepancy'].message}")
    if idx.get("RequestClarification") and idx["RequestClarification"].ok:
        lines.append(f"Clarification requested: {idx['RequestClarification'].message}")
    lines.append("\nCitations: " + ", ".join(model_json["citations"]))
    return "\n".join(lines)

def handle(invoice_data: Dict[str,Any]) -> str:
    contract = open("contracts/invoice_dispute_v1.yaml").read()
    claims: List[Dict[str,Any]] = []  # load real claims from policy
    plan = call_model(contract, claims, invoice_data)

    receipts: List[ToolReceipt] = []
    for prop in plan["tool_proposals"]:
        reason = verify_proposal(prop)
        if reason:
            receipts.append(ToolReceipt(tool=prop["name"], ok=False, ref="blocked", message=reason)); continue
        rec = execute(prop)
        receipts.append(rec)
        if not rec.ok and prop["name"] in {"ResolveDiscrepancy"}:
            break
    return render_response(plan, receipts)

if __name__ == "__main__":
    example_invoice_data = {
      "invoice_id":"inv-001",
      "amount": 500,
      "contract_id":"contract-001",
      "purchase_order_id":"PO-001"
    }
    print(handle(example_invoice_data))

The Prompt You’d Send to the Model (concise and testable)

System:
You are InvoiceDisputeAgent. Follow the contract:
- Ask once if invoice_id, amount, contract_id, or purchase_order_id is missing.
- Cite 1–2 claim_ids per factual sentence using provided claims.
- Propose tools; never assert success without a receipt.
- Output JSON with keys: summary, decision, dispute_details, citations[], next_steps[], tool_proposals[].

Claims (eligible only):
[ ... JSON array of dispute policy claims like above ... ]

User:
Dispute this invoice:
{"invoice_id":"inv-001","amount":500,"contract_id":"contract-001","purchase_order_id":"PO-001"}

How to adapt quickly

Replace invoice validation, discrepancy checking, and dispute resolution tools with your internal ERP and invoice management systems. Implement idempotency to ensure no accidental double disputes or invoice records are created. Load claims from company policies regarding payment terms, pricing rules, and purchase order guidelines. Add validation layers to ensure compliance with internal financial policies and external regulations. Ship the contract, policy bundle, and decoder settings behind a feature flag for rapid iteration, canary deployments, and rollback support.