hud-python 0.3.4__py3-none-any.whl → 0.4.0__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/__init__.py +22 -89
- hud/agents/__init__.py +17 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +379 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +354 -0
- hud/clients/fastmcp.py +202 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -414
- hud/tools/computer/hud.py +376 -328
- hud/tools/computer/openai.py +295 -286
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.0.dist-info/METADATA +474 -0
- hud_python-0.4.0.dist-info/RECORD +132 -0
- hud_python-0.4.0.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.4.dist-info/METADATA +0 -284
- hud_python-0.3.4.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import time
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from collections.abc import Awaitable, Callable
|
|
9
|
-
|
|
10
|
-
from wrapt import wrap_function_wrapper
|
|
11
|
-
|
|
12
|
-
from hud.telemetry.context import (
|
|
13
|
-
create_notification_record,
|
|
14
|
-
create_request_record,
|
|
15
|
-
create_response_record,
|
|
16
|
-
get_current_task_run_id,
|
|
17
|
-
)
|
|
18
|
-
from hud.telemetry.mcp_models import DirectionType, MCPCallType, StatusType
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class MCPInstrumentor:
|
|
24
|
-
"""
|
|
25
|
-
Context-aware instrumentor for MCP calls.
|
|
26
|
-
Only instruments MCP methods when there's an active trace context.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self) -> None:
|
|
30
|
-
self._installed = False
|
|
31
|
-
|
|
32
|
-
def install(self) -> None:
|
|
33
|
-
"""Install instrumentation for MCP - but only activate when trace context exists."""
|
|
34
|
-
logger.debug("MCPInstrumentor: install() called (context-aware mode)")
|
|
35
|
-
if self._installed:
|
|
36
|
-
logger.debug("MCP instrumentation already installed")
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
try:
|
|
40
|
-
# Import and wrap the main session methods
|
|
41
|
-
import mcp.shared.session # noqa: F401
|
|
42
|
-
|
|
43
|
-
wrap_function_wrapper(
|
|
44
|
-
"mcp.shared.session",
|
|
45
|
-
"BaseSession.send_request",
|
|
46
|
-
self._context_aware_send_request_wrapper,
|
|
47
|
-
)
|
|
48
|
-
wrap_function_wrapper(
|
|
49
|
-
"mcp.shared.session",
|
|
50
|
-
"BaseSession.send_notification",
|
|
51
|
-
self._context_aware_send_notification_wrapper,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
logger.debug("Successfully wrapped BaseSession methods for context-aware telemetry")
|
|
55
|
-
except ImportError:
|
|
56
|
-
logger.debug("mcp.shared.session not available yet")
|
|
57
|
-
except Exception as e:
|
|
58
|
-
logger.warning("Failed to wrap BaseSession methods: %s", e)
|
|
59
|
-
|
|
60
|
-
self._installed = True
|
|
61
|
-
logger.debug("MCP instrumentation installed (context-aware mode)")
|
|
62
|
-
|
|
63
|
-
async def _context_aware_send_request_wrapper(
|
|
64
|
-
self,
|
|
65
|
-
wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
|
|
66
|
-
instance: Any,
|
|
67
|
-
args: Any,
|
|
68
|
-
kwargs: Any,
|
|
69
|
-
) -> Any:
|
|
70
|
-
"""Context-aware send request wrapper."""
|
|
71
|
-
hud_task_run_id = get_current_task_run_id()
|
|
72
|
-
|
|
73
|
-
if not hud_task_run_id:
|
|
74
|
-
# No active trace - pass through without instrumentation
|
|
75
|
-
return await wrapped(*args, **kwargs)
|
|
76
|
-
|
|
77
|
-
start_time = time.time()
|
|
78
|
-
request = args[0] if args else None
|
|
79
|
-
|
|
80
|
-
# Extract method name from the request
|
|
81
|
-
method_name = "unknown_method"
|
|
82
|
-
message_id = None
|
|
83
|
-
request_data = None
|
|
84
|
-
|
|
85
|
-
if request:
|
|
86
|
-
try:
|
|
87
|
-
# Get model dump for Pydantic models
|
|
88
|
-
if hasattr(request, "model_dump"):
|
|
89
|
-
request_data = request.model_dump(mode="json", exclude_none=True)
|
|
90
|
-
# Extract method from the dump
|
|
91
|
-
if isinstance(request_data, dict) and "method" in request_data:
|
|
92
|
-
method_name = request_data["method"]
|
|
93
|
-
# Fallback to direct method attribute
|
|
94
|
-
elif hasattr(request, "method"):
|
|
95
|
-
method_name = str(request.method)
|
|
96
|
-
request_data = {"method": method_name}
|
|
97
|
-
except Exception as e:
|
|
98
|
-
logger.debug("Failed to extract method: %s", e)
|
|
99
|
-
|
|
100
|
-
# Create the request record
|
|
101
|
-
create_request_record(
|
|
102
|
-
method=method_name,
|
|
103
|
-
message_id=message_id,
|
|
104
|
-
call_type=MCPCallType.SEND_REQUEST,
|
|
105
|
-
direction=DirectionType.SENT,
|
|
106
|
-
status=StatusType.STARTED,
|
|
107
|
-
start_time=start_time,
|
|
108
|
-
request_data=request_data,
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
try:
|
|
112
|
-
# Call the wrapped method and get the result
|
|
113
|
-
result = await wrapped(*args, **kwargs)
|
|
114
|
-
|
|
115
|
-
# Get the actual message_id from the session after sending
|
|
116
|
-
if hasattr(instance, "_request_id"):
|
|
117
|
-
# Current request ID minus 1 (since it was incremented)
|
|
118
|
-
message_id = getattr(instance, "_request_id", 1) - 1
|
|
119
|
-
|
|
120
|
-
# Update request record with completion
|
|
121
|
-
create_request_record(
|
|
122
|
-
method=method_name,
|
|
123
|
-
message_id=message_id,
|
|
124
|
-
call_type=MCPCallType.SEND_REQUEST,
|
|
125
|
-
direction=DirectionType.SENT,
|
|
126
|
-
status=StatusType.COMPLETED,
|
|
127
|
-
start_time=start_time,
|
|
128
|
-
end_time=time.time(),
|
|
129
|
-
duration=time.time() - start_time,
|
|
130
|
-
request_data=request_data,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# Capture the response
|
|
134
|
-
if result is not None:
|
|
135
|
-
response_data = None
|
|
136
|
-
try:
|
|
137
|
-
if hasattr(result, "model_dump"):
|
|
138
|
-
response_data = result.model_dump(mode="json", exclude_none=True)
|
|
139
|
-
else:
|
|
140
|
-
response_data = {"_type": type(result).__name__}
|
|
141
|
-
except Exception as e:
|
|
142
|
-
logger.debug("Failed to serialize response data: %s", e)
|
|
143
|
-
response_data = {"_type": type(result).__name__, "_error": str(e)}
|
|
144
|
-
|
|
145
|
-
create_response_record(
|
|
146
|
-
method=method_name,
|
|
147
|
-
related_request_id=message_id,
|
|
148
|
-
is_error=False,
|
|
149
|
-
message_id=message_id,
|
|
150
|
-
direction=DirectionType.RECEIVED,
|
|
151
|
-
call_type=MCPCallType.RECEIVE_RESPONSE,
|
|
152
|
-
response_data=response_data,
|
|
153
|
-
timestamp=time.time(),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
return result
|
|
157
|
-
|
|
158
|
-
except Exception as e:
|
|
159
|
-
# Log the error
|
|
160
|
-
create_request_record(
|
|
161
|
-
method=method_name,
|
|
162
|
-
message_id=message_id,
|
|
163
|
-
call_type=MCPCallType.SEND_REQUEST,
|
|
164
|
-
direction=DirectionType.SENT,
|
|
165
|
-
status=StatusType.ERROR,
|
|
166
|
-
start_time=start_time,
|
|
167
|
-
end_time=time.time(),
|
|
168
|
-
duration=time.time() - start_time,
|
|
169
|
-
error=str(e),
|
|
170
|
-
error_type=type(e).__name__,
|
|
171
|
-
request_data=request_data,
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
# Also record error response
|
|
175
|
-
create_response_record(
|
|
176
|
-
method=method_name,
|
|
177
|
-
related_request_id=message_id,
|
|
178
|
-
is_error=True,
|
|
179
|
-
message_id=message_id,
|
|
180
|
-
direction=DirectionType.RECEIVED,
|
|
181
|
-
call_type=MCPCallType.RECEIVE_RESPONSE,
|
|
182
|
-
status=StatusType.ERROR,
|
|
183
|
-
error=str(e),
|
|
184
|
-
error_type=type(e).__name__,
|
|
185
|
-
timestamp=time.time(),
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
raise
|
|
189
|
-
|
|
190
|
-
async def _context_aware_send_notification_wrapper(
|
|
191
|
-
self,
|
|
192
|
-
wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
|
|
193
|
-
instance: Any,
|
|
194
|
-
args: Any,
|
|
195
|
-
kwargs: Any,
|
|
196
|
-
) -> Any:
|
|
197
|
-
"""Context-aware send notification wrapper."""
|
|
198
|
-
hud_task_run_id = get_current_task_run_id()
|
|
199
|
-
|
|
200
|
-
if not hud_task_run_id:
|
|
201
|
-
# No active trace - pass through without instrumentation
|
|
202
|
-
return await wrapped(*args, **kwargs)
|
|
203
|
-
|
|
204
|
-
start_time = time.time()
|
|
205
|
-
notification = args[0] if args else None
|
|
206
|
-
|
|
207
|
-
# Extract method name
|
|
208
|
-
method_name = "unknown_method"
|
|
209
|
-
notification_data = None
|
|
210
|
-
|
|
211
|
-
if notification:
|
|
212
|
-
try:
|
|
213
|
-
# Get model dump for Pydantic models
|
|
214
|
-
if hasattr(notification, "model_dump"):
|
|
215
|
-
notification_data = notification.model_dump(mode="json", exclude_none=True)
|
|
216
|
-
# Extract method from the dump
|
|
217
|
-
if isinstance(notification_data, dict) and "method" in notification_data:
|
|
218
|
-
method_name = notification_data["method"]
|
|
219
|
-
# Fallback to direct method attribute
|
|
220
|
-
elif hasattr(notification, "method"):
|
|
221
|
-
method_name = str(notification.method)
|
|
222
|
-
notification_data = {"method": method_name}
|
|
223
|
-
except Exception as e:
|
|
224
|
-
logger.debug("Failed to extract notification method: %s", e)
|
|
225
|
-
|
|
226
|
-
record_data_base = {
|
|
227
|
-
"method": method_name,
|
|
228
|
-
"message_id": None, # Notifications don't have IDs
|
|
229
|
-
"call_type": MCPCallType.SEND_NOTIFICATION,
|
|
230
|
-
"direction": DirectionType.SENT,
|
|
231
|
-
"notification_data": notification_data,
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
try:
|
|
235
|
-
create_notification_record(
|
|
236
|
-
**record_data_base, status=StatusType.STARTED, start_time=start_time
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
result = await wrapped(*args, **kwargs)
|
|
240
|
-
|
|
241
|
-
create_notification_record(
|
|
242
|
-
**record_data_base,
|
|
243
|
-
status=StatusType.COMPLETED,
|
|
244
|
-
start_time=start_time,
|
|
245
|
-
end_time=time.time(),
|
|
246
|
-
duration=time.time() - start_time,
|
|
247
|
-
)
|
|
248
|
-
return result
|
|
249
|
-
except Exception as e:
|
|
250
|
-
create_notification_record(
|
|
251
|
-
**record_data_base,
|
|
252
|
-
status=StatusType.ERROR,
|
|
253
|
-
start_time=start_time,
|
|
254
|
-
end_time=time.time(),
|
|
255
|
-
duration=time.time() - start_time,
|
|
256
|
-
error=str(e),
|
|
257
|
-
error_type=type(e).__name__,
|
|
258
|
-
)
|
|
259
|
-
raise
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
logger = logging.getLogger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class InstrumentorRegistry:
|
|
10
|
-
"""Registry for telemetry instrumentors."""
|
|
11
|
-
|
|
12
|
-
def __init__(self) -> None:
|
|
13
|
-
self._instrumentors: dict[str, Any] = {}
|
|
14
|
-
|
|
15
|
-
def register(self, name: str, instrumentor: Any) -> None:
|
|
16
|
-
"""Register an instrumentor.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
name: Name of the instrumentor
|
|
20
|
-
instrumentor: The instrumentor instance
|
|
21
|
-
"""
|
|
22
|
-
self._instrumentors[name] = instrumentor
|
|
23
|
-
logger.debug("Registered instrumentor: %s", name)
|
|
24
|
-
|
|
25
|
-
def _safe_install(self, name: str, instrumentor: Any) -> tuple[str, Exception | None]:
|
|
26
|
-
"""Safely install an instrumentor and return result."""
|
|
27
|
-
try:
|
|
28
|
-
instrumentor.install()
|
|
29
|
-
return name, None
|
|
30
|
-
except Exception as e:
|
|
31
|
-
return name, e
|
|
32
|
-
|
|
33
|
-
def install_all(self) -> None:
|
|
34
|
-
"""Install all registered instrumentors."""
|
|
35
|
-
# Use map to apply safe installation to all instrumentors
|
|
36
|
-
installation_results = [
|
|
37
|
-
self._safe_install(name, instrumentor)
|
|
38
|
-
for name, instrumentor in self._instrumentors.items()
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
# Process results
|
|
42
|
-
for name, error in installation_results:
|
|
43
|
-
if error is None:
|
|
44
|
-
logger.debug("Installed instrumentor: %s", name)
|
|
45
|
-
else:
|
|
46
|
-
logger.warning("Failed to install instrumentor %s: %s", name, error)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Create a singleton registry
|
|
50
|
-
registry = InstrumentorRegistry()
|
|
51
|
-
|
|
52
|
-
# Try to register MCP instrumentor if available
|
|
53
|
-
try:
|
|
54
|
-
from .mcp import MCPInstrumentor
|
|
55
|
-
|
|
56
|
-
registry.register("mcp", MCPInstrumentor())
|
|
57
|
-
logger.debug("MCP instrumentor registered")
|
|
58
|
-
except Exception as e:
|
|
59
|
-
logger.debug("Could not register MCP instrumentor: %s", e)
|
hud/telemetry/mcp_models.py
DELETED
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from enum import Enum
|
|
5
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
|
6
|
-
|
|
7
|
-
# Import MCP types
|
|
8
|
-
from mcp.types import JSONRPCError, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse
|
|
9
|
-
from pydantic import BaseModel, Field, field_validator
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from mcp.shared.message import SessionMessage
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class DirectionType(str, Enum):
|
|
16
|
-
"""Direction of an MCP message"""
|
|
17
|
-
|
|
18
|
-
SENT = "sent"
|
|
19
|
-
RECEIVED = "received"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class StatusType(str, Enum):
|
|
23
|
-
"""Status of an MCP operation"""
|
|
24
|
-
|
|
25
|
-
STARTED = "started"
|
|
26
|
-
COMPLETED = "completed"
|
|
27
|
-
ERROR = "error"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class MCPCallType(str, Enum):
|
|
31
|
-
"""Enum for different types of MCP calls in telemetry."""
|
|
32
|
-
|
|
33
|
-
# Requests and Notifications
|
|
34
|
-
SEND_REQUEST = "mcp.send_request"
|
|
35
|
-
SEND_NOTIFICATION = "mcp.send_notification"
|
|
36
|
-
|
|
37
|
-
# Responses
|
|
38
|
-
RECEIVE_RESPONSE = "mcp.receive_response"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class BaseMCPCall(BaseModel):
|
|
42
|
-
"""Base model for all MCP telemetry records"""
|
|
43
|
-
|
|
44
|
-
task_run_id: str
|
|
45
|
-
call_type: str
|
|
46
|
-
timestamp: float = Field(default_factory=lambda: datetime.now().timestamp())
|
|
47
|
-
method: str = "unknown_method"
|
|
48
|
-
status: StatusType
|
|
49
|
-
direction: DirectionType | None = None
|
|
50
|
-
# Additional data that might be useful for any call
|
|
51
|
-
message_id: str | int | None = None
|
|
52
|
-
|
|
53
|
-
# Mapping of call types to model classes - to be populated by subclasses
|
|
54
|
-
_call_type_mapping: ClassVar[dict[str, type["BaseMCPCall"]]] = {}
|
|
55
|
-
|
|
56
|
-
@field_validator("call_type")
|
|
57
|
-
@classmethod
|
|
58
|
-
def validate_call_type(cls, v: str) -> str:
|
|
59
|
-
"""Allow any string but preferably from MCPCallType"""
|
|
60
|
-
return v
|
|
61
|
-
|
|
62
|
-
@classmethod
|
|
63
|
-
def from_dict(cls, data: dict[str, Any]) -> BaseMCPCall:
|
|
64
|
-
"""Create a record from a dictionary, using the appropriate subclass"""
|
|
65
|
-
call_type = data.get("call_type", "")
|
|
66
|
-
record_cls = cls._call_type_mapping.get(call_type, BaseMCPCall)
|
|
67
|
-
return record_cls.model_validate(data)
|
|
68
|
-
|
|
69
|
-
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
70
|
-
"""Register subclasses in the mapping by their default call_type"""
|
|
71
|
-
super().__init_subclass__(**kwargs)
|
|
72
|
-
if hasattr(cls, "__annotations__") and "call_type" in cls.__annotations__:
|
|
73
|
-
default_call_type = getattr(cls, "call_type", None)
|
|
74
|
-
if isinstance(default_call_type, str):
|
|
75
|
-
BaseMCPCall._call_type_mapping[default_call_type] = cls
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class MCPRequestCall(BaseMCPCall):
|
|
79
|
-
"""Record for an MCP request"""
|
|
80
|
-
|
|
81
|
-
direction: DirectionType = DirectionType.SENT
|
|
82
|
-
call_type: str = MCPCallType.SEND_REQUEST
|
|
83
|
-
start_time: float
|
|
84
|
-
end_time: float | None = None
|
|
85
|
-
duration: float | None = None
|
|
86
|
-
request_id: str | int | None = None
|
|
87
|
-
request_data: dict[str, Any] | None = None
|
|
88
|
-
error: str | None = None
|
|
89
|
-
error_type: str | None = None
|
|
90
|
-
|
|
91
|
-
@classmethod
|
|
92
|
-
def from_jsonrpc_request(
|
|
93
|
-
cls,
|
|
94
|
-
request: JSONRPCRequest,
|
|
95
|
-
task_run_id: str,
|
|
96
|
-
status: StatusType = StatusType.STARTED,
|
|
97
|
-
**kwargs: Any,
|
|
98
|
-
) -> MCPRequestCall:
|
|
99
|
-
"""Create telemetry record from a JSONRPCRequest"""
|
|
100
|
-
return cls(
|
|
101
|
-
task_run_id=task_run_id,
|
|
102
|
-
status=status,
|
|
103
|
-
request_id=request.id,
|
|
104
|
-
message_id=request.id,
|
|
105
|
-
method=request.method,
|
|
106
|
-
request_data=request.model_dump(exclude_none=True),
|
|
107
|
-
start_time=datetime.now().timestamp(),
|
|
108
|
-
**kwargs,
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
@classmethod
|
|
112
|
-
def from_session_message(
|
|
113
|
-
cls,
|
|
114
|
-
message: SessionMessage,
|
|
115
|
-
task_run_id: str,
|
|
116
|
-
status: StatusType = StatusType.STARTED,
|
|
117
|
-
**kwargs: Any,
|
|
118
|
-
) -> MCPRequestCall | None:
|
|
119
|
-
"""Create telemetry record from a SessionMessage containing a JSONRPCRequest"""
|
|
120
|
-
if (
|
|
121
|
-
hasattr(message, "message")
|
|
122
|
-
and hasattr(message.message, "root")
|
|
123
|
-
and isinstance(message.message.root, JSONRPCRequest)
|
|
124
|
-
):
|
|
125
|
-
return cls.from_jsonrpc_request(
|
|
126
|
-
message.message.root, task_run_id=task_run_id, status=status, **kwargs
|
|
127
|
-
)
|
|
128
|
-
return None
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
class MCPResponseCall(BaseMCPCall):
|
|
132
|
-
"""Record for an MCP response"""
|
|
133
|
-
|
|
134
|
-
direction: DirectionType = DirectionType.RECEIVED
|
|
135
|
-
call_type: str = MCPCallType.RECEIVE_RESPONSE
|
|
136
|
-
is_response_or_error: bool = True
|
|
137
|
-
is_error: bool = False
|
|
138
|
-
response_id: str | int | None = None
|
|
139
|
-
related_request_id: str | int | None = None
|
|
140
|
-
response_data: dict[str, Any] | None = None
|
|
141
|
-
error: str | None = None
|
|
142
|
-
error_type: str | None = None
|
|
143
|
-
|
|
144
|
-
@classmethod
|
|
145
|
-
def from_jsonrpc_response(
|
|
146
|
-
cls, response: JSONRPCResponse | JSONRPCError, task_run_id: str, **kwargs: Any
|
|
147
|
-
) -> MCPResponseCall:
|
|
148
|
-
"""Create telemetry record from a JSONRPCResponse or JSONRPCError"""
|
|
149
|
-
is_error = isinstance(response, JSONRPCError)
|
|
150
|
-
|
|
151
|
-
result = cls(
|
|
152
|
-
task_run_id=task_run_id,
|
|
153
|
-
status=StatusType.COMPLETED,
|
|
154
|
-
response_id=response.id,
|
|
155
|
-
message_id=response.id,
|
|
156
|
-
related_request_id=response.id, # In MCP, response ID matches request ID
|
|
157
|
-
is_error=is_error,
|
|
158
|
-
method=f"response_to_id_{response.id}",
|
|
159
|
-
response_data=response.model_dump(exclude_none=True),
|
|
160
|
-
**kwargs,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
if is_error and hasattr(response, "error"):
|
|
164
|
-
result.error = response.error.message
|
|
165
|
-
result.error_type = str(response.error.code)
|
|
166
|
-
|
|
167
|
-
return result
|
|
168
|
-
|
|
169
|
-
@classmethod
|
|
170
|
-
def from_session_message(
|
|
171
|
-
cls, message: SessionMessage, task_run_id: str, **kwargs: Any
|
|
172
|
-
) -> MCPResponseCall | None:
|
|
173
|
-
"""Create telemetry record from a SessionMessage containing a response or error"""
|
|
174
|
-
if (
|
|
175
|
-
hasattr(message, "message")
|
|
176
|
-
and hasattr(message.message, "root")
|
|
177
|
-
and isinstance(message.message.root, JSONRPCResponse | JSONRPCError)
|
|
178
|
-
):
|
|
179
|
-
return cls.from_jsonrpc_response(
|
|
180
|
-
message.message.root, task_run_id=task_run_id, **kwargs
|
|
181
|
-
)
|
|
182
|
-
return None
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
class MCPNotificationCall(BaseMCPCall):
|
|
186
|
-
"""Record for an MCP notification"""
|
|
187
|
-
|
|
188
|
-
direction: DirectionType = DirectionType.SENT
|
|
189
|
-
call_type: str = MCPCallType.SEND_NOTIFICATION
|
|
190
|
-
start_time: float
|
|
191
|
-
end_time: float | None = None
|
|
192
|
-
duration: float | None = None
|
|
193
|
-
notification_data: dict[str, Any] | None = None
|
|
194
|
-
error: str | None = None
|
|
195
|
-
error_type: str | None = None
|
|
196
|
-
|
|
197
|
-
@classmethod
|
|
198
|
-
def from_jsonrpc_notification(
|
|
199
|
-
cls,
|
|
200
|
-
notification: JSONRPCNotification,
|
|
201
|
-
task_run_id: str,
|
|
202
|
-
status: StatusType = StatusType.STARTED,
|
|
203
|
-
**kwargs: Any,
|
|
204
|
-
) -> MCPNotificationCall:
|
|
205
|
-
"""Create telemetry record from a JSONRPCNotification"""
|
|
206
|
-
return cls(
|
|
207
|
-
task_run_id=task_run_id,
|
|
208
|
-
status=status,
|
|
209
|
-
method=notification.method,
|
|
210
|
-
notification_data=notification.model_dump(exclude_none=True),
|
|
211
|
-
start_time=datetime.now().timestamp(),
|
|
212
|
-
**kwargs,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
@classmethod
|
|
216
|
-
def from_session_message(
|
|
217
|
-
cls,
|
|
218
|
-
message: SessionMessage,
|
|
219
|
-
task_run_id: str,
|
|
220
|
-
status: StatusType = StatusType.STARTED,
|
|
221
|
-
**kwargs: Any,
|
|
222
|
-
) -> MCPNotificationCall | None:
|
|
223
|
-
"""Create telemetry record from a SessionMessage containing a JSONRPCNotification"""
|
|
224
|
-
if (
|
|
225
|
-
hasattr(message, "message")
|
|
226
|
-
and hasattr(message.message, "root")
|
|
227
|
-
and isinstance(message.message.root, JSONRPCNotification)
|
|
228
|
-
):
|
|
229
|
-
return cls.from_jsonrpc_notification(
|
|
230
|
-
message.message.root, task_run_id=task_run_id, status=status, **kwargs
|
|
231
|
-
)
|
|
232
|
-
return None
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
class MCPTelemetryRecord(BaseModel):
|
|
236
|
-
"""Container for a set of related MCP telemetry records"""
|
|
237
|
-
|
|
238
|
-
task_run_id: str
|
|
239
|
-
records: list[BaseMCPCall]
|
|
240
|
-
timestamp: float = Field(default_factory=lambda: datetime.now().timestamp())
|
|
241
|
-
|
|
242
|
-
@property
|
|
243
|
-
def count_by_type(self) -> dict[str, int]:
|
|
244
|
-
"""Count records by call_type"""
|
|
245
|
-
result: dict[str, int] = {}
|
|
246
|
-
for record in self.records:
|
|
247
|
-
result[record.call_type] = result.get(record.call_type, 0) + 1
|
|
248
|
-
return result
|
|
249
|
-
|
|
250
|
-
@property
|
|
251
|
-
def count_by_direction(self) -> dict[str, int]:
|
|
252
|
-
"""Count records by direction"""
|
|
253
|
-
result: dict[str, int] = {}
|
|
254
|
-
for record in self.records:
|
|
255
|
-
if record.direction:
|
|
256
|
-
direction = record.direction.value
|
|
257
|
-
result[direction] = result.get(direction, 0) + 1
|
|
258
|
-
return result
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
class TrajectoryStep(BaseModel):
|
|
262
|
-
"""Model for telemetry export format."""
|
|
263
|
-
|
|
264
|
-
type: str = Field(default="mcp-step")
|
|
265
|
-
observation_url: str | None = None
|
|
266
|
-
observation_text: str | None = None
|
|
267
|
-
actions: list[dict[str, Any]] = Field(default_factory=list)
|
|
268
|
-
start_timestamp: str | None = None # ISO 8601 format
|
|
269
|
-
end_timestamp: str | None = None # ISO 8601 format
|
|
270
|
-
metadata: dict[str, Any] = Field(default_factory=dict)
|
hud/telemetry/tests/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Tests for hud.telemetry module
|