hud-python 0.4.2__py3-none-any.whl → 0.4.3__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/langchain.py +12 -1
- hud/agents/misc/response_agent.py +3 -1
- hud/cli/build.py +98 -27
- hud/cli/init.py +14 -15
- hud/cli/mcp_server.py +55 -80
- hud/cli/pull.py +19 -25
- hud/cli/push.py +92 -58
- hud/clients/base.py +2 -3
- hud/clients/fastmcp.py +22 -2
- hud/clients/mcp_use.py +35 -15
- hud/datasets.py +5 -0
- hud/utils/design.py +106 -38
- hud/utils/mcp.py +1 -1
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.2.dist-info → hud_python-0.4.3.dist-info}/METADATA +2 -2
- {hud_python-0.4.2.dist-info → hud_python-0.4.3.dist-info}/RECORD +20 -20
- {hud_python-0.4.2.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.2.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.2.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +0 -0
hud/cli/pull.py
CHANGED
|
@@ -9,14 +9,11 @@ import click
|
|
|
9
9
|
import requests
|
|
10
10
|
import typer
|
|
11
11
|
import yaml
|
|
12
|
-
from rich.console import Console
|
|
13
12
|
from rich.table import Table
|
|
14
13
|
|
|
15
14
|
from hud.settings import settings
|
|
16
15
|
from hud.utils.design import HUDDesign
|
|
17
16
|
|
|
18
|
-
console = Console()
|
|
19
|
-
|
|
20
17
|
|
|
21
18
|
def get_docker_manifest(image: str) -> dict | None:
|
|
22
19
|
"""Get manifest from Docker registry without pulling the image."""
|
|
@@ -32,8 +29,7 @@ def get_docker_manifest(image: str) -> dict | None:
|
|
|
32
29
|
|
|
33
30
|
return json.loads(result.stdout)
|
|
34
31
|
except Exception:
|
|
35
|
-
|
|
36
|
-
return None
|
|
32
|
+
return None
|
|
37
33
|
|
|
38
34
|
|
|
39
35
|
def get_image_size_from_manifest(manifest: dict) -> int | None:
|
|
@@ -52,7 +48,6 @@ def get_image_size_from_manifest(manifest: dict) -> int | None:
|
|
|
52
48
|
|
|
53
49
|
return total_size if total_size > 0 else None
|
|
54
50
|
except Exception:
|
|
55
|
-
click.echo("Failed to get image size from manifest", err=True)
|
|
56
51
|
return None
|
|
57
52
|
|
|
58
53
|
|
|
@@ -200,10 +195,8 @@ def pull_environment(
|
|
|
200
195
|
# Minimal data from Docker manifest
|
|
201
196
|
table.add_row("Source", "Docker Registry")
|
|
202
197
|
if not yes:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)
|
|
206
|
-
console.print("For full environment details, use a lock file.\n")
|
|
198
|
+
design.warning("Note: Limited metadata available from Docker registry.")
|
|
199
|
+
design.info("For full environment details, use a lock file.\n")
|
|
207
200
|
else:
|
|
208
201
|
# Full lock file data
|
|
209
202
|
if "build" in lock_data:
|
|
@@ -233,18 +226,19 @@ def pull_environment(
|
|
|
233
226
|
# No metadata available
|
|
234
227
|
table.add_row("Source", "Unknown")
|
|
235
228
|
|
|
236
|
-
console
|
|
229
|
+
# Use design's console to maintain consistent output
|
|
230
|
+
design.console.print(table)
|
|
237
231
|
|
|
238
232
|
# Tool summary (show after table)
|
|
239
233
|
if lock_data and "tools" in lock_data:
|
|
240
|
-
|
|
234
|
+
design.section_title("Available Tools")
|
|
241
235
|
for tool in lock_data["tools"]:
|
|
242
|
-
|
|
236
|
+
design.info(f"• {tool['name']}: {tool['description']}")
|
|
243
237
|
|
|
244
238
|
# Show warnings if no metadata
|
|
245
239
|
if not lock_data and not yes:
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
design.warning("No metadata available for this image.")
|
|
241
|
+
design.info("The image will be pulled without verification.")
|
|
248
242
|
|
|
249
243
|
# If verify only, stop here
|
|
250
244
|
if verify_only:
|
|
@@ -253,7 +247,7 @@ def pull_environment(
|
|
|
253
247
|
|
|
254
248
|
# Ask for confirmation unless --yes
|
|
255
249
|
if not yes:
|
|
256
|
-
|
|
250
|
+
design.info("")
|
|
257
251
|
# Show simple name for confirmation, not the full digest
|
|
258
252
|
if ":" in image_ref and "@" in image_ref:
|
|
259
253
|
simple_name = image_ref.split("@")[0]
|
|
@@ -278,7 +272,7 @@ def pull_environment(
|
|
|
278
272
|
|
|
279
273
|
for line in process.stdout or []:
|
|
280
274
|
if verbose or "Downloading" in line or "Extracting" in line or "Pull complete" in line:
|
|
281
|
-
|
|
275
|
+
design.info(line.rstrip())
|
|
282
276
|
|
|
283
277
|
process.wait()
|
|
284
278
|
|
|
@@ -311,14 +305,14 @@ def pull_environment(
|
|
|
311
305
|
# Extract simple name for examples
|
|
312
306
|
simple_ref = image_ref.split("@")[0] if ":" in image_ref and "@" in image_ref else image_ref
|
|
313
307
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
308
|
+
design.info("1. Quick analysis (from metadata):")
|
|
309
|
+
design.command_example(f"hud analyze {simple_ref}")
|
|
310
|
+
design.info("")
|
|
311
|
+
design.info("2. Live analysis (runs container):")
|
|
312
|
+
design.command_example(f"hud analyze {simple_ref} --live")
|
|
313
|
+
design.info("")
|
|
314
|
+
design.info("3. Run the environment:")
|
|
315
|
+
design.command_example(f"hud run {simple_ref}")
|
|
322
316
|
|
|
323
317
|
|
|
324
318
|
def pull_command(
|
hud/cli/push.py
CHANGED
|
@@ -6,17 +6,13 @@ import json
|
|
|
6
6
|
import subprocess
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
import click
|
|
10
9
|
import requests
|
|
11
10
|
import typer
|
|
12
11
|
import yaml
|
|
13
|
-
from rich.console import Console
|
|
14
12
|
|
|
15
13
|
from hud.settings import settings
|
|
16
14
|
from hud.utils.design import HUDDesign
|
|
17
15
|
|
|
18
|
-
console = Console()
|
|
19
|
-
|
|
20
16
|
|
|
21
17
|
def get_docker_username() -> str | None:
|
|
22
18
|
"""Get the current Docker username if logged in."""
|
|
@@ -51,9 +47,9 @@ def get_docker_username() -> str | None:
|
|
|
51
47
|
if username and username != "token": # Skip token-based auth
|
|
52
48
|
return username
|
|
53
49
|
except Exception:
|
|
54
|
-
|
|
50
|
+
pass # Silent failure, try other methods
|
|
55
51
|
except Exception:
|
|
56
|
-
|
|
52
|
+
pass # Silent failure, try other methods
|
|
57
53
|
|
|
58
54
|
# Alternative: Check credsStore/credHelpers
|
|
59
55
|
for config_path in config_paths:
|
|
@@ -89,11 +85,11 @@ def get_docker_username() -> str | None:
|
|
|
89
85
|
if username and username != "token":
|
|
90
86
|
return username
|
|
91
87
|
except Exception:
|
|
92
|
-
|
|
88
|
+
pass # Silent failure, try other methods
|
|
93
89
|
except Exception:
|
|
94
|
-
|
|
90
|
+
pass # Silent failure, try other methods
|
|
95
91
|
except Exception:
|
|
96
|
-
|
|
92
|
+
pass # Silent failure, try other methods
|
|
97
93
|
return None
|
|
98
94
|
|
|
99
95
|
|
|
@@ -108,7 +104,6 @@ def get_docker_image_labels(image: str) -> dict:
|
|
|
108
104
|
)
|
|
109
105
|
return json.loads(result.stdout.strip()) or {}
|
|
110
106
|
except Exception:
|
|
111
|
-
click.echo("Failed to get Docker image labels", err=True)
|
|
112
107
|
return {}
|
|
113
108
|
|
|
114
109
|
|
|
@@ -136,11 +131,12 @@ def push_environment(
|
|
|
136
131
|
# Check for API key first
|
|
137
132
|
if not settings.api_key:
|
|
138
133
|
design.error("No HUD API key found")
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
134
|
+
design.warning("A HUD API key is required to push environments.")
|
|
135
|
+
design.info("\nTo get started:")
|
|
136
|
+
design.info("1. Get your API key at: https://hud.so/settings")
|
|
137
|
+
design.command_example("export HUD_API_KEY=your-key-here", "Set your API key")
|
|
138
|
+
design.command_example("hud push", "Try again")
|
|
139
|
+
design.info("")
|
|
144
140
|
raise typer.Exit(1)
|
|
145
141
|
|
|
146
142
|
# Load lock file
|
|
@@ -152,6 +148,9 @@ def push_environment(
|
|
|
152
148
|
if not local_image and "build" in lock_data:
|
|
153
149
|
# New format might have image elsewhere
|
|
154
150
|
local_image = lock_data.get("image", "")
|
|
151
|
+
|
|
152
|
+
# Get internal version from lock file
|
|
153
|
+
internal_version = lock_data.get("build", {}).get("version", None)
|
|
155
154
|
|
|
156
155
|
# If no image specified, try to be smart
|
|
157
156
|
if not image:
|
|
@@ -172,14 +171,20 @@ def push_environment(
|
|
|
172
171
|
if "/" in base_name:
|
|
173
172
|
base_name = base_name.split("/")[-1]
|
|
174
173
|
|
|
175
|
-
# Use provided tag or
|
|
176
|
-
|
|
174
|
+
# Use provided tag, or internal version, or current tag as fallback
|
|
175
|
+
if tag:
|
|
176
|
+
final_tag = tag
|
|
177
|
+
design.info(f"Using specified tag: {tag}")
|
|
178
|
+
elif internal_version:
|
|
179
|
+
final_tag = internal_version
|
|
180
|
+
design.info(f"Using internal version from lock file: {internal_version}")
|
|
181
|
+
else:
|
|
182
|
+
final_tag = current_tag
|
|
183
|
+
design.info(f"Using current tag: {current_tag}")
|
|
177
184
|
|
|
178
185
|
# Suggest a registry image
|
|
179
186
|
image = f"{username}/{base_name}:{final_tag}"
|
|
180
187
|
design.info(f"Auto-detected Docker username: {username}")
|
|
181
|
-
if tag:
|
|
182
|
-
design.info(f"Using specified tag: {tag}")
|
|
183
188
|
design.info(f"Will push to: {image}")
|
|
184
189
|
|
|
185
190
|
if not yes and not typer.confirm(f"\nPush to {image}?"):
|
|
@@ -190,44 +195,77 @@ def push_environment(
|
|
|
190
195
|
"Not logged in to Docker Hub. Please specify --image or run 'docker login'"
|
|
191
196
|
)
|
|
192
197
|
raise typer.Exit(1)
|
|
193
|
-
elif tag:
|
|
198
|
+
elif tag or internal_version:
|
|
194
199
|
# Handle tag when image is provided
|
|
200
|
+
# Prefer explicit tag over internal version
|
|
201
|
+
final_tag = tag if tag else internal_version
|
|
202
|
+
|
|
195
203
|
if ":" in image:
|
|
196
204
|
# Image already has a tag
|
|
197
205
|
existing_tag = image.split(":")[-1]
|
|
198
|
-
if existing_tag !=
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
if existing_tag != final_tag:
|
|
207
|
+
if tag:
|
|
208
|
+
design.warning(f"Image already has tag '{existing_tag}', overriding with '{final_tag}'")
|
|
209
|
+
else:
|
|
210
|
+
design.info(f"Image has tag '{existing_tag}', but using internal version '{final_tag}'")
|
|
211
|
+
image = image.rsplit(":", 1)[0] + f":{final_tag}"
|
|
201
212
|
# else: tags match, no action needed
|
|
202
213
|
else:
|
|
203
|
-
# Image has no tag, append the
|
|
204
|
-
image = f"{image}:{
|
|
205
|
-
|
|
214
|
+
# Image has no tag, append the appropriate one
|
|
215
|
+
image = f"{image}:{final_tag}"
|
|
216
|
+
|
|
217
|
+
if tag:
|
|
218
|
+
design.info(f"Using specified tag: {tag}")
|
|
219
|
+
else:
|
|
220
|
+
design.info(f"Using internal version from lock file: {internal_version}")
|
|
206
221
|
design.info(f"Will push to: {image}")
|
|
207
222
|
|
|
208
223
|
# Verify local image exists
|
|
209
224
|
# Extract the tag part (before @sha256:...) for Docker operations
|
|
210
225
|
local_tag = local_image.split("@")[0] if "@" in local_image else local_image
|
|
211
|
-
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
226
|
+
|
|
227
|
+
# Also check for version-tagged image if we have internal version
|
|
228
|
+
version_tag = None
|
|
229
|
+
if internal_version and ":" in local_tag:
|
|
230
|
+
base_name = local_tag.split(":")[0]
|
|
231
|
+
version_tag = f"{base_name}:{internal_version}"
|
|
232
|
+
|
|
233
|
+
# Try to find the image - prefer version tag if it exists
|
|
234
|
+
image_to_push = None
|
|
235
|
+
if version_tag:
|
|
236
|
+
try:
|
|
237
|
+
subprocess.run(["docker", "inspect", version_tag], capture_output=True, check=True) # noqa: S603, S607
|
|
238
|
+
image_to_push = version_tag
|
|
239
|
+
design.info(f"Found version-tagged image: {version_tag}")
|
|
240
|
+
except subprocess.CalledProcessError:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
if not image_to_push:
|
|
244
|
+
try:
|
|
245
|
+
subprocess.run(["docker", "inspect", local_tag], capture_output=True, check=True) # noqa: S603, S607
|
|
246
|
+
image_to_push = local_tag
|
|
247
|
+
except subprocess.CalledProcessError:
|
|
248
|
+
design.error(f"Local image not found: {local_tag}")
|
|
249
|
+
if version_tag:
|
|
250
|
+
design.error(f"Also tried: {version_tag}")
|
|
251
|
+
design.info("Run 'hud build' first to create the image")
|
|
252
|
+
raise typer.Exit(1) # noqa: B904
|
|
219
253
|
|
|
220
254
|
# Check if local image has the expected label
|
|
221
|
-
labels = get_docker_image_labels(
|
|
255
|
+
labels = get_docker_image_labels(image_to_push)
|
|
222
256
|
expected_label = labels.get("org.hud.manifest.head", "")
|
|
257
|
+
version_label = labels.get("org.hud.version", "")
|
|
223
258
|
|
|
224
259
|
# Skip hash verification - the lock file may have been updated with digest after build
|
|
225
|
-
if verbose
|
|
226
|
-
|
|
260
|
+
if verbose:
|
|
261
|
+
if expected_label:
|
|
262
|
+
design.info(f"Image label: {expected_label[:12]}...")
|
|
263
|
+
if version_label:
|
|
264
|
+
design.info(f"Version label: {version_label}")
|
|
227
265
|
|
|
228
266
|
# Tag the image for push
|
|
229
|
-
design.progress_message(f"Tagging {
|
|
230
|
-
subprocess.run(["docker", "tag",
|
|
267
|
+
design.progress_message(f"Tagging {image_to_push} as {image}")
|
|
268
|
+
subprocess.run(["docker", "tag", image_to_push, image], check=True) # noqa: S603, S607
|
|
231
269
|
|
|
232
270
|
# Push the image
|
|
233
271
|
design.progress_message(f"Pushing {image} to registry...")
|
|
@@ -243,7 +281,7 @@ def push_environment(
|
|
|
243
281
|
)
|
|
244
282
|
|
|
245
283
|
for line in process.stdout or []:
|
|
246
|
-
|
|
284
|
+
design.info(line.rstrip())
|
|
247
285
|
|
|
248
286
|
process.wait()
|
|
249
287
|
|
|
@@ -267,8 +305,8 @@ def push_environment(
|
|
|
267
305
|
design.success("Push complete!")
|
|
268
306
|
|
|
269
307
|
# Show the final image reference
|
|
270
|
-
|
|
271
|
-
|
|
308
|
+
design.section_title("Pushed Image")
|
|
309
|
+
design.status_item("Registry", pushed_digest, primary=True)
|
|
272
310
|
|
|
273
311
|
# Update the lock file with registry information
|
|
274
312
|
lock_data["image"] = pushed_digest
|
|
@@ -286,7 +324,7 @@ def push_environment(
|
|
|
286
324
|
with open(lock_path, "w") as f:
|
|
287
325
|
yaml.dump(lock_data, f, default_flow_style=False, sort_keys=False)
|
|
288
326
|
|
|
289
|
-
|
|
327
|
+
design.success("Updated lock file with registry image")
|
|
290
328
|
|
|
291
329
|
# Upload lock file to HUD registry
|
|
292
330
|
try:
|
|
@@ -324,34 +362,30 @@ def push_environment(
|
|
|
324
362
|
|
|
325
363
|
if response.status_code in [200, 201]:
|
|
326
364
|
design.success("Metadata uploaded to HUD registry")
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
)
|
|
365
|
+
design.info("Others can now pull with:")
|
|
366
|
+
design.command_example(f"hud pull {name_with_tag}")
|
|
367
|
+
design.info("")
|
|
330
368
|
else:
|
|
331
369
|
design.warning(f"Could not upload to registry: {response.status_code}")
|
|
332
370
|
if verbose:
|
|
333
371
|
design.info(f"Response: {response.text}")
|
|
334
|
-
|
|
372
|
+
design.info("Share hud.lock.yaml manually\n")
|
|
335
373
|
else:
|
|
336
374
|
if verbose:
|
|
337
375
|
design.info("Could not parse registry path for upload")
|
|
338
|
-
|
|
339
|
-
" Share [cyan]hud.lock.yaml[/cyan] to let others reproduce your exact environment\n" # noqa: E501
|
|
340
|
-
)
|
|
376
|
+
design.info("Share hud.lock.yaml to let others reproduce your exact environment\n")
|
|
341
377
|
except Exception as e:
|
|
342
378
|
design.warning(f"Registry upload failed: {e}")
|
|
343
|
-
|
|
379
|
+
design.info("Share hud.lock.yaml manually\n")
|
|
344
380
|
|
|
345
381
|
# Show usage examples
|
|
346
382
|
design.section_title("What's Next?")
|
|
347
383
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
" Share the updated [cyan]hud.lock.yaml[/cyan] for others to reproduce your exact environment" # noqa: E501
|
|
354
|
-
)
|
|
384
|
+
design.info("Test locally:")
|
|
385
|
+
design.command_example(f"hud run {image}")
|
|
386
|
+
design.info("")
|
|
387
|
+
design.info("Share environment:")
|
|
388
|
+
design.info(" Share the updated hud.lock.yaml for others to reproduce your exact environment")
|
|
355
389
|
|
|
356
390
|
# TODO: Upload lock file to HUD registry
|
|
357
391
|
if sign:
|
hud/clients/base.py
CHANGED
|
@@ -149,10 +149,9 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
149
149
|
"Please ensure your HUD_API_KEY environment variable is set correctly. "
|
|
150
150
|
"You can get an API key at https://app.hud.so"
|
|
151
151
|
) from e
|
|
152
|
-
# Generic 401 error
|
|
153
152
|
raise RuntimeError(
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
"Authentication failed (401 Unauthorized). "
|
|
154
|
+
"Please check your credentials or API key."
|
|
156
155
|
) from e
|
|
157
156
|
raise
|
|
158
157
|
|
hud/clients/fastmcp.py
CHANGED
|
@@ -73,7 +73,7 @@ class FastMCPHUDClient(BaseHUDClient):
|
|
|
73
73
|
return
|
|
74
74
|
|
|
75
75
|
# Create FastMCP client with the custom transport
|
|
76
|
-
timeout =
|
|
76
|
+
timeout = 10 * 60 # 5 minutes
|
|
77
77
|
os.environ["FASTMCP_CLIENT_INIT_TIMEOUT"] = str(timeout)
|
|
78
78
|
|
|
79
79
|
# Create custom transport with retry support for HTTP servers
|
|
@@ -82,7 +82,27 @@ class FastMCPHUDClient(BaseHUDClient):
|
|
|
82
82
|
|
|
83
83
|
if self._stack is None:
|
|
84
84
|
self._stack = AsyncExitStack()
|
|
85
|
-
|
|
85
|
+
try:
|
|
86
|
+
await self._stack.enter_async_context(self._client)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
# Check for authentication errors
|
|
89
|
+
error_msg = str(e)
|
|
90
|
+
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
91
|
+
# Check if connecting to HUD API
|
|
92
|
+
for server_config in mcp_config.values():
|
|
93
|
+
url = server_config.get("url", "")
|
|
94
|
+
if "mcp.hud.so" in url:
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
"Authentication failed for HUD API. "
|
|
97
|
+
"Please ensure your HUD_API_KEY environment variable is set correctly. "
|
|
98
|
+
"You can get an API key at https://app.hud.so"
|
|
99
|
+
) from e
|
|
100
|
+
# Generic 401 error
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
f"Authentication failed (401 Unauthorized). "
|
|
103
|
+
f"Please check your credentials or API key."
|
|
104
|
+
) from e
|
|
105
|
+
raise
|
|
86
106
|
|
|
87
107
|
# Configure validation for output schemas based on client setting
|
|
88
108
|
from mcp.client.session import ValidationOptions
|
hud/clients/mcp_use.py
CHANGED
|
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any
|
|
|
7
7
|
|
|
8
8
|
from mcp import Implementation
|
|
9
9
|
from mcp.shared.exceptions import McpError
|
|
10
|
-
from mcp_use.client import MCPClient as MCPUseClient
|
|
11
10
|
from pydantic import AnyUrl
|
|
12
11
|
|
|
13
12
|
from hud.types import MCPToolCall, MCPToolResult
|
|
@@ -17,8 +16,16 @@ from .base import BaseHUDClient
|
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
19
18
|
from mcp import types
|
|
19
|
+
from mcp_use.client import MCPClient as MCPUseClient
|
|
20
20
|
from mcp_use.session import MCPSession as MCPUseSession
|
|
21
21
|
|
|
22
|
+
try:
|
|
23
|
+
from mcp_use.client import MCPClient as MCPUseClient
|
|
24
|
+
from mcp_use.session import MCPSession as MCPUseSession
|
|
25
|
+
except ImportError:
|
|
26
|
+
MCPUseClient = None # type: ignore[misc, assignment]
|
|
27
|
+
MCPUseSession = None # type: ignore[misc, assignment]
|
|
28
|
+
|
|
22
29
|
logger = logging.getLogger(__name__)
|
|
23
30
|
|
|
24
31
|
|
|
@@ -39,9 +46,15 @@ class MCPUseHUDClient(BaseHUDClient):
|
|
|
39
46
|
"""
|
|
40
47
|
super().__init__(mcp_config=mcp_config, **kwargs)
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
if MCPUseClient is None or MCPUseSession is None:
|
|
50
|
+
raise ImportError(
|
|
51
|
+
"MCP-use dependencies are not available. "
|
|
52
|
+
"Please install the optional agent dependencies: pip install 'hud-python[agent]'"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self._sessions: dict[str, Any] = {} # Will be MCPUseSession when available
|
|
43
56
|
self._tool_map: dict[str, tuple[str, types.Tool]] = {}
|
|
44
|
-
self._client:
|
|
57
|
+
self._client: Any | None = None # Will be MCPUseClient when available
|
|
45
58
|
|
|
46
59
|
async def _connect(self, mcp_config: dict[str, dict[str, Any]]) -> None:
|
|
47
60
|
"""Create all sessions for MCP-use client."""
|
|
@@ -50,23 +63,30 @@ class MCPUseHUDClient(BaseHUDClient):
|
|
|
50
63
|
return
|
|
51
64
|
|
|
52
65
|
config = {"mcpServers": mcp_config}
|
|
66
|
+
if MCPUseClient is None:
|
|
67
|
+
raise ImportError("MCPUseClient is not available")
|
|
53
68
|
self._client = MCPUseClient.from_dict(config)
|
|
54
69
|
try:
|
|
70
|
+
assert self._client is not None # For type checker
|
|
55
71
|
self._sessions = await self._client.create_all_sessions()
|
|
56
72
|
logger.info("Created %d MCP sessions", len(self._sessions))
|
|
57
73
|
|
|
58
74
|
# Configure validation for all sessions based on client setting
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
try:
|
|
76
|
+
from mcp.client.session import ValidationOptions # type: ignore[import-not-found]
|
|
77
|
+
|
|
78
|
+
for session in self._sessions.values():
|
|
79
|
+
if (
|
|
80
|
+
hasattr(session, "connector")
|
|
81
|
+
and hasattr(session.connector, "client_session")
|
|
82
|
+
and session.connector.client_session is not None
|
|
83
|
+
):
|
|
84
|
+
session.connector.client_session._validation_options = ValidationOptions(
|
|
85
|
+
strict_output_validation=self._strict_validation
|
|
86
|
+
)
|
|
87
|
+
except ImportError:
|
|
88
|
+
# ValidationOptions may not be available in some mcp versions
|
|
89
|
+
pass
|
|
70
90
|
|
|
71
91
|
# Log session details in verbose mode
|
|
72
92
|
if self.verbose and self._sessions:
|
|
@@ -273,6 +293,6 @@ class MCPUseHUDClient(BaseHUDClient):
|
|
|
273
293
|
logger.debug("MCP-use client disconnected")
|
|
274
294
|
|
|
275
295
|
# Legacy compatibility methods (limited; tests should not rely on these)
|
|
276
|
-
def get_sessions(self) -> dict[str,
|
|
296
|
+
def get_sessions(self) -> dict[str, Any]:
|
|
277
297
|
"""Get active MCP sessions."""
|
|
278
298
|
return self._sessions
|
hud/datasets.py
CHANGED
|
@@ -12,6 +12,7 @@ from datasets import Dataset, load_dataset
|
|
|
12
12
|
from pydantic import BaseModel, Field, field_validator
|
|
13
13
|
|
|
14
14
|
from hud.agents.misc import ResponseAgent
|
|
15
|
+
from hud.settings import settings
|
|
15
16
|
|
|
16
17
|
from .types import MCPToolCall
|
|
17
18
|
|
|
@@ -94,6 +95,10 @@ class Task(BaseModel):
|
|
|
94
95
|
|
|
95
96
|
# Start with current environment variables
|
|
96
97
|
mapping = dict(os.environ)
|
|
98
|
+
mapping.update(settings.model_dump())
|
|
99
|
+
|
|
100
|
+
if settings.api_key:
|
|
101
|
+
mapping["HUD_API_KEY"] = settings.api_key
|
|
97
102
|
|
|
98
103
|
def substitute_in_value(obj: Any) -> Any:
|
|
99
104
|
"""Recursively substitute variables in nested structures."""
|