hud-python 0.4.14__py3-none-any.whl → 0.4.16__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.

Files changed (42) hide show
  1. hud/agents/base.py +118 -33
  2. hud/agents/claude.py +1 -1
  3. hud/agents/openai.py +5 -16
  4. hud/agents/tests/test_openai.py +24 -79
  5. hud/cli/__init__.py +137 -15
  6. hud/cli/analyze.py +2 -4
  7. hud/cli/build.py +6 -2
  8. hud/cli/dev.py +67 -0
  9. hud/cli/eval.py +90 -35
  10. hud/cli/hf.py +406 -0
  11. hud/cli/init.py +38 -19
  12. hud/cli/rl/README.md +243 -0
  13. hud/cli/rl/__init__.py +82 -0
  14. hud/cli/rl/init.py +370 -0
  15. hud/cli/rl/pod.py +491 -0
  16. hud/cli/rl/ssh.py +288 -0
  17. hud/cli/rl/train.py +421 -0
  18. hud/cli/rl/utils.py +165 -0
  19. hud/cli/tests/test_mcp_server.py +1 -4
  20. hud/clients/base.py +2 -0
  21. hud/clients/fastmcp.py +7 -2
  22. hud/clients/mcp_use.py +3 -1
  23. hud/clients/utils/retry_transport.py +34 -8
  24. hud/datasets/__init__.py +32 -0
  25. hud/datasets/execution/__init__.py +13 -0
  26. hud/datasets/execution/parallel.py +592 -0
  27. hud/datasets/execution/runner.py +123 -0
  28. hud/datasets/task.py +107 -0
  29. hud/datasets/utils.py +118 -0
  30. hud/otel/instrumentation.py +2 -1
  31. hud/server/server.py +58 -21
  32. hud/settings.py +12 -0
  33. hud/types.py +31 -10
  34. hud/utils/design.py +168 -2
  35. hud/utils/tests/test_version.py +1 -1
  36. hud/version.py +1 -1
  37. {hud_python-0.4.14.dist-info → hud_python-0.4.16.dist-info}/METADATA +4 -3
  38. {hud_python-0.4.14.dist-info → hud_python-0.4.16.dist-info}/RECORD +41 -28
  39. hud/datasets.py +0 -327
  40. {hud_python-0.4.14.dist-info → hud_python-0.4.16.dist-info}/WHEEL +0 -0
  41. {hud_python-0.4.14.dist-info → hud_python-0.4.16.dist-info}/entry_points.txt +0 -0
  42. {hud_python-0.4.14.dist-info → hud_python-0.4.16.dist-info}/licenses/LICENSE +0 -0
