Skip to content

Commit 75d354e

Browse files
committed
Merge remote-tracking branch 'origin/main' into pi-hf-sandbox-backend
2 parents daa57f2 + 60ecc7b commit 75d354e

10 files changed

Lines changed: 191 additions & 170 deletions

File tree

src/openenv/cli/commands/collect.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -86,34 +86,35 @@ def _resolve_api_key(provider: str) -> str:
8686
)
8787

8888

89+
def _extract_legal_actions(messages: list[dict[str, Any]]) -> list[int]:
90+
for message in reversed(messages):
91+
content = message.get("content") or ""
92+
if not isinstance(content, str):
93+
continue
94+
try:
95+
payload = json.loads(content)
96+
except (json.JSONDecodeError, ValueError):
97+
payload = None
98+
if isinstance(payload, dict) and "legal_actions" in payload:
99+
legal = payload["legal_actions"]
100+
if isinstance(legal, list):
101+
return [int(a) for a in legal]
102+
match = re.search(r"Legal actions:\s*\[([^\]]*)\]", content)
103+
if match:
104+
raw = match.group(1).strip()
105+
if not raw:
106+
return []
107+
return [int(x) for x in raw.split(",")]
108+
return []
109+
110+
89111
def _build_scripted_model_step():
90112
"""Default teacher: pick the first legal action from the latest observation.
91113
92114
Matches the scripted teacher in ``examples/ttt_collect_demo.py``. Lets
93115
users smoke-test the CLI without any API key configured.
94116
"""
95117

96-
def _extract_legal_actions(messages: list[dict[str, Any]]) -> list[int]:
97-
for message in reversed(messages):
98-
content = message.get("content") or ""
99-
if not isinstance(content, str):
100-
continue
101-
try:
102-
payload = json.loads(content)
103-
except (json.JSONDecodeError, ValueError):
104-
payload = None
105-
if isinstance(payload, dict) and "legal_actions" in payload:
106-
legal = payload["legal_actions"]
107-
if isinstance(legal, list):
108-
return [int(a) for a in legal]
109-
match = re.search(r"Legal actions:\s*\[([^\]]*)\]", content)
110-
if match:
111-
raw = match.group(1).strip()
112-
if not raw:
113-
return []
114-
return [int(x) for x in raw.split(",")]
115-
return []
116-
117118
def model_step(messages, tools, sampling):
118119
del sampling
119120
legal = _extract_legal_actions(messages)

src/openenv/cli/importers/base.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,42 @@ def qualified_name(self) -> str:
257257
return f"{self.module_path}:{self.class_name}"
258258

259259

260+
@dataclass(frozen=True)
261+
class ImportPackageContext:
262+
"""Shared details produced while scaffolding an imported environment package."""
263+
264+
class_name_prefix: str
265+
vendor_dir: str
266+
267+
268+
def prepare_import_package(
269+
source: Path, destination: Path, env_name: str
270+
) -> ImportPackageContext:
271+
from openenv.cli.commands.init import (
272+
_copy_template_directory,
273+
_create_template_replacements,
274+
)
275+
276+
replacements = _create_template_replacements(env_name)
277+
_copy_template_directory(
278+
"openenv.cli.templates.openenv_env",
279+
"",
280+
destination,
281+
replacements,
282+
env_name,
283+
)
284+
285+
vendor_dir = safe_vendor_dir_name(source)
286+
vendor_path = destination / "vendor" / vendor_dir
287+
copy_source_tree(source, vendor_path)
288+
ensure_vendor_package(vendor_path)
289+
290+
return ImportPackageContext(
291+
class_name_prefix=replacements["__ENV_CLASS_NAME__"],
292+
vendor_dir=vendor_dir,
293+
)
294+
295+
260296
class EnvironmentImporter(Protocol):
261297
source_type: str
262298

