Spaces:
Sleeping
Sleeping
| """ | |
| models.py — Type-safe data contracts for the Email Triage RL Environment | |
| ========================================================================= | |
| PURPOSE: | |
| Defines EXACTLY what data looks like when flowing between: | |
| - server/environment.py (the kitchen that creates data) | |
| - client.py (the waiter that carries data) | |
| - inference.py (the AI that reads data) | |
| - grader.py (the food critic that scores) | |
| Everyone imports from here. Change here → change everywhere. | |
| CHANGES FROM ORIGINAL: | |
| - estimated_response_time max raised from 120 → 180 | |
| (email_bank.json has a 180-min email; old cap caused Pydantic validation error) | |
| - All other fields unchanged | |
| """ | |
| from __future__ import annotations | |
| from enum import IntEnum, Enum | |
| from typing import Dict, List, Optional | |
| from pydantic import BaseModel, Field | |
| # --------------------------------------------------------------------------- | |
| # 1. ENUMS | |
| # --------------------------------------------------------------------------- | |
| class SenderImportance(str, Enum): | |
| """ | |
| The "tier" of a sender. Controls relationship decay rates and reward weights. | |
| VIP → Boss, Team Lead, Key Client (ignore_penalty = -20, respond_boost = +15) | |
| NORMAL → Teammates, Collaborators (ignore_penalty = -10, respond_boost = +10) | |
| SPAM → Newsletters, Cold Outreach (ignore_penalty = 0, respond_boost = 0) | |
| """ | |
| VIP = "VIP" | |
| NORMAL = "Normal" | |
| SPAM = "Spam" | |
| class Action(IntEnum): | |
| """ | |
| The two actions the AI agent can take each step. | |
| IGNORE (0) → Skip this email. Saves time but damages relationship. | |
| RESPOND (1) → Reply to this email. Costs time but boosts relationship. | |
| """ | |
| IGNORE = 0 | |
| RESPOND = 1 | |
| # --------------------------------------------------------------------------- | |
| # 2. EMAIL | |
| # --------------------------------------------------------------------------- | |
| class Email(BaseModel): | |
| """ | |
| Represents a single email in the inbox. | |
| KEY DESIGN DECISIONS: | |
| - base_priority (1-10): inherent importance. NOT always correlated with | |
| sender_importance (a VIP can send a low-priority "lunch?" email). | |
| - estimated_response_time: HIDDEN from the agent — it sees email_length instead. | |
| This is Blind Spot #1. | |
| - times_ignored: tracks ignore count per email; urgency escalates. | |
| - is_followup: True if this email exists because a previous one was ignored. | |
| - parent_email_id: links follow-up to its original email. | |
| """ | |
| email_id: int | |
| sender: str = Field(..., description="Full email address, e.g. boss@company.com") | |
| sender_domain: str = Field(..., description="'internal', 'external', or 'vip-domain'") | |
| subject: str | |
| body: str = Field(..., description="Full email body text") | |
| # Importance & Priority | |
| sender_importance: str = Field(..., description="VIP | Normal | Spam") | |
| base_priority: int = Field(..., ge=1, le=10, description="Inherent priority 1-10") | |
| # Time tracking (Blind Spot #1) — max 180 to support long audit emails | |
| estimated_response_time: int = Field( | |
| ..., ge=5, le=180, | |
| description="Minutes to respond. HIDDEN from agent — it sees email_length instead." | |
| ) | |
| # Episode tracking | |
| received_at_timestep: int = Field(default=0) | |
| times_ignored: int = Field(default=0) | |
| sender_urgency_multiplier: float = Field( | |
| default=1.0, | |
| description="Escalates by +0.5 each time this sender is ignored" | |
| ) | |
| # Follow-up tracking | |
| is_followup: bool = Field( | |
| default=False, | |
| description="True if generated because a previous email from this sender was ignored" | |
| ) | |
| parent_email_id: Optional[int] = Field( | |
| default=None, | |
| description="email_id of the original email if this is a follow-up" | |
| ) | |
| bank_email_id: Optional[int] = None | |
| # --------------------------------------------------------------------------- | |
| # 3. RELATIONSHIP | |
| # --------------------------------------------------------------------------- | |
| class Relationship(BaseModel): | |
| """ | |
| Tracks the health of our working relationship with each sender. | |
| health: Current score (0-100). Starts at 75. | |
| importance_weight: VIP=3, Normal=2, Spam=0. Used in health_penalty formula. | |
| degradation_rate: Health lost per ignore. Pulled from RELATIONSHIP_CONFIG. | |
| is_angry: True if VIP was ignored. Triggers follow-up generation. | |
| interaction_count: Count of times we responded to this sender. | |
| DECAY TABLE (from RELATIONSHIP_CONFIG below): | |
| VIP: ignore_penalty=-20, respond_boost=+15 | |
| Normal: ignore_penalty=-10, respond_boost=+10 | |
| Spam: ignore_penalty=0, respond_boost=0 | |
| """ | |
| sender_email: str | |
| health: float = Field(default=75.0, ge=0.0, le=100.0) | |
| importance: str = Field(..., description="VIP | Normal | Spam") | |
| importance_weight: int = Field(..., ge=0, le=3) | |
| degradation_rate: float = Field(...) | |
| is_angry: bool = Field(default=False) | |
| interaction_count: int = Field(default=0) | |
| # --------------------------------------------------------------------------- | |
| # 4. STATE (God-mode view — used by grader and for debugging) | |
| # --------------------------------------------------------------------------- | |
| class State(BaseModel): | |
| """ | |
| Complete snapshot of the environment. | |
| IMPORTANT: environment.py must return a DEEP COPY (not live references). | |
| Use copy.deepcopy() before returning. | |
| """ | |
| inbox: List[Email] = Field(..., description="All emails in this episode") | |
| current_email_index: int = Field(default=0) | |
| relationships: Dict[str, Relationship] = Field( | |
| ..., description="Maps sender_email → Relationship object" | |
| ) | |
| current_timestep: int = Field(default=0) | |
| time_budget_remaining: int = Field(default=480) | |
| total_time_spent: int = Field(default=0) | |
| emails_handled: int = Field(default=0) | |
| # --------------------------------------------------------------------------- | |
| # 5. OBSERVATION (What the agent actually sees — partial view) | |
| # --------------------------------------------------------------------------- | |
| class EmailObservation(BaseModel): | |
| """ | |
| What the LLM agent sees at each step. A LIMITED view of State. | |
| HIDDEN from agent (intentional partial observability): | |
| - estimated_response_time (agent sees email_length as proxy only) | |
| - other senders' relationship scores | |
| - future emails in the inbox | |
| - relationship degradation rates | |
| """ | |
| email_id: int | |
| sender: str | |
| subject: str | |
| body: str | |
| sender_importance: str = Field(..., description="VIP | Normal | Spam") | |
| email_length: int = Field(..., description="Character count. Proxy for time cost.") | |
| relationship_score: float = Field(..., description="Health with THIS sender only (0-100)") | |
| time_budget_remaining: int = Field(...) | |
| emails_remaining: int = Field(...) | |
| # --------------------------------------------------------------------------- | |
| # 6. STEP RESPONSE | |
| # --------------------------------------------------------------------------- | |
| class StepResponse(BaseModel): | |
| """ | |
| The full response from POST /step. | |
| """ | |
| observation: EmailObservation = Field( | |
| ..., description="The NEXT email to act on (or final observation if done=True)" | |
| ) | |
| reward: float = Field(...) | |
| done: bool = Field(...) | |
| info: dict = Field(default_factory=dict) | |
| # --------------------------------------------------------------------------- | |
| # 7. RELATIONSHIP CONFIG (constants — imported by environment.py) | |
| # --------------------------------------------------------------------------- | |
| RELATIONSHIP_CONFIG = { | |
| "VIP": { | |
| "ignore_penalty": -20, | |
| "respond_boost": +15, | |
| "followup_multiplier": 1.5, | |
| "importance_weight": 3, | |
| }, | |
| "Normal": { | |
| "ignore_penalty": -10, | |
| "respond_boost": +10, | |
| "followup_multiplier": 1.2, | |
| "importance_weight": 2, | |
| }, | |
| "Spam": { | |
| "ignore_penalty": 0, | |
| "respond_boost": 0, | |
| "followup_multiplier": 1.0, | |
| "importance_weight": 0, | |
| }, | |
| } |