hud-python 0.4.56__py3-none-any.whl → 0.4.58__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/cli/__init__.py +20 -7
- hud/cli/dev.py +135 -5
- hud/cli/eval.py +2 -2
- hud/cli/flows/dev.py +10 -19
- hud/cli/init.py +14 -18
- hud/cli/push.py +2 -2
- hud/cli/rl/__init__.py +1 -1
- hud/cli/rl/celebrate.py +1 -1
- hud/cli/rl/remote_runner.py +3 -3
- hud/cli/tests/test_convert.py +367 -0
- hud/cli/utils/version_check.py +7 -6
- hud/clients/base.py +29 -3
- hud/clients/fastmcp.py +3 -3
- hud/clients/mcp_use.py +2 -2
- hud/clients/tests/test_protocol.py +9 -3
- hud/otel/config.py +1 -1
- hud/otel/context.py +2 -2
- hud/server/server.py +306 -0
- hud/shared/hints.py +3 -3
- hud/telemetry/job.py +2 -2
- hud/tools/playwright.py +8 -1
- hud/types.py +1 -1
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.56.dist-info → hud_python-0.4.58.dist-info}/METADATA +1 -1
- {hud_python-0.4.56.dist-info → hud_python-0.4.58.dist-info}/RECORD +29 -28
- {hud_python-0.4.56.dist-info → hud_python-0.4.58.dist-info}/WHEEL +0 -0
- {hud_python-0.4.56.dist-info → hud_python-0.4.58.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.56.dist-info → hud_python-0.4.58.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Tests for the convert command."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from hud.cli.flows.tasks import convert_tasks_to_remote
|
|
11
|
+
from hud.types import Task
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestConvertCommand:
|
|
15
|
+
"""Test the convert command functionality."""
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def temp_tasks_file(self, tmp_path):
|
|
19
|
+
"""Create a temporary tasks file."""
|
|
20
|
+
tasks = [
|
|
21
|
+
{
|
|
22
|
+
"prompt": "Test task 1",
|
|
23
|
+
"mcp_config": {
|
|
24
|
+
"local": {
|
|
25
|
+
"command": "docker",
|
|
26
|
+
"args": ["run", "--rm", "-i", "test-image:latest"],
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
tasks_file = tmp_path / "tasks.json"
|
|
32
|
+
tasks_file.write_text(json.dumps(tasks))
|
|
33
|
+
return tasks_file
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def mock_env_dir(self, tmp_path):
|
|
37
|
+
"""Create a mock environment directory with lock file."""
|
|
38
|
+
env_dir = tmp_path / "env"
|
|
39
|
+
env_dir.mkdir()
|
|
40
|
+
|
|
41
|
+
# Create lock file
|
|
42
|
+
lock_data = {
|
|
43
|
+
"images": {
|
|
44
|
+
"remote": "registry.hud.so/test-org/test-env:v1.0.0",
|
|
45
|
+
"local": "test-env:latest",
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
lock_file = env_dir / "hud.lock.yaml"
|
|
49
|
+
import yaml
|
|
50
|
+
|
|
51
|
+
lock_file.write_text(yaml.dump(lock_data))
|
|
52
|
+
|
|
53
|
+
return env_dir
|
|
54
|
+
|
|
55
|
+
@patch("hud.cli.flows.tasks._derive_remote_image")
|
|
56
|
+
@patch("hud.cli.flows.tasks._ensure_pushed")
|
|
57
|
+
@patch("hud.cli.flows.tasks.find_environment_dir")
|
|
58
|
+
@patch("hud.cli.flows.tasks.load_tasks")
|
|
59
|
+
@patch("hud.settings.settings")
|
|
60
|
+
def test_convert_tasks_basic(
|
|
61
|
+
self,
|
|
62
|
+
mock_settings,
|
|
63
|
+
mock_load_tasks,
|
|
64
|
+
mock_find_env,
|
|
65
|
+
mock_ensure_pushed,
|
|
66
|
+
mock_derive_remote,
|
|
67
|
+
temp_tasks_file,
|
|
68
|
+
mock_env_dir,
|
|
69
|
+
):
|
|
70
|
+
"""Test basic task conversion from local to remote."""
|
|
71
|
+
# Setup mocks
|
|
72
|
+
mock_settings.api_key = "test-api-key"
|
|
73
|
+
mock_find_env.return_value = mock_env_dir
|
|
74
|
+
|
|
75
|
+
# Mock the push check to return updated lock data
|
|
76
|
+
mock_ensure_pushed.return_value = {
|
|
77
|
+
"images": {
|
|
78
|
+
"remote": "registry.hud.so/test-org/test-env:v1.0.0",
|
|
79
|
+
"local": "test-env:v1.0.0",
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Mock derive remote image
|
|
84
|
+
mock_derive_remote.return_value = "registry.hud.so/test-org/test-env:v1.0.0"
|
|
85
|
+
|
|
86
|
+
task = Task(
|
|
87
|
+
prompt="Test task",
|
|
88
|
+
mcp_config={
|
|
89
|
+
"local": {"command": "docker", "args": ["run", "--rm", "-i", "test-image:latest"]}
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
raw_task = {
|
|
93
|
+
"prompt": "Test task",
|
|
94
|
+
"mcp_config": {
|
|
95
|
+
"local": {"command": "docker", "args": ["run", "--rm", "-i", "test-image:latest"]}
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
mock_load_tasks.side_effect = [[task], [raw_task]]
|
|
100
|
+
|
|
101
|
+
# Run conversion
|
|
102
|
+
result_path = convert_tasks_to_remote(str(temp_tasks_file))
|
|
103
|
+
|
|
104
|
+
# Check result
|
|
105
|
+
assert result_path.endswith("remote_tasks.json")
|
|
106
|
+
assert Path(result_path).exists()
|
|
107
|
+
|
|
108
|
+
# Verify converted content
|
|
109
|
+
with open(result_path) as f:
|
|
110
|
+
converted_tasks = json.load(f)
|
|
111
|
+
|
|
112
|
+
assert len(converted_tasks) == 1
|
|
113
|
+
assert "hud" in converted_tasks[0]["mcp_config"]
|
|
114
|
+
assert converted_tasks[0]["mcp_config"]["hud"]["url"] == "https://mcp.hud.so/v3/mcp"
|
|
115
|
+
|
|
116
|
+
@patch("hud.settings.settings")
|
|
117
|
+
def test_convert_missing_api_key(self, mock_settings, temp_tasks_file):
|
|
118
|
+
"""Test that conversion fails without API key."""
|
|
119
|
+
mock_settings.api_key = ""
|
|
120
|
+
|
|
121
|
+
with pytest.raises(typer.Exit):
|
|
122
|
+
convert_tasks_to_remote(str(temp_tasks_file))
|
|
123
|
+
|
|
124
|
+
@patch("hud.cli.flows.tasks.find_environment_dir")
|
|
125
|
+
@patch("hud.cli.flows.tasks.load_tasks")
|
|
126
|
+
@patch("hud.settings.settings")
|
|
127
|
+
def test_convert_already_remote(
|
|
128
|
+
self, mock_settings, mock_load_tasks, mock_find_env, temp_tasks_file
|
|
129
|
+
):
|
|
130
|
+
"""Test that already remote tasks are not converted again."""
|
|
131
|
+
mock_settings.api_key = "test-api-key"
|
|
132
|
+
mock_find_env.return_value = None # No env dir needed for remote tasks
|
|
133
|
+
|
|
134
|
+
# Create task that's already remote
|
|
135
|
+
task = Task(
|
|
136
|
+
prompt="Test task",
|
|
137
|
+
mcp_config={
|
|
138
|
+
"remote": {
|
|
139
|
+
"url": "https://mcp.hud.so",
|
|
140
|
+
"headers": {"Mcp-Image": "registry.hud.so/test/image:v1"},
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
mock_load_tasks.return_value = [task]
|
|
146
|
+
|
|
147
|
+
# Should return original path without modification
|
|
148
|
+
result_path = convert_tasks_to_remote(str(temp_tasks_file))
|
|
149
|
+
assert result_path == str(temp_tasks_file)
|
|
150
|
+
|
|
151
|
+
@patch("hud.cli.flows.tasks.find_environment_dir")
|
|
152
|
+
@patch("hud.cli.flows.tasks.load_tasks")
|
|
153
|
+
@patch("hud.settings.settings")
|
|
154
|
+
def test_convert_no_environment(
|
|
155
|
+
self, mock_settings, mock_load_tasks, mock_find_env, temp_tasks_file
|
|
156
|
+
):
|
|
157
|
+
"""Test that conversion fails when no environment is found."""
|
|
158
|
+
mock_settings.api_key = "test-api-key"
|
|
159
|
+
mock_find_env.return_value = None
|
|
160
|
+
|
|
161
|
+
task = Task(
|
|
162
|
+
prompt="Test task",
|
|
163
|
+
mcp_config={
|
|
164
|
+
"local": {"command": "docker", "args": ["run", "--rm", "-i", "test-image:latest"]}
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
mock_load_tasks.return_value = [task]
|
|
169
|
+
|
|
170
|
+
with pytest.raises(typer.Exit):
|
|
171
|
+
convert_tasks_to_remote(str(temp_tasks_file))
|
|
172
|
+
|
|
173
|
+
@patch("hud.utils.hud_console.hud_console.confirm")
|
|
174
|
+
@patch("hud.cli.flows.tasks._derive_remote_image")
|
|
175
|
+
@patch("hud.cli.flows.tasks._ensure_pushed")
|
|
176
|
+
@patch("hud.cli.flows.tasks.find_environment_dir")
|
|
177
|
+
@patch("hud.cli.flows.tasks.load_tasks")
|
|
178
|
+
@patch("hud.settings.settings")
|
|
179
|
+
def test_convert_with_env_vars(
|
|
180
|
+
self,
|
|
181
|
+
mock_settings,
|
|
182
|
+
mock_load_tasks,
|
|
183
|
+
mock_find_env,
|
|
184
|
+
mock_ensure_pushed,
|
|
185
|
+
mock_derive_remote,
|
|
186
|
+
mock_confirm,
|
|
187
|
+
temp_tasks_file,
|
|
188
|
+
mock_env_dir,
|
|
189
|
+
):
|
|
190
|
+
"""Test conversion includes environment variables as headers."""
|
|
191
|
+
mock_settings.api_key = "test-api-key"
|
|
192
|
+
mock_find_env.return_value = mock_env_dir
|
|
193
|
+
mock_confirm.return_value = True # Always confirm in tests
|
|
194
|
+
|
|
195
|
+
# Mock the push check to return updated lock data
|
|
196
|
+
mock_ensure_pushed.return_value = {
|
|
197
|
+
"images": {
|
|
198
|
+
"remote": "registry.hud.so/test-org/test-env:v1.0.0",
|
|
199
|
+
"local": "test-env:v1.0.0",
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Mock derive remote image
|
|
204
|
+
mock_derive_remote.return_value = "registry.hud.so/test-org/test-env:v1.0.0"
|
|
205
|
+
|
|
206
|
+
# Add .env file with API keys
|
|
207
|
+
env_file = mock_env_dir / ".env"
|
|
208
|
+
env_file.write_text("OPENAI_API_KEY=sk-test123\nANTHROPIC_API_KEY=sk-ant456")
|
|
209
|
+
|
|
210
|
+
task = Task(
|
|
211
|
+
prompt="Test task",
|
|
212
|
+
mcp_config={
|
|
213
|
+
"local": {
|
|
214
|
+
"command": "docker",
|
|
215
|
+
"args": ["run", "--rm", "-i", "-e", "OPENAI_API_KEY", "test-image:latest"],
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
raw_task = task.model_dump()
|
|
220
|
+
|
|
221
|
+
mock_load_tasks.side_effect = [[task], [raw_task]]
|
|
222
|
+
|
|
223
|
+
# Run conversion
|
|
224
|
+
result_path = convert_tasks_to_remote(str(temp_tasks_file))
|
|
225
|
+
|
|
226
|
+
# Verify headers include env vars
|
|
227
|
+
with open(result_path) as f:
|
|
228
|
+
converted_tasks = json.load(f)
|
|
229
|
+
|
|
230
|
+
headers = converted_tasks[0]["mcp_config"]["hud"]["headers"]
|
|
231
|
+
assert "Env-Openai-Api-Key" in headers
|
|
232
|
+
assert headers["Env-Openai-Api-Key"] == "${OPENAI_API_KEY}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class TestConvertHelperFunctions:
|
|
236
|
+
"""Test helper functions used by convert command."""
|
|
237
|
+
|
|
238
|
+
def test_env_var_to_header_key(self):
|
|
239
|
+
"""Test environment variable name conversion to header format."""
|
|
240
|
+
from hud.cli.flows.tasks import _env_var_to_header_key
|
|
241
|
+
|
|
242
|
+
assert _env_var_to_header_key("OPENAI_API_KEY") == "Env-Openai-Api-Key"
|
|
243
|
+
assert _env_var_to_header_key("ANTHROPIC_API_KEY") == "Env-Anthropic-Api-Key"
|
|
244
|
+
assert _env_var_to_header_key("SIMPLE") == "Env-Simple"
|
|
245
|
+
assert _env_var_to_header_key("MULTIPLE_WORD_VAR") == "Env-Multiple-Word-Var"
|
|
246
|
+
|
|
247
|
+
def test_extract_dotenv_api_key_vars(self):
|
|
248
|
+
"""Test extraction of API-like variables from .env file."""
|
|
249
|
+
# Create test env directory with .env file
|
|
250
|
+
import tempfile
|
|
251
|
+
|
|
252
|
+
from hud.cli.flows.tasks import _extract_dotenv_api_key_vars
|
|
253
|
+
|
|
254
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
255
|
+
env_dir = Path(tmpdir)
|
|
256
|
+
env_file = env_dir / ".env"
|
|
257
|
+
env_file.write_text("""
|
|
258
|
+
# Test .env file
|
|
259
|
+
OPENAI_API_KEY=sk-test123
|
|
260
|
+
ANTHROPIC_API_KEY=sk-ant456
|
|
261
|
+
SOME_TOKEN=abc123
|
|
262
|
+
CLIENT_SECRET=secret789
|
|
263
|
+
USER_PASSWORD=pass123
|
|
264
|
+
REGULAR_VAR=not_included
|
|
265
|
+
HUD_API_URL=https://api.hud.so
|
|
266
|
+
""")
|
|
267
|
+
|
|
268
|
+
result = _extract_dotenv_api_key_vars(env_dir)
|
|
269
|
+
|
|
270
|
+
# Should include only API-like variables
|
|
271
|
+
assert "OPENAI_API_KEY" in result
|
|
272
|
+
assert "ANTHROPIC_API_KEY" in result
|
|
273
|
+
assert "SOME_TOKEN" in result
|
|
274
|
+
assert "CLIENT_SECRET" in result
|
|
275
|
+
assert "USER_PASSWORD" in result
|
|
276
|
+
assert "REGULAR_VAR" not in result
|
|
277
|
+
assert "HUD_API_URL" in result # API in name, so it's included
|
|
278
|
+
|
|
279
|
+
def test_is_remote_url(self):
|
|
280
|
+
"""Test remote URL detection."""
|
|
281
|
+
from hud.cli.flows.tasks import _is_remote_url
|
|
282
|
+
|
|
283
|
+
# This function matches URLs with domain names (not localhost or IPs)
|
|
284
|
+
assert _is_remote_url("https://mcp.hud.so")
|
|
285
|
+
assert _is_remote_url("http://mcp.hud.so")
|
|
286
|
+
assert _is_remote_url("https://mcp.hud.so/some/path")
|
|
287
|
+
assert _is_remote_url("https://example.com") # Also matches
|
|
288
|
+
assert not _is_remote_url("http://localhost:8000") # localhost doesn't match
|
|
289
|
+
assert not _is_remote_url("file:///path/to/file") # file:// doesn't match
|
|
290
|
+
|
|
291
|
+
def test_extract_env_vars_from_docker_args(self):
|
|
292
|
+
"""Test extraction of environment variables from docker arguments."""
|
|
293
|
+
from hud.cli.flows.tasks import _extract_env_vars_from_docker_args
|
|
294
|
+
|
|
295
|
+
# Test with various docker arg formats
|
|
296
|
+
args = [
|
|
297
|
+
"run",
|
|
298
|
+
"--rm",
|
|
299
|
+
"-i",
|
|
300
|
+
"-e",
|
|
301
|
+
"VAR1",
|
|
302
|
+
"-e",
|
|
303
|
+
"VAR2=value",
|
|
304
|
+
"--env",
|
|
305
|
+
"VAR3",
|
|
306
|
+
"--env=VAR4",
|
|
307
|
+
# Note: -eFOO compact form is not supported by the implementation
|
|
308
|
+
"--env-file",
|
|
309
|
+
".env",
|
|
310
|
+
"-p",
|
|
311
|
+
"8080:80",
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
result = _extract_env_vars_from_docker_args(args)
|
|
315
|
+
|
|
316
|
+
assert "VAR1" in result
|
|
317
|
+
assert "VAR2" in result
|
|
318
|
+
assert "VAR3" in result
|
|
319
|
+
assert "VAR4" in result
|
|
320
|
+
# FOO is not extracted because -eFOO compact form is not supported
|
|
321
|
+
assert len(result) == 4
|
|
322
|
+
|
|
323
|
+
def test_derive_remote_image(self):
|
|
324
|
+
"""Test deriving remote image from lock data."""
|
|
325
|
+
from hud.cli.flows.tasks import _derive_remote_image
|
|
326
|
+
|
|
327
|
+
# The function derives remote image from images.local, not images.remote
|
|
328
|
+
lock_data = {"images": {"local": "test-env:v1.0.0"}}
|
|
329
|
+
result = _derive_remote_image(lock_data)
|
|
330
|
+
assert result == "test-env:v1.0.0"
|
|
331
|
+
|
|
332
|
+
# Test fallback to legacy format
|
|
333
|
+
lock_data = {
|
|
334
|
+
"image": "test-org/test-env:v1.0.0",
|
|
335
|
+
}
|
|
336
|
+
result = _derive_remote_image(lock_data)
|
|
337
|
+
assert result == "test-org/test-env:v1.0.0"
|
|
338
|
+
|
|
339
|
+
def test_extract_vars_from_task_configs(self):
|
|
340
|
+
"""Test extraction of env vars from task configurations."""
|
|
341
|
+
from hud.cli.flows.tasks import _extract_vars_from_task_configs
|
|
342
|
+
|
|
343
|
+
raw_tasks = [
|
|
344
|
+
{
|
|
345
|
+
"prompt": "Task 1",
|
|
346
|
+
"mcp_config": {
|
|
347
|
+
"local": {"command": "docker", "args": ["run", "-e", "API_KEY1", "image1"]}
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"prompt": "Task 2",
|
|
352
|
+
"mcp_config": {
|
|
353
|
+
"local": {
|
|
354
|
+
"command": "docker",
|
|
355
|
+
"args": ["run", "-e", "API_KEY2", "--env", "API_KEY3", "image2"],
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{"prompt": "Task 3", "mcp_config": {"remote": {"url": "https://mcp.hud.so"}}},
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
result = _extract_vars_from_task_configs(raw_tasks)
|
|
363
|
+
|
|
364
|
+
assert "API_KEY1" in result
|
|
365
|
+
assert "API_KEY2" in result
|
|
366
|
+
assert "API_KEY3" in result
|
|
367
|
+
assert len(result) == 3
|
hud/cli/utils/version_check.py
CHANGED
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import contextlib
|
|
19
19
|
import json
|
|
20
|
+
import logging
|
|
20
21
|
import os
|
|
21
22
|
import time
|
|
22
23
|
from pathlib import Path
|
|
@@ -27,6 +28,9 @@ from packaging import version
|
|
|
27
28
|
|
|
28
29
|
from hud.utils.hud_console import HUDConsole
|
|
29
30
|
|
|
31
|
+
# Logger for version checking
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
30
34
|
# Cache location for version check data
|
|
31
35
|
CACHE_DIR = Path.home() / ".hud" / ".cache"
|
|
32
36
|
VERSION_CACHE_FILE = CACHE_DIR / "version_check.json"
|
|
@@ -218,7 +222,7 @@ def display_update_prompt(console: HUDConsole | None = None) -> None:
|
|
|
218
222
|
console: HUDConsole instance for output. If None, creates a new one.
|
|
219
223
|
"""
|
|
220
224
|
if console is None:
|
|
221
|
-
console = HUDConsole()
|
|
225
|
+
console = HUDConsole(logger=logger)
|
|
222
226
|
|
|
223
227
|
try:
|
|
224
228
|
info = check_for_updates()
|
|
@@ -231,11 +235,8 @@ def display_update_prompt(console: HUDConsole | None = None) -> None:
|
|
|
231
235
|
f" Run: [bold yellow]uv tool upgrade hud-python[/bold yellow] to update"
|
|
232
236
|
)
|
|
233
237
|
|
|
234
|
-
# Display
|
|
235
|
-
console.
|
|
236
|
-
f"\n[yellow]{update_msg}[/yellow]\n",
|
|
237
|
-
highlight=False,
|
|
238
|
-
)
|
|
238
|
+
# Display using console info
|
|
239
|
+
console.info(f"[yellow]{update_msg}[/yellow]")
|
|
239
240
|
except Exception: # noqa: S110
|
|
240
241
|
# Never let version checking disrupt the user's workflow
|
|
241
242
|
pass
|
hud/clients/base.py
CHANGED
|
@@ -104,6 +104,7 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
104
104
|
|
|
105
105
|
self._initialized = False
|
|
106
106
|
self._telemetry_data = {} # Initialize telemetry data
|
|
107
|
+
self._cached_resources: list[types.Resource] = [] # Cache for resources
|
|
107
108
|
|
|
108
109
|
if self.verbose:
|
|
109
110
|
self._setup_verbose_logging()
|
|
@@ -139,7 +140,7 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
139
140
|
raise HudAuthenticationError(
|
|
140
141
|
f'Sending authorization "{headers.get("Authorization", "")}", which may'
|
|
141
142
|
" 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.
|
|
143
|
+
" as a header. You can get an API key at https://hud.ai"
|
|
143
144
|
)
|
|
144
145
|
# Subclasses implement connection
|
|
145
146
|
await self._connect(self._mcp_config)
|
|
@@ -170,6 +171,7 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
170
171
|
if self._initialized:
|
|
171
172
|
await self._disconnect()
|
|
172
173
|
self._initialized = False
|
|
174
|
+
self._cached_resources.clear()
|
|
173
175
|
hud_console.info("Environment Shutdown completed")
|
|
174
176
|
else:
|
|
175
177
|
hud_console.debug("Client was not initialized, skipping disconnect")
|
|
@@ -211,9 +213,22 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
211
213
|
"""List all available tools."""
|
|
212
214
|
raise NotImplementedError
|
|
213
215
|
|
|
214
|
-
@abstractmethod
|
|
215
216
|
async def list_resources(self) -> list[types.Resource]:
|
|
216
|
-
"""List all available resources.
|
|
217
|
+
"""List all available resources.
|
|
218
|
+
|
|
219
|
+
Uses cached resources if available, otherwise fetches from the server.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of available resources.
|
|
223
|
+
"""
|
|
224
|
+
# If cache is empty, populate it
|
|
225
|
+
if not self._cached_resources:
|
|
226
|
+
self._cached_resources = await self._list_resources_impl()
|
|
227
|
+
return self._cached_resources
|
|
228
|
+
|
|
229
|
+
@abstractmethod
|
|
230
|
+
async def _list_resources_impl(self) -> list[types.Resource]:
|
|
231
|
+
"""Implementation-specific resource listing. Subclasses must implement this."""
|
|
217
232
|
raise NotImplementedError
|
|
218
233
|
|
|
219
234
|
@abstractmethod
|
|
@@ -270,6 +285,17 @@ class BaseHUDClient(AgentMCPClient):
|
|
|
270
285
|
async def _fetch_telemetry(self) -> None:
|
|
271
286
|
"""Common telemetry fetching for all hud clients."""
|
|
272
287
|
try:
|
|
288
|
+
# Get resources (will use cache if available, otherwise fetch)
|
|
289
|
+
resources = await self.list_resources()
|
|
290
|
+
telemetry_available = any(
|
|
291
|
+
str(resource.uri) == "telemetry://live" for resource in resources
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if not telemetry_available:
|
|
295
|
+
if self.verbose:
|
|
296
|
+
hud_console.debug("Telemetry resource not available from server")
|
|
297
|
+
return
|
|
298
|
+
|
|
273
299
|
# Try to read telemetry resource directly
|
|
274
300
|
result = await self.read_resource("telemetry://live")
|
|
275
301
|
if result and result.contents:
|
hud/clients/fastmcp.py
CHANGED
|
@@ -95,7 +95,7 @@ class FastMCPHUDClient(BaseHUDClient):
|
|
|
95
95
|
raise RuntimeError(
|
|
96
96
|
"Authentication failed for HUD API. "
|
|
97
97
|
"Please ensure your HUD_API_KEY environment variable is set correctly." # noqa: E501
|
|
98
|
-
"You can get an API key at https://hud.
|
|
98
|
+
"You can get an API key at https://hud.ai"
|
|
99
99
|
) from e
|
|
100
100
|
# Generic 401 error
|
|
101
101
|
raise RuntimeError(
|
|
@@ -143,8 +143,8 @@ class FastMCPHUDClient(BaseHUDClient):
|
|
|
143
143
|
structuredContent=result.structured_content,
|
|
144
144
|
)
|
|
145
145
|
|
|
146
|
-
async def
|
|
147
|
-
"""
|
|
146
|
+
async def _list_resources_impl(self) -> list[types.Resource]:
|
|
147
|
+
"""Implementation of resource listing for FastMCP client."""
|
|
148
148
|
if self._client is None:
|
|
149
149
|
raise ValueError("Client is not connected, call initialize() first")
|
|
150
150
|
return await self._client.list_resources()
|
hud/clients/mcp_use.py
CHANGED
|
@@ -243,8 +243,8 @@ class MCPUseHUDClient(BaseHUDClient):
|
|
|
243
243
|
structuredContent=result.structuredContent,
|
|
244
244
|
)
|
|
245
245
|
|
|
246
|
-
async def
|
|
247
|
-
"""
|
|
246
|
+
async def _list_resources_impl(self) -> list[types.Resource]:
|
|
247
|
+
"""Implementation of resource listing for MCP-use client."""
|
|
248
248
|
if self._client is None or not self._sessions:
|
|
249
249
|
raise ValueError("Client is not connected, call initialize() first")
|
|
250
250
|
|
|
@@ -35,9 +35,15 @@ class MockClient(BaseHUDClient):
|
|
|
35
35
|
raise RuntimeError("Not connected")
|
|
36
36
|
return self._mock_tools
|
|
37
37
|
|
|
38
|
-
async def
|
|
39
|
-
"""Minimal
|
|
40
|
-
|
|
38
|
+
async def _list_resources_impl(self) -> list[types.Resource]:
|
|
39
|
+
"""Minimal resource listing implementation for tests."""
|
|
40
|
+
from pydantic import AnyUrl
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
types.Resource(
|
|
44
|
+
uri=AnyUrl("telemetry://live"), name="telemetry", description="Live telemetry data"
|
|
45
|
+
)
|
|
46
|
+
]
|
|
41
47
|
|
|
42
48
|
async def _call_tool(self, tool_call: MCPToolCall) -> MCPToolResult:
|
|
43
49
|
if tool_call.name == "test_tool":
|
hud/otel/config.py
CHANGED
|
@@ -113,7 +113,7 @@ def configure_telemetry(
|
|
|
113
113
|
# Error if no exporters are configured
|
|
114
114
|
raise ValueError(
|
|
115
115
|
"No telemetry backend configured. Either:\n"
|
|
116
|
-
"1. Set HUD_API_KEY environment variable for HUD telemetry (https://hud.
|
|
116
|
+
"1. Set HUD_API_KEY environment variable for HUD telemetry (https://hud.ai)\n"
|
|
117
117
|
"2. Use enable_otlp=True with configure_telemetry() for alternative backends (e.g., Jaeger)\n" # noqa: E501
|
|
118
118
|
)
|
|
119
119
|
elif not settings.telemetry_enabled:
|
hud/otel/context.py
CHANGED
|
@@ -408,7 +408,7 @@ def _print_trace_url(task_run_id: str) -> None:
|
|
|
408
408
|
if not (settings.telemetry_enabled and settings.api_key):
|
|
409
409
|
return
|
|
410
410
|
|
|
411
|
-
url = f"https://hud.
|
|
411
|
+
url = f"https://hud.ai/trace/{task_run_id}"
|
|
412
412
|
header = "🚀 See your agent live at:"
|
|
413
413
|
|
|
414
414
|
# ANSI color codes
|
|
@@ -447,7 +447,7 @@ def _print_trace_complete_url(task_run_id: str, error_occurred: bool = False) ->
|
|
|
447
447
|
if not (settings.telemetry_enabled and settings.api_key):
|
|
448
448
|
return
|
|
449
449
|
|
|
450
|
-
url = f"https://hud.
|
|
450
|
+
url = f"https://hud.ai/trace/{task_run_id}"
|
|
451
451
|
|
|
452
452
|
# ANSI color codes
|
|
453
453
|
GREEN = "\033[92m"
|