hud-python 0.4.45__py3-none-any.whl → 0.5.1__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.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +11 -5
- hud/agents/base.py +220 -500
- hud/agents/claude.py +200 -240
- hud/agents/gemini.py +275 -0
- hud/agents/gemini_cua.py +335 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +41 -36
- hud/agents/openai.py +291 -292
- hud/agents/{openai_chat_generic.py → openai_chat.py} +80 -34
- hud/agents/operator.py +211 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +379 -210
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +376 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/cli/__init__.py +461 -545
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +664 -110
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +882 -734
- hud/cli/eval.py +782 -668
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/push.py +29 -11
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +108 -6
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +69 -0
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_git.py +142 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +40 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +327 -0
- hud/datasets/runner.py +192 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +50 -0
- hud/environment/connection.py +206 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +109 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +694 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +112 -0
- hud/environment/scenarios.py +493 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +218 -0
- hud/environment/tests/test_environment.py +161 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +201 -0
- hud/environment/tests/test_scenarios.py +280 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +674 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +185 -0
- hud/eval/manager.py +466 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +340 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +145 -0
- hud/eval/types.py +63 -0
- hud/eval/utils.py +183 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +151 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +158 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +16 -2
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +4 -0
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +167 -57
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +61 -3
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.1.dist-info/METADATA +264 -0
- hud_python-0.5.1.dist-info/RECORD +299 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Version checking utilities for HUD CLI.
|
|
2
|
+
|
|
3
|
+
This module handles checking for updates to the hud-python package
|
|
4
|
+
and prompting users to upgrade when a new version is available.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Checks PyPI for the latest version of hud-python
|
|
8
|
+
- Caches results for 6 hours to avoid excessive API calls
|
|
9
|
+
- Displays a friendly prompt when an update is available
|
|
10
|
+
- Can be disabled with HUD_SKIP_VERSION_CHECK=1 environment variable
|
|
11
|
+
|
|
12
|
+
The version check runs automatically at the start of most CLI commands,
|
|
13
|
+
but is skipped for help and version commands to keep them fast.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import contextlib
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import NamedTuple
|
|
25
|
+
|
|
26
|
+
import httpx
|
|
27
|
+
from packaging import version
|
|
28
|
+
|
|
29
|
+
from hud.utils.hud_console import HUDConsole
|
|
30
|
+
|
|
31
|
+
# Logger for version checking
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Cache location for version check data
|
|
35
|
+
CACHE_DIR = Path.home() / ".hud" / ".cache"
|
|
36
|
+
VERSION_CACHE_FILE = CACHE_DIR / "version_check.json"
|
|
37
|
+
|
|
38
|
+
# Cache duration in seconds (6 hours)
|
|
39
|
+
CACHE_DURATION = 6 * 60 * 60
|
|
40
|
+
|
|
41
|
+
# PyPI API URL for package info
|
|
42
|
+
PYPI_URL = "https://pypi.org/pypi/hud-python/json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VersionInfo(NamedTuple):
|
|
46
|
+
"""Version information from PyPI."""
|
|
47
|
+
|
|
48
|
+
latest: str
|
|
49
|
+
current: str
|
|
50
|
+
is_outdated: bool
|
|
51
|
+
checked_at: float
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_current_version() -> str:
|
|
55
|
+
"""Get the currently installed version of hud-python."""
|
|
56
|
+
try:
|
|
57
|
+
from hud import __version__
|
|
58
|
+
|
|
59
|
+
return __version__
|
|
60
|
+
except ImportError:
|
|
61
|
+
return "unknown"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _fetch_latest_version() -> str | None:
|
|
65
|
+
"""Fetch the latest version from PyPI.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The latest version string, or None if the request fails.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
with httpx.Client(timeout=3.0) as client:
|
|
72
|
+
response = client.get(PYPI_URL)
|
|
73
|
+
if response.status_code == 200:
|
|
74
|
+
data = response.json()
|
|
75
|
+
return data["info"]["version"]
|
|
76
|
+
except Exception: # noqa: S110
|
|
77
|
+
# Silently fail - we don't want to disrupt the user's workflow
|
|
78
|
+
# if PyPI is down or there's a network issue
|
|
79
|
+
pass
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _load_cache() -> VersionInfo | None:
|
|
84
|
+
"""Load cached version information.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Cached VersionInfo if valid, None otherwise.
|
|
88
|
+
"""
|
|
89
|
+
if not VERSION_CACHE_FILE.exists():
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
with open(VERSION_CACHE_FILE) as f:
|
|
94
|
+
data = json.load(f)
|
|
95
|
+
|
|
96
|
+
# Check if cache is still valid
|
|
97
|
+
if time.time() - data["checked_at"] > CACHE_DURATION:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
return VersionInfo(
|
|
101
|
+
latest=data["latest"],
|
|
102
|
+
current=data["current"],
|
|
103
|
+
is_outdated=data["is_outdated"],
|
|
104
|
+
checked_at=data["checked_at"],
|
|
105
|
+
)
|
|
106
|
+
except Exception:
|
|
107
|
+
# If cache is corrupted, return None
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _save_cache(info: VersionInfo) -> None:
|
|
112
|
+
"""Save version information to cache.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
info: Version information to cache.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
# Create cache directory if it doesn't exist
|
|
119
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
with open(VERSION_CACHE_FILE, "w") as f:
|
|
122
|
+
json.dump(
|
|
123
|
+
{
|
|
124
|
+
"latest": info.latest,
|
|
125
|
+
"current": info.current,
|
|
126
|
+
"is_outdated": info.is_outdated,
|
|
127
|
+
"checked_at": info.checked_at,
|
|
128
|
+
},
|
|
129
|
+
f,
|
|
130
|
+
)
|
|
131
|
+
except Exception: # noqa: S110
|
|
132
|
+
# Silently fail if we can't write cache
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _compare_versions(current: str, latest: str) -> bool:
|
|
137
|
+
"""Compare versions to determine if an update is available.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
current: Current version string
|
|
141
|
+
latest: Latest version string
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if latest is newer than current, False otherwise.
|
|
145
|
+
"""
|
|
146
|
+
if current == "unknown":
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
current_v = version.parse(current)
|
|
151
|
+
latest_v = version.parse(latest)
|
|
152
|
+
return latest_v > current_v
|
|
153
|
+
except Exception:
|
|
154
|
+
# If we can't parse versions, assume no update needed
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def check_for_updates() -> VersionInfo | None:
|
|
159
|
+
"""Check for updates to hud-python.
|
|
160
|
+
|
|
161
|
+
This function checks PyPI for the latest version and caches the result
|
|
162
|
+
for 6 hours to avoid excessive API calls.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
VersionInfo if check succeeds, None otherwise.
|
|
166
|
+
"""
|
|
167
|
+
# Check if we're in CI/testing environment
|
|
168
|
+
if os.environ.get("CI") or os.environ.get("HUD_SKIP_VERSION_CHECK"):
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# Get current version first
|
|
172
|
+
current = _get_current_version()
|
|
173
|
+
if current == "unknown":
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
# Try to load from cache
|
|
177
|
+
cached_info = _load_cache()
|
|
178
|
+
|
|
179
|
+
# If cache exists but current version has changed (user upgraded), invalidate cache
|
|
180
|
+
if cached_info and cached_info.current != current:
|
|
181
|
+
cached_info = None # Force fresh check
|
|
182
|
+
|
|
183
|
+
if cached_info:
|
|
184
|
+
# Update the current version in the cached info to reflect reality
|
|
185
|
+
# but keep the cached latest version and timestamp
|
|
186
|
+
return VersionInfo(
|
|
187
|
+
latest=cached_info.latest,
|
|
188
|
+
current=current, # Use actual current version, not cached
|
|
189
|
+
is_outdated=_compare_versions(current, cached_info.latest),
|
|
190
|
+
checked_at=cached_info.checked_at,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Fetch latest version from PyPI
|
|
194
|
+
latest = _fetch_latest_version()
|
|
195
|
+
if not latest:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
# Compare versions
|
|
199
|
+
is_outdated = _compare_versions(current, latest)
|
|
200
|
+
|
|
201
|
+
# Create version info
|
|
202
|
+
info = VersionInfo(
|
|
203
|
+
latest=latest,
|
|
204
|
+
current=current,
|
|
205
|
+
is_outdated=is_outdated,
|
|
206
|
+
checked_at=time.time(),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Save to cache
|
|
210
|
+
_save_cache(info)
|
|
211
|
+
|
|
212
|
+
return info
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def display_update_prompt(console: HUDConsole | None = None) -> None:
|
|
216
|
+
"""Display update prompt if a new version is available.
|
|
217
|
+
|
|
218
|
+
This function checks for updates and displays a prompt to the user
|
|
219
|
+
if their version is outdated.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
console: HUDConsole instance for output. If None, creates a new one.
|
|
223
|
+
"""
|
|
224
|
+
if console is None:
|
|
225
|
+
console = HUDConsole(logger=logger)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
info = check_for_updates()
|
|
229
|
+
if info and info.is_outdated:
|
|
230
|
+
# Create update message
|
|
231
|
+
update_msg = (
|
|
232
|
+
f"🆕 A new version of hud-python is available: "
|
|
233
|
+
f"[bold cyan]{info.latest}[/bold cyan] "
|
|
234
|
+
f"(current: [dim]{info.current}[/dim])\n"
|
|
235
|
+
f" Run: [bold yellow]uv tool upgrade hud-python[/bold yellow] to update"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Display using console info
|
|
239
|
+
console.info(f"[yellow]{update_msg}[/yellow]")
|
|
240
|
+
except Exception: # noqa: S110
|
|
241
|
+
# Never let version checking disrupt the user's workflow
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def force_version_check() -> VersionInfo | None:
|
|
246
|
+
"""Force a version check, bypassing the cache.
|
|
247
|
+
|
|
248
|
+
This is useful for explicit version checks or testing.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
VersionInfo if check succeeds, None otherwise.
|
|
252
|
+
"""
|
|
253
|
+
# Clear the cache to force a fresh check
|
|
254
|
+
if VERSION_CACHE_FILE.exists():
|
|
255
|
+
with contextlib.suppress(Exception):
|
|
256
|
+
VERSION_CACHE_FILE.unlink()
|
|
257
|
+
|
|
258
|
+
return check_for_updates()
|
hud/cli/{rl → utils}/viewer.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Inline JSON preview with expandable view
|
|
1
|
+
"""Inline JSON preview with expandable view.
|
|
2
2
|
|
|
3
3
|
Uses minimal terminal interaction for inline display.
|
|
4
4
|
"""
|
|
@@ -46,7 +46,7 @@ def _truncate_value(value: Any, max_len: int = 60) -> str:
|
|
|
46
46
|
if len(value) > max_len:
|
|
47
47
|
return value[:max_len] + "…"
|
|
48
48
|
return value
|
|
49
|
-
elif isinstance(value,
|
|
49
|
+
elif isinstance(value, dict | list):
|
|
50
50
|
s = json.dumps(value, separators=(",", ":"))
|
|
51
51
|
if len(s) > max_len:
|
|
52
52
|
return s[:max_len] + "…"
|
hud/clients/README.md
CHANGED
|
@@ -7,8 +7,8 @@ This directory contains the MCP client implementations for HUD SDK. The architec
|
|
|
7
7
|
```
|
|
8
8
|
hud/clients/
|
|
9
9
|
├── base.py # Protocol definition and base class
|
|
10
|
-
├── mcp_use.py # MCP-use based implementation (
|
|
11
|
-
├── fastmcp.py # FastMCP based implementation (
|
|
10
|
+
├── mcp_use.py # MCP-use based implementation (default)
|
|
11
|
+
├── fastmcp.py # FastMCP based implementation (alternative)
|
|
12
12
|
└── __init__.py # Exports and default client
|
|
13
13
|
```
|
|
14
14
|
|
|
@@ -30,15 +30,15 @@ class AgentMCPClient(Protocol):
|
|
|
30
30
|
|
|
31
31
|
## Available Implementations
|
|
32
32
|
|
|
33
|
-
### 1. MCPUseHUDClient
|
|
33
|
+
### 1. MCPUseHUDClient (Default)
|
|
34
34
|
- Based on the `mcp_use` library
|
|
35
35
|
- Supports multiple concurrent server connections
|
|
36
36
|
- Battle-tested and stable
|
|
37
37
|
- Good for complex multi-server setups
|
|
38
38
|
|
|
39
|
-
### 2. FastMCPHUDClient
|
|
39
|
+
### 2. FastMCPHUDClient
|
|
40
40
|
- Based on the `fastmcp` library
|
|
41
|
-
-
|
|
41
|
+
- Alternative implementation with different transport handling
|
|
42
42
|
- Supports various transports (HTTP, WebSocket, stdio, in-memory)
|
|
43
43
|
- Better type safety and structured data support
|
|
44
44
|
|
|
@@ -47,7 +47,7 @@ class AgentMCPClient(Protocol):
|
|
|
47
47
|
### Basic Usage
|
|
48
48
|
|
|
49
49
|
```python
|
|
50
|
-
from hud.clients import
|
|
50
|
+
from hud.clients import MCPClient, FastMCPHUDClient
|
|
51
51
|
|
|
52
52
|
# Configuration works for both clients
|
|
53
53
|
mcp_config = {
|
|
@@ -57,10 +57,10 @@ mcp_config = {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
client =
|
|
60
|
+
# Default client (MCPUseHUDClient)
|
|
61
|
+
client = MCPClient(mcp_config)
|
|
62
62
|
|
|
63
|
-
#
|
|
63
|
+
# Alternative: FastMCP client
|
|
64
64
|
client = FastMCPHUDClient(mcp_config)
|
|
65
65
|
|
|
66
66
|
# Both use the same API
|
|
@@ -73,13 +73,14 @@ async with client:
|
|
|
73
73
|
|
|
74
74
|
```python
|
|
75
75
|
from hud.agents import ClaudeAgent
|
|
76
|
+
from hud.clients import MCPClient
|
|
76
77
|
|
|
77
78
|
# Either client works with agents
|
|
78
|
-
client =
|
|
79
|
+
client = MCPClient(mcp_config)
|
|
79
80
|
|
|
80
81
|
agent = ClaudeAgent(
|
|
81
82
|
mcp_client=client,
|
|
82
|
-
model="claude-
|
|
83
|
+
model="claude-sonnet-4-5"
|
|
83
84
|
)
|
|
84
85
|
|
|
85
86
|
# Agent works identically with either client
|
hud/clients/__init__.py
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from .base import AgentMCPClient, BaseHUDClient
|
|
6
|
+
from .environment import EnvironmentClient
|
|
6
7
|
from .fastmcp import FastMCPHUDClient
|
|
7
|
-
from .mcp_use import MCPUseHUDClient
|
|
8
8
|
|
|
9
|
-
# Default to
|
|
10
|
-
MCPClient =
|
|
9
|
+
# Default to FastMCP client (no optional dependencies)
|
|
10
|
+
MCPClient = FastMCPHUDClient
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
13
|
"AgentMCPClient",
|
|
14
14
|
"BaseHUDClient",
|
|
15
|
+
"EnvironmentClient",
|
|
15
16
|
"FastMCPHUDClient",
|
|
16
17
|
"MCPClient",
|
|
17
18
|
]
|
hud/clients/base.py
CHANGED
|
@@ -12,7 +12,6 @@ from mcp.types import Implementation
|
|
|
12
12
|
from hud.shared.exceptions import HudAuthenticationError, HudException
|
|
13
13
|
from hud.types import MCPToolCall, MCPToolResult
|
|
14
14
|
from hud.utils.hud_console import HUDConsole
|
|
15
|
-
from hud.utils.mcp import setup_hud_telemetry
|
|
16
15
|
from hud.version import __version__ as hud_version
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
@@ -86,7 +85,6 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
86
85
|
mcp_config: dict[str, dict[str, Any]] | None = None,
|
|
87
86
|
verbose: bool = False,
|
|
88
87
|
strict_validation: bool = False,
|
|
89
|
-
auto_trace: bool = True,
|
|
90
88
|
) -> None:
|
|
91
89
|
"""
|
|
92
90
|
Initialize base client.
|
|
@@ -99,11 +97,11 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
99
97
|
self.verbose = verbose
|
|
100
98
|
self._mcp_config = mcp_config
|
|
101
99
|
self._strict_validation = strict_validation
|
|
102
|
-
self._auto_trace = auto_trace
|
|
103
|
-
self._auto_trace_cm: Any | None = None # Store auto-created trace context manager
|
|
104
100
|
|
|
105
101
|
self._initialized = False
|
|
106
102
|
self._telemetry_data = {} # Initialize telemetry data
|
|
103
|
+
self._cached_resources: list[types.Resource] = [] # Cache for resources
|
|
104
|
+
self._cached_prompts: list[types.Prompt] = [] # Cache for prompts
|
|
107
105
|
|
|
108
106
|
if self.verbose:
|
|
109
107
|
self._setup_verbose_logging()
|
|
@@ -126,8 +124,6 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
126
124
|
"Either pass it to the constructor or call initialize with a configuration"
|
|
127
125
|
)
|
|
128
126
|
|
|
129
|
-
self._auto_trace_cm = setup_hud_telemetry(self._mcp_config, auto_trace=self._auto_trace)
|
|
130
|
-
|
|
131
127
|
hud_console.debug("Initializing MCP client...")
|
|
132
128
|
|
|
133
129
|
try:
|
|
@@ -135,18 +131,18 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
135
131
|
for server_config in self._mcp_config.values():
|
|
136
132
|
url = server_config.get("url", "")
|
|
137
133
|
headers = server_config.get("headers", {})
|
|
138
|
-
if "mcp.hud.
|
|
134
|
+
if "mcp.hud.ai" in url and len(headers.get("Authorization", "")) < 10:
|
|
139
135
|
raise HudAuthenticationError(
|
|
140
136
|
f'Sending authorization "{headers.get("Authorization", "")}", which may'
|
|
141
137
|
" be incomplete. Ensure HUD_API_KEY environment variable is set or send it"
|
|
142
|
-
" as a header. You can get an API key at https://hud.
|
|
138
|
+
" as a header. You can get an API key at https://hud.ai"
|
|
143
139
|
)
|
|
144
140
|
# Subclasses implement connection
|
|
145
141
|
await self._connect(self._mcp_config)
|
|
146
142
|
except HudException:
|
|
147
143
|
raise
|
|
148
144
|
except Exception as e:
|
|
149
|
-
|
|
145
|
+
hud_console.error(f"Failed to initialize MCP client: {e}")
|
|
150
146
|
raise HudException from e
|
|
151
147
|
|
|
152
148
|
# Common hud behavior - fetch telemetry
|
|
@@ -156,21 +152,12 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
156
152
|
|
|
157
153
|
async def shutdown(self) -> None:
|
|
158
154
|
"""Disconnect from the MCP server."""
|
|
159
|
-
# Clean up auto-created trace if any
|
|
160
|
-
if self._auto_trace_cm:
|
|
161
|
-
try:
|
|
162
|
-
self._auto_trace_cm.__exit__(None, None, None)
|
|
163
|
-
hud_console.info("Closed auto-created trace")
|
|
164
|
-
except Exception as e:
|
|
165
|
-
hud_console.warning(f"Failed to close auto-created trace: {e}")
|
|
166
|
-
finally:
|
|
167
|
-
self._auto_trace_cm = None
|
|
168
|
-
|
|
169
|
-
# Disconnect from server
|
|
170
155
|
if self._initialized:
|
|
171
156
|
await self._disconnect()
|
|
172
157
|
self._initialized = False
|
|
173
|
-
|
|
158
|
+
self._cached_resources.clear()
|
|
159
|
+
self._cached_prompts.clear()
|
|
160
|
+
hud_console.info("Environment Shutdown completed")
|
|
174
161
|
else:
|
|
175
162
|
hud_console.debug("Client was not initialized, skipping disconnect")
|
|
176
163
|
|
|
@@ -211,11 +198,41 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
211
198
|
"""List all available tools."""
|
|
212
199
|
raise NotImplementedError
|
|
213
200
|
|
|
214
|
-
@abstractmethod
|
|
215
201
|
async def list_resources(self) -> list[types.Resource]:
|
|
216
|
-
"""List all available resources.
|
|
202
|
+
"""List all available resources.
|
|
203
|
+
|
|
204
|
+
Uses cached resources if available, otherwise fetches from the server.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of available resources.
|
|
208
|
+
"""
|
|
209
|
+
# If cache is empty, populate it
|
|
210
|
+
if not self._cached_resources:
|
|
211
|
+
self._cached_resources = await self._list_resources_impl()
|
|
212
|
+
return self._cached_resources
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
async def _list_resources_impl(self) -> list[types.Resource]:
|
|
216
|
+
"""Implementation-specific resource listing. Subclasses must implement this."""
|
|
217
217
|
raise NotImplementedError
|
|
218
218
|
|
|
219
|
+
async def list_prompts(self) -> list[types.Prompt]:
|
|
220
|
+
"""List all available prompts.
|
|
221
|
+
|
|
222
|
+
Uses cached prompts if available, otherwise fetches from the server.
|
|
223
|
+
Prompts are optional in MCP; default implementation returns an empty list.
|
|
224
|
+
"""
|
|
225
|
+
if not self._cached_prompts:
|
|
226
|
+
self._cached_prompts = await self._list_prompts_impl()
|
|
227
|
+
return self._cached_prompts
|
|
228
|
+
|
|
229
|
+
async def _list_prompts_impl(self) -> list[types.Prompt]:
|
|
230
|
+
"""Implementation-specific prompt listing (optional).
|
|
231
|
+
|
|
232
|
+
Subclasses can override to support prompt discovery.
|
|
233
|
+
"""
|
|
234
|
+
return []
|
|
235
|
+
|
|
219
236
|
@abstractmethod
|
|
220
237
|
async def _call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
|
|
221
238
|
"""Execute a tool by name."""
|
|
@@ -270,6 +287,17 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
270
287
|
async def _fetch_telemetry(self) -> None:
|
|
271
288
|
"""Common telemetry fetching for all hud clients."""
|
|
272
289
|
try:
|
|
290
|
+
# Get resources (will use cache if available, otherwise fetch)
|
|
291
|
+
resources = await self.list_resources()
|
|
292
|
+
telemetry_available = any(
|
|
293
|
+
str(resource.uri) == "telemetry://live" for resource in resources
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if not telemetry_available:
|
|
297
|
+
if self.verbose:
|
|
298
|
+
hud_console.debug("Telemetry resource not available from server")
|
|
299
|
+
return
|
|
300
|
+
|
|
273
301
|
# Try to read telemetry resource directly
|
|
274
302
|
result = await self.read_resource("telemetry://live")
|
|
275
303
|
if result and result.contents:
|
|
@@ -321,6 +349,9 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
321
349
|
"hub_tools": {},
|
|
322
350
|
"telemetry": self._telemetry_data,
|
|
323
351
|
"resources": [],
|
|
352
|
+
"prompts": [],
|
|
353
|
+
"scenarios": [],
|
|
354
|
+
"verbose": self.verbose,
|
|
324
355
|
"metadata": {
|
|
325
356
|
"servers": list(self._mcp_config.keys()), # type: ignore
|
|
326
357
|
"initialized": self._initialized,
|
|
@@ -333,7 +364,7 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
333
364
|
tool_info = {
|
|
334
365
|
"name": tool.name,
|
|
335
366
|
"description": tool.description,
|
|
336
|
-
"
|
|
367
|
+
"inputSchema": tool.inputSchema,
|
|
337
368
|
}
|
|
338
369
|
analysis["tools"].append(tool_info)
|
|
339
370
|
|
|
@@ -352,16 +383,125 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
352
383
|
try:
|
|
353
384
|
resources = await self.list_resources()
|
|
354
385
|
for resource in resources:
|
|
355
|
-
resource_info = {
|
|
386
|
+
resource_info: dict[str, Any] = {
|
|
356
387
|
"uri": str(resource.uri),
|
|
357
388
|
"name": resource.name,
|
|
358
389
|
"description": resource.description,
|
|
359
390
|
"mime_type": getattr(resource, "mimeType", None),
|
|
360
391
|
}
|
|
392
|
+
# Include meta field if present (contains scenario source code)
|
|
393
|
+
meta = getattr(resource, "meta", None)
|
|
394
|
+
if meta:
|
|
395
|
+
resource_info["meta"] = meta
|
|
361
396
|
analysis["resources"].append(resource_info)
|
|
362
397
|
except Exception as e:
|
|
363
398
|
if self.verbose:
|
|
364
|
-
hud_console.debug(
|
|
399
|
+
hud_console.debug("Could not list resources: " + str(e))
|
|
400
|
+
|
|
401
|
+
# Get all prompts (optional)
|
|
402
|
+
try:
|
|
403
|
+
prompts = await self.list_prompts()
|
|
404
|
+
for prompt in prompts:
|
|
405
|
+
raw_args = getattr(prompt, "arguments", []) or []
|
|
406
|
+
args: list[dict[str, Any]] = [
|
|
407
|
+
{
|
|
408
|
+
"name": getattr(a, "name", None),
|
|
409
|
+
"required": getattr(a, "required", None),
|
|
410
|
+
"description": getattr(a, "description", None),
|
|
411
|
+
}
|
|
412
|
+
for a in raw_args
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
prompt_info: dict[str, Any] = {
|
|
416
|
+
"name": prompt.name,
|
|
417
|
+
"description": prompt.description,
|
|
418
|
+
"arguments": args,
|
|
419
|
+
}
|
|
420
|
+
# Include meta field if present
|
|
421
|
+
meta = getattr(prompt, "meta", None)
|
|
422
|
+
if meta:
|
|
423
|
+
prompt_info["meta"] = meta
|
|
424
|
+
# Merge type/default info from meta.arguments into the arguments array
|
|
425
|
+
if isinstance(meta, dict) and "arguments" in meta:
|
|
426
|
+
meta_args = {a["name"]: a for a in meta["arguments"] if "name" in a}
|
|
427
|
+
for arg in args:
|
|
428
|
+
arg_name = arg.get("name")
|
|
429
|
+
if arg_name and arg_name in meta_args:
|
|
430
|
+
meta_arg = meta_args[arg_name]
|
|
431
|
+
if "default" in meta_arg:
|
|
432
|
+
arg["default"] = meta_arg["default"]
|
|
433
|
+
if "type" in meta_arg:
|
|
434
|
+
arg["type"] = meta_arg["type"]
|
|
435
|
+
if "inputSchema" in meta_arg:
|
|
436
|
+
arg["inputSchema"] = meta_arg["inputSchema"]
|
|
437
|
+
analysis["prompts"].append(prompt_info)
|
|
438
|
+
except Exception as e:
|
|
439
|
+
if self.verbose:
|
|
440
|
+
hud_console.debug("Could not list prompts: " + str(e))
|
|
441
|
+
|
|
442
|
+
# Derive "scenarios" from Environment.@scenario prompts/resources.
|
|
443
|
+
# A scenario is exposed as:
|
|
444
|
+
# - Prompt: name "{env}:{scenario}" with description prefix "[Setup]"
|
|
445
|
+
# - Resource: uri "{env}:{scenario}" with description prefix "[Evaluate]"
|
|
446
|
+
# Both prompt and resource contain meta.code with the scenario source code
|
|
447
|
+
scenarios_by_id: dict[str, dict[str, Any]] = {}
|
|
448
|
+
|
|
449
|
+
for p in analysis.get("prompts", []):
|
|
450
|
+
desc = (p.get("description") or "").strip()
|
|
451
|
+
if not desc.startswith("[Setup]"):
|
|
452
|
+
continue
|
|
453
|
+
scenario_id = p.get("name")
|
|
454
|
+
if not scenario_id:
|
|
455
|
+
continue
|
|
456
|
+
env_name, scenario_name = ([*scenario_id.split(":", 1), ""])[:2]
|
|
457
|
+
scenario_info: dict[str, Any] = {
|
|
458
|
+
"id": scenario_id,
|
|
459
|
+
"env": env_name,
|
|
460
|
+
"name": scenario_name or scenario_id,
|
|
461
|
+
"setup_description": desc,
|
|
462
|
+
"arguments": p.get("arguments") or [],
|
|
463
|
+
"has_setup_prompt": True,
|
|
464
|
+
"has_evaluate_resource": False,
|
|
465
|
+
}
|
|
466
|
+
# Extract code from meta field if present
|
|
467
|
+
meta = p.get("meta")
|
|
468
|
+
if meta and isinstance(meta, dict) and "code" in meta:
|
|
469
|
+
scenario_info["code"] = meta["code"]
|
|
470
|
+
scenarios_by_id[scenario_id] = scenario_info
|
|
471
|
+
|
|
472
|
+
for r in analysis.get("resources", []):
|
|
473
|
+
desc = (r.get("description") or "").strip()
|
|
474
|
+
if not desc.startswith("[Evaluate]"):
|
|
475
|
+
continue
|
|
476
|
+
scenario_id = r.get("uri")
|
|
477
|
+
if not scenario_id:
|
|
478
|
+
continue
|
|
479
|
+
env_name, scenario_name = ([*scenario_id.split(":", 1), ""])[:2]
|
|
480
|
+
if scenario_id not in scenarios_by_id:
|
|
481
|
+
scenarios_by_id[scenario_id] = {
|
|
482
|
+
"id": scenario_id,
|
|
483
|
+
"env": env_name,
|
|
484
|
+
"name": scenario_name or scenario_id,
|
|
485
|
+
"arguments": [],
|
|
486
|
+
"has_setup_prompt": False,
|
|
487
|
+
"has_evaluate_resource": True,
|
|
488
|
+
}
|
|
489
|
+
scenarios_by_id[scenario_id]["evaluate_description"] = desc
|
|
490
|
+
scenarios_by_id[scenario_id]["has_evaluate_resource"] = True
|
|
491
|
+
# Extract code from meta field if not already present (from prompt)
|
|
492
|
+
meta = r.get("meta")
|
|
493
|
+
if (
|
|
494
|
+
meta
|
|
495
|
+
and isinstance(meta, dict)
|
|
496
|
+
and "code" in meta
|
|
497
|
+
and "code" not in scenarios_by_id[scenario_id]
|
|
498
|
+
):
|
|
499
|
+
scenarios_by_id[scenario_id]["code"] = meta["code"]
|
|
500
|
+
|
|
501
|
+
analysis["scenarios"] = sorted(
|
|
502
|
+
scenarios_by_id.values(),
|
|
503
|
+
key=lambda s: (str(s.get("env") or ""), str(s.get("name") or "")),
|
|
504
|
+
)
|
|
365
505
|
|
|
366
506
|
return analysis
|
|
367
507
|
|