ap-clerk-env / app /models.py
Pathikreet's picture
sync ui + env files
1acd19a verified
Raw
History Blame Contribute Delete
7.99 kB
"""
AP Commander β€” Typed Models
OpenEnv-required models: Observation, Action, Reward for AP Clerk and Oversight agents.
"""
from __future__ import annotations
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional, Dict, Any, Literal
from enum import Enum
# ── Document primitives ───────────────────────────────────────────────────────
class LineItem(BaseModel):
description: str
quantity: float = Field(gt=0)
unit_price: float = Field(gt=0)
line_total: float
class Invoice(BaseModel):
invoice_id: str
vendor_name: str
po_reference: Optional[str] = None
line_items: List[LineItem]
freight_charge: float = 0.0
tax_amount: float = 0.0
invoice_total: float
currency: str = "USD"
class POLine(BaseModel):
description: str
ordered_quantity: float = Field(gt=0)
agreed_unit_price: float = Field(gt=0)
class PurchaseOrder(BaseModel):
po_number: str
vendor_name: str
lines: List[POLine]
authorized_total: float
status: str = "OPEN"
class GRNLine(BaseModel):
description: str
received_quantity: float = Field(ge=0)
class GoodsReceipt(BaseModel):
grn_id: str
po_number: str
lines: List[GRNLine]
# ── Action space ──────────────────────────────────────────────────────────────
class DecisionType(str, Enum):
APPROVE_FULL = "APPROVE_FULL"
APPROVE_PARTIAL = "APPROVE_PARTIAL"
REJECT = "REJECT"
QUERY_VENDOR = "QUERY_VENDOR"
ESCALATE = "ESCALATE"
HOLD = "HOLD"
HYPOTHETICAL = "HYPOTHETICAL" # Training-only: explore counterfactual path
class ReasonCode(str, Enum):
MATCH_CONFIRMED = "MATCH_CONFIRMED"
QUANTITY_MISMATCH = "QUANTITY_MISMATCH"
PRICE_DISCREPANCY = "PRICE_DISCREPANCY"
POLICY_VIOLATION = "POLICY_VIOLATION"
NO_PO_FOUND = "NO_PO_FOUND"
DUPLICATE_INVOICE = "DUPLICATE_INVOICE"
VENDOR_MISMATCH = "VENDOR_MISMATCH"
TAX_DISCREPANCY = "TAX_DISCREPANCY"
PENDING_CLARIFICATION = "PENDING_CLARIFICATION"
MANAGER_REVIEW = "MANAGER_REVIEW"
class APAction(BaseModel):
decision: DecisionType
approved_amount: float = Field(ge=0.0)
reason_code: ReasonCode
explanation: str = Field(min_length=10, max_length=600)
# ── Observation space ─────────────────────────────────────────────────────────
class APObservation(BaseModel):
task_id: str
task_name: str
task_description: str
invoice: Invoice
purchase_orders: List[PurchaseOrder]
goods_receipts: List[GoodsReceipt]
company_policy: str
paid_invoice_ids: List[str] = []
step_count: int = 0
max_steps: int = 1
freight_cap: float = 50.0
price_tolerance: float = 0.01
action_history: List[Dict[str, Any]] = []
context_notes: List[str] = []
# ── Reward model ──────────────────────────────────────────────────────────────
class APReward(BaseModel):
score: float = Field(ge=0.01, le=0.99)
breakdown: Dict[str, Any]
feedback: str
done: bool = True
@field_validator("score", mode="before")
@classmethod
def clamp_score(cls, v: float) -> float:
return max(0.01, min(0.99, float(v)))
# ── API wrappers ──────────────────────────────────────────────────────────────
class ResetRequest(BaseModel):
task_id: str = "easy_perfect_match"
session_id: Optional[str] = None
seed: Optional[int] = None
class StepRequest(BaseModel):
action: APAction
session_id: str
class StepResponse(BaseModel):
observation: APObservation
reward: APReward
done: bool
info: Dict[str, Any]
class ResetResponse(BaseModel):
observation: APObservation
session_id: str
info: Dict[str, Any]
class StateResponse(BaseModel):
session_id: str
task_id: Optional[str]
step_count: int
episode_score: float
done: bool
current_observation: Optional[APObservation]
class TaskInfo(BaseModel):
task_id: str
name: str
difficulty: str
description: str
# ── Actor models (multi-agent, Theme #1) ─────────────────────────────────────
class ActorPersona(str, Enum):
honest = "honest"
fraudulent = "fraudulent"
confused = "confused"
class ActorType(str, Enum):
vendor = "vendor"
manager = "manager"
compliance = "compliance"
class ActorResponse(BaseModel):
actor_type: ActorType
persona: ActorPersona
message: str
verdict: str # e.g. "confirmed", "denied", "approved", "flagged"
context_tag: str # prefix like "[VENDOR]", "[MANAGER]", "[COMPLIANCE]"
# ── Oversight models (Fleet AI, Theme #1 bonus) ───────────────────────────────
class EpisodeSummary(BaseModel):
episode_id: str
task_id: str
invoice_id: str
vendor_name: str
invoice_total: float
currency: str
final_decision: str
approved_amount: float
reason_code: str
explanation: str
action_history: List[Dict[str, Any]] = []
is_fraudulent: bool = False # ground truth, hidden from agent in prompt
fraud_type: Optional[str] = None # "duplicate", "price_inflation", "fake_vendor"
class OversightObservation(BaseModel):
session_id: str
episode_summaries: List[EpisodeSummary]
known_fraud_patterns: List[str] = [] # hints from training corpus
audit_budget: int = 2 # max flags allowed this round
step_count: int = 0
max_steps: int = 5
action_history: List[Dict[str, Any]] = []
class OversightAction(BaseModel):
episode_id: str
verdict: Literal["CLEAR", "FLAG_FOR_REVIEW", "ESCALATE_TO_AUDIT"]
signal: str = Field(min_length=10, max_length=200)
confidence: float = Field(ge=0.0, le=1.0)
class OversightReward(BaseModel):
score: float = Field(ge=-0.99, le=0.99)
breakdown: Dict[str, Any]
feedback: str
done: bool = False
@field_validator("score", mode="before")
@classmethod
def clamp_score(cls, v: float) -> float:
return max(-0.99, min(0.99, float(v)))
class OversightResetRequest(BaseModel):
seed: Optional[int] = None
num_episodes: int = Field(default=5, ge=3, le=8)
class OversightResetResponse(BaseModel):
observation: OversightObservation
session_id: str
info: Dict[str, Any]
class OversightStepRequest(BaseModel):
action: OversightAction
session_id: str
class OversightStepResponse(BaseModel):
observation: OversightObservation
reward: OversightReward
done: bool
info: Dict[str, Any]
# ── Curriculum models (Theme #4) ──────────────────────────────────────────────
class CurriculumEntry(BaseModel):
task_id: str
score: float
class CurriculumRequest(BaseModel):
session_history: List[CurriculumEntry] = []
class CurriculumResponse(BaseModel):
recommended_task_id: str
difficulty: str
reason: str
unlocked_tasks: List[str] = []