morty649 commited on
Commit
d02cfdb
·
0 Parent(s):

SmartCalendarResolver

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .agents/skills/openenv-cli/.gitignore +1 -0
  2. .agents/skills/openenv-cli/SKILL.md +18 -0
  3. .codex/skills/openenv-cli +1 -0
  4. .dockerignore +5 -0
  5. .python-version +1 -0
  6. Dockerfile +28 -0
  7. README.md +0 -0
  8. calender_en/.dockerignore +5 -0
  9. calender_en/Dockerfile +50 -0
  10. calender_en/README.md +106 -0
  11. calender_en/__init__.py +16 -0
  12. calender_en/__pycache__/__init__.cpython-314.pyc +0 -0
  13. calender_en/__pycache__/client.cpython-314.pyc +0 -0
  14. calender_en/__pycache__/inference.cpython-314.pyc +0 -0
  15. calender_en/__pycache__/models.cpython-314.pyc +0 -0
  16. calender_en/client.py +109 -0
  17. calender_en/inference.py +91 -0
  18. calender_en/models.py +59 -0
  19. calender_en/openenv.yaml +7 -0
  20. calender_en/openenv_calender_en.egg-info/PKG-INFO +9 -0
  21. calender_en/openenv_calender_en.egg-info/SOURCES.txt +15 -0
  22. calender_en/openenv_calender_en.egg-info/dependency_links.txt +1 -0
  23. calender_en/openenv_calender_en.egg-info/entry_points.txt +2 -0
  24. calender_en/openenv_calender_en.egg-info/requires.txt +5 -0
  25. calender_en/openenv_calender_en.egg-info/top_level.txt +1 -0
  26. calender_en/pyproject.toml +45 -0
  27. calender_en/server/Dockerfile +80 -0
  28. calender_en/server/__init__.py +11 -0
  29. calender_en/server/__pycache__/__init__.cpython-314.pyc +0 -0
  30. calender_en/server/__pycache__/app.cpython-314.pyc +0 -0
  31. calender_en/server/__pycache__/calender_en_environment.cpython-314.pyc +0 -0
  32. calender_en/server/app.py +84 -0
  33. calender_en/server/calender_en_environment.py +306 -0
  34. calender_en/server/requirements.txt +6 -0
  35. calender_en/uv.lock +0 -0
  36. inference.py +5 -0
  37. main.py +6 -0
  38. openenv.yaml +6 -0
  39. pyproject.toml +13 -0
  40. server/__init__.py +1 -0
  41. server/app.py +9 -0
  42. tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  43. tests/__pycache__/test_app.cpython-314-pytest-9.0.2.pyc +0 -0
  44. tests/__pycache__/test_environment.cpython-314-pytest-9.0.2.pyc +0 -0
  45. tests/__pycache__/test_inference.cpython-314-pytest-9.0.2.pyc +0 -0
  46. tests/__pycache__/test_models.cpython-314-pytest-9.0.2.pyc +0 -0
  47. tests/conftest.py +11 -0
  48. tests/test_app.py +12 -0
  49. tests/test_environment.py +163 -0
  50. tests/test_inference.py +30 -0
