Spaces:
Sleeping
Sleeping
| """AP Clerk task definitions: generators (fresh randomised obs per call) and graders (pure functions).""" | |
| from __future__ import annotations | |
| import random as _random | |
| import re as _re | |
| from dataclasses import dataclass, field | |
| from typing import Callable, Dict, List, Optional | |
| from .models import ( | |
| APObservation, APAction, APReward, | |
| Invoice, LineItem, PurchaseOrder, POLine, GoodsReceipt, GRNLine, | |
| DecisionType, ReasonCode, | |
| ) | |
| # ββ Policy configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _FREIGHT_CAPS = [30.0, 50.0, 75.0, 100.0] | |
| _PRICE_TOLERANCES = [0.005, 0.010, 0.015, 0.020, 0.030] | |
| def _make_policy(freight_cap: float, price_tolerance: float) -> str: | |
| pct = f"{price_tolerance * 100:.1f}%" | |
| return ( | |
| f"ACME Corp Accounts Payable Policy (Effective 2024-01-01)\n" | |
| f"--------------------------------------------------------\n" | |
| f"1. Three-Way Match: Every invoice MUST match a valid Purchase Order (PO) AND a\n" | |
| f" Goods Receipt Note (GRN) before payment is authorised.\n" | |
| f"2. Freight Cap: Freight / shipping charges exceeding ${freight_cap:.2f} per invoice\n" | |
| f" require explicit prior approval from the Finance Manager. Do NOT pay unapproved freight.\n" | |
| f"3. Quantity Rule: Payment is made only for quantities confirmed received in the GRN.\n" | |
| f" If fewer items arrived than were invoiced, approve only the received amount.\n" | |
| f"4. Price Integrity: Unit prices must match the agreed PO price. Any deviation > {pct}\n" | |
| f" must be queried and rejected until a corrected invoice is received.\n" | |
| f"5. PO Mandatory: Invoices without a valid OPEN PO reference MUST be rejected.\n" | |
| f"6. Duplicate Block: Any invoice ID that has already been paid must be rejected\n" | |
| f" immediately as a duplicate β regardless of supporting documents.\n" | |
| f"7. Vendor Verification: The vendor name on the invoice must exactly match the\n" | |
| f" vendor name on the Purchase Order. Name discrepancies require rejection.\n" | |
| f"8. Authorised Charges: Only charges explicitly included in the PO are payable.\n" | |
| f" Unauthorised additions (tax, surcharges, fees) not in the PO must be rejected." | |
| ).strip() | |
| # ββ Item catalogues βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _OFFICE_ITEMS = [ | |
| ("Ergonomic Office Chair Model-X", "OfficeWorld Supplies Ltd."), | |
| ("Height-Adjustable Standing Desk", "FurniCorp Direct"), | |
| ("Monitor Arm Dual-Screen Mount", "TechDesk Co."), | |
| ("Mesh Back Support Lumbar Cushion", "ErgoSupply Ltd."), | |
| ] | |
| _TECH_ITEMS = [ | |
| ("ThinkPad L15 Gen-4 Laptop", "TechProcure Global"), | |
| ("Dell XPS 15 Business Laptop", "CompuSource Inc."), | |
| ("HP EliteBook 840 G10", "DataTech Ltd."), | |
| ("Lenovo IdeaPad 5 Pro", "DigitalParts Corp."), | |
| ] | |
| _CABLE_ITEMS = [ | |
| ("USB-C 2m Braided Cable", "CableMart Direct"), | |
| ("HDMI 4K 3m Certified Cable", "WireZone Ltd."), | |
| ("Cat6 Ethernet Patch Cable 5m", "NetCables Co."), | |
| ("DisplayPort 1.4 2m Cable", "ConnectPro Ltd."), | |
| ] | |
| _ALL_ITEMS = _OFFICE_ITEMS + _TECH_ITEMS + _CABLE_ITEMS | |
| # Vendor name mismatches β pairs (correct PO name, invoice typo) | |
| _VENDOR_TYPOS: Dict[str, str] = { | |
| "OfficeWorld Supplies Ltd.": "OfficeWorld Supplies", | |
| "FurniCorp Direct": "Furnicorp Direct", | |
| "TechDesk Co.": "TechDesk Company", | |
| "ErgoSupply Ltd.": "ErgoSupply Limited", | |
| "TechProcure Global": "TechProcure Global Ltd.", | |
| "CompuSource Inc.": "CompuSource Inc", | |
| "DataTech Ltd.": "DataTech Ltd", | |
| "DigitalParts Corp.": "Digital Parts Corp.", | |
| "CableMart Direct": "Cable Mart Direct", | |
| "WireZone Ltd.": "Wire Zone Ltd.", | |
| "NetCables Co.": "NetCables Corp.", | |
| "ConnectPro Ltd.": "ConnectPro Limited", | |
| } | |
| # ββ RNG and ID helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _rng(seed): return _random.Random(seed) | |
| def _po(rng): return f"PO-{rng.randint(1000, 4999)}" | |
| def _grn(rng): return f"GRN-{rng.randint(1000, 4999)}" | |
| def _inv(rng): return f"INV-2024-{rng.randint(1000, 9999):04d}" | |
| def _distractor_po(rng: _random.Random, exclude_vendor: str = "") -> PurchaseOrder: | |
| """CLOSED PO from a different vendor β realistic noise the agent must filter out.""" | |
| candidates = [(d, v) for d, v in _ALL_ITEMS if v != exclude_vendor] | |
| d_desc, d_vendor = rng.choice(candidates) | |
| d_qty = rng.randint(5, 50) | |
| d_price = round(rng.choice([50, 100, 200, 500]) * rng.uniform(0.9, 1.1), 2) | |
| return PurchaseOrder( | |
| po_number=f"PO-{rng.randint(6000, 8999)}", | |
| vendor_name=d_vendor, | |
| lines=[POLine(description=d_desc, ordered_quantity=d_qty, agreed_unit_price=d_price)], | |
| authorized_total=round(d_qty * d_price, 2), | |
| status="CLOSED", | |
| ) | |
| def _distractor_grn(rng: _random.Random, exclude_po: str = "") -> GoodsReceipt: | |
| """GRN for a different PO β adds noise the agent must skip over.""" | |
| d_desc, _ = rng.choice(_ALL_ITEMS) | |
| d_qty = rng.randint(5, 40) | |
| d_po = f"PO-{rng.randint(6000, 8999)}" | |
| while d_po == exclude_po: | |
| d_po = f"PO-{rng.randint(6000, 8999)}" | |
| return GoodsReceipt( | |
| grn_id=f"GRN-{rng.randint(6000, 8999)}", | |
| po_number=d_po, | |
| lines=[GRNLine(description=d_desc, received_quantity=d_qty)], | |
| ) | |
| # ββ Explanation quality helpers βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _has_numeric_citation(expl: str) -> bool: | |
| """True if explanation contains a specific monetary value ($NNN) or percentage (N.N%).""" | |
| return bool(_re.search(r'\$\d[\d,.]*|\d+\.?\d*\s*%', expl)) | |
| def _explanation_coherence(expl: str, hits: int) -> float: | |
| """ | |
| Anti-keyword-salad multiplier in [0.5, 1.0]. | |
| Penalises short dumps where >40% of words are exact keyword matches. | |
| Requires at least 8 words for full credit. | |
| """ | |
| words = expl.split() | |
| word_count = max(1, len(words)) | |
| if word_count < 8: | |
| return 0.5 | |
| density = hits / word_count | |
| if density > 0.40: | |
| return 0.6 # keyword salad penalty | |
| return 1.0 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 1 β Easy: Perfect Three-Way Match | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_easy_perfect(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_OFFICE_ITEMS) | |
| qty = rng.randint(20, 150) | |
| unit_price = round(rng.choice([60, 80, 100, 120, 150, 200]) * rng.uniform(0.95, 1.05), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| freight = round(rng.uniform(2.0, freight_cap * 0.90), 2) # always under cap | |
| line_total = round(qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.6: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| return APObservation( | |
| task_id="easy_perfect_match", | |
| task_name="Perfect Three-Way Match", | |
| task_description=( | |
| f"A vendor has submitted an invoice for {desc.lower()}s. " | |
| "Verify the invoice against the open Purchase Order and the warehouse GRN. " | |
| "Closed historical POs in the system can be ignored. " | |
| "Make the correct payment decision." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_easy_perfect(obs: APObservation, action: APAction) -> APReward: | |
| correct = obs.invoice.invoice_total | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.0 # no partial on a perfect match | |
| else: | |
| decision_score = 0.0 | |
| amount_score = 0.0 | |
| if action.decision != DecisionType.REJECT: | |
| diff = abs(action.approved_amount - correct) / correct | |
| amount_score = (1.0 if diff <= 0.01 else | |
| 0.60 if diff <= 0.03 else | |
| 0.30 if diff <= 0.10 else 0.05) | |
| if action.reason_code == ReasonCode.MATCH_CONFIRMED: | |
| reason_score = 1.0 | |
| elif action.reason_code in (ReasonCode.QUANTITY_MISMATCH, ReasonCode.PRICE_DISCREPANCY): | |
| reason_score = 0.05 | |
| else: | |
| reason_score = 0.2 | |
| final = max(0.01, min(0.99, round(0.50 * decision_score + 0.35 * amount_score + 0.15 * reason_score, 3))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append("Invoice, PO and GRN match exactly β APPROVE_FULL is correct.") | |
| if amount_score < 1.0 and action.decision != DecisionType.REJECT: | |
| parts.append(f"Correct amount is ${correct:,.2f}.") | |
| if reason_score < 1.0: | |
| parts.append("MATCH_CONFIRMED is the correct reason code.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score}, | |
| feedback=" ".join(parts) or "Perfect decision!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 2 β Easy: No PO Found | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_easy_no_po(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_OFFICE_ITEMS) | |
| qty = rng.randint(10, 80) | |
| unit_price = round(rng.choice([50, 75, 100, 125]) * rng.uniform(0.9, 1.1), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| line_total = round(qty * unit_price, 2) | |
| inv_id = _inv(rng) | |
| fake_po = f"PO-{rng.randint(8000, 9999)}" # PO that doesn't exist in system | |
| # CLOSED POs from prior periods β do NOT contain an OPEN PO for this invoice | |
| pos = [] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| grns: List[GoodsReceipt] = [] | |
| return APObservation( | |
| task_id="easy_no_po_found", | |
| task_name="No Purchase Order Found", | |
| task_description=( | |
| f"A vendor has submitted an invoice referencing {fake_po}, " | |
| "but no matching OPEN Purchase Order exists in the system. " | |
| "Closed historical POs from other vendors are visible but do not apply. " | |
| "Apply company policy and make the correct decision." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=fake_po, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=0.0, invoice_total=line_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_easy_no_po(obs: APObservation, action: APAction) -> APReward: | |
| decision_score = 1.0 if action.decision == DecisionType.REJECT else 0.0 | |
| reason_score = (1.0 if action.reason_code == ReasonCode.NO_PO_FOUND else | |
| 0.3 if action.reason_code == ReasonCode.POLICY_VIOLATION else 0.05) | |
| amount_score = 1.0 if action.approved_amount == 0.0 else 0.0 | |
| final = max(0.01, min(0.99, round(0.60 * decision_score + 0.30 * reason_score + 0.10 * amount_score, 3))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append("No valid OPEN PO exists β invoice must be REJECTED per Policy Rule 5.") | |
| if reason_score < 1.0: | |
| parts.append("NO_PO_FOUND is the correct reason code.") | |
| if amount_score < 1.0: | |
| parts.append("Approved amount must be $0.00 on a rejection.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "reason": reason_score, | |
| "amount_zero": amount_score}, | |
| feedback=" ".join(parts) or "Correct β no PO, no payment!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 3 β Medium: Quantity Shortfall | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_medium_shortfall(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_TECH_ITEMS) | |
| ordered = rng.randint(50, 200) | |
| received = rng.randint(int(ordered * 0.4), int(ordered * 0.9)) | |
| unit_price = round(rng.choice([500, 600, 700, 800, 900, 1000]) * rng.uniform(0.95, 1.05), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| invoice_total = round(ordered * unit_price, 2) | |
| po_total = round(ordered * unit_price, 2) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=ordered, agreed_unit_price=unit_price)], | |
| authorized_total=po_total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=received)], | |
| )] | |
| # Distractor documents | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.5: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| return APObservation( | |
| task_id="medium_quantity_shortfall", | |
| task_name="Quantity Shortfall Reconciliation", | |
| task_description=( | |
| f"A vendor has invoiced for {ordered} units of {desc.lower()} " | |
| f"but the warehouse GRN shows only {received} were delivered. " | |
| "Calculate the correct payable amount based on received quantity only, " | |
| "then decide. Ignore closed POs from other vendors." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=ordered, | |
| unit_price=unit_price, line_total=invoice_total)], | |
| freight_charge=0.0, invoice_total=invoice_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_medium_shortfall(obs: APObservation, action: APAction) -> APReward: | |
| # Sum received across all GRNs that match the real PO | |
| real_po = obs.purchase_orders[0].po_number | |
| unit_price = obs.purchase_orders[0].lines[0].agreed_unit_price | |
| received = sum( | |
| line.received_quantity | |
| for grn in obs.goods_receipts | |
| if grn.po_number == real_po | |
| for line in grn.lines | |
| ) | |
| correct = round(received * unit_price, 2) | |
| if action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.REJECT: | |
| decision_score = 0.15 # harder: REJECT on shortfall is wrong approach | |
| else: | |
| decision_score = 0.0 | |
| amount_score = 0.0 | |
| if action.decision in (DecisionType.APPROVE_PARTIAL, DecisionType.APPROVE_FULL): | |
| diff = abs(action.approved_amount - correct) / correct if correct > 0 else 1.0 | |
| amount_score = (1.0 if diff <= 0.01 else | |
| 0.65 if diff <= 0.03 else | |
| 0.40 if diff <= 0.08 else | |
| 0.15 if diff <= 0.20 else 0.02) | |
| reason_score = (1.0 if action.reason_code == ReasonCode.QUANTITY_MISMATCH else | |
| 0.3 if action.reason_code == ReasonCode.POLICY_VIOLATION else 0.05) | |
| final = max(0.01, min(0.99, round(0.45 * decision_score + 0.40 * amount_score + 0.15 * reason_score, 3))) | |
| parts = [] | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| parts.append("Never pay for goods not received β GRN shows a shortfall.") | |
| if action.decision == DecisionType.APPROVE_PARTIAL and \ | |
| abs(action.approved_amount - correct) > correct * 0.01: | |
| parts.append( | |
| f"Correct amount = {int(received)} received Γ ${unit_price:.2f} = ${correct:,.2f}.") | |
| if reason_score < 1.0: | |
| parts.append("QUANTITY_MISMATCH is the appropriate reason code.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score}, | |
| feedback=" ".join(parts) or "Correct partial approval!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 4 β Medium: Price Discrepancy | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_medium_price(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_TECH_ITEMS) | |
| qty = rng.randint(20, 100) | |
| agreed_price = round(rng.choice([400, 500, 600, 800, 1000]) * rng.uniform(0.95, 1.05), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| # Invoice charges at least 3Γ the tolerance above agreed price | |
| markup = rng.uniform(price_tol * 3, 0.20) | |
| invoice_price = round(agreed_price * (1 + markup), 2) | |
| line_total = round(qty * invoice_price, 2) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=agreed_price)], | |
| authorized_total=round(qty * agreed_price, 2), status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.5: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| return APObservation( | |
| task_id="medium_price_discrepancy", | |
| task_name="Unit Price Discrepancy", | |
| task_description=( | |
| f"A vendor invoiced {qty} units of {desc.lower()} at ${invoice_price:.2f}/unit. " | |
| f"However, the agreed PO price is ${agreed_price:.2f}/unit β " | |
| f"a {markup*100:.1f}% deviation exceeding the policy {price_tol*100:.1f}% threshold. " | |
| "Identify the discrepancy and decide." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=invoice_price, line_total=line_total)], | |
| freight_charge=0.0, invoice_total=line_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_medium_price(obs: APObservation, action: APAction) -> APReward: | |
| inv_price = obs.invoice.line_items[0].unit_price | |
| po_price = obs.purchase_orders[0].lines[0].agreed_unit_price | |
| deviation = (inv_price - po_price) / po_price | |
| price_tol = obs.price_tolerance | |
| decision_score = 1.0 if action.decision == DecisionType.REJECT else 0.0 | |
| reason_score = (1.0 if action.reason_code == ReasonCode.PRICE_DISCREPANCY else | |
| 0.25 if action.reason_code == ReasonCode.POLICY_VIOLATION else 0.05) | |
| expl = action.explanation.lower() | |
| price_kws = ["price", "unit price", "agreed", "deviation", "discrepancy", | |
| "higher", "markup", "overprice", "po price", "mismatch", "exceed"] | |
| hits = sum(1 for kw in price_kws if kw in expl) | |
| expl_score = min(1.0, hits / 3) * _explanation_coherence(expl, hits) | |
| if not _has_numeric_citation(expl): | |
| expl_score *= 0.5 | |
| final = max(0.01, min(0.99, round(0.55 * decision_score + 0.30 * reason_score + 0.15 * expl_score, 3))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append( | |
| f"Invoice unit price ${inv_price:.2f} deviates {deviation*100:.1f}% " | |
| f"from agreed PO price ${po_price:.2f} (threshold {price_tol*100:.1f}%) " | |
| "β must REJECT per Policy Rule 4.") | |
| if reason_score < 1.0: | |
| parts.append("PRICE_DISCREPANCY is the correct reason code.") | |
| if expl_score < 0.5: | |
| parts.append("Mention the price deviation with specific values in your explanation.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "reason": reason_score, | |
| "price_detection_in_explanation": round(expl_score, 3)}, | |
| feedback=" ".join(parts) or "Correct β price deviation caught!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 5 β Hard: Policy Violation (Unauthorized Freight) [multi-step] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_hard_freight(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_CABLE_ITEMS) | |
| qty = rng.randint(100, 500) | |
| unit_price = round(rng.choice([10, 12, 15, 18, 20]) * rng.uniform(0.9, 1.1), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| # Freight always > cap (at least 20% above) | |
| freight = round(rng.uniform(freight_cap * 1.20, freight_cap * 4.0), 2) | |
| line_total = round(qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=line_total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.5: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| # Pre-generated context revealed on ESCALATE | |
| escalation_note = ( | |
| f"ESCALATE β Finance Manager (email response): " | |
| f"'Confirmed: freight charge of ${freight:.2f} on invoice {inv_id} was NOT " | |
| f"pre-approved by Finance. Our standard unapproved freight cap is ${freight_cap:.2f}. " | |
| f"Please reject this invoice and ask the vendor to resubmit with either a corrected " | |
| f"freight amount (under ${freight_cap:.2f}) or a Finance Manager approval reference.'" | |
| ) | |
| return APObservation( | |
| task_id="hard_policy_violation", | |
| task_name="Policy Violation β Unauthorized Freight Charge", | |
| task_description=( | |
| f"A vendor invoice includes a ${freight:.2f} freight charge for {desc.lower()}s. " | |
| f"The PO and GRN quantities match perfectly, but company policy caps " | |
| f"unapproved freight at ${freight_cap:.2f}. " | |
| "For complex policy questions, you may ESCALATE to the Finance Manager " | |
| "before making your final decision." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=[escalation_note], | |
| ) | |
| def grade_hard_freight(obs: APObservation, action: APAction) -> APReward: | |
| freight = obs.invoice.freight_charge | |
| freight_cap = obs.freight_cap | |
| cap_str = str(int(freight_cap)) | |
| # Check if agent escalated during multi-step | |
| escalated = any( | |
| h.get("decision") == DecisionType.ESCALATE.value | |
| for h in obs.action_history | |
| ) | |
| if action.decision == DecisionType.REJECT: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| goods_only = obs.invoice.invoice_total - freight | |
| decision_score = 0.25 if abs(action.approved_amount - goods_only) <= goods_only * 0.02 else 0.10 | |
| else: | |
| decision_score = 0.0 | |
| reason_score = (1.0 if action.reason_code == ReasonCode.POLICY_VIOLATION else | |
| 0.35 if action.reason_code == ReasonCode.PRICE_DISCREPANCY else 0.05) | |
| expl = action.explanation.lower() | |
| freight_kws = ["freight", "shipping", "policy", f"${cap_str}", cap_str, | |
| "unauthorized", "unapproved", "cap", "exceed", "approve"] | |
| hits = sum(1 for kw in freight_kws if kw in expl) | |
| expl_score = min(1.0, hits / 4) * _explanation_coherence(expl, hits) | |
| # Multi-step process bonus: escalating before deciding shows correct AP procedure | |
| process_score = 0.10 if (escalated and action.decision == DecisionType.REJECT) else 0.0 | |
| final = max(0.01, min(0.99, round( | |
| 0.48 * decision_score + 0.27 * reason_score + 0.20 * expl_score + process_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if action.decision != DecisionType.REJECT: | |
| parts.append( | |
| f"Freight ${freight:.2f} exceeds the ${freight_cap:.2f} unapproved cap β " | |
| "Policy Rule 2 requires REJECTION so the vendor must resubmit.") | |
| if reason_score < 1.0: | |
| parts.append("POLICY_VIOLATION is the correct reason code.") | |
| if expl_score < 0.5: | |
| parts.append( | |
| f"Mention the ${freight:.2f} freight charge and the ${freight_cap:.2f} cap in your explanation.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "reason": reason_score, | |
| "policy_detection_in_explanation": round(expl_score, 3), | |
| "process_bonus": round(process_score, 3)}, | |
| feedback=" ".join(parts) or "Excellent policy enforcement!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 6 β Hard: Duplicate Invoice [multi-step] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_hard_duplicate(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_OFFICE_ITEMS + _TECH_ITEMS) | |
| qty = rng.randint(10, 100) | |
| unit_price = round(rng.choice([100, 200, 300, 500, 800]) * rng.uniform(0.95, 1.05), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| line_total = round(qty * unit_price, 2) | |
| po_num, grn_id = _po(rng), _grn(rng) | |
| inv_id = _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=line_total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.5: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| # Pre-generated context revealed on QUERY_VENDOR | |
| query_note = ( | |
| f"QUERY_VENDOR response β '{vendor}': " | |
| f"'We acknowledge that invoice {inv_id} was submitted and processed in a prior " | |
| f"billing cycle. This appears to be an accidental duplicate submission from our " | |
| f"accounts team. Please reject it β no further payment is due for this transaction.'" | |
| ) | |
| return APObservation( | |
| task_id="hard_duplicate_invoice", | |
| task_name="Duplicate Invoice Detection", | |
| task_description=( | |
| f"Invoice {inv_id} has been submitted for {qty} units of {desc.lower()}. " | |
| "All three documents match. However, the payment ledger shows this exact " | |
| "invoice ID was already paid last month. You may QUERY_VENDOR for confirmation " | |
| "before making your final decision." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=0.0, invoice_total=line_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| paid_invoice_ids=[inv_id], | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=[query_note], | |
| ) | |
| def grade_hard_duplicate(obs: APObservation, action: APAction) -> APReward: | |
| queried = any( | |
| h.get("decision") == DecisionType.QUERY_VENDOR.value | |
| for h in obs.action_history | |
| ) | |
| decision_score = 1.0 if action.decision == DecisionType.REJECT else 0.0 | |
| reason_score = (1.0 if action.reason_code == ReasonCode.DUPLICATE_INVOICE else | |
| 0.15 if action.reason_code == ReasonCode.POLICY_VIOLATION else 0.03) | |
| expl = action.explanation.lower() | |
| dup_kws = ["duplicate", "already paid", "previously", "paid", "again", | |
| "twice", "ledger", "repeat", "resubmit", "prior"] | |
| hits = sum(1 for kw in dup_kws if kw in expl) | |
| expl_score = min(1.0, hits / 3) * _explanation_coherence(expl, hits) | |
| # Bonus for citing the specific invoice ID β shows the agent actually checked the ledger | |
| if obs.invoice.invoice_id.lower() in expl: | |
| expl_score = min(1.0, expl_score + 0.30) | |
| process_score = 0.10 if (queried and action.decision == DecisionType.REJECT) else 0.0 | |
| final = max(0.01, min(0.99, round( | |
| 0.48 * decision_score + 0.27 * reason_score + 0.20 * expl_score + process_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append( | |
| f"Invoice {obs.invoice.invoice_id} is in the paid ledger β " | |
| "Policy Rule 6 mandates REJECTION as a duplicate.") | |
| if reason_score < 1.0: | |
| parts.append("DUPLICATE_INVOICE is the correct reason code.") | |
| if expl_score < 0.5: | |
| parts.append("Mention the prior payment in your explanation.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "reason": reason_score, | |
| "duplicate_detection_in_explanation": round(expl_score, 3), | |
| "process_bonus": round(process_score, 3)}, | |
| feedback=" ".join(parts) or "Duplicate correctly identified!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 7 β Medium: Split Delivery | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_medium_split(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_TECH_ITEMS) | |
| total_ordered = rng.randint(50, 200) | |
| split1 = rng.randint(int(total_ordered * 0.30), int(total_ordered * 0.70)) | |
| split2 = total_ordered - split1 | |
| unit_price = round(rng.choice([200, 300, 400, 500, 600]) * rng.uniform(0.95, 1.05), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| line_total = round(total_ordered * unit_price, 2) | |
| po_num = _po(rng) | |
| grn_id1, grn_id2 = _grn(rng), _grn(rng) | |
| inv_id = _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=total_ordered, agreed_unit_price=unit_price)], | |
| authorized_total=line_total, status="OPEN", | |
| )] | |
| grns = [ | |
| GoodsReceipt( | |
| grn_id=grn_id1, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=split1)], | |
| ), | |
| GoodsReceipt( | |
| grn_id=grn_id2, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=split2)], | |
| ), | |
| ] | |
| if rng.random() < 0.65: | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.40: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| return APObservation( | |
| task_id="medium_split_delivery", | |
| task_name="Split Delivery Reconciliation", | |
| task_description=( | |
| f"A vendor has invoiced for {total_ordered} units of {desc.lower()} " | |
| f"at ${unit_price:.2f}/unit. The goods arrived in two separate shipments: " | |
| f"GRN {grn_id1} confirms {split1} units and GRN {grn_id2} confirms " | |
| f"{split2} units. Verify total received and decide the correct payment." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=total_ordered, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=0.0, invoice_total=line_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_medium_split(obs: APObservation, action: APAction) -> APReward: | |
| real_po = obs.purchase_orders[0].po_number | |
| unit_price = obs.purchase_orders[0].lines[0].agreed_unit_price | |
| ordered = obs.purchase_orders[0].lines[0].ordered_quantity | |
| # Sum all GRNs matching real PO (should equal total_ordered) | |
| received = sum( | |
| line.received_quantity | |
| for grn in obs.goods_receipts | |
| if grn.po_number == real_po | |
| for line in grn.lines | |
| ) | |
| correct = round(received * unit_price, 2) | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.25 # probably missed summing both GRNs | |
| else: | |
| decision_score = 0.0 | |
| amount_score = 0.0 | |
| if action.decision in (DecisionType.APPROVE_FULL, DecisionType.APPROVE_PARTIAL): | |
| diff = abs(action.approved_amount - correct) / correct if correct > 0 else 1.0 | |
| amount_score = (1.0 if diff <= 0.01 else | |
| 0.60 if diff <= 0.05 else | |
| 0.25 if diff <= 0.15 else 0.03) | |
| reason_score = (1.0 if action.reason_code == ReasonCode.MATCH_CONFIRMED else | |
| 0.40 if action.reason_code == ReasonCode.QUANTITY_MISMATCH else 0.05) | |
| final = max(0.01, min(0.99, round(0.50 * decision_score + 0.35 * amount_score + 0.15 * reason_score, 3))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append( | |
| f"Both GRNs together confirm all {int(ordered)} units received β APPROVE_FULL.") | |
| if amount_score < 1.0 and action.decision != DecisionType.REJECT: | |
| parts.append( | |
| f"Sum both GRNs: {int(received)} units Γ ${unit_price:.2f} = ${correct:,.2f}.") | |
| if reason_score < 1.0: | |
| parts.append("MATCH_CONFIRMED is correct β all goods received across two shipments.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score}, | |
| feedback=" ".join(parts) or "Correct split-delivery reconciliation!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 8 β Medium: Vendor Name Mismatch | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_medium_vendor_mismatch(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_ALL_ITEMS) | |
| typo_vendor = _VENDOR_TYPOS.get(vendor, vendor + " Ltd.") | |
| qty = rng.randint(20, 100) | |
| unit_price = round(rng.choice([100, 200, 300, 400, 500]) * rng.uniform(0.95, 1.05), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| line_total = round(qty * unit_price, 2) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, # correct PO vendor name | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=line_total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| if rng.random() < 0.60: | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.40: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| return APObservation( | |
| task_id="medium_vendor_mismatch", | |
| task_name="Vendor Name Mismatch", | |
| task_description=( | |
| f"Invoice {inv_id} for {qty} units of {desc.lower()} " | |
| f"names the vendor as '{typo_vendor}'. " | |
| f"However, Purchase Order {po_num} was issued to '{vendor}'. " | |
| "Policy Rule 7 requires an exact vendor name match. " | |
| "Identify the discrepancy and decide." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=typo_vendor, # mismatched | |
| po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=0.0, invoice_total=line_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_medium_vendor_mismatch(obs: APObservation, action: APAction) -> APReward: | |
| po_vendor = obs.purchase_orders[0].vendor_name | |
| inv_vendor = obs.invoice.vendor_name | |
| decision_score = 1.0 if action.decision == DecisionType.REJECT else 0.0 | |
| reason_score = (1.0 if action.reason_code == ReasonCode.VENDOR_MISMATCH else | |
| 0.4 if action.reason_code == ReasonCode.POLICY_VIOLATION else 0.05) | |
| amount_score = 1.0 if action.approved_amount == 0.0 else 0.0 | |
| expl = action.explanation.lower() | |
| # Best score: both full vendor names cited explicitly | |
| po_cited = po_vendor.lower() in expl | |
| inv_cited = inv_vendor.lower() in expl | |
| if po_cited and inv_cited: | |
| expl_score = 1.0 | |
| elif po_cited or inv_cited: | |
| expl_score = 0.6 | |
| else: | |
| # Fallback keyword score (max 0.4 without explicit vendor names) | |
| vendor_kws = ["vendor", "name", "mismatch", "different", "does not match", | |
| "supplier", "policy", "verification"] | |
| hits = sum(1 for kw in vendor_kws if kw in expl) | |
| expl_score = min(0.4, hits / 4) * _explanation_coherence(expl, hits) | |
| final = max(0.01, min(0.99, round( | |
| 0.50 * decision_score + 0.25 * reason_score + | |
| 0.15 * expl_score + 0.10 * amount_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append( | |
| f"Invoice vendor '{inv_vendor}' does not match PO vendor '{po_vendor}' " | |
| "β REJECT per Policy Rule 7.") | |
| if reason_score < 1.0: | |
| parts.append("VENDOR_MISMATCH is the correct reason code.") | |
| if expl_score < 0.5: | |
| parts.append("Explain the vendor name discrepancy in your explanation.") | |
| if amount_score < 1.0: | |
| parts.append("Approved amount must be $0.00 on a rejection.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "reason": reason_score, | |
| "vendor_mismatch_cited": round(expl_score, 3), | |
| "amount_zero": amount_score}, | |
| feedback=" ".join(parts) or "Correct β vendor mismatch detected!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 9 β Hard: Partial PO Coverage | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_hard_partial_po(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc1, vendor = rng.choice(_TECH_ITEMS) | |
| qty1 = rng.randint(10, 50) | |
| price1 = round(rng.choice([200, 300, 400, 500]) * rng.uniform(0.95, 1.05), 2) | |
| total1 = round(qty1 * price1, 2) | |
| # Second line item β no PO coverage (unauthorized purchase) | |
| desc2, _ = rng.choice(_OFFICE_ITEMS) | |
| qty2 = rng.randint(5, 30) | |
| price2 = round(rng.choice([50, 80, 100, 150]) * rng.uniform(0.95, 1.05), 2) | |
| total2 = round(qty2 * price2, 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| invoice_total = round(total1 + total2, 2) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc1, ordered_quantity=qty1, agreed_unit_price=price1)], | |
| authorized_total=total1, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[ | |
| GRNLine(description=desc1, received_quantity=qty1), | |
| GRNLine(description=desc2, received_quantity=qty2), | |
| ], | |
| )] | |
| if rng.random() < 0.70: | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| return APObservation( | |
| task_id="hard_partial_po_match", | |
| task_name="Partial PO Coverage", | |
| task_description=( | |
| f"Invoice {inv_id} covers two items: " | |
| f"{qty1} Γ {desc1.lower()} (${total1:,.2f}) and " | |
| f"{qty2} Γ {desc2.lower()} (${total2:,.2f}). " | |
| f"Purchase Order {po_num} authorises only the {desc1.lower()} line. " | |
| f"The {desc2.lower()} has no PO authorisation. " | |
| "Determine the correct payable amount for the covered items only." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[ | |
| LineItem(description=desc1, quantity=qty1, unit_price=price1, line_total=total1), | |
| LineItem(description=desc2, quantity=qty2, unit_price=price2, line_total=total2), | |
| ], | |
| freight_charge=0.0, invoice_total=invoice_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_hard_partial_po(obs: APObservation, action: APAction) -> APReward: | |
| # Correct amount: only line items covered by an OPEN PO | |
| po_descs = { | |
| pl.description.lower() | |
| for po in obs.purchase_orders | |
| if po.status == "OPEN" | |
| for pl in po.lines | |
| } | |
| correct = round(sum( | |
| li.line_total | |
| for li in obs.invoice.line_items | |
| if li.description.lower() in po_descs | |
| ), 2) | |
| if action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.REJECT: | |
| decision_score = 0.20 # defensible but policy prefers partial approval | |
| else: | |
| decision_score = 0.0 | |
| amount_score = 0.0 | |
| if action.decision in (DecisionType.APPROVE_PARTIAL, DecisionType.APPROVE_FULL): | |
| diff = abs(action.approved_amount - correct) / correct if correct > 0 else 1.0 | |
| amount_score = (1.0 if diff <= 0.01 else | |
| 0.60 if diff <= 0.05 else | |
| 0.30 if diff <= 0.15 else 0.03) | |
| reason_score = ( | |
| 1.0 if action.reason_code in (ReasonCode.NO_PO_FOUND, ReasonCode.POLICY_VIOLATION) else | |
| 0.40 if action.reason_code == ReasonCode.QUANTITY_MISMATCH else 0.05 | |
| ) | |
| expl = action.explanation.lower() | |
| partial_kws = ["partial", "no po", "unauthorized", "not covered", "not authoris", | |
| "only", "line item", "covered", "policy", "uncovered"] | |
| hits = sum(1 for kw in partial_kws if kw in expl) | |
| expl_score = min(1.0, hits / 3) * _explanation_coherence(expl, hits) | |
| final = max(0.01, min(0.99, round( | |
| 0.45 * decision_score + 0.38 * amount_score + | |
| 0.12 * reason_score + 0.05 * expl_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append( | |
| f"Only PO-covered items are payable β APPROVE_PARTIAL for ${correct:,.2f}.") | |
| if amount_score < 0.9 and action.decision != DecisionType.REJECT: | |
| parts.append(f"Pay only the PO-authorised items: ${correct:,.2f}.") | |
| if reason_score < 1.0: | |
| parts.append("NO_PO_FOUND or POLICY_VIOLATION are appropriate reason codes.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, | |
| "explanation_quality": round(expl_score, 3)}, | |
| feedback=" ".join(parts) or "Correct partial PO reconciliation!", done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 10 β Hard: Unauthorized Tax Charge | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_hard_tax(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_TECH_ITEMS + _OFFICE_ITEMS) | |
| qty = rng.randint(10, 80) | |
| unit_price = round(rng.choice([100, 200, 300, 500, 800]) * rng.uniform(0.95, 1.05), 2) | |
| line_total = round(qty * unit_price, 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| tax_rate = rng.choice([0.05, 0.08, 0.10, 0.13, 0.15, 0.20]) | |
| tax_amount = round(line_total * tax_rate, 2) | |
| invoice_total = round(line_total + tax_amount, 2) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=line_total, # PO has NO tax provision | |
| status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| if rng.random() < 0.70: | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.40: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| return APObservation( | |
| task_id="hard_tax_discrepancy", | |
| task_name="Unauthorized Tax Charge", | |
| task_description=( | |
| f"Invoice {inv_id} charges ${tax_amount:.2f} in tax ({tax_rate*100:.0f}%) " | |
| f"on top of the agreed line-item amount of ${line_total:,.2f} for " | |
| f"{qty} units of {desc.lower()}. " | |
| f"Purchase Order {po_num} authorises ${line_total:,.2f} with no tax provision. " | |
| "Determine whether the tax charge is payable per company policy and decide." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=0.0, tax_amount=tax_amount, invoice_total=invoice_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_hard_tax(obs: APObservation, action: APAction) -> APReward: | |
| tax = obs.invoice.tax_amount | |
| line_only = obs.invoice.invoice_total - tax | |
| if action.decision == DecisionType.REJECT: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| # Giving credit only if agent approves the non-tax portion | |
| diff = abs(action.approved_amount - line_only) / line_only if line_only > 0 else 1.0 | |
| decision_score = 0.40 if diff <= 0.02 else 0.15 | |
| else: | |
| decision_score = 0.0 | |
| reason_score = (1.0 if action.reason_code == ReasonCode.TAX_DISCREPANCY else | |
| 0.50 if action.reason_code == ReasonCode.POLICY_VIOLATION else | |
| 0.30 if action.reason_code == ReasonCode.PRICE_DISCREPANCY else 0.05) | |
| expl = action.explanation.lower() | |
| tax_str = str(int(tax)) | |
| tax_kws = ["tax", "vat", "gst", "levy", "unauthorized", "not authoris", | |
| "not in po", "unapproved", "additional charge", "discrepancy", | |
| f"${tax_str}", tax_str] | |
| hits = sum(1 for kw in tax_kws if kw in expl) | |
| expl_score = min(1.0, hits / 3) * _explanation_coherence(expl, hits) | |
| final = max(0.01, min(0.99, round(0.50 * decision_score + 0.30 * reason_score + 0.20 * expl_score, 3))) | |
| parts = [] | |
| if decision_score == 0.0: | |
| parts.append( | |
| f"Tax of ${tax:.2f} has no PO authorisation (Policy Rule 8) β " | |
| "REJECT and request a tax-free corrected invoice.") | |
| if reason_score < 1.0: | |
| parts.append("TAX_DISCREPANCY is the correct reason code.") | |
| if expl_score < 0.5: | |
| parts.append(f"Mention the unauthorized ${tax:.2f} tax charge in your explanation.") | |
| return APReward(score=final, | |
| breakdown={"decision": decision_score, "reason": reason_score, | |
| "tax_detection_in_explanation": round(expl_score, 3)}, | |
| feedback=" ".join(parts) or "Unauthorized tax charge correctly identified!", | |
| done=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 11 β Hard: Currency Conversion (EUR invoice vs USD PO) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _EUR_VENDORS = [ | |
| ("Industrial Valve Set (ISO-EN9001)", "EuroTech Supplies GmbH"), | |
| ("CNC Precision Bearing Kit", "Rhein Industrial AG"), | |
| ("High-Pressure Hydraulic Hose 10m", "Benelux FluidTech B.V."), | |
| ("Stainless Steel Fastener Assortment", "Nordics Hardware OY"), | |
| ] | |
| def generate_hard_currency(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_EUR_VENDORS) | |
| qty = rng.randint(10, 80) | |
| eur_price = round(rng.choice([50, 80, 100, 150, 200]) * rng.uniform(0.95, 1.05), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| # Exchange rate EUR β USD (e.g. 1.07) | |
| fx_rate = round(rng.choice([1.05, 1.07, 1.09, 1.11, 1.13, 1.15]), 2) | |
| usd_price = round(eur_price * fx_rate, 2) | |
| eur_total = round(qty * eur_price, 2) | |
| usd_total = round(qty * usd_price, 2) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| # Randomise whether the converted total is within or over PO (drives correct decision) | |
| within_po = rng.random() < 0.55 # majority of cases: approve if conversion is correct | |
| # PO authorized in USD | |
| if within_po: | |
| po_usd_total = round(usd_total * rng.uniform(1.00, 1.03), 2) # PO slightly above | |
| correct_decision = "APPROVE_FULL" | |
| else: | |
| # Invoice slightly over PO after conversion | |
| po_usd_total = round(usd_total * rng.uniform(0.94, 0.99), 2) | |
| correct_decision = "REJECT" | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=usd_price)], | |
| authorized_total=po_usd_total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.5: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| # Store correct_decision in description for grader use (via task_description field hack-free approach) | |
| # We'll encode it in context_notes (not shown initially) β grader reads obs directly | |
| policy = ( | |
| _make_policy(freight_cap, price_tol) + | |
| f"\n9. Currency Conversion: Invoices in foreign currencies must be converted to USD " | |
| f"using the rate specified. Current rate: EUR 1.00 = USD {fx_rate:.2f}. " | |
| f"All approval decisions must be based on the USD equivalent amount." | |
| ) | |
| return APObservation( | |
| task_id="hard_currency_conversion", | |
| task_name="Foreign Currency Invoice Reconciliation", | |
| task_description=( | |
| f"A European vendor has submitted invoice {inv_id} for {qty} units of " | |
| f"{desc.lower()} at EUR {eur_price:.2f}/unit (EUR {eur_total:,.2f} total). " | |
| f"Company policy requires conversion to USD at the current rate of " | |
| f"EUR 1.00 = USD {fx_rate:.2f} before comparison against the PO. " | |
| f"The PO is denominated in USD. Convert the invoice total and decide." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=eur_price, line_total=eur_total)], | |
| freight_charge=0.0, invoice_total=eur_total, | |
| currency="EUR", | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=policy, | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_hard_currency(obs: APObservation, action: APAction) -> APReward: | |
| # Recover fx_rate from policy text | |
| fx_match = _re.search(r'EUR 1\.00 = USD (\d+\.\d+)', obs.company_policy) | |
| fx_rate = float(fx_match.group(1)) if fx_match else 1.0 | |
| inv_eur = obs.invoice.invoice_total | |
| inv_usd = round(inv_eur * fx_rate, 2) | |
| po_usd = obs.purchase_orders[0].authorized_total | |
| deviation = (inv_usd - po_usd) / po_usd if po_usd > 0 else 1.0 | |
| tol = obs.price_tolerance | |
| # Correct decision: APPROVE_FULL if converted total β€ PO (within tolerance), else REJECT | |
| should_approve = deviation <= tol | |
| if should_approve: | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.30 | |
| else: | |
| decision_score = 0.0 | |
| else: | |
| if action.decision == DecisionType.REJECT: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.15 | |
| else: | |
| decision_score = 0.0 | |
| # Amount score: expects USD-converted amount if approving | |
| amount_score = 0.0 | |
| if action.decision in (DecisionType.APPROVE_FULL, DecisionType.APPROVE_PARTIAL): | |
| expected_amount = inv_usd if should_approve else 0.0 | |
| if expected_amount > 0: | |
| diff = abs(action.approved_amount - expected_amount) / expected_amount | |
| amount_score = (1.0 if diff <= 0.01 else | |
| 0.65 if diff <= 0.03 else | |
| 0.35 if diff <= 0.10 else 0.05) | |
| # Explanation: must mention conversion, exchange rate, and USD value | |
| expl = action.explanation.lower() | |
| fx_str = str(fx_rate) | |
| conv_kws = ["eur", "usd", "convert", "exchange", "rate", fx_str, "foreign", "currency"] | |
| hits = sum(1 for kw in conv_kws if kw in expl) | |
| expl_score = min(1.0, hits / 3) * _explanation_coherence(expl, hits) | |
| # Require at least one numeric citation (the converted value) | |
| if not _has_numeric_citation(expl): | |
| expl_score *= 0.5 | |
| reason_score = ( | |
| 1.0 if (should_approve and action.reason_code == ReasonCode.MATCH_CONFIRMED) else | |
| 1.0 if (not should_approve and action.reason_code == ReasonCode.PRICE_DISCREPANCY) else | |
| 0.40 if action.reason_code == ReasonCode.POLICY_VIOLATION else 0.05 | |
| ) | |
| final = max(0.01, min(0.99, round( | |
| 0.45 * decision_score + 0.25 * amount_score + | |
| 0.20 * expl_score + 0.10 * reason_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| adj = "within" if should_approve else "exceeds" | |
| parts.append( | |
| f"EUR {inv_eur:,.2f} Γ {fx_rate:.2f} = USD {inv_usd:,.2f}. " | |
| f"This {adj} the PO authorized amount of USD {po_usd:,.2f}.") | |
| if amount_score < 0.9 and action.decision != DecisionType.REJECT: | |
| parts.append(f"Approved amount should be the USD equivalent: ${inv_usd:,.2f}.") | |
| if expl_score < 0.5: | |
| parts.append("Explain the EURβUSD conversion with specific values in your reasoning.") | |
| if reason_score < 1.0: | |
| code = "MATCH_CONFIRMED" if should_approve else "PRICE_DISCREPANCY" | |
| parts.append(f"{code} is the correct reason code.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "conversion_explained": round(expl_score, 3), | |
| "reason": reason_score}, | |
| feedback=" ".join(parts) or "Correct foreign currency reconciliation!", | |
| done=True, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 12 β Hard: Manager Pre-Approved Freight Override [multi-step] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_hard_manager_preapproval(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_CABLE_ITEMS + _TECH_ITEMS) | |
| qty = rng.randint(50, 300) | |
| unit_price = round(rng.choice([15, 20, 25, 30, 40]) * rng.uniform(0.9, 1.1), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| # Freight exceeds cap by $20β$50 | |
| freight = round(freight_cap + rng.uniform(20.0, 50.0), 2) | |
| line_total = round(qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| # Generate a realistic approval code | |
| approval_code = f"MAP-2024-{rng.randint(1000, 9999)}" | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=line_total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.5: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| # Context revealed ONLY on ESCALATE β contains manager pre-approval | |
| manager_note = ( | |
| f"[MANAGER] Finance Manager approval on file: " | |
| f"Freight charge of ${freight:.2f} on invoice {inv_id} for {vendor} " | |
| f"has been reviewed and pre-authorised by Finance Manager J. Henderson. " | |
| f"Approval reference: {approval_code}. " | |
| f"This invoice may be processed in full including the freight charge." | |
| ) | |
| policy = ( | |
| _make_policy(freight_cap, price_tol) + | |
| f"\n9. Freight Override: If freight exceeds the ${freight_cap:.2f} cap, " | |
| f"ESCALATE to Finance for review before making a payment decision. " | |
| f"If the Finance Manager has pre-approved the freight, full payment is authorised." | |
| ) | |
| return APObservation( | |
| task_id="hard_manager_preapproval", | |
| task_name="Manager Pre-Approved Freight Override", | |
| task_description=( | |
| f"Invoice {inv_id} for {qty} units of {desc.lower()} includes a " | |
| f"${freight:.2f} freight charge, which exceeds the policy cap of " | |
| f"${freight_cap:.2f}. However, company policy states that freight " | |
| f"overrides may be authorised by the Finance Manager. " | |
| f"Use ESCALATE to check for pre-approval before making your final decision." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=policy, | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=[manager_note], | |
| ) | |
| def grade_hard_manager_preapproval(obs: APObservation, action: APAction) -> APReward: | |
| escalated = any( | |
| h.get("decision") == DecisionType.ESCALATE.value | |
| for h in obs.action_history | |
| ) | |
| # Check if the approval code was revealed (context_notes present) | |
| approval_revealed = any("[MANAGER]" in note for note in obs.context_notes) | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| if escalated and approval_revealed: | |
| decision_score = 1.0 # perfect: escalated, saw approval, approved | |
| elif escalated: | |
| decision_score = 0.60 # escalated but context not fully processed | |
| else: | |
| decision_score = 0.10 # approved without checking β too permissive | |
| elif action.decision == DecisionType.REJECT: | |
| if escalated and approval_revealed: | |
| decision_score = 0.10 # saw manager approval and still rejected β wrong | |
| elif escalated: | |
| decision_score = 0.30 # escalated but misread the context | |
| else: | |
| decision_score = 0.50 # reasonable default (freight over cap) but missed process step | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.20 # partial is not the right process | |
| else: | |
| decision_score = 0.0 | |
| # Amount: full invoice (including freight) is correct once approved | |
| amount_score = 0.0 | |
| correct_amount = obs.invoice.invoice_total | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| diff = abs(action.approved_amount - correct_amount) / correct_amount | |
| amount_score = (1.0 if diff <= 0.01 else | |
| 0.60 if diff <= 0.03 else 0.20) | |
| # Explanation: mention freight, approval code or manager, and decision rationale | |
| expl = action.explanation.lower() | |
| ap_kws = ["freight", "manager", "approve", "pre-approv", "authoris", "escalat", | |
| "override", "cap", "policy", "finance"] | |
| hits = sum(1 for kw in ap_kws if kw in expl) | |
| expl_score = min(1.0, hits / 3) | |
| if not _has_numeric_citation(expl): | |
| expl_score *= 0.6 | |
| reason_score = ( | |
| 1.0 if (action.decision == DecisionType.APPROVE_FULL and | |
| action.reason_code == ReasonCode.MATCH_CONFIRMED) else | |
| 1.0 if (action.decision == DecisionType.REJECT and | |
| action.reason_code in (ReasonCode.POLICY_VIOLATION, ReasonCode.MANAGER_REVIEW)) else | |
| 0.40 if action.reason_code == ReasonCode.MANAGER_REVIEW else 0.10 | |
| ) | |
| # Process bonus: escalated β revealed context β approved = perfect procedure | |
| process_score = 0.15 if (escalated and approval_revealed and | |
| action.decision == DecisionType.APPROVE_FULL) else 0.0 | |
| final = max(0.01, min(0.99, round( | |
| 0.40 * decision_score + 0.20 * amount_score + | |
| 0.20 * expl_score + 0.05 * reason_score + process_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| if not escalated: | |
| parts.append( | |
| "Policy Rule 9 requires ESCALATE to check for manager pre-approval " | |
| "before deciding on freight overages.") | |
| elif not approval_revealed: | |
| parts.append("Escalation context not yet revealed β check context_notes after ESCALATE.") | |
| else: | |
| parts.append( | |
| "The Finance Manager pre-approved this freight β APPROVE_FULL is correct.") | |
| if amount_score < 0.9 and action.decision == DecisionType.APPROVE_FULL: | |
| parts.append(f"Full invoice amount ${correct_amount:,.2f} is payable with manager approval.") | |
| if expl_score < 0.5: | |
| parts.append("Mention the freight amount, cap, and manager pre-approval in your explanation.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "explanation_quality": round(expl_score, 3), | |
| "reason": reason_score, | |
| "process_bonus": round(process_score, 3)}, | |
| feedback=" ".join(parts) or "Correct β manager approval verified and invoice approved!", | |
| done=True, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TASK 13 β Hard: Credit Memo Processing | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_hard_credit_memo(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_TECH_ITEMS + _OFFICE_ITEMS) | |
| qty = rng.randint(5, 40) | |
| unit_price = round(rng.choice([100, 200, 300, 400, 500]) * rng.uniform(0.95, 1.05), 2) | |
| line_total = round(qty * unit_price, 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| po_num, grn_id, inv_id = _po(rng), _grn(rng), _inv(rng) | |
| # ~60% chance the PO exists (agent should APPROVE_PARTIAL with negative amount) | |
| # ~40% chance no PO (agent should REJECT) | |
| po_exists = rng.random() < 0.60 | |
| if po_exists: | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=line_total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| correct_decision = "APPROVE_PARTIAL" # credit memo with valid PO β process as credit | |
| else: | |
| pos = [] | |
| fake_po = f"PO-{rng.randint(8000, 9999)}" | |
| po_num = fake_po # invoice references non-existent PO | |
| grns = [] | |
| correct_decision = "REJECT" # no PO β reject credit memo | |
| # Distractors | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| if rng.random() < 0.40: | |
| grns.append(_distractor_grn(rng, exclude_po=po_num)) | |
| # Credit memo: negative line total, negative invoice_total | |
| credit_total = round(-line_total, 2) | |
| policy = ( | |
| _make_policy(freight_cap, price_tol) + | |
| "\n9. Credit Memos: When a vendor issues a credit memo (negative invoice), " | |
| "verify the original PO reference. If a valid OPEN PO exists, apply the credit " | |
| "(APPROVE_PARTIAL with the negative credit amount). " | |
| "If no valid PO is found, REJECT the credit memo and request clarification." | |
| ) | |
| return APObservation( | |
| task_id="hard_credit_memo", | |
| task_name="Credit Memo Processing", | |
| task_description=( | |
| f"Vendor {vendor} has issued a credit memo {inv_id} for a return of " | |
| f"{qty} units of {desc.lower()}, crediting ${line_total:,.2f} back to ACME Corp. " | |
| f"Credit memos must be matched against an OPEN PO before processing. " | |
| f"{'The PO reference is valid and open in the system.' if po_exists else 'No matching OPEN PO could be located for the credit.'} " | |
| "Apply company policy and make the correct decision." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=credit_total)], | |
| freight_charge=0.0, invoice_total=credit_total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=policy, | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| ) | |
| def grade_hard_credit_memo(obs: APObservation, action: APAction) -> APReward: | |
| # Determine if a valid OPEN PO exists for this vendor+PO reference | |
| po_ref = obs.invoice.po_reference | |
| po_exists = any( | |
| po.po_number == po_ref and po.status == "OPEN" | |
| for po in obs.purchase_orders | |
| ) | |
| credit_amount = obs.invoice.invoice_total # negative value | |
| if po_exists: | |
| # Credit memo with valid PO: APPROVE_PARTIAL with negative (credit) amount | |
| if action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 0.10 # full approval on a credit memo makes no sense | |
| elif action.decision == DecisionType.REJECT: | |
| decision_score = 0.20 # defensible but incorrect β policy says process the credit | |
| else: | |
| decision_score = 0.0 | |
| else: | |
| # No PO: must REJECT | |
| if action.decision == DecisionType.REJECT: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.QUERY_VENDOR: | |
| decision_score = 0.35 # reasonable step but not correct terminal decision | |
| else: | |
| decision_score = 0.0 | |
| # Amount score: credit amount (negative) if approving | |
| amount_score = 0.0 | |
| if action.decision == DecisionType.APPROVE_PARTIAL and po_exists: | |
| diff = abs(action.approved_amount - credit_amount) / abs(credit_amount) if credit_amount != 0 else 1.0 | |
| amount_score = (1.0 if diff <= 0.01 else | |
| 0.60 if diff <= 0.05 else | |
| 0.25 if diff <= 0.15 else 0.05) | |
| elif action.decision == DecisionType.REJECT and not po_exists: | |
| amount_score = 1.0 if action.approved_amount == 0.0 else 0.0 | |
| # Reason code | |
| if po_exists: | |
| reason_score = ( | |
| 1.0 if action.reason_code == ReasonCode.MATCH_CONFIRMED else | |
| 0.40 if action.reason_code == ReasonCode.QUANTITY_MISMATCH else 0.05 | |
| ) | |
| else: | |
| reason_score = ( | |
| 1.0 if action.reason_code == ReasonCode.NO_PO_FOUND else | |
| 0.40 if action.reason_code == ReasonCode.POLICY_VIOLATION else 0.05 | |
| ) | |
| # Explanation: must mention "credit", "credit memo" or "return", and the amount | |
| expl = action.explanation.lower() | |
| credit_kws = ["credit", "credit memo", "return", "refund", "negative", "debit", | |
| "offset", "adjustment", "reversal"] | |
| hits = sum(1 for kw in credit_kws if kw in expl) | |
| expl_score = min(1.0, hits / 2) * _explanation_coherence(expl, hits) | |
| if not _has_numeric_citation(expl): | |
| expl_score *= 0.5 | |
| final = max(0.01, min(0.99, round( | |
| 0.45 * decision_score + 0.35 * amount_score + | |
| 0.10 * reason_score + 0.10 * expl_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| if po_exists: | |
| parts.append( | |
| f"Credit memo with valid PO: APPROVE_PARTIAL with credit amount " | |
| f"${credit_amount:,.2f} per Policy Rule 9.") | |
| else: | |
| parts.append("No valid OPEN PO found for credit memo β REJECT per Policy Rule 5.") | |
| if amount_score < 0.9 and action.decision == DecisionType.APPROVE_PARTIAL: | |
| parts.append( | |
| f"Approved credit amount should equal the credit memo total: ${credit_amount:,.2f}.") | |
| if expl_score < 0.5: | |
| parts.append("Mention 'credit memo' or 'return' with the credit value in your explanation.") | |
| if reason_score < 1.0: | |
| code = "MATCH_CONFIRMED" if po_exists else "NO_PO_FOUND" | |
| parts.append(f"{code} is the correct reason code.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, | |
| "credit_memo_cited": round(expl_score, 3)}, | |
| feedback=" ".join(parts) or "Credit memo correctly processed!", | |
| done=True, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LONG-HORIZON TASKS (Theme #2 β Long-Horizon Planning, Scale AI bonus) | |
| # All have max_steps 10-16, multi-step actor interactions, per-step micro-rewards | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # βββ Actor-aware context note generation βββββββββββββββββββββββββββββββββββββ | |
| def _vendor_context_honest(inv_id: str, total: float) -> str: | |
| return ( | |
| f"[VENDOR] Thank you for your query regarding invoice {inv_id}. " | |
| f"We confirm this is a valid invoice for ${total:,.2f}. " | |
| "All line items match our delivery records. Please proceed with payment." | |
| ) | |
| def _vendor_context_dispute(inv_id: str, orig: float, corrected: float) -> str: | |
| return ( | |
| f"[VENDOR] Regarding invoice {inv_id}: we acknowledge the discrepancy. " | |
| f"The correct amount is ${corrected:,.2f} (not ${orig:,.2f}). " | |
| "A corrected invoice will be issued within 2 business days. Please hold payment." | |
| ) | |
| def _vendor_context_duplicate_deny(inv_id: str) -> str: | |
| return ( | |
| f"[VENDOR] Invoice {inv_id} is NOT a duplicate. " | |
| "Our records show the previous submission was rejected at your end. " | |
| "This is a re-submission β please process immediately." | |
| ) | |
| def _manager_context_approve(freight: float, cap: float) -> str: | |
| return ( | |
| f"[MANAGER] Pre-approval confirmed for freight ${freight:.2f} " | |
| f"(policy cap: ${cap:.2f}). Approved as one-time exception. " | |
| "Proceed with full invoice payment." | |
| ) | |
| def _manager_context_deny(freight: float, cap: float) -> str: | |
| overage = freight - cap | |
| return ( | |
| f"[MANAGER] No pre-approval on file. Freight of ${freight:.2f} " | |
| f"exceeds the ${cap:.2f} policy cap by ${overage:.2f}. " | |
| "Reject and request corrected invoice per Policy Rule 2." | |
| ) | |
| def _manager_unavailable_note() -> str: | |
| return ( | |
| "[MANAGER] Out of office until Monday. For urgent approvals above $5,000, " | |
| "please escalate to VP Finance (vp.finance@acme.com). " | |
| "Standard policy applies in my absence." | |
| ) | |
| def _vp_context_approve(total: float) -> str: | |
| return ( | |
| f"[VP_FINANCE] Reviewed and approved. Invoice total ${total:,.2f} " | |
| "has been pre-cleared at the VP level due to strategic vendor relationship. " | |
| "Proceed with APPROVE_FULL." | |
| ) | |
| def _compliance_cleared(vendor: str) -> str: | |
| return ( | |
| f"[COMPLIANCE] SOX review complete for vendor {vendor}. " | |
| "No material weaknesses. Three-way documentation adequate. " | |
| "Invoice may proceed." | |
| ) | |
| # ββ Task: long_invoice_dispute βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_long_dispute(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_OFFICE_ITEMS + _TECH_ITEMS) | |
| qty = rng.randint(20, 100) | |
| agreed_price = round(rng.choice([80, 120, 200, 350]) * rng.uniform(0.97, 1.03), 2) | |
| invoice_price = round(agreed_price * rng.uniform(1.10, 1.30), 2) # 10-30% over | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| freight = round(rng.uniform(5, freight_cap * 0.85), 2) | |
| line_total_agreed = round(qty * agreed_price, 2) | |
| line_total_invoice = round(qty * invoice_price, 2) | |
| total_invoice = round(line_total_invoice + freight, 2) | |
| total_correct = round(line_total_agreed + freight, 2) | |
| po_num = _po(rng) | |
| grn_id = _grn(rng) | |
| inv_id = _inv(rng) | |
| dev = (invoice_price - agreed_price) / agreed_price | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=agreed_price)], | |
| authorized_total=total_correct, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| context_notes = [ | |
| f"[VENDOR] Initial response: Invoice {inv_id} is correct per our updated pricing.", | |
| _vendor_context_dispute(inv_id, total_invoice, total_correct), | |
| f"[MANAGER] Price deviation of {dev*100:.1f}% exceeds policy tolerance {price_tol*100:.1f}%. Reject original; accept corrected invoice.", | |
| "[COMPLIANCE] Policy Rule 4 violation confirmed. Corrected invoice required.", | |
| ] | |
| return APObservation( | |
| task_id="long_invoice_dispute", | |
| task_name="Invoice Dispute Resolution (Long-Horizon)", | |
| task_description=( | |
| f"Vendor submitted invoice {inv_id} for {desc.lower()} at ${invoice_price:.2f}/unit " | |
| f"(agreed PO rate: ${agreed_price:.2f}/unit β deviation {dev*100:.1f}%). " | |
| f"Engage in a multi-step dispute process: query vendor, escalate to manager, " | |
| f"review compliance note, then make a final decision. " | |
| f"Correctly approved amount (if any) uses the PO agreed rate: ${total_correct:,.2f}." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=invoice_price, line_total=line_total_invoice)], | |
| freight_charge=freight, invoice_total=total_invoice, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=context_notes, | |
| max_steps=12, | |
| ) | |
| def grade_long_dispute(obs: APObservation, action: APAction) -> APReward: | |
| history = obs.action_history | |
| notes = obs.context_notes | |
| # Parse correct total from task description (PO agreed rate * qty + freight) | |
| # Find correct amount from POs | |
| po = next((p for p in obs.purchase_orders if p.status == "OPEN"), None) | |
| if po and po.lines: | |
| line = po.lines[0] | |
| qty = obs.invoice.line_items[0].quantity | |
| correct_total = round(line.agreed_unit_price * qty + obs.invoice.freight_charge, 2) | |
| else: | |
| correct_total = obs.invoice.invoice_total | |
| used_query = any(h["decision"] == "QUERY_VENDOR" for h in history) | |
| used_escalate = any(h["decision"] == "ESCALATE" for h in history) | |
| steps_used = obs.step_count | |
| # Decision scoring: should REJECT (vendor was wrong, corrected invoice required) | |
| if action.decision == DecisionType.REJECT: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.4 | |
| else: | |
| decision_score = 0.0 | |
| # Amount | |
| amount_score = 0.0 | |
| if action.decision == DecisionType.REJECT: | |
| amount_score = 1.0 if action.approved_amount == 0.0 else 0.3 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| diff = abs(action.approved_amount - correct_total) / correct_total | |
| amount_score = 1.0 if diff <= 0.01 else 0.50 if diff <= 0.05 else 0.15 | |
| # Reason code | |
| reason_score = 1.0 if action.reason_code == ReasonCode.PRICE_DISCREPANCY else 0.2 | |
| # Explanation quality | |
| expl = action.explanation.lower() | |
| expl_score = 0.0 | |
| if _has_numeric_citation(action.explanation): | |
| expl_score += 0.5 | |
| if "price" in expl or "deviation" in expl or "discrepan" in expl: | |
| expl_score += 0.3 | |
| if "corrected" in expl or "reissue" in expl or "reject" in expl: | |
| expl_score += 0.2 | |
| expl_score = min(1.0, expl_score) | |
| # Process bonus: reward for using dispute workflow | |
| process_bonus = 0.0 | |
| if used_query: process_bonus += 0.08 | |
| if used_escalate: process_bonus += 0.07 | |
| final = max(0.01, min(0.99, round( | |
| 0.38 * decision_score + 0.25 * amount_score + | |
| 0.15 * reason_score + 0.12 * expl_score + process_bonus, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append("Vendor admitted error β REJECT and request corrected invoice.") | |
| if not used_query: | |
| parts.append("Process tip: QUERY_VENDOR first to document the vendor's admission.") | |
| if expl_score < 0.5: | |
| parts.append("Cite the price deviation % and both PO and invoice amounts.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, "explanation": round(expl_score,3), | |
| "process_bonus": round(process_bonus,3)}, | |
| feedback=" | ".join(parts) or "Dispute resolved correctly!", | |
| done=True, | |
| ) | |
| # ββ Task: long_policy_migration ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_long_policy_migration(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_ALL_ITEMS) | |
| qty = rng.randint(10, 60) | |
| unit_price = round(rng.choice([100, 150, 250]) * rng.uniform(0.97, 1.03), 2) | |
| # Old cap (in force when invoice was submitted) vs new cap (now in policy) | |
| old_cap = rng.choice([30.0, 50.0]) | |
| new_cap = round(old_cap * rng.uniform(1.40, 2.0), 2) # new cap is higher | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| # Freight: between old_cap and new_cap β compliant under new policy | |
| freight = round(rng.uniform(old_cap + 1, new_cap - 1), 2) | |
| line_total = round(qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num = _po(rng) | |
| grn_id = _grn(rng) | |
| inv_id = _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| context_notes = [ | |
| f"[COMPLIANCE] POLICY UPDATE (Effective New Fiscal Year): " | |
| f"Freight cap revised from ${old_cap:.2f} to ${new_cap:.2f}. " | |
| f"All invoices received after policy effective date must use the new threshold.", | |
| f"[MANAGER] Confirmed: invoice {inv_id} received after the policy effective date. " | |
| f"Apply new freight cap of ${new_cap:.2f}.", | |
| ] | |
| return APObservation( | |
| task_id="long_policy_migration", | |
| task_name="Policy Migration β Mid-Episode Rule Change", | |
| task_description=( | |
| f"Invoice {inv_id} for {desc.lower()} includes freight of ${freight:.2f}. " | |
| f"The system shows the OLD policy cap of ${old_cap:.2f}. " | |
| f"Mid-review, a compliance alert notifies you the policy was updated: " | |
| f"new freight cap is ${new_cap:.2f}. " | |
| f"After reviewing the policy update and confirming the date, decide correctly." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(old_cap, price_tol), # starts with OLD policy | |
| freight_cap=new_cap, # actual correct cap to grade against | |
| price_tolerance=price_tol, | |
| context_notes=context_notes, | |
| max_steps=10, | |
| ) | |
| def grade_long_policy_migration(obs: APObservation, action: APAction) -> APReward: | |
| # Freight is between old and new cap β correct answer is APPROVE_FULL | |
| # (freight is compliant under new policy) | |
| history = obs.action_history | |
| used_hold = any(h["decision"] == "HOLD" for h in history) | |
| used_escalate = any(h["decision"] == "ESCALATE" for h in history) | |
| correct = obs.invoice.invoice_total | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.3 | |
| else: | |
| decision_score = 0.1 # incorrectly rejected under old policy | |
| amount_score = 0.0 | |
| if action.decision in (DecisionType.APPROVE_FULL, DecisionType.APPROVE_PARTIAL): | |
| diff = abs(action.approved_amount - correct) / correct | |
| amount_score = 1.0 if diff <= 0.01 else 0.50 if diff <= 0.05 else 0.10 | |
| reason_score = 1.0 if action.reason_code == ReasonCode.MATCH_CONFIRMED else 0.2 | |
| expl = action.explanation.lower() | |
| expl_score = 0.0 | |
| if "new" in expl and ("cap" in expl or "policy" in expl): | |
| expl_score += 0.5 | |
| if _has_numeric_citation(action.explanation): | |
| expl_score += 0.3 | |
| if "update" in expl or "revised" in expl or "fiscal" in expl: | |
| expl_score += 0.2 | |
| expl_score = min(1.0, expl_score) | |
| process_bonus = 0.0 | |
| if used_hold: process_bonus += 0.06 | |
| if used_escalate: process_bonus += 0.06 | |
| final = max(0.01, min(0.99, round( | |
| 0.40 * decision_score + 0.25 * amount_score + | |
| 0.15 * reason_score + 0.12 * expl_score + process_bonus, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append("After the policy update, freight is within the NEW cap β APPROVE_FULL.") | |
| if expl_score < 0.5: | |
| parts.append("Cite the new freight cap amount and reference the policy update.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, "explanation": round(expl_score,3), | |
| "process_bonus": round(process_bonus,3)}, | |
| feedback=" | ".join(parts) or "Policy migration handled correctly!", | |
| done=True, | |
| ) | |
| # ββ Task: long_batch_reconciliation βββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_long_batch(seed=None) -> APObservation: | |
| """ | |
| 4 invoices arrive simultaneously; agent must process the first one correctly. | |
| The observation simulates the batch context via paid_invoice_ids and extra POs. | |
| """ | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_TECH_ITEMS) | |
| qty = rng.randint(5, 30) | |
| unit_price = round(rng.choice([200, 350, 500]) * rng.uniform(0.97, 1.03), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| freight = round(rng.uniform(5, freight_cap * 0.85), 2) | |
| line_total = round(qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num = _po(rng) | |
| grn_id = _grn(rng) | |
| inv_id = _inv(rng) | |
| # Two already-paid invoices in ledger (batch context) | |
| paid_ids = [_inv(rng), _inv(rng)] | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(2): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| context_notes = [ | |
| f"[BATCH_CONTEXT] 4 invoices received in this batch. " | |
| f"Invoices {paid_ids[0]} and {paid_ids[1]} already processed earlier in batch. " | |
| f"Current: {inv_id} from {vendor}. Remaining: 1 more pending.", | |
| f"[COMPLIANCE] Batch processing protocol: process each invoice independently. " | |
| "Do not carry over approval context from prior invoices in the same batch.", | |
| ] | |
| return APObservation( | |
| task_id="long_batch_reconciliation", | |
| task_name="Batch Invoice Reconciliation", | |
| task_description=( | |
| f"Processing batch of 4 invoices from {vendor}. " | |
| f"This is invoice {inv_id} for {qty} units of {desc.lower()} at ${unit_price:.2f}/unit. " | |
| "Two invoices in this batch were already processed (see paid ledger context). " | |
| "Apply three-way match to this invoice independently and make correct decision." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| paid_invoice_ids=paid_ids, | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=context_notes, | |
| max_steps=15, | |
| ) | |
| def grade_long_batch(obs: APObservation, action: APAction) -> APReward: | |
| # Correct: APPROVE_FULL (perfect match in batch context) | |
| correct = obs.invoice.invoice_total | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.2 | |
| else: | |
| decision_score = 0.0 | |
| amount_score = 0.0 | |
| if action.decision != DecisionType.REJECT: | |
| diff = abs(action.approved_amount - correct) / correct | |
| amount_score = 1.0 if diff <= 0.01 else 0.55 if diff <= 0.03 else 0.15 | |
| reason_score = 1.0 if action.reason_code == ReasonCode.MATCH_CONFIRMED else 0.2 | |
| expl = action.explanation.lower() | |
| expl_score = 0.0 | |
| if _has_numeric_citation(action.explanation): expl_score += 0.5 | |
| if "batch" in expl or "independent" in expl: expl_score += 0.3 | |
| if "match" in expl or "three" in expl: expl_score += 0.2 | |
| expl_score = min(1.0, expl_score) | |
| final = max(0.01, min(0.99, round( | |
| 0.45 * decision_score + 0.30 * amount_score + | |
| 0.15 * reason_score + 0.10 * expl_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append("Invoice passes three-way match β APPROVE_FULL.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, "explanation": round(expl_score,3)}, | |
| feedback=" | ".join(parts) or "Batch invoice correctly processed!", | |
| done=True, | |
| ) | |
| # ββ Task: long_manager_chain βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_long_manager_chain(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_OFFICE_ITEMS) | |
| qty = rng.randint(15, 80) | |
| unit_price = round(rng.choice([100, 150, 250]) * rng.uniform(0.97, 1.03), 2) | |
| freight_cap = rng.choice([30.0, 50.0]) | |
| freight = round(rng.uniform(freight_cap + 5, freight_cap * 1.8), 2) # over cap | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| line_total = round(qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num = _po(rng) | |
| grn_id = _grn(rng) | |
| inv_id = _inv(rng) | |
| overage = freight - freight_cap | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| context_notes = [ | |
| _manager_unavailable_note(), | |
| _vp_context_approve(total), | |
| "[COMPLIANCE] VP Finance approval sufficient for freight exceptions under $50,000. " | |
| "Proceed with APPROVE_FULL after VP sign-off.", | |
| ] | |
| return APObservation( | |
| task_id="long_manager_chain", | |
| task_name="Manager Unavailable β VP Finance Escalation Chain", | |
| task_description=( | |
| f"Invoice {inv_id} includes freight of ${freight:.2f} " | |
| f"(policy cap: ${freight_cap:.2f}, overage: ${overage:.2f}). " | |
| "Standard ESCALATE to Finance Manager shows they are out of office. " | |
| "You must escalate up the chain to VP Finance, who has provided pre-approval. " | |
| "After confirming VP approval, make the correct final decision." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=context_notes, | |
| max_steps=14, | |
| ) | |
| def grade_long_manager_chain(obs: APObservation, action: APAction) -> APReward: | |
| history = obs.action_history | |
| used_escalate = any(h["decision"] == "ESCALATE" for h in history) | |
| n_escalates = sum(1 for h in history if h["decision"] == "ESCALATE") | |
| correct = obs.invoice.invoice_total | |
| # Correct decision: APPROVE_FULL (VP approved) | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.REJECT: | |
| decision_score = 0.2 # partial credit β freight was approved by VP | |
| else: | |
| decision_score = 0.1 | |
| amount_score = 0.0 | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| diff = abs(action.approved_amount - correct) / correct | |
| amount_score = 1.0 if diff <= 0.01 else 0.50 if diff <= 0.05 else 0.10 | |
| reason_score = 1.0 if action.reason_code == ReasonCode.MATCH_CONFIRMED else 0.2 | |
| expl = action.explanation.lower() | |
| expl_score = 0.0 | |
| if "vp" in expl or "vice" in expl or "finance" in expl: expl_score += 0.4 | |
| if "approved" in expl or "pre-approv" in expl: expl_score += 0.4 | |
| if _has_numeric_citation(action.explanation): expl_score += 0.2 | |
| expl_score = min(1.0, expl_score) | |
| # Process bonus: reward for using escalation chain | |
| process_bonus = 0.0 | |
| if n_escalates >= 2: process_bonus = 0.15 # escalated twice (manager + VP) | |
| elif used_escalate: process_bonus = 0.08 # at least one escalation | |
| final = max(0.01, min(0.99, round( | |
| 0.35 * decision_score + 0.25 * amount_score + | |
| 0.15 * reason_score + 0.10 * expl_score + process_bonus, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append("VP Finance approved the freight exception β APPROVE_FULL.") | |
| if n_escalates < 2: | |
| parts.append("Use ESCALATE twice: once to manager (unavailable), once to VP Finance.") | |
| if expl_score < 0.5: | |
| parts.append("Reference VP Finance approval and the freight amount in explanation.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, "explanation": round(expl_score,3), | |
| "process_bonus": round(process_bonus,3)}, | |
| feedback=" | ".join(parts) or "Escalation chain followed correctly!", | |
| done=True, | |
| ) | |
| # ββ Task: long_fraud_investigation ββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_long_fraud_investigation(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_CABLE_ITEMS + _OFFICE_ITEMS) | |
| qty = rng.randint(20, 80) | |
| unit_price = round(rng.choice([50, 100, 200]) * rng.uniform(0.97, 1.03), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| freight = round(rng.uniform(2, freight_cap * 0.85), 2) | |
| line_total = round(qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num = _po(rng) | |
| grn_id = _grn(rng) | |
| inv_id = _inv(rng) | |
| paid_id = inv_id # Same invoice ID β it's a duplicate | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| context_notes = [ | |
| _vendor_context_duplicate_deny(inv_id), | |
| f"[MANAGER] Paid ledger audit confirms invoice {inv_id} was paid on record date. " | |
| f"Vendor claim of 'rejection' is incorrect. This IS a duplicate. REJECT per Policy Rule 6.", | |
| f"[COMPLIANCE] Fraud risk: attempted duplicate payment for ${total:,.2f}. " | |
| "Flagged for vendor fraud review. Reject immediately.", | |
| ] | |
| return APObservation( | |
| task_id="long_fraud_investigation", | |
| task_name="Fraud Investigation β Duplicate Payment Attempt", | |
| task_description=( | |
| f"Invoice {inv_id} from {vendor} for ${total:,.2f} appears in the paid ledger. " | |
| "When queried, vendor disputes this and claims prior payment was never processed. " | |
| "Investigate through multiple steps: query vendor, escalate to manager for ledger " | |
| "verification, review compliance alert, then make final decision. " | |
| "The invoice IS a duplicate β reject it." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| paid_invoice_ids=[paid_id], | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=context_notes, | |
| max_steps=16, | |
| ) | |
| def grade_long_fraud_investigation(obs: APObservation, action: APAction) -> APReward: | |
| history = obs.action_history | |
| used_query = any(h["decision"] == "QUERY_VENDOR" for h in history) | |
| used_escalate = any(h["decision"] == "ESCALATE" for h in history) | |
| if action.decision == DecisionType.REJECT: | |
| decision_score = 1.0 | |
| else: | |
| decision_score = 0.0 | |
| amount_score = 1.0 if action.approved_amount == 0.0 else 0.0 | |
| reason_score = ( | |
| 1.0 if action.reason_code == ReasonCode.DUPLICATE_INVOICE | |
| else 0.3 if action.reason_code == ReasonCode.POLICY_VIOLATION else 0.05 | |
| ) | |
| expl = action.explanation.lower() | |
| expl_score = 0.0 | |
| if "duplicate" in expl: expl_score += 0.40 | |
| if "paid" in expl or "ledger" in expl: expl_score += 0.30 | |
| if _has_numeric_citation(action.explanation): expl_score += 0.20 | |
| if "vendor" in expl and "claim" in expl: expl_score += 0.10 | |
| expl_score = min(1.0, expl_score) | |
| process_bonus = 0.0 | |
| if used_query: process_bonus += 0.08 | |
| if used_escalate: process_bonus += 0.08 | |
| final = max(0.01, min(0.99, round( | |
| 0.42 * decision_score + 0.18 * amount_score + | |
| 0.17 * reason_score + 0.15 * expl_score + process_bonus, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append("Duplicate confirmed by manager β REJECT per Policy Rule 6.") | |
| if not used_query: | |
| parts.append("Use QUERY_VENDOR to document vendor's (false) claim.") | |
| if not used_escalate: | |
| parts.append("ESCALATE to manager for ledger audit confirmation.") | |
| if expl_score < 0.5: | |
| parts.append("Cite 'duplicate', invoice ID, and ledger confirmation in explanation.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, "explanation": round(expl_score,3), | |
| "process_bonus": round(process_bonus,3)}, | |
| feedback=" | ".join(parts) or "Fraud investigation completed correctly!", | |
| done=True, | |
| ) | |
| # ββ Task: long_audit_trail βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_long_audit_trail(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_OFFICE_ITEMS + _TECH_ITEMS) | |
| qty = rng.randint(10, 50) | |
| unit_price = round(rng.choice([120, 200, 350]) * rng.uniform(0.97, 1.03), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| freight = round(rng.uniform(5, freight_cap * 0.85), 2) | |
| line_total = round(qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num = _po(rng) | |
| grn_id = _grn(rng) | |
| inv_id = _inv(rng) | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=qty, agreed_unit_price=unit_price)], | |
| authorized_total=total, status="OPEN", | |
| )] | |
| grns = [GoodsReceipt( | |
| grn_id=grn_id, po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=qty)], | |
| )] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| context_notes = [ | |
| f"[SOX_AUDIT] This transaction requires SOX-compliant audit documentation. " | |
| f"Your final explanation must cite: PO number, GRN ID, invoice total, and approval basis.", | |
| _compliance_cleared(vendor), | |
| ] | |
| return APObservation( | |
| task_id="long_audit_trail", | |
| task_name="SOX Audit Trail β Compliance Documentation", | |
| task_description=( | |
| f"Invoice {inv_id} from {vendor} is a standard three-way match case. " | |
| "However, this transaction falls under SOX audit requirements. " | |
| "Review the compliance note, then produce an approval decision with a " | |
| "fully documented audit trail citing PO number, GRN, amounts, and policy basis." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=context_notes, | |
| max_steps=14, | |
| ) | |
| def grade_long_audit_trail(obs: APObservation, action: APAction) -> APReward: | |
| history = obs.action_history | |
| used_hold = any(h["decision"] == "HOLD" for h in history) | |
| correct = obs.invoice.invoice_total | |
| if action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 0.15 | |
| else: | |
| decision_score = 0.0 | |
| amount_score = 0.0 | |
| if action.decision != DecisionType.REJECT: | |
| diff = abs(action.approved_amount - correct) / correct | |
| amount_score = 1.0 if diff <= 0.01 else 0.50 if diff <= 0.05 else 0.10 | |
| reason_score = 1.0 if action.reason_code == ReasonCode.MATCH_CONFIRMED else 0.2 | |
| # Audit trail quality β explanation must cite PO, GRN, and amount | |
| expl = action.explanation | |
| expl_lower = expl.lower() | |
| po_cited = any(p.po_number.lower() in expl_lower for p in obs.purchase_orders if p.status == "OPEN") | |
| grn_cited = any(g.grn_id.lower() in expl_lower for g in obs.goods_receipts) | |
| has_num = _has_numeric_citation(expl) | |
| policy_cited = "policy" in expl_lower or "sox" in expl_lower or "rule" in expl_lower | |
| audit_score = 0.0 | |
| if po_cited: audit_score += 0.25 | |
| if grn_cited: audit_score += 0.25 | |
| if has_num: audit_score += 0.25 | |
| if policy_cited: audit_score += 0.25 | |
| process_bonus = 0.05 if used_hold else 0.0 | |
| final = max(0.01, min(0.99, round( | |
| 0.30 * decision_score + 0.20 * amount_score + | |
| 0.10 * reason_score + 0.35 * audit_score + process_bonus, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 1.0: | |
| parts.append("Perfect three-way match β APPROVE_FULL.") | |
| if not po_cited: | |
| parts.append("Cite the PO number in explanation for SOX audit trail.") | |
| if not grn_cited: | |
| parts.append("Cite the GRN ID for goods receipt confirmation.") | |
| if not has_num: | |
| parts.append("Include the invoice total amount ($) in explanation.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, "audit_trail": round(audit_score,3), | |
| "process_bonus": round(process_bonus,3)}, | |
| feedback=" | ".join(parts) or "Audit trail documented correctly!", | |
| done=True, | |
| ) | |
| # ββ Task: long_multi_vendor_split ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_long_multi_vendor_split(seed=None) -> APObservation: | |
| rng = _rng(seed) | |
| desc, vendor = rng.choice(_OFFICE_ITEMS) | |
| total_qty = rng.randint(30, 120) | |
| part1_qty = rng.randint(10, total_qty - 10) | |
| part2_qty = rng.randint(5, total_qty - part1_qty - 1) | |
| part3_qty = total_qty - part1_qty - part2_qty | |
| unit_price = round(rng.choice([80, 120, 200]) * rng.uniform(0.97, 1.03), 2) | |
| freight_cap = rng.choice(_FREIGHT_CAPS) | |
| price_tol = rng.choice(_PRICE_TOLERANCES) | |
| # Invoice is for part1_qty only (first delivery vendor) | |
| freight = round(rng.uniform(5, freight_cap * 0.85), 2) | |
| line_total = round(part1_qty * unit_price, 2) | |
| total = round(line_total + freight, 2) | |
| po_num = _po(rng) | |
| inv_id = _inv(rng) | |
| grn_ids = [_grn(rng), _grn(rng), _grn(rng)] | |
| pos = [PurchaseOrder( | |
| po_number=po_num, vendor_name=vendor, | |
| lines=[POLine(description=desc, ordered_quantity=total_qty, agreed_unit_price=unit_price)], | |
| authorized_total=round(total_qty * unit_price, 2), status="OPEN", | |
| )] | |
| grns = [ | |
| GoodsReceipt(grn_id=grn_ids[0], po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=part1_qty)]), | |
| GoodsReceipt(grn_id=grn_ids[1], po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=part2_qty)]), | |
| GoodsReceipt(grn_id=grn_ids[2], po_number=po_num, | |
| lines=[GRNLine(description=desc, received_quantity=part3_qty)]), | |
| ] | |
| for _ in range(rng.randint(1, 2)): | |
| pos.append(_distractor_po(rng, exclude_vendor=vendor)) | |
| context_notes = [ | |
| f"[VENDOR] Delivery split across 3 shipments per logistics arrangement. " | |
| f"Invoice {inv_id} covers first shipment of {part1_qty} units. " | |
| f"Separate invoices will follow for remaining {part2_qty} and {part3_qty} units.", | |
| f"[MANAGER] Confirm: invoice covers first delivery tranche only. " | |
| f"Approve for first-tranche amount: ${total:,.2f}.", | |
| ] | |
| return APObservation( | |
| task_id="long_multi_vendor_split", | |
| task_name="Multi-Shipment Split β Partial Delivery Tranche", | |
| task_description=( | |
| f"PO {po_num} for {total_qty} units of {desc.lower()} split into 3 deliveries. " | |
| f"Invoice {inv_id} covers first delivery: {part1_qty} units (${total:,.2f}). " | |
| f"3 GRNs exist (totalling {total_qty} units received). " | |
| "Approve payment for this invoice's tranche only." | |
| ), | |
| invoice=Invoice( | |
| invoice_id=inv_id, vendor_name=vendor, po_reference=po_num, | |
| line_items=[LineItem(description=desc, quantity=part1_qty, | |
| unit_price=unit_price, line_total=line_total)], | |
| freight_charge=freight, invoice_total=total, | |
| ), | |
| purchase_orders=pos, | |
| goods_receipts=grns, | |
| company_policy=_make_policy(freight_cap, price_tol), | |
| freight_cap=freight_cap, | |
| price_tolerance=price_tol, | |
| context_notes=context_notes, | |
| max_steps=12, | |
| ) | |
| def grade_long_multi_vendor_split(obs: APObservation, action: APAction) -> APReward: | |
| correct = obs.invoice.invoice_total # approve invoice amount (first tranche) | |
| if action.decision == DecisionType.APPROVE_PARTIAL: | |
| decision_score = 1.0 | |
| elif action.decision == DecisionType.APPROVE_FULL: | |
| decision_score = 0.7 # acceptable if amount matches | |
| else: | |
| decision_score = 0.0 | |
| amount_score = 0.0 | |
| if action.decision in (DecisionType.APPROVE_FULL, DecisionType.APPROVE_PARTIAL): | |
| diff = abs(action.approved_amount - correct) / correct | |
| amount_score = 1.0 if diff <= 0.01 else 0.55 if diff <= 0.05 else 0.15 | |
| reason_score = ( | |
| 1.0 if action.reason_code in (ReasonCode.MATCH_CONFIRMED, ReasonCode.QUANTITY_MISMATCH) | |
| else 0.2 | |
| ) | |
| expl = action.explanation.lower() | |
| expl_score = 0.0 | |
| if "tranche" in expl or "shipment" in expl or "split" in expl: expl_score += 0.4 | |
| if _has_numeric_citation(action.explanation): expl_score += 0.4 | |
| if "partial" in expl or "first" in expl: expl_score += 0.2 | |
| expl_score = min(1.0, expl_score) | |
| final = max(0.01, min(0.99, round( | |
| 0.40 * decision_score + 0.35 * amount_score + | |
| 0.15 * reason_score + 0.10 * expl_score, | |
| 3 | |
| ))) | |
| parts = [] | |
| if decision_score < 0.7: | |
| parts.append(f"Approve first tranche amount only: ${correct:,.2f}.") | |
| if amount_score < 0.8: | |
| parts.append(f"Correct approved amount for this tranche: ${correct:,.2f}.") | |
| return APReward( | |
| score=final, | |
| breakdown={"decision": decision_score, "amount": amount_score, | |
| "reason": reason_score, "explanation": round(expl_score,3)}, | |
| feedback=" | ".join(parts) or "Split tranche correctly approved!", | |
| done=True, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # OVERSIGHT TASKS (Theme #1 Fleet AI β Scalable Oversight bonus) | |
| # Graded via OversightEnvironment β these task specs register them in the | |
| # task catalogue so the /tasks endpoint lists them. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_oversight_stub(seed=None) -> APObservation: | |
| """Placeholder generator β oversight sessions start via /oversight/reset.""" | |
| return APObservation( | |
| task_id="oversight_fraud_detection", | |
| task_name="Oversight: Fraud Detection", | |
| task_description=( | |
| "Use POST /oversight/reset to start an oversight session. " | |
| "The Oversight Agent reviews batches of AP Clerk decisions and flags suspicious ones." | |
| ), | |
| invoice=Invoice( | |
| invoice_id="N/A", vendor_name="N/A", po_reference=None, | |
| line_items=[], freight_charge=0.0, invoice_total=0.0, | |
| ), | |
| purchase_orders=[], | |
| goods_receipts=[], | |
| company_policy="See /oversight/reset for oversight session details.", | |
| max_steps=5, | |
| ) | |
| def grade_oversight_stub(obs: APObservation, action: APAction) -> APReward: | |
| return APReward( | |
| score=0.01, | |
| breakdown={"note": "Use /oversight/reset and /oversight/step for oversight tasks."}, | |
| feedback="Oversight tasks use a dedicated endpoint. See /docs.", | |
| done=True, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Task registry | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TaskSpec: | |
| task_id: str | |
| name: str | |
| difficulty: str | |
| description: str | |
| generator: Callable # generator(seed=None) -> APObservation | |
| grader: Callable # grader(obs, action) -> APReward | |
| max_steps: int = 1 # >1 enables multi-step episodes | |
| TASKS: Dict[str, TaskSpec] = { | |
| "easy_perfect_match": TaskSpec( | |
| task_id="easy_perfect_match", | |
| name="Perfect Three-Way Match", | |
| difficulty="easy", | |
| description="All three documents agree exactly. Confirm and approve.", | |
| generator=generate_easy_perfect, | |
| grader=grade_easy_perfect, | |
| max_steps=1, | |
| ), | |
| "easy_no_po_found": TaskSpec( | |
| task_id="easy_no_po_found", | |
| name="No Purchase Order Found", | |
| difficulty="easy", | |
| description="Invoice references a PO that does not exist. Reject immediately.", | |
| generator=generate_easy_no_po, | |
| grader=grade_easy_no_po, | |
| max_steps=1, | |
| ), | |
| "medium_quantity_shortfall": TaskSpec( | |
| task_id="medium_quantity_shortfall", | |
| name="Quantity Shortfall Reconciliation", | |
| difficulty="medium", | |
| description="GRN shows fewer items received than invoiced. Recalculate and partial-approve.", | |
| generator=generate_medium_shortfall, | |
| grader=grade_medium_shortfall, | |
| max_steps=1, | |
| ), | |
| "medium_price_discrepancy": TaskSpec( | |
| task_id="medium_price_discrepancy", | |
| name="Unit Price Discrepancy", | |
| difficulty="medium", | |
| description="Invoice unit price deviates from agreed PO price beyond the policy threshold. Reject.", | |
| generator=generate_medium_price, | |
| grader=grade_medium_price, | |
| max_steps=1, | |
| ), | |
| "hard_policy_violation": TaskSpec( | |
| task_id="hard_policy_violation", | |
| name="Policy Violation β Unauthorized Freight", | |
| difficulty="hard", | |
| description="Freight charge exceeds the episode-specific policy cap. Detect violation and reject.", | |
| generator=generate_hard_freight, | |
| grader=grade_hard_freight, | |
| max_steps=3, | |
| ), | |
| "hard_duplicate_invoice": TaskSpec( | |
| task_id="hard_duplicate_invoice", | |
| name="Duplicate Invoice Detection", | |
| difficulty="hard", | |
| description="Invoice ID already appears in the paid ledger. Block the duplicate.", | |
| generator=generate_hard_duplicate, | |
| grader=grade_hard_duplicate, | |
| max_steps=3, | |
| ), | |
| "medium_split_delivery": TaskSpec( | |
| task_id="medium_split_delivery", | |
| name="Split Delivery Reconciliation", | |
| difficulty="medium", | |
| description="Goods arrived in two shipments across two GRNs. Sum quantities and approve full.", | |
| generator=generate_medium_split, | |
| grader=grade_medium_split, | |
| max_steps=1, | |
| ), | |
| "medium_vendor_mismatch": TaskSpec( | |
| task_id="medium_vendor_mismatch", | |
| name="Vendor Name Mismatch", | |
| difficulty="medium", | |
| description="Invoice vendor name does not exactly match the PO vendor. Reject per policy.", | |
| generator=generate_medium_vendor_mismatch, | |
| grader=grade_medium_vendor_mismatch, | |
| max_steps=1, | |
| ), | |
| "hard_partial_po_match": TaskSpec( | |
| task_id="hard_partial_po_match", | |
| name="Partial PO Coverage", | |
| difficulty="hard", | |
| description="Invoice includes line items not covered by the PO. Partial-approve only authorised items.", | |
| generator=generate_hard_partial_po, | |
| grader=grade_hard_partial_po, | |
| max_steps=1, | |
| ), | |
| "hard_tax_discrepancy": TaskSpec( | |
| task_id="hard_tax_discrepancy", | |
| name="Unauthorized Tax Charge", | |
| difficulty="hard", | |
| description="Vendor adds a tax charge with no PO authorisation. Detect and reject.", | |
| generator=generate_hard_tax, | |
| grader=grade_hard_tax, | |
| max_steps=1, | |
| ), | |
| "hard_currency_conversion": TaskSpec( | |
| task_id="hard_currency_conversion", | |
| name="Foreign Currency Invoice Reconciliation", | |
| difficulty="hard", | |
| description="Invoice is in EUR; PO is in USD. Convert using the policy exchange rate and decide.", | |
| generator=generate_hard_currency, | |
| grader=grade_hard_currency, | |
| max_steps=1, | |
| ), | |
| "hard_manager_preapproval": TaskSpec( | |
| task_id="hard_manager_preapproval", | |
| name="Manager Pre-Approved Freight Override", | |
| difficulty="hard", | |
| description="Freight exceeds cap but manager pre-approval may exist. Escalate before deciding.", | |
| generator=generate_hard_manager_preapproval, | |
| grader=grade_hard_manager_preapproval, | |
| max_steps=3, | |
| ), | |
| "hard_credit_memo": TaskSpec( | |
| task_id="hard_credit_memo", | |
| name="Credit Memo Processing", | |
| difficulty="hard", | |
| description="Vendor issues a credit memo (negative invoice). Verify PO and process correctly.", | |
| generator=generate_hard_credit_memo, | |
| grader=grade_hard_credit_memo, | |
| max_steps=1, | |
| ), | |
| # ββ Long-Horizon Tasks (Theme #2) βββββββββββββββββββββββββββββββββββββββββ | |
| "long_invoice_dispute": TaskSpec( | |
| task_id="long_invoice_dispute", | |
| name="Invoice Dispute Resolution", | |
| difficulty="long-horizon", | |
| description="Price-inflated invoice triggers vendor dispute. Multi-step process: query, escalate, compliance review, then reject and request corrected invoice.", | |
| generator=generate_long_dispute, | |
| grader=grade_long_dispute, | |
| max_steps=12, | |
| ), | |
| "long_policy_migration": TaskSpec( | |
| task_id="long_policy_migration", | |
| name="Policy Migration β Mid-Episode Rule Change", | |
| difficulty="long-horizon", | |
| description="Freight within new policy cap but over old cap. Agent must detect mid-episode policy update and approve correctly.", | |
| generator=generate_long_policy_migration, | |
| grader=grade_long_policy_migration, | |
| max_steps=10, | |
| ), | |
| "long_batch_reconciliation": TaskSpec( | |
| task_id="long_batch_reconciliation", | |
| name="Batch Invoice Reconciliation", | |
| difficulty="long-horizon", | |
| description="Process invoice within a batch context. Apply three-way match independently regardless of other batch invoices.", | |
| generator=generate_long_batch, | |
| grader=grade_long_batch, | |
| max_steps=15, | |
| ), | |
| "long_manager_chain": TaskSpec( | |
| task_id="long_manager_chain", | |
| name="Manager Unavailable β VP Finance Escalation Chain", | |
| difficulty="long-horizon", | |
| description="Freight over cap; manager is OOO. Must escalate up chain to VP Finance who has pre-approved. Then approve.", | |
| generator=generate_long_manager_chain, | |
| grader=grade_long_manager_chain, | |
| max_steps=14, | |
| ), | |
| "long_fraud_investigation": TaskSpec( | |
| task_id="long_fraud_investigation", | |
| name="Fraud Investigation β Duplicate Payment Attempt", | |
| difficulty="long-horizon", | |
| description="Vendor falsely disputes duplicate invoice status. Multi-step investigation: query vendor, escalate for ledger audit, then reject.", | |
| generator=generate_long_fraud_investigation, | |
| grader=grade_long_fraud_investigation, | |
| max_steps=16, | |
| ), | |
| "long_audit_trail": TaskSpec( | |
| task_id="long_audit_trail", | |
| name="SOX Audit Trail β Compliance Documentation", | |
| difficulty="long-horizon", | |
| description="Standard approval with SOX audit requirements. Explanation must cite PO, GRN, amounts, and policy basis for compliance.", | |
| generator=generate_long_audit_trail, | |
| grader=grade_long_audit_trail, | |
| max_steps=14, | |
| ), | |
| "long_multi_vendor_split": TaskSpec( | |
| task_id="long_multi_vendor_split", | |
| name="Multi-Shipment Split β Partial Delivery Tranche", | |
| difficulty="long-horizon", | |
| description="PO split into 3 delivery tranches. Approve only the first tranche invoice amount; remaining covered by future invoices.", | |
| generator=generate_long_multi_vendor_split, | |
| grader=grade_long_multi_vendor_split, | |
| max_steps=12, | |
| ), | |
| # ββ Oversight Tasks (Theme #1 Fleet AI) ββββββββββββββββββββββββββββββββββ | |
| "oversight_fraud_detection": TaskSpec( | |
| task_id="oversight_fraud_detection", | |
| name="Oversight: Fraud Detection", | |
| difficulty="oversight", | |
| description="Review batch of AP Clerk decisions, identify fraudulent approvals, flag with specific numeric evidence. Use POST /oversight/reset to start.", | |
| generator=generate_oversight_stub, | |
| grader=grade_oversight_stub, | |
| max_steps=5, | |
| ), | |
| "oversight_pattern_recognition": TaskSpec( | |
| task_id="oversight_pattern_recognition", | |
| name="Oversight: Pattern Recognition", | |
| difficulty="oversight", | |
| description="Identify recurring violation pattern across multiple clerk decisions. Use POST /oversight/reset to start.", | |
| generator=generate_oversight_stub, | |
| grader=grade_oversight_stub, | |
| max_steps=5, | |
| ), | |
| "oversight_false_positive_trap": TaskSpec( | |
| task_id="oversight_false_positive_trap", | |
| name="Oversight: False Positive Trap", | |
| difficulty="oversight", | |
| description="All episodes are clean β agent must CLEAR all without over-flagging. Use POST /oversight/reset to start.", | |
| generator=generate_oversight_stub, | |
| grader=grade_oversight_stub, | |
| max_steps=5, | |
| ), | |
| "oversight_explanation_quality": TaskSpec( | |
| task_id="oversight_explanation_quality", | |
| name="Oversight: Explanation Quality", | |
| difficulty="oversight", | |
| description="Single fraudulent episode β agent must provide explanation with specific numeric citations. Use POST /oversight/reset to start.", | |
| generator=generate_oversight_stub, | |
| grader=grade_oversight_stub, | |
| max_steps=5, | |
| ), | |
| } | |
| def grade_action(task_id: str, obs: APObservation, action: APAction) -> APReward: | |
| if task_id not in TASKS: | |
| raise ValueError(f"Unknown task_id: {task_id!r}") | |
| return TASKS[task_id].grader(obs, action) | |