krishnah27 commited on
Commit
54092f4
·
verified ·
1 Parent(s): 2f80506

Upload OpenEnv model release environment

Browse files
.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: Openenv Model Release Env
3
- emoji: 🌖
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: docker
7
- pinned: false
8
- license: apache-2.0
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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