.agents/skills/openenv-cli/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .codex
.agents/skills/openenv-cli/SKILL.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: openenv-cli
3
+ description: "OpenEnv CLI (`openenv`) for scaffolding, validating, building, and pushing OpenEnv environments."
4
+ ---
5
+
6
+ Install: `pip install openenv-core`
7
+
8
+ The OpenEnv CLI command `openenv` is available.
9
+ Use `openenv --help` to view available commands.
10
+
11
+ Generated with `openenv-core v0.2.3`. Run `openenv skills add --force` to regenerate.
12
+
13
+ ## Tips
14
+
15
+ - Start with `openenv init <env_name>` to scaffold a new environment
16
+ - Validate projects with `openenv validate`
17
+ - Build and deploy with `openenv build` and `openenv push`
18
+ - Use `openenv <command> --help` for command-specific options
.codex/skills/openenv-cli ADDED
@@ -0,0 +1 @@
 
 
1
+ ../../.agents/skills/openenv-cli
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .git
2
+ .venv
3
+ __pycache__
4
+ *.pyc
5
+ .pytest_cache
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.14
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
2
+ FROM ${BASE_IMAGE}
3
+
4
+ WORKDIR /app/env
5
+ ENV UV_PYTHON=python3
6
+
7
+ RUN apt-get update && \
8
+ apt-get install -y --no-install-recommends git && \
9
+ rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY . /app/env
12
+
13
+ RUN if ! command -v uv >/dev/null 2>&1; then \
14
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
15
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
16
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
17
+ fi
18
+
19
+ RUN --mount=type=cache,target=/root/.cache/uv \
20
+ uv sync
21
+
22
+ ENV PATH="/app/env/.venv/bin:$PATH"
23
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
24
+
25
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
26
+ CMD curl -f http://localhost:8000/health || exit 1
27
+
28
+ CMD ["uv", "run", "python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
README.md ADDED
File without changes
calender_en/.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .git
2
+ .venv
3
+ __pycache__
4
+ *.pyc
5
+ .pytest_cache
calender_en/Dockerfile ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Root Dockerfile to support `docker build -t smart-calendar-env .`
2
+
3
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
4
+ FROM ${BASE_IMAGE} AS builder
5
+
6
+ WORKDIR /app
7
+
8
+ RUN apt-get update && \
9
+ apt-get install -y --no-install-recommends git && \
10
+ rm -rf /var/lib/apt/lists/*
11
+
12
+ COPY . /app/env
13
+ WORKDIR /app/env
14
+
15
+ RUN if ! command -v uv >/dev/null 2>&1; then \
16
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
17
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
18
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
19
+ fi
20
+
21
+ RUN --mount=type=cache,target=/root/.cache/uv \
22
+ if [ -f uv.lock ]; then \
23
+ uv sync --frozen --no-install-project --no-editable; \
24
+ else \
25
+ uv sync --no-install-project --no-editable; \
26
+ fi
27
+
28
+ RUN --mount=type=cache,target=/root/.cache/uv \
29
+ if [ -f uv.lock ]; then \
30
+ uv sync --frozen --no-editable; \
31
+ else \
32
+ uv sync --no-editable; \
33
+ fi
34
+
35
+ FROM ${BASE_IMAGE}
36
+
37
+ WORKDIR /app
38
+
39
+ COPY --from=builder /app/env/.venv /app/.venv
40
+ COPY --from=builder /app/env /app/env
41
+
42
+ ENV PATH="/app/.venv/bin:$PATH"
43
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
44
+
45
+
46
+
47
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
48
+ CMD curl -f http://localhost:8000/health || exit 1
49
+
50
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
calender_en/README.md ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SmartCalendarResolver
3
+ emoji: "📅"
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
+ - scheduling
13
+ ---
14
+
15
+ # SmartCalendarResolver
16
+
17
+ SmartCalendarResolver is a deterministic OpenEnv scheduling environment. Each episode walks through a fixed four-stage workflow:
18
+
19
+ 1. understand request
20
+ 2. evaluate availability
21
+ 3. propose slot
22
+ 4. confirm schedule
23
+
24
+ The environment uses a small in-code dataset with easy, medium, and hard scenarios. There is no randomness.
25
+
26
+ ## Action Model
27
+
28
+ `CalenderEnAction` fields:
29
+
30
+ - `stage`: one of `understand_request`, `evaluate_availability`, `propose_slot`, `confirm_schedule`
31
+ - `proposed_time_slot`: optional slot string used during proposal or confirmation
32
+ - `confirm_schedule`: boolean used only during confirmation
33
+ - `final_note`: short reasoning or confirmation note
34
+
35
+ ## Observation Model
36
+
37
+ `CalenderEnObservation` fields:
38
+
39
+ - `request`
40
+ - `availability`
41
+ - `constraints`
42
+ - `step_count`
43
+ - `reward`
44
+ - `done`
45
+ - `feedback`
46
+ - `next_expected_stage`
47
+
48
+ ## Reward Behavior
49
+
50
+ - Correct stage ordering is rewarded.
51
+ - Correct slot selection is rewarded.
52
+ - Respecting deadline and earliest-slot constraints is rewarded.
53
+ - Proper final confirmation is rewarded.
54
+ - Invalid or repeated actions are penalized.
55
+
56
+ ## Local Development
57
+
58
+ Install dependencies:
59
+
60
+ ```bash
61
+ uv sync
62
+ ```
63
+
64
+ Validate the environment:
65
+
66
+ ```bash
67
+ uv run openenv validate .
68
+ ```
69
+
70
+ Run the deterministic baseline:
71
+
72
+ ```bash
73
+ uv run python inference.py
74
+ ```
75
+
76
+ Start the FastAPI server:
77
+
78
+ ```bash
79
+ uv run python server/app.py
80
+ ```
81
+
82
+ Health check:
83
+
84
+ ```bash
85
+ curl http://localhost:8000/health
86
+ ```
87
+
88
+ ## Docker
89
+
90
+ Build:
91
+
92
+ ```bash
93
+ docker build -t smart-calendar-env .
94
+ ```
95
+
96
+ Run:
97
+
98
+ ```bash
99
+ docker run -p 8000:8000 smart-calendar-env
100
+ ```
101
+
102
+ Then verify:
103
+
104
+ ```bash
105
+ curl http://localhost:8000/health
106
+ ```
calender_en/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Calender En Environment."""
8
+
9
+ from .client import CalenderEnEnv
10
+ from .models import CalenderEnAction, CalenderEnObservation
11
+
12
+ __all__ = [
13
+ "CalenderEnAction",
14
+ "CalenderEnObservation",
15
+ "CalenderEnEnv",
16
+ ]
calender_en/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (329 Bytes). View file
 
calender_en/__pycache__/client.cpython-314.pyc ADDED
Binary file (4.63 kB). View file
 
calender_en/__pycache__/inference.cpython-314.pyc ADDED
Binary file (4.67 kB). View file
 
calender_en/__pycache__/models.cpython-314.pyc ADDED
Binary file (3.32 kB). View file
 
calender_en/client.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Calender En Environment Client."""
8
+
9
+ from typing import Dict
10
+
11
+ from openenv.core import EnvClient
12
+ from openenv.core.client_types import StepResult
13
+ from openenv.core.env_server.types import State
14
+
15
+ from .models import CalenderEnAction, CalenderEnObservation
16
+
17
+
18
+ class CalenderEnEnv(
19
+ EnvClient[CalenderEnAction, CalenderEnObservation, State]
20
+ ):
21
+ """
22
+ Client for the Calender En Environment.
23
+
24
+ This client maintains a persistent WebSocket connection to the environment server,
25
+ enabling efficient multi-step interactions with lower latency.
26
+ Each client instance has its own dedicated environment session on the server.
27
+
28
+ Example:
29
+ >>> # Connect to a running server
30
+ >>> with CalenderEnEnv(base_url="http://localhost:8000") as client:
31
+ ... result = client.reset()
32
+ ... print(result.observation.request)
33
+ ...
34
+ ... result = client.step(
35
+ ... CalenderEnAction(stage="understand_request", final_note="Identify participants and deadline.")
36
+ ... )
37
+ ... print(result.observation.feedback)
38
+
39
+ Example with Docker:
40
+ >>> # Automatically start container and connect
41
+ >>> client = CalenderEnEnv.from_docker_image("calender_en-env:latest")
42
+ >>> try:
43
+ ... result = client.reset()
44
+ ... result = client.step(
45
+ ... CalenderEnAction(stage="understand_request", final_note="Identify participants and deadline.")
46
+ ... )
47
+ ... finally:
48
+ ... client.close()
49
+ """
50
+
51
+ def _step_payload(self, action: CalenderEnAction) -> Dict:
52
+ """
53
+ Convert CalenderEnAction to JSON payload for step message.
54
+
55
+ Args:
56
+ action: CalenderEnAction instance
57
+
58
+ Returns:
59
+ Dictionary representation suitable for JSON encoding
60
+ """
61
+ return {
62
+ "stage": action.stage,
63
+ "proposed_time_slot": action.proposed_time_slot,
64
+ "confirm_schedule": action.confirm_schedule,
65
+ "final_note": action.final_note,
66
+ }
67
+
68
+ def _parse_result(self, payload: Dict) -> StepResult[CalenderEnObservation]:
69
+ """
70
+ Parse server response into StepResult[CalenderEnObservation].
71
+
72
+ Args:
73
+ payload: JSON response data from server
74
+
75
+ Returns:
76
+ StepResult with CalenderEnObservation
77
+ """
78
+ obs_data = payload.get("observation", {})
79
+ observation = CalenderEnObservation(
80
+ request=obs_data.get("request", ""),
81
+ availability=obs_data.get("availability", {}),
82
+ constraints=obs_data.get("constraints", {}),
83
+ step_count=obs_data.get("step_count", 0),
84
+ reward=obs_data.get("reward", payload.get("reward", 0.0)),
85
+ done=payload.get("done", False),
86
+ feedback=obs_data.get("feedback", ""),
87
+ next_expected_stage=obs_data.get("next_expected_stage"),
88
+ )
89
+
90
+ return StepResult(
91
+ observation=observation,
92
+ reward=payload.get("reward"),
93
+ done=payload.get("done", False),
94
+ )
95
+
96
+ def _parse_state(self, payload: Dict) -> State:
97
+ """
98
+ Parse server response into State object.
99
+
100
+ Args:
101
+ payload: JSON response from state request
102
+
103
+ Returns:
104
+ State object with episode_id and step_count
105
+ """
106
+ return State(
107
+ episode_id=payload.get("episode_id"),
108
+ step_count=payload.get("step_count", 0),
109
+ )
calender_en/inference.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic baseline for the SmartCalendarResolver environment."""
2
+
3
+ from typing import List
4
+
5
+ try:
6
+ from calender_en.models import CalenderEnAction
7
+ from calender_en.server.calender_en_environment import CalenderEnEnvironment
8
+ except ModuleNotFoundError:
9
+ from models import CalenderEnAction
10
+ from server.calender_en_environment import CalenderEnEnvironment
11
+
12
+ TASK_NAME = "smart_calendar_resolution"
13
+ ENV_NAME = "calender_en"
14
+ MODEL_NAME = "deterministic-baseline"
15
+
16
+
17
+ def _policy() -> List[CalenderEnAction]:
18
+ return [
19
+ CalenderEnAction(
20
+ stage="understand_request",
21
+ final_note="Identify the meeting objective, participants, and deadline.",
22
+ ),
23
+ CalenderEnAction(
24
+ stage="evaluate_availability",
25
+ final_note="Intersect participant availability and filter slots before the deadline.",
26
+ ),
27
+ CalenderEnAction(
28
+ stage="propose_slot",
29
+ proposed_time_slot="2026-04-08 10:00-10:30 UTC",
30
+ final_note="Choose the earliest common 30 minute slot before the deadline.",
31
+ ),
32
+ CalenderEnAction(
33
+ stage="confirm_schedule",
34
+ proposed_time_slot="2026-04-08 10:00-10:30 UTC",
35
+ confirm_schedule=True,
36
+ final_note="Confirmed with all participants and calendar invite is ready.",
37
+ ),
38
+ ]
39
+
40
+
41
+ def _format_action(action: CalenderEnAction) -> str:
42
+ parts = [f"stage={action.stage}"]
43
+ if action.proposed_time_slot:
44
+ parts.append(f"slot={action.proposed_time_slot}")
45
+ parts.append(f"confirm={str(action.confirm_schedule).lower()}")
46
+ if action.final_note:
47
+ parts.append(f"note={action.final_note}")
48
+ return "|".join(parts)
49
+
50
+
51
+ def main() -> None:
52
+ env = CalenderEnEnvironment()
53
+ rewards: List[str] = []
54
+ steps = 0
55
+ success = False
56
+
57
+ print(f"[START] task={TASK_NAME} env={ENV_NAME} model={MODEL_NAME}")
58
+
59
+ try:
60
+ env.reset()
61
+ for action in _policy():
62
+ steps += 1
63
+ error = "null"
64
+ try:
65
+ observation = env.step(action)
66
+ reward_text = f"{observation.reward:.2f}"
67
+ done_text = str(observation.done).lower()
68
+ rewards.append(reward_text)
69
+ print(
70
+ f"[STEP] step={steps} action={_format_action(action)} "
71
+ f"reward={reward_text} done={done_text} error={error}"
72
+ )
73
+ success = observation.done
74
+ except Exception as exc:
75
+ reward_text = "0.00"
76
+ rewards.append(reward_text)
77
+ print(
78
+ f"[STEP] step={steps} action={_format_action(action)} "
79
+ f"reward={reward_text} done=false error={str(exc)}"
80
+ )
81
+ success = False
82
+ break
83
+ except Exception:
84
+ success = False
85
+ finally:
86
+ rewards_text = ",".join(rewards)
87
+ print(f"[END] success={str(success).lower()} steps={steps} rewards={rewards_text}")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
calender_en/models.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Data models for the SmartCalendarResolver environment."""
8
+
9
+ from typing import Dict, List, Literal, Optional
10
+
11
+ from openenv.core.env_server.types import Action, Observation
12
+ from pydantic import Field
13
+
14
+ StageName = Literal[
15
+ "understand_request",
16
+ "evaluate_availability",
17
+ "propose_slot",
18
+ "confirm_schedule",
19
+ ]
20
+
21
+
22
+ class CalenderEnAction(Action):
23
+ """Typed action for the deterministic scheduling workflow."""
24
+
25
+ stage: StageName = Field(..., description="Current scheduling stage being executed")
26
+ proposed_time_slot: Optional[str] = Field(
27
+ default=None,
28
+ description="Candidate slot proposed by the agent when proposing or confirming",
29
+ )
30
+ confirm_schedule: bool = Field(
31
+ default=False,
32
+ description="Whether the agent is explicitly confirming the selected schedule",
33
+ )
34
+ final_note: str = Field(
35
+ default="",
36
+ description="Short reasoning or confirmation note for the current stage",
37
+ )
38
+
39
+
40
+ class CalenderEnObservation(Observation):
41
+ """Observation for the SmartCalendarResolver workflow."""
42
+
43
+ request: str = Field(default="", description="Scheduling request under consideration")
44
+ availability: Dict[str, List[str]] = Field(
45
+ default_factory=dict,
46
+ description="Participant availability windows for the active scenario",
47
+ )
48
+ constraints: Dict[str, str] = Field(
49
+ default_factory=dict,
50
+ description="Scheduling constraints such as duration, priority, and deadline",
51
+ )
52
+ step_count: int = Field(default=0, description="Current number of steps taken")
53
+ reward: float = Field(default=0.0, description="Reward assigned to the latest step")
54
+ done: bool = Field(default=False, description="Whether the episode is complete")
55
+ feedback: str = Field(default="", description="Environment feedback for the latest action")
56
+ next_expected_stage: Optional[StageName] = Field(
57
+ default="understand_request",
58
+ description="Next valid stage expected by the environment",
59
+ )
calender_en/openenv.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: calender_en
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
7
+
calender_en/openenv_calender_en.egg-info/PKG-INFO ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: openenv-calender_en
3
+ Version: 0.1.0
4
+ Summary: Calender En environment for OpenEnv
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: openenv-core[core]>=0.2.2
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
9
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
calender_en/openenv_calender_en.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ pyproject.toml
3
+ ./__init__.py
4
+ ./client.py
5
+ ./inference.py
6
+ ./models.py
7
+ openenv_calender_en.egg-info/PKG-INFO
8
+ openenv_calender_en.egg-info/SOURCES.txt
9
+ openenv_calender_en.egg-info/dependency_links.txt
10
+ openenv_calender_en.egg-info/entry_points.txt
11
+ openenv_calender_en.egg-info/requires.txt
12
+ openenv_calender_en.egg-info/top_level.txt
13
+ server/__init__.py
14
+ server/app.py
15
+ server/calender_en_environment.py
calender_en/openenv_calender_en.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
calender_en/openenv_calender_en.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ server = calender_en.server.app:main
calender_en/openenv_calender_en.egg-info/requires.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ openenv-core[core]>=0.2.2
2
+
3
+ [dev]
4
+ pytest>=8.0.0
5
+ pytest-cov>=4.0.0
calender_en/openenv_calender_en.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ calender_en
calender_en/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-calender_en"
13
+ version = "0.1.0"
14
+ description = "Calender En 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.2",
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 calender_en.server.app
40
+ server = "calender_en.server.app:main"
41
+
42
+ [tool.setuptools]
43
+ include-package-data = true
44
+ packages = ["calender_en", "calender_en.server"]
45
+ package-dir = { "calender_en" = ".", "calender_en.server" = "server" }
calender_en/server/Dockerfile ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=calender_en
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
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
calender_en/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
+ """Calender En environment server components."""
8
+
9
+ from .calender_en_environment import CalenderEnEnvironment
10
+
11
+ __all__ = ["CalenderEnEnvironment"]
calender_en/server/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (286 Bytes). View file
 
calender_en/server/__pycache__/app.cpython-314.pyc ADDED
Binary file (3.06 kB). View file
 
calender_en/server/__pycache__/calender_en_environment.cpython-314.pyc ADDED
Binary file (16.5 kB). View file
 
calender_en/server/app.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Calender En Environment.
9
+
10
+ This module creates an HTTP server that exposes the CalenderEnEnvironment
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
+ try:
39
+ from ..models import CalenderEnAction, CalenderEnObservation
40
+ from .calender_en_environment import CalenderEnEnvironment
41
+ except ImportError:
42
+ import sys
43
+ from pathlib import Path
44
+
45
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
46
+ from models import CalenderEnAction, CalenderEnObservation
47
+ from server.calender_en_environment import CalenderEnEnvironment
48
+
49
+
50
+ # Create the app with web interface and README integration
51
+ app = create_app(
52
+ CalenderEnEnvironment,
53
+ CalenderEnAction,
54
+ CalenderEnObservation,
55
+ env_name="calender_en",
56
+ max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
57
+ )
58
+
59
+
60
+ def main() -> None:
61
+ """
62
+ Entry point for direct execution via uv run or python -m.
63
+
64
+ This function enables running the server without Docker:
65
+ uv run --project . server
66
+ uv run --project . server --port 8001
67
+ python -m calender_en.server.app
68
+
69
+ For production deployments, consider using uvicorn directly with
70
+ multiple workers:
71
+ uvicorn calender_en.server.app:app --workers 4
72
+ """
73
+ import argparse
74
+ import uvicorn
75
+
76
+ parser = argparse.ArgumentParser()
77
+ parser.add_argument("--host", default="0.0.0.0")
78
+ parser.add_argument("--port", type=int, default=8000)
79
+ args = parser.parse_args()
80
+ uvicorn.run(app, host=args.host, port=args.port)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
calender_en/server/calender_en_environment.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic SmartCalendarResolver environment implementation."""
2
+
3
+ from typing import Dict, List, Optional, TypedDict
4
+ from uuid import uuid4
5
+
6
+ from openenv.core.env_server.interfaces import Environment
7
+ from openenv.core.env_server.types import State
8
+
9
+ try:
10
+ from ..models import CalenderEnAction, CalenderEnObservation, StageName
11
+ except ImportError:
12
+ from models import CalenderEnAction, CalenderEnObservation, StageName
13
+
14
+
15
+ class Scenario(TypedDict):
16
+ difficulty: str
17
+ request: str
18
+ participants: List[str]
19
+ availability: Dict[str, List[str]]
20
+ constraints: Dict[str, str]
21
+ ground_truth: str
22
+
23
+
24
+ SCENARIOS: List[Scenario] = [
25
+ {
26
+ "difficulty": "easy",
27
+ "request": "Schedule a 30 minute kickoff between Alex and Priya before April 9.",
28
+ "participants": ["Alex", "Priya"],
29
+ "availability": {
30
+ "Alex": [
31
+ "2026-04-08 10:00-10:30 UTC",
32
+ "2026-04-08 14:00-14:30 UTC",
33
+ ],
34
+ "Priya": [
35
+ "2026-04-08 10:00-10:30 UTC",
36
+ "2026-04-08 16:00-16:30 UTC",
37
+ ],
38
+ },
39
+ "constraints": {
40
+ "duration": "30 minutes",
41
+ "priority": "kickoff meeting should happen at the earliest common slot",
42
+ "deadline": "2026-04-09 18:00 UTC",
43
+ },
44
+ "ground_truth": "2026-04-08 10:00-10:30 UTC",
45
+ },
46
+ {
47
+ "difficulty": "medium",
48
+ "request": "Book a 45 minute design review for Mei, Jordan, and Sam before April 12 noon.",
49
+ "participants": ["Mei", "Jordan", "Sam"],
50
+ "availability": {
51
+ "Mei": [
52
+ "2026-04-10 13:00-13:45 UTC",
53
+ "2026-04-11 09:00-09:45 UTC",
54
+ ],
55
+ "Jordan": [
56
+ "2026-04-10 13:00-13:45 UTC",
57
+ "2026-04-11 11:00-11:45 UTC",
58
+ ],
59
+ "Sam": [
60
+ "2026-04-10 13:00-13:45 UTC",
61
+ "2026-04-11 09:00-09:45 UTC",
62
+ ],
63
+ },
64
+ "constraints": {
65
+ "duration": "45 minutes",
66
+ "priority": "use the earliest shared slot that avoids missing the review deadline",
67
+ "deadline": "2026-04-12 12:00 UTC",
68
+ },
69
+ "ground_truth": "2026-04-10 13:00-13:45 UTC",
70
+ },
71
+ {
72
+ "difficulty": "hard",
73
+ "request": "Find a 60 minute executive sync for Elena, Ravi, Noor, and Luis before April 15 15:00 UTC.",
74
+ "participants": ["Elena", "Ravi", "Noor", "Luis"],
75
+ "availability": {
76
+ "Elena": [
77
+ "2026-04-14 08:00-09:00 UTC",
78
+ "2026-04-15 09:00-10:00 UTC",
79
+ ],
80
+ "Ravi": [
81
+ "2026-04-14 08:00-09:00 UTC",
82
+ "2026-04-15 14:00-15:00 UTC",
83
+ ],
84
+ "Noor": [
85
+ "2026-04-14 08:00-09:00 UTC",
86
+ "2026-04-14 16:00-17:00 UTC",
87
+ ],
88
+ "Luis": [
89
+ "2026-04-14 08:00-09:00 UTC",
90
+ "2026-04-15 09:00-10:00 UTC",
91
+ ],
92
+ },
93
+ "constraints": {
94
+ "duration": "60 minutes",
95
+ "priority": "executive sync must include every participant and happen before the deadline",
96
+ "deadline": "2026-04-15 15:00 UTC",
97
+ },
98
+ "ground_truth": "2026-04-14 08:00-09:00 UTC",
99
+ },
100
+ ]
101
+
102
+ STAGES: List[StageName] = [
103
+ "understand_request",
104
+ "evaluate_availability",
105
+ "propose_slot",
106
+ "confirm_schedule",
107
+ ]
108
+
109
+
110
+ class CalenderEnEnvironment(Environment):
111
+ """A deterministic multi-step scheduling environment."""
112
+
113
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
114
+
115
+ def __init__(self) -> None:
116
+ self._state = State(episode_id=str(uuid4()), step_count=0)
117
+ self._reset_count = 0
118
+ self._scenario: Optional[Scenario] = None
119
+ self._completed_stages: List[StageName] = []
120
+ self._history: List[Dict[str, object]] = []
121
+ self._selected_slot: Optional[str] = None
122
+ self._done = False
123
+ self._solved = False
124
+
125
+ def reset(self) -> CalenderEnObservation:
126
+ self._state = State(episode_id=str(uuid4()), step_count=0)
127
+ self._scenario = SCENARIOS[self._reset_count % len(SCENARIOS)]
128
+ self._reset_count += 1
129
+ self._completed_stages = []
130
+ self._history = []
131
+ self._selected_slot = None
132
+ self._done = False
133
+ self._solved = False
134
+ return self._observation(
135
+ reward=0.0,
136
+ done=False,
137
+ feedback=(
138
+ f"Loaded {self._scenario['difficulty']} scheduling scenario for "
139
+ f"{', '.join(self._scenario['participants'])}."
140
+ ),
141
+ next_expected_stage=STAGES[0],
142
+ )
143
+
144
+ def step(self, action: CalenderEnAction) -> CalenderEnObservation: # type: ignore[override]
145
+ if self._scenario is None:
146
+ raise RuntimeError("Environment must be reset before step().")
147
+
148
+ self._state.step_count += 1
149
+ expected_stage = self._expected_stage()
150
+ if expected_stage is None:
151
+ return self._observation(
152
+ reward=-1.0,
153
+ done=True,
154
+ feedback="Episode is already complete. Call reset() to start a new scenario.",
155
+ next_expected_stage=None,
156
+ )
157
+
158
+ if action.stage in self._completed_stages:
159
+ return self._observation(
160
+ reward=-1.5,
161
+ done=self._done,
162
+ feedback=f"Stage '{action.stage}' was already completed. Repeated actions are penalized.",
163
+ next_expected_stage=expected_stage,
164
+ )
165
+
166
+ if action.stage != expected_stage:
167
+ return self._observation(
168
+ reward=-1.0,
169
+ done=False,
170
+ feedback=f"Invalid stage order. Expected '{expected_stage}' next.",
171
+ next_expected_stage=expected_stage,
172
+ )
173
+
174
+ handler = getattr(self, f"_handle_{action.stage}")
175
+ reward, feedback = handler(action)
176
+ self._history.append(
177
+ {
178
+ "stage": action.stage,
179
+ "proposed_time_slot": action.proposed_time_slot,
180
+ "confirm_schedule": action.confirm_schedule,
181
+ "reward": reward,
182
+ "feedback": feedback,
183
+ }
184
+ )
185
+ self._completed_stages.append(action.stage)
186
+ next_stage = self._expected_stage()
187
+ self._done = next_stage is None
188
+ return self._observation(
189
+ reward=reward,
190
+ done=self._done,
191
+ feedback=feedback,
192
+ next_expected_stage=next_stage,
193
+ )
194
+
195
+ @property
196
+ def state(self) -> State:
197
+ return self._state
198
+
199
+ @property
200
+ def history(self) -> List[Dict[str, object]]:
201
+ return list(self._history)
202
+
203
+ @property
204
+ def solved(self) -> bool:
205
+ return self._solved
206
+
207
+ def _expected_stage(self) -> Optional[StageName]:
208
+ if len(self._completed_stages) >= len(STAGES):
209
+ return None
210
+ return STAGES[len(self._completed_stages)]
211
+
212
+ def _handle_understand_request(self, action: CalenderEnAction) -> tuple[float, str]:
213
+ note = action.final_note.lower()
214
+ if action.proposed_time_slot or action.confirm_schedule:
215
+ return -0.5, "Understanding stage should not propose or confirm a schedule yet."
216
+ if "participant" in note or "deadline" in note or "objective" in note:
217
+ return 1.0, "Request understood. Participants and deadline were identified correctly."
218
+ return 0.5, "Request acknowledged. More explicit mention of participants or deadline would be better."
219
+
220
+ def _handle_evaluate_availability(self, action: CalenderEnAction) -> tuple[float, str]:
221
+ note = action.final_note.lower()
222
+ if action.proposed_time_slot or action.confirm_schedule:
223
+ return -0.5, "Availability stage should evaluate options before proposing a slot."
224
+ common_slots = self._common_slots()
225
+ if not common_slots:
226
+ return -2.0, "Scenario is misconfigured because no common slots exist."
227
+ if "earliest" in note or "intersect" in note or "shared" in note:
228
+ return 1.5, f"Availability evaluated. Common slot candidates: {', '.join(common_slots)}."
229
+ return 1.0, f"Availability checked. Common slot candidates: {', '.join(common_slots)}."
230
+
231
+ def _handle_propose_slot(self, action: CalenderEnAction) -> tuple[float, str]:
232
+ proposed_slot = action.proposed_time_slot
233
+ if not proposed_slot:
234
+ return -2.0, "A proposed_time_slot is required during the propose_slot stage."
235
+ if action.confirm_schedule:
236
+ return -0.5, "Do not confirm the meeting before the confirmation stage."
237
+ reward = 0.0
238
+ feedback_parts: List[str] = []
239
+ if proposed_slot not in self._common_slots():
240
+ reward -= 2.0
241
+ feedback_parts.append("Proposed slot is not available for every participant.")
242
+ else:
243
+ reward += 1.5
244
+ feedback_parts.append("Proposed slot satisfies shared availability.")
245
+ if proposed_slot == self._scenario["ground_truth"]:
246
+ reward += 2.0
247
+ feedback_parts.append("Proposed slot matches the correct deterministic solution.")
248
+ self._selected_slot = proposed_slot
249
+ else:
250
+ feedback_parts.append(
251
+ f"Correct slot is {self._scenario['ground_truth']} based on earliest valid availability."
252
+ )
253
+ self._selected_slot = proposed_slot
254
+ if "earliest" in action.final_note.lower() or "deadline" in action.final_note.lower():
255
+ reward += 0.5
256
+ feedback_parts.append("Proposal note reflects the deadline and priority constraints.")
257
+ return reward, " ".join(feedback_parts)
258
+
259
+ def _handle_confirm_schedule(self, action: CalenderEnAction) -> tuple[float, str]:
260
+ proposed_slot = action.proposed_time_slot or self._selected_slot
261
+ if not action.confirm_schedule:
262
+ return -2.0, "Confirmation stage requires confirm_schedule=True."
263
+ if not proposed_slot:
264
+ return -1.5, "Confirmation requires a concrete time slot."
265
+ if proposed_slot != self._scenario["ground_truth"]:
266
+ return -1.0, "Confirmation used the wrong slot."
267
+ reward = 2.0
268
+ note = action.final_note.lower()
269
+ if "confirmed" in note or "invite" in note:
270
+ reward += 1.0
271
+ feedback = "Schedule confirmed with a valid final note."
272
+ else:
273
+ feedback = "Schedule confirmed, but the final note should explicitly mention confirmation."
274
+ self._selected_slot = proposed_slot
275
+ self._solved = True
276
+ return reward, feedback
277
+
278
+ def _common_slots(self) -> List[str]:
279
+ if self._scenario is None:
280
+ return []
281
+ participant_slots = [
282
+ set(self._scenario["availability"][participant])
283
+ for participant in self._scenario["participants"]
284
+ ]
285
+ common = set.intersection(*participant_slots)
286
+ return sorted(common)
287
+
288
+ def _observation(
289
+ self,
290
+ reward: float,
291
+ done: bool,
292
+ feedback: str,
293
+ next_expected_stage: Optional[StageName],
294
+ ) -> CalenderEnObservation:
295
+ if self._scenario is None:
296
+ raise RuntimeError("Scenario is not initialized.")
297
+ return CalenderEnObservation(
298
+ request=self._scenario["request"],
299
+ availability=self._scenario["availability"],
300
+ constraints=self._scenario["constraints"],
301
+ step_count=self._state.step_count,
302
+ reward=reward,
303
+ done=done,
304
+ feedback=feedback,
305
+ next_expected_stage=next_expected_stage,
306
+ )
calender_en/server/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ openenv[core]>=0.2.0
2
+ fastapi>=0.115.0
3
+ uvicorn>=0.24.0
4
+
5
+
6
+
calender_en/uv.lock ADDED
The diff for this file is too large to render. See raw diff
 
inference.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from calender_en.inference import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from calender-env-v1!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
openenv.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: calender_en
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
pyproject.toml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "calender-env-v1"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "openenv-core[core]>=0.2.2",
9
+ "pytest>=8.0.0",
10
+ ]
11
+
12
+ [project.scripts]
13
+ server = "server.app:main"
server/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Root deployment wrappers for the SmartCalendarResolver server."""
server/app.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from calender_en.server.app import app, main as package_main
2
+
3
+
4
+ def main() -> None:
5
+ package_main()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc ADDED
Binary file (735 Bytes). View file
 
tests/__pycache__/test_app.cpython-314-pytest-9.0.2.pyc ADDED
Binary file (2.48 kB). View file
 
tests/__pycache__/test_environment.cpython-314-pytest-9.0.2.pyc ADDED
Binary file (26.4 kB). View file
 
tests/__pycache__/test_inference.cpython-314-pytest-9.0.2.pyc ADDED
Binary file (6.04 kB). View file
 
tests/__pycache__/test_models.cpython-314-pytest-9.0.2.pyc ADDED
Binary file (4.96 kB). View file
 
tests/conftest.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+
5
+ ROOT = Path(__file__).resolve().parents[1]
6
+ PACKAGE_ROOT = ROOT / "calender_en"
7
+
8
+ for path in (ROOT, PACKAGE_ROOT):
9
+ path_str = str(path)
10
+ if path_str not in sys.path:
11
+ sys.path.insert(0, path_str)
tests/test_app.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.testclient import TestClient
2
+
3
+ from calender_en.server.app import app
4
+
5
+
6
+ def test_health_endpoint_returns_healthy() -> None:
7
+ client = TestClient(app)
8
+
9
+ response = client.get("/health")
10
+
11
+ assert response.status_code == 200
12
+ assert response.json() == {"status":"healthy"}
tests/test_environment.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from calender_en.models import CalenderEnAction
2
+ from calender_en.server.calender_en_environment import CalenderEnEnvironment
3
+
4
+
5
+ def _advance_to_proposal(env: CalenderEnEnvironment) -> None:
6
+ env.step(
7
+ CalenderEnAction(
8
+ stage="understand_request",
9
+ final_note="Identify participants, objective, and deadline.",
10
+ )
11
+ )
12
+ env.step(
13
+ CalenderEnAction(
14
+ stage="evaluate_availability",
15
+ final_note="Intersect shared availability and choose the earliest feasible option.",
16
+ )
17
+ )
18
+
19
+
20
+ def test_reset_returns_valid_initial_observation() -> None:
21
+ env = CalenderEnEnvironment()
22
+
23
+ observation = env.reset()
24
+
25
+ assert observation.request
26
+ assert observation.availability
27
+ assert observation.constraints
28
+ assert observation.step_count == 0
29
+ assert observation.reward == 0.0
30
+ assert observation.done is False
31
+ assert observation.next_expected_stage == "understand_request"
32
+ assert env.state.step_count == 0
33
+ assert env.history == []
34
+ assert env.solved is False
35
+
36
+
37
+ def test_step_follows_expected_multi_stage_flow() -> None:
38
+ env = CalenderEnEnvironment()
39
+ env.reset()
40
+
41
+ understand = env.step(
42
+ CalenderEnAction(
43
+ stage="understand_request",
44
+ final_note="Identify participants, objective, and deadline.",
45
+ )
46
+ )
47
+ availability = env.step(
48
+ CalenderEnAction(
49
+ stage="evaluate_availability",
50
+ final_note="Intersect shared availability and prioritize the earliest option.",
51
+ )
52
+ )
53
+ proposal = env.step(
54
+ CalenderEnAction(
55
+ stage="propose_slot",
56
+ proposed_time_slot="2026-04-08 10:00-10:30 UTC",
57
+ final_note="Pick the earliest shared slot before the deadline.",
58
+ )
59
+ )
60
+ confirmation = env.step(
61
+ CalenderEnAction(
62
+ stage="confirm_schedule",
63
+ proposed_time_slot="2026-04-08 10:00-10:30 UTC",
64
+ confirm_schedule=True,
65
+ final_note="Confirmed and invite is ready.",
66
+ )
67
+ )
68
+
69
+ assert understand.next_expected_stage == "evaluate_availability"
70
+ assert availability.next_expected_stage == "propose_slot"
71
+ assert proposal.next_expected_stage == "confirm_schedule"
72
+ assert confirmation.done is True
73
+ assert confirmation.next_expected_stage is None
74
+ assert env.solved is True
75
+
76
+
77
+ def test_deterministic_scenario_cycling() -> None:
78
+ env = CalenderEnEnvironment()
79
+
80
+ requests = [env.reset().request for _ in range(4)]
81
+
82
+ assert requests[0] != requests[1]
83
+ assert requests[1] != requests[2]
84
+ assert requests[0] == requests[3]
85
+
86
+
87
+ def test_correct_slot_scores_higher_than_wrong_slot() -> None:
88
+ correct_env = CalenderEnEnvironment()
89
+ correct_env.reset()
90
+ _advance_to_proposal(correct_env)
91
+ correct = correct_env.step(
92
+ CalenderEnAction(
93
+ stage="propose_slot",
94
+ proposed_time_slot="2026-04-08 10:00-10:30 UTC",
95
+ final_note="Pick the earliest shared slot before the deadline.",
96
+ )
97
+ )
98
+
99
+ wrong_env = CalenderEnEnvironment()
100
+ wrong_env.reset()
101
+ _advance_to_proposal(wrong_env)
102
+ wrong = wrong_env.step(
103
+ CalenderEnAction(
104
+ stage="propose_slot",
105
+ proposed_time_slot="2026-04-08 14:00-14:30 UTC",
106
+ final_note="Pick a slot even if it is not shared.",
107
+ )
108
+ )
109
+
110
+ assert correct.reward > wrong.reward
111
+
112
+
113
+ def test_state_updates_episode_id_step_count_history_and_solved() -> None:
114
+ env = CalenderEnEnvironment()
115
+
116
+ first_reset = env.reset()
117
+ first_episode_id = env.state.episode_id
118
+ assert first_reset.step_count == 0
119
+ assert env.history == []
120
+ assert env.solved is False
121
+
122
+ env.step(
123
+ CalenderEnAction(
124
+ stage="understand_request",
125
+ final_note="Identify participants, objective, and deadline.",
126
+ )
127
+ )
128
+ assert env.state.step_count == 1
129
+ assert len(env.history) == 1
130
+ assert env.history[0]["stage"] == "understand_request"
131
+ assert env.solved is False
132
+
133
+ env.step(
134
+ CalenderEnAction(
135
+ stage="evaluate_availability",
136
+ final_note="Intersect shared availability and prioritize the earliest option.",
137
+ )
138
+ )
139
+ env.step(
140
+ CalenderEnAction(
141
+ stage="propose_slot",
142
+ proposed_time_slot="2026-04-08 10:00-10:30 UTC",
143
+ final_note="Pick the earliest shared slot before the deadline.",
144
+ )
145
+ )
146
+ env.step(
147
+ CalenderEnAction(
148
+ stage="confirm_schedule",
149
+ proposed_time_slot="2026-04-08 10:00-10:30 UTC",
150
+ confirm_schedule=True,
151
+ final_note="Confirmed and invite is ready.",
152
+ )
153
+ )
154
+
155
+ assert env.state.step_count == 4
156
+ assert len(env.history) == 4
157
+ assert env.solved is True
158
+
159
+ env.reset()
160
+ assert env.state.episode_id != first_episode_id
161
+ assert env.state.step_count == 0
162
+ assert env.history == []
163
+ assert env.solved is False
tests/test_inference.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import redirect_stdout
2
+ from io import StringIO
3
+
4
+ from calender_en import inference
5
+
6
+
7
+ def test_inference_runs_end_to_end_without_crashing() -> None:
8
+ output = StringIO()
9
+
10
+ with redirect_stdout(output):
11
+ inference.main()
12
+
13
+ rendered = output.getvalue()
14
+ lines = rendered.strip().splitlines()
15
+ assert len(lines) == 6
16
+ assert lines[0] == "[START] task=smart_calendar_resolution env=calender_en model=deterministic-baseline"
17
+ assert lines[-1] == "[END] success=true steps=4 rewards=1.00,1.50,4.00,3.00"
18
+ assert all(line.startswith("[STEP]") for line in lines[1:5])
19
+
20
+
21
+ def test_inference_output_is_deterministic() -> None:
22
+ first = StringIO()
23
+ second = StringIO()
24
+
25
+ with redirect_stdout(first):
26
+ inference.main()
27
+ with redirect_stdout(second):
28
+ inference.main()
29
+
30
+ assert first.getvalue() == second.getvalue()