hoony commited on
Commit
143b299
·
verified ·
1 Parent(s): 2598524

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local OpenEnv sources)
10
+ # - Standalone environments (with openenv from PyPI/Git)
11
+ # The build script (openenv build) handles context detection and sets appropriate build args.
12
+
13
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
14
+ FROM ${BASE_IMAGE} AS builder
15
+
16
+ WORKDIR /app
17
+
18
+ # Ensure git is available (required for installing dependencies from VCS)
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git && \
21
+ rm -rf /var/lib/apt/lists/*
22
+
23
+ # Build argument to control whether we're building standalone or in-repo
24
+ ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=executive_inbox
26
+
27
+ # Copy environment code (always at root of build context)
28
+ COPY . /app/env
29
+
30
+ # For in-repo builds, openenv is already vendored in the build context
31
+ # For standalone builds, openenv will be installed via pyproject.toml
32
+ WORKDIR /app/env
33
+
34
+ # Ensure uv is available (for local builds where base image lacks it)
35
+ RUN if ! command -v uv >/dev/null 2>&1; then \
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
37
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
38
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
39
+ fi
40
+
41
+ # Install dependencies using uv sync
42
+ # If uv.lock exists, use it; otherwise resolve on the fly
43
+ RUN --mount=type=cache,target=/root/.cache/uv \
44
+ if [ -f uv.lock ]; then \
45
+ uv sync --frozen --no-install-project --no-editable; \
46
+ else \
47
+ uv sync --no-install-project --no-editable; \
48
+ fi
49
+
50
+ RUN --mount=type=cache,target=/root/.cache/uv \
51
+ if [ -f uv.lock ]; then \
52
+ uv sync --frozen --no-editable; \
53
+ else \
54
+ uv sync --no-editable; \
55
+ fi
56
+
57
+ # Final runtime stage
58
+ FROM ${BASE_IMAGE}
59
+
60
+ WORKDIR /app
61
+
62
+ # Copy the virtual environment from builder
63
+ COPY --from=builder /app/env/.venv /app/.venv
64
+
65
+ # Copy the environment code
66
+ COPY --from=builder /app/env /app/env
67
+
68
+ # Set PATH to use the virtual environment
69
+ ENV PATH="/app/.venv/bin:$PATH"
70
+
71
+ # Set PYTHONPATH so imports work correctly
72
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
+
74
+ # Health check
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD curl -f http://localhost:8000/health || exit 1
77
+
78
+ # Run the FastAPI server
79
+ # The module path is constructed to work with the /app/env structure
80
+ ENV ENABLE_WEB_INTERFACE=true
81
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
README.md CHANGED
@@ -1,11 +1,69 @@
1
  ---
2
- title: Executive Inbox
3
- emoji: 🚀
4
- colorFrom: gray
5
- colorTo: blue
6
  sdk: docker
7
  pinned: false
8
- license: mit
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Executive Inbox Environment (RL)
3
+ emoji: ⏱️
4
+ colorFrom: pink
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # Executive Inbox Environment Native Architecture
15
+
16
+ A robust text-based OpenEnv environment built on top of the native PyTorch `openenv.core.env_server.Environment` class architecture. This environment simulates a stressful day in the life of an Executive Assistant or busy executive, forcing the `Action` space to parse emails, check calendars, reschedule personal conflicts around work emergencies, and reply via thread IDs.
17
+
18
+ Developed strictly to align with OpenEnv Statement 3.2 (Personalized Tasks & Email/Calendar Parsing).
19
+
20
+ ## Quick Start
21
+
22
+ You can test a programmatic AI policy (looping over actions to find the solution) by running the evaluator test:
23
+
24
+ ```bash
25
+ uv run python examples/heuristic_policy.py
26
+ ```
27
+ This loop runs **10 distinct episodes** against our generative procedural combinatorics.
28
+
29
+ You can also run the deterministic single-step example:
30
+ ```bash
31
+ uv run python examples/executive_inbox_example.py
32
+ ```
33
+
34
+ ## Features for Deep Reinforcement Learning
35
+
36
+ The environment was ripped off the `FastMCP` architecture and rebuilt precisely to support Reinforcement Learning evaluations:
37
+
38
+ 1. **Native Observations & Validations**:
39
+ - We utilize `ExecutiveInboxAction(action_type=...)` and our `step()` function returns a structured `ExecutiveInboxObservation(output: Any, error: str, done: bool, reward: float)`.
40
+ 2. **Procedural Complexity (No Memorizing)**:
41
+ - Built on `data_pools.py`, we randomly sample combinations of "Crisis Emails", "Personal Conflicts", and "Noise Events" and drop them randomly into a 9-to-5 calendar layout every single episode.
42
+ - All email IDs (e.g. `e_1x9w`) and meeting IDs (`c_00k4`) are randomly generated on `reset()`.
43
+ 3. **Double Overlaps**:
44
+ - The environment purposefully seeds **two separate conflicts** occurring simultaneously at distinct times.
45
+ - For example: *A VIP crisis at 10:00 AM overlaps a flight, and a server outage at 3:00 PM overlaps a doctor's appointment.*
46
+ 4. **Rich Constraints**:
47
+ - `move_meeting` enforces that the new meeting block cannot be placed on an occupied time block (like 2:00 PM), failing gracefully and penalizing the agent.
48
+ 5. **Delegations Tools**:
49
+ - Includes full implementations of `reply_to_email` and `delegate_meeting` to fulfill the "Personalized Constraints" and "Delegation" feedback block from evaluators.
50
+
51
+ ## Reward & Shaping Mechanics
52
+
53
+ - Max Step count is set to **15 steps**. Reaching the limit terminates the episode with `Done=True` and `Reward=-1.0`.
54
+ - Every valid tool call returns a `-0.01` negative penalty to enforce efficient operations (the heuristic optimal solver takes 6 steps to win).
55
+ - Every **Invalid Action** (missing parameters, or hallucinating an `email_id` that does not exist in the current random layout) throws a `-0.1` invalid step penalty and increments the verification `state.invalid_actions_taken`.
56
+ - Resolving **Both Conflicts** and sending **Both Confirmatory Emails** terminates the episode with `Done=True` and awards `+1.0`.
57
+
58
+ ## Architecture
59
+
60
+ ```
61
+ executive_inbox/
62
+ ├── models.py # Pydantic schemas (ExecutiveInboxAction/Observation)
63
+ ├── client.py # Model dump overrides for local testing
64
+ ├── server/
65
+ │ ├── executive_inbox_environment.py # Native environment loop + validations
66
+ │ ├── data_pools.py # Combinatorial random pools of scenarios
67
+ │ └── app.py # OpenEnv FastAPI app
68
+ └── README.md
69
+ ```
__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Executive Inbox Environment."""
8
+
9
+ from .models import ExecutiveInboxAction, ExecutiveInboxObservation
10
+
11
+ # Allow server-side imports to work even when optional MCP/client deps are missing.
12
+ try:
13
+ from .client import ExecutiveInboxEnv
14
+ except ModuleNotFoundError:
15
+ ExecutiveInboxEnv = None # type: ignore[assignment]
16
+
17
+ __all__ = [
18
+ "ExecutiveInboxAction",
19
+ "ExecutiveInboxObservation",
20
+ ]
21
+
22
+ if ExecutiveInboxEnv is not None:
23
+ __all__.append("ExecutiveInboxEnv")
client.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Executive Inbox Environment Client."""
8
+
9
+ from typing import Dict
10
+
11
+ from openenv.core import EnvClient
12
+ from openenv.core.client_types import StepResult
13
+
14
+ from .models import ExecutiveInboxAction, ExecutiveInboxObservation, ExecutiveInboxState
15
+
16
+
17
+ class ExecutiveInboxEnv(
18
+ EnvClient[ExecutiveInboxAction, ExecutiveInboxObservation, ExecutiveInboxState]
19
+ ):
20
+ """
21
+ Client for the Executive Inbox Environment.
22
+
23
+ This client maintains a persistent WebSocket connection to the environment server,
24
+ enabling efficient multi-step interactions with lower latency.
25
+ Each client instance has its own dedicated environment session on the server.
26
+
27
+ Example:
28
+ >>> # Connect to a running server
29
+ >>> with ExecutiveInboxEnv(base_url="http://localhost:8000") as client:
30
+ ... result = client.reset()
31
+ ... print(result.observation.output)
32
+ ...
33
+ ... result = client.step(ExecutiveInboxAction(action_type="get_calendar"))
34
+ ... print(result.observation.output)
35
+
36
+ Example with Docker:
37
+ >>> # Automatically start container and connect
38
+ >>> client = ExecutiveInboxEnv.from_docker_image("executive_inbox-env:latest")
39
+ >>> try:
40
+ ... result = client.reset()
41
+ ... result = client.step(ExecutiveInboxAction(action_type="get_calendar"))
42
+ ... finally:
43
+ ... client.close()
44
+ """
45
+
46
+ def _step_payload(self, action: ExecutiveInboxAction) -> Dict:
47
+ """
48
+ Convert ExecutiveInboxAction to JSON payload for step message.
49
+ """
50
+ return action.model_dump(exclude_none=True)
51
+
52
+ def _parse_result(self, payload: Dict) -> StepResult[ExecutiveInboxObservation]:
53
+ """
54
+ Parse server response into StepResult[ExecutiveInboxObservation].
55
+ """
56
+ obs_data = payload.get("observation", {})
57
+
58
+ observation = ExecutiveInboxObservation(
59
+ output=obs_data.get("output", None),
60
+ error=obs_data.get("error", None),
61
+ done=obs_data.get("done", payload.get("done", False)),
62
+ reward=obs_data.get("reward", payload.get("reward", 0.0)),
63
+ )
64
+
65
+ return StepResult(
66
+ observation=observation,
67
+ reward=payload.get("reward", 0.0),
68
+ done=payload.get("done", False),
69
+ )
70
+
71
+ def _parse_state(self, payload: Dict) -> ExecutiveInboxState:
72
+ """
73
+ Parse server response into ExecutiveInboxState object.
74
+ """
75
+ return ExecutiveInboxState(
76
+ episode_id=payload.get("episode_id"),
77
+ step_count=payload.get("step_count", 0),
78
+ conflict_resolved=payload.get("conflict_resolved", False),
79
+ emails_sent=payload.get("emails_sent", 0),
80
+ invalid_actions_taken=payload.get("invalid_actions_taken", 0),
81
+ is_timeout=payload.get("is_timeout", False)
82
+ )
models.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Data models for the Executive Inbox Environment.
9
+ """
10
+
11
+ from typing import Any, Optional
12
+ from pydantic import BaseModel, Field
13
+
14
+ # We use openenv.core.env_server.types for State based on the previous working file
15
+ from openenv.core.env_server.types import State
16
+
17
+ class ExecutiveInboxAction(BaseModel):
18
+ """Native action schema for the Executive Inbox Environment."""
19
+ action_type: str = Field(..., description="Action to perform (e.g., 'read_inbox', 'move_meeting', 'delegate_meeting')")
20
+
21
+ # Optional arguments depending on the tool chosen
22
+ email_id: Optional[str] = None
23
+ to: Optional[str] = None
24
+ subject: Optional[str] = None
25
+ body: Optional[str] = None
26
+ meeting_id: Optional[str] = None
27
+ new_time: Optional[str] = None
28
+ delegate_email: Optional[str] = None
29
+
30
+ class ExecutiveInboxObservation(BaseModel):
31
+ """Native observation schema passed back to the RL policy."""
32
+ output: Any = Field(description="The functional output string or dict array from the environment tool.")
33
+ error: Optional[str] = Field(default=None, description="Detailed error message if the action failed.")
34
+ done: bool = Field(default=False, description="Whether the episode has terminated.")
35
+ reward: float = Field(default=0.0, description="The reward accumulated on this step.")
36
+
37
+ class ExecutiveInboxState(State):
38
+ """Episode state metadata for the Executive Inbox environment."""
39
+ conflict_resolved: bool = Field(default=False, description="Whether the schedule conflict is resolved")
40
+ partial_conflicts_resolved: int = Field(default=0, description="Number of individual conflict pairs resolved")
41
+ emails_sent: int = Field(default=0, description="Number of emails sent")
42
+ crisis_emails_opened: int = Field(default=0, description="Number of true crisis emails opened")
43
+ correct_meeting_moves: int = Field(default=0, description="Number of correct crisis meetings moved or delegated")
44
+ correct_replies: int = Field(default=0, description="Number of correct crisis-thread replies with resolution language")
45
+ invalid_actions_taken: int = Field(default=0, description="Counter for hallucinated or invalid tool calls")
46
+ is_timeout: bool = Field(default=False, description="Whether the episode ended due to hitting the max step limit")
openenv.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: executive_inbox
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
7
+
pyproject.toml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=45", "wheel"]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "openenv-executive_inbox"
13
+ version = "0.1.0"
14
+ description = "Executive Inbox environment for OpenEnv"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv runtime (provides FastAPI server + HTTP client types)
18
+ # install from github
19
+ # "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
20
+ "openenv-core[core]==0.2.1",
21
+ # Environment-specific dependencies
22
+ # Add all dependencies needed for your environment here
23
+ # Examples:
24
+ # "numpy>=1.19.0",
25
+ # "torch>=2.0.0",
26
+ # "gymnasium>=0.29.0",
27
+ # "openspiel>=1.0.0",
28
+ # "smolagents>=1.22.0,<2",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=8.0.0",
34
+ "pytest-cov>=4.0.0",
35
+ ]
36
+
37
+ [project.scripts]
38
+ # Server entry point - enables running via: uv run --project . server
39
+ # or: python -m executive_inbox.server.app
40
+ server = "executive_inbox.server.app:main"
41
+
42
+ [tool.setuptools]
43
+ include-package-data = true
44
+ packages = ["executive_inbox", "executive_inbox.server"]
45
+ package-dir = { "executive_inbox" = ".", "executive_inbox.server" = "server" }
server/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Executive Inbox environment server components."""
8
+
9
+ from .executive_inbox_environment import ExecutiveInboxEnvironment
10
+
11
+ __all__ = ["ExecutiveInboxEnvironment"]
server/app.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ FastAPI application for the Executive Inbox Environment.
9
+
10
+ This module creates an HTTP server that exposes the ExecutiveInboxEnvironment
11
+ over HTTP and WebSocket endpoints, compatible with EnvClient.
12
+
13
+ Endpoints:
14
+ - POST /reset: Reset the environment
15
+ - POST /step: Execute an action
16
+ - GET /state: Get current environment state
17
+ - GET /schema: Get action/observation schemas
18
+ - WS /ws: WebSocket endpoint for persistent sessions
19
+
20
+ Usage:
21
+ # Development (with auto-reload):
22
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
+
24
+ # Production:
25
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
+
27
+ # Or run directly:
28
+ python -m server.app
29
+ """
30
+
31
+ try:
32
+ from openenv.core.env_server.http_server import create_app
33
+ except Exception as e: # pragma: no cover
34
+ raise ImportError(
35
+ "openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
36
+ ) from e
37
+
38
+ # Import from local models.py (PYTHONPATH includes /app/env in Docker)
39
+ from models import ExecutiveInboxAction, ExecutiveInboxObservation
40
+
41
+ from .executive_inbox_environment import ExecutiveInboxEnvironment
42
+
43
+
44
+ # Create the app with web interface and README integration
45
+ app = create_app(
46
+ ExecutiveInboxEnvironment,
47
+ ExecutiveInboxAction,
48
+ ExecutiveInboxObservation,
49
+ env_name="executive_inbox",
50
+ max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
51
+ )
52
+
53
+
54
+ def main():
55
+ """
56
+ Entry point for direct execution via uv run or python -m.
57
+
58
+ This function enables running the server without Docker:
59
+ uv run --project . server
60
+ uv run --project . server --port 8001
61
+ python -m executive_inbox.server.app
62
+
63
+ Args:
64
+ For production deployments, consider using uvicorn directly with
65
+ multiple workers:
66
+ uvicorn executive_inbox.server.app:app --workers 4
67
+ """
68
+ import argparse
69
+ import uvicorn
70
+
71
+ parser = argparse.ArgumentParser()
72
+ parser.add_argument("--host", default="0.0.0.0")
73
+ parser.add_argument("--port", type=int, default=8000)
74
+ args = parser.parse_args()
75
+ uvicorn.run(app, host=args.host, port=args.port)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
server/data_pools.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Combinatorial Data Pools for the Executive Inbox Environment.
3
+ Provides modular templates to exponentially scale the scenario permutations during RL training.
4
+ """
5
+
6
+ # ==============================================================================
7
+ # 1. CRISIS SCENARIOS (Work Overlaps)
8
+ # ==============================================================================
9
+
10
+ CRISIS_TYPES = [
11
+ {"type": "Client Attrition", "calendar_title": "VIP Client Strategy Meeting", "owner_prefix": ["boss", "vp.sales", "head.revenue"]},
12
+ {"type": "Server Outage", "calendar_title": "Prod Outage War Room", "owner_prefix": ["cto", "vp.eng", "head.infra"]},
13
+ {"type": "PR Disaster", "calendar_title": "Comms & PR Review", "owner_prefix": ["head.of.pr", "cmo", "vp.comms"]},
14
+ {"type": "Legal Threat", "calendar_title": "Legal Review (Feature Launch)", "owner_prefix": ["general.counsel", "head.legal", "vp.legal"]}
15
+ ]
16
+
17
+ CRISIS_SUBJECTS = [
18
+ "Important sync at {time}",
19
+ "Required attendance: meeting at {time}",
20
+ "Need everyone in the war room at {time}",
21
+ "Strategy discussion - {time}",
22
+ "Please join the {time} call",
23
+ "Emergency Huddle - {time}",
24
+ "All-hands crisis response ({time})",
25
+ "Drop everything: sync at {time}",
26
+ "Immediate attention required ({time})",
27
+ "Briefing session at {time} sharp"
28
+ ]
29
+
30
+ # Note: We omit explicit cheating keywords like "URGENT" or "EMERGENCY" where possible,
31
+ # forcing the agent to rely on semantic context.
32
+ CRISIS_BODIES = [
33
+ "We have a massive situation unfolding. I'm pulling the whole team into a room at {time}. Clear your calendar.",
34
+ "The executive team is furious about the latest developments. We need a resolution plan drawn up by {time}. Be there.",
35
+ "This is arguably the worst time for this to happen. I am assembling a response team at {time}. Do not miss this.",
36
+ "I just got off the phone with the board. We need to do damage control immediately. Syncing at {time}.",
37
+ "We are bleeding revenue by the minute. Everyone needs to be hands-on-keyboard in the conference room at {time}.",
38
+ "The press is already starting to ask questions. We need to get our story straight. Meet me in my office at {time}.",
39
+ "Our legal exposure here is massive. We are pulling everyone into a mandatory review session at {time}.",
40
+ "The situation has escalated. We need an all-hands-on-deck response starting exactly at {time}."
41
+ ]
42
+
43
+ # ==============================================================================
44
+ # 2. PERSONAL CONFLICTS (Immovable Personal Commitments)
45
+ # ==============================================================================
46
+
47
+ PERSONAL_TYPES = [
48
+ {"type": "Recital", "calendar_title": "Daughter's Piano Recital"},
49
+ {"type": "Flight", "calendar_title": "Flight Options (Do not schedule)"},
50
+ {"type": "Doctor", "calendar_title": "Specialist Consultation"},
51
+ {"type": "Contractor", "calendar_title": "Contractor Arrival Window"},
52
+ {"type": "Dinner", "calendar_title": "Anniversary Dinner"},
53
+ {"type": "Childcare", "calendar_title": "School Pickup (Coordination)"}
54
+ ]
55
+
56
+ PERSONAL_SENDERS = [
57
+ "school@academy.edu", "alerts@delta.com", "noreply@medical-portal.net",
58
+ "dispatch@plumbing.com", "reservations@opentable.com", "nanny@care.com",
59
+ "frontdesk@pediatrics.org", "updates@united.com", "scheduling@hvac-pros.com"
60
+ ]
61
+
62
+ PERSONAL_SUBJECTS = [
63
+ "Reminder: Appointment at {time}",
64
+ "Upcoming schedule: {time}",
65
+ "Confirmation for {time}",
66
+ "Your {time} itinerary",
67
+ "Action Required: Coverage needed at {time}",
68
+ "Arrival window confirmed ({time})",
69
+ "Don't forget: today at {time}",
70
+ "Reservation details - {time}"
71
+ ]
72
+
73
+ PERSONAL_BODIES = [
74
+ "This is a final confirmation that your appointment begins promptly at {time}. Please ensure you arrive 15 minutes early.",
75
+ "We are writing to confirm your slot today at {time}. If you need to reschedule, you will be subject to a strict cancellation fee.",
76
+ "Please remember that you need to be physically present at {time}. We cannot hold your spot if you are late.",
77
+ "Your departure/arrival window is set for {time}. Let us know immediately if this changes, as we have no other availability this week.",
78
+ "Just a quick reminder about the commitment at {time}. It's very important that you are on time today!",
79
+ "I won't be able to handle things at {time} as planned. You will absolutely need to step in and cover this slot.",
80
+ "The schedule is locked in for {time}. See you then!"
81
+ ]
82
+
83
+
84
+ # ==============================================================================
85
+ # 3. NOISE EMAILS (Daily Office Chatter)
86
+ # ==============================================================================
87
+
88
+ NOISE_EMAILS = [
89
+ {"subject": "Weekly Newsletter", "sender": "updates@producthunt.com", "body": "Here are the top 10 products of the week. Number 3 will shock you!"},
90
+ {"subject": "Happy Hour on Friday?", "sender": "sarah.team@company.com", "body": "Hey everyone, are we still doing drinks at O'Malleys this Friday? Need a headcount."},
91
+ {"subject": "Your Amazon.com order has shipped", "sender": "auto-confirm@amazon.com", "body": "Your order for 'Ergonomic Office Chair' has shipped and will arrive tomorrow."},
92
+ {"subject": "Expense Report Not Yet Submitted", "sender": "finance-bot@company.com", "body": "You have 3 unsubmitted expenses from your trip to New York. Please submit by EOD."},
93
+ {"subject": "Lunch in 10?", "sender": "mark.dev@company.com", "body": "Heading down to the cafeteria. Want me to grab you a sandwich?"},
94
+ {"subject": "New login from Mac OS", "sender": "security@it-support.net", "body": "We detected a new login to your account from a Mac device. If this was you, ignore this email."},
95
+ {"subject": "Re: Project Alpha timeline", "sender": "jessica.pm@company.com", "body": "I think we can hit the Q3 deadline if we cut the reporting feature. Thoughts?"},
96
+ {"subject": "Can you review my PR?", "sender": "intern.bob@company.com", "body": "I just pushed the fix for the login bug. PR #4092. Let me know if it looks okay!"},
97
+ {"subject": "Client prep notes for tomorrow", "sender": "vp.sales@company.com", "body": "Not urgent today. I just want your notes before tomorrow's client prep."},
98
+ {"subject": "Draft talking points for next week's review", "sender": "cmo@company.com", "body": "Please send the draft deck when you have a chance. This is for next week, not today's fire drill."},
99
+ {"subject": "Quick legal wording pass", "sender": "head.legal@company.com", "body": "Can you glance at the wording later this afternoon? No immediate action needed."},
100
+ {"subject": "System Maintenance Tonight", "sender": "eng-leads@company.com", "body": "The staging databases will be down for maintenance between 1 AM and 3 AM tonight."},
101
+ {"subject": "Did you leave your jacket in the conference room?", "sender": "office.manager@company.com", "body": "Someone left a blue Patagonia jacket in room 4B."}
102
+ ]
103
+
104
+ NOISE_CALENDAR = [
105
+ {"title": "Focus Time", "participants": []},
106
+ {"title": "1:1 with Jane", "participants": ["jane.manager@company.com"]},
107
+ {"title": "Weekly Team Sync", "participants": ["eng-team@company.com", "design-team@company.com"]},
108
+ {"title": "Product Roadmap Review", "participants": ["execs@company.com", "jessica.pm@company.com"]},
109
+ {"title": "Dentist Follow-up", "participants": []},
110
+ {"title": "Interview: Frontend Engineer", "participants": ["recruiting@company.com", "candidate@gmail.com"]},
111
+ {"title": "Vendor Pitch: CloudFlare", "participants": ["sales@cloudflare.com", "it-leads@company.com"]},
112
+ {"title": "All-Hands Rehearsal", "participants": ["ceo@company.com", "comms@company.com"]},
113
+ {"title": "Executive Check-in", "participants": ["vp.sales@company.com", "chief.of.staff@company.com"]},
114
+ {"title": "Legal Draft Review", "participants": ["head.legal@company.com", "chief.of.staff@company.com"]},
115
+ {"title": "Lunch / Run", "participants": []},
116
+ {"title": "Architecture Brainstorm", "participants": ["mark.dev@company.com", "sarah.team@company.com"]}
117
+ ]
server/executive_inbox_environment.py ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List, Optional
2
+ import random
3
+ import string
4
+
5
+ from openenv.core.env_server import Environment
6
+ from ..models import ExecutiveInboxAction, ExecutiveInboxObservation, ExecutiveInboxState
7
+
8
+ def semi_random_id(prefix: str) -> str:
9
+ """Generate a stable but random-looking ID for scenarios."""
10
+ chars = string.ascii_lowercase + string.digits
11
+ return f"{prefix}_{''.join(random.choices(chars, k=4))}"
12
+
13
+
14
+ class ExecutiveInboxEnvironment(
15
+ Environment[
16
+ ExecutiveInboxAction, ExecutiveInboxObservation, ExecutiveInboxState
17
+ ]
18
+ ):
19
+ """
20
+ Simulates an executive inbox where an AI agent must handle calendar conflicts
21
+ and email threads. Built with procedurally generated state to prevent memorization.
22
+ """
23
+ MAX_STEPS = 15
24
+
25
+ def __init__(self, num_conflicts: int = 2, dense_reward: bool = True):
26
+ super().__init__()
27
+ self._num_conflicts = max(1, min(num_conflicts, 2))
28
+ self._dense_reward = dense_reward
29
+ self._step_count = 0
30
+ self._inbox = []
31
+ self._sent_emails = []
32
+ self._calendar = []
33
+ self._done = False
34
+ self._reward = 0.0
35
+ self._state = ExecutiveInboxState()
36
+ self._reset_environment()
37
+
38
+ @property
39
+ def name(self) -> str:
40
+ return "ExecutiveInboxEnvironment"
41
+
42
+ @property
43
+ def state(self) -> ExecutiveInboxState:
44
+ return self._state
45
+
46
+ def reset(self) -> ExecutiveInboxObservation:
47
+ self._reset_environment()
48
+ return ExecutiveInboxObservation(
49
+ output="Environment initialized.",
50
+ done=self._done,
51
+ reward=self._reward
52
+ )
53
+
54
+ def _reset_environment(self):
55
+ """Internal reset to procedurally generate a fresh scenario."""
56
+ self._step_count = 0
57
+ self._state = ExecutiveInboxState()
58
+ self._init_scenario()
59
+
60
+ def _init_scenario(self):
61
+ """Initialize procedural variables for the scenario."""
62
+ from .data_pools import NOISE_EMAILS, NOISE_CALENDAR, CRISIS_TYPES, CRISIS_SUBJECTS, CRISIS_BODIES, PERSONAL_TYPES, PERSONAL_SENDERS, PERSONAL_SUBJECTS, PERSONAL_BODIES
63
+
64
+ # 1. Base Variables
65
+ times = ["9:00 AM", "10:00 AM", "1:00 PM", "2:00 PM", "3:00 PM", "4:00 PM"]
66
+
67
+ # Pick one or two distinct conflict times depending on the curriculum setting.
68
+ self._conflict_times = random.sample(times, self._num_conflicts)
69
+
70
+ domain = random.choice(["company.com", "corp.net", "enterprise.org"])
71
+ vip_domain = random.choice(["bigcorp.com", "megacorp.com", "globaltech.io"])
72
+
73
+ # 2. Pick the Scenarios
74
+ crises = random.sample(CRISIS_TYPES, self._num_conflicts)
75
+ conflicts = random.sample(PERSONAL_TYPES, self._num_conflicts)
76
+
77
+ # Save validation targets
78
+ self._targets = []
79
+ for i in range(self._num_conflicts):
80
+ self._targets.append({
81
+ "time": self._conflict_times[i],
82
+ "owner_email": f"{random.choice(crises[i]['owner_prefix'])}@{domain}",
83
+ "meeting_id": semi_random_id("c"),
84
+ "crisis_data": crises[i],
85
+ "conflict_data": conflicts[i],
86
+ "delegate_email": "chief.of.staff@company.com",
87
+ "crisis_email_id": None,
88
+ "crisis_subject": None,
89
+ "personal_email_id": None,
90
+ "personal_meeting_id": None,
91
+ "delegated_to": None,
92
+ "opened_crisis_email": False,
93
+ "moved_correct_meeting": False,
94
+ "replied_with_resolution": False,
95
+ "resolved": False
96
+ })
97
+
98
+ # 3. Build Inbox
99
+ self._inbox = []
100
+
101
+ for target in self._targets:
102
+ # Dynamic String Compilation
103
+ c_subj = random.choice(CRISIS_SUBJECTS).format(time=target["time"])
104
+ c_body = random.choice(CRISIS_BODIES).format(time=target["time"], vip_domain=vip_domain)
105
+ c_body += (
106
+ f" If you cannot attend, delegate the meeting to {target['delegate_email']}."
107
+ )
108
+
109
+ p_sender = random.choice(PERSONAL_SENDERS)
110
+ p_subj = random.choice(PERSONAL_SUBJECTS).format(time=target["time"])
111
+ p_body = random.choice(PERSONAL_BODIES).format(time=target["time"])
112
+
113
+ # Schema Drift: 10% chance to lowercase subject
114
+ if random.random() < 0.10: c_subj = c_subj.lower()
115
+ if random.random() < 0.10: p_subj = p_subj.lower()
116
+
117
+ # Crisis Email
118
+ crisis_email = {
119
+ "id": semi_random_id("e"),
120
+ "sender": target["owner_email"],
121
+ "subject": c_subj,
122
+ "date": "Today 08:30 AM",
123
+ "body": c_body
124
+ }
125
+ self._inbox.append(crisis_email)
126
+ target["crisis_email_id"] = crisis_email["id"]
127
+ target["crisis_subject"] = crisis_email["subject"]
128
+
129
+ # Personal Conflict Email
130
+ personal_email = {
131
+ "id": semi_random_id("e"),
132
+ "sender": p_sender,
133
+ "subject": p_subj,
134
+ "date": "Today 08:00 AM",
135
+ "body": p_body
136
+ }
137
+ self._inbox.append(personal_email)
138
+ target["personal_email_id"] = personal_email["id"]
139
+
140
+ # Noise Emails (Pick 3-5 random ones)
141
+ num_noise = random.randint(3, 5)
142
+ for noise in random.sample(NOISE_EMAILS, num_noise):
143
+ self._inbox.append({
144
+ "id": semi_random_id("e"),
145
+ "sender": noise["sender"],
146
+ "subject": noise["subject"],
147
+ "date": f"Today 0{random.randint(6,9)}:00 AM",
148
+ "body": noise["body"]
149
+ })
150
+
151
+ random.shuffle(self._inbox)
152
+ self._sent_emails = []
153
+
154
+ # 4. Build Calendar
155
+ self._calendar = []
156
+
157
+ for target in self._targets:
158
+ # Crisis Event
159
+ self._calendar.append({
160
+ "id": target["meeting_id"],
161
+ "time": target["time"],
162
+ "title": target["crisis_data"]["calendar_title"],
163
+ "participants": [target["delegate_email"], f"vip@{vip_domain}"]
164
+ })
165
+
166
+ # Personal Event
167
+ personal_meeting = {
168
+ "id": semi_random_id("c"),
169
+ "time": target["time"],
170
+ "title": target["conflict_data"]["calendar_title"],
171
+ "participants": [random.choice(PERSONAL_SENDERS)]
172
+ }
173
+ self._calendar.append(personal_meeting)
174
+ target["personal_meeting_id"] = personal_meeting["id"]
175
+
176
+ # Noise Events (Pick 2-3 randomly, assigned to non-conflict times)
177
+ num_noise_events = random.randint(2, 3)
178
+ available_times = [t for t in times if t not in self._conflict_times]
179
+
180
+ if num_noise_events > len(available_times):
181
+ num_noise_events = len(available_times)
182
+
183
+ random_times_for_noise = random.sample(available_times, num_noise_events)
184
+
185
+ for i, noise_cal in enumerate(random.sample(NOISE_CALENDAR, num_noise_events)):
186
+ self._calendar.append({
187
+ "id": semi_random_id("c"),
188
+ "time": random_times_for_noise[i],
189
+ "title": noise_cal["title"],
190
+ "participants": noise_cal["participants"]
191
+ })
192
+
193
+ random.shuffle(self._calendar)
194
+
195
+ self._done = False
196
+ self._reward = 0.0
197
+
198
+ def _add_reward(self, amount: float) -> None:
199
+ if self._dense_reward:
200
+ self._reward += amount
201
+
202
+ def _find_target_for_email(self, email_id: str) -> Optional[Dict[str, Any]]:
203
+ return next((target for target in self._targets if target["crisis_email_id"] == email_id), None)
204
+
205
+ def _find_target_for_meeting(self, meeting_id: str) -> Optional[Dict[str, Any]]:
206
+ return next((target for target in self._targets if target["meeting_id"] == meeting_id), None)
207
+
208
+ def _body_mentions_resolution(self, body: str) -> bool:
209
+ body_text = body.lower()
210
+ keywords = ["moved", "rescheduled", "delegate", "delegated", "updated the schedule"]
211
+ return any(keyword in body_text for keyword in keywords)
212
+
213
+ def _has_resolution_reply(self, target: Dict[str, Any]) -> bool:
214
+ owner_email = str(target["owner_email"]).lower()
215
+ crisis_subject = str(target.get("crisis_subject", "")).lower()
216
+ for email in self._sent_emails:
217
+ to = str(email.get("to", "")).lower()
218
+ subject = str(email.get("subject", "")).lower()
219
+ body = str(email.get("body", ""))
220
+ if to != owner_email:
221
+ continue
222
+ if crisis_subject and crisis_subject not in subject:
223
+ continue
224
+ if self._body_mentions_resolution(body):
225
+ return True
226
+ return False
227
+
228
+ def _validate_task(self):
229
+ """Check if all overlapping conflicts are successfully resolved."""
230
+ if self._done:
231
+ return
232
+
233
+ resolved_count = 0
234
+
235
+ for target in self._targets:
236
+ # Check if the crisis meeting was moved off the conflict time
237
+ crisis_meeting = next((m for m in self._calendar if m["id"] == target["meeting_id"]), None)
238
+
239
+ moved_off_conflict = bool(crisis_meeting and crisis_meeting["time"] != target["time"])
240
+ delegated_correctly = (
241
+ str(target.get("delegated_to", "")).lower()
242
+ == str(target.get("delegate_email", "")).lower()
243
+ )
244
+ sent_resolution = self._has_resolution_reply(target)
245
+
246
+ if (moved_off_conflict or delegated_correctly) and sent_resolution:
247
+ resolved_count += 1
248
+ if not target["resolved"]:
249
+ target["resolved"] = True
250
+ self._state.partial_conflicts_resolved += 1
251
+ self._add_reward(0.30)
252
+
253
+ if resolved_count == len(self._targets):
254
+ self._done = True
255
+ self._state.conflict_resolved = True
256
+ if self._dense_reward:
257
+ self._reward += 0.70
258
+ else:
259
+ self._reward = 1.0
260
+
261
+ def step(self, action: ExecutiveInboxAction) -> ExecutiveInboxObservation:
262
+ """Process an action and return the observation and reward."""
263
+ self._step_count += 1
264
+
265
+ # Penalize for inefficient exploration
266
+ self._reward -= 0.01
267
+
268
+ # Check for Max Steps Limit Timeout
269
+ if self._step_count >= self.MAX_STEPS:
270
+ self._done = True
271
+ self._state.is_timeout = True
272
+ if self._dense_reward:
273
+ self._reward = max(self._reward - 0.50, -1.0)
274
+ else:
275
+ self._reward = -1.0
276
+ return ExecutiveInboxObservation(
277
+ output=None,
278
+ error=f"Timeout reached. Maximum of {self.MAX_STEPS} steps allowed.",
279
+ done=self._done,
280
+ reward=self._reward
281
+ )
282
+
283
+ output = None
284
+ error = None
285
+
286
+ # 1. read_inbox
287
+ if action.action_type == "read_inbox":
288
+ if action.email_id:
289
+ email = next((e for e in self._inbox if e["id"] == action.email_id), None)
290
+ if email:
291
+ output = email
292
+ target = self._find_target_for_email(action.email_id)
293
+ if target is not None and not target["opened_crisis_email"]:
294
+ target["opened_crisis_email"] = True
295
+ self._state.crisis_emails_opened += 1
296
+ self._add_reward(0.10)
297
+ else:
298
+ self._reward -= 0.1
299
+ self._state.invalid_actions_taken += 1
300
+ error = f"Error: Email with ID {action.email_id} not found."
301
+ else:
302
+ # Summarize without bodies
303
+ output = [{"id": e["id"], "sender": e["sender"], "subject": e["subject"]} for e in self._inbox]
304
+
305
+ # 2. send_email
306
+ elif action.action_type == "send_email":
307
+ if not action.to or not action.subject or not action.body:
308
+ self._reward -= 0.1
309
+ self._state.invalid_actions_taken += 1
310
+ error = "Error: Cannot send email without all fields (to, subject, body)."
311
+ else:
312
+ new_id = f"email_{len(self._sent_emails) + 1}"
313
+ self._sent_emails.append({
314
+ "id": new_id, "to": action.to, "subject": action.subject, "body": action.body
315
+ })
316
+ self._state.emails_sent += 1
317
+ self._validate_task()
318
+ output = f"Email sent successfully to {action.to}"
319
+
320
+ # 3. reply_to_email
321
+ elif action.action_type == "reply_to_email":
322
+ if not action.email_id or not action.body:
323
+ self._reward -= 0.1
324
+ self._state.invalid_actions_taken += 1
325
+ error = "Error: Cannot reply without email_id and body."
326
+ else:
327
+ original_email = next((e for e in self._inbox if e["id"] == action.email_id), None)
328
+ if not original_email:
329
+ self._reward -= 0.1
330
+ self._state.invalid_actions_taken += 1
331
+ error = f"Error: Email with ID {action.email_id} not found."
332
+ else:
333
+ to = original_email["sender"]
334
+ subject = f"Re: {original_email['subject']}"
335
+ new_id = f"email_{len(self._sent_emails) + 1}"
336
+ self._sent_emails.append({
337
+ "id": new_id, "to": to, "subject": subject, "body": action.body
338
+ })
339
+ self._state.emails_sent += 1
340
+ target = self._find_target_for_email(action.email_id)
341
+ if (
342
+ target is not None
343
+ and self._body_mentions_resolution(action.body)
344
+ and not target["replied_with_resolution"]
345
+ ):
346
+ target["replied_with_resolution"] = True
347
+ self._state.correct_replies += 1
348
+ self._add_reward(0.15)
349
+ self._validate_task()
350
+ output = f"Replied to {to} successfully."
351
+
352
+ # 4. get_calendar
353
+ elif action.action_type == "get_calendar":
354
+ output = self._calendar
355
+
356
+ # 5. move_meeting
357
+ elif action.action_type == "move_meeting":
358
+ if not action.meeting_id or not action.new_time:
359
+ self._reward -= 0.1
360
+ self._state.invalid_actions_taken += 1
361
+ error = "Error: Cannot move meeting without providing meeting_id and new_time."
362
+ else:
363
+ # Constraint Check
364
+ occupied = next((m for m in self._calendar if m["time"] == action.new_time and m["id"] != action.meeting_id), None)
365
+ if occupied:
366
+ self._reward -= 0.1
367
+ self._state.invalid_actions_taken += 1
368
+ error = f"Error: Cannot move meeting to {action.new_time}. That slot is already occupied by '{occupied['title']}'."
369
+ else:
370
+ meeting_found = False
371
+ for meeting in self._calendar:
372
+ if meeting["id"] == action.meeting_id:
373
+ old_time = meeting["time"]
374
+ meeting["time"] = action.new_time
375
+ meeting_found = True
376
+ target = self._find_target_for_meeting(action.meeting_id)
377
+ if (
378
+ target is not None
379
+ and action.new_time != target["time"]
380
+ and not target["moved_correct_meeting"]
381
+ ):
382
+ target["moved_correct_meeting"] = True
383
+ self._state.correct_meeting_moves += 1
384
+ self._add_reward(0.20)
385
+ self._validate_task()
386
+ output = f"Meeting '{meeting['title']}' moved from {old_time} to {action.new_time}."
387
+ break
388
+ if not meeting_found:
389
+ self._reward -= 0.1
390
+ self._state.invalid_actions_taken += 1
391
+ error = f"Error: Meeting with ID {action.meeting_id} not found."
392
+
393
+ # 6. delegate_meeting
394
+ elif action.action_type == "delegate_meeting":
395
+ if not action.meeting_id or not action.delegate_email:
396
+ self._reward -= 0.1
397
+ self._state.invalid_actions_taken += 1
398
+ error = "Error: Cannot delegate meeting without meeting_id and delegate_email."
399
+ else:
400
+ target = next((t for t in self._targets if t["meeting_id"] == action.meeting_id), None)
401
+ if target is None:
402
+ self._reward -= 0.1
403
+ self._state.invalid_actions_taken += 1
404
+ error = "Error: Only crisis meetings can be delegated."
405
+ elif action.delegate_email.lower() != target["delegate_email"].lower():
406
+ self._reward -= 0.1
407
+ self._state.invalid_actions_taken += 1
408
+ error = (
409
+ "Error: Invalid delegate. Route crisis meetings to "
410
+ f"{target['delegate_email']}."
411
+ )
412
+ else:
413
+ meeting_found = False
414
+ for i, meeting in enumerate(self._calendar):
415
+ if meeting["id"] == action.meeting_id:
416
+ title = meeting["title"]
417
+ self._calendar.pop(i)
418
+ target["delegated_to"] = action.delegate_email
419
+ if not target["moved_correct_meeting"]:
420
+ target["moved_correct_meeting"] = True
421
+ self._state.correct_meeting_moves += 1
422
+ self._add_reward(0.20)
423
+ meeting_found = True
424
+ self._validate_task()
425
+ output = f"Meeting '{title}' (ID {action.meeting_id}) successfully delegated to {action.delegate_email}."
426
+ break
427
+ if not meeting_found:
428
+ self._reward -= 0.1
429
+ self._state.invalid_actions_taken += 1
430
+ error = f"Error: Meeting with ID {action.meeting_id} not found on calendar."
431
+
432
+ else:
433
+ self._reward -= 0.1
434
+ self._state.invalid_actions_taken += 1
435
+ error = f"Error: Unknown action type '{action.action_type}'"
436
+
437
+ return ExecutiveInboxObservation(
438
+ output=output,
439
+ error=error,
440
+ done=self._done,
441
+ reward=self._reward
442
+ )
server/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ openenv-core[core]==0.2.1
2
+ fastapi>=0.115.0
3
+ uvicorn>=0.24.0
4
+
5
+
6
+
uv.lock ADDED
The diff for this file is too large to render. See raw diff