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.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -414
  87. hud/tools/computer/hud.py +376 -328
  88. hud/tools/computer/openai.py +295 -286
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.4.dist-info/METADATA +0 -284
  190. hud_python-0.3.4.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {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)
@@ -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)
@@ -1 +0,0 @@
1
- # Tests for hud.telemetry module