src/openenv/cli/importers/ors.py

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
from .base import (
99
append_dependency_files,
1010
collect_source_dependencies,
11-
copy_source_tree,
1211
DetectedEnvironment,
13-
ensure_vendor_package,
1412
iter_python_files,
1513
module_path,
14+
prepare_import_package,
1615
render_importer_template,
17-
safe_vendor_dir_name,
1816
write_text,
1917
)
2018

@@ -209,39 +207,20 @@ def generate(
209207
env_name: str,
210208
detected: DetectedEnvironment,
211209
) -> None:
212-
from openenv.cli.commands.init import (
213-
_copy_template_directory,
214-
_create_template_replacements,
215-
)
216-
217-
replacements = _create_template_replacements(env_name)
218-
_copy_template_directory(
219-
"openenv.cli.templates.openenv_env",
220-
"",
221-
destination,
222-
replacements,
223-
env_name,
224-
)
225-
226-
vendor_dir = safe_vendor_dir_name(source)
227-
vendor_path = destination / "vendor" / vendor_dir
228-
copy_source_tree(source, vendor_path)
229-
ensure_vendor_package(vendor_path)
230-
231-
prefix = replacements["__ENV_CLASS_NAME__"]
210+
package = prepare_import_package(source, destination, env_name)
232211
write_text(
233212
destination / "server" / f"{env_name}_environment.py",
234213
_wrapper_source(
235214
env_name=env_name,
236-
class_name_prefix=prefix,
215+
class_name_prefix=package.class_name_prefix,
237216
source_module=detected.module_path,
238217
source_class=detected.class_name,
239-
vendor_dir=vendor_dir,
218+
vendor_dir=package.vendor_dir,
240219
),
241220
)
242221
write_text(
243222
destination / "server" / "app.py",
244-
_app_source(env_name=env_name, class_name_prefix=prefix),
223+
_app_source(env_name=env_name, class_name_prefix=package.class_name_prefix),
245224
)
246225
dependencies = collect_source_dependencies(source)
247226
for dependency in detect_ors_dependencies(source):

src/openenv/cli/importers/verifiers.py

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
from .base import (
99
append_dependency_files,
1010
collect_source_dependencies,
11-
copy_source_tree,
1211
DetectedEnvironment,
13-
ensure_vendor_package,
1412
iter_python_files,
1513
module_path,
14+
prepare_import_package,
1615
render_importer_template,
17-
safe_vendor_dir_name,
1816
write_text,
1917
)
2018

@@ -110,38 +108,19 @@ def generate(
110108
env_name: str,
111109
detected: DetectedEnvironment,
112110
) -> None:
113-
from openenv.cli.commands.init import (
114-
_copy_template_directory,
115-
_create_template_replacements,
116-
)
117-
118-
replacements = _create_template_replacements(env_name)
119-
_copy_template_directory(
120-
"openenv.cli.templates.openenv_env",
121-
"",
122-
destination,
123-
replacements,
124-
env_name,
125-
)
126-
127-
vendor_dir = safe_vendor_dir_name(source)
128-
vendor_path = destination / "vendor" / vendor_dir
129-
copy_source_tree(source, vendor_path)
130-
ensure_vendor_package(vendor_path)
131-
132-
prefix = replacements["__ENV_CLASS_NAME__"]
111+
package = prepare_import_package(source, destination, env_name)
133112
write_text(
134113
destination / "server" / f"{env_name}_environment.py",
135114
_wrapper_source(
136115
env_name=env_name,
137-
class_name_prefix=prefix,
116+
class_name_prefix=package.class_name_prefix,
138117
source_module=detected.module_path,
139-
vendor_dir=vendor_dir,
118+
vendor_dir=package.vendor_dir,
140119
),
141120
)
142121
write_text(
143122
destination / "server" / "app.py",
144-
_app_source(env_name=env_name, class_name_prefix=prefix),
123+
_app_source(env_name=env_name, class_name_prefix=package.class_name_prefix),
145124
)
146125
dependencies = collect_source_dependencies(source)
147126
if "verifiers>=0.1.14" not in dependencies:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
3+
"""Shared helpers for reading environment server configuration."""
4+
5+
from __future__ import annotations
6+
7+
import json
8+
import re
9+
10+
import yaml
11+
12+
13+
def parse_openenv_app_field(yaml_content: str) -> str | None:
14+
"""Extract the top-level ``app`` value from raw ``openenv.yaml`` content."""
15+
try:
16+
data = yaml.safe_load(yaml_content) or {}
17+
except Exception:
18+
return None
19+
20+
if not isinstance(data, dict):
21+
return None
22+
23+
value = data.get("app")
24+
if isinstance(value, str):
25+
value = value.strip()
26+
return value if value else None
27+
return None
28+
29+
30+
def parse_dockerfile_cmd(dockerfile_content: str) -> str | None:
31+
"""Extract the server command from the last Dockerfile ``CMD``."""
32+
last_cmd: str | None = None
33+
for line in dockerfile_content.splitlines():
34+
stripped = line.strip()
35+
if stripped.startswith("#"):
36+
continue
37+
match = re.match(r"CMD\s+(.+)", stripped, flags=re.IGNORECASE)
38+
if match:
39+
last_cmd = match.group(1).strip()
40+
41+
if last_cmd is None:
42+
return None
43+
44+
if last_cmd.startswith("["):
45+
try:
46+
parts = json.loads(last_cmd)
47+
if isinstance(parts, list) and all(isinstance(p, str) for p in parts):
48+
return " ".join(parts)
49+
except (json.JSONDecodeError, TypeError):
50+
# Invalid exec-form JSON falls back to the raw CMD below.
51+
pass
52+
53+
return last_cmd if last_cmd else None

