Upload OpenEnv model release environment
Browse files- .dockerignore +8 -0
- .gitignore +11 -0
- README.md +109 -7
- __init__.py +18 -0
- client.py +44 -0
- inference.py +259 -0
- models.py +95 -0
- openenv.yaml +9 -0
- pyproject.toml +42 -0
- server/Dockerfile +11 -0
- server/__init__.py +5 -0
- server/app.py +36 -0
- server/model_release_env_environment.py +412 -0
- server/requirements.txt +6 -0
- uv.lock +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
.git
|
| 3 |
+
.venv
|
| 4 |
+
.pytest_cache
|
| 5 |
+
dist
|
| 6 |
+
build
|
| 7 |
+
outputs
|
| 8 |
+
tests
|
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
.pytest_cache/
|
| 4 |
+
*.pyc
|
| 5 |
+
*.egg-info/
|
| 6 |
+
dist/
|
| 7 |
+
build/
|
| 8 |
+
outputs/logs/*
|
| 9 |
+
outputs/evals/*
|
| 10 |
+
!outputs/logs/.gitkeep
|
| 11 |
+
!outputs/evals/.gitkeep
|
README.md
CHANGED
|
@@ -1,11 +1,113 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji: 🌖
|
| 4 |
-
colorFrom: pink
|
| 5 |
-
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Model Release Env
|
|
|
|
|
|
|
|
|
|
| 3 |
sdk: docker
|
| 4 |
+
app_port: 8000
|
| 5 |
+
base_path: /web
|
| 6 |
+
tags:
|
| 7 |
+
- openenv
|
| 8 |
+
- llm
|
| 9 |
+
- evaluation
|
| 10 |
+
- release
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Model Release Env
|
| 14 |
+
|
| 15 |
+
Model Release Env is an OpenEnv environment for release-readiness decisions around LLM launches. The agent acts like a release engineer reviewing structured evidence, updating a release package, and making the correct launch decision under tight operational constraints.
|
| 16 |
+
|
| 17 |
+
The current offline heuristic smoke baseline scores `1.00` on all three tasks, for an average score of `1.00`.
|
| 18 |
+
|
| 19 |
+
## Why this is a real environment
|
| 20 |
+
|
| 21 |
+
This is modeled on a real workflow used before shipping model checkpoints: confirm what can be published, align the release card with approved evidence, and block unsafe launches when compliance or safety signals fail. The environment is deterministic, fast to evaluate, and shaped for RL because every intermediate edit changes a measurable checklist score.
|
| 22 |
+
|
| 23 |
+
## Tasks
|
| 24 |
+
|
| 25 |
+
Three tasks are included.
|
| 26 |
+
|
| 27 |
+
1. `card_completion_easy`: finish a draft release card with the correct model identity, evaluation summary, limitations, and release channel.
|
| 28 |
+
2. `policy_alignment_medium`: align the package with licensing and serving-policy constraints.
|
| 29 |
+
3. `launch_gate_hard`: detect a regression plus a critical safety issue and hold the launch.
|
| 30 |
+
|
| 31 |
+
Each task exposes three evidence documents, a structured release package, and a small action space.
|
| 32 |
+
|
| 33 |
+
## Action Space
|
| 34 |
+
|
| 35 |
+
`inspect`: reveal one hidden evidence document.
|
| 36 |
+
|
| 37 |
+
`set_field`: update one structured package field.
|
| 38 |
+
|
| 39 |
+
`set_decision`: set the release channel to `public`, `beta`, or `hold`.
|
| 40 |
+
|
| 41 |
+
`submit`: finish the episode and receive the final score.
|
| 42 |
+
|
| 43 |
+
## Reward Design
|
| 44 |
+
|
| 45 |
+
The score is a weighted checklist in `[0, 1]`.
|
| 46 |
+
|
| 47 |
+
- First-time document inspection gives a small positive reward.
|
| 48 |
+
- Editing a field that satisfies a previously unsatisfied checklist item gives positive reward equal to the score gain.
|
| 49 |
+
- Invalid or non-improving edits incur a small penalty.
|
| 50 |
+
- `submit` returns the final normalized score.
|
| 51 |
+
|
| 52 |
+
This gives dense partial credit while keeping grading fully programmatic.
|
| 53 |
+
|
| 54 |
+
## Project Layout
|
| 55 |
+
|
| 56 |
+
- `models.py`: typed action, observation, and state contracts.
|
| 57 |
+
- `client.py`: typed OpenEnv client.
|
| 58 |
+
- `server/model_release_env_environment.py`: deterministic task logic and grading.
|
| 59 |
+
- `server/app.py`: FastAPI/OpenEnv server entry point.
|
| 60 |
+
- `inference.py`: baseline runner with OpenAI-client support and an offline heuristic fallback.
|
| 61 |
+
|
| 62 |
+
## Local Development
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
uv sync
|
| 66 |
+
uv run server
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
Open the local server at `http://localhost:8000/web`.
|
| 70 |
+
|
| 71 |
+
## Docker
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
docker build -t model-release-env:latest -f server/Dockerfile .
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Baseline Runner
|
| 78 |
+
|
| 79 |
+
The hackathon runner expects a root-level `inference.py`.
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
HF_TOKEN=hf_xxx uv run python inference.py
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
Relevant variables:
|
| 86 |
+
|
| 87 |
+
- `API_BASE_URL` defaults to `https://router.huggingface.co/v1`
|
| 88 |
+
- `MODEL_NAME` defaults to `Qwen/Qwen2.5-72B-Instruct`
|
| 89 |
+
- `HF_TOKEN` is required for LLM-backed runs
|
| 90 |
+
- `LOCAL_IMAGE_NAME` defaults to `model-release-env:latest`
|
| 91 |
+
- `ENV_BASE_URL` can be used instead of Docker for a running server
|
| 92 |
+
|
| 93 |
+
If `HF_TOKEN` is missing, `inference.py` uses a deterministic heuristic fallback so the project can still be smoke-tested offline.
|
| 94 |
+
|
| 95 |
+
On machines with a polluted user-site Python installation, prefix local runs with `env -u PYTHONPATH` to prevent incompatible global packages from overriding the repo environment.
|
| 96 |
+
|
| 97 |
+
## Example Client Usage
|
| 98 |
+
|
| 99 |
+
```python
|
| 100 |
+
from model_release_env import ModelReleaseAction, ModelReleaseEnv
|
| 101 |
+
|
| 102 |
+
with ModelReleaseEnv(base_url="http://localhost:8000").sync() as env:
|
| 103 |
+
result = env.reset(task_name="card_completion_easy")
|
| 104 |
+
result = env.step(ModelReleaseAction(action_type="inspect", target="release_brief"))
|
| 105 |
+
print(result.observation.visible_documents)
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
## Validation Notes
|
| 109 |
+
|
| 110 |
+
- The environment implements typed `Action`, `Observation`, and `State` models.
|
| 111 |
+
- `reset`, `step`, and `state` follow the OpenEnv contract.
|
| 112 |
+
- The baseline logs use the required `[START]`, `[STEP]`, and `[END]` markers.
|
| 113 |
+
- The server responds to `/reset`, which is required by the submission validator.
|
__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model Release Environment for OpenEnv."""
|
| 2 |
+
|
| 3 |
+
from .models import ModelReleaseAction, ModelReleaseObservation, ModelReleaseState
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"ModelReleaseAction",
|
| 7 |
+
"ModelReleaseObservation",
|
| 8 |
+
"ModelReleaseState",
|
| 9 |
+
"ModelReleaseEnv",
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def __getattr__(name: str):
|
| 14 |
+
if name == "ModelReleaseEnv":
|
| 15 |
+
from .client import ModelReleaseEnv
|
| 16 |
+
|
| 17 |
+
return ModelReleaseEnv
|
| 18 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
client.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Client for the Model Release environment."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from openenv.core.client_types import StepResult
|
| 9 |
+
from openenv.core.env_client import EnvClient
|
| 10 |
+
|
| 11 |
+
from .models import ModelReleaseAction, ModelReleaseObservation, ModelReleaseState
|
| 12 |
+
except ImportError:
|
| 13 |
+
from openenv.core.client_types import StepResult
|
| 14 |
+
from openenv.core.env_client import EnvClient
|
| 15 |
+
|
| 16 |
+
from models import ModelReleaseAction, ModelReleaseObservation, ModelReleaseState
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ModelReleaseEnv(
|
| 20 |
+
EnvClient[ModelReleaseAction, ModelReleaseObservation, ModelReleaseState]
|
| 21 |
+
):
|
| 22 |
+
"""Typed WebSocket client for deterministic LLM release workflows."""
|
| 23 |
+
|
| 24 |
+
def _step_payload(self, action: ModelReleaseAction) -> Dict[str, Any]:
|
| 25 |
+
return action.model_dump()
|
| 26 |
+
|
| 27 |
+
def _parse_result(self, payload: Dict[str, Any]) -> StepResult[ModelReleaseObservation]:
|
| 28 |
+
observation = ModelReleaseObservation(**payload.get("observation", {}))
|
| 29 |
+
return StepResult(
|
| 30 |
+
observation=observation,
|
| 31 |
+
reward=payload.get("reward"),
|
| 32 |
+
done=payload.get("done", False),
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
def _parse_state(self, payload: Dict[str, Any]) -> ModelReleaseState:
|
| 36 |
+
return ModelReleaseState(**payload)
|
| 37 |
+
|
| 38 |
+
async def reset(
|
| 39 |
+
self, task_name: str | None = None, **kwargs: Any
|
| 40 |
+
) -> StepResult[ModelReleaseObservation]:
|
| 41 |
+
reset_kwargs = dict(kwargs)
|
| 42 |
+
if task_name is not None:
|
| 43 |
+
reset_kwargs["task_name"] = task_name
|
| 44 |
+
return await super().reset(**reset_kwargs)
|
inference.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hackathon baseline runner for Model Release Env."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
import sys
|
| 10 |
+
from typing import Any, Dict, List, Optional
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _sanitize_sys_path() -> None:
|
| 14 |
+
current_tag = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
| 15 |
+
sys.path[:] = [
|
| 16 |
+
entry
|
| 17 |
+
for entry in sys.path
|
| 18 |
+
if entry == "" or "/site-packages" not in entry or current_tag in entry
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
_sanitize_sys_path()
|
| 23 |
+
|
| 24 |
+
from openai import OpenAI
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
from model_release_env import ModelReleaseAction, ModelReleaseEnv
|
| 28 |
+
except ImportError:
|
| 29 |
+
from client import ModelReleaseEnv
|
| 30 |
+
from models import ModelReleaseAction
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 34 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
|
| 35 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 36 |
+
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME", "model-release-env:latest")
|
| 37 |
+
ENV_BASE_URL = os.getenv("ENV_BASE_URL")
|
| 38 |
+
BENCHMARK = os.getenv("BENCHMARK_NAME", "model_release_env")
|
| 39 |
+
MAX_STEPS = int(os.getenv("MODEL_RELEASE_MAX_STEPS", "8"))
|
| 40 |
+
SUCCESS_THRESHOLD = float(os.getenv("MODEL_RELEASE_SUCCESS_THRESHOLD", "0.75"))
|
| 41 |
+
|
| 42 |
+
DEFAULT_TASKS = [
|
| 43 |
+
"card_completion_easy",
|
| 44 |
+
"policy_alignment_medium",
|
| 45 |
+
"launch_gate_hard",
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
HEURISTIC_PLANS: Dict[str, List[Dict[str, str]]] = {
|
| 49 |
+
"card_completion_easy": [
|
| 50 |
+
{"action_type": "inspect", "target": "release_brief"},
|
| 51 |
+
{"action_type": "inspect", "target": "eval_sheet"},
|
| 52 |
+
{"action_type": "inspect", "target": "risk_note"},
|
| 53 |
+
{
|
| 54 |
+
"action_type": "set_field",
|
| 55 |
+
"target": "base_model",
|
| 56 |
+
"value": "Qwen2.5-7B-Instruct",
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"action_type": "set_field",
|
| 60 |
+
"target": "eval_summary",
|
| 61 |
+
"value": "gsm8k=0.78; math500=0.61; aime24=0.18",
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"action_type": "set_field",
|
| 65 |
+
"target": "limitations",
|
| 66 |
+
"value": "Fails on long derivations and is not approved for medical advice.",
|
| 67 |
+
},
|
| 68 |
+
{"action_type": "set_decision", "value": "beta"},
|
| 69 |
+
{"action_type": "submit"},
|
| 70 |
+
],
|
| 71 |
+
"policy_alignment_medium": [
|
| 72 |
+
{"action_type": "inspect", "target": "license_audit"},
|
| 73 |
+
{"action_type": "inspect", "target": "safety_report"},
|
| 74 |
+
{"action_type": "inspect", "target": "policy_note"},
|
| 75 |
+
{
|
| 76 |
+
"action_type": "set_field",
|
| 77 |
+
"target": "license",
|
| 78 |
+
"value": "apache-2.0",
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"action_type": "set_field",
|
| 82 |
+
"target": "safety_guardrails",
|
| 83 |
+
"value": "prompt_injection_filter; pii_redaction",
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"action_type": "set_field",
|
| 87 |
+
"target": "blocked_use_cases",
|
| 88 |
+
"value": "legal advice; medical advice",
|
| 89 |
+
},
|
| 90 |
+
{"action_type": "set_decision", "value": "beta"},
|
| 91 |
+
{"action_type": "submit"},
|
| 92 |
+
],
|
| 93 |
+
"launch_gate_hard": [
|
| 94 |
+
{"action_type": "inspect", "target": "regression_report"},
|
| 95 |
+
{"action_type": "inspect", "target": "red_team_note"},
|
| 96 |
+
{"action_type": "inspect", "target": "launch_policy"},
|
| 97 |
+
{
|
| 98 |
+
"action_type": "set_field",
|
| 99 |
+
"target": "known_regressions",
|
| 100 |
+
"value": "latency; throughput",
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"action_type": "set_field",
|
| 104 |
+
"target": "launch_blockers",
|
| 105 |
+
"value": "chain-of-thought extraction bypass",
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"action_type": "set_field",
|
| 109 |
+
"target": "public_summary",
|
| 110 |
+
"value": "release on hold pending latency and safety fixes.",
|
| 111 |
+
},
|
| 112 |
+
{"action_type": "set_decision", "value": "hold"},
|
| 113 |
+
{"action_type": "submit"},
|
| 114 |
+
],
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _task_names() -> List[str]:
|
| 119 |
+
raw = os.getenv("MODEL_RELEASE_TASKS")
|
| 120 |
+
if not raw:
|
| 121 |
+
return list(DEFAULT_TASKS)
|
| 122 |
+
return [item.strip() for item in raw.split(",") if item.strip()]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _compact_action(action: ModelReleaseAction) -> str:
|
| 126 |
+
value = action.value.replace(" ", "_") if action.value else ""
|
| 127 |
+
if action.action_type == "inspect":
|
| 128 |
+
return f"inspect({action.target})"
|
| 129 |
+
if action.action_type == "set_field":
|
| 130 |
+
return f"set_field({action.target}={value})"
|
| 131 |
+
if action.action_type == "set_decision":
|
| 132 |
+
return f"set_decision({value})"
|
| 133 |
+
return "submit()"
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _stderr(message: str) -> None:
|
| 137 |
+
print(message, file=sys.stderr)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _llm_client() -> Optional[OpenAI]:
|
| 141 |
+
if not HF_TOKEN:
|
| 142 |
+
return None
|
| 143 |
+
return OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _extract_json_block(content: str) -> Dict[str, Any]:
|
| 147 |
+
match = re.search(r"\{.*\}", content, re.DOTALL)
|
| 148 |
+
if not match:
|
| 149 |
+
raise ValueError("No JSON object found in model response")
|
| 150 |
+
return json.loads(match.group(0))
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _observation_prompt(observation: Any) -> str:
|
| 154 |
+
payload = {
|
| 155 |
+
"task_name": observation.task_name,
|
| 156 |
+
"goal": observation.goal,
|
| 157 |
+
"document_index": observation.document_index,
|
| 158 |
+
"visible_documents": observation.visible_documents,
|
| 159 |
+
"package_snapshot": observation.package_snapshot,
|
| 160 |
+
"checklist_status": observation.checklist_status,
|
| 161 |
+
"available_fields": observation.available_fields,
|
| 162 |
+
"available_decisions": observation.available_decisions,
|
| 163 |
+
"inspected_documents": observation.inspected_documents,
|
| 164 |
+
"remaining_steps": observation.remaining_steps,
|
| 165 |
+
"score": observation.score,
|
| 166 |
+
"last_action_error": observation.last_action_error,
|
| 167 |
+
}
|
| 168 |
+
return json.dumps(payload, indent=2, sort_keys=True)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _heuristic_action(task_name: str, step_index: int) -> ModelReleaseAction:
|
| 172 |
+
plan = HEURISTIC_PLANS[task_name]
|
| 173 |
+
if step_index >= len(plan):
|
| 174 |
+
return ModelReleaseAction(action_type="submit")
|
| 175 |
+
return ModelReleaseAction(**plan[step_index])
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _model_action(
|
| 179 |
+
client: OpenAI,
|
| 180 |
+
task_name: str,
|
| 181 |
+
observation: Any,
|
| 182 |
+
) -> ModelReleaseAction:
|
| 183 |
+
system_prompt = (
|
| 184 |
+
"You are operating an OpenEnv release-readiness environment. "
|
| 185 |
+
"Return exactly one JSON object with keys action_type, target, and value. "
|
| 186 |
+
"Allowed action_type values: inspect, set_field, set_decision, submit. "
|
| 187 |
+
"Use inspect before editing. Keep values compact and deterministic."
|
| 188 |
+
)
|
| 189 |
+
user_prompt = (
|
| 190 |
+
f"Task: {task_name}\n"
|
| 191 |
+
"Choose the single best next action given the observation below.\n"
|
| 192 |
+
"Observation JSON:\n"
|
| 193 |
+
f"{_observation_prompt(observation)}"
|
| 194 |
+
)
|
| 195 |
+
response = client.chat.completions.create(
|
| 196 |
+
model=MODEL_NAME,
|
| 197 |
+
temperature=0.0,
|
| 198 |
+
max_tokens=220,
|
| 199 |
+
messages=[
|
| 200 |
+
{"role": "system", "content": system_prompt},
|
| 201 |
+
{"role": "user", "content": user_prompt},
|
| 202 |
+
],
|
| 203 |
+
)
|
| 204 |
+
content = response.choices[0].message.content or ""
|
| 205 |
+
return ModelReleaseAction(**_extract_json_block(content))
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
async def _create_env() -> ModelReleaseEnv:
|
| 209 |
+
if ENV_BASE_URL:
|
| 210 |
+
return ModelReleaseEnv(base_url=ENV_BASE_URL)
|
| 211 |
+
return await ModelReleaseEnv.from_docker_image(LOCAL_IMAGE_NAME)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
async def _run_task(env: ModelReleaseEnv, task_name: str, llm: Optional[OpenAI]) -> float:
|
| 215 |
+
print(f"[START] benchmark={BENCHMARK} task={task_name}")
|
| 216 |
+
result = await env.reset(task_name=task_name)
|
| 217 |
+
step_index = 0
|
| 218 |
+
|
| 219 |
+
while not result.done and step_index < MAX_STEPS:
|
| 220 |
+
try:
|
| 221 |
+
if llm is None:
|
| 222 |
+
action = _heuristic_action(task_name, step_index)
|
| 223 |
+
else:
|
| 224 |
+
action = _model_action(llm, task_name, result.observation)
|
| 225 |
+
except Exception as exc:
|
| 226 |
+
_stderr(f"planner fallback for {task_name}: {exc}")
|
| 227 |
+
action = _heuristic_action(task_name, step_index)
|
| 228 |
+
|
| 229 |
+
result = await env.step(action)
|
| 230 |
+
error = result.observation.last_action_error or "null"
|
| 231 |
+
reward = 0.0 if result.reward is None else float(result.reward)
|
| 232 |
+
print(
|
| 233 |
+
f"[STEP] action={_compact_action(action)} reward={reward:.2f} "
|
| 234 |
+
f"done={str(result.done)} error={error}"
|
| 235 |
+
)
|
| 236 |
+
step_index += 1
|
| 237 |
+
|
| 238 |
+
score = float(result.observation.score)
|
| 239 |
+
success = score >= SUCCESS_THRESHOLD
|
| 240 |
+
print(f"[END] success={str(success)} score={score:.2f}")
|
| 241 |
+
return score
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
async def main() -> int:
|
| 245 |
+
llm = _llm_client()
|
| 246 |
+
env = await _create_env()
|
| 247 |
+
scores: List[float] = []
|
| 248 |
+
|
| 249 |
+
async with env:
|
| 250 |
+
for task_name in _task_names():
|
| 251 |
+
scores.append(await _run_task(env, task_name, llm))
|
| 252 |
+
|
| 253 |
+
average_score = sum(scores) / len(scores) if scores else 0.0
|
| 254 |
+
_stderr(f"average_score={average_score:.2f}")
|
| 255 |
+
return 0
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
if __name__ == "__main__":
|
| 259 |
+
raise SystemExit(asyncio.run(main()))
|
models.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Typed models for the Model Release environment."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 6 |
+
|
| 7 |
+
from pydantic import Field
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from openenv.core.env_server.types import Action, Observation, State
|
| 11 |
+
except ImportError:
|
| 12 |
+
from openenv_core.env_server.types import Action, Observation, State
|
| 13 |
+
|
| 14 |
+
ActionType = Literal["inspect", "set_field", "set_decision", "submit"]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ModelReleaseAction(Action):
|
| 18 |
+
"""Action schema for release-readiness workflows."""
|
| 19 |
+
|
| 20 |
+
action_type: ActionType = Field(
|
| 21 |
+
..., description="One of inspect, set_field, set_decision, or submit"
|
| 22 |
+
)
|
| 23 |
+
target: str = Field(
|
| 24 |
+
default="",
|
| 25 |
+
description="Document name for inspect or field name for set_field",
|
| 26 |
+
)
|
| 27 |
+
value: str = Field(
|
| 28 |
+
default="",
|
| 29 |
+
description="Field value for set_field or release decision for set_decision",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class ModelReleaseObservation(Observation):
|
| 34 |
+
"""Observation returned after reset and step operations."""
|
| 35 |
+
|
| 36 |
+
task_name: str = Field(default="", description="Current task identifier")
|
| 37 |
+
difficulty: str = Field(default="easy", description="Task difficulty label")
|
| 38 |
+
goal: str = Field(default="", description="What the agent must accomplish")
|
| 39 |
+
document_index: Dict[str, str] = Field(
|
| 40 |
+
default_factory=dict,
|
| 41 |
+
description="Available documents mapped to short summaries",
|
| 42 |
+
)
|
| 43 |
+
visible_documents: Dict[str, str] = Field(
|
| 44 |
+
default_factory=dict,
|
| 45 |
+
description="Full contents of documents that have been inspected",
|
| 46 |
+
)
|
| 47 |
+
package_snapshot: Dict[str, Any] = Field(
|
| 48 |
+
default_factory=dict,
|
| 49 |
+
description="Current structured release package fields",
|
| 50 |
+
)
|
| 51 |
+
checklist_status: Dict[str, bool] = Field(
|
| 52 |
+
default_factory=dict,
|
| 53 |
+
description="Progress on deterministic grader checks",
|
| 54 |
+
)
|
| 55 |
+
available_fields: List[str] = Field(
|
| 56 |
+
default_factory=list,
|
| 57 |
+
description="Fields the agent may edit with set_field",
|
| 58 |
+
)
|
| 59 |
+
available_decisions: List[str] = Field(
|
| 60 |
+
default_factory=list,
|
| 61 |
+
description="Allowed release decisions for the task",
|
| 62 |
+
)
|
| 63 |
+
inspected_documents: List[str] = Field(
|
| 64 |
+
default_factory=list,
|
| 65 |
+
description="Documents already revealed to the agent",
|
| 66 |
+
)
|
| 67 |
+
remaining_steps: int = Field(default=0, description="Steps left in the episode")
|
| 68 |
+
last_action_error: Optional[str] = Field(
|
| 69 |
+
default=None,
|
| 70 |
+
description="Validation or execution error from the last action",
|
| 71 |
+
)
|
| 72 |
+
score: float = Field(
|
| 73 |
+
default=0.0,
|
| 74 |
+
description="Current normalized task score in the range [0, 1]",
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class ModelReleaseState(State):
|
| 79 |
+
"""Episode state for release-readiness tasks."""
|
| 80 |
+
|
| 81 |
+
task_name: str = Field(default="", description="Current task identifier")
|
| 82 |
+
difficulty: str = Field(default="easy", description="Task difficulty label")
|
| 83 |
+
completed_checks: List[str] = Field(
|
| 84 |
+
default_factory=list,
|
| 85 |
+
description="Checklist items currently satisfied",
|
| 86 |
+
)
|
| 87 |
+
inspected_documents: List[str] = Field(
|
| 88 |
+
default_factory=list,
|
| 89 |
+
description="Document names revealed during the episode",
|
| 90 |
+
)
|
| 91 |
+
release_decision: str = Field(
|
| 92 |
+
default="undecided",
|
| 93 |
+
description="Current release channel or gate decision",
|
| 94 |
+
)
|
| 95 |
+
score: float = Field(default=0.0, description="Current normalized task score")
|
openenv.yaml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: model_release_env
|
| 3 |
+
version: 0.1.0
|
| 4 |
+
description: Deterministic release-readiness environment for LLM launches.
|
| 5 |
+
type: space
|
| 6 |
+
runtime: fastapi
|
| 7 |
+
app: server.app:app
|
| 8 |
+
port: 8000
|
| 9 |
+
default_image: model-release-env:latest
|
pyproject.toml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=69.0", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "openenv-model-release-env"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "OpenEnv environment for deterministic LLM release readiness tasks"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.10"
|
| 11 |
+
dependencies = [
|
| 12 |
+
"fastapi>=0.115.0",
|
| 13 |
+
"openai>=1.52.0",
|
| 14 |
+
"openenv-core>=0.2.0",
|
| 15 |
+
"pydantic>=2.7.0",
|
| 16 |
+
"PyYAML>=6.0.0",
|
| 17 |
+
"uvicorn>=0.30.0",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
[project.optional-dependencies]
|
| 21 |
+
dev = [
|
| 22 |
+
"pytest>=8.3.0",
|
| 23 |
+
"requests>=2.32.0",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
[project.scripts]
|
| 27 |
+
server = "server.app:main"
|
| 28 |
+
|
| 29 |
+
[tool.setuptools]
|
| 30 |
+
packages = ["model_release_env", "server"]
|
| 31 |
+
include-package-data = true
|
| 32 |
+
|
| 33 |
+
[tool.setuptools.package-dir]
|
| 34 |
+
model_release_env = "."
|
| 35 |
+
server = "server"
|
| 36 |
+
|
| 37 |
+
[tool.setuptools.package-data]
|
| 38 |
+
model_release_env = ["openenv.yaml", "README.md"]
|
| 39 |
+
|
| 40 |
+
[tool.pytest.ini_options]
|
| 41 |
+
testpaths = ["tests"]
|
| 42 |
+
addopts = "-q"
|
server/Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM ghcr.io/meta-pytorch/openenv-base:latest
|
| 2 |
+
|
| 3 |
+
WORKDIR /app/env
|
| 4 |
+
|
| 5 |
+
COPY . /app/env
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir .
|
| 8 |
+
|
| 9 |
+
EXPOSE 8000
|
| 10 |
+
|
| 11 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
server/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Server package for the Model Release environment."""
|
| 2 |
+
|
| 3 |
+
from .model_release_env_environment import ModelReleaseEnvironment
|
| 4 |
+
|
| 5 |
+
__all__ = ["ModelReleaseEnvironment"]
|
server/app.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI application for the Model Release environment."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
from openenv.core.env_server.http_server import create_app
|
| 7 |
+
except ImportError as exc: # pragma: no cover
|
| 8 |
+
raise ImportError(
|
| 9 |
+
"openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
|
| 10 |
+
) from exc
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from model_release_env.models import ModelReleaseAction, ModelReleaseObservation
|
| 14 |
+
from server.model_release_env_environment import ModelReleaseEnvironment
|
| 15 |
+
except ImportError:
|
| 16 |
+
from models import ModelReleaseAction, ModelReleaseObservation
|
| 17 |
+
from server.model_release_env_environment import ModelReleaseEnvironment
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
app = create_app(
|
| 21 |
+
ModelReleaseEnvironment,
|
| 22 |
+
ModelReleaseAction,
|
| 23 |
+
ModelReleaseObservation,
|
| 24 |
+
env_name="model_release_env",
|
| 25 |
+
max_concurrent_envs=4,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def main(host: str = "0.0.0.0", port: int = 8000) -> None:
|
| 30 |
+
import uvicorn
|
| 31 |
+
|
| 32 |
+
uvicorn.run(app, host=host, port=port)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
if __name__ == "__main__":
|
| 36 |
+
main()
|
server/model_release_env_environment.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Environment logic for deterministic LLM release-readiness tasks."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
from copy import deepcopy
|
| 8 |
+
from typing import Any, Dict, Tuple
|
| 9 |
+
from uuid import uuid4
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
from openenv.core.env_server.interfaces import Environment
|
| 13 |
+
except ImportError:
|
| 14 |
+
from openenv.core.env_server.interfaces import Environment
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
from model_release_env.models import (
|
| 18 |
+
ModelReleaseAction,
|
| 19 |
+
ModelReleaseObservation,
|
| 20 |
+
ModelReleaseState,
|
| 21 |
+
)
|
| 22 |
+
except ImportError:
|
| 23 |
+
from models import ModelReleaseAction, ModelReleaseObservation, ModelReleaseState
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
DEFAULT_TASK = os.getenv("MODEL_RELEASE_TASK", "card_completion_easy")
|
| 27 |
+
DEFAULT_MAX_STEPS = int(os.getenv("MODEL_RELEASE_MAX_STEPS", "8"))
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _normalize(value: str) -> str:
|
| 31 |
+
return re.sub(r"\s+", " ", value.strip().lower())
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _check_rule(rule: Dict[str, Any], value: Any) -> bool:
|
| 35 |
+
normalized_value = _normalize(str(value or ""))
|
| 36 |
+
rule_type = rule["type"]
|
| 37 |
+
if rule_type == "exact":
|
| 38 |
+
return normalized_value == _normalize(rule["expected"])
|
| 39 |
+
if rule_type == "contains_all":
|
| 40 |
+
return all(token in normalized_value for token in rule["tokens"])
|
| 41 |
+
return False
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
TASKS: Dict[str, Dict[str, Any]] = {
|
| 45 |
+
"card_completion_easy": {
|
| 46 |
+
"difficulty": "easy",
|
| 47 |
+
"goal": "Complete the missing release-card fields before shipping the model.",
|
| 48 |
+
"max_steps": 8,
|
| 49 |
+
"allowed_decisions": ["public", "beta", "hold"],
|
| 50 |
+
"documents": {
|
| 51 |
+
"release_brief": (
|
| 52 |
+
"Candidate alias: Qwen2.5-7B-Instruct. Context window: 32768 tokens. "
|
| 53 |
+
"Training scope in the draft is acceptable and does not need edits."
|
| 54 |
+
),
|
| 55 |
+
"eval_sheet": (
|
| 56 |
+
"Evaluation summary for the candidate release: gsm8k=0.78; math500=0.61; "
|
| 57 |
+
"aime24=0.18. Use this exact compact format in the release package."
|
| 58 |
+
),
|
| 59 |
+
"risk_note": (
|
| 60 |
+
"Known limitations: fails on long derivations and is not approved for medical advice. "
|
| 61 |
+
"Because red-team review is still pending, the correct release channel is beta."
|
| 62 |
+
),
|
| 63 |
+
},
|
| 64 |
+
"document_summaries": {
|
| 65 |
+
"release_brief": "Base model identity and draft scope.",
|
| 66 |
+
"eval_sheet": "Benchmark numbers approved for publication.",
|
| 67 |
+
"risk_note": "Known limitations and launch recommendation.",
|
| 68 |
+
},
|
| 69 |
+
"starting_package": {
|
| 70 |
+
"base_model": "",
|
| 71 |
+
"training_data_scope": "filtered math tutoring data",
|
| 72 |
+
"eval_summary": "",
|
| 73 |
+
"limitations": "",
|
| 74 |
+
"release_channel": "public",
|
| 75 |
+
},
|
| 76 |
+
"editable_fields": ["base_model", "eval_summary", "limitations"],
|
| 77 |
+
"checks": {
|
| 78 |
+
"base_model": {
|
| 79 |
+
"type": "exact",
|
| 80 |
+
"expected": "Qwen2.5-7B-Instruct",
|
| 81 |
+
"weight": 0.25,
|
| 82 |
+
},
|
| 83 |
+
"eval_summary": {
|
| 84 |
+
"type": "exact",
|
| 85 |
+
"expected": "gsm8k=0.78; math500=0.61; aime24=0.18",
|
| 86 |
+
"weight": 0.25,
|
| 87 |
+
},
|
| 88 |
+
"limitations": {
|
| 89 |
+
"type": "contains_all",
|
| 90 |
+
"tokens": ["long derivations", "medical advice"],
|
| 91 |
+
"weight": 0.25,
|
| 92 |
+
},
|
| 93 |
+
"release_channel": {
|
| 94 |
+
"type": "exact",
|
| 95 |
+
"expected": "beta",
|
| 96 |
+
"weight": 0.25,
|
| 97 |
+
},
|
| 98 |
+
},
|
| 99 |
+
},
|
| 100 |
+
"policy_alignment_medium": {
|
| 101 |
+
"difficulty": "medium",
|
| 102 |
+
"goal": "Resolve the licensing and safety-policy gaps before publication.",
|
| 103 |
+
"max_steps": 8,
|
| 104 |
+
"allowed_decisions": ["public", "beta", "hold"],
|
| 105 |
+
"documents": {
|
| 106 |
+
"license_audit": (
|
| 107 |
+
"Base weights are Apache-2.0. The published model card must keep the license field as apache-2.0."
|
| 108 |
+
),
|
| 109 |
+
"safety_report": (
|
| 110 |
+
"Required serving guardrails: prompt_injection_filter; pii_redaction. "
|
| 111 |
+
"Both must appear in the safety_guardrails field."
|
| 112 |
+
),
|
| 113 |
+
"policy_note": (
|
| 114 |
+
"Blocked use cases: legal advice and medical advice. The release channel remains beta "
|
| 115 |
+
"until manual compliance review is complete."
|
| 116 |
+
),
|
| 117 |
+
},
|
| 118 |
+
"document_summaries": {
|
| 119 |
+
"license_audit": "Approved license value.",
|
| 120 |
+
"safety_report": "Mandatory runtime guardrails.",
|
| 121 |
+
"policy_note": "Disallowed uses and launch channel.",
|
| 122 |
+
},
|
| 123 |
+
"starting_package": {
|
| 124 |
+
"license": "unknown",
|
| 125 |
+
"safety_guardrails": "",
|
| 126 |
+
"blocked_use_cases": "",
|
| 127 |
+
"release_channel": "public",
|
| 128 |
+
},
|
| 129 |
+
"editable_fields": ["license", "safety_guardrails", "blocked_use_cases"],
|
| 130 |
+
"checks": {
|
| 131 |
+
"license": {
|
| 132 |
+
"type": "exact",
|
| 133 |
+
"expected": "apache-2.0",
|
| 134 |
+
"weight": 0.25,
|
| 135 |
+
},
|
| 136 |
+
"safety_guardrails": {
|
| 137 |
+
"type": "contains_all",
|
| 138 |
+
"tokens": ["prompt_injection_filter", "pii_redaction"],
|
| 139 |
+
"weight": 0.25,
|
| 140 |
+
},
|
| 141 |
+
"blocked_use_cases": {
|
| 142 |
+
"type": "contains_all",
|
| 143 |
+
"tokens": ["legal advice", "medical advice"],
|
| 144 |
+
"weight": 0.25,
|
| 145 |
+
},
|
| 146 |
+
"release_channel": {
|
| 147 |
+
"type": "exact",
|
| 148 |
+
"expected": "beta",
|
| 149 |
+
"weight": 0.25,
|
| 150 |
+
},
|
| 151 |
+
},
|
| 152 |
+
},
|
| 153 |
+
"launch_gate_hard": {
|
| 154 |
+
"difficulty": "hard",
|
| 155 |
+
"goal": "Make the correct launch decision after a regression and a critical safety finding.",
|
| 156 |
+
"max_steps": 8,
|
| 157 |
+
"allowed_decisions": ["public", "beta", "hold"],
|
| 158 |
+
"documents": {
|
| 159 |
+
"regression_report": (
|
| 160 |
+
"Regression summary: latency +18%; throughput -12%; accuracy unchanged. "
|
| 161 |
+
"Known regressions should list latency and throughput."
|
| 162 |
+
),
|
| 163 |
+
"red_team_note": (
|
| 164 |
+
"Critical blocker: chain-of-thought extraction bypass reproduced in 3/5 runs. "
|
| 165 |
+
"This must appear in the launch_blockers field."
|
| 166 |
+
),
|
| 167 |
+
"launch_policy": (
|
| 168 |
+
"Policy: any critical safety issue or >10% latency regression requires hold channel. "
|
| 169 |
+
"Replace the public summary with: release on hold pending latency and safety fixes."
|
| 170 |
+
),
|
| 171 |
+
},
|
| 172 |
+
"document_summaries": {
|
| 173 |
+
"regression_report": "Performance regressions for the candidate release.",
|
| 174 |
+
"red_team_note": "Critical red-team finding.",
|
| 175 |
+
"launch_policy": "Mandatory launch-gate rule.",
|
| 176 |
+
},
|
| 177 |
+
"starting_package": {
|
| 178 |
+
"known_regressions": "none",
|
| 179 |
+
"launch_blockers": "",
|
| 180 |
+
"public_summary": "Faster than the previous release and ready for public launch.",
|
| 181 |
+
"release_channel": "public",
|
| 182 |
+
},
|
| 183 |
+
"editable_fields": ["known_regressions", "launch_blockers", "public_summary"],
|
| 184 |
+
"checks": {
|
| 185 |
+
"known_regressions": {
|
| 186 |
+
"type": "contains_all",
|
| 187 |
+
"tokens": ["latency", "throughput"],
|
| 188 |
+
"weight": 0.25,
|
| 189 |
+
},
|
| 190 |
+
"launch_blockers": {
|
| 191 |
+
"type": "contains_all",
|
| 192 |
+
"tokens": ["chain-of-thought extraction bypass"],
|
| 193 |
+
"weight": 0.25,
|
| 194 |
+
},
|
| 195 |
+
"public_summary": {
|
| 196 |
+
"type": "exact",
|
| 197 |
+
"expected": "release on hold pending latency and safety fixes.",
|
| 198 |
+
"weight": 0.25,
|
| 199 |
+
},
|
| 200 |
+
"release_channel": {
|
| 201 |
+
"type": "exact",
|
| 202 |
+
"expected": "hold",
|
| 203 |
+
"weight": 0.25,
|
| 204 |
+
},
|
| 205 |
+
},
|
| 206 |
+
},
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class ModelReleaseEnvironment(
|
| 211 |
+
Environment[ModelReleaseAction, ModelReleaseObservation, ModelReleaseState]
|
| 212 |
+
):
|
| 213 |
+
"""Release-readiness environment for deterministic LLM launch workflows."""
|
| 214 |
+
|
| 215 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 216 |
+
|
| 217 |
+
def __init__(self, max_steps: int | None = None, default_task: str | None = None):
|
| 218 |
+
super().__init__()
|
| 219 |
+
self._configured_max_steps = max_steps or DEFAULT_MAX_STEPS
|
| 220 |
+
self._default_task = default_task or DEFAULT_TASK
|
| 221 |
+
self._task_name = self._default_task if self._default_task in TASKS else next(iter(TASKS))
|
| 222 |
+
self._task_spec: Dict[str, Any] = {}
|
| 223 |
+
self._package: Dict[str, Any] = {}
|
| 224 |
+
self._visible_documents: Dict[str, str] = {}
|
| 225 |
+
self._inspected_documents: set[str] = set()
|
| 226 |
+
self._last_action_error: str | None = None
|
| 227 |
+
self._score_by_check: Dict[str, bool] = {}
|
| 228 |
+
self._task_score = 0.0
|
| 229 |
+
self._state = ModelReleaseState(
|
| 230 |
+
episode_id=str(uuid4()),
|
| 231 |
+
step_count=0,
|
| 232 |
+
task_name=self._task_name,
|
| 233 |
+
difficulty="easy",
|
| 234 |
+
completed_checks=[],
|
| 235 |
+
inspected_documents=[],
|
| 236 |
+
release_decision="undecided",
|
| 237 |
+
score=0.0,
|
| 238 |
+
)
|
| 239 |
+
self.reset(task_name=self._task_name)
|
| 240 |
+
|
| 241 |
+
def reset(
|
| 242 |
+
self,
|
| 243 |
+
seed: int | None = None,
|
| 244 |
+
episode_id: str | None = None,
|
| 245 |
+
task_name: str | None = None,
|
| 246 |
+
**kwargs: Any,
|
| 247 |
+
) -> ModelReleaseObservation:
|
| 248 |
+
del seed, kwargs
|
| 249 |
+
selected_task = task_name or self._default_task
|
| 250 |
+
if selected_task not in TASKS:
|
| 251 |
+
selected_task = next(iter(TASKS))
|
| 252 |
+
|
| 253 |
+
self._task_name = selected_task
|
| 254 |
+
self._task_spec = deepcopy(TASKS[selected_task])
|
| 255 |
+
self._package = deepcopy(self._task_spec["starting_package"])
|
| 256 |
+
self._visible_documents = {}
|
| 257 |
+
self._inspected_documents = set()
|
| 258 |
+
self._last_action_error = None
|
| 259 |
+
self._task_score, self._score_by_check = self._compute_score()
|
| 260 |
+
|
| 261 |
+
self._state = ModelReleaseState(
|
| 262 |
+
episode_id=episode_id or str(uuid4()),
|
| 263 |
+
step_count=0,
|
| 264 |
+
task_name=self._task_name,
|
| 265 |
+
difficulty=self._task_spec["difficulty"],
|
| 266 |
+
completed_checks=self._completed_checks(),
|
| 267 |
+
inspected_documents=[],
|
| 268 |
+
release_decision=self._package.get("release_channel", "undecided"),
|
| 269 |
+
score=self._task_score,
|
| 270 |
+
)
|
| 271 |
+
return self._build_observation(reward=0.0, done=False)
|
| 272 |
+
|
| 273 |
+
def step(self, action: ModelReleaseAction, **kwargs: Any) -> ModelReleaseObservation:
|
| 274 |
+
del kwargs
|
| 275 |
+
self._state.step_count += 1
|
| 276 |
+
self._last_action_error = None
|
| 277 |
+
reward = 0.0
|
| 278 |
+
score_before = self._task_score
|
| 279 |
+
package_before = deepcopy(self._package)
|
| 280 |
+
done = False
|
| 281 |
+
metadata: Dict[str, Any] = {"action_type": action.action_type}
|
| 282 |
+
|
| 283 |
+
if action.action_type == "inspect":
|
| 284 |
+
reward = self._handle_inspect(action)
|
| 285 |
+
elif action.action_type == "set_field":
|
| 286 |
+
reward = self._handle_set_field(action, score_before, package_before)
|
| 287 |
+
elif action.action_type == "set_decision":
|
| 288 |
+
reward = self._handle_set_decision(action, score_before, package_before)
|
| 289 |
+
elif action.action_type == "submit":
|
| 290 |
+
self._task_score, self._score_by_check = self._compute_score()
|
| 291 |
+
reward = round(self._task_score, 2)
|
| 292 |
+
done = True
|
| 293 |
+
metadata["submitted"] = True
|
| 294 |
+
else:
|
| 295 |
+
self._last_action_error = f"unsupported action_type: {action.action_type}"
|
| 296 |
+
reward = -0.05
|
| 297 |
+
|
| 298 |
+
self._task_score, self._score_by_check = self._compute_score()
|
| 299 |
+
|
| 300 |
+
if self._state.step_count >= self._task_spec["max_steps"]:
|
| 301 |
+
done = True
|
| 302 |
+
|
| 303 |
+
self._state.completed_checks = self._completed_checks()
|
| 304 |
+
self._state.inspected_documents = sorted(self._inspected_documents)
|
| 305 |
+
self._state.release_decision = self._package.get("release_channel", "undecided")
|
| 306 |
+
self._state.score = self._task_score
|
| 307 |
+
|
| 308 |
+
observation = self._build_observation(reward=reward, done=done)
|
| 309 |
+
observation.metadata["score_before"] = round(score_before, 2)
|
| 310 |
+
observation.metadata["score_after"] = round(self._task_score, 2)
|
| 311 |
+
observation.metadata["package_changed"] = package_before != self._package
|
| 312 |
+
observation.metadata.update(metadata)
|
| 313 |
+
return observation
|
| 314 |
+
|
| 315 |
+
@property
|
| 316 |
+
def state(self) -> ModelReleaseState:
|
| 317 |
+
return self._state
|
| 318 |
+
|
| 319 |
+
def close(self) -> None:
|
| 320 |
+
return None
|
| 321 |
+
|
| 322 |
+
def _handle_inspect(self, action: ModelReleaseAction) -> float:
|
| 323 |
+
document_name = action.target.strip()
|
| 324 |
+
documents = self._task_spec["documents"]
|
| 325 |
+
if document_name not in documents:
|
| 326 |
+
self._last_action_error = f"unknown document: {document_name}"
|
| 327 |
+
return -0.05
|
| 328 |
+
if document_name in self._inspected_documents:
|
| 329 |
+
return 0.0
|
| 330 |
+
|
| 331 |
+
self._inspected_documents.add(document_name)
|
| 332 |
+
self._visible_documents[document_name] = documents[document_name]
|
| 333 |
+
return 0.04
|
| 334 |
+
|
| 335 |
+
def _handle_set_field(
|
| 336 |
+
self,
|
| 337 |
+
action: ModelReleaseAction,
|
| 338 |
+
score_before: float,
|
| 339 |
+
package_before: Dict[str, Any],
|
| 340 |
+
) -> float:
|
| 341 |
+
target = action.target.strip()
|
| 342 |
+
value = action.value.strip()
|
| 343 |
+
if target not in self._task_spec["editable_fields"]:
|
| 344 |
+
self._last_action_error = f"field is not editable in this task: {target}"
|
| 345 |
+
return -0.05
|
| 346 |
+
if not value:
|
| 347 |
+
self._last_action_error = f"empty value for field: {target}"
|
| 348 |
+
return -0.05
|
| 349 |
+
|
| 350 |
+
self._package[target] = value
|
| 351 |
+
new_score, _ = self._compute_score()
|
| 352 |
+
if _normalize(str(package_before.get(target, ""))) == _normalize(value):
|
| 353 |
+
return 0.0
|
| 354 |
+
if new_score > score_before:
|
| 355 |
+
return round(new_score - score_before, 2)
|
| 356 |
+
return -0.05
|
| 357 |
+
|
| 358 |
+
def _handle_set_decision(
|
| 359 |
+
self,
|
| 360 |
+
action: ModelReleaseAction,
|
| 361 |
+
score_before: float,
|
| 362 |
+
package_before: Dict[str, Any],
|
| 363 |
+
) -> float:
|
| 364 |
+
decision = action.value.strip().lower()
|
| 365 |
+
if decision not in self._task_spec["allowed_decisions"]:
|
| 366 |
+
self._last_action_error = f"invalid decision: {decision}"
|
| 367 |
+
return -0.05
|
| 368 |
+
|
| 369 |
+
self._package["release_channel"] = decision
|
| 370 |
+
new_score, _ = self._compute_score()
|
| 371 |
+
if _normalize(str(package_before.get("release_channel", ""))) == _normalize(decision):
|
| 372 |
+
return 0.0
|
| 373 |
+
if new_score > score_before:
|
| 374 |
+
return round(new_score - score_before, 2)
|
| 375 |
+
return -0.05
|
| 376 |
+
|
| 377 |
+
def _compute_score(self) -> Tuple[float, Dict[str, bool]]:
|
| 378 |
+
matched: Dict[str, bool] = {}
|
| 379 |
+
total = 0.0
|
| 380 |
+
for name, rule in self._task_spec["checks"].items():
|
| 381 |
+
value = self._package.get(name, "")
|
| 382 |
+
is_match = _check_rule(rule, value)
|
| 383 |
+
matched[name] = is_match
|
| 384 |
+
if is_match:
|
| 385 |
+
total += float(rule["weight"])
|
| 386 |
+
return round(min(total, 1.0), 2), matched
|
| 387 |
+
|
| 388 |
+
def _completed_checks(self) -> list[str]:
|
| 389 |
+
return [name for name, passed in self._score_by_check.items() if passed]
|
| 390 |
+
|
| 391 |
+
def _build_observation(self, reward: float, done: bool) -> ModelReleaseObservation:
|
| 392 |
+
return ModelReleaseObservation(
|
| 393 |
+
task_name=self._task_name,
|
| 394 |
+
difficulty=self._task_spec["difficulty"],
|
| 395 |
+
goal=self._task_spec["goal"],
|
| 396 |
+
document_index=deepcopy(self._task_spec["document_summaries"]),
|
| 397 |
+
visible_documents=deepcopy(self._visible_documents),
|
| 398 |
+
package_snapshot=deepcopy(self._package),
|
| 399 |
+
checklist_status=deepcopy(self._score_by_check),
|
| 400 |
+
available_fields=list(self._task_spec["editable_fields"]),
|
| 401 |
+
available_decisions=list(self._task_spec["allowed_decisions"]),
|
| 402 |
+
inspected_documents=sorted(self._inspected_documents),
|
| 403 |
+
remaining_steps=max(self._task_spec["max_steps"] - self._state.step_count, 0),
|
| 404 |
+
last_action_error=self._last_action_error,
|
| 405 |
+
score=self._task_score,
|
| 406 |
+
reward=reward,
|
| 407 |
+
done=done,
|
| 408 |
+
metadata={
|
| 409 |
+
"task_count": len(TASKS),
|
| 410 |
+
"release_channel": self._package.get("release_channel", "undecided"),
|
| 411 |
+
},
|
| 412 |
+
)
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.115.0
|
| 2 |
+
openai>=1.52.0
|
| 3 |
+
openenv-core>=0.2.0
|
| 4 |
+
pydantic>=2.7.0
|
| 5 |
+
PyYAML>=6.0.0
|
| 6 |
+
uvicorn>=0.30.0
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|