hud-python 0.4.30__py3-none-any.whl → 0.4.32__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/agents/openai_chat_generic.py +1 -1
- hud/cli/flows/tasks.py +253 -0
- hud/cli/init.py +2 -2
- hud/cli/push.py +1 -0
- hud/cli/rl/__init__.py +35 -457
- hud/cli/rl/local_runner.py +571 -0
- hud/cli/rl/remote_runner.py +88 -63
- hud/cli/utils/docker.py +94 -0
- hud/rl/train.py +3 -0
- hud/rl/vllm_adapter.py +32 -14
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.30.dist-info → hud_python-0.4.32.dist-info}/METADATA +26 -27
- {hud_python-0.4.30.dist-info → hud_python-0.4.32.dist-info}/RECORD +17 -16
- {hud_python-0.4.30.dist-info → hud_python-0.4.32.dist-info}/WHEEL +0 -0
- {hud_python-0.4.30.dist-info → hud_python-0.4.32.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.30.dist-info → hud_python-0.4.32.dist-info}/licenses/LICENSE +0 -0
|
@@ -231,7 +231,7 @@ class GenericOpenAIChatAgent(MCPAgent):
|
|
|
231
231
|
for tc in msg.tool_calls:
|
|
232
232
|
if tc.function.name is not None: # type: ignore
|
|
233
233
|
# _oai_to_mcp returns a single MCPToolCall; append it
|
|
234
|
-
tool_calls.append(self._oai_to_mcp(tc))
|
|
234
|
+
tool_calls.append(self._oai_to_mcp(tc)) # noqa: PERF401
|
|
235
235
|
|
|
236
236
|
# Only stop on length (token limit), never on "stop"
|
|
237
237
|
done = choice.finish_reason == "length"
|
hud/cli/flows/tasks.py
CHANGED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from hud.cli.build import build_environment
|
|
12
|
+
from hud.cli.push import push_environment
|
|
13
|
+
from hud.cli.utils.docker import require_docker_running
|
|
14
|
+
from hud.cli.utils.environment import is_environment_directory
|
|
15
|
+
from hud.cli.utils.registry import extract_name_and_tag
|
|
16
|
+
from hud.utils.hud_console import hud_console
|
|
17
|
+
from hud.utils.tasks import load_tasks
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from hud.types import Task
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_remote_url(url: str) -> bool:
|
|
24
|
+
"""Match the remote url."""
|
|
25
|
+
# See if a url is a remote url
|
|
26
|
+
return bool(re.match(r"^(https?:\/\/)?(www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(\/\S*)?$", url))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_tasks(tasks: list[Task]) -> bool:
|
|
30
|
+
"""Validate the tasks file: return True if tasks already reference a remote MCP URL.
|
|
31
|
+
|
|
32
|
+
A task is considered remote if any "url" field anywhere inside mcp_config
|
|
33
|
+
is a valid remote URL (e.g., https://mcp.hud.so/v3/mcp).
|
|
34
|
+
"""
|
|
35
|
+
def _has_remote_url(obj: Any) -> bool:
|
|
36
|
+
if isinstance(obj, dict):
|
|
37
|
+
for k, v in obj.items():
|
|
38
|
+
if k == "url" and isinstance(v, str) and _is_remote_url(v):
|
|
39
|
+
return True
|
|
40
|
+
if _has_remote_url(v):
|
|
41
|
+
return True
|
|
42
|
+
elif isinstance(obj, list):
|
|
43
|
+
for item in obj:
|
|
44
|
+
if _has_remote_url(item):
|
|
45
|
+
return True
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
for task in tasks:
|
|
49
|
+
cfg = task.mcp_config or {}
|
|
50
|
+
if not _has_remote_url(cfg):
|
|
51
|
+
return False
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _find_environment_dir(tasks_path: Path) -> Path | None:
|
|
56
|
+
"""Find the environment directory related to a tasks file.
|
|
57
|
+
|
|
58
|
+
Strategy:
|
|
59
|
+
- Prefer a directory containing hud.lock.yaml
|
|
60
|
+
- Fallback to a directory that looks like an environment (Dockerfile + pyproject.toml)
|
|
61
|
+
- Search the tasks file directory, CWD, and a couple of parents
|
|
62
|
+
"""
|
|
63
|
+
candidates: list[Path] = []
|
|
64
|
+
cwd = Path.cwd()
|
|
65
|
+
candidates.extend([tasks_path.parent, cwd])
|
|
66
|
+
|
|
67
|
+
# Add parents (up to 2 levels for each)
|
|
68
|
+
for base in list(candidates):
|
|
69
|
+
p = base
|
|
70
|
+
for _ in range(2):
|
|
71
|
+
p = p.parent
|
|
72
|
+
if p not in candidates:
|
|
73
|
+
candidates.append(p)
|
|
74
|
+
|
|
75
|
+
# Prefer those with hud.lock.yaml
|
|
76
|
+
for d in candidates:
|
|
77
|
+
if (d / "hud.lock.yaml").exists():
|
|
78
|
+
return d
|
|
79
|
+
|
|
80
|
+
# Otherwise, find a plausible environment dir
|
|
81
|
+
for d in candidates:
|
|
82
|
+
try:
|
|
83
|
+
if is_environment_directory(d):
|
|
84
|
+
return d
|
|
85
|
+
except Exception as e:
|
|
86
|
+
hud_console.debug(f"Skipping path {d}: {e}")
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _ensure_built(env_dir: Path) -> dict[str, Any]:
|
|
93
|
+
"""Ensure the environment is built and a lock file exists; return lock data."""
|
|
94
|
+
lock_path = env_dir / "hud.lock.yaml"
|
|
95
|
+
if not lock_path.exists():
|
|
96
|
+
hud_console.warning("No hud.lock.yaml found. The environment hasn't been built.")
|
|
97
|
+
if not hud_console.confirm("Build the environment now (runs 'hud build')?", default=True):
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
# Check Docker availability before attempting a build
|
|
100
|
+
require_docker_running()
|
|
101
|
+
# Run build (non-interactive). If Docker isn't running, this will raise and stop the flow.
|
|
102
|
+
build_environment(str(env_dir))
|
|
103
|
+
|
|
104
|
+
# Load lock file
|
|
105
|
+
with open(lock_path) as f:
|
|
106
|
+
lock_data = yaml.safe_load(f) or {}
|
|
107
|
+
return lock_data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _ensure_pushed(env_dir: Path, lock_data: dict[str, Any]) -> dict[str, Any]:
|
|
111
|
+
"""Ensure the environment is pushed to a registry; return updated lock data."""
|
|
112
|
+
pushed = bool(lock_data.get("push"))
|
|
113
|
+
if not pushed:
|
|
114
|
+
hud_console.warning("Environment not pushed to a registry yet.")
|
|
115
|
+
if not hud_console.confirm("Push to a registry now (runs 'hud push')?", default=True):
|
|
116
|
+
raise typer.Exit(1)
|
|
117
|
+
# Check Docker availability before attempting a push
|
|
118
|
+
require_docker_running()
|
|
119
|
+
|
|
120
|
+
# If Docker or login is not configured, the push function will fail and halt.
|
|
121
|
+
push_environment(str(env_dir), yes=True)
|
|
122
|
+
|
|
123
|
+
# Reload lock after push
|
|
124
|
+
lock_path = env_dir / "hud.lock.yaml"
|
|
125
|
+
with open(lock_path) as f:
|
|
126
|
+
lock_data = yaml.safe_load(f) or {}
|
|
127
|
+
|
|
128
|
+
return lock_data
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _derive_remote_image(lock_data: dict[str, Any]) -> str:
|
|
132
|
+
"""Derive org/name:tag from lock file for MCP header.
|
|
133
|
+
|
|
134
|
+
Preference order:
|
|
135
|
+
1) lock_data["push"]["image_with_tag"] if present
|
|
136
|
+
2) Derive from lock_data["image"] (may be a digest; falls back to latest)
|
|
137
|
+
"""
|
|
138
|
+
push_info = lock_data.get("push", {}) if isinstance(lock_data, dict) else {}
|
|
139
|
+
|
|
140
|
+
# 1) Exact image_with_tag if present
|
|
141
|
+
pushed_with_tag = str(push_info.get("image_with_tag", "")).strip()
|
|
142
|
+
if pushed_with_tag:
|
|
143
|
+
name, tag = extract_name_and_tag(pushed_with_tag)
|
|
144
|
+
return f"{name}:{tag}"
|
|
145
|
+
|
|
146
|
+
# Base name always comes from lock_data.image to preserve org/repo
|
|
147
|
+
image_ref = str(lock_data.get("image", "")).strip()
|
|
148
|
+
if not image_ref:
|
|
149
|
+
raise typer.Exit("Lock file missing image reference")
|
|
150
|
+
name, tag = extract_name_and_tag(image_ref)
|
|
151
|
+
return f"{name}:{tag}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def convert_tasks_to_remote(tasks_file: str) -> str:
|
|
155
|
+
"""Convert a local tasks file to remote MCP tasks and return new filename.
|
|
156
|
+
|
|
157
|
+
Steps:
|
|
158
|
+
1) Find env dir; ensure built (hud.lock.yaml), otherwise build
|
|
159
|
+
2) Ensure pushed to registry, otherwise push
|
|
160
|
+
3) Create remote_[tasks].json with mcp_config pointing to mcp.hud.so and Mcp-Image
|
|
161
|
+
4) Return the new tasks file path
|
|
162
|
+
"""
|
|
163
|
+
tasks_path = Path(tasks_file).resolve()
|
|
164
|
+
|
|
165
|
+
tasks = load_tasks(str(tasks_path))
|
|
166
|
+
|
|
167
|
+
# Ensure HUD_API_KEY is available: prefer process env, else load from env_dir/.env
|
|
168
|
+
from hud.settings import settings
|
|
169
|
+
|
|
170
|
+
if not settings.api_key or not settings.api_key.strip():
|
|
171
|
+
hud_console.error("HUD_API_KEY is not set")
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
|
|
174
|
+
# Load tasks (supports .json and .jsonl)
|
|
175
|
+
if _validate_tasks(tasks):
|
|
176
|
+
return str(tasks_path)
|
|
177
|
+
|
|
178
|
+
# Locate environment
|
|
179
|
+
env_dir = _find_environment_dir(tasks_path)
|
|
180
|
+
if not env_dir:
|
|
181
|
+
hud_console.error("Could not locate an environment directory (Dockerfile + pyproject.toml)")
|
|
182
|
+
hud_console.hint("Ensure you're in or near your environment folder before running 'hud rl'")
|
|
183
|
+
raise typer.Exit(1)
|
|
184
|
+
|
|
185
|
+
# Ensure built and pushed
|
|
186
|
+
lock_data = _ensure_built(env_dir)
|
|
187
|
+
lock_data = _ensure_pushed(env_dir, lock_data)
|
|
188
|
+
|
|
189
|
+
# Derive remote image name org/name:tag
|
|
190
|
+
remote_image = _derive_remote_image(lock_data)
|
|
191
|
+
|
|
192
|
+
# Helper to strip extra fields from tool calls
|
|
193
|
+
def _simplify_tool_call(tool: Any) -> Any:
|
|
194
|
+
def _one(x: Any) -> dict[str, Any]:
|
|
195
|
+
try:
|
|
196
|
+
data = x.model_dump() if hasattr(x, "model_dump") else dict(x)
|
|
197
|
+
except Exception:
|
|
198
|
+
try:
|
|
199
|
+
data = dict(x)
|
|
200
|
+
except Exception:
|
|
201
|
+
return {}
|
|
202
|
+
# Keep only name and arguments
|
|
203
|
+
name = data.get("name")
|
|
204
|
+
arguments = data.get("arguments", {})
|
|
205
|
+
return {"name": name, "arguments": arguments}
|
|
206
|
+
|
|
207
|
+
if tool is None:
|
|
208
|
+
return None
|
|
209
|
+
if isinstance(tool, list):
|
|
210
|
+
return [_one(x) for x in tool]
|
|
211
|
+
return _one(tool)
|
|
212
|
+
|
|
213
|
+
# Convert to list[dict]
|
|
214
|
+
tasks_payload: list[dict[str, Any]] = []
|
|
215
|
+
for t in tasks:
|
|
216
|
+
item: dict[str, Any] = {
|
|
217
|
+
"prompt": t.prompt,
|
|
218
|
+
"mcp_config": {
|
|
219
|
+
"hud": {
|
|
220
|
+
"url": "https://mcp.hud.so/v3/mcp",
|
|
221
|
+
"headers": {
|
|
222
|
+
"Authorization": "Bearer ${HUD_API_KEY}",
|
|
223
|
+
"Mcp-Image": remote_image,
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Optional fields, omit Nones
|
|
230
|
+
if t.setup_tool is not None:
|
|
231
|
+
item["setup_tool"] = _simplify_tool_call(t.setup_tool)
|
|
232
|
+
if t.evaluate_tool is not None:
|
|
233
|
+
item["evaluate_tool"] = _simplify_tool_call(t.evaluate_tool)
|
|
234
|
+
if t.agent_tools is not None:
|
|
235
|
+
item["agent_tools"] = t.agent_tools
|
|
236
|
+
if t.system_prompt is not None:
|
|
237
|
+
item["system_prompt"] = t.system_prompt
|
|
238
|
+
if t.metadata:
|
|
239
|
+
item["metadata"] = t.metadata
|
|
240
|
+
|
|
241
|
+
tasks_payload.append(item)
|
|
242
|
+
|
|
243
|
+
# Write new file: remote_<name>.json (always JSON array)
|
|
244
|
+
remote_name = f"remote_{tasks_path.stem}.json"
|
|
245
|
+
remote_path = tasks_path.parent / remote_name
|
|
246
|
+
with open(remote_path, "w", encoding="utf-8") as f:
|
|
247
|
+
json.dump(tasks_payload, f, ensure_ascii=False, indent=2)
|
|
248
|
+
f.write("\n")
|
|
249
|
+
|
|
250
|
+
hud_console.success(f"Created remote tasks file: {remote_path.name}")
|
|
251
|
+
hud_console.hint("Proceeding with RL training on the remote environment")
|
|
252
|
+
|
|
253
|
+
return str(remote_path)
|
hud/cli/init.py
CHANGED
|
@@ -433,11 +433,11 @@ NOTEBOOK_TEMPLATE = """{{
|
|
|
433
433
|
|
|
434
434
|
ENV_FILE_TEMPLATE = """# HUD API Configuration
|
|
435
435
|
# Get your API key from https://app.hud.so/account
|
|
436
|
-
HUD_API_KEY=
|
|
436
|
+
HUD_API_KEY=""
|
|
437
437
|
|
|
438
438
|
# Anthropic API Configuration (optional)
|
|
439
439
|
# Required for using Claude agents - get from https://console.anthropic.com/
|
|
440
|
-
ANTHROPIC_API_KEY=
|
|
440
|
+
ANTHROPIC_API_KEY=""
|
|
441
441
|
"""
|
|
442
442
|
|
|
443
443
|
README_TEMPLATE = """# {title}
|
hud/cli/push.py
CHANGED
|
@@ -332,6 +332,7 @@ def push_environment(
|
|
|
332
332
|
"source": local_image,
|
|
333
333
|
"pushedAt": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
334
334
|
"registry": pushed_digest.split("/")[0] if "/" in pushed_digest else "docker.io",
|
|
335
|
+
"image_with_tag": image,
|
|
335
336
|
}
|
|
336
337
|
|
|
337
338
|
# Save updated lock file
|