hud-python 0.4.45__py3-none-any.whl → 0.5.13__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.
Files changed (282) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +70 -5
  3. hud/agents/base.py +238 -500
  4. hud/agents/claude.py +236 -247
  5. hud/agents/gateway.py +42 -0
  6. hud/agents/gemini.py +264 -0
  7. hud/agents/gemini_cua.py +324 -0
  8. hud/agents/grounded_openai.py +98 -100
  9. hud/agents/misc/integration_test_agent.py +51 -20
  10. hud/agents/misc/response_agent.py +48 -36
  11. hud/agents/openai.py +282 -296
  12. hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
  13. hud/agents/operator.py +199 -0
  14. hud/agents/resolver.py +70 -0
  15. hud/agents/tests/conftest.py +133 -0
  16. hud/agents/tests/test_base.py +300 -622
  17. hud/agents/tests/test_base_runtime.py +233 -0
  18. hud/agents/tests/test_claude.py +381 -214
  19. hud/agents/tests/test_client.py +9 -10
  20. hud/agents/tests/test_gemini.py +369 -0
  21. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  22. hud/agents/tests/test_openai.py +377 -140
  23. hud/agents/tests/test_operator.py +362 -0
  24. hud/agents/tests/test_resolver.py +192 -0
  25. hud/agents/tests/test_run_eval.py +179 -0
  26. hud/agents/types.py +148 -0
  27. hud/cli/__init__.py +493 -546
  28. hud/cli/analyze.py +43 -5
  29. hud/cli/build.py +699 -113
  30. hud/cli/debug.py +8 -5
  31. hud/cli/dev.py +889 -732
  32. hud/cli/eval.py +793 -667
  33. hud/cli/flows/dev.py +167 -0
  34. hud/cli/flows/init.py +191 -0
  35. hud/cli/flows/tasks.py +153 -56
  36. hud/cli/flows/templates.py +151 -0
  37. hud/cli/flows/tests/__init__.py +1 -0
  38. hud/cli/flows/tests/test_dev.py +126 -0
  39. hud/cli/init.py +60 -58
  40. hud/cli/pull.py +1 -1
  41. hud/cli/push.py +38 -13
  42. hud/cli/rft.py +311 -0
  43. hud/cli/rft_status.py +145 -0
  44. hud/cli/tests/test_analyze.py +5 -5
  45. hud/cli/tests/test_analyze_metadata.py +3 -2
  46. hud/cli/tests/test_analyze_module.py +120 -0
  47. hud/cli/tests/test_build.py +110 -8
  48. hud/cli/tests/test_build_failure.py +41 -0
  49. hud/cli/tests/test_build_module.py +50 -0
  50. hud/cli/tests/test_cli_init.py +6 -1
  51. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  52. hud/cli/tests/test_cli_root.py +140 -0
  53. hud/cli/tests/test_convert.py +361 -0
  54. hud/cli/tests/test_debug.py +12 -10
  55. hud/cli/tests/test_dev.py +197 -0
  56. hud/cli/tests/test_eval.py +251 -0
  57. hud/cli/tests/test_eval_bedrock.py +51 -0
  58. hud/cli/tests/test_init.py +124 -0
  59. hud/cli/tests/test_main_module.py +11 -5
  60. hud/cli/tests/test_mcp_server.py +12 -100
  61. hud/cli/tests/test_push.py +1 -1
  62. hud/cli/tests/test_push_happy.py +74 -0
  63. hud/cli/tests/test_push_wrapper.py +23 -0
  64. hud/cli/tests/test_registry.py +1 -1
  65. hud/cli/tests/test_utils.py +1 -1
  66. hud/cli/{rl → utils}/celebrate.py +14 -12
  67. hud/cli/utils/config.py +18 -1
  68. hud/cli/utils/docker.py +130 -4
  69. hud/cli/utils/env_check.py +9 -9
  70. hud/cli/utils/git.py +136 -0
  71. hud/cli/utils/interactive.py +39 -5
  72. hud/cli/utils/metadata.py +70 -1
  73. hud/cli/utils/runner.py +1 -1
  74. hud/cli/utils/server.py +2 -2
  75. hud/cli/utils/source_hash.py +3 -3
  76. hud/cli/utils/tasks.py +4 -1
  77. hud/cli/utils/tests/__init__.py +0 -0
  78. hud/cli/utils/tests/test_config.py +58 -0
  79. hud/cli/utils/tests/test_docker.py +93 -0
  80. hud/cli/utils/tests/test_docker_hints.py +71 -0
  81. hud/cli/utils/tests/test_env_check.py +74 -0
  82. hud/cli/utils/tests/test_environment.py +42 -0
  83. hud/cli/utils/tests/test_git.py +142 -0
  84. hud/cli/utils/tests/test_interactive_module.py +60 -0
  85. hud/cli/utils/tests/test_local_runner.py +50 -0
  86. hud/cli/utils/tests/test_logging_utils.py +23 -0
  87. hud/cli/utils/tests/test_metadata.py +49 -0
  88. hud/cli/utils/tests/test_package_runner.py +35 -0
  89. hud/cli/utils/tests/test_registry_utils.py +49 -0
  90. hud/cli/utils/tests/test_remote_runner.py +25 -0
  91. hud/cli/utils/tests/test_runner_modules.py +52 -0
  92. hud/cli/utils/tests/test_source_hash.py +36 -0
  93. hud/cli/utils/tests/test_tasks.py +80 -0
  94. hud/cli/utils/version_check.py +258 -0
  95. hud/cli/{rl → utils}/viewer.py +2 -2
  96. hud/clients/README.md +12 -11
  97. hud/clients/__init__.py +4 -3
  98. hud/clients/base.py +166 -26
  99. hud/clients/environment.py +51 -0
  100. hud/clients/fastmcp.py +13 -6
  101. hud/clients/mcp_use.py +45 -15
  102. hud/clients/tests/test_analyze_scenarios.py +206 -0
  103. hud/clients/tests/test_protocol.py +9 -3
  104. hud/datasets/__init__.py +23 -20
  105. hud/datasets/loader.py +326 -0
  106. hud/datasets/runner.py +198 -105
  107. hud/datasets/tests/__init__.py +0 -0
  108. hud/datasets/tests/test_loader.py +221 -0
  109. hud/datasets/tests/test_utils.py +315 -0
  110. hud/datasets/utils.py +270 -90
  111. hud/environment/__init__.py +52 -0
  112. hud/environment/connection.py +258 -0
  113. hud/environment/connectors/__init__.py +33 -0
  114. hud/environment/connectors/base.py +68 -0
  115. hud/environment/connectors/local.py +177 -0
  116. hud/environment/connectors/mcp_config.py +137 -0
  117. hud/environment/connectors/openai.py +101 -0
  118. hud/environment/connectors/remote.py +172 -0
  119. hud/environment/environment.py +835 -0
  120. hud/environment/integrations/__init__.py +45 -0
  121. hud/environment/integrations/adk.py +67 -0
  122. hud/environment/integrations/anthropic.py +196 -0
  123. hud/environment/integrations/gemini.py +92 -0
  124. hud/environment/integrations/langchain.py +82 -0
  125. hud/environment/integrations/llamaindex.py +68 -0
  126. hud/environment/integrations/openai.py +238 -0
  127. hud/environment/mock.py +306 -0
  128. hud/environment/router.py +263 -0
  129. hud/environment/scenarios.py +620 -0
  130. hud/environment/tests/__init__.py +1 -0
  131. hud/environment/tests/test_connection.py +317 -0
  132. hud/environment/tests/test_connectors.py +205 -0
  133. hud/environment/tests/test_environment.py +593 -0
  134. hud/environment/tests/test_integrations.py +257 -0
  135. hud/environment/tests/test_local_connectors.py +242 -0
  136. hud/environment/tests/test_scenarios.py +1086 -0
  137. hud/environment/tests/test_tools.py +208 -0
  138. hud/environment/types.py +23 -0
  139. hud/environment/utils/__init__.py +35 -0
  140. hud/environment/utils/formats.py +215 -0
  141. hud/environment/utils/schema.py +171 -0
  142. hud/environment/utils/tool_wrappers.py +113 -0
  143. hud/eval/__init__.py +67 -0
  144. hud/eval/context.py +727 -0
  145. hud/eval/display.py +299 -0
  146. hud/eval/instrument.py +187 -0
  147. hud/eval/manager.py +533 -0
  148. hud/eval/parallel.py +268 -0
  149. hud/eval/task.py +372 -0
  150. hud/eval/tests/__init__.py +1 -0
  151. hud/eval/tests/test_context.py +178 -0
  152. hud/eval/tests/test_eval.py +210 -0
  153. hud/eval/tests/test_manager.py +152 -0
  154. hud/eval/tests/test_parallel.py +168 -0
  155. hud/eval/tests/test_task.py +291 -0
  156. hud/eval/types.py +65 -0
  157. hud/eval/utils.py +194 -0
  158. hud/patches/__init__.py +19 -0
  159. hud/patches/mcp_patches.py +308 -0
  160. hud/patches/warnings.py +54 -0
  161. hud/samples/browser.py +4 -4
  162. hud/server/__init__.py +2 -1
  163. hud/server/low_level.py +2 -1
  164. hud/server/router.py +164 -0
  165. hud/server/server.py +567 -80
  166. hud/server/tests/test_mcp_server_integration.py +11 -11
  167. hud/server/tests/test_mcp_server_more.py +1 -1
  168. hud/server/tests/test_server_extra.py +2 -0
  169. hud/settings.py +45 -3
  170. hud/shared/exceptions.py +36 -10
  171. hud/shared/hints.py +26 -1
  172. hud/shared/requests.py +15 -3
  173. hud/shared/tests/test_exceptions.py +40 -31
  174. hud/shared/tests/test_hints.py +167 -0
  175. hud/telemetry/__init__.py +20 -19
  176. hud/telemetry/exporter.py +201 -0
  177. hud/telemetry/instrument.py +165 -253
  178. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  179. hud/telemetry/tests/test_exporter.py +258 -0
  180. hud/telemetry/tests/test_instrument.py +401 -0
  181. hud/tools/__init__.py +18 -2
  182. hud/tools/agent.py +223 -0
  183. hud/tools/apply_patch.py +639 -0
  184. hud/tools/base.py +54 -4
  185. hud/tools/bash.py +2 -2
  186. hud/tools/computer/__init__.py +36 -3
  187. hud/tools/computer/anthropic.py +2 -2
  188. hud/tools/computer/gemini.py +385 -0
  189. hud/tools/computer/hud.py +23 -6
  190. hud/tools/computer/openai.py +20 -21
  191. hud/tools/computer/qwen.py +434 -0
  192. hud/tools/computer/settings.py +37 -0
  193. hud/tools/edit.py +3 -7
  194. hud/tools/executors/base.py +4 -2
  195. hud/tools/executors/pyautogui.py +1 -1
  196. hud/tools/grounding/grounded_tool.py +13 -18
  197. hud/tools/grounding/grounder.py +10 -31
  198. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  199. hud/tools/jupyter.py +330 -0
  200. hud/tools/playwright.py +18 -3
  201. hud/tools/shell.py +308 -0
  202. hud/tools/tests/test_agent_tool.py +355 -0
  203. hud/tools/tests/test_apply_patch.py +718 -0
  204. hud/tools/tests/test_computer.py +4 -9
  205. hud/tools/tests/test_computer_actions.py +24 -2
  206. hud/tools/tests/test_jupyter_tool.py +181 -0
  207. hud/tools/tests/test_shell.py +596 -0
  208. hud/tools/tests/test_submit.py +85 -0
  209. hud/tools/tests/test_types.py +193 -0
  210. hud/tools/types.py +21 -1
  211. hud/types.py +194 -56
  212. hud/utils/__init__.py +2 -0
  213. hud/utils/env.py +67 -0
  214. hud/utils/hud_console.py +89 -18
  215. hud/utils/mcp.py +15 -58
  216. hud/utils/strict_schema.py +162 -0
  217. hud/utils/tests/test_init.py +1 -2
  218. hud/utils/tests/test_mcp.py +1 -28
  219. hud/utils/tests/test_pretty_errors.py +186 -0
  220. hud/utils/tests/test_tool_shorthand.py +154 -0
  221. hud/utils/tests/test_version.py +1 -1
  222. hud/utils/types.py +20 -0
  223. hud/version.py +1 -1
  224. hud_python-0.5.13.dist-info/METADATA +264 -0
  225. hud_python-0.5.13.dist-info/RECORD +305 -0
  226. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
  227. hud/agents/langchain.py +0 -261
  228. hud/agents/lite_llm.py +0 -72
  229. hud/cli/rl/__init__.py +0 -180
  230. hud/cli/rl/config.py +0 -101
  231. hud/cli/rl/display.py +0 -133
  232. hud/cli/rl/gpu.py +0 -63
  233. hud/cli/rl/gpu_utils.py +0 -321
  234. hud/cli/rl/local_runner.py +0 -595
  235. hud/cli/rl/presets.py +0 -96
  236. hud/cli/rl/remote_runner.py +0 -463
  237. hud/cli/rl/rl_api.py +0 -150
  238. hud/cli/rl/vllm.py +0 -177
  239. hud/cli/rl/wait_utils.py +0 -89
  240. hud/datasets/parallel.py +0 -687
  241. hud/misc/__init__.py +0 -1
  242. hud/misc/claude_plays_pokemon.py +0 -292
  243. hud/otel/__init__.py +0 -35
  244. hud/otel/collector.py +0 -142
  245. hud/otel/config.py +0 -181
  246. hud/otel/context.py +0 -570
  247. hud/otel/exporters.py +0 -369
  248. hud/otel/instrumentation.py +0 -135
  249. hud/otel/processors.py +0 -121
  250. hud/otel/tests/__init__.py +0 -1
  251. hud/otel/tests/test_processors.py +0 -197
  252. hud/rl/README.md +0 -30
  253. hud/rl/__init__.py +0 -1
  254. hud/rl/actor.py +0 -176
  255. hud/rl/buffer.py +0 -405
  256. hud/rl/chat_template.jinja +0 -101
  257. hud/rl/config.py +0 -192
  258. hud/rl/distributed.py +0 -132
  259. hud/rl/learner.py +0 -637
  260. hud/rl/tests/__init__.py +0 -1
  261. hud/rl/tests/test_learner.py +0 -186
  262. hud/rl/train.py +0 -382
  263. hud/rl/types.py +0 -101
  264. hud/rl/utils/start_vllm_server.sh +0 -30
  265. hud/rl/utils.py +0 -524
  266. hud/rl/vllm_adapter.py +0 -143
  267. hud/telemetry/job.py +0 -352
  268. hud/telemetry/replay.py +0 -74
  269. hud/telemetry/tests/test_replay.py +0 -40
  270. hud/telemetry/tests/test_trace.py +0 -63
  271. hud/telemetry/trace.py +0 -158
  272. hud/utils/agent_factories.py +0 -86
  273. hud/utils/async_utils.py +0 -65
  274. hud/utils/group_eval.py +0 -223
  275. hud/utils/progress.py +0 -149
  276. hud/utils/tasks.py +0 -127
  277. hud/utils/tests/test_async_utils.py +0 -173
  278. hud/utils/tests/test_progress.py +0 -261
  279. hud_python-0.4.45.dist-info/METADATA +0 -552
  280. hud_python-0.4.45.dist-info/RECORD +0 -228
  281. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
  282. {hud_python-0.4.45.dist-info → hud_python-0.5.13.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()
@@ -1,4 +1,4 @@
1
- """Inline JSON preview with expandable view for RL flow.
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, (dict, list)):
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 (legacy)
11
- ├── fastmcp.py # FastMCP based implementation (modern)
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 (Default)
39
+ ### 2. FastMCPHUDClient
40
40
  - Based on the `fastmcp` library
41
- - Modern, clean API with better error handling
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 MCPUseHUDClient, FastMCPHUDClient
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
- # Option 1: MCP-use client
61
- client = MCPUseHUDClient(mcp_config)
60
+ # Default client (MCPUseHUDClient)
61
+ client = MCPClient(mcp_config)
62
62
 
63
- # Option 2: FastMCP client
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 = FastMCPHUDClient(mcp_config)
79
+ client = MCPClient(mcp_config)
79
80
 
80
81
  agent = ClaudeAgent(
81
82
  mcp_client=client,
82
- model="claude-3-7-sonnet-20250219"
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 MCP-use for new features
10
- MCPClient = MCPUseHUDClient
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.so" in url and len(headers.get("Authorization", "")) < 10:
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.so"
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
- # Auto-converts to appropriate HUD exception type with hints
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
- hud_console.info("Shutdown completed")
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
- "input_schema": tool.inputSchema,
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(f"Could not list resources: {e}")
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