AvaneeshKGarg's picture
Initial commit: Customer Support Inbox OpenEnv
046cdff
Raw
History Blame Contribute Delete
8.53 kB
"""
OpenEnv typed models for Customer Support Inbox environment.
Implements Observation, Action, Reward per OpenEnv spec.
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
# ─── Enums ────────────────────────────────────────────────────────────────────
class TicketCategory(str, Enum):
BILLING = "billing"
TECHNICAL = "technical"
SHIPPING = "shipping"
RETURNS = "returns"
GENERAL = "general"
ACCOUNT = "account"
COMPLAINT = "complaint"
class TicketPriority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class TicketStatus(str, Enum):
OPEN = "open"
IN_PROGRESS = "in_progress"
WAITING_CUSTOMER = "waiting_customer"
ESCALATED = "escalated"
RESOLVED = "resolved"
CLOSED = "closed"
class ActionType(str, Enum):
CLASSIFY = "classify" # Assign category + priority
RESPOND = "respond" # Send a message to the customer
ESCALATE = "escalate" # Escalate ticket to senior team
RESOLVE = "resolve" # Mark ticket as resolved with resolution notes
REQUEST_INFO = "request_info" # Ask customer for more information
TAG = "tag" # Add searchable tags
ASSIGN = "assign" # Assign to department/agent
class SentimentLabel(str, Enum):
POSITIVE = "positive"
NEUTRAL = "neutral"
NEGATIVE = "negative"
ANGRY = "angry"
# ─── Sub-models ───────────────────────────────────────────────────────────────
class Message(BaseModel):
"""A single message in a ticket conversation."""
role: str = Field(..., description="'customer' or 'agent'")
content: str = Field(..., description="Message text")
timestamp: str = Field(..., description="ISO 8601 timestamp")
metadata: Dict[str, Any] = Field(default_factory=dict)
class CustomerProfile(BaseModel):
"""Basic customer profile information."""
customer_id: str
name: str
email: str
account_tier: str = Field(default="standard", description="standard | premium | enterprise")
total_orders: int = Field(default=0)
previous_tickets: int = Field(default=0)
sentiment: SentimentLabel = Field(default=SentimentLabel.NEUTRAL)
class TicketState(BaseModel):
"""Full internal state of a support ticket."""
ticket_id: str
subject: str
body: str
category: Optional[TicketCategory] = None
priority: Optional[TicketPriority] = None
status: TicketStatus = TicketStatus.OPEN
tags: List[str] = Field(default_factory=list)
assigned_to: Optional[str] = None
conversation: List[Message] = Field(default_factory=list)
customer: CustomerProfile = Field(...)
created_at: str = Field(...)
updated_at: str = Field(...)
sla_deadline: str = Field(...)
resolution_notes: Optional[str] = None
escalation_reason: Optional[str] = None
turn_count: int = Field(default=0, description="Number of agent actions taken")
max_turns: int = Field(default=10, description="Maximum allowed turns before episode ends")
task_id: str = Field(default="task1")
task_metadata: Dict[str, Any] = Field(default_factory=dict)
# ─── OpenEnv Core Models ───────────────────────────────────────────────────────
class Observation(BaseModel):
"""
OpenEnv Observation β€” what the agent sees each step.
Contains all information needed to decide the next action.
"""
# Ticket basics
ticket_id: str = Field(..., description="Unique ticket identifier")
subject: str = Field(..., description="Ticket subject line")
body: str = Field(..., description="Original customer message")
status: TicketStatus = Field(..., description="Current ticket status")
category: Optional[TicketCategory] = Field(None, description="Assigned category (None if unclassified)")
priority: Optional[TicketPriority] = Field(None, description="Assigned priority (None if unset)")
tags: List[str] = Field(default_factory=list, description="Applied tags")
# Customer context
customer: CustomerProfile = Field(..., description="Customer profile info")
# Conversation history
conversation: List[Message] = Field(default_factory=list, description="Full message thread")
# Episode state
turn_count: int = Field(..., description="Actions taken so far this episode")
turns_remaining: int = Field(..., description="Actions left before forced termination")
time_to_sla: str = Field(..., description="Human-readable time remaining on SLA")
# Task framing
task_id: str = Field(..., description="Current task identifier")
task_description: str = Field(..., description="What the agent must accomplish")
task_objectives: List[str] = Field(default_factory=list, description="Specific success criteria")
# Available actions
available_actions: List[str] = Field(..., description="Valid ActionTypes for current state")
# Feedback from last action
last_action_result: Optional[str] = Field(None, description="Outcome message from previous step")
last_reward: float = Field(default=0.0, description="Reward earned on last step")
# Episode meta
episode_done: bool = Field(default=False, description="Whether the episode has ended")
info: Dict[str, Any] = Field(default_factory=dict, description="Extra diagnostic info")
class Action(BaseModel):
"""
OpenEnv Action β€” what the agent can do each step.
Not all fields are required; depends on action_type.
"""
action_type: ActionType = Field(..., description="The type of action to perform")
# For CLASSIFY
category: Optional[TicketCategory] = Field(None, description="Category to assign (for CLASSIFY)")
priority: Optional[TicketPriority] = Field(None, description="Priority to assign (for CLASSIFY)")
# For RESPOND / REQUEST_INFO
response_text: Optional[str] = Field(None, description="Message to send to customer (for RESPOND/REQUEST_INFO)")
# For ESCALATE
escalation_reason: Optional[str] = Field(None, description="Reason for escalation (for ESCALATE)")
escalation_team: Optional[str] = Field(None, description="Team to escalate to: 'tier2' | 'billing_specialist' | 'engineering'")
# For RESOLVE
resolution_notes: Optional[str] = Field(None, description="Resolution summary (for RESOLVE)")
resolution_category: Optional[str] = Field(None, description="How was it resolved: 'fixed' | 'refunded' | 'explained' | 'workaround'")
# For TAG
tags: Optional[List[str]] = Field(None, description="Tags to add (for TAG)")
# For ASSIGN
assigned_to: Optional[str] = Field(None, description="Department/agent to assign (for ASSIGN)")
class RewardBreakdown(BaseModel):
"""Detailed breakdown of reward components."""
classification_accuracy: float = Field(default=0.0, ge=0.0, le=1.0)
response_quality: float = Field(default=0.0, ge=0.0, le=1.0)
resolution_completeness: float = Field(default=0.0, ge=0.0, le=1.0)
sla_compliance: float = Field(default=0.0, ge=0.0, le=1.0)
efficiency: float = Field(default=0.0, ge=0.0, le=1.0)
customer_sentiment_improvement: float = Field(default=0.0, ge=0.0, le=1.0)
escalation_appropriateness: float = Field(default=0.0, ge=0.0, le=1.0)
step_penalty: float = Field(default=0.0, ge=-1.0, le=0.0)
class Reward(BaseModel):
"""
OpenEnv Reward β€” returned with each step.
Provides fine-grained partial credit signals.
"""
score: float = Field(..., ge=0.0, le=1.0, description="Overall step reward (0.0–1.0)")
cumulative_score: float = Field(default=0.0, ge=0.0, le=1.0, description="Accumulated reward this episode")
done: bool = Field(..., description="Whether this action ended the episode")
breakdown: RewardBreakdown = Field(default_factory=RewardBreakdown, description="Per-component reward details")
feedback: str = Field(default="", description="Human-readable reward explanation")
task_complete: bool = Field(default=False, description="Whether the task objective was fully met")