hud-python 0.4.29__py3-none-any.whl → 0.4.31__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/base.py +12 -4
- hud/agents/openai_chat_generic.py +2 -1
- hud/cli/flows/tasks.py +185 -0
- hud/cli/init.py +2 -2
- hud/cli/rl/__init__.py +40 -458
- hud/cli/rl/display.py +1 -1
- hud/cli/rl/local_runner.py +571 -0
- hud/cli/rl/remote_runner.py +11 -2
- hud/cli/utils/docker.py +94 -0
- hud/native/comparator.py +6 -6
- hud/native/tests/test_comparator.py +8 -8
- hud/native/tests/test_native_init.py +12 -10
- hud/rl/README.md +2 -3
- hud/rl/learner.py +3 -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.29.dist-info → hud_python-0.4.31.dist-info}/METADATA +26 -27
- {hud_python-0.4.29.dist-info → hud_python-0.4.31.dist-info}/RECORD +23 -22
- {hud_python-0.4.29.dist-info → hud_python-0.4.31.dist-info}/WHEEL +0 -0
- {hud_python-0.4.29.dist-info → hud_python-0.4.31.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.29.dist-info → hud_python-0.4.31.dist-info}/licenses/LICENSE +0 -0
hud/agents/base.py
CHANGED
|
@@ -153,16 +153,24 @@ class MCPAgent(ABC):
|
|
|
153
153
|
if task.setup_tool:
|
|
154
154
|
if isinstance(task.setup_tool, list):
|
|
155
155
|
for tool in task.setup_tool:
|
|
156
|
-
if self.agent_tools
|
|
156
|
+
if not self.agent_tools or (
|
|
157
|
+
self.agent_tools and tool.name not in self.agent_tools
|
|
158
|
+
):
|
|
157
159
|
self.lifecycle_tools.append(tool.name)
|
|
158
|
-
elif self.agent_tools
|
|
160
|
+
elif not self.agent_tools or (
|
|
161
|
+
self.agent_tools and task.setup_tool.name not in self.agent_tools
|
|
162
|
+
):
|
|
159
163
|
self.lifecycle_tools.append(task.setup_tool.name)
|
|
160
164
|
if task.evaluate_tool:
|
|
161
165
|
if isinstance(task.evaluate_tool, list):
|
|
162
166
|
for tool in task.evaluate_tool:
|
|
163
|
-
if self.agent_tools
|
|
167
|
+
if not self.agent_tools or (
|
|
168
|
+
self.agent_tools and tool.name not in self.agent_tools
|
|
169
|
+
):
|
|
164
170
|
self.lifecycle_tools.append(tool.name)
|
|
165
|
-
elif self.agent_tools
|
|
171
|
+
elif not self.agent_tools or (
|
|
172
|
+
self.agent_tools and task.evaluate_tool.name not in self.agent_tools
|
|
173
|
+
):
|
|
166
174
|
self.lifecycle_tools.append(task.evaluate_tool.name)
|
|
167
175
|
if task.system_prompt:
|
|
168
176
|
self.system_prompt += "\n\n" + task.system_prompt
|
|
@@ -230,7 +230,8 @@ class GenericOpenAIChatAgent(MCPAgent):
|
|
|
230
230
|
if msg.tool_calls:
|
|
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)) # noqa: PERF401
|
|
234
235
|
|
|
235
236
|
# Only stop on length (token limit), never on "stop"
|
|
236
237
|
done = choice.finish_reason == "length"
|
hud/cli/flows/tasks.py
CHANGED
|
@@ -0,0 +1,185 @@
|
|
|
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."""
|
|
31
|
+
for task in tasks:
|
|
32
|
+
if not task.mcp_config or (not _is_remote_url(task.mcp_config.get("url", ""))):
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _find_environment_dir(tasks_path: Path) -> Path | None:
|
|
38
|
+
"""Find the environment directory related to a tasks file.
|
|
39
|
+
|
|
40
|
+
Strategy:
|
|
41
|
+
- Prefer a directory containing hud.lock.yaml
|
|
42
|
+
- Fallback to a directory that looks like an environment (Dockerfile + pyproject.toml)
|
|
43
|
+
- Search the tasks file directory, CWD, and a couple of parents
|
|
44
|
+
"""
|
|
45
|
+
candidates: list[Path] = []
|
|
46
|
+
cwd = Path.cwd()
|
|
47
|
+
candidates.extend([tasks_path.parent, cwd])
|
|
48
|
+
|
|
49
|
+
# Add parents (up to 2 levels for each)
|
|
50
|
+
for base in list(candidates):
|
|
51
|
+
p = base
|
|
52
|
+
for _ in range(2):
|
|
53
|
+
p = p.parent
|
|
54
|
+
if p not in candidates:
|
|
55
|
+
candidates.append(p)
|
|
56
|
+
|
|
57
|
+
# Prefer those with hud.lock.yaml
|
|
58
|
+
for d in candidates:
|
|
59
|
+
if (d / "hud.lock.yaml").exists():
|
|
60
|
+
return d
|
|
61
|
+
|
|
62
|
+
# Otherwise, find a plausible environment dir
|
|
63
|
+
for d in candidates:
|
|
64
|
+
try:
|
|
65
|
+
if is_environment_directory(d):
|
|
66
|
+
return d
|
|
67
|
+
except Exception as e:
|
|
68
|
+
hud_console.debug(f"Skipping path {d}: {e}")
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _ensure_built(env_dir: Path) -> dict[str, Any]:
|
|
75
|
+
"""Ensure the environment is built and a lock file exists; return lock data."""
|
|
76
|
+
lock_path = env_dir / "hud.lock.yaml"
|
|
77
|
+
if not lock_path.exists():
|
|
78
|
+
hud_console.warning("No hud.lock.yaml found. The environment hasn't been built.")
|
|
79
|
+
if not hud_console.confirm("Build the environment now (runs 'hud build')?", default=True):
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
# Check Docker availability before attempting a build
|
|
82
|
+
require_docker_running()
|
|
83
|
+
# Run build (non-interactive). If Docker isn't running, this will raise and stop the flow.
|
|
84
|
+
build_environment(str(env_dir))
|
|
85
|
+
|
|
86
|
+
# Load lock file
|
|
87
|
+
with open(lock_path) as f:
|
|
88
|
+
lock_data = yaml.safe_load(f) or {}
|
|
89
|
+
return lock_data
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _ensure_pushed(env_dir: Path, lock_data: dict[str, Any]) -> dict[str, Any]:
|
|
93
|
+
"""Ensure the environment is pushed to a registry; return updated lock data."""
|
|
94
|
+
pushed = bool(lock_data.get("push"))
|
|
95
|
+
if not pushed:
|
|
96
|
+
hud_console.warning("Environment not pushed to a registry yet.")
|
|
97
|
+
if not hud_console.confirm("Push to a registry now (runs 'hud push')?", default=True):
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
# Check Docker availability before attempting a push
|
|
100
|
+
require_docker_running()
|
|
101
|
+
|
|
102
|
+
# If Docker or login is not configured, the push function will fail and halt.
|
|
103
|
+
push_environment(str(env_dir))
|
|
104
|
+
|
|
105
|
+
# Reload lock after push
|
|
106
|
+
lock_path = env_dir / "hud.lock.yaml"
|
|
107
|
+
with open(lock_path) as f:
|
|
108
|
+
lock_data = yaml.safe_load(f) or {}
|
|
109
|
+
|
|
110
|
+
return lock_data
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _derive_remote_image(lock_data: dict[str, Any]) -> str:
|
|
114
|
+
"""Derive org/name:tag from lock file image field for MCP header."""
|
|
115
|
+
image_ref = str(lock_data.get("image", "")).strip()
|
|
116
|
+
if not image_ref:
|
|
117
|
+
raise typer.Exit("Lock file missing image reference")
|
|
118
|
+
name, tag = extract_name_and_tag(image_ref)
|
|
119
|
+
return f"{name}:{tag}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def convert_tasks_to_remote(tasks_file: str) -> str:
|
|
123
|
+
"""Convert a local tasks file to remote MCP tasks and return new filename.
|
|
124
|
+
|
|
125
|
+
Steps:
|
|
126
|
+
1) Find env dir; ensure built (hud.lock.yaml), otherwise build
|
|
127
|
+
2) Ensure pushed to registry, otherwise push
|
|
128
|
+
3) Create remote_[tasks].json with mcp_config pointing to mcp.hud.so and Mcp-Image
|
|
129
|
+
4) Return the new tasks file path
|
|
130
|
+
"""
|
|
131
|
+
tasks_path = Path(tasks_file).resolve()
|
|
132
|
+
|
|
133
|
+
tasks = load_tasks(str(tasks_path))
|
|
134
|
+
|
|
135
|
+
# Ensure HUD_API_KEY is available: prefer process env, else load from env_dir/.env
|
|
136
|
+
from hud.settings import settings
|
|
137
|
+
|
|
138
|
+
if not settings.api_key or not settings.api_key.strip():
|
|
139
|
+
hud_console.error("HUD_API_KEY is not set")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
# Load tasks (supports .json and .jsonl)
|
|
143
|
+
if _validate_tasks(tasks):
|
|
144
|
+
return str(tasks_path)
|
|
145
|
+
|
|
146
|
+
# Locate environment
|
|
147
|
+
env_dir = _find_environment_dir(tasks_path)
|
|
148
|
+
if not env_dir:
|
|
149
|
+
hud_console.error("Could not locate an environment directory (Dockerfile + pyproject.toml)")
|
|
150
|
+
hud_console.hint("Ensure you're in or near your environment folder before running 'hud rl'")
|
|
151
|
+
raise typer.Exit(1)
|
|
152
|
+
|
|
153
|
+
# Ensure built and pushed
|
|
154
|
+
lock_data = _ensure_built(env_dir)
|
|
155
|
+
lock_data = _ensure_pushed(env_dir, lock_data)
|
|
156
|
+
|
|
157
|
+
# Derive remote image name org/name:tag
|
|
158
|
+
remote_image = _derive_remote_image(lock_data)
|
|
159
|
+
|
|
160
|
+
# Convert to list[dict]
|
|
161
|
+
tasks_payload: list[dict[str, Any]] = []
|
|
162
|
+
for t in tasks:
|
|
163
|
+
item = t.model_dump()
|
|
164
|
+
item["mcp_config"] = {
|
|
165
|
+
"hud": {
|
|
166
|
+
"url": "https://mcp.hud.so/v3/mcp",
|
|
167
|
+
"headers": {
|
|
168
|
+
"Authorization": "Bearer ${HUD_API_KEY}",
|
|
169
|
+
"Mcp-Image": remote_image,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
tasks_payload.append(item)
|
|
174
|
+
|
|
175
|
+
# Write new file: remote_<name>.json (always JSON array)
|
|
176
|
+
remote_name = f"remote_{tasks_path.stem}.json"
|
|
177
|
+
remote_path = tasks_path.parent / remote_name
|
|
178
|
+
with open(remote_path, "w", encoding="utf-8") as f:
|
|
179
|
+
json.dump(tasks_payload, f, ensure_ascii=False, indent=2)
|
|
180
|
+
f.write("\n")
|
|
181
|
+
|
|
182
|
+
hud_console.success(f"Created remote tasks file: {remote_path.name}")
|
|
183
|
+
hud_console.hint("Proceeding with RL training on the remote environment")
|
|
184
|
+
|
|
185
|
+
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}
|