src/openenv/core/containers/runtime/daytona_provider.py

Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@
88

99
from __future__ import annotations
1010

11-
import json
1211
import os
1312
import shlex
1413
import time
1514
from typing import Any, Callable, Dict, Optional
1615

17-
import yaml
18-
16+
from ._server_config import parse_dockerfile_cmd, parse_openenv_app_field
1917
from .providers import ContainerProvider
2018

2119

@@ -149,19 +147,7 @@ def _parse_app_field(yaml_content: str) -> Optional[str]:
149147
150148
Uses PyYAML to handle comments, quotes, and nested keys correctly.
151149
"""
152-
try:
153-
data = yaml.safe_load(yaml_content) or {}
154-
except Exception:
155-
return None
156-
157-
if not isinstance(data, dict):
158-
return None
159-
160-
value = data.get("app")
161-
if isinstance(value, str):
162-
value = value.strip()
163-
return value if value else None
164-
return None
150+
return parse_openenv_app_field(yaml_content)
165151

166152
@staticmethod
167153
def _parse_dockerfile_cmd(dockerfile_content: str) -> Optional[str]:
@@ -176,32 +162,7 @@ def _parse_dockerfile_cmd(dockerfile_content: str) -> Optional[str]:
176162
Returns:
177163
The command as a single string, or ``None`` if no ``CMD`` found.
178164
"""
179-
import re
180-
181-
last_cmd: Optional[str] = None
182-
for line in dockerfile_content.splitlines():
183-
stripped = line.strip()
184-
# Skip comments
185-
if stripped.startswith("#"):
186-
continue
187-
match = re.match(r"CMD\s+(.+)", stripped, flags=re.IGNORECASE)
188-
if match:
189-
last_cmd = match.group(1).strip()
190-
191-
if last_cmd is None:
192-
return None
193-
194-
# Exec form: CMD ["executable", "param1", ...]
195-
if last_cmd.startswith("["):
196-
try:
197-
parts = json.loads(last_cmd)
198-
if isinstance(parts, list) and all(isinstance(p, str) for p in parts):
199-
return " ".join(parts)
200-
except (json.JSONDecodeError, TypeError):
201-
pass
202-
203-
# Shell form: CMD executable param1 ...
204-
return last_cmd if last_cmd else None
165+
return parse_dockerfile_cmd(dockerfile_content)
205166

206167
@staticmethod
207168
def strip_buildkit_syntax(dockerfile_content: str) -> str:

0 commit comments

Comments
 (0)