hud/cli/rl/train.py ADDED
@@ -0,0 +1,421 @@
1
+ """Main RL training command implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import typer
12
+
13
+ from hud.settings import settings
14
+ from hud.utils.design import HUDDesign
15
+
16
+ from .pod import run_prime_training
17
+ from .utils import (
18
+ detect_image_name,
19
+ get_primary_dataset,
20
+ validate_dataset_name,
21
+ )
22
+
23
+ design = HUDDesign()
24
+
25
+
26
+ def train_command_wrapper(
27
+ model: str,
28
+ dataset: str | None,
29
+ config: Path | None,
30
+ gpus: str,
31
+ provider: str,
32
+ output_dir: Path,
33
+ ) -> None:
34
+ """Wrapper to handle interactive prompts before entering async context."""
35
+ # Pre-flight checks for required environment variables
36
+ design.section_title("🔍 Pre-flight Checks")
37
+
38
+ missing_vars = []
39
+
40
+ # Check HUD API key
41
+ if not settings.api_key:
42
+ missing_vars.append("HUD_API_KEY")
43
+ else:
44
+ design.success("✓ HUD_API_KEY configured")
45
+
46
+ # Check WANDB API key (optional but recommended)
47
+ if not getattr(settings, "wandb_api_key", None):
48
+ design.warning("⚠ WANDB_API_KEY not set (optional but recommended for training metrics)")
49
+ else:
50
+ design.success("✓ WANDB_API_KEY configured")
51
+
52
+ # Check PRIME API key (required for remote training)
53
+ if provider == "prime" and not getattr(settings, "prime_api_key", None):
54
+ missing_vars.append("PRIME_API_KEY")
55
+ elif provider == "prime":
56
+ design.success("✓ PRIME_API_KEY configured")
57
+
58
+ if missing_vars:
59
+ design.error(f"Missing required environment variables: {', '.join(missing_vars)}")
60
+ design.info("")
61
+ design.info("Set them using one of these methods:")
62
+ design.info("1. Environment variables:")
63
+ for var in missing_vars:
64
+ design.command_example(f"export {var}=your-{var.lower().replace('_', '-')}")
65
+ design.info("")
66
+ design.info("2. Create a .env file in your project root:")
67
+ design.command_example(
68
+ "\n".join([f"{var}=your-{var.lower().replace('_', '-')}" for var in missing_vars]),
69
+ "env",
70
+ )
71
+ raise typer.Exit(1)
72
+
73
+ # Check for required components
74
+ missing = check_requirements(config, dataset)
75
+
76
+ # Auto-detect config if not specified and exactly one exists
77
+ if not config and "config" not in missing:
78
+ config_dir = Path("configs")
79
+ if config_dir.exists():
80
+ yaml_files = list(config_dir.glob("*.yaml"))
81
+ if len(yaml_files) == 1:
82
+ config = yaml_files[0]
83
+ design.info(f"Using config: {config}")
84
+
85
+ # Store user choice for pod creation
86
+ auto_create_pod = None
87
+ team_id = None
88
+
89
+ if missing:
90
+ # Handle interactive prompts here
91
+ if "config" in missing:
92
+ if missing["config"] == "multiple":
93
+ # Select from multiple configs
94
+ config_dir = Path("configs")
95
+ yaml_files = list(config_dir.glob("*.yaml"))
96
+ config_names = [f.name for f in yaml_files]
97
+ selected_config = design.select(
98
+ "Multiple config files found. Select one:", config_names
99
+ )
100
+ config = config_dir / selected_config
101
+ else:
102
+ # No config found, offer to generate
103
+ generate_config = design.select(
104
+ "No config file found. Would you like to generate one?",
105
+ ["Yes, generate config", "No, I'll create it manually"],
106
+ )
107
+
108
+ if generate_config == "Yes, generate config":
109
+ design.info("Running 'hud rl init' to generate config...")
110
+ design.info("")
111
+ # Import here to avoid circular imports
112
+ from .init import init_command_wrapper
113
+
114
+ init_command_wrapper(".", None, False, False)
115
+
116
+ # Look for generated config
117
+ config_dir = Path("configs")
118
+ if config_dir.exists():
119
+ yaml_files = list(config_dir.glob("*.yaml"))
120
+ if yaml_files:
121
+ config = yaml_files[0]
122
+ design.success(f"Using generated config: {config}")
123
+ else:
124
+ design.error("Config generation failed")
125
+ raise typer.Exit(1)
126
+ else:
127
+ design.info("Please create a config file and try again")
128
+ raise typer.Exit(1)
129
+
130
+ if "dataset" in missing:
131
+ # Check if we have tasks.json
132
+ tasks_file = Path("tasks.json")
133
+ if tasks_file.exists():
134
+ create_dataset = design.select(
135
+ "Found tasks.json. Would you like to upload it as a dataset?",
136
+ ["Yes, upload to HuggingFace", "No, I'll handle it manually"],
137
+ )
138
+
139
+ if create_dataset == "Yes, upload to HuggingFace":
140
+ dataset_name = typer.prompt("Enter dataset name (e.g., username/dataset-name)")
141
+
142
+ if not validate_dataset_name(dataset_name):
143
+ design.error("Invalid dataset name format. Expected: username/dataset-name")
144
+ raise typer.Exit(1)
145
+
146
+ design.info(f"Running 'hud hf tasks.json --name {dataset_name}'...")
147
+ design.info("")
148
+
149
+ # Run hf command
150
+ result = subprocess.run( # noqa: S603
151
+ ["hud", "hf", "tasks.json", "--name", dataset_name], # noqa: S607
152
+ capture_output=True,
153
+ text=True,
154
+ )
155
+
156
+ if result.returncode == 0:
157
+ design.success("Dataset uploaded successfully")
158
+ dataset = dataset_name
159
+ else:
160
+ design.error("Failed to upload dataset")
161
+ if result.stderr:
162
+ design.error(result.stderr)
163
+ raise typer.Exit(1)
164
+ else:
165
+ design.info("Please specify a dataset with --dataset")
166
+ raise typer.Exit(1)
167
+ else:
168
+ design.error("No dataset specified and no tasks.json found")
169
+ design.info("Use --dataset to specify a HuggingFace dataset")
170
+ raise typer.Exit(1)
171
+
172
+ # Ask about pod creation for Prime training
173
+ if provider == "prime":
174
+ # Check if team ID is globally configured
175
+ team_check = subprocess.run(
176
+ ["prime", "config", "view"], # noqa: S607
177
+ capture_output=True,
178
+ text=True,
179
+ )
180
+
181
+ has_global_team = False
182
+ if team_check.returncode == 0:
183
+ # Parse the table output - look for Team ID row
184
+ for line in team_check.stdout.split("\n"):
185
+ if "team id" in line.lower():
186
+ # Check if there's a value after the | separator
187
+ parts = line.split("|")
188
+ if len(parts) >= 2:
189
+ # Get the value part and check if it's not empty
190
+ value = parts[1].strip()
191
+ if value and value != "None":
192
+ has_global_team = True
193
+ design.info("Using globally configured team ID")
194
+ break
195
+
196
+ if not has_global_team:
197
+ # Only ask if no global team is configured
198
+ auto_create_pod = design.select(
199
+ "How would you like to create the Prime Intellect pod?",
200
+ ["Personal account (automated)", "Team account (enter team ID)"],
201
+ )
202
+
203
+ # If team account selected, get the team ID
204
+ if auto_create_pod == "Team account (enter team ID)":
205
+ team_id = typer.prompt("Enter your team ID (e.g., team_abc123def456)")
206
+
207
+ # Save it globally automatically
208
+ subprocess.run(["prime", "config", "set-team-id", team_id]) # noqa: S603, S607
209
+ design.success("Team ID saved globally")
210
+
211
+ auto_create_pod = (
212
+ "Personal account (automated)" # Treat as automated after getting team ID
213
+ )
214
+
215
+ # Now run the async command
216
+ asyncio.run(
217
+ train_command(
218
+ model=model,
219
+ dataset=dataset,
220
+ config=config,
221
+ gpus=gpus,
222
+ provider=provider,
223
+ output_dir=output_dir,
224
+ auto_create_pod=auto_create_pod,
225
+ team_id=team_id,
226
+ )
227
+ )
228
+
229
+
230
+ async def train_command(
231
+ model: str,
232
+ dataset: str | None,
233
+ config: Path | None,
234
+ gpus: str,
235
+ provider: str,
236
+ output_dir: Path,
237
+ auto_create_pod: str | None = None,
238
+ team_id: str | None = None,
239
+ ) -> None:
240
+ """Run RL training on HUD environments."""
241
+ design.header("🤖 HUD RL Training")
242
+
243
+ # Get environment image
244
+ image = detect_image_name()
245
+ if not image:
246
+ design.error("No environment image found")
247
+ design.hint("Run 'hud build' first or specify with 'hud rl init <image>'")
248
+ raise typer.Exit(1)
249
+
250
+ # Validate dataset has sufficient tasks for training
251
+ dataset_size = None
252
+ if dataset:
253
+ design.info(f"Validating dataset: {dataset}")
254
+ try:
255
+ # Try to load dataset info from HuggingFace
256
+ from datasets import load_dataset_builder
257
+
258
+ ds_builder = load_dataset_builder(dataset)
259
+ ds_info = ds_builder.info
260
+
261
+ # Check split sizes
262
+ train_size = ds_info.splits.get("train", None) if ds_info.splits else None
263
+ if train_size and train_size.num_examples < 4:
264
+ design.error(f"Dataset '{dataset}' has only {train_size.num_examples} tasks")
265
+ design.info("RL training requires at least 4 tasks for proper batching")
266
+ design.hint("Consider adding more tasks or duplicating existing ones")
267
+ raise typer.Exit(1)
268
+ elif train_size:
269
+ dataset_size = train_size.num_examples
270
+ design.success(f"✓ Dataset has {dataset_size} tasks")
271
+ except Exception as e:
272
+ # If we can't validate, warn but continue
273
+ design.warning(f"Could not validate dataset size: {e}")
274
+ design.info("Proceeding with training - ensure dataset has at least 4 tasks")
275
+
276
+ # Use dataset from command or lock file
277
+ if not dataset:
278
+ dataset = get_primary_dataset()
279
+ if dataset:
280
+ design.info(f"Using dataset from lock file: {dataset}")
281
+
282
+ # Display configuration
283
+ design.section_title("📋 Training Configuration")
284
+ design.json_config(
285
+ json.dumps(
286
+ {
287
+ "Model": model,
288
+ "Dataset": dataset,
289
+ "Config": str(config) if config else None,
290
+ "Environment": image,
291
+ "GPUs": gpus,
292
+ "Provider": provider,
293
+ "Output": str(output_dir),
294
+ },
295
+ indent=2,
296
+ )
297
+ )
298
+
299
+ if not config:
300
+ design.error("No config file found")
301
+ design.hint("Run 'hud rl init' to generate a config file")
302
+ raise typer.Exit(1)
303
+
304
+ if not dataset:
305
+ design.error("No dataset found")
306
+ design.hint("Run 'hud hf tasks.json' to create a dataset")
307
+ raise typer.Exit(1)
308
+
309
+ # Always run remote training
310
+ await run_remote_training(
311
+ model=model,
312
+ dataset=dataset,
313
+ config=config,
314
+ gpus=gpus,
315
+ provider=provider,
316
+ output_dir=output_dir,
317
+ image=image,
318
+ auto_create_pod=auto_create_pod,
319
+ team_id=team_id,
320
+ dataset_size=dataset_size,
321
+ )
322
+
323
+
324
+ def check_requirements(config: Path | None, dataset: str | None) -> dict[str, Any]:
325
+ """Check if required components are present."""
326
+ missing = {}
327
+
328
+ # Check config
329
+ if not config:
330
+ config_dir = Path("configs")
331
+ if config_dir.exists():
332
+ yaml_files = list(config_dir.glob("*.yaml"))
333
+ if not yaml_files:
334
+ missing["config"] = "none"
335
+ elif len(yaml_files) > 1:
336
+ missing["config"] = "multiple"
337
+ # If exactly one config, we'll use it
338
+ else:
339
+ missing["config"] = "none"
340
+
341
+ # Check dataset
342
+ if not dataset:
343
+ # Check lock file for dataset
344
+ primary_dataset = get_primary_dataset()
345
+ if not primary_dataset:
346
+ missing["dataset"] = "none"
347
+
348
+ return missing
349
+
350
+
351
+ def generate_config_interactive() -> Path | None:
352
+ """Generate config interactively and return the path."""
353
+ from .init import init_command
354
+
355
+ # Run init command
356
+ asyncio.run(init_command(".", None, False, False))
357
+
358
+ # Look for generated config
359
+ config_dir = Path("configs")
360
+ if config_dir.exists():
361
+ yaml_files = list(config_dir.glob("*.yaml"))
362
+ if yaml_files:
363
+ return yaml_files[0]
364
+
365
+ return None
366
+
367
+
368
+ def create_dataset_interactive() -> str | None:
369
+ """Create dataset interactively and return the name."""
370
+ # Check if tasks.json exists
371
+ tasks_file = Path("tasks.json")
372
+ if not tasks_file.exists():
373
+ design.error("No tasks.json file found")
374
+ return None
375
+
376
+ # Prompt for dataset name
377
+ dataset_name = typer.prompt("Enter HuggingFace dataset name (e.g., username/dataset-name)")
378
+
379
+ if not validate_dataset_name(dataset_name):
380
+ design.error("Invalid dataset name format")
381
+ return None
382
+
383
+ # Run hf command
384
+ result = subprocess.run( # noqa: S603
385
+ ["hud", "hf", "tasks.json", "--name", dataset_name], # noqa: S607
386
+ capture_output=True,
387
+ text=True,
388
+ )
389
+
390
+ if result.returncode == 0:
391
+ return dataset_name
392
+ else:
393
+ design.error("Failed to create dataset")
394
+ if result.stderr:
395
+ design.error(result.stderr)
396
+ return None
397
+
398
+
399
+ async def run_remote_training(
400
+ model: str,
401
+ dataset: str,
402
+ config: Path,
403
+ gpus: str,
404
+ provider: str,
405
+ output_dir: Path,
406
+ image: str,
407
+ auto_create_pod: str | None = None,
408
+ team_id: str | None = None,
409
+ dataset_size: int | None = None,
410
+ ) -> None:
411
+ """Run training on remote infrastructure."""
412
+ design.section_title("🚀 Remote Training")
413
+
414
+ if provider == "prime":
415
+ await run_prime_training(
416
+ model, dataset, config, gpus, output_dir, image, auto_create_pod, team_id, dataset_size
417
+ )
418
+ else:
419
+ design.error(f"Provider '{provider}' not yet supported")
420
+ design.info("Currently supported: prime")
421
+ raise typer.Exit(1)
hud/cli/rl/utils.py ADDED
@@ -0,0 +1,165 @@
1
+ """Shared utilities for RL commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import yaml
10
+
11
+ from hud.utils.design import HUDDesign
12
+
13
+ design = HUDDesign()
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def read_lock_file() -> dict[str, Any]:
19
+ """Read and parse hud.lock.yaml file."""
20
+ lock_file = Path("hud.lock.yaml")
21
+ if not lock_file.exists():
22
+ return {}
23
+
24
+ try:
25
+ with open(lock_file) as f:
26
+ return yaml.safe_load(f) or {}
27
+ except Exception as e:
28
+ design.warning(f"Could not read hud.lock.yaml: {e}")
29
+ return {}
30
+
31
+
32
+ def write_lock_file(data: dict[str, Any]) -> bool:
33
+ """Write data to hud.lock.yaml file."""
34
+ lock_file = Path("hud.lock.yaml")
35
+
36
+ try:
37
+ with open(lock_file, "w") as f:
38
+ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
39
+ return True
40
+ except Exception as e:
41
+ design.warning(f"Could not write hud.lock.yaml: {e}")
42
+ return False
43
+
44
+
45
+ def get_mcp_config_from_lock() -> dict[str, Any] | None:
46
+ """Get MCP configuration from lock file."""
47
+ lock_data = read_lock_file()
48
+
49
+ # Check if there's an image reference
50
+ image = lock_data.get("image")
51
+ if image:
52
+ return {
53
+ "hud": {
54
+ "url": "https://mcp.hud.so/v3/mcp",
55
+ "headers": {"Authorization": "Bearer $HUD_API_KEY", "Mcp-Image": image},
56
+ }
57
+ }
58
+
59
+ return None
60
+
61
+
62
+ def get_primary_dataset() -> str | None:
63
+ """Get primary dataset name from lock file."""
64
+ lock_data = read_lock_file()
65
+ return lock_data.get("primary_dataset", {}).get("name")
66
+
67
+
68
+ def get_image_from_lock() -> str | None:
69
+ """Get image name from lock file."""
70
+ lock_data = read_lock_file()
71
+ return lock_data.get("image")
72
+
73
+
74
+ def detect_image_name() -> str | None:
75
+ """Try to detect image name from various sources."""
76
+ # First check lock file
77
+ image = get_image_from_lock()
78
+ if image:
79
+ return image
80
+
81
+ # Check pyproject.toml
82
+ pyproject = Path("pyproject.toml")
83
+ if pyproject.exists():
84
+ try:
85
+ import tomllib
86
+
87
+ with open(pyproject, "rb") as f:
88
+ data = tomllib.load(f)
89
+
90
+ # Check for hud.image_name
91
+ image = data.get("tool", {}).get("hud", {}).get("image_name")
92
+ if image:
93
+ return image
94
+
95
+ # Use project name
96
+ name = data.get("project", {}).get("name")
97
+ if name:
98
+ return f"{name}:latest"
99
+ except Exception:
100
+ logger.warning("Failed to load pyproject.toml")
101
+
102
+ # Use directory name as last resort
103
+ return f"{Path.cwd().name}:latest"
104
+
105
+
106
+ def validate_dataset_name(name: str) -> bool:
107
+ """Validate HuggingFace dataset name format."""
108
+ if not name:
109
+ return False
110
+
111
+ if "/" not in name:
112
+ design.error(f"Invalid dataset name: {name}")
113
+ design.info("Dataset name should be in format: org/dataset")
114
+ return False
115
+
116
+ parts = name.split("/")
117
+ if len(parts) != 2:
118
+ design.error(f"Invalid dataset name: {name}")
119
+ return False
120
+
121
+ org, dataset = parts
122
+ if not org or not dataset:
123
+ design.error(f"Invalid dataset name: {name}")
124
+ return False
125
+
126
+ # Check for valid characters (alphanumeric, dash, underscore)
127
+ import re
128
+
129
+ if not re.match(r"^[a-zA-Z0-9_-]+$", org) or not re.match(r"^[a-zA-Z0-9_-]+$", dataset):
130
+ design.error(f"Invalid characters in dataset name: {name}")
131
+ design.info("Use only letters, numbers, dashes, and underscores")
132
+ return False
133
+
134
+ return True
135
+
136
+
137
+ def create_tasks_template() -> list[dict[str, Any]]:
138
+ """Create a template for tasks.json file."""
139
+ return [
140
+ {
141
+ "id": "example-task-001",
142
+ "prompt": "Complete the first TODO item in the list",
143
+ "mcp_config": {
144
+ "# TODO": "Add your MCP configuration here",
145
+ "# Example for remote": {
146
+ "hud": {
147
+ "url": "https://mcp.hud.so/v3/mcp",
148
+ "headers": {
149
+ "Authorization": "Bearer $HUD_API_KEY",
150
+ "Mcp-Image": "your-org/your-env:latest",
151
+ },
152
+ }
153
+ },
154
+ "# Example for local": {
155
+ "local": {"command": "docker", "args": ["run", "--rm", "-i", "your-env:latest"]}
156
+ },
157
+ },
158
+ "setup_tool": {"name": "setup", "arguments": {"name": "todo_seed", "num_items": 5}},
159
+ "evaluate_tool": {
160
+ "name": "evaluate",
161
+ "arguments": {"name": "todo_completed", "expected_count": 1},
162
+ },
163
+ "metadata": {"difficulty": "easy", "category": "task_completion"},
164
+ }
165
+ ]
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING
5
+ from pathlib import Path # noqa: TC003
6
6
  from unittest.mock import MagicMock, patch
