hud-python 0.4.57__py3-none-any.whl → 0.4.59__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/cli/dev.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import contextlib
6
7
  import importlib
7
8
  import importlib.util
8
9
  import logging
@@ -13,6 +14,8 @@ import threading
13
14
  from pathlib import Path
14
15
  from typing import Any
15
16
 
17
+ import typer
18
+
16
19
  from hud.utils.hud_console import HUDConsole
17
20
 
18
21
  hud_console = HUDConsole()
@@ -26,6 +29,7 @@ def show_dev_server_info(
26
29
  interactive: bool,
27
30
  env_dir: Path | None = None,
28
31
  new: bool = False,
32
+ docker_mode: bool = False,
29
33
  ) -> str:
30
34
  """Show consistent server info for both Python and Docker modes.
31
35
 
@@ -54,7 +58,15 @@ def show_dev_server_info(
54
58
  if transport == "http":
55
59
  hud_console.section_title("Quick Links")
56
60
  hud_console.info(f"{hud_console.sym.ITEM} Docs: http://localhost:{port}/docs")
57
- hud_console.info(f"{hud_console.sym.ITEM} Cursor: {cursor_deeplink}")
61
+ hud_console.info(f"{hud_console.sym.ITEM} Cursor:")
62
+ # Display the Cursor link on its own line to prevent wrapping
63
+ hud_console.link(cursor_deeplink)
64
+
65
+ # Show eval endpoint if in Docker mode
66
+ if docker_mode:
67
+ hud_console.info(
68
+ f"{hud_console.sym.ITEM} Eval API: http://localhost:{port}/eval (POST)"
69
+ )
58
70
 
59
71
  # Check for VNC (browser environment)
60
72
  if env_dir and (env_dir / "environment" / "server.py").exists():
@@ -510,6 +522,9 @@ def run_docker_dev_server(
510
522
  new: bool = False,
511
523
  ) -> None:
512
524
  """Run MCP server in Docker with volume mounts, expose via local HTTP proxy."""
525
+ import atexit
526
+ import signal
527
+
513
528
  import typer
514
529
  import yaml
515
530
 
@@ -522,6 +537,69 @@ def run_docker_dev_server(
522
537
 
523
538
  cwd = Path.cwd()
524
539
 
540
+ # Container name will be set later and used for cleanup
541
+ container_name: str | None = None
542
+ cleanup_done = False
543
+
544
+ def cleanup_container() -> None:
545
+ """Clean up Docker container on exit."""
546
+ nonlocal cleanup_done
547
+ if cleanup_done or not container_name:
548
+ return
549
+
550
+ cleanup_done = True
551
+ hud_console.debug(f"Cleaning up container: {container_name}")
552
+
553
+ # Check if container is still running
554
+ try:
555
+ result = subprocess.run( # noqa: S603
556
+ ["docker", "ps", "-q", "-f", f"name={container_name}"], # noqa: S607
557
+ stdout=subprocess.PIPE,
558
+ stderr=subprocess.DEVNULL,
559
+ text=True,
560
+ timeout=5,
561
+ )
562
+ if not result.stdout.strip():
563
+ # Container is not running, just try to remove it
564
+ subprocess.run( # noqa: S603
565
+ ["docker", "rm", "-f", container_name], # noqa: S607
566
+ stdout=subprocess.DEVNULL,
567
+ stderr=subprocess.DEVNULL,
568
+ timeout=5,
569
+ )
570
+ return
571
+ except Exception: # noqa: S110
572
+ pass
573
+
574
+ try:
575
+ # First try to stop gracefully
576
+ subprocess.run( # noqa: S603
577
+ ["docker", "stop", container_name], # noqa: S607
578
+ stdout=subprocess.DEVNULL,
579
+ stderr=subprocess.DEVNULL,
580
+ timeout=10,
581
+ )
582
+ hud_console.debug(f"Container {container_name} stopped successfully")
583
+ except subprocess.TimeoutExpired:
584
+ # Force kill if stop times out
585
+ hud_console.debug(f"Container {container_name} stop timeout, forcing kill")
586
+ with contextlib.suppress(Exception):
587
+ subprocess.run( # noqa: S603
588
+ ["docker", "kill", container_name], # noqa: S607
589
+ stdout=subprocess.DEVNULL,
590
+ stderr=subprocess.DEVNULL,
591
+ timeout=5,
592
+ )
593
+
594
+ # Set up signal handlers for cleanup
595
+ def signal_handler(signum: int, frame: Any) -> None:
596
+ cleanup_container()
597
+ sys.exit(0)
598
+
599
+ signal.signal(signal.SIGTERM, signal_handler)
600
+ if sys.platform != "win32":
601
+ signal.signal(signal.SIGHUP, signal_handler)
602
+
525
603
  # Find environment directory (current or parent with hud.lock.yaml)
526
604
  env_dir = cwd
527
605
  lock_path = env_dir / "hud.lock.yaml"
@@ -562,10 +640,14 @@ def run_docker_dev_server(
562
640
  base_name = image_name.replace(":", "-").replace("/", "-")
563
641
  container_name = f"{base_name}-dev-{pid}"
564
642
 
643
+ # Register cleanup function with atexit
644
+ atexit.register(cleanup_container)
645
+
565
646
  # Build docker run command with volume mounts and folder-mode envs
566
647
  from .utils.docker import create_docker_run_command
567
648
 
568
649
  base_args = [
650
+ "--rm", # Automatically remove container when it stops
569
651
  "--name",
570
652
  container_name,
571
653
  "-v",
@@ -643,6 +725,7 @@ def run_docker_dev_server(
643
725
  interactive=interactive,
644
726
  env_dir=env_dir,
645
727
  new=new,
728
+ docker_mode=True,
646
729
  )
647
730
  hud_console.dim_info(
648
731
  "",
@@ -679,6 +762,11 @@ def run_docker_dev_server(
679
762
  os.environ["_HUD_DEV_DOCKER_CONTAINER"] = container_name
680
763
  hud_console.debug(f"Docker container: {container_name}")
681
764
 
765
+ # Store the docker mcp_config for the eval endpoint
766
+ import json
767
+
768
+ os.environ["_HUD_DEV_DOCKER_MCP_CONFIG"] = json.dumps(mcp_config)
769
+
682
770
  # Create FastMCP proxy using the ProxyClient
683
771
  fastmcp_proxy = FastMCP.as_proxy(proxy_client)
684
772
 
@@ -713,7 +801,15 @@ def run_docker_dev_server(
713
801
  asyncio.run(run_proxy())
714
802
  except KeyboardInterrupt:
715
803
  hud_console.info("\n\nStopping...")
804
+ cleanup_container()
716
805
  raise typer.Exit(0) from None
806
+ except Exception:
807
+ # Ensure cleanup happens on any exception
808
+ cleanup_container()
809
+ raise
810
+ finally:
811
+ # Final cleanup attempt
812
+ cleanup_container()
717
813
 
718
814
 
719
815
  def run_mcp_dev_server(
@@ -732,6 +828,20 @@ def run_mcp_dev_server(
732
828
  docker_args = docker_args or []
733
829
  cwd = Path.cwd()
734
830
 
831
+ # Find an available port if not using stdio transport
832
+ if not stdio:
833
+ from hud.cli.utils.logging import find_free_port
834
+
835
+ actual_port = find_free_port(port)
836
+ if actual_port is None:
837
+ hud_console.error(f"No available ports found starting from {port}")
838
+ raise typer.Exit(1)
839
+
840
+ if actual_port != port:
841
+ hud_console.info(f"Port {port} is in use, using port {actual_port} instead")
842
+
843
+ port = actual_port
844
+
735
845
  # Auto-detect Docker mode if Dockerfile present and no module specified
736
846
  if not docker and module is None and should_use_docker_mode(cwd):
737
847
  hud_console.note("Detected Dockerfile - using Docker mode with volume mounts")
hud/cli/eval.py CHANGED
@@ -53,7 +53,7 @@ def get_available_models() -> list[dict[str, str | None]]:
53
53
  try:
54
54
  from hud.cli.rl import rl_api
55
55
 
56
- hud_console.info("Fetching your models from https://hud.so/models")
56
+ hud_console.info("Fetching your models from https://hud.ai/models")
57
57
  models = rl_api.list_models()
58
58
 
59
59
  # Filter for ready models only and sort by recency
@@ -188,6 +188,24 @@ def build_agent(
188
188
  else:
189
189
  return OperatorAgent(verbose=verbose)
190
190
 
191
+ elif agent_type == AgentType.GEMINI:
192
+ try:
193
+ from hud.agents import GeminiAgent
194
+ except ImportError as e:
195
+ hud_console.error(
196
+ "Gemini agent dependencies are not installed. "
197
+ "Please install with: pip install 'hud-python[agent]'"
198
+ )
199
+ raise typer.Exit(1) from e
200
+
201
+ gemini_kwargs: dict[str, Any] = {
202
+ "model": model or "gemini-2.5-computer-use-preview-10-2025",
203
+ "verbose": verbose,
204
+ }
205
+ if allowed_tools:
206
+ gemini_kwargs["allowed_tools"] = allowed_tools
207
+ return GeminiAgent(**gemini_kwargs)
208
+
191
209
  elif agent_type == AgentType.LITELLM:
192
210
  try:
193
211
  from hud.agents.lite_llm import LiteAgent
@@ -344,6 +362,17 @@ async def run_single_task(
344
362
  agent_config = {"verbose": verbose}
345
363
  if allowed_tools:
346
364
  agent_config["allowed_tools"] = allowed_tools
365
+ elif agent_type == AgentType.GEMINI:
366
+ from hud.agents import GeminiAgent
367
+
368
+ agent_class = GeminiAgent
369
+ agent_config = {
370
+ "model": model or "gemini-2.5-computer-use-preview-10-2025",
371
+ "verbose": verbose,
372
+ "validate_api_key": False,
373
+ }
374
+ if allowed_tools:
375
+ agent_config["allowed_tools"] = allowed_tools
347
376
  elif agent_type == AgentType.LITELLM:
348
377
  from hud.agents.lite_llm import LiteAgent
349
378
 
@@ -534,6 +563,26 @@ async def run_full_dataset(
534
563
  if allowed_tools:
535
564
  agent_config["allowed_tools"] = allowed_tools
536
565
 
566
+ elif agent_type == AgentType.GEMINI:
567
+ try:
568
+ from hud.agents import GeminiAgent
569
+
570
+ agent_class = GeminiAgent
571
+ except ImportError as e:
572
+ hud_console.error(
573
+ "Gemini agent dependencies are not installed. "
574
+ "Please install with: pip install 'hud-python[agent]'"
575
+ )
576
+ raise typer.Exit(1) from e
577
+
578
+ agent_config = {
579
+ "model": model or "gemini-2.5-computer-use-preview-10-2025",
580
+ "verbose": verbose,
581
+ "validate_api_key": False,
582
+ }
583
+ if allowed_tools:
584
+ agent_config["allowed_tools"] = allowed_tools
585
+
537
586
  elif agent_type == AgentType.LITELLM:
538
587
  try:
539
588
  from hud.agents.lite_llm import LiteAgent
@@ -641,7 +690,7 @@ def eval_command(
641
690
  agent: AgentType = typer.Option( # noqa: B008
642
691
  AgentType.CLAUDE,
643
692
  "--agent",
644
- help="Agent backend to use (claude, openai, vllm for local server, or litellm)",
693
+ help="Agent backend to use (claude, gemini, openai, vllm for local servers, or litellm)",
645
694
  ),
646
695
  model: str | None = typer.Option(
647
696
  None,
@@ -757,6 +806,13 @@ def eval_command(
757
806
  "Set it in your environment or run: hud set ANTHROPIC_API_KEY=your-key-here"
758
807
  )
759
808
  raise typer.Exit(1)
809
+ elif agent == AgentType.GEMINI:
810
+ if not settings.gemini_api_key:
811
+ hud_console.error("GEMINI_API_KEY is required for Gemini agent")
812
+ hud_console.info(
813
+ "Set it in your environment or run: hud set GEMINI_API_KEY=your-key-here"
814
+ )
815
+ raise typer.Exit(1)
760
816
  elif agent == AgentType.OPENAI and not settings.openai_api_key:
761
817
  hud_console.error("OPENAI_API_KEY is required for OpenAI agent")
762
818
  hud_console.info("Set it in your environment or run: hud set OPENAI_API_KEY=your-key-here")
@@ -771,7 +827,7 @@ def eval_command(
771
827
  # Check for HUD_API_KEY if using HUD services
772
828
  if not settings.api_key:
773
829
  hud_console.warning("HUD_API_KEY not set. Some features may be limited.")
774
- hud_console.info("Get your API key at: https://hud.so")
830
+ hud_console.info("Get your API key at: https://hud.ai")
775
831
  hud_console.info("Set it in your environment or run: hud set HUD_API_KEY=your-key-here")
776
832
 
777
833
  # Parse allowed tools
hud/cli/flows/dev.py CHANGED
@@ -47,11 +47,11 @@ async def create_dynamic_trace(
47
47
 
48
48
  try:
49
49
  resp = await make_request("POST", url=url, json=payload, api_key=api_key)
50
- # New API returns an id; construct the URL as https://hud.so/trace/{id}
50
+ # New API returns an id; construct the URL as https://hud.ai/trace/{id}
51
51
  trace_id = resp.get("id")
52
52
 
53
53
  if isinstance(trace_id, str) and trace_id:
54
- return trace_id, f"https://hud.so/trace/{trace_id}"
54
+ return trace_id, f"https://hud.ai/trace/{trace_id}"
55
55
  return None, None
56
56
  except Exception as e:
57
57
  # Do not interrupt dev flow
@@ -114,7 +114,9 @@ def show_dev_ui(
114
114
  label = "Base image" if is_docker else "Server"
115
115
  hud_console.info("")
116
116
  hud_console.info(f"{hud_console.sym.ITEM} {label}: {server_name}")
117
- hud_console.info(f"{hud_console.sym.ITEM} Cursor: {cursor_deeplink}")
117
+ hud_console.info(f"{hud_console.sym.ITEM} Cursor:")
118
+ # Display the Cursor link on its own line to prevent wrapping
119
+ hud_console.link(cursor_deeplink)
118
120
  hud_console.info("")
119
121
  hud_console.info(f"{hud_console.sym.SUCCESS} Hot-reload enabled")
120
122
  if is_docker:
hud/cli/init.py CHANGED
@@ -182,17 +182,17 @@ def create_environment(
182
182
 
183
183
  hud_console = HUDConsole()
184
184
 
185
- # Determine environment name/target directory
186
- if name is None:
187
- current_dir = Path.cwd()
188
- name = current_dir.name
189
- target_dir = current_dir
190
- hud_console.info(f"Using current directory name: {name}")
191
- else:
192
- target_dir = Path(directory) / name
193
-
194
185
  # Choose preset
195
186
  preset_normalized = (preset or "").strip().lower() if preset else _prompt_for_preset()
187
+
188
+ # If no name is provided, use the preset name as the environment name
189
+ if name is None:
190
+ name = preset_normalized
191
+ hud_console.info(f"Using preset name as environment name: {name}")
192
+
193
+ # Always create a new directory based on the name
194
+ target_dir = Path.cwd() / name if directory == "." else Path(directory) / name
195
+
196
196
  if preset_normalized not in PRESET_MAP:
197
197
  hud_console.warning(
198
198
  f"Unknown preset '{preset_normalized}', defaulting to 'blank' "
@@ -263,14 +263,10 @@ def create_environment(
263
263
  hud_console.status_item(entry, "added")
264
264
 
265
265
  hud_console.section_title("Next steps")
266
- if target_dir == Path.cwd():
267
- hud_console.info("1. Start development server (with MCP inspector):")
268
- hud_console.command_example("hud dev --inspector")
269
- else:
270
- hud_console.info("1. Enter the directory:")
271
- hud_console.command_example(f"cd {target_dir}")
272
- hud_console.info("\n2. Start development server (with MCP inspector):")
273
- hud_console.command_example("hud dev --inspector")
274
-
266
+ # Since we now almost always create a new directory, show cd command
267
+ hud_console.info("1. Enter the directory:")
268
+ hud_console.command_example(f"cd {target_dir.name}")
269
+ hud_console.info("\n2. Start development server (with MCP inspector):")
270
+ hud_console.command_example("hud dev --inspector")
275
271
  hud_console.info("\n3. Review the README in this preset for specific instructions.")
276
272
  hud_console.info("\n4. Customize as needed.")
hud/cli/push.py CHANGED
@@ -152,7 +152,7 @@ def push_environment(
152
152
  hud_console.error("No HUD API key found")
153
153
  hud_console.warning("A HUD API key is required to push environments.")
154
154
  hud_console.info("\nTo get started:")
155
- hud_console.info("1. Get your API key at: https://hud.so/settings")
155
+ hud_console.info("1. Get your API key at: https://hud.ai/settings")
156
156
  hud_console.info("Set it in your environment or run: hud set HUD_API_KEY=your-key-here")
157
157
  hud_console.command_example("hud push", "Try again")
158
158
  hud_console.info("")
@@ -440,7 +440,7 @@ def push_environment(
440
440
  elif response.status_code == 401:
441
441
  hud_console.error("Authentication failed")
442
442
  hud_console.info("Check your HUD_API_KEY is valid")
443
- hud_console.info("Get a new key at: https://hud.so/settings")
443
+ hud_console.info("Get a new key at: https://hud.ai/settings")
444
444
  hud_console.info("Set it in your environment or run: hud set HUD_API_KEY=your-key-here")
445
445
  elif response.status_code == 403:
446
446
  hud_console.error("Permission denied")
hud/cli/rl/__init__.py CHANGED
@@ -25,7 +25,7 @@ def rl_command(
25
25
  ),
26
26
  model: str | None = typer.Argument(
27
27
  None,
28
- help="Model to train from https://hud.so/models (default: interactive selection)",
28
+ help="Model to train from https://hud.ai/models (default: interactive selection)",
29
29
  ),
30
30
  config_file: Path | None = typer.Option( # noqa: B008
31
31
  None,
hud/cli/rl/celebrate.py CHANGED
@@ -133,7 +133,7 @@ def show_confetti(console: Console, seconds: float = 2.5) -> None:
133
133
  """
