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.

@@ -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)) # noqa: PERF401
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=your_hud_api_key_here
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=your_anthropic_api_key_here
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