Harden deployment and Space compatibility
Browse files- .dockerignore +8 -1
- .env.example +9 -0
- .gitignore +13 -4
- Dockerfile +11 -0
- README.md +43 -0
- inference.py +34 -6
- models.py +4 -0
- server/model_release_env_environment.py +4 -0
.dockerignore
CHANGED
|
@@ -1,8 +1,15 @@
|
|
| 1 |
__pycache__
|
| 2 |
.git
|
|
|
|
| 3 |
.venv
|
| 4 |
.pytest_cache
|
| 5 |
dist
|
| 6 |
build
|
| 7 |
outputs
|
| 8 |
-
tests
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
__pycache__
|
| 2 |
.git
|
| 3 |
+
.gitattributes
|
| 4 |
.venv
|
| 5 |
.pytest_cache
|
| 6 |
dist
|
| 7 |
build
|
| 8 |
outputs
|
| 9 |
+
tests
|
| 10 |
+
.env
|
| 11 |
+
.env.*
|
| 12 |
+
*.pem
|
| 13 |
+
*.key
|
| 14 |
+
.vscode
|
| 15 |
+
.idea
|
.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
HF_TOKEN=hf_your_write_or_inference_token_here
|
| 2 |
+
API_BASE_URL=https://router.huggingface.co/v1
|
| 3 |
+
MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
| 4 |
+
BENCHMARK_NAME=model_release_env
|
| 5 |
+
MODEL_RELEASE_MAX_STEPS=8
|
| 6 |
+
MODEL_RELEASE_SUCCESS_THRESHOLD=0.75
|
| 7 |
+
MODEL_RELEASE_TASKS=card_completion_easy,policy_alignment_medium,launch_gate_hard
|
| 8 |
+
LOCAL_IMAGE_NAME=model-release-env:latest
|
| 9 |
+
ENV_BASE_URL=http://localhost:8000
|
.gitignore
CHANGED
|
@@ -2,10 +2,19 @@
|
|
| 2 |
__pycache__/
|
| 3 |
.pytest_cache/
|
| 4 |
*.pyc
|
|
|
|
|
|
|
| 5 |
*.egg-info/
|
| 6 |
dist/
|
| 7 |
build/
|
| 8 |
-
outputs/
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
__pycache__/
|
| 3 |
.pytest_cache/
|
| 4 |
*.pyc
|
| 5 |
+
*.pyo
|
| 6 |
+
*.pyd
|
| 7 |
*.egg-info/
|
| 8 |
dist/
|
| 9 |
build/
|
| 10 |
+
outputs/
|
| 11 |
+
.env
|
| 12 |
+
.env.*
|
| 13 |
+
!.env.example
|
| 14 |
+
*.pem
|
| 15 |
+
*.key
|
| 16 |
+
*.p12
|
| 17 |
+
.vscode/
|
| 18 |
+
.idea/
|
| 19 |
+
*.swp
|
| 20 |
+
*.swo
|
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"]
|
README.md
CHANGED
|
@@ -16,6 +16,8 @@ Model Release Env is an OpenEnv environment for release-readiness decisions arou
|
|
| 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.
|
|
@@ -40,6 +42,8 @@ Each task exposes three evidence documents, a structured release package, and a
|
|
| 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]`.
|
|
@@ -68,12 +72,16 @@ uv run server
|
|
| 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`.
|
|
@@ -94,6 +102,41 @@ If `HF_TOKEN` is missing, `inference.py` uses a deterministic heuristic fallback
|
|
| 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
|
|
|
|
| 16 |
|
| 17 |
The current offline heuristic smoke baseline scores `1.00` on all three tasks, for an average score of `1.00`.
|
| 18 |
|
| 19 |
+
The environment now exposes `critical_gaps` in each observation so an agent can see which launch checks are still failing without reverse-engineering the full checklist.
|
| 20 |
+
|
| 21 |
## Why this is a real environment
|
| 22 |
|
| 23 |
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.
|
|
|
|
| 42 |
|
| 43 |
`submit`: finish the episode and receive the final score.
|
| 44 |
|
| 45 |
+
Observations also include `critical_gaps`, a prioritized list of currently unsatisfied checks. This improves agent ergonomics and makes the environment more useful for RL agents that need dense tactical feedback.
|
| 46 |
+
|
| 47 |
## Reward Design
|
| 48 |
|
| 49 |
The score is a weighted checklist in `[0, 1]`.
|
|
|
|
| 72 |
|
| 73 |
Open the local server at `http://localhost:8000/web`.
|
| 74 |
|
| 75 |
+
If you prefer environment files, copy `.env.example` to `.env` locally. `.env` is ignored by git and docker to avoid leaking tokens.
|
| 76 |
+
|
| 77 |
## Docker
|
| 78 |
|
| 79 |
```bash
|
| 80 |
docker build -t model-release-env:latest -f server/Dockerfile .
|
| 81 |
```
|
| 82 |
|
| 83 |
+
For Hugging Face Docker Spaces, the repository also includes a root `Dockerfile` because Spaces looks for Docker entrypoints at repo root.
|
| 84 |
+
|
| 85 |
## Baseline Runner
|
| 86 |
|
| 87 |
The hackathon runner expects a root-level `inference.py`.
|
|
|
|
| 102 |
|
| 103 |
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.
|
| 104 |
|
| 105 |
+
The baseline runner redacts token-like strings from fallback error messages so model or transport failures do not echo secrets into logs.
|
| 106 |
+
|
| 107 |
+
## Security
|
| 108 |
+
|
| 109 |
+
- Keep tokens only in environment variables or an untracked `.env` file.
|
| 110 |
+
- Never commit credentials. `.env*`, key files, editor metadata, and local build caches are ignored.
|
| 111 |
+
- The Space upload contains only tracked project files; no local secrets or virtualenv artifacts are uploaded.
|
| 112 |
+
- The baseline runner redacts token-like values in fallback logs.
|
| 113 |
+
|
| 114 |
+
## Hugging Face Space
|
| 115 |
+
|
| 116 |
+
Space URL: `https://huggingface.co/spaces/krishnah27/openenv-model-release-env`
|
| 117 |
+
|
| 118 |
+
Public clone URL:
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
git clone https://huggingface.co/spaces/krishnah27/openenv-model-release-env
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
If you need authenticated push access from another machine, use the same clone URL and provide a Hugging Face write token as the git password when prompted.
|
| 125 |
+
|
| 126 |
+
## Submission
|
| 127 |
+
|
| 128 |
+
Submit these two URLs in the hackathon form:
|
| 129 |
+
|
| 130 |
+
1. GitHub repository: `https://github.com/krishnakumarbhat/openenv-model-release-env`
|
| 131 |
+
2. Hugging Face Space: `https://huggingface.co/spaces/krishnah27/openenv-model-release-env`
|
| 132 |
+
|
| 133 |
+
Recommended final checklist before clicking submit:
|
| 134 |
+
|
| 135 |
+
1. Open the GitHub repo and confirm the latest commit is present.
|
| 136 |
+
2. Open the Hugging Face Space and confirm the Docker build starts or finishes successfully.
|
| 137 |
+
3. Verify the root `inference.py` exists in both places.
|
| 138 |
+
4. Submit the GitHub URL and Hugging Face Space URL.
|
| 139 |
+
|
| 140 |
## Example Client Usage
|
| 141 |
|
| 142 |
```python
|
inference.py
CHANGED
|
@@ -32,12 +32,27 @@ except ImportError:
|
|
| 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 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
DEFAULT_TASKS = [
|
| 43 |
"card_completion_easy",
|
|
@@ -137,10 +152,18 @@ def _stderr(message: str) -> None:
|
|
| 137 |
print(message, file=sys.stderr)
|
| 138 |
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
def _llm_client() -> Optional[OpenAI]:
|
| 141 |
-
|
|
|
|
| 142 |
return None
|
| 143 |
-
return OpenAI(base_url=API_BASE_URL, api_key=
|
| 144 |
|
| 145 |
|
| 146 |
def _extract_json_block(content: str) -> Dict[str, Any]:
|
|
@@ -158,6 +181,7 @@ def _observation_prompt(observation: Any) -> str:
|
|
| 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,
|
|
@@ -201,6 +225,8 @@ def _model_action(
|
|
| 201 |
{"role": "user", "content": user_prompt},
|
| 202 |
],
|
| 203 |
)
|
|
|
|
|
|
|
| 204 |
content = response.choices[0].message.content or ""
|
| 205 |
return ModelReleaseAction(**_extract_json_block(content))
|
| 206 |
|
|
@@ -223,7 +249,9 @@ async def _run_task(env: ModelReleaseEnv, task_name: str, llm: Optional[OpenAI])
|
|
| 223 |
else:
|
| 224 |
action = _model_action(llm, task_name, result.observation)
|
| 225 |
except Exception as exc:
|
| 226 |
-
_stderr(
|
|
|
|
|
|
|
| 227 |
action = _heuristic_action(task_name, step_index)
|
| 228 |
|
| 229 |
result = await env.step(action)
|
|
|
|
| 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 |
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME", "model-release-env:latest")
|
| 36 |
ENV_BASE_URL = os.getenv("ENV_BASE_URL")
|
| 37 |
BENCHMARK = os.getenv("BENCHMARK_NAME", "model_release_env")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _get_env_int(name: str, default: str) -> int:
|
| 41 |
+
try:
|
| 42 |
+
return int(os.getenv(name, default))
|
| 43 |
+
except ValueError as exc:
|
| 44 |
+
raise SystemExit(f"Invalid integer for {name}: {exc}") from exc
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _get_env_float(name: str, default: str) -> float:
|
| 48 |
+
try:
|
| 49 |
+
return float(os.getenv(name, default))
|
| 50 |
+
except ValueError as exc:
|
| 51 |
+
raise SystemExit(f"Invalid float for {name}: {exc}") from exc
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
MAX_STEPS = _get_env_int("MODEL_RELEASE_MAX_STEPS", "8")
|
| 55 |
+
SUCCESS_THRESHOLD = _get_env_float("MODEL_RELEASE_SUCCESS_THRESHOLD", "0.75")
|
| 56 |
|
| 57 |
DEFAULT_TASKS = [
|
| 58 |
"card_completion_easy",
|
|
|
|
| 152 |
print(message, file=sys.stderr)
|
| 153 |
|
| 154 |
|
| 155 |
+
def _redact_message(message: str) -> str:
|
| 156 |
+
redacted = re.sub(r"hf_[A-Za-z0-9]+", "hf_[REDACTED]", message)
|
| 157 |
+
redacted = re.sub(r"sk_[A-Za-z0-9]+", "sk_[REDACTED]", redacted)
|
| 158 |
+
redacted = re.sub(r"https://[^\s:@]+:[^\s@]+@", "https://[REDACTED]@", redacted)
|
| 159 |
+
return redacted
|
| 160 |
+
|
| 161 |
+
|
| 162 |
def _llm_client() -> Optional[OpenAI]:
|
| 163 |
+
token = os.getenv("HF_TOKEN")
|
| 164 |
+
if not token:
|
| 165 |
return None
|
| 166 |
+
return OpenAI(base_url=API_BASE_URL, api_key=token)
|
| 167 |
|
| 168 |
|
| 169 |
def _extract_json_block(content: str) -> Dict[str, Any]:
|
|
|
|
| 181 |
"visible_documents": observation.visible_documents,
|
| 182 |
"package_snapshot": observation.package_snapshot,
|
| 183 |
"checklist_status": observation.checklist_status,
|
| 184 |
+
"critical_gaps": observation.critical_gaps,
|
| 185 |
"available_fields": observation.available_fields,
|
| 186 |
"available_decisions": observation.available_decisions,
|
| 187 |
"inspected_documents": observation.inspected_documents,
|
|
|
|
| 225 |
{"role": "user", "content": user_prompt},
|
| 226 |
],
|
| 227 |
)
|
| 228 |
+
if not response.choices:
|
| 229 |
+
raise ValueError("Model returned no choices")
|
| 230 |
content = response.choices[0].message.content or ""
|
| 231 |
return ModelReleaseAction(**_extract_json_block(content))
|
| 232 |
|
|
|
|
| 249 |
else:
|
| 250 |
action = _model_action(llm, task_name, result.observation)
|
| 251 |
except Exception as exc:
|
| 252 |
+
_stderr(
|
| 253 |
+
f"planner fallback for {task_name}: {_redact_message(str(exc))}"
|
| 254 |
+
)
|
| 255 |
action = _heuristic_action(task_name, step_index)
|
| 256 |
|
| 257 |
result = await env.step(action)
|
models.py
CHANGED
|
@@ -52,6 +52,10 @@ class ModelReleaseObservation(Observation):
|
|
| 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",
|
|
|
|
| 52 |
default_factory=dict,
|
| 53 |
description="Progress on deterministic grader checks",
|
| 54 |
)
|
| 55 |
+
critical_gaps: List[str] = Field(
|
| 56 |
+
default_factory=list,
|
| 57 |
+
description="Unsatisfied checks in priority order",
|
| 58 |
+
)
|
| 59 |
available_fields: List[str] = Field(
|
| 60 |
default_factory=list,
|
| 61 |
description="Fields the agent may edit with set_field",
|
server/model_release_env_environment.py
CHANGED
|
@@ -389,6 +389,9 @@ class ModelReleaseEnvironment(
|
|
| 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"],
|
|
@@ -397,6 +400,7 @@ class ModelReleaseEnvironment(
|
|
| 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),
|
|
|
|
| 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 |
+
critical_gaps = [
|
| 393 |
+
name for name, satisfied in self._score_by_check.items() if not satisfied
|
| 394 |
+
]
|
| 395 |
return ModelReleaseObservation(
|
| 396 |
task_name=self._task_name,
|
| 397 |
difficulty=self._task_spec["difficulty"],
|
|
|
|
| 400 |
visible_documents=deepcopy(self._visible_documents),
|
| 401 |
package_snapshot=deepcopy(self._package),
|
| 402 |
checklist_status=deepcopy(self._score_by_check),
|
| 403 |
+
critical_gaps=critical_gaps,
|
| 404 |
available_fields=list(self._task_spec["editable_fields"]),
|
| 405 |
available_decisions=list(self._task_spec["allowed_decisions"]),
|
| 406 |
inspected_documents=sorted(self._inspected_documents),
|