ap-clerk-env / baseline.py
Pathikreet's picture
Upload baseline.py with huggingface_hub
af74676 verified
Raw
History Blame Contribute Delete
18.9 kB
"""
baseline.py β€” Optimal scripted agent baseline against live HF Space.
Runs all 20 runnable tasks via HTTP, records every step, saves JSON + plots.
Usage: python baseline.py
"""
import json, re, time, datetime
import requests
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
ENV = 'https://pathikreet-ap-clerk-env.hf.space'
SEED = 42
OUT_JSON = 'baseline_results.json'
OUT_PLOT = 'baseline_plot.png'
# ── Helpers ────────────────────────────────────────────────────────────────────
def reset(task_id, seed=SEED):
r = requests.post(f'{ENV}/reset', json={'task_id': task_id, 'seed': seed}, timeout=20)
r.raise_for_status()
return r.json()
def step(session_id, action):
r = requests.post(f'{ENV}/step', json={'session_id': session_id, 'action': action}, timeout=20)
r.raise_for_status()
return r.json()
def make_action(decision, amount, reason, explanation):
return {'decision': decision, 'approved_amount': amount,
'reason_code': reason, 'explanation': explanation}
# ── Optimal action sequences per task ─────────────────────────────────────────
def run_task(task_id):
"""Returns list of step dicts: {step, action, score, breakdown, done}"""
data = reset(task_id)
sid = data['session_id']
obs = data['observation']
steps = []
def do_step(decision, amount, reason, expl):
r = step(sid, make_action(decision, amount, reason, expl))
steps.append({
'step': len(steps) + 1,
'action': decision,
'score': r['reward']['score'],
'breakdown': r['reward'].get('breakdown', {}),
'feedback': r['reward'].get('feedback', ''),
'done': r['done'],
})
return r
# ── pre-terminal intermediate steps ───────────────────────────────────────
inv = obs['invoice']
total = inv['invoice_total']
items = inv.get('line_items', [{}])
pos = obs.get('purchase_orders', [{}])
grns = obs.get('goods_receipts', [{}])
notes = obs.get('context_notes', [])
if task_id == 'hard_policy_violation':
do_step('ESCALATE', 0.0, 'POLICY_VIOLATION',
f"Freight ${inv.get('freight_charge',0):.2f} may exceed cap. Escalating to Finance Manager.")
if task_id == 'hard_duplicate_invoice':
do_step('QUERY_VENDOR', 0.0, 'PENDING_CLARIFICATION',
f"Invoice {inv['invoice_id']} found in paid ledger. Querying vendor to confirm.")
if task_id == 'hard_manager_preapproval':
do_step('ESCALATE', 0.0, 'MANAGER_REVIEW',
f"Freight ${inv.get('freight_charge',0):.2f} exceeds cap. Checking manager pre-approval.")
if task_id == 'long_invoice_dispute':
do_step('QUERY_VENDOR', 0.0, 'PENDING_CLARIFICATION',
f"Invoice price ${items[0].get('unit_price',0):.2f} exceeds agreed PO price. Querying vendor.")
do_step('ESCALATE', 0.0, 'MANAGER_REVIEW',
"Vendor acknowledged error. Escalating to Finance Manager.")
if task_id == 'long_policy_migration':
do_step('HOLD', 0.0, 'PENDING_CLARIFICATION',
f"Freight ${inv.get('freight_charge',0):.2f} near policy threshold. Holding for compliance review.")
if task_id == 'long_manager_chain':
do_step('ESCALATE', 0.0, 'MANAGER_REVIEW',
f"Freight ${inv.get('freight_charge',0):.2f} exceeds cap. Escalating to Finance Manager.")
do_step('ESCALATE', 0.0, 'MANAGER_REVIEW',
"Manager out of office. Re-escalating to VP Finance.")
if task_id == 'long_fraud_investigation':
do_step('QUERY_VENDOR', 0.0, 'PENDING_CLARIFICATION',
f"Invoice {inv['invoice_id']} appears in paid ledger. Querying vendor.")
do_step('ESCALATE', 0.0, 'MANAGER_REVIEW',
"Vendor disputes duplicate. Escalating to manager for paid-ledger audit.")
if task_id == 'long_audit_trail':
do_step('HOLD', 0.0, 'PENDING_CLARIFICATION',
"SOX audit requirement detected. Holding for compliance documentation review.")
# ── terminal decisions ─────────────────────────────────────────────────────
# re-fetch obs from latest step response if available
po = pos[0] if pos else {}
po_lines = po.get('lines', [{}])
if task_id == 'easy_perfect_match':
do_step('APPROVE_FULL', total, 'MATCH_CONFIRMED',
f"Invoice ${total:,.2f}, PO and GRN match exactly. Freight within cap. Approving.")
elif task_id == 'easy_no_po_found':
do_step('REJECT', 0.0, 'NO_PO_FOUND',
f"Invoice references {inv.get('po_reference','N/A')} but no matching OPEN PO found. Rejecting per Rule 5.")
elif task_id == 'medium_quantity_shortfall':
agreed_up = po_lines[0].get('agreed_unit_price', 0)
recv_qty = sum(l.get('received_quantity', 0) for g in grns for l in g.get('lines', []))
amt = round(recv_qty * agreed_up, 2)
do_step('APPROVE_PARTIAL', amt, 'QUANTITY_MISMATCH',
f"GRN confirms {int(recv_qty)} of {int(items[0].get('quantity',0))} units received. Approving ${amt:,.2f} per Rule 3.")
elif task_id == 'medium_price_discrepancy':
inv_p = items[0].get('unit_price', 0)
po_p = po_lines[0].get('agreed_unit_price', 1)
dev = (inv_p - po_p) / po_p * 100
do_step('REJECT', 0.0, 'PRICE_DISCREPANCY',
f"Invoice price ${inv_p:.2f} vs PO ${po_p:.2f} β€” {dev:.1f}% deviation exceeds tolerance. Rejecting per Rule 4.")
elif task_id == 'medium_split_delivery':
recv_qty = sum(l.get('received_quantity', 0) for g in grns for l in g.get('lines', []))
agreed_up = po_lines[0].get('agreed_unit_price', 0)
amt = round(recv_qty * agreed_up + inv.get('freight_charge', 0), 2)
do_step('APPROVE_FULL', amt, 'MATCH_CONFIRMED',
f"Two GRNs confirm all {int(recv_qty)} units received across split shipments. Approving ${amt:,.2f}.")
elif task_id == 'medium_vendor_mismatch':
inv_v = inv.get('vendor_name', '')
po_v = po.get('vendor_name', '')
do_step('REJECT', 0.0, 'VENDOR_MISMATCH',
f'Invoice vendor "{inv_v}" does not match PO vendor "{po_v}". Rejecting per Rule 7.')
elif task_id == 'hard_policy_violation':
do_step('REJECT', 0.0, 'POLICY_VIOLATION',
f"Freight ${inv.get('freight_charge',0):.2f} exceeds cap. Manager: NOT pre-approved. Rejecting per Rule 2.")
elif task_id == 'hard_duplicate_invoice':
do_step('REJECT', 0.0, 'DUPLICATE_INVOICE',
f"Invoice {inv['invoice_id']} already paid per ledger. Vendor confirmed. Rejecting per Rule 6.")
elif task_id == 'hard_partial_po_match':
po_descs = {pl.get('description','').lower() for p in pos if p.get('status')=='OPEN' for pl in p.get('lines',[])}
amt = round(sum(li.get('line_total',0) for li in items if li.get('description','').lower() in po_descs), 2)
do_step('APPROVE_PARTIAL', amt, 'NO_PO_FOUND',
f"Only PO-covered items payable (${amt:,.2f}). Unapproved items excluded per Rule 8.")
elif task_id == 'hard_tax_discrepancy':
tax = inv.get('tax_amount', 0)
do_step('REJECT', 0.0, 'TAX_DISCREPANCY',
f"Invoice includes ${tax:.2f} tax not in PO authorisation. Unapproved per Rule 8.")
elif task_id == 'hard_currency_conversion':
policy = obs.get('company_policy', '')
fx_match = re.search(r'EUR 1\.00 = USD (\d+\.\d+)', policy)
fx = float(fx_match.group(1)) if fx_match else 1.0
inv_eur = total
inv_usd = round(inv_eur * fx, 2)
po_usd = po.get('authorized_total', inv_usd)
tol = obs.get('price_tolerance', 0.05)
dev = (inv_usd - po_usd) / po_usd if po_usd > 0 else 1.0
if dev <= tol:
do_step('APPROVE_FULL', inv_usd, 'MATCH_CONFIRMED',
f"EUR {inv_eur:,.2f} Γ— {fx:.2f} = USD {inv_usd:,.2f}. Within PO ${po_usd:,.2f}. Approving.")
else:
do_step('REJECT', 0.0, 'PRICE_DISCREPANCY',
f"EUR {inv_eur:,.2f} Γ— {fx:.2f} = USD {inv_usd:,.2f} exceeds PO ${po_usd:,.2f} by {dev*100:.1f}%.")
elif task_id == 'hard_manager_preapproval':
approval = any('[MANAGER]' in n for n in notes)
if approval:
do_step('APPROVE_FULL', total, 'MATCH_CONFIRMED',
f"Freight override confirmed by Finance Manager. Approving ${total:,.2f}.")
else:
do_step('REJECT', 0.0, 'POLICY_VIOLATION',
f"Freight ${inv.get('freight_charge',0):.2f} exceeds cap. No pre-approval found.")
elif task_id == 'hard_credit_memo':
po_ref = inv.get('po_reference', '')
po_exists = any(p.get('po_number') == po_ref and p.get('status') == 'OPEN' for p in pos)
credit_amt = abs(total) # credit memos have negative invoice_total β€” approved_amount must be >= 0
if po_exists:
do_step('APPROVE_PARTIAL', credit_amt, 'MATCH_CONFIRMED',
f"Credit memo {inv['invoice_id']} has valid OPEN PO. Credit ${credit_amt:,.2f} approved per Rule 9.")
else:
do_step('REJECT', 0.0, 'NO_PO_FOUND',
f"Credit memo {inv['invoice_id']} β€” no valid OPEN PO. Rejecting.")
elif task_id == 'long_invoice_dispute':
agreed = po_lines[0].get('agreed_unit_price', 0)
qty = items[0].get('quantity', 0)
correct_total = round(qty * agreed + inv.get('freight_charge', 0), 2)
do_step('REJECT', 0.0, 'PRICE_DISCREPANCY',
f"Invoice price ${items[0].get('unit_price',0):.2f} vs PO ${agreed:.2f}. Correct amount ${correct_total:,.2f}. Rejecting original per Rule 4.")
elif task_id == 'long_policy_migration':
do_step('APPROVE_FULL', total, 'MATCH_CONFIRMED',
f"Policy update confirmed new freight cap. Freight now compliant. Three-way match confirmed. Approving ${total:,.2f}.")
elif task_id == 'long_batch_reconciliation':
do_step('APPROVE_FULL', total, 'MATCH_CONFIRMED',
f"Independent three-way match confirmed in batch context. PO, GRN, invoice ${total:,.2f} all match. Approving.")
elif task_id == 'long_manager_chain':
do_step('APPROVE_FULL', total, 'MATCH_CONFIRMED',
f"VP Finance pre-approval confirmed via escalation chain. Approving ${total:,.2f}.")
elif task_id == 'long_fraud_investigation':
do_step('REJECT', 0.0, 'DUPLICATE_INVOICE',
f"Invoice {inv['invoice_id']} confirmed duplicate by manager audit. Vendor dispute rejected. Rejecting ${total:,.2f} per Rule 6.")
elif task_id == 'long_audit_trail':
po_num = po.get('po_number', 'N/A')
grn_id = grns[0].get('grn_id', 'N/A') if grns else 'N/A'
do_step('APPROVE_FULL', total, 'MATCH_CONFIRMED',
f"SOX audit trail: PO {po_num} confirmed OPEN; GRN {grn_id} confirms receipt; Invoice ${total:,.2f} matches. Compliance cleared. Approving per Rule 1.")
elif task_id == 'long_multi_vendor_split':
do_step('APPROVE_PARTIAL', total, 'MATCH_CONFIRMED',
f"First tranche: invoice ${total:,.2f} covers {int(items[0].get('quantity',0))} units per split delivery. Approving first tranche only.")
return steps
# ── Main ───────────────────────────────────────────────────────────────────────
RUNNABLE_TASKS = [
# easy
'easy_perfect_match', 'easy_no_po_found',
# medium
'medium_quantity_shortfall', 'medium_price_discrepancy',
'medium_split_delivery', 'medium_vendor_mismatch',
# hard
'hard_policy_violation', 'hard_duplicate_invoice',
'hard_partial_po_match', 'hard_tax_discrepancy',
'hard_currency_conversion', 'hard_manager_preapproval', 'hard_credit_memo',
# long-horizon
'long_invoice_dispute', 'long_policy_migration', 'long_batch_reconciliation',
'long_manager_chain', 'long_fraud_investigation', 'long_audit_trail',
'long_multi_vendor_split',
]
DIFFICULTY = {
'easy_perfect_match': 'easy', 'easy_no_po_found': 'easy',
'medium_quantity_shortfall': 'medium', 'medium_price_discrepancy': 'medium',
'medium_split_delivery': 'medium', 'medium_vendor_mismatch': 'medium',
'hard_policy_violation': 'hard', 'hard_duplicate_invoice': 'hard',
'hard_partial_po_match': 'hard', 'hard_tax_discrepancy': 'hard',
'hard_currency_conversion': 'hard', 'hard_manager_preapproval': 'hard',
'hard_credit_memo': 'hard',
'long_invoice_dispute': 'long-horizon', 'long_policy_migration': 'long-horizon',
'long_batch_reconciliation': 'long-horizon', 'long_manager_chain': 'long-horizon',
'long_fraud_investigation': 'long-horizon', 'long_audit_trail': 'long-horizon',
'long_multi_vendor_split': 'long-horizon',
}
COLORS = {'easy': '#2ecc71', 'medium': '#f39c12', 'hard': '#e74c3c', 'long-horizon': '#9b59b6'}
def main():
print(f'Baseline run β€” {ENV}')
print(f'Started: {datetime.datetime.now().isoformat()}')
print('=' * 70)
all_results = {}
by_diff = {'easy': [], 'medium': [], 'hard': [], 'long-horizon': []}
for task_id in RUNNABLE_TASKS:
diff = DIFFICULTY[task_id]
try:
steps = run_task(task_id)
final_score = steps[-1]['score']
all_results[task_id] = {
'difficulty': diff,
'steps': steps,
'final_score': final_score,
'num_steps': len(steps),
}
by_diff[diff].append(final_score)
step_str = ' -> '.join(f"{s['action']}({s['score']:.2f})" for s in steps)
print(f"[{diff[:4]}] {task_id:<38} {final_score:.3f} | {step_str}")
except Exception as e:
print(f" ERROR {task_id}: {e}")
all_results[task_id] = {'difficulty': diff, 'steps': [], 'final_score': 0.01, 'num_steps': 0, 'error': str(e)}
by_diff[diff].append(0.01)
time.sleep(0.3)
# Summary
print('=' * 70)
summary = {}
for diff in ['easy', 'medium', 'hard', 'long-horizon']:
scores = by_diff[diff]
if scores:
mean = sum(scores) / len(scores)
summary[diff] = {'mean': round(mean, 4), 'scores': [round(s, 4) for s in scores], 'n': len(scores)}
print(f" {diff:<14}: mean={mean:.3f} n={len(scores)} scores={[round(s,3) for s in scores]}")
all_scores = [v['final_score'] for v in all_results.values()]
overall_mean = sum(all_scores) / len(all_scores)
print(f" {'OVERALL':<14}: mean={overall_mean:.3f} n={len(all_scores)}")
print('=' * 70)
# Save JSON
output = {
'run_type': 'optimal_scripted_agent',
'env_url': ENV,
'seed': SEED,
'timestamp': datetime.datetime.now().isoformat(),
'summary': summary,
'overall_mean': round(overall_mean, 4),
'tasks': all_results,
}
with open(OUT_JSON, 'w') as f:
json.dump(output, f, indent=2)
print(f'Saved: {OUT_JSON}')
# ── Plots ──────────────────────────────────────────────────────────────────
fig = plt.figure(figsize=(16, 10))
gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.4, wspace=0.35)
# Plot 1: Score per task (bar chart)
ax1 = fig.add_subplot(gs[0, :])
task_names = list(all_results.keys())
scores = [all_results[t]['final_score'] for t in task_names]
bar_colors = [COLORS[all_results[t]['difficulty']] for t in task_names]
bars = ax1.bar(range(len(task_names)), scores, color=bar_colors, alpha=0.85, edgecolor='white')
ax1.set_xticks(range(len(task_names)))
ax1.set_xticklabels([t.replace('_', '\n') for t in task_names], fontsize=7, rotation=0)
ax1.set_ylim(0, 1.05)
ax1.set_ylabel('Final Score (0.01–0.99)')
ax1.set_title('Optimal Agent Baseline β€” Score per Task', fontsize=13, fontweight='bold')
ax1.axhline(overall_mean, color='black', linestyle='--', linewidth=1.2, label=f'Overall mean: {overall_mean:.3f}')
for bar, score in zip(bars, scores):
ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,
f'{score:.2f}', ha='center', va='bottom', fontsize=7)
from matplotlib.patches import Patch
legend_els = [Patch(facecolor=c, label=d) for d, c in COLORS.items()]
legend_els.append(plt.Line2D([0],[0], color='black', linestyle='--', label=f'Mean {overall_mean:.3f}'))
ax1.legend(handles=legend_els, loc='lower right', fontsize=8)
# Plot 2: Mean score by difficulty
ax2 = fig.add_subplot(gs[1, 0])
diffs = [d for d in ['easy', 'medium', 'hard', 'long-horizon'] if by_diff[d]]
means = [sum(by_diff[d])/len(by_diff[d]) for d in diffs]
colors = [COLORS[d] for d in diffs]
ax2.bar(diffs, means, color=colors, alpha=0.85, edgecolor='white')
for i, (d, m) in enumerate(zip(diffs, means)):
ax2.text(i, m + 0.01, f'{m:.3f}', ha='center', fontsize=9, fontweight='bold')
ax2.set_ylim(0, 1.05)
ax2.set_ylabel('Mean Score')
ax2.set_title('Mean Score by Difficulty', fontsize=11, fontweight='bold')
# Plot 3: Step-level rewards for multi-step tasks
ax3 = fig.add_subplot(gs[1, 1])
multi_step = {t: v for t, v in all_results.items() if v['num_steps'] > 1}
for task_id, v in multi_step.items():
step_scores = [s['score'] for s in v['steps']]
label = task_id.replace('_', ' ')
ax3.plot(range(1, len(step_scores)+1), step_scores, marker='o',
color=COLORS[v['difficulty']], alpha=0.7, linewidth=1.5,
label=label if len(label) < 22 else label[:20]+'…')
ax3.set_xlabel('Step Number')
ax3.set_ylabel('Score at Step')
ax3.set_title('Per-Step Rewards β€” Multi-Step Tasks', fontsize=11, fontweight='bold')
ax3.set_ylim(0, 1.05)
ax3.legend(fontsize=6, loc='lower right')
plt.suptitle(f'AP Commander β€” Optimal Agent Baseline | seed={SEED} | {datetime.datetime.now().strftime("%Y-%m-%d")}',
fontsize=12, y=1.01)
plt.savefig(OUT_PLOT, dpi=130, bbox_inches='tight')
print(f'Saved: {OUT_PLOT}')
plt.close()
if __name__ == '__main__':
main()