hud-python 0.2.10__py3-none-any.whl → 0.3.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 +14 -5
- hud/env/docker_client.py +1 -1
- hud/env/environment.py +10 -7
- hud/env/local_docker_client.py +1 -1
- hud/env/remote_client.py +1 -1
- hud/env/remote_docker_client.py +2 -2
- hud/exceptions.py +2 -1
- hud/mcp_agent/__init__.py +15 -0
- hud/mcp_agent/base.py +723 -0
- hud/mcp_agent/claude.py +316 -0
- hud/mcp_agent/langchain.py +231 -0
- hud/mcp_agent/openai.py +318 -0
- hud/mcp_agent/tests/__init__.py +1 -0
- hud/mcp_agent/tests/test_base.py +437 -0
- hud/settings.py +14 -2
- hud/task.py +4 -0
- hud/telemetry/__init__.py +11 -7
- hud/telemetry/_trace.py +82 -71
- hud/telemetry/context.py +9 -27
- hud/telemetry/exporter.py +6 -5
- hud/telemetry/instrumentation/mcp.py +174 -410
- hud/telemetry/mcp_models.py +13 -74
- hud/telemetry/tests/test_context.py +9 -6
- hud/telemetry/tests/test_trace.py +92 -61
- hud/tools/__init__.py +21 -0
- hud/tools/base.py +65 -0
- hud/tools/bash.py +137 -0
- hud/tools/computer/__init__.py +13 -0
- hud/tools/computer/anthropic.py +411 -0
- hud/tools/computer/hud.py +315 -0
- hud/tools/computer/openai.py +283 -0
- hud/tools/edit.py +290 -0
- hud/tools/executors/__init__.py +13 -0
- hud/tools/executors/base.py +331 -0
- hud/tools/executors/pyautogui.py +585 -0
- hud/tools/executors/tests/__init__.py +1 -0
- hud/tools/executors/tests/test_base_executor.py +338 -0
- hud/tools/executors/tests/test_pyautogui_executor.py +162 -0
- hud/tools/executors/xdo.py +503 -0
- hud/tools/helper/README.md +56 -0
- hud/tools/helper/__init__.py +9 -0
- hud/tools/helper/mcp_server.py +78 -0
- hud/tools/helper/server_initialization.py +115 -0
- hud/tools/helper/utils.py +58 -0
- hud/tools/playwright_tool.py +373 -0
- hud/tools/tests/__init__.py +3 -0
- hud/tools/tests/test_bash.py +152 -0
- hud/tools/tests/test_computer.py +52 -0
- hud/tools/tests/test_computer_actions.py +34 -0
- hud/tools/tests/test_edit.py +233 -0
- hud/tools/tests/test_init.py +27 -0
- hud/tools/tests/test_playwright_tool.py +183 -0
- hud/tools/tests/test_tools.py +154 -0
- hud/tools/tests/test_utils.py +156 -0
- hud/tools/utils.py +50 -0
- hud/types.py +10 -1
- hud/utils/tests/test_init.py +21 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/METADATA +9 -6
- hud_python-0.3.0.dist-info/RECORD +124 -0
- hud_python-0.2.10.dist-info/RECORD +0 -85
- {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/WHEEL +0 -0
- {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,494 +2,258 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import time
|
|
5
|
-
from contextlib import asynccontextmanager
|
|
6
5
|
from typing import TYPE_CHECKING, Any
|
|
7
6
|
|
|
8
7
|
if TYPE_CHECKING:
|
|
9
|
-
from collections.abc import
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
from mcp.shared.message import SessionMessage # type: ignore
|
|
13
|
-
|
|
14
|
-
# MCP type imports for type checking and attribute access safety
|
|
15
|
-
from mcp.types import ( # type: ignore
|
|
16
|
-
JSONRPCError,
|
|
17
|
-
JSONRPCNotification,
|
|
18
|
-
JSONRPCRequest,
|
|
19
|
-
JSONRPCResponse,
|
|
20
|
-
)
|
|
21
|
-
from wrapt import register_post_import_hook, wrap_function_wrapper
|
|
10
|
+
from wrapt import wrap_function_wrapper
|
|
22
11
|
|
|
23
12
|
from hud.telemetry.context import (
|
|
24
|
-
buffer_mcp_call,
|
|
25
13
|
create_notification_record,
|
|
26
14
|
create_request_record,
|
|
27
15
|
create_response_record,
|
|
28
16
|
get_current_task_run_id,
|
|
29
17
|
)
|
|
30
|
-
from hud.telemetry.mcp_models import DirectionType, MCPCallType,
|
|
18
|
+
from hud.telemetry.mcp_models import DirectionType, MCPCallType, StatusType
|
|
31
19
|
|
|
32
20
|
logger = logging.getLogger(__name__)
|
|
33
21
|
|
|
34
22
|
|
|
35
23
|
class MCPInstrumentor:
|
|
36
24
|
"""
|
|
37
|
-
|
|
25
|
+
Context-aware instrumentor for MCP calls.
|
|
26
|
+
Only instruments MCP methods when there's an active trace context.
|
|
38
27
|
"""
|
|
39
28
|
|
|
40
29
|
def __init__(self) -> None:
|
|
41
30
|
self._installed = False
|
|
42
31
|
|
|
43
32
|
def install(self) -> None:
|
|
44
|
-
"""Install instrumentation for MCP"""
|
|
45
|
-
logger.debug("MCPInstrumentor: install() called")
|
|
33
|
+
"""Install instrumentation for MCP - but only activate when trace context exists."""
|
|
34
|
+
logger.debug("MCPInstrumentor: install() called (context-aware mode)")
|
|
46
35
|
if self._installed:
|
|
47
36
|
logger.debug("MCP instrumentation already installed")
|
|
48
37
|
return
|
|
49
38
|
|
|
50
|
-
# Updated hooks: removed _transport_wrapper and _base_session_init_wrapper related hooks
|
|
51
|
-
# if they become no-ops. For now, let's assume _transport_wrapper might still be used,
|
|
52
|
-
# but simplified. _base_session_init_wrapper hook is removed.
|
|
53
|
-
hooks = [
|
|
54
|
-
("mcp.client.sse", "sse_client", self._transport_wrapper),
|
|
55
|
-
("mcp.server.sse", "SseServerTransport.connect_sse", self._transport_wrapper),
|
|
56
|
-
("mcp.client.stdio", "stdio_client", self._transport_wrapper),
|
|
57
|
-
("mcp.server.stdio", "stdio_server", self._transport_wrapper),
|
|
58
|
-
("mcp.shared.session", "BaseSession._receive_loop", self._receive_loop_wrapper),
|
|
59
|
-
]
|
|
60
|
-
|
|
61
|
-
def create_hook(module: str, func_name: str, wrapper: Any) -> Any:
|
|
62
|
-
return lambda _: wrap_function_wrapper(module, func_name, wrapper)
|
|
63
|
-
|
|
64
|
-
for module, func_name, wrapper in hooks:
|
|
65
|
-
logger.debug(
|
|
66
|
-
"MCPInstrumentor: Registering post-import hook for %s.%s", module, func_name
|
|
67
|
-
)
|
|
68
|
-
register_post_import_hook(
|
|
69
|
-
create_hook(module, func_name, wrapper),
|
|
70
|
-
module,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Removed hook for BaseSession.__init__ (it was for _base_session_init_wrapper)
|
|
74
|
-
|
|
75
|
-
logger.debug(
|
|
76
|
-
"MCPInstrumentor: Attempting immediate instrumentation of already imported modules."
|
|
77
|
-
)
|
|
78
|
-
for module, func_name, wrapper in hooks:
|
|
79
|
-
try:
|
|
80
|
-
mod = __import__(module, fromlist=[func_name.split(".")[0]])
|
|
81
|
-
target_obj = mod
|
|
82
|
-
parts = func_name.split(".")
|
|
83
|
-
for i, part in enumerate(parts):
|
|
84
|
-
if hasattr(target_obj, part):
|
|
85
|
-
if i == len(parts) - 1:
|
|
86
|
-
target_obj = getattr(target_obj, part)
|
|
87
|
-
else:
|
|
88
|
-
target_obj = getattr(target_obj, part)
|
|
89
|
-
else:
|
|
90
|
-
target_obj = None
|
|
91
|
-
break
|
|
92
|
-
|
|
93
|
-
if target_obj and callable(target_obj):
|
|
94
|
-
logger.debug("MCPInstrumentor: Wrapping %s.%s", module, func_name)
|
|
95
|
-
wrap_function_wrapper(module, func_name, wrapper)
|
|
96
|
-
else:
|
|
97
|
-
logger.debug(
|
|
98
|
-
"Function %s not found in %s during immediate instrumentation attempt",
|
|
99
|
-
func_name,
|
|
100
|
-
module,
|
|
101
|
-
)
|
|
102
|
-
except ImportError:
|
|
103
|
-
logger.debug("Module %s not imported yet, post-import hook will cover it", module)
|
|
104
|
-
except Exception as e:
|
|
105
|
-
logger.warning("Failed to immediately wrap %s.%s: %s", module, func_name, e)
|
|
106
|
-
|
|
107
39
|
try:
|
|
108
|
-
# Import
|
|
109
|
-
|
|
40
|
+
# Import and wrap the main session methods
|
|
41
|
+
import mcp.shared.session # noqa: F401
|
|
42
|
+
|
|
110
43
|
wrap_function_wrapper(
|
|
111
44
|
"mcp.shared.session",
|
|
112
45
|
"BaseSession.send_request",
|
|
113
|
-
self.
|
|
46
|
+
self._context_aware_send_request_wrapper,
|
|
114
47
|
)
|
|
115
48
|
wrap_function_wrapper(
|
|
116
49
|
"mcp.shared.session",
|
|
117
50
|
"BaseSession.send_notification",
|
|
118
|
-
self.
|
|
51
|
+
self._context_aware_send_notification_wrapper,
|
|
119
52
|
)
|
|
120
|
-
|
|
53
|
+
|
|
54
|
+
logger.debug("Successfully wrapped BaseSession methods for context-aware telemetry")
|
|
121
55
|
except ImportError:
|
|
122
|
-
logger.debug("mcp.shared.session not
|
|
56
|
+
logger.debug("mcp.shared.session not available yet")
|
|
123
57
|
except Exception as e:
|
|
124
58
|
logger.warning("Failed to wrap BaseSession methods: %s", e)
|
|
125
59
|
|
|
126
60
|
self._installed = True
|
|
127
|
-
logger.debug("MCP instrumentation installed (
|
|
128
|
-
|
|
129
|
-
@asynccontextmanager
|
|
130
|
-
async def _transport_wrapper(
|
|
131
|
-
self, wrapped: Callable[..., Any], instance: Any, args: Any, kwargs: Any
|
|
132
|
-
) -> AsyncGenerator[Any, None]:
|
|
133
|
-
"""Wrap transport functions. Simplified: passes through original streams."""
|
|
134
|
-
original_func_name = f"{wrapped.__module__}.{wrapped.__name__}"
|
|
135
|
-
logger.debug(
|
|
136
|
-
"MCPInstrumentor: _transport_wrapper called for %s (passthrough)", original_func_name
|
|
137
|
-
)
|
|
61
|
+
logger.debug("MCP instrumentation installed (context-aware mode)")
|
|
138
62
|
|
|
139
|
-
|
|
140
|
-
# Higher level wrappers (_send_request, _receive_loop) will handle HUD Task ID.
|
|
141
|
-
|
|
142
|
-
async with wrapped(*args, **kwargs) as (read_stream, write_stream):
|
|
143
|
-
logger.debug("_transport_wrapper: Yielding original streams for %s", original_func_name)
|
|
144
|
-
yield read_stream, write_stream # Pass original streams directly
|
|
145
|
-
logger.debug(
|
|
146
|
-
"_transport_wrapper: Exited original stream context for %s", original_func_name
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
async def _receive_loop_wrapper(
|
|
63
|
+
async def _context_aware_send_request_wrapper(
|
|
150
64
|
self,
|
|
151
65
|
wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
|
|
152
66
|
instance: Any,
|
|
153
67
|
args: Any,
|
|
154
68
|
kwargs: Any,
|
|
155
69
|
) -> Any:
|
|
156
|
-
"""
|
|
157
|
-
|
|
158
|
-
"""
|
|
159
|
-
logger.debug("MCPInstrumentor: _receive_loop_wrapper called")
|
|
160
|
-
|
|
161
|
-
orig_handle_incoming = instance._handle_incoming
|
|
162
|
-
|
|
163
|
-
async def handle_incoming_with_telemetry(req_or_msg: Any) -> Any:
|
|
164
|
-
start_time = time.time()
|
|
165
|
-
hud_task_run_id = get_current_task_run_id()
|
|
166
|
-
method_name = "unknown_method"
|
|
167
|
-
message_id = None
|
|
168
|
-
is_response_or_error_flag = False
|
|
169
|
-
call_type_override = MCPCallType.HANDLE_INCOMING
|
|
170
|
-
|
|
171
|
-
actual_message_root = None
|
|
172
|
-
# SessionMessage
|
|
173
|
-
if hasattr(req_or_msg, "message") and hasattr(req_or_msg.message, "root"):
|
|
174
|
-
actual_message_root = req_or_msg.message.root
|
|
175
|
-
elif hasattr(req_or_msg, "root"): # e.g. RequestResponder
|
|
176
|
-
actual_message_root = req_or_msg.root
|
|
177
|
-
elif isinstance(
|
|
178
|
-
req_or_msg, JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError
|
|
179
|
-
):
|
|
180
|
-
actual_message_root = req_or_msg
|
|
181
|
-
|
|
182
|
-
if actual_message_root:
|
|
183
|
-
if isinstance(actual_message_root, JSONRPCRequest):
|
|
184
|
-
method_name = actual_message_root.method
|
|
185
|
-
message_id = actual_message_root.id
|
|
186
|
-
# call_type_override can remain HANDLE_INCOMING or be more specific if desired
|
|
187
|
-
elif isinstance(actual_message_root, JSONRPCNotification):
|
|
188
|
-
method_name = actual_message_root.method
|
|
189
|
-
message_id = None # Notifications don't have IDs
|
|
190
|
-
# call_type_override can remain HANDLE_INCOMING
|
|
191
|
-
elif isinstance(actual_message_root, JSONRPCResponse | JSONRPCError):
|
|
192
|
-
# This case implies _handle_incoming is processing a response/error directly
|
|
193
|
-
# (e.g. an error encountered while trying to send/route a response previously)
|
|
194
|
-
message_id = actual_message_root.id
|
|
195
|
-
method_name = f"internal_response_handling_for_id_{message_id}"
|
|
196
|
-
is_response_or_error_flag = True
|
|
197
|
-
# Treat as receiving a response internally
|
|
198
|
-
call_type_override = MCPCallType.RECEIVE_RESPONSE
|
|
199
|
-
else:
|
|
200
|
-
# Fallback for other types, if any, that might appear here
|
|
201
|
-
if hasattr(actual_message_root, "method"):
|
|
202
|
-
method_name = actual_message_root.method
|
|
203
|
-
if hasattr(actual_message_root, "id"):
|
|
204
|
-
message_id = actual_message_root.id
|
|
205
|
-
|
|
206
|
-
record_data_base = {
|
|
207
|
-
"method": method_name,
|
|
208
|
-
"direction": DirectionType.RECEIVED,
|
|
209
|
-
"call_type": call_type_override,
|
|
210
|
-
"message_id": message_id,
|
|
211
|
-
"is_response_or_error": is_response_or_error_flag, # For MCPRequestCall model
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
try:
|
|
215
|
-
if hud_task_run_id:
|
|
216
|
-
create_request_record(
|
|
217
|
-
**record_data_base,
|
|
218
|
-
status=StatusType.STARTED,
|
|
219
|
-
start_time=start_time,
|
|
220
|
-
# request_data might be populated if needed for HANDLE_INCOMING
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
result = await orig_handle_incoming(req_or_msg)
|
|
224
|
-
|
|
225
|
-
if hud_task_run_id:
|
|
226
|
-
create_request_record(
|
|
227
|
-
**record_data_base,
|
|
228
|
-
status=StatusType.COMPLETED,
|
|
229
|
-
start_time=start_time,
|
|
230
|
-
end_time=time.time(),
|
|
231
|
-
duration=time.time() - start_time,
|
|
232
|
-
# result_data might be populated if needed
|
|
233
|
-
)
|
|
234
|
-
return result
|
|
235
|
-
except Exception as e:
|
|
236
|
-
if hud_task_run_id:
|
|
237
|
-
create_request_record(
|
|
238
|
-
**record_data_base,
|
|
239
|
-
status=StatusType.ERROR,
|
|
240
|
-
start_time=start_time,
|
|
241
|
-
end_time=time.time(),
|
|
242
|
-
duration=time.time() - start_time,
|
|
243
|
-
error=str(e),
|
|
244
|
-
error_type=type(e).__name__,
|
|
245
|
-
)
|
|
246
|
-
raise
|
|
247
|
-
|
|
248
|
-
instance._handle_incoming = handle_incoming_with_telemetry
|
|
249
|
-
|
|
250
|
-
try:
|
|
251
|
-
logger.debug("MCPInstrumentor: Entering instrumented _receive_loop section")
|
|
252
|
-
# The original logic for distinguishing request/notification from response for
|
|
253
|
-
# telemetry:
|
|
254
|
-
# Ensure streams are context managed
|
|
255
|
-
async with instance._read_stream, instance._write_stream:
|
|
256
|
-
async for message in instance._read_stream:
|
|
257
|
-
hud_task_run_id = get_current_task_run_id() # Get ID for each message
|
|
258
|
-
|
|
259
|
-
if isinstance(message, Exception):
|
|
260
|
-
await instance._handle_incoming(message) # Will be wrapped
|
|
261
|
-
continue
|
|
262
|
-
|
|
263
|
-
# Ensure we are dealing with SessionMessage
|
|
264
|
-
if (
|
|
265
|
-
not isinstance(message, SessionMessage)
|
|
266
|
-
or not hasattr(message, "message")
|
|
267
|
-
or not hasattr(message.message, "root")
|
|
268
|
-
):
|
|
269
|
-
logger.warning(
|
|
270
|
-
"Unexpected message type in _receive_loop: %s", type(message)
|
|
271
|
-
)
|
|
272
|
-
await instance._handle_incoming(
|
|
273
|
-
RuntimeError(f"Unknown message type: {message}")
|
|
274
|
-
)
|
|
275
|
-
continue
|
|
276
|
-
|
|
277
|
-
msg_root = message.message.root
|
|
278
|
-
|
|
279
|
-
if isinstance(msg_root, JSONRPCRequest | JSONRPCNotification):
|
|
280
|
-
# Let the (wrapped) _handle_incoming deal with these and record telemetry
|
|
281
|
-
await instance._handle_incoming(message)
|
|
282
|
-
elif isinstance(msg_root, JSONRPCResponse | JSONRPCError):
|
|
283
|
-
# This is a direct response/error, record it here
|
|
284
|
-
logger.debug(
|
|
285
|
-
"MCPInstrumentor: Capturing direct response/error ID: %s", msg_root.id
|
|
286
|
-
)
|
|
287
|
-
if hud_task_run_id:
|
|
288
|
-
is_error = isinstance(msg_root, JSONRPCError)
|
|
289
|
-
error_message = None
|
|
290
|
-
error_code = None
|
|
291
|
-
if is_error and hasattr(msg_root, "error"):
|
|
292
|
-
error_message = getattr(msg_root.error, "message", None)
|
|
293
|
-
error_code = str(getattr(msg_root.error, "code", None))
|
|
294
|
-
|
|
295
|
-
create_response_record(
|
|
296
|
-
method=f"response_to_id_{msg_root.id}", # Consistent method naming
|
|
297
|
-
related_request_id=msg_root.id,
|
|
298
|
-
response_id=msg_root.id,
|
|
299
|
-
is_error=is_error,
|
|
300
|
-
response_data=msg_root.model_dump(exclude_none=True),
|
|
301
|
-
error=error_message,
|
|
302
|
-
error_type=error_code,
|
|
303
|
-
direction=DirectionType.RECEIVED,
|
|
304
|
-
call_type=MCPCallType.RECEIVE_RESPONSE,
|
|
305
|
-
timestamp=time.time(), # Add timestamp here for accuracy
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
# Original logic to pass response to waiting stream
|
|
309
|
-
stream = instance._response_streams.pop(msg_root.id, None)
|
|
310
|
-
if stream:
|
|
311
|
-
await stream.send(msg_root)
|
|
312
|
-
else:
|
|
313
|
-
# This case should ideally be handled by _handle_incoming if it's an
|
|
314
|
-
# unsolicited response
|
|
315
|
-
logger.warning(
|
|
316
|
-
"Received response/error with unknown request ID %s and no "
|
|
317
|
-
"response stream.",
|
|
318
|
-
msg_root.id,
|
|
319
|
-
)
|
|
320
|
-
# Potentially pass to _handle_incoming if that's desired for unroutable
|
|
321
|
-
# responses
|
|
322
|
-
await instance._handle_incoming(message) # Let wrapped handler decide
|
|
323
|
-
else:
|
|
324
|
-
logger.warning(
|
|
325
|
-
"Unknown message root type in _receive_loop: %s", type(msg_root)
|
|
326
|
-
)
|
|
327
|
-
await instance._handle_incoming(message) # Let wrapped handler decide
|
|
70
|
+
"""Context-aware send request wrapper."""
|
|
71
|
+
hud_task_run_id = get_current_task_run_id()
|
|
328
72
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
instance._handle_incoming = orig_handle_incoming
|
|
333
|
-
# Re-raising to ensure original behavior if full loop fails, or call wrapped directly
|
|
334
|
-
# For simplicity now, we just log and let finally restore.
|
|
335
|
-
raise
|
|
336
|
-
finally:
|
|
337
|
-
instance._handle_incoming = orig_handle_incoming
|
|
338
|
-
logger.debug("MCPInstrumentor: Restored original _handle_incoming")
|
|
73
|
+
if not hud_task_run_id:
|
|
74
|
+
# No active trace - pass through without instrumentation
|
|
75
|
+
return await wrapped(*args, **kwargs)
|
|
339
76
|
|
|
340
|
-
async def _send_request_telemetry_wrapper(
|
|
341
|
-
self,
|
|
342
|
-
wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
|
|
343
|
-
instance: Any,
|
|
344
|
-
args: Any,
|
|
345
|
-
kwargs: Any,
|
|
346
|
-
) -> Any:
|
|
347
77
|
start_time = time.time()
|
|
348
|
-
|
|
78
|
+
request = args[0] if args else None
|
|
79
|
+
|
|
80
|
+
# Extract method name from the request
|
|
349
81
|
method_name = "unknown_method"
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if args and len(args) > 0:
|
|
354
|
-
request_session_msg = args[0] # Assuming SessionMessage
|
|
355
|
-
if hasattr(request_session_msg, "message") and hasattr(
|
|
356
|
-
request_session_msg.message, "root"
|
|
357
|
-
):
|
|
358
|
-
actual_request = request_session_msg.message.root
|
|
359
|
-
if hasattr(actual_request, "method"):
|
|
360
|
-
method_name = actual_request.method
|
|
361
|
-
if hasattr(actual_request, "id"):
|
|
362
|
-
request_id = actual_request.id
|
|
363
|
-
if hasattr(actual_request, "model_dump"):
|
|
364
|
-
try:
|
|
365
|
-
request_obj_data = actual_request.model_dump(exclude_none=True)
|
|
366
|
-
except Exception as e:
|
|
367
|
-
logger.warning(
|
|
368
|
-
"Could not dump request data for %s: %s",
|
|
369
|
-
method_name,
|
|
370
|
-
e,
|
|
371
|
-
)
|
|
82
|
+
message_id = None
|
|
83
|
+
request_data = None
|
|
372
84
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
)
|
|
381
110
|
|
|
382
111
|
try:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
)
|
|
387
132
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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(),
|
|
400
154
|
)
|
|
155
|
+
|
|
401
156
|
return result
|
|
157
|
+
|
|
402
158
|
except Exception as e:
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
)
|
|
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
|
+
|
|
413
188
|
raise
|
|
414
189
|
|
|
415
|
-
async def
|
|
190
|
+
async def _context_aware_send_notification_wrapper(
|
|
416
191
|
self,
|
|
417
192
|
wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
|
|
418
193
|
instance: Any,
|
|
419
194
|
args: Any,
|
|
420
195
|
kwargs: Any,
|
|
421
196
|
) -> Any:
|
|
422
|
-
|
|
197
|
+
"""Context-aware send notification wrapper."""
|
|
423
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
|
|
424
208
|
method_name = "unknown_method"
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
method_name,
|
|
442
|
-
e,
|
|
443
|
-
)
|
|
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)
|
|
444
225
|
|
|
445
226
|
record_data_base = {
|
|
446
227
|
"method": method_name,
|
|
228
|
+
"message_id": None, # Notifications don't have IDs
|
|
229
|
+
"call_type": MCPCallType.SEND_NOTIFICATION,
|
|
447
230
|
"direction": DirectionType.SENT,
|
|
448
|
-
"
|
|
449
|
-
"notification_data": notification_obj_data,
|
|
231
|
+
"notification_data": notification_data,
|
|
450
232
|
}
|
|
451
233
|
|
|
452
234
|
try:
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
)
|
|
235
|
+
create_notification_record(
|
|
236
|
+
**record_data_base, status=StatusType.STARTED, start_time=start_time
|
|
237
|
+
)
|
|
457
238
|
|
|
458
|
-
result = await wrapped(*args, **kwargs)
|
|
239
|
+
result = await wrapped(*args, **kwargs)
|
|
459
240
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
)
|
|
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
|
+
)
|
|
468
248
|
return result
|
|
469
249
|
except Exception as e:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
)
|
|
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
|
+
)
|
|
480
259
|
raise
|
|
481
|
-
|
|
482
|
-
def record_manual_test(self, **kwargs: Any) -> bool:
|
|
483
|
-
"""Record a manual test telemetry entry"""
|
|
484
|
-
hud_task_run_id = get_current_task_run_id()
|
|
485
|
-
if not hud_task_run_id:
|
|
486
|
-
logger.warning("Manual test not recorded: No active task_run_id")
|
|
487
|
-
return False
|
|
488
|
-
try:
|
|
489
|
-
record = MCPManualTestCall.create(task_run_id=hud_task_run_id, **kwargs)
|
|
490
|
-
buffer_mcp_call(record) # buffer_mcp_call handles Pydantic model directly
|
|
491
|
-
logger.debug("Manual test recorded: %s", record.model_dump(exclude_none=True))
|
|
492
|
-
return True
|
|
493
|
-
except Exception as e:
|
|
494
|
-
logger.warning("Manual test not recorded: %s", e)
|
|
495
|
-
return False
|