Spaces:
Sleeping
Sleeping
File size: 13,879 Bytes
562f58d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | """
Typed models for the Invoice Exception Handler OpenEnv environment.
Every object the agent sees or produces is defined here as a Pydantic model.
This is the single source of truth for the data contract between the
environment simulation and the agent.
"""
from __future__ import annotations
import time
from enum import Enum
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Enumerations
# ---------------------------------------------------------------------------
class ActionType(str, Enum):
"""The nine action types an agent can take during an episode."""
INSPECT_FIELD = "inspect_field"
CROSS_CHECK = "cross_check"
RUN_CHECK = "run_check"
QUERY_SUPPLIER = "query_supplier"
QUERY_INTERNAL = "query_internal"
APPLY_RULE = "apply_rule"
MAKE_DECISION = "make_decision"
ROUTE_TO = "route_to"
CLOSE_CASE = "close_case"
class DecisionType(str, Enum):
"""Possible decisions the agent can make on a flagged invoice."""
APPROVE = "approve"
REJECT = "reject"
HOLD = "hold"
PARTIAL_APPROVE = "partial_approve"
class CaseStatus(str, Enum):
"""Lifecycle status of an invoice exception case."""
OPEN = "open"
IN_REVIEW = "in_review"
DECIDED = "decided"
ROUTED = "routed"
CLOSED = "closed"
# ---------------------------------------------------------------------------
# Document models — read-only context given to the agent
# ---------------------------------------------------------------------------
class LineItem(BaseModel):
"""One line on an invoice or purchase order."""
description: str = Field(..., description="Item description")
quantity: int = Field(..., description="Number of units")
unit_price: float = Field(..., description="Price per unit in INR")
total: float = Field(..., description="Line total in INR (quantity × unit_price)")
tax_rate: Optional[float] = Field(None, description="Tax rate as a percentage, if applicable")
class PurchaseOrder(BaseModel):
"""What was agreed to be purchased."""
po_number: str = Field(..., description="Unique PO identifier")
vendor_name: str = Field(..., description="Supplier name on the PO")
po_date: str = Field(..., description="Date the PO was raised (YYYY-MM-DD)")
line_items: List[LineItem] = Field(default_factory=list, description="Items on the PO")
total_amount: float = Field(..., description="Total PO value in INR")
payment_terms: str = Field("Net-30", description="Payment terms")
currency: str = Field("INR", description="Currency code")
class Invoice(BaseModel):
"""What the supplier is claiming — the document under exception review."""
invoice_number: str = Field(..., description="Unique invoice identifier")
supplier_name: str = Field(..., description="Supplier name on the invoice")
invoice_date: str = Field(..., description="Date of the invoice (YYYY-MM-DD)")
due_date: str = Field(..., description="Payment due date (YYYY-MM-DD)")
po_reference: str = Field(..., description="PO number referenced by this invoice")
line_items: List[LineItem] = Field(default_factory=list, description="Items invoiced")
subtotal: float = Field(..., description="Pre-tax total in INR")
tax_amount: float = Field(..., description="Total tax amount in INR")
tax_rate: float = Field(..., description="Applied tax rate as a percentage")
total_amount: float = Field(..., description="Grand total including tax in INR")
bank_account: str = Field(..., description="Supplier bank account on the invoice")
bank_name: str = Field("", description="Bank name")
ifsc_code: str = Field("", description="IFSC / routing code")
supplier_gstin: str = Field("", description="GST Identification Number on the invoice")
supplier_email: str = Field("", description="Email address on the invoice")
currency: str = Field("INR", description="Currency code")
class GoodsReceiptNote(BaseModel):
"""What actually arrived at the warehouse (or service confirmation)."""
grn_number: str = Field(..., description="Unique GRN identifier")
po_reference: str = Field(..., description="PO number this receipt is against")
receipt_date: str = Field(..., description="Date goods/services were received (YYYY-MM-DD)")
items_received: List[Dict[str, Any]] = Field(
default_factory=list,
description="List of received item dicts with description, quantity_received, quantity_pending, quantity_rejected"
)
receiving_officer: str = Field("", description="Person who signed the receipt")
notes: str = Field("", description="Any delivery notes or discrepancies observed")
class SupplierMaster(BaseModel):
"""The verified, registered supplier record in the company's ERP system."""
supplier_id: str = Field(..., description="Internal supplier code")
supplier_name: str = Field(..., description="Registered legal name")
registered_address: str = Field("", description="Registered business address")
gstin: str = Field(..., description="Verified GST Identification Number")
bank_account: str = Field(..., description="Verified bank account number")
bank_name: str = Field("", description="Bank name")
ifsc_code: str = Field("", description="Verified IFSC / routing code")
contact_email: str = Field("", description="Registered email address")
contact_phone: str = Field("", description="Registered phone number")
registered_domain: str = Field("", description="Verified email domain for the supplier")
pan_number: str = Field("", description="PAN (tax ID)")
status: str = Field("active", description="Supplier status: active, suspended, blacklisted")
class ExceptionFlag(BaseModel):
"""Why the AP system flagged this invoice for manual review."""
flag_code: str = Field(..., description="Machine-readable code, e.g. PRICE_MISMATCH")
flag_description: str = Field(..., description="Human-readable explanation of the flag")
auto_hold: bool = Field(False, description="Whether the system placed an automatic payment hold")
flagged_date: str = Field("", description="Date the flag was raised (YYYY-MM-DD)")
severity: str = Field("medium", description="low / medium / high / critical")
# ---------------------------------------------------------------------------
# Action model
# ---------------------------------------------------------------------------
class Action(BaseModel):
"""
An action the agent wants to take.
Use the classmethod constructors for convenience:
Action.run_check("tolerance_rule")
Action.make_decision("approve", "reason here")
"""
type: ActionType = Field(..., description="Which action type to execute")
params: Dict[str, Any] = Field(default_factory=dict, description="Parameters for the action")
# --- Classmethod constructors for each action type ---
@classmethod
def inspect_field(cls, document: str, field: str) -> Action:
"""Look at a specific field in a document."""
return cls(type=ActionType.INSPECT_FIELD, params={"document": document, "field": field})
@classmethod
def cross_check(cls, field: str, doc_a: str, doc_b: str) -> Action:
"""Compare a field between two documents."""
return cls(type=ActionType.CROSS_CHECK, params={"field": field, "doc_a": doc_a, "doc_b": doc_b})
@classmethod
def run_check(cls, check_name: str) -> Action:
"""Run a named validation check."""
return cls(type=ActionType.RUN_CHECK, params={"check_name": check_name})
@classmethod
def query_supplier(cls, question: str, channel: str = "email") -> Action:
"""Ask the supplier a question via a specific channel."""
return cls(type=ActionType.QUERY_SUPPLIER, params={"question": question, "channel": channel})
@classmethod
def query_internal(cls, department: str, question: str) -> Action:
"""Ask an internal department a question."""
return cls(type=ActionType.QUERY_INTERNAL, params={"department": department, "question": question})
@classmethod
def apply_rule(cls, rule_id: str) -> Action:
"""Apply a named business policy rule."""
return cls(type=ActionType.APPLY_RULE, params={"rule_id": rule_id})
@classmethod
def make_decision(cls, decision: str, reason: str) -> Action:
"""Make a case decision with a documented reason."""
return cls(type=ActionType.MAKE_DECISION, params={"decision": decision, "reason": reason})
@classmethod
def route_to(cls, team: str, notes: str = "") -> Action:
"""Escalate the case to a specific team."""
return cls(type=ActionType.ROUTE_TO, params={"team": team, "notes": notes})
@classmethod
def close_case(cls, summary: str) -> Action:
"""Close the case with an audit trail summary."""
return cls(type=ActionType.CLOSE_CASE, params={"summary": summary})
# ---------------------------------------------------------------------------
# Result models — returned by simulators
# ---------------------------------------------------------------------------
class InspectionResult(BaseModel):
"""What came back from inspecting a specific field in a document."""
document: str = Field(..., description="Which document was inspected")
field: str = Field(..., description="Which field was inspected")
value: Any = Field(..., description="The value found in that field")
note: str = Field("", description="Any contextual note about the value")
timestamp: float = Field(default_factory=time.time, description="When the inspection happened")
class CheckResult(BaseModel):
"""What came back from running a validation check or cross-check."""
check_name: str = Field(..., description="Name of the check that was run")
passed: bool = Field(..., description="Whether the check passed (True) or failed (False)")
detail: str = Field("", description="Human-readable detail of what was found")
timestamp: float = Field(default_factory=time.time, description="When the check was run")
class QueryResult(BaseModel):
"""What came back from querying a supplier or internal department."""
target: str = Field(..., description="Who was queried (supplier, procurement, finance, etc.)")
question: str = Field("", description="The question that was asked")
response: str = Field(..., description="The response received")
channel: str = Field("email", description="Communication channel used (email, phone, etc.)")
timestamp: float = Field(default_factory=time.time, description="When the query was made")
# ---------------------------------------------------------------------------
# State models
# ---------------------------------------------------------------------------
class EnvironmentState(BaseModel):
"""
The full observable state returned by reset() and step().
This is what the agent sees at every turn — all documents, all history,
and all available actions/checks/rules for the current task.
"""
task_id: str = Field(..., description="Which task is currently running")
step_number: int = Field(0, description="Current step number in the episode")
case_status: CaseStatus = Field(CaseStatus.OPEN, description="Current lifecycle status")
# The five documents
purchase_order: PurchaseOrder = Field(..., description="The purchase order")
invoice: Invoice = Field(..., description="The invoice under review")
grn: GoodsReceiptNote = Field(..., description="The goods receipt note")
supplier_master: SupplierMaster = Field(..., description="The verified supplier record")
exception_flag: ExceptionFlag = Field(..., description="Why this invoice was flagged")
# Agent history — what has been done so far
inspections: List[InspectionResult] = Field(default_factory=list, description="Fields inspected")
checks_run: List[CheckResult] = Field(default_factory=list, description="Checks completed")
queries: List[QueryResult] = Field(default_factory=list, description="Queries made")
rules_applied: List[str] = Field(default_factory=list, description="Rules applied")
# Decision state
decision: Optional[str] = Field(None, description="Current decision if one has been made")
decision_reason: Optional[str] = Field(None, description="Reason for the decision")
routed_to: List[str] = Field(default_factory=list, description="Teams case has been routed to")
case_closed: bool = Field(False, description="Whether the case has been closed")
close_summary: Optional[str] = Field(None, description="Closure summary if case is closed")
# Action hints — what the agent can do
available_actions: List[str] = Field(default_factory=list, description="All valid action types")
available_checks: List[str] = Field(default_factory=list, description="Check names for this task")
available_rules: List[str] = Field(default_factory=list, description="Rule IDs for this task")
knowledge_base: List[str] = Field(default_factory=list, description="Policy entries for this task")
# Running totals
cumulative_reward: float = Field(0.0, description="Sum of all rewards received so far")
class StepResult(BaseModel):
"""What step() returns — the observation, reward, done flag, and info dict."""
observation: EnvironmentState = Field(..., description="Updated environment state after the action")
reward: float = Field(..., description="Reward for this specific action")
done: bool = Field(False, description="Whether the episode is over")
info: Dict[str, Any] = Field(default_factory=dict, description="Extra info about the step")
|