Spaces:
Sleeping
Sleeping
Commit ·
d02cfdb
0
Parent(s):
SmartCalendarResolver
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .agents/skills/openenv-cli/.gitignore +1 -0
- .agents/skills/openenv-cli/SKILL.md +18 -0
- .codex/skills/openenv-cli +1 -0
- .dockerignore +5 -0
- .python-version +1 -0
- Dockerfile +28 -0
- README.md +0 -0
- calender_en/.dockerignore +5 -0
- calender_en/Dockerfile +50 -0
- calender_en/README.md +106 -0
- calender_en/__init__.py +16 -0
- calender_en/__pycache__/__init__.cpython-314.pyc +0 -0
- calender_en/__pycache__/client.cpython-314.pyc +0 -0
- calender_en/__pycache__/inference.cpython-314.pyc +0 -0
- calender_en/__pycache__/models.cpython-314.pyc +0 -0
- calender_en/client.py +109 -0
- calender_en/inference.py +91 -0
- calender_en/models.py +59 -0
- calender_en/openenv.yaml +7 -0
- calender_en/openenv_calender_en.egg-info/PKG-INFO +9 -0
- calender_en/openenv_calender_en.egg-info/SOURCES.txt +15 -0
- calender_en/openenv_calender_en.egg-info/dependency_links.txt +1 -0
- calender_en/openenv_calender_en.egg-info/entry_points.txt +2 -0
- calender_en/openenv_calender_en.egg-info/requires.txt +5 -0
- calender_en/openenv_calender_en.egg-info/top_level.txt +1 -0
- calender_en/pyproject.toml +45 -0
- calender_en/server/Dockerfile +80 -0
- calender_en/server/__init__.py +11 -0
- calender_en/server/__pycache__/__init__.cpython-314.pyc +0 -0
- calender_en/server/__pycache__/app.cpython-314.pyc +0 -0
- calender_en/server/__pycache__/calender_en_environment.cpython-314.pyc +0 -0
- calender_en/server/app.py +84 -0
- calender_en/server/calender_en_environment.py +306 -0
- calender_en/server/requirements.txt +6 -0
- calender_en/uv.lock +0 -0
- inference.py +5 -0
- main.py +6 -0
- openenv.yaml +6 -0
- pyproject.toml +13 -0
- server/__init__.py +1 -0
- server/app.py +9 -0
- tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- tests/__pycache__/test_app.cpython-314-pytest-9.0.2.pyc +0 -0
- tests/__pycache__/test_environment.cpython-314-pytest-9.0.2.pyc +0 -0
- tests/__pycache__/test_inference.cpython-314-pytest-9.0.2.pyc +0 -0
- tests/__pycache__/test_models.cpython-314-pytest-9.0.2.pyc +0 -0
- tests/conftest.py +11 -0
- tests/test_app.py +12 -0
- tests/test_environment.py +163 -0
- 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()
|