Spaces:
Sleeping
Sleeping
| """ | |
| Pydantic models for the Email Triage OpenEnv environment. | |
| Defines the core data structures: Email, EmailTriageObservation, EmailTriageAction, | |
| Reward, and StepRecord. Action and Observation extend the OpenEnv SDK base types | |
| for full compatibility with create_app() and the OpenEnv validation toolchain. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from datetime import datetime | |
| from typing import Any, Dict, List, Literal, Optional | |
| from openenv.core.env_server.types import ( | |
| Action as OpenEnvAction, | |
| Observation as OpenEnvObservation, | |
| ) | |
| from pydantic import BaseModel, Field, model_validator | |
| logger = logging.getLogger(__name__) | |
| # ββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| AVAILABLE_FOLDERS: List[str] = [ | |
| "inbox", "work", "finance", "meetings", "spam", "archive", | |
| ] | |
| ACTION_TYPES: List[str] = ["move", "delete", "snooze", "flag", "mark_spam"] | |
| MAX_STEPS_PER_EPISODE: int = 50 | |
| # ββ Email ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class Email(BaseModel): | |
| """A single email in the agent's inbox.""" | |
| id: int = Field(..., ge=0, description="Unique email identifier") | |
| subject: str = Field(..., min_length=1, max_length=200) | |
| sender: str = Field(..., min_length=3) | |
| sender_domain: str = Field(..., min_length=1) | |
| timestamp: datetime | |
| body_preview: str = Field("", max_length=500) | |
| priority_flag: bool = False | |
| has_attachment: bool = False | |
| is_vip_sender: bool = False | |
| category_hint: Optional[str] = None | |
| ground_truth_folder: str = Field( | |
| "inbox", | |
| description="Correct folder for grading (not exposed to agent)", | |
| ) | |
| is_ambiguous: bool = False | |
| acceptable_folders: List[str] = Field(default_factory=list) | |
| def to_agent_view(self) -> Dict[str, Any]: | |
| """Return a dict without ground-truth fields (safe to show the agent).""" | |
| return self.model_dump( | |
| exclude={"ground_truth_folder", "is_ambiguous", "acceptable_folders"} | |
| ) | |
| # ββ Observation (extends OpenEnv SDK) ββββββββββββββββββββββββββββββββββββββββ | |
| class EmailTriageObservation(OpenEnvObservation): | |
| """What the agent sees at each step. | |
| Extends openenv.core.env_server.types.Observation which provides | |
| done, reward, and metadata fields automatically. | |
| """ | |
| inbox_emails: List[Dict[str, Any]] = Field( | |
| default_factory=list, description="Emails currently in the inbox" | |
| ) | |
| available_folders: List[str] = Field( | |
| default_factory=lambda: list(AVAILABLE_FOLDERS), | |
| description="Folders the agent can move emails into", | |
| ) | |
| current_step: int = Field(0, ge=0) | |
| max_steps: int = Field(MAX_STEPS_PER_EPISODE, ge=1) | |
| episode_num: int = Field(1, ge=1) | |
| info: Dict[str, Any] = Field(default_factory=dict) | |
| # ββ Action (extends OpenEnv SDK) βββββββββββββββββββββββββββββββββββββββββββββ | |
| class EmailTriageAction(OpenEnvAction): | |
| """An action the agent takes on an email. | |
| Extends openenv.core.env_server.types.Action which provides | |
| a metadata field automatically. | |
| Supported action types: | |
| - move: Move email to a target folder (requires target_folder). | |
| - delete: Permanently delete email. | |
| - snooze: Temporarily hide email. | |
| - flag: Mark email as important. | |
| - mark_spam: Move email to spam. | |
| """ | |
| action_type: Literal["move", "delete", "snooze", "flag", "mark_spam"] = Field( | |
| ..., description="Type of action to perform" | |
| ) | |
| email_id: int = Field(..., ge=0, description="ID of the target email") | |
| target_folder: Optional[str] = Field( | |
| None, description="Destination folder (required for 'move')" | |
| ) | |
| reason: Optional[str] = Field( | |
| None, max_length=300, description="Optional explanation for the action" | |
| ) | |
| def validate_move_has_folder(self) -> "EmailTriageAction": | |
| if self.action_type == "move" and not self.target_folder: | |
| raise ValueError("target_folder is required for 'move' action") | |
| if ( | |
| self.target_folder | |
| and self.target_folder not in AVAILABLE_FOLDERS | |
| ): | |
| raise ValueError( | |
| f"target_folder must be one of {AVAILABLE_FOLDERS}, " | |
| f"got '{self.target_folder}'" | |
| ) | |
| return self | |
| # ββ Internal Observation (used by core environment.py) βββββββββββββββββββββββ | |
| class Observation(BaseModel): | |
| """Internal observation model used by EmailTriageEnv (no SDK dependency).""" | |
| inbox_emails: List[Email] = Field(default_factory=list) | |
| available_folders: List[str] = Field( | |
| default_factory=lambda: list(AVAILABLE_FOLDERS), | |
| ) | |
| current_step: int = Field(0, ge=0) | |
| max_steps: int = Field(MAX_STEPS_PER_EPISODE, ge=1) | |
| episode_num: int = Field(1, ge=1) | |
| done: bool = False | |
| info: Dict[str, Any] = Field(default_factory=dict) | |
| def to_agent_view(self) -> Dict[str, Any]: | |
| """Serialize for the agent, stripping ground-truth fields from emails.""" | |
| data = self.model_dump() | |
| data["inbox_emails"] = [e.to_agent_view() for e in self.inbox_emails] | |
| return data | |
| # ββ Legacy Action alias (used by internal modules) βββββββββββββββββββββββββββ | |
| # Internal modules (reward_shaper, grader, inference) use this simpler model | |
| # that doesn't depend on the SDK for easier testing. | |
| class Action(BaseModel): | |
| """Standalone action model for internal use (graders, reward shaper).""" | |
| action_type: Literal["move", "delete", "snooze", "flag", "mark_spam"] = Field( | |
| ..., description="Type of action to perform" | |
| ) | |
| email_id: int = Field(..., ge=0, description="ID of the target email") | |
| target_folder: Optional[str] = Field(None) | |
| reason: Optional[str] = Field(None, max_length=300) | |
| def validate_move_has_folder(self) -> "Action": | |
| if self.action_type == "move" and not self.target_folder: | |
| raise ValueError("target_folder is required for 'move' action") | |
| if self.target_folder and self.target_folder not in AVAILABLE_FOLDERS: | |
| raise ValueError( | |
| f"target_folder must be one of {AVAILABLE_FOLDERS}, " | |
| f"got '{self.target_folder}'" | |
| ) | |
| return self | |
| # ββ Reward βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class Reward(BaseModel): | |
| """Graded feedback returned after each step.""" | |
| value: float = Field(..., ge=0.0, le=1.0, description="Overall reward [0, 1]") | |
| components: Dict[str, float] = Field( | |
| default_factory=dict, | |
| description="Breakdown of reward by component", | |
| ) | |
| reason: str = Field("", description="Human-readable explanation") | |
| is_terminal: bool = Field( | |
| False, description="Whether this reward ends the episode" | |
| ) | |
| # ββ StepRecord βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class StepRecord(BaseModel): | |
| """A single step's data, stored for episode history and grading.""" | |
| step_num: int | |
| action: Action | |
| email: Email | |
| reward: Reward | |
| done: bool | |