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 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 and tool.name not in 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 and task.setup_tool.name not in 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 and tool.name not in 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 and task.evaluate_tool.name not in 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
- tool_calls.extend(self._oai_to_mcp(tc))
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=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}