Introduction
Here’s a second, production-grade agent you can lift into your stack today: an Expense Report Auditor & Reimbursement Agent. It reads a submitted expense, checks policy eligibility, flags issues, and—when allowed—creates a reimbursement. As before, the agent never claims success unless the backend executed the action and returned a receipt.
The Use Case
Employees submit expenses (amount, category, merchant, date, receipt image URL). The agent should verify policy limits, check duplicate submissions, validate receipt presence, and either propose a reimbursement or request corrections. All decisions must cite policy facts; all actions must yield receipts.
Prompt Contract (the agent’s interface)
# file: contracts/expense_auditor_v1.yaml
role: "ExpenseAuditor"
scope: >
Audit a single expense. Use eligible claims for policy facts and prior submissions.
Ask once for missing fields (amount_cents, category, date, merchant, receipt_url).
Propose tool calls; never assert success without a receipt.
output:
type: object
required: [summary, findings, decision, citations, tool_proposals]
properties:
summary: {type: string, maxWords: 60}
findings: {type: array, items: {type: string, maxWords: 18}, maxItems: 8}
decision: {type: string, enum: ["approve","approve_with_adjustment","reject","need_more_info"]}
citations: {type: array, items: {type: string}}
tool_proposals:
type: array
items:
type: object
required: [name, args, preconditions, idempotency_key]
properties:
name: {type: string, enum: [CheckDuplicates, CheckPolicy, CreateReimbursement]}
args: {type: object}
preconditions: {type: string}
idempotency_key: {type: string}
policy_id: "expense_policy.v5.us"
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 shown to the model)
[
{"claim_id":"policy:meal:cap_usd","text":"Meal expenses capped at $75 per person per day.",
"effective_date":"2025-06-01","source_id":"doc:policy_v5","span":"capped at $75"},
{"claim_id":"policy:receipt_required_over_25","text":"Receipts required for any expense over $25.",
"effective_date":"2025-06-01","source_id":"doc:policy_v5","span":"over $25"},
{"claim_id":"policy:no_alcohol_reimbursed","text":"Alcohol is not reimbursable.",
"effective_date":"2025-06-01","source_id":"doc:policy_v5","span":"not reimbursable"},
{"claim_id":"history:merchant:dup_hash:2025-10-04","text":"Expense hash MERCH123-2025-10-04-2599 filed by user U007.",
"effective_date":"2025-10-04","source_id":"sys:expenses","span":"filed by user U007"}
]
Tool Interfaces (typed, with receipts)
# tools.py
from pydantic import BaseModel, Field, HttpUrl
from datetime import date
from typing import Optional, Dict
class CheckDuplicatesArgs(BaseModel):
user_id: str
merchant: str
amount_cents: int
expense_date: date
class CheckPolicyArgs(BaseModel):
category: str
amount_cents: int
attendees: Optional[int] = Field(default=1)
has_receipt: bool
contains_alcohol: bool
class CreateReimbursementArgs(BaseModel):
user_id: str
amount_cents: int
expense_id: str
memo: 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 date
DUP_DB = {("U007","MERCH123",2599,date(2025,10,4)): "exp-abc"} # prior expense key
MEAL_CAP_CENTS = 7500 # $75
def check_duplicates(a: CheckDuplicatesArgs) -> ToolReceipt:
key = (a.user_id, a.merchant, a.amount_cents, a.expense_date)
if key in DUP_DB:
return ToolReceipt(tool="CheckDuplicates", ok=False, ref="dup-found",
message="Duplicate submission", data={"prior_expense_id": DUP_DB[key]})
return ToolReceipt(tool="CheckDuplicates", ok=True, ref="dup-none", message="No duplicate detected")
def check_policy(a: CheckPolicyArgs) -> ToolReceipt:
if a.amount_cents > 2500 and not a.has_receipt:
return ToolReceipt(tool="CheckPolicy", ok=False, ref="pol-receipt", message="Receipt required > $25")
if a.category.lower() == "meal" and a.amount_cents > MEAL_CAP_CENTS * a.attendees:
return ToolReceipt(tool="CheckPolicy", ok=False, ref="pol-meal-cap",
message="Exceeds meal cap", data={"cap_cents": MEAL_CAP_CENTS * a.attendees})
if a.contains_alcohol:
return ToolReceipt(tool="CheckPolicy", ok=False, ref="pol-alcohol", message="Alcohol not reimbursable")
return ToolReceipt(tool="CheckPolicy", ok=True, ref="pol-ok", message="Eligible under policy")
def create_reimbursement(a: CreateReimbursementArgs) -> ToolReceipt:
return ToolReceipt(tool="CreateReimbursement", ok=True, ref=f"rb-{a.expense_id}",
message="Reimbursement created", data={"amount_cents": a.amount_cents})
Agent Loop (proposal → verification → execution → receipts)
# agent_expense.py
import uuid, json
from datetime import date
from typing import Any, Dict, List
from pydantic import BaseModel, ValidationError
from tools import *
from adapters import *
ALLOWED_TOOLS = {"CheckDuplicates","CheckPolicy","CreateReimbursement"}
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"
if p["name"] == "CreateReimbursement" and p["args"].get("amount_cents", 0) <= 0:
return "Amount must be positive"
return ""
def execute(p: Dict[str, Any]) -> ToolReceipt:
n, a = p["name"], p["args"]
if n == "CheckDuplicates":
a["expense_date"] = date.fromisoformat(a["expense_date"])
return check_duplicates(CheckDuplicatesArgs(**a))
if n == "CheckPolicy":
return check_policy(CheckPolicyArgs(**a))
if n == "CreateReimbursement":
return create_reimbursement(CreateReimbursementArgs(**a))
return ToolReceipt(tool=n, ok=False, ref="none", message="Unknown tool")
# ---- Pretend model call that returns a plan matching the contract ----
def call_model(contract_yaml: str, claims: List[Dict[str,Any]], expense: Dict[str,Any]) -> Dict[str,Any]:
needs_adjust = expense["category"]=="meal" and expense["amount_cents"]>7500*expense.get("attendees",1)
approved_amount = min(expense["amount_cents"], 7500*expense.get("attendees",1))
decision = "approve_with_adjustment" if needs_adjust else "approve"
return {
"summary": "Expense audited; eligible with cap applied.",
"findings": ["Receipt present","No duplicate found","Meal cap applied if needed"],
"decision": decision,
"citations": ["policy:meal:cap_usd","policy:receipt_required_over_25","policy:no_alcohol_reimbursed"],
"tool_proposals": [
{"name":"CheckDuplicates",
"args":{"user_id":expense["user_id"],"merchant":expense["merchant"],
"amount_cents":expense["amount_cents"],"expense_date":expense["date"]},
"preconditions":"Same user+merchant+date+amount not submitted.",
"idempotency_key": new_idem()},
{"name":"CheckPolicy",
"args":{"category":expense["category"],"amount_cents":expense["amount_cents"],
"attendees":expense.get("attendees",1),
"has_receipt": bool(expense.get("receipt_url")),
"contains_alcohol": expense.get("contains_alcohol", False)},
"preconditions":"Receipt present if >$25; meal under cap; no alcohol.",
"idempotency_key": new_idem()},
{"name":"CreateReimbursement",
"args":{"user_id":expense["user_id"],
"amount_cents": approved_amount if decision.startswith("approve") else 0,
"expense_id": expense["expense_id"],
"memo": f"{expense['category']} at {expense['merchant']}"},
"preconditions":"Policy ok and no duplicate.",
"idempotency_key": new_idem()}
]
}
def render_response(model_json: Dict[str,Any], receipts: List[ToolReceipt]) -> str:
idx = {r.tool:r for r in receipts}
out = []
out.append(model_json["summary"])
out.append("")
out.append("Findings:")
for f in model_json["findings"]:
out.append(f"• {f}")
out.append(f"\nDecision: {model_json['decision']}")
if "CreateReimbursement" in idx and idx["CreateReimbursement"].ok:
amt = idx["CreateReimbursement"].data["amount_cents"] / 100
out.append(f"Reimbursement created: ${amt:.2f} (ref {idx['CreateReimbursement'].ref})")
elif "CreateReimbursement" in idx and not idx["CreateReimbursement"].ok:
out.append(f"Reimbursement not created: {idx['CreateReimbursement'].message}")
out.append("\nCitations: " + ", ".join(model_json["citations"]))
return "\n".join(out)
def handle(expense: Dict[str,Any]) -> str:
contract = open("contracts/expense_auditor_v1.yaml").read()
claims: List[Dict[str,Any]] = [] # plug real claim loader (policy + user history)
plan = call_model(contract, claims, expense)
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 {"CreateReimbursement"}:
break
return render_response(plan, receipts)
if __name__ == "__main__":
example_expense = {
"expense_id":"exp-999",
"user_id":"U007",
"merchant":"MERCH123",
"category":"meal",
"amount_cents": 9800,
"date":"2025-10-18",
"attendees":1,
"receipt_url":"https://receipts/r1.png",
"contains_alcohol": False
}
print(handle(example_expense))
The Prompt You’d Send to the Model (concise and testable)
System:
You are ExpenseAuditor. Follow the contract:
- Audit the expense; ask once if required fields are 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, findings[], decision, citations[], tool_proposals[].
Claims (eligible only):
[ ... JSON array of claim records like above ... ]
User:
Please audit this expense and reimburse if eligible:
{"expense_id":"exp-999","user_id":"U007","merchant":"MERCH123","category":"meal",
"amount_cents":9800,"date":"2025-10-18","attendees":1,
"receipt_url":"https://receipts/r1.png","contains_alcohol":false}
How to adapt fast
Swap the mock adapters with your finance APIs, enforce idempotency on payouts, and load claims from your policy docs and expense history with freshness windows. Add a validator step before execution to enforce schema, lexicon, locale, citation coverage, and “no implied writes.” Ship the contract, policy bundle, and decoder settings behind a flag with canary + rollback.