Spaces:
Sleeping
Sleeping
File size: 8,139 Bytes
1bb18a0 | 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 | """
Core environment: ExecOpsEnv
Manages the CEO simulation state machine.
"""
from typing import Optional
from .models import Action, Email, CalendarEvent, Goal, EnvState, StepResult
from .tasks import get_task
from .reward import compute_reward
# 30-minute time slots starting at 9:00 AM
TIME_SLOTS = [
"9:00 AM", "9:30 AM", "10:00 AM", "10:30 AM",
"11:00 AM", "11:30 AM", "12:00 PM", "12:30 PM",
"1:00 PM", "1:30 PM", "2:00 PM", "2:30 PM",
"3:00 PM", "3:30 PM", "4:00 PM", "4:30 PM",
"5:00 PM",
]
URGENCY_DECAY = 0.15 # Urgency drops by this amount each step
class ExecOpsEnv:
def __init__(self, task_id: str = "easy"):
self.task_id = task_id
self._task_data = get_task(task_id)
self._state: Optional[EnvState] = None
self.reset()
def reset(self) -> dict:
"""Reinitialize state from task definition. Returns initial observation."""
task = get_task(self.task_id)
emails = [Email(**e) for e in task["emails"]]
calendar = [CalendarEvent(**c) for c in task["calendar"]]
goals = [Goal(**g) for g in task["goals"]]
self._state = EnvState(
current_step=0,
max_steps=task["max_steps"],
current_time=TIME_SLOTS[task.get("start_time_slot", 0)],
inbox=emails,
calendar=calendar,
goals=goals,
history=[],
total_reward=0.0,
done=False,
)
return self._build_observation()
def step(self, action: Action) -> dict:
"""
Execute one step of the environment.
Order: validate → apply → update state → advance time → decay urgency
→ compute reward → check done → return StepResult
"""
if self._state is None:
raise RuntimeError("Environment not initialized. Call reset() first.")
if self._state.done:
raise RuntimeError("Environment is done. Call reset() to restart.")
# 1. Validate action
valid_action_types = {"reply", "schedule", "delegate", "ignore"}
if action.type not in valid_action_types:
raise ValueError(
f"Invalid action type '{action.type}'. Must be one of: {sorted(valid_action_types)}"
)
if not action.email_id:
raise ValueError("Action is missing required field 'email_id'.")
email = self._find_email(action.email_id)
if email is None:
raise ValueError(f"Email '{action.email_id}' not found in inbox.")
if email.handled:
raise ValueError(f"Email '{action.email_id}' is already handled.")
# 2. Apply action
email.handled = True
email.action_taken = action.type
# Update calendar if scheduling
if action.type == "schedule":
self._add_to_calendar(email)
# 3. Record action in history
step_entry = {
"step": self._state.current_step,
"time": self._state.current_time,
"action": action.type,
"email_id": action.email_id,
"email_subject": email.subject,
"email_priority": email.priority,
}
# 6. Compute reward BEFORE marking goals complete (so reward fn sees pending goals)
reward = compute_reward(action, email, self._state.goals, self._state)
# Now mark goals completed
for goal in self._state.goals:
if goal.target_email_id == action.email_id and not goal.completed:
if goal.required_action == action.type:
goal.completed = True
step_entry["reward"] = reward
self._state.total_reward += reward
self._state.history.append(step_entry)
# 4. Advance time
self._advance_time()
# 5. Decay urgency on unhandled emails
self._decay_urgency()
# 7. Check termination
self._state.current_step += 1
self._check_done()
obs = self._build_observation()
return {
"observation": obs,
"reward": reward,
"done": self._state.done,
"info": {
"step": self._state.current_step,
"total_reward": self._state.total_reward,
"action_applied": action.type,
"email_subject": email.subject,
},
}
def get_state(self) -> dict:
"""Return full serialized state."""
if self._state is None:
raise RuntimeError("Environment not initialized.")
return {
"current_step": self._state.current_step,
"max_steps": self._state.max_steps,
"current_time": self._state.current_time,
"inbox": [e.model_dump() for e in self._state.inbox],
"calendar": [c.model_dump() for c in self._state.calendar],
"goals": [g.model_dump() for g in self._state.goals],
"history": self._state.history,
"total_reward": self._state.total_reward,
"done": self._state.done,
"task_id": self.task_id,
"task_name": self._task_data.get("task_name", ""),
"task_description": self._task_data.get("description", ""),
}
# -------------------------
# Private helpers
# -------------------------
def _build_observation(self) -> dict:
"""Build the observation dict that the agent sees."""
unhandled = [e for e in self._state.inbox if not e.handled]
pending_goals = [g for g in self._state.goals if not g.completed]
return {
"time": self._state.current_time,
"step": self._state.current_step,
"max_steps": self._state.max_steps,
"steps_remaining": self._state.max_steps - self._state.current_step,
"inbox": [e.model_dump() for e in self._state.inbox],
"unhandled_count": len(unhandled),
"calendar": [c.model_dump() for c in self._state.calendar],
"goals": [g.model_dump() for g in self._state.goals],
"pending_goals": [g.model_dump() for g in pending_goals],
"total_reward": self._state.total_reward,
"done": self._state.done,
"task_id": self.task_id,
"task_name": self._task_data.get("task_name", ""),
}
def _find_email(self, email_id: str) -> Optional[Email]:
for email in self._state.inbox:
if email.id == email_id:
return email
return None
def _advance_time(self):
"""Move to the next 30-minute time slot."""
try:
idx = TIME_SLOTS.index(self._state.current_time)
if idx + 1 < len(TIME_SLOTS):
self._state.current_time = TIME_SLOTS[idx + 1]
except ValueError:
pass
def _decay_urgency(self):
"""Reduce urgency of all unhandled emails."""
for email in self._state.inbox:
if not email.handled:
email.urgency = max(0.0, email.urgency - URGENCY_DECAY)
def _check_done(self):
"""Mark done if max steps reached or all goals completed."""
all_goals_done = all(g.completed for g in self._state.goals)
if self._state.current_step >= self._state.max_steps or all_goals_done:
self._state.done = True
def _add_to_calendar(self, email: Email):
"""Add a scheduling event to the calendar for scheduled emails."""
# Find next available slot
locked_slots = {c.time_slot for c in self._state.calendar if c.locked}
used_slots = {c.time_slot for c in self._state.calendar}
for slot in range(len(TIME_SLOTS)):
if slot not in used_slots and slot not in locked_slots:
self._state.calendar.append(
CalendarEvent(
time_slot=slot,
title=f"Scheduled: {email.subject[:40]}",
attendee=email.sender.split("<")[0].strip(),
locked=False,
)
)
break
|