7
7
 
8
8
  import pytest
@@ -16,9 +16,6 @@ from hud.cli.dev import (
16
16
  update_pyproject_toml,
17
17
  )
18
18
 
19
- if TYPE_CHECKING:
20
- from pathlib import Path
21
-
22
19
 
23
20
  class TestCreateMCPServer:
24
21
  """Test MCP server creation."""
hud/clients/base.py CHANGED
@@ -286,6 +286,8 @@ class BaseHUDClient(AgentMCPClient):
286
286
  logger.info("📡 Telemetry data fetched:")
287
287
  if "live_url" in telemetry_data:
288
288
  logger.info(" 🖥️ Live URL: %s", telemetry_data["live_url"])
289
+ if "vnc_url" in telemetry_data:
290
+ logger.info(" 🖥️ VNC URL: %s", telemetry_data["vnc_url"])
289
291
  if "cdp_url" in telemetry_data:
290
292
  logger.info(" 🦾 CDP URL: %s", telemetry_data["cdp_url"])
291
293
  if "status" in telemetry_data:
hud/clients/fastmcp.py CHANGED
@@ -106,8 +106,13 @@ class FastMCPHUDClient(BaseHUDClient):
106
106
 
107
107
  # Configure validation for output schemas based on client setting
108
108
  try:
109
- if hasattr(self._client, "_session_state") and self._client._session_state.session is not None: # noqa: E501
110
- self._client._session_state.session._validate_structured_outputs = self._strict_validation # noqa: E501
109
+ if (
110
+ hasattr(self._client, "_session_state")
111
+ and self._client._session_state.session is not None
112
+ ):
113
+ self._client._session_state.session._validate_structured_outputs = (
114
+ self._strict_validation
115
+ )
111
116
  except ImportError:
112
117
  pass
113
118
 
hud/clients/mcp_use.py CHANGED
@@ -79,7 +79,9 @@ class MCPUseHUDClient(BaseHUDClient):
79
79
  and hasattr(session.connector, "client_session")
80
80
  and session.connector.client_session is not None
81
81
  ):
82
- session.connector.client_session._validate_structured_outputs = self._strict_validation # noqa: E501
82
+ session.connector.client_session._validate_structured_outputs = (
83
+ self._strict_validation
84
+ )
83
85
  except ImportError:
84
86
  # ValidationOptions may not be available in some mcp versions
85
87
  pass