134
134
  # Show celebratory message first
135
135
  console.print(
136
- "[bold green]🎉 Starting training! See your model on https://hud.so/models[/bold green]"
136
+ "[bold green]🎉 Starting training! See your model on https://hud.ai/models[/bold green]"
137
137
  )
138
138
  time.sleep(0.3) # Brief pause to see the message
139
139
 
@@ -55,7 +55,7 @@ def ensure_vllm_deployed(
55
55
  hud_console.info("Waiting for vLLM server to be ready...")
56
56
  start_time = time.time()
57
57
  with hud_console.progress() as progress:
58
- progress.update("Checking deployment status (see live status on https://hud.so/models)")
58
+ progress.update("Checking deployment status (see live status on https://hud.ai/models)")
59
59
  while True:
60
60
  if time.time() - start_time > timeout:
61
61
  hud_console.error("Timeout waiting for vLLM deployment")
@@ -139,7 +139,7 @@ def run_remote_training(
139
139
  hud_console.section_title("Model Selection")
140
140
 
141
141
  # Fetch existing models
142
- hud_console.info("Fetching your models from https://hud.so/models")
142
+ hud_console.info("Fetching your models from https://hud.ai/models")
143
143
 
144
144
  try:
145
145
  models = rl_api.list_models()
@@ -312,7 +312,7 @@ def run_remote_training(
312
312
  # gpu_table.add_column("Price/hr", style="yellow")
313
313
 
314
314
  # for gpu, info in GPU_PRICING.items():
315
- # gpu_table.add_row(gpu, info["memory"], "see pricing on hud.so")
315
+ # gpu_table.add_row(gpu, info["memory"], "see pricing on hud.ai")
316
316
 
317
317
  # console.print(gpu_table)
318
318
 
@@ -68,6 +68,26 @@ class TestBuildAgent:
68
68
  )
69
69
  assert result == mock_instance
70
70
 
71
+ def test_builds_gemini_agent(self) -> None:
72
+ """Test building a Gemini agent."""
73
+ with patch("hud.agents.GeminiAgent") as mock_runner:
74
+ mock_instance = Mock()
75
+ mock_runner.return_value = mock_instance
76
+
77
+ result = build_agent(
78
+ AgentType.GEMINI,
79
+ model="gemini-test",
80
+ allowed_tools=["gemini_computer"],
81
+ verbose=True,
82
+ )
83
+
84
+ mock_runner.assert_called_once_with(
85
+ model="gemini-test",
86
+ verbose=True,
87
+ allowed_tools=["gemini_computer"],
88
+ )
89
+ assert result == mock_instance
90
+
71
91
 
72
92
  class TestRunSingleTask:
73
93
  """Test the run_single_task function."""
hud/clients/base.py CHANGED
@@ -140,7 +140,7 @@ class BaseHUDClient(AgentMCPClient):
140
140
  raise HudAuthenticationError(
141
141
  f'Sending authorization "{headers.get("Authorization", "")}", which may'
142
142
  " be incomplete. Ensure HUD_API_KEY environment variable is set or send it"
143
- " as a header. You can get an API key at https://hud.so"
143
+ " as a header. You can get an API key at https://hud.ai"
144
144
  )
145
145
  # Subclasses implement connection
146
146
  await self._connect(self._mcp_config)
hud/clients/fastmcp.py CHANGED
@@ -95,7 +95,7 @@ class FastMCPHUDClient(BaseHUDClient):
95
95
  raise RuntimeError(
96
96
  "Authentication failed for HUD API. "
97
97
  "Please ensure your HUD_API_KEY environment variable is set correctly." # noqa: E501
98
- "You can get an API key at https://hud.so"
98
+ "You can get an API key at https://hud.ai"
99
99
  ) from e
100
100
  # Generic 401 error
101
101
  raise RuntimeError(
hud/otel/config.py CHANGED
@@ -113,7 +113,7 @@ def configure_telemetry(
113
113
  # Error if no exporters are configured
114
114
  raise ValueError(
115
115
  "No telemetry backend configured. Either:\n"
116
- "1. Set HUD_API_KEY environment variable for HUD telemetry (https://hud.so)\n"
116
+ "1. Set HUD_API_KEY environment variable for HUD telemetry (https://hud.ai)\n"
117
117
  "2. Use enable_otlp=True with configure_telemetry() for alternative backends (e.g., Jaeger)\n" # noqa: E501
118
118
  )
119
119
  elif not settings.telemetry_enabled:
hud/otel/context.py CHANGED
@@ -408,7 +408,7 @@ def _print_trace_url(task_run_id: str) -> None:
408
408
  if not (settings.telemetry_enabled and settings.api_key):
409
409
  return
410
410
 
411
- url = f"https://hud.so/trace/{task_run_id}"
411
+ url = f"https://hud.ai/trace/{task_run_id}"
412
412
  header = "🚀 See your agent live at:"
413
413
 
414
414
  # ANSI color codes
@@ -447,7 +447,7 @@ def _print_trace_complete_url(task_run_id: str, error_occurred: bool = False) ->
447
447
  if not (settings.telemetry_enabled and settings.api_key):
448
448
  return
449
449
 
450
- url = f"https://hud.so/trace/{task_run_id}"
450
+ url = f"https://hud.ai/trace/{task_run_id}"
451
451
 
452
452
  # ANSI color codes
453
453
  GREEN = "\033[92m"