hud-python 0.4.54__py3-none-any.whl → 0.4.56__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 +8 -0
- hud/agents/claude.py +4 -3
- hud/agents/openai.py +2 -1
- hud/agents/openai_chat_generic.py +3 -2
- hud/agents/tests/test_claude.py +2 -2
- hud/agents/tests/test_openai.py +1 -1
- hud/agents/utils.py +50 -0
- hud/cli/__init__.py +52 -1
- hud/cli/build.py +185 -25
- hud/cli/dev.py +129 -39
- hud/cli/eval.py +99 -1
- hud/cli/flows/dev.py +155 -0
- hud/cli/flows/tasks.py +29 -9
- hud/cli/init.py +3 -1
- hud/cli/utils/docker.py +6 -3
- hud/clients/base.py +2 -2
- hud/otel/context.py +42 -1
- hud/server/server.py +29 -3
- hud/settings.py +6 -0
- hud/telemetry/async_context.py +16 -2
- hud/telemetry/trace.py +6 -1
- hud/utils/group_eval.py +14 -2
- hud/utils/tests/test_agent_factories.py +2 -1
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.54.dist-info → hud_python-0.4.56.dist-info}/METADATA +1 -1
- {hud_python-0.4.54.dist-info → hud_python-0.4.56.dist-info}/RECORD +30 -28
- {hud_python-0.4.54.dist-info → hud_python-0.4.56.dist-info}/WHEEL +0 -0
- {hud_python-0.4.54.dist-info → hud_python-0.4.56.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.54.dist-info → hud_python-0.4.56.dist-info}/licenses/LICENSE +0 -0
hud/cli/dev.py
CHANGED
|
@@ -25,6 +25,7 @@ def show_dev_server_info(
|
|
|
25
25
|
inspector: bool,
|
|
26
26
|
interactive: bool,
|
|
27
27
|
env_dir: Path | None = None,
|
|
28
|
+
new: bool = False,
|
|
28
29
|
) -> str:
|
|
29
30
|
"""Show consistent server info for both Python and Docker modes.
|
|
30
31
|
|
|
@@ -125,6 +126,7 @@ async def run_mcp_module(
|
|
|
125
126
|
verbose: bool,
|
|
126
127
|
inspector: bool,
|
|
127
128
|
interactive: bool,
|
|
129
|
+
new: bool = False,
|
|
128
130
|
) -> None:
|
|
129
131
|
"""Run an MCP module directly."""
|
|
130
132
|
# Check if this is a reload (not first run)
|
|
@@ -222,14 +224,53 @@ async def run_mcp_module(
|
|
|
222
224
|
|
|
223
225
|
# Show server info only on first run
|
|
224
226
|
if not is_reload:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
# Try dynamic trace first for HTTP mode (only if --new)
|
|
228
|
+
live_trace_url: str | None = None
|
|
229
|
+
if transport == "http" and new:
|
|
230
|
+
try:
|
|
231
|
+
local_mcp_config: dict[str, dict[str, Any]] = {
|
|
232
|
+
"hud": {
|
|
233
|
+
"url": f"http://localhost:{port}/mcp",
|
|
234
|
+
"headers": {},
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
from hud.cli.flows.dev import create_dynamic_trace
|
|
239
|
+
|
|
240
|
+
live_trace_url = await create_dynamic_trace(
|
|
241
|
+
mcp_config=local_mcp_config,
|
|
242
|
+
build_status=False,
|
|
243
|
+
environment_name=mcp_server.name or "mcp-server",
|
|
244
|
+
)
|
|
245
|
+
except Exception: # noqa: S110
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
# Show UI using shared flow logic
|
|
249
|
+
if transport == "http" and live_trace_url and new:
|
|
250
|
+
# Minimal UI with live trace
|
|
251
|
+
from hud.cli.flows.dev import generate_cursor_deeplink, show_dev_ui
|
|
252
|
+
|
|
253
|
+
server_name = mcp_server.name or "mcp-server"
|
|
254
|
+
cursor_deeplink = generate_cursor_deeplink(server_name, port)
|
|
255
|
+
|
|
256
|
+
show_dev_ui(
|
|
257
|
+
live_trace_url=live_trace_url,
|
|
258
|
+
server_name=server_name,
|
|
259
|
+
port=port,
|
|
260
|
+
cursor_deeplink=cursor_deeplink,
|
|
261
|
+
is_docker=False,
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
# Full UI for HTTP without trace, or stdio mode
|
|
265
|
+
show_dev_server_info(
|
|
266
|
+
server_name=mcp_server.name or "mcp-server",
|
|
267
|
+
port=port,
|
|
268
|
+
transport=transport,
|
|
269
|
+
inspector=inspector,
|
|
270
|
+
interactive=interactive,
|
|
271
|
+
env_dir=Path.cwd().parent if (Path.cwd().parent / "environment").exists() else None,
|
|
272
|
+
new=new,
|
|
273
|
+
)
|
|
233
274
|
|
|
234
275
|
# Check if there's an environment backend and remind user to start it (first run only)
|
|
235
276
|
if not is_reload:
|
|
@@ -238,7 +279,8 @@ async def run_mcp_module(
|
|
|
238
279
|
if env_dir.exists() and (env_dir / "server.py").exists():
|
|
239
280
|
hud_console.info("")
|
|
240
281
|
hud_console.info(
|
|
241
|
-
f"{hud_console.sym.FLOW} Don't forget to start the environment backend in another
|
|
282
|
+
f"{hud_console.sym.FLOW} Don't forget to start the environment backend in another "
|
|
283
|
+
"terminal:"
|
|
242
284
|
)
|
|
243
285
|
hud_console.info(" cd environment && uv run python uvicorn server:app --reload")
|
|
244
286
|
|
|
@@ -347,6 +389,7 @@ def run_with_reload(
|
|
|
347
389
|
verbose: bool,
|
|
348
390
|
inspector: bool,
|
|
349
391
|
interactive: bool,
|
|
392
|
+
new: bool = False,
|
|
350
393
|
) -> None:
|
|
351
394
|
"""Run module with file watching and auto-reload."""
|
|
352
395
|
try:
|
|
@@ -389,6 +432,11 @@ def run_with_reload(
|
|
|
389
432
|
|
|
390
433
|
if verbose:
|
|
391
434
|
cmd.append("--verbose")
|
|
435
|
+
|
|
436
|
+
if new:
|
|
437
|
+
cmd.append("--new")
|
|
438
|
+
|
|
439
|
+
if verbose:
|
|
392
440
|
hud_console.info(f"Starting: {' '.join(cmd)}")
|
|
393
441
|
|
|
394
442
|
# Mark as reload after first run to suppress logs
|
|
@@ -454,7 +502,12 @@ def run_with_reload(
|
|
|
454
502
|
|
|
455
503
|
|
|
456
504
|
def run_docker_dev_server(
|
|
457
|
-
port: int,
|
|
505
|
+
port: int,
|
|
506
|
+
verbose: bool,
|
|
507
|
+
inspector: bool,
|
|
508
|
+
interactive: bool,
|
|
509
|
+
docker_args: list[str],
|
|
510
|
+
new: bool = False,
|
|
458
511
|
) -> None:
|
|
459
512
|
"""Run MCP server in Docker with volume mounts, expose via local HTTP proxy."""
|
|
460
513
|
import typer
|
|
@@ -462,6 +515,11 @@ def run_docker_dev_server(
|
|
|
462
515
|
|
|
463
516
|
from hud.server import MCPServer
|
|
464
517
|
|
|
518
|
+
# Ensure Docker CLI and daemon are available before proceeding
|
|
519
|
+
from .utils.docker import require_docker_running
|
|
520
|
+
|
|
521
|
+
require_docker_running()
|
|
522
|
+
|
|
465
523
|
cwd = Path.cwd()
|
|
466
524
|
|
|
467
525
|
# Find environment directory (current or parent with hud.lock.yaml)
|
|
@@ -528,15 +586,6 @@ def run_docker_dev_server(
|
|
|
528
586
|
env_dir=env_dir,
|
|
529
587
|
)
|
|
530
588
|
|
|
531
|
-
# Env flags already injected by create_docker_run_command
|
|
532
|
-
|
|
533
|
-
# Print startup info
|
|
534
|
-
hud_console.header("HUD Development Mode (Docker)")
|
|
535
|
-
|
|
536
|
-
if verbose:
|
|
537
|
-
hud_console.section_title("Docker Command")
|
|
538
|
-
hud_console.info(" ".join(docker_cmd))
|
|
539
|
-
|
|
540
589
|
# Create MCP config pointing to the Docker container's stdio
|
|
541
590
|
mcp_config = {
|
|
542
591
|
"docker": {
|
|
@@ -545,15 +594,62 @@ def run_docker_dev_server(
|
|
|
545
594
|
}
|
|
546
595
|
}
|
|
547
596
|
|
|
548
|
-
#
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
597
|
+
# Attempt to create dynamic trace early (before any UI)
|
|
598
|
+
import asyncio as _asy
|
|
599
|
+
|
|
600
|
+
from hud.cli.flows.dev import create_dynamic_trace, generate_cursor_deeplink, show_dev_ui
|
|
601
|
+
|
|
602
|
+
live_trace_url: str | None = None
|
|
603
|
+
if new:
|
|
604
|
+
try:
|
|
605
|
+
local_mcp_config: dict[str, dict[str, Any]] = {
|
|
606
|
+
"hud": {
|
|
607
|
+
"url": f"http://localhost:{port}/mcp",
|
|
608
|
+
"headers": {},
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
live_trace_url = _asy.run(
|
|
612
|
+
create_dynamic_trace(
|
|
613
|
+
mcp_config=local_mcp_config,
|
|
614
|
+
build_status=True,
|
|
615
|
+
environment_name=image_name,
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
except Exception: # noqa: S110
|
|
619
|
+
pass
|
|
620
|
+
|
|
621
|
+
# Show appropriate UI
|
|
622
|
+
if live_trace_url and new:
|
|
623
|
+
# Minimal UI with live trace
|
|
624
|
+
cursor_deeplink = generate_cursor_deeplink(image_name, port)
|
|
625
|
+
show_dev_ui(
|
|
626
|
+
live_trace_url=live_trace_url,
|
|
627
|
+
server_name=image_name,
|
|
628
|
+
port=port,
|
|
629
|
+
cursor_deeplink=cursor_deeplink,
|
|
630
|
+
is_docker=True,
|
|
631
|
+
)
|
|
632
|
+
else:
|
|
633
|
+
# Full UI
|
|
634
|
+
hud_console.header("HUD Development Mode (Docker)")
|
|
635
|
+
if verbose:
|
|
636
|
+
hud_console.section_title("Docker Command")
|
|
637
|
+
hud_console.info(" ".join(docker_cmd))
|
|
638
|
+
show_dev_server_info(
|
|
639
|
+
server_name=image_name,
|
|
640
|
+
port=port,
|
|
641
|
+
transport="http",
|
|
642
|
+
inspector=inspector,
|
|
643
|
+
interactive=interactive,
|
|
644
|
+
env_dir=env_dir,
|
|
645
|
+
new=new,
|
|
646
|
+
)
|
|
647
|
+
hud_console.dim_info(
|
|
648
|
+
"",
|
|
649
|
+
"Container restarts on file changes (mounted volumes), "
|
|
650
|
+
"if changing tools run hud dev again",
|
|
651
|
+
)
|
|
652
|
+
hud_console.info("")
|
|
557
653
|
|
|
558
654
|
# Suppress logs unless verbose
|
|
559
655
|
if not verbose:
|
|
@@ -562,13 +658,6 @@ def run_docker_dev_server(
|
|
|
562
658
|
logging.getLogger("uvicorn").setLevel(logging.ERROR)
|
|
563
659
|
os.environ["FASTMCP_DISABLE_BANNER"] = "1"
|
|
564
660
|
|
|
565
|
-
# Note about hot-reload behavior
|
|
566
|
-
hud_console.dim_info(
|
|
567
|
-
"",
|
|
568
|
-
"Container restarts on file changes (mounted volumes), if changing tools run hud dev again",
|
|
569
|
-
)
|
|
570
|
-
hud_console.info("")
|
|
571
|
-
|
|
572
661
|
# Create and run proxy with HUD helpers
|
|
573
662
|
async def run_proxy() -> None:
|
|
574
663
|
from fastmcp import FastMCP
|
|
@@ -617,6 +706,7 @@ def run_mcp_dev_server(
|
|
|
617
706
|
watch: list[str] | None,
|
|
618
707
|
docker: bool = False,
|
|
619
708
|
docker_args: list[str] | None = None,
|
|
709
|
+
new: bool = False,
|
|
620
710
|
) -> None:
|
|
621
711
|
"""Run MCP development server with hot-reload."""
|
|
622
712
|
docker_args = docker_args or []
|
|
@@ -627,12 +717,12 @@ def run_mcp_dev_server(
|
|
|
627
717
|
hud_console.note("Detected Dockerfile - using Docker mode with volume mounts")
|
|
628
718
|
hud_console.dim_info("Tip", "Use 'hud dev --help' to see all options")
|
|
629
719
|
hud_console.info("")
|
|
630
|
-
run_docker_dev_server(port, verbose, inspector, interactive, docker_args)
|
|
720
|
+
run_docker_dev_server(port, verbose, inspector, interactive, docker_args, new)
|
|
631
721
|
return
|
|
632
722
|
|
|
633
723
|
# Route to Docker mode if explicitly requested
|
|
634
724
|
if docker:
|
|
635
|
-
run_docker_dev_server(port, verbose, inspector, interactive, docker_args)
|
|
725
|
+
run_docker_dev_server(port, verbose, inspector, interactive, docker_args, new)
|
|
636
726
|
return
|
|
637
727
|
|
|
638
728
|
transport = "stdio" if stdio else "http"
|
|
@@ -676,6 +766,6 @@ def run_mcp_dev_server(
|
|
|
676
766
|
is_child = os.environ.get("_HUD_DEV_CHILD") == "1"
|
|
677
767
|
|
|
678
768
|
if is_child:
|
|
679
|
-
asyncio.run(run_mcp_module(module, transport, port, verbose, False, False))
|
|
769
|
+
asyncio.run(run_mcp_module(module, transport, port, verbose, False, False, new))
|
|
680
770
|
else:
|
|
681
|
-
run_with_reload(module, watch_paths, transport, port, verbose, inspector, interactive)
|
|
771
|
+
run_with_reload(module, watch_paths, transport, port, verbose, inspector, interactive, new)
|
hud/cli/eval.py
CHANGED
|
@@ -22,6 +22,28 @@ logger = logging.getLogger(__name__)
|
|
|
22
22
|
hud_console = HUDConsole()
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _tasks_use_local_mcp(tasks: list[Task]) -> bool:
|
|
26
|
+
"""Return True if any task's MCP config uses a local command instead of a URL.
|
|
27
|
+
|
|
28
|
+
A config is considered local when a server entry contains a 'command' key and
|
|
29
|
+
does not provide a 'url'.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
for t in tasks:
|
|
33
|
+
cfg = getattr(t, "mcp_config", {}) or {}
|
|
34
|
+
if not isinstance(cfg, dict):
|
|
35
|
+
continue
|
|
36
|
+
for server_cfg in cfg.values():
|
|
37
|
+
if isinstance(server_cfg, dict) and (
|
|
38
|
+
"command" in server_cfg and not server_cfg.get("url")
|
|
39
|
+
):
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
except Exception:
|
|
43
|
+
# Be conservative: if detection fails, do not block
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
25
47
|
def get_available_models() -> list[dict[str, str | None]]:
|
|
26
48
|
"""Fetch available models from the HUD API (only ready models).
|
|
27
49
|
|
|
@@ -265,7 +287,33 @@ async def run_single_task(
|
|
|
265
287
|
"Using first task from dataset (run with --full to run the entire dataset)..."
|
|
266
288
|
)
|
|
267
289
|
|
|
268
|
-
|
|
290
|
+
# Warn/confirm if the task uses local MCP config
|
|
291
|
+
try:
|
|
292
|
+
if group_size > 1 and _tasks_use_local_mcp([task]):
|
|
293
|
+
hud_console.warning(
|
|
294
|
+
"Detected a local MCP configuration (uses 'command' instead of a 'url')."
|
|
295
|
+
)
|
|
296
|
+
hud_console.info(
|
|
297
|
+
"Ensure there are no exposed port conflicts during Docker runs/builds in eval."
|
|
298
|
+
)
|
|
299
|
+
proceed = hud_console.confirm(
|
|
300
|
+
"Proceed with running local MCP servers for this evaluation?",
|
|
301
|
+
default=True,
|
|
302
|
+
)
|
|
303
|
+
if not proceed:
|
|
304
|
+
# Provide a helpful next step
|
|
305
|
+
hud_console.hint("You can convert tasks to remote with: hud convert <tasks_file>")
|
|
306
|
+
raise typer.Exit(1)
|
|
307
|
+
# Always show the convert hint for awareness
|
|
308
|
+
hud_console.hint(
|
|
309
|
+
"Avoid local port conflicts by converting to remote: hud convert <tasks_file>"
|
|
310
|
+
)
|
|
311
|
+
except typer.Exit:
|
|
312
|
+
raise
|
|
313
|
+
except Exception as e:
|
|
314
|
+
hud_console.debug(f"Local MCP confirmation skipped due to error: {e}")
|
|
315
|
+
|
|
316
|
+
task_prompt = task.prompt
|
|
269
317
|
|
|
270
318
|
# Use grouped evaluation if group_size > 1
|
|
271
319
|
agent_config: dict[str, Any] = {}
|
|
@@ -387,6 +435,56 @@ async def run_full_dataset(
|
|
|
387
435
|
hud_console.error(f"No tasks found in: {source}")
|
|
388
436
|
raise typer.Exit(1)
|
|
389
437
|
|
|
438
|
+
# Warn/confirm once if any task uses local MCP config
|
|
439
|
+
try:
|
|
440
|
+
if _tasks_use_local_mcp(tasks):
|
|
441
|
+
hud_console.warning(
|
|
442
|
+
"Detected local MCP configurations (use 'command' instead of a 'url')."
|
|
443
|
+
)
|
|
444
|
+
hud_console.info(
|
|
445
|
+
"When running many tasks concurrently, exposed host ports from Docker may conflict."
|
|
446
|
+
)
|
|
447
|
+
proceed = hud_console.confirm(
|
|
448
|
+
"Proceed with running local MCP servers for this evaluation?",
|
|
449
|
+
default=True,
|
|
450
|
+
)
|
|
451
|
+
if not proceed:
|
|
452
|
+
# Helpful hint when source is a file path
|
|
453
|
+
try:
|
|
454
|
+
path = Path(source)
|
|
455
|
+
if path.exists():
|
|
456
|
+
hud_console.hint(
|
|
457
|
+
f"You can convert tasks to remote with: hud convert {path.name}"
|
|
458
|
+
)
|
|
459
|
+
else:
|
|
460
|
+
hud_console.hint(
|
|
461
|
+
"You can convert tasks to remote with: hud convert <tasks_file>"
|
|
462
|
+
)
|
|
463
|
+
except Exception:
|
|
464
|
+
hud_console.hint(
|
|
465
|
+
"You can convert tasks to remote with: hud convert <tasks_file>"
|
|
466
|
+
)
|
|
467
|
+
raise typer.Exit(1)
|
|
468
|
+
# Always show the convert hint for awareness
|
|
469
|
+
try:
|
|
470
|
+
path = Path(source)
|
|
471
|
+
if path.exists():
|
|
472
|
+
hud_console.hint(
|
|
473
|
+
f"Convert to remote to avoid port conflicts: hud convert {path.name}"
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
hud_console.hint(
|
|
477
|
+
"Convert to remote to avoid port conflicts: hud convert <tasks_file>"
|
|
478
|
+
)
|
|
479
|
+
except Exception:
|
|
480
|
+
hud_console.hint(
|
|
481
|
+
"Convert to remote to avoid port conflicts: hud convert <tasks_file>"
|
|
482
|
+
)
|
|
483
|
+
except typer.Exit:
|
|
484
|
+
raise
|
|
485
|
+
except Exception as e:
|
|
486
|
+
hud_console.debug(f"Local MCP confirmation skipped due to error: {e}")
|
|
487
|
+
|
|
390
488
|
# Convert Task objects to dicts for dataset runners
|
|
391
489
|
dataset_or_tasks = [task.model_dump() for task in tasks]
|
|
392
490
|
|
hud/cli/flows/dev.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import contextlib
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from hud.settings import settings
|
|
10
|
+
from hud.shared.requests import make_request
|
|
11
|
+
from hud.utils.hud_console import hud_console
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def create_dynamic_trace(
|
|
17
|
+
*,
|
|
18
|
+
mcp_config: dict[str, dict[str, Any]],
|
|
19
|
+
build_status: bool,
|
|
20
|
+
environment_name: str,
|
|
21
|
+
) -> str | None:
|
|
22
|
+
"""
|
|
23
|
+
Create a dynamic trace for HUD dev sessions when running in HTTP mode.
|
|
24
|
+
|
|
25
|
+
Sends a POST to the HUD API with:
|
|
26
|
+
- mcp_config: points to the local MCP config (same as Cursor)
|
|
27
|
+
- build_status: True if Docker mode (built image), False if basic Python mode
|
|
28
|
+
- environment_name: Name of the environment/server/image
|
|
29
|
+
|
|
30
|
+
Returns the full URL to the live trace when successful, otherwise None.
|
|
31
|
+
"""
|
|
32
|
+
api_base = settings.hud_api_url.rstrip("/")
|
|
33
|
+
# Endpoint TBD; use a sensible default path that the backend can wire up
|
|
34
|
+
url = f"{api_base}/dev/dynamic-traces"
|
|
35
|
+
|
|
36
|
+
payload = {
|
|
37
|
+
"mcp_config": mcp_config,
|
|
38
|
+
"build_status": bool(build_status),
|
|
39
|
+
"environment_name": environment_name,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Best-effort; if missing API key, log and continue
|
|
43
|
+
api_key = settings.api_key
|
|
44
|
+
if not api_key:
|
|
45
|
+
logger.warning("Skipping dynamic trace creation; missing HUD_API_KEY")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
try:
|
|
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}
|
|
51
|
+
trace_id = None
|
|
52
|
+
if isinstance(resp, dict):
|
|
53
|
+
trace_id = resp.get("id")
|
|
54
|
+
if trace_id is None:
|
|
55
|
+
data = resp.get("data", {}) or {}
|
|
56
|
+
if isinstance(data, dict):
|
|
57
|
+
trace_id = data.get("id")
|
|
58
|
+
# Backcompat: if url is provided directly
|
|
59
|
+
if not trace_id:
|
|
60
|
+
direct_url = resp.get("url") or (resp.get("data", {}) or {}).get("url")
|
|
61
|
+
if isinstance(direct_url, str) and direct_url:
|
|
62
|
+
return direct_url
|
|
63
|
+
|
|
64
|
+
if isinstance(trace_id, str) and trace_id:
|
|
65
|
+
return f"https://hud.so/trace/{trace_id}"
|
|
66
|
+
return None
|
|
67
|
+
except Exception as e:
|
|
68
|
+
# Do not interrupt dev flow
|
|
69
|
+
try:
|
|
70
|
+
preview = json.dumps(payload)[:500]
|
|
71
|
+
logger.warning("Failed to create dynamic dev trace: %s | payload=%s", e, preview)
|
|
72
|
+
except Exception:
|
|
73
|
+
logger.warning("Failed to create dynamic dev trace: %s", e)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def show_dev_ui(
|
|
78
|
+
*,
|
|
79
|
+
live_trace_url: str,
|
|
80
|
+
server_name: str,
|
|
81
|
+
port: int,
|
|
82
|
+
cursor_deeplink: str,
|
|
83
|
+
is_docker: bool = False,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Show the minimal dev UI with live trace link.
|
|
87
|
+
|
|
88
|
+
This is called only when we have a successful trace URL.
|
|
89
|
+
For full UI mode, the caller should use show_dev_server_info() directly.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
live_trace_url: URL to the live trace
|
|
93
|
+
server_name: Name of the server/image
|
|
94
|
+
port: Port the server is running on
|
|
95
|
+
cursor_deeplink: Pre-generated Cursor deeplink URL
|
|
96
|
+
is_docker: Whether this is Docker mode (affects hot-reload message)
|
|
97
|
+
"""
|
|
98
|
+
import webbrowser
|
|
99
|
+
|
|
100
|
+
from rich.panel import Panel
|
|
101
|
+
|
|
102
|
+
# Show header first
|
|
103
|
+
hud_console.header("HUD Development Server", icon="🚀")
|
|
104
|
+
|
|
105
|
+
# Try to open the live trace in the default browser
|
|
106
|
+
with contextlib.suppress(Exception):
|
|
107
|
+
# new=2 -> open in a new tab, if possible
|
|
108
|
+
webbrowser.open(live_trace_url, new=2)
|
|
109
|
+
|
|
110
|
+
# Show panel with just the link
|
|
111
|
+
# Center the link and style it: blue, bold, underlined
|
|
112
|
+
link_markup = f"[bold underline rgb(108,113,196)][link={live_trace_url}]{live_trace_url}[/link][/bold underline rgb(108,113,196)]" # noqa: E501
|
|
113
|
+
# Use center alignment by surrounding with spaces via justify
|
|
114
|
+
from rich.align import Align
|
|
115
|
+
|
|
116
|
+
panel = Panel(
|
|
117
|
+
Align.center(link_markup),
|
|
118
|
+
title="🔗 Live Dev Trace",
|
|
119
|
+
border_style="rgb(192,150,12)", # HUD gold
|
|
120
|
+
padding=(1, 2),
|
|
121
|
+
)
|
|
122
|
+
hud_console.console.print(panel)
|
|
123
|
+
|
|
124
|
+
# Show other info below
|
|
125
|
+
label = "Base image" if is_docker else "Server"
|
|
126
|
+
hud_console.info("")
|
|
127
|
+
hud_console.info(f"{hud_console.sym.ITEM} {label}: {server_name}")
|
|
128
|
+
hud_console.info(f"{hud_console.sym.ITEM} Cursor: {cursor_deeplink}")
|
|
129
|
+
hud_console.info("")
|
|
130
|
+
hud_console.info(f"{hud_console.sym.SUCCESS} Hot-reload enabled")
|
|
131
|
+
if is_docker:
|
|
132
|
+
hud_console.dim_info(
|
|
133
|
+
"",
|
|
134
|
+
"Container restarts on file changes (mounted volumes), "
|
|
135
|
+
"if changing tools run hud dev again",
|
|
136
|
+
)
|
|
137
|
+
hud_console.info("")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def generate_cursor_deeplink(server_name: str, port: int) -> str:
|
|
141
|
+
"""Generate a Cursor deeplink for the MCP server.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
server_name: Name of the server
|
|
145
|
+
port: Port the server is running on
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Cursor deeplink URL
|
|
149
|
+
"""
|
|
150
|
+
server_config = {"url": f"http://localhost:{port}/mcp"}
|
|
151
|
+
config_json = json.dumps(server_config, indent=2)
|
|
152
|
+
config_base64 = base64.b64encode(config_json.encode()).decode()
|
|
153
|
+
return (
|
|
154
|
+
f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_base64}"
|
|
155
|
+
)
|
hud/cli/flows/tasks.py
CHANGED
|
@@ -11,7 +11,7 @@ import yaml
|
|
|
11
11
|
|
|
12
12
|
from hud.cli.push import push_environment
|
|
13
13
|
from hud.cli.utils.docker import require_docker_running
|
|
14
|
-
from hud.cli.utils.env_check import
|
|
14
|
+
from hud.cli.utils.env_check import find_environment_dir
|
|
15
15
|
from hud.cli.utils.registry import extract_name_and_tag
|
|
16
16
|
from hud.utils.hud_console import hud_console
|
|
17
17
|
from hud.utils.tasks import load_tasks
|
|
@@ -56,7 +56,9 @@ def _validate_tasks(tasks: list[Task]) -> bool:
|
|
|
56
56
|
return True
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def _ensure_pushed(
|
|
59
|
+
def _ensure_pushed(
|
|
60
|
+
env_dir: Path, lock_data: dict[str, Any], check_docker: bool = True
|
|
61
|
+
) -> dict[str, Any]:
|
|
60
62
|
"""Ensure the environment is pushed to a registry; return updated lock data."""
|
|
61
63
|
pushed = bool(lock_data.get("push"))
|
|
62
64
|
if not pushed:
|
|
@@ -64,7 +66,8 @@ def _ensure_pushed(env_dir: Path, lock_data: dict[str, Any]) -> dict[str, Any]:
|
|
|
64
66
|
if not hud_console.confirm("Push to a registry now (runs 'hud push')?", default=True):
|
|
65
67
|
raise typer.Exit(1)
|
|
66
68
|
# Check Docker availability before attempting a push
|
|
67
|
-
|
|
69
|
+
if check_docker:
|
|
70
|
+
require_docker_running()
|
|
68
71
|
|
|
69
72
|
# If Docker or login is not configured, the push function will fail and halt.
|
|
70
73
|
push_environment(str(env_dir), yes=True)
|
|
@@ -293,9 +296,24 @@ def convert_tasks_to_remote(tasks_file: str) -> str:
|
|
|
293
296
|
hud_console.hint("Ensure you're in or near your environment folder before running 'hud rl'")
|
|
294
297
|
raise typer.Exit(1)
|
|
295
298
|
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
299
|
+
# For convert command, we don't need Docker running - just check for lock file
|
|
300
|
+
# This avoids showing Docker-related messages during conversion
|
|
301
|
+
lock_path = env_dir / "hud.lock.yaml"
|
|
302
|
+
if not lock_path.exists():
|
|
303
|
+
hud_console.error("No hud.lock.yaml found. The environment needs to be built first.")
|
|
304
|
+
hud_console.info("Run 'hud build' in the environment directory to build it.")
|
|
305
|
+
raise typer.Exit(1)
|
|
306
|
+
|
|
307
|
+
# Load lock data directly
|
|
308
|
+
try:
|
|
309
|
+
with open(lock_path) as f:
|
|
310
|
+
lock_data: dict[str, Any] = yaml.safe_load(f) or {}
|
|
311
|
+
except Exception as e:
|
|
312
|
+
hud_console.error(f"Failed to read hud.lock.yaml: {e}")
|
|
313
|
+
raise typer.Exit(1) from e
|
|
314
|
+
|
|
315
|
+
# Check if pushed - don't check Docker for convert command
|
|
316
|
+
lock_data = _ensure_pushed(env_dir, lock_data, check_docker=False)
|
|
299
317
|
|
|
300
318
|
# Derive remote image name org/name:tag
|
|
301
319
|
remote_image = _derive_remote_image(lock_data)
|
|
@@ -387,8 +405,11 @@ def convert_tasks_to_remote(tasks_file: str) -> str:
|
|
|
387
405
|
f"Detected env vars in .env that look like API keys: {names_preview}.\n"
|
|
388
406
|
"Include them as remote headers (values will be ${VAR} placeholders)?"
|
|
389
407
|
)
|
|
390
|
-
if hud_console.confirm(prompt, default=True):
|
|
391
|
-
|
|
408
|
+
if not hud_console.confirm(prompt, default=True):
|
|
409
|
+
# User cancelled - exit without creating the file
|
|
410
|
+
hud_console.info("Conversion cancelled by user")
|
|
411
|
+
raise typer.Exit(0)
|
|
412
|
+
all_detected.update(missing)
|
|
392
413
|
|
|
393
414
|
# Final set of env vars to convert to headers
|
|
394
415
|
provided_keys = all_detected
|
|
@@ -461,6 +482,5 @@ def convert_tasks_to_remote(tasks_file: str) -> str:
|
|
|
461
482
|
f.write("\n")
|
|
462
483
|
|
|
463
484
|
hud_console.success(f"Created remote tasks file: {remote_path.name}")
|
|
464
|
-
hud_console.hint("Proceeding with RL training on the remote environment")
|
|
465
485
|
|
|
466
486
|
return str(remote_path)
|
hud/cli/init.py
CHANGED
|
@@ -23,6 +23,7 @@ PRESET_MAP: dict[str, str | None] = {
|
|
|
23
23
|
"blank": "blank",
|
|
24
24
|
"deep-research": "deepresearch",
|
|
25
25
|
"browser": "browser",
|
|
26
|
+
"rubrics": "rubrics",
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
SKIP_DIR_NAMES = {"node_modules", "__pycache__", "dist", "build", ".next", ".git"}
|
|
@@ -91,6 +92,7 @@ def _prompt_for_preset() -> str:
|
|
|
91
92
|
{"name": "blank", "message": "blank"},
|
|
92
93
|
{"name": "deep-research", "message": "deep-research"},
|
|
93
94
|
{"name": "browser", "message": "browser"},
|
|
95
|
+
{"name": "rubrics", "message": "rubrics"},
|
|
94
96
|
]
|
|
95
97
|
display_choices = [c["message"] for c in choices]
|
|
96
98
|
selected = questionary.select(
|
|
@@ -194,7 +196,7 @@ def create_environment(
|
|
|
194
196
|
if preset_normalized not in PRESET_MAP:
|
|
195
197
|
hud_console.warning(
|
|
196
198
|
f"Unknown preset '{preset_normalized}', defaulting to 'blank' "
|
|
197
|
-
"(available: blank, deep-research, browser)"
|
|
199
|
+
"(available: blank, deep-research, browser, rubrics)"
|
|
198
200
|
)
|
|
199
201
|
preset_normalized = "blank"
|
|
200
202
|
|
hud/cli/utils/docker.py
CHANGED
|
@@ -308,7 +308,10 @@ def require_docker_running() -> None:
|
|
|
308
308
|
"Is Docker running? Open Docker Desktop and wait until it reports 'Running'"
|
|
309
309
|
)
|
|
310
310
|
raise typer.Exit(1) from e
|
|
311
|
-
except
|
|
312
|
-
|
|
311
|
+
except typer.Exit:
|
|
312
|
+
# Propagate cleanly without extra noise; hints already printed above
|
|
313
|
+
raise
|
|
314
|
+
except Exception:
|
|
315
|
+
# Unknown failure - keep output minimal and avoid stack traces
|
|
313
316
|
hud_console.hint("Is the Docker daemon running?")
|
|
314
|
-
raise typer.Exit(1)
|
|
317
|
+
raise typer.Exit(1) # noqa: B904
|
hud/clients/base.py
CHANGED
|
@@ -146,7 +146,7 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
146
146
|
except HudException:
|
|
147
147
|
raise
|
|
148
148
|
except Exception as e:
|
|
149
|
-
|
|
149
|
+
hud_console.error(f"Failed to initialize MCP client: {e}")
|
|
150
150
|
raise HudException from e
|
|
151
151
|
|
|
152
152
|
# Common hud behavior - fetch telemetry
|
|
@@ -333,7 +333,7 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
333
333
|
tool_info = {
|
|
334
334
|
"name": tool.name,
|
|
335
335
|
"description": tool.description,
|
|
336
|
-
"
|
|
336
|
+
"inputSchema": tool.inputSchema,
|
|
337
337
|
}
|
|
338
338
|
analysis["tools"].append(tool_info)
|
|
339
339
|
|