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")