hud-python 0.4.1__py3-none-any.whl → 0.4.3__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 (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
@@ -1,379 +1,379 @@
1
- """General-purpose instrumentation decorator for HUD telemetry.
2
-
3
- This module provides the instrument() decorator that users can use
4
- to instrument any function with OpenTelemetry spans.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import asyncio
10
- import functools
11
- import inspect
12
- import json
13
- import logging
14
- from typing import TYPE_CHECKING, Any, TypeVar, overload
15
-
16
- import pydantic_core
17
- from opentelemetry import trace
18
- from opentelemetry.trace import SpanKind, Status, StatusCode
19
-
20
- from hud.otel import configure_telemetry, is_telemetry_configured
21
- from hud.otel.context import get_current_task_run_id
22
-
23
- if TYPE_CHECKING:
24
- from collections.abc import Awaitable, Callable
25
- from typing import ParamSpec
26
-
27
- P = ParamSpec("P")
28
- R = TypeVar("R")
29
-
30
- logger = logging.getLogger(__name__)
31
-
32
-
33
- def _serialize_value(value: Any, max_items: int = 10) -> Any:
34
- """Serialize a value for span attributes.
35
-
36
- Uses pydantic_core.to_json for robust serialization of complex objects.
37
-
38
- Args:
39
- value: The value to serialize
40
- max_items: Maximum number of items for collections
41
-
42
- Returns:
43
- JSON-serializable version of the value
44
- """
45
- # Simple types pass through
46
- if isinstance(value, str | int | float | bool | type(None)):
47
- return value
48
-
49
- # For collections, we need to limit size first
50
- if isinstance(value, list | tuple):
51
- value = value[:max_items] if len(value) > max_items else value
52
- elif isinstance(value, dict) and len(value) > max_items:
53
- value = dict(list(value.items())[:max_items])
54
-
55
- # Use pydantic_core for serialization - it handles:
56
- # - Pydantic models (via model_dump)
57
- # - Dataclasses (via asdict)
58
- # - Bytes (encodes to string)
59
- # - Custom objects (via __dict__ or repr)
60
- # - Complex nested structures
61
- try:
62
- # Convert to JSON bytes then back to Python objects
63
- # This ensures we get JSON-serializable types
64
- json_bytes = pydantic_core.to_json(value, fallback=str)
65
- return json.loads(json_bytes)
66
- except Exception:
67
- # Fallback if pydantic_core fails somehow
68
- return f"<{type(value).__name__}>"
69
-
70
-
71
- @overload
72
- def instrument(
73
- func: None = None,
74
- *,
75
- name: str | None = None,
76
- span_type: str = "function",
77
- attributes: dict[str, Any] | None = None,
78
- record_args: bool = True,
79
- record_result: bool = True,
80
- span_kind: SpanKind = SpanKind.INTERNAL,
81
- ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
82
-
83
-
84
- @overload
85
- def instrument(
86
- func: Callable[P, R],
87
- *,
88
- name: str | None = None,
89
- span_type: str = "function",
90
- attributes: dict[str, Any] | None = None,
91
- record_args: bool = True,
92
- record_result: bool = True,
93
- span_kind: SpanKind = SpanKind.INTERNAL,
94
- ) -> Callable[P, R]: ...
95
-
96
-
97
- @overload
98
- def instrument(
99
- func: Callable[P, Awaitable[R]],
100
- *,
101
- name: str | None = None,
102
- span_type: str = "function",
103
- attributes: dict[str, Any] | None = None,
104
- record_args: bool = True,
105
- record_result: bool = True,
106
- span_kind: SpanKind = SpanKind.INTERNAL,
107
- ) -> Callable[P, Awaitable[R]]: ...
108
-
109
-
110
- def instrument(
111
- func: Callable[..., Any] | None = None,
112
- *,
113
- name: str | None = None,
114
- span_type: str = "function",
115
- attributes: dict[str, Any] | None = None,
116
- record_args: bool = True,
117
- record_result: bool = True,
118
- span_kind: SpanKind = SpanKind.INTERNAL,
119
- ) -> Callable[..., Any]:
120
- """Instrument a function to emit OpenTelemetry spans.
121
-
122
- This decorator wraps any function to automatically create spans for
123
- observability. It works with both sync and async functions.
124
-
125
- Args:
126
- func: The function to instrument (when used without parentheses)
127
- name: Custom span name (defaults to fully qualified function name)
128
- span_type: The category for this span (e.g., "agent", "mcp", "database", "validation")
129
- attributes: Additional attributes to attach to every span
130
- record_args: Whether to record function arguments in the request field
131
- record_result: Whether to record function result in the result field
132
- span_kind: OpenTelemetry span kind (INTERNAL, CLIENT, SERVER, etc.)
133
-
134
- Returns:
135
- The instrumented function that emits spans
136
-
137
- Examples:
138
- # Basic usage - defaults to category="function"
139
- @hud.instrument
140
- async def process_data(items: list[str]) -> dict:
141
- return {"count": len(items)}
142
-
143
- # Custom category
144
- @hud.instrument(
145
- span_type="database", # This becomes category="database"
146
- record_args=True,
147
- record_result=True
148
- )
149
- async def query_users(filter: dict) -> list[User]:
150
- return await db.find(filter)
151
-
152
- # Agent instrumentation
153
- @hud.instrument(
154
- span_type="agent", # category="agent" gets special handling
155
- record_args=False, # Don't record large message arrays
156
- record_result=True
157
- )
158
- async def get_model_response(self, messages: list) -> Response:
159
- return await self.model.complete(messages)
160
-
161
- # Instrument third-party functions
162
- import requests
163
- requests.get = hud.instrument(
164
- span_type="http", # category="http"
165
- span_kind=SpanKind.CLIENT
166
- )(requests.get)
167
-
168
- # Conditional instrumentation
169
- if settings.enable_db_tracing:
170
- db.query = hud.instrument(db.query)
171
- """
172
- # Don't configure telemetry at decoration time - wait until first call
173
- # This allows users to configure alternative backends before importing agents
174
-
175
- def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
176
- # Check if already instrumented
177
- if hasattr(func, "_hud_instrumented"):
178
- logger.debug("Function %s already instrumented, skipping", func.__name__)
179
- return func
180
-
181
- # Get function metadata
182
- func_module = getattr(func, "__module__", "unknown")
183
- func_name = getattr(func, "__name__", "unknown")
184
- func_qualname = getattr(func, "__qualname__", func_name)
185
-
186
- # Determine span name
187
- span_name = name or f"{func_module}.{func_qualname}"
188
-
189
- # Get function signature for argument parsing
190
- try:
191
- sig = inspect.signature(func)
192
- except (ValueError, TypeError):
193
- sig = None
194
-
195
- @functools.wraps(func)
196
- async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
197
- # Ensure telemetry is configured (lazy initialization)
198
- # Only configure with defaults if user hasn't configured it yet
199
- if not is_telemetry_configured():
200
- configure_telemetry()
201
-
202
- tracer = trace.get_tracer("hud-sdk")
203
-
204
- # Build span attributes
205
- span_attrs = {
206
- "category": span_type, # span_type IS the category
207
- "function.module": func_module,
208
- "function.name": func_name,
209
- "function.qualname": func_qualname,
210
- }
211
-
212
- # Add custom attributes
213
- if attributes:
214
- span_attrs.update(attributes)
215
-
216
- # Add current task_run_id if available
217
- task_run_id = get_current_task_run_id()
218
- if task_run_id:
219
- span_attrs["hud.task_run_id"] = task_run_id
220
-
221
- # Record function arguments if requested
222
- if record_args and sig:
223
- try:
224
- bound_args = sig.bind(*args, **kwargs)
225
- bound_args.apply_defaults()
226
-
227
- # Serialize arguments (with safety limits)
228
- args_dict = {}
229
- for param_name, value in bound_args.arguments.items():
230
- try:
231
- # Skip 'self' and 'cls' parameters
232
- if param_name in ("self", "cls"):
233
- continue
234
-
235
- args_dict[param_name] = _serialize_value(value)
236
- except Exception:
237
- args_dict[param_name] = "<serialization_error>"
238
-
239
- if args_dict:
240
- args_json = json.dumps(args_dict)
241
- span_attrs["function.arguments"] = args_json
242
- # Always set generic request field for consistency
243
- span_attrs["request"] = args_json
244
- except Exception as e:
245
- logger.debug("Failed to record function arguments: %s", e)
246
-
247
- with tracer.start_as_current_span(
248
- span_name,
249
- kind=span_kind,
250
- attributes=span_attrs,
251
- ) as span:
252
- try:
253
- # Execute the function
254
- result = await func(*args, **kwargs)
255
-
256
- # Record result if requested
257
- if record_result:
258
- try:
259
- serialized = _serialize_value(result)
260
- result_json = json.dumps(serialized)
261
- span.set_attribute("function.result", result_json)
262
- # Always set generic result field for consistency
263
- span.set_attribute("result", result_json)
264
-
265
- # Also set result type for complex objects
266
- if not isinstance(
267
- result, str | int | float | bool | type(None) | list | tuple | dict
268
- ):
269
- span.set_attribute("function.result_type", type(result).__name__)
270
- except Exception as e:
271
- logger.debug("Failed to record function result: %s", e)
272
-
273
- span.set_status(Status(StatusCode.OK))
274
- return result
275
-
276
- except Exception as e:
277
- # Record exception and set error status
278
- span.record_exception(e)
279
- span.set_status(Status(StatusCode.ERROR, str(e)))
280
- raise
281
-
282
- @functools.wraps(func)
283
- def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
284
- # Ensure telemetry is configured (lazy initialization)
285
- # Only configure with defaults if user hasn't configured it yet
286
- if not is_telemetry_configured():
287
- configure_telemetry()
288
-
289
- tracer = trace.get_tracer("hud-sdk")
290
-
291
- # Build span attributes (same as async)
292
- span_attrs = {
293
- "category": span_type, # span_type IS the category
294
- "function.module": func_module,
295
- "function.name": func_name,
296
- "function.qualname": func_qualname,
297
- }
298
-
299
- if attributes:
300
- span_attrs.update(attributes)
301
-
302
- task_run_id = get_current_task_run_id()
303
- if task_run_id:
304
- span_attrs["hud.task_run_id"] = task_run_id
305
-
306
- # Record function arguments if requested
307
- if record_args and sig:
308
- try:
309
- bound_args = sig.bind(*args, **kwargs)
310
- bound_args.apply_defaults()
311
-
312
- args_dict = {}
313
- for param_name, value in bound_args.arguments.items():
314
- try:
315
- if param_name in ("self", "cls"):
316
- continue
317
-
318
- args_dict[param_name] = _serialize_value(value)
319
- except Exception:
320
- args_dict[param_name] = "<serialization_error>"
321
-
322
- if args_dict:
323
- args_json = json.dumps(args_dict)
324
- span_attrs["function.arguments"] = args_json
325
- # Always set generic request field for consistency
326
- span_attrs["request"] = args_json
327
- except Exception as e:
328
- logger.debug("Failed to record function arguments: %s", e)
329
-
330
- with tracer.start_as_current_span(
331
- span_name,
332
- kind=span_kind,
333
- attributes=span_attrs,
334
- ) as span:
335
- try:
336
- # Execute the function
337
- result = func(*args, **kwargs)
338
-
339
- # Record result if requested
340
- if record_result:
341
- try:
342
- serialized = _serialize_value(result)
343
- result_json = json.dumps(serialized)
344
- span.set_attribute("function.result", result_json)
345
- # Always set generic result field for consistency
346
- span.set_attribute("result", result_json)
347
-
348
- # Also set result type for complex objects
349
- if not isinstance(
350
- result, str | int | float | bool | type(None) | list | tuple | dict
351
- ):
352
- span.set_attribute("function.result_type", type(result).__name__)
353
- except Exception as e:
354
- logger.debug("Failed to record function result: %s", e)
355
-
356
- span.set_status(Status(StatusCode.OK))
357
- return result
358
-
359
- except Exception as e:
360
- span.record_exception(e)
361
- span.set_status(Status(StatusCode.ERROR, str(e)))
362
- raise
363
-
364
- # Choose wrapper based on function type
365
- wrapper = async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
366
-
367
- # Mark as instrumented
368
- wrapper._hud_instrumented = True # type: ignore[attr-defined]
369
- wrapper._hud_original = func # type: ignore[attr-defined]
370
-
371
- return wrapper
372
-
373
- # Handle usage with or without parentheses
374
- if func is None:
375
- # Called with arguments: @instrument(name="foo")
376
- return decorator
377
- else:
378
- # Called without arguments: @instrument
379
- return decorator(func)
1
+ """General-purpose instrumentation decorator for HUD telemetry.
2
+
3
+ This module provides the instrument() decorator that users can use
4
+ to instrument any function with OpenTelemetry spans.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import functools
11
+ import inspect
12
+ import json
13
+ import logging
14
+ from typing import TYPE_CHECKING, Any, TypeVar, overload
15
+
16
+ import pydantic_core
17
+ from opentelemetry import trace
18
+ from opentelemetry.trace import SpanKind, Status, StatusCode
19
+
20
+ from hud.otel import configure_telemetry, is_telemetry_configured
21
+ from hud.otel.context import get_current_task_run_id
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Awaitable, Callable
25
+ from typing import ParamSpec
26
+
27
+ P = ParamSpec("P")
28
+ R = TypeVar("R")
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def _serialize_value(value: Any, max_items: int = 10) -> Any:
34
+ """Serialize a value for span attributes.
35
+
36
+ Uses pydantic_core.to_json for robust serialization of complex objects.
37
+
38
+ Args:
39
+ value: The value to serialize
40
+ max_items: Maximum number of items for collections
41
+
42
+ Returns:
43
+ JSON-serializable version of the value
44
+ """
45
+ # Simple types pass through
46
+ if isinstance(value, str | int | float | bool | type(None)):
47
+ return value
48
+
49
+ # For collections, we need to limit size first
50
+ if isinstance(value, list | tuple):
51
+ value = value[:max_items] if len(value) > max_items else value
52
+ elif isinstance(value, dict) and len(value) > max_items:
53
+ value = dict(list(value.items())[:max_items])
54
+
55
+ # Use pydantic_core for serialization - it handles:
56
+ # - Pydantic models (via model_dump)
57
+ # - Dataclasses (via asdict)
58
+ # - Bytes (encodes to string)
59
+ # - Custom objects (via __dict__ or repr)
60
+ # - Complex nested structures
61
+ try:
62
+ # Convert to JSON bytes then back to Python objects
63
+ # This ensures we get JSON-serializable types
64
+ json_bytes = pydantic_core.to_json(value, fallback=str)
65
+ return json.loads(json_bytes)
66
+ except Exception:
67
+ # Fallback if pydantic_core fails somehow
68
+ return f"<{type(value).__name__}>"
69
+
70
+
71
+ @overload
72
+ def instrument(
73
+ func: None = None,
74
+ *,
75
+ name: str | None = None,
76
+ span_type: str = "function",
77
+ attributes: dict[str, Any] | None = None,
78
+ record_args: bool = True,
79
+ record_result: bool = True,
80
+ span_kind: SpanKind = SpanKind.INTERNAL,
81
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
82
+
83
+
84
+ @overload
85
+ def instrument(
86
+ func: Callable[P, R],
87
+ *,
88
+ name: str | None = None,
89
+ span_type: str = "function",
90
+ attributes: dict[str, Any] | None = None,
91
+ record_args: bool = True,
92
+ record_result: bool = True,
93
+ span_kind: SpanKind = SpanKind.INTERNAL,
94
+ ) -> Callable[P, R]: ...
95
+
96
+
97
+ @overload
98
+ def instrument(
99
+ func: Callable[P, Awaitable[R]],
100
+ *,
101
+ name: str | None = None,
102
+ span_type: str = "function",
103
+ attributes: dict[str, Any] | None = None,
104
+ record_args: bool = True,
105
+ record_result: bool = True,
106
+ span_kind: SpanKind = SpanKind.INTERNAL,
107
+ ) -> Callable[P, Awaitable[R]]: ...
108
+
109
+
110
+ def instrument(
111
+ func: Callable[..., Any] | None = None,
112
+ *,
113
+ name: str | None = None,
114
+ span_type: str = "function",
115
+ attributes: dict[str, Any] | None = None,
116
+ record_args: bool = True,
117
+ record_result: bool = True,
118
+ span_kind: SpanKind = SpanKind.INTERNAL,
119
+ ) -> Callable[..., Any]:
120
+ """Instrument a function to emit OpenTelemetry spans.
121
+
122
+ This decorator wraps any function to automatically create spans for
123
+ observability. It works with both sync and async functions.
124
+
125
+ Args:
126
+ func: The function to instrument (when used without parentheses)
127
+ name: Custom span name (defaults to fully qualified function name)
128
+ span_type: The category for this span (e.g., "agent", "mcp", "database", "validation")
129
+ attributes: Additional attributes to attach to every span
130
+ record_args: Whether to record function arguments in the request field
131
+ record_result: Whether to record function result in the result field
132
+ span_kind: OpenTelemetry span kind (INTERNAL, CLIENT, SERVER, etc.)
133
+
134
+ Returns:
135
+ The instrumented function that emits spans
136
+
137
+ Examples:
138
+ # Basic usage - defaults to category="function"
139
+ @hud.instrument
140
+ async def process_data(items: list[str]) -> dict:
141
+ return {"count": len(items)}
142
+
143
+ # Custom category
144
+ @hud.instrument(
145
+ span_type="database", # This becomes category="database"
146
+ record_args=True,
147
+ record_result=True
148
+ )
149
+ async def query_users(filter: dict) -> list[User]:
150
+ return await db.find(filter)
151
+
152
+ # Agent instrumentation
153
+ @hud.instrument(
154
+ span_type="agent", # category="agent" gets special handling
155
+ record_args=False, # Don't record large message arrays
156
+ record_result=True
157
+ )
158
+ async def get_model_response(self, messages: list) -> Response:
159
+ return await self.model.complete(messages)
160
+
161
+ # Instrument third-party functions
162
+ import requests
163
+ requests.get = hud.instrument(
164
+ span_type="http", # category="http"
165
+ span_kind=SpanKind.CLIENT
166
+ )(requests.get)
167
+
168
+ # Conditional instrumentation
169
+ if settings.enable_db_tracing:
170
+ db.query = hud.instrument(db.query)
171
+ """
172
+ # Don't configure telemetry at decoration time - wait until first call
173
+ # This allows users to configure alternative backends before importing agents
174
+
175
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
176
+ # Check if already instrumented
177
+ if hasattr(func, "_hud_instrumented"):
178
+ logger.debug("Function %s already instrumented, skipping", func.__name__)
179
+ return func
180
+
181
+ # Get function metadata
182
+ func_module = getattr(func, "__module__", "unknown")
183
+ func_name = getattr(func, "__name__", "unknown")
184
+ func_qualname = getattr(func, "__qualname__", func_name)
185
+
186
+ # Determine span name
187
+ span_name = name or f"{func_module}.{func_qualname}"
188
+
189
+ # Get function signature for argument parsing
190
+ try:
191
+ sig = inspect.signature(func)
192
+ except (ValueError, TypeError):
193
+ sig = None
194
+
195
+ @functools.wraps(func)
196
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
197
+ # Ensure telemetry is configured (lazy initialization)
198
+ # Only configure with defaults if user hasn't configured it yet
199
+ if not is_telemetry_configured():
200
+ configure_telemetry()
201
+
202
+ tracer = trace.get_tracer("hud-sdk")
203
+
204
+ # Build span attributes
205
+ span_attrs = {
206
+ "category": span_type, # span_type IS the category
207
+ "function.module": func_module,
208
+ "function.name": func_name,
209
+ "function.qualname": func_qualname,
210
+ }
211
+
212
+ # Add custom attributes
213
+ if attributes:
214
+ span_attrs.update(attributes)
215
+
216
+ # Add current task_run_id if available
217
+ task_run_id = get_current_task_run_id()
218
+ if task_run_id:
219
+ span_attrs["hud.task_run_id"] = task_run_id
220
+
221
+ # Record function arguments if requested
222
+ if record_args and sig:
223
+ try:
224
+ bound_args = sig.bind(*args, **kwargs)
225
+ bound_args.apply_defaults()
226
+
227
+ # Serialize arguments (with safety limits)
228
+ args_dict = {}
229
+ for param_name, value in bound_args.arguments.items():
230
+ try:
231
+ # Skip 'self' and 'cls' parameters
232
+ if param_name in ("self", "cls"):
233
+ continue
234
+
235
+ args_dict[param_name] = _serialize_value(value)
236
+ except Exception:
237
+ args_dict[param_name] = "<serialization_error>"
238
+
239
+ if args_dict:
240
+ args_json = json.dumps(args_dict)
241
+ span_attrs["function.arguments"] = args_json
242
+ # Always set generic request field for consistency
243
+ span_attrs["request"] = args_json
244
+ except Exception as e:
245
+ logger.debug("Failed to record function arguments: %s", e)
246
+
247
+ with tracer.start_as_current_span(
248
+ span_name,
249
+ kind=span_kind,
250
+ attributes=span_attrs,
251
+ ) as span:
252
+ try:
253
+ # Execute the function
254
+ result = await func(*args, **kwargs)
255
+
256
+ # Record result if requested
257
+ if record_result:
258
+ try:
259
+ serialized = _serialize_value(result)
260
+ result_json = json.dumps(serialized)
261
+ span.set_attribute("function.result", result_json)
262
+ # Always set generic result field for consistency
263
+ span.set_attribute("result", result_json)
264
+
265
+ # Also set result type for complex objects
266
+ if not isinstance(
267
+ result, str | int | float | bool | type(None) | list | tuple | dict
268
+ ):
269
+ span.set_attribute("function.result_type", type(result).__name__)
270
+ except Exception as e:
271
+ logger.debug("Failed to record function result: %s", e)
272
+
273
+ span.set_status(Status(StatusCode.OK))
274
+ return result
275
+
276
+ except Exception as e:
277
+ # Record exception and set error status
278
+ span.record_exception(e)
279
+ span.set_status(Status(StatusCode.ERROR, str(e)))
280
+ raise
281
+
282
+ @functools.wraps(func)
283
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
284
+ # Ensure telemetry is configured (lazy initialization)
285
+ # Only configure with defaults if user hasn't configured it yet
286
+ if not is_telemetry_configured():
287
+ configure_telemetry()
288
+
289
+ tracer = trace.get_tracer("hud-sdk")
290
+
291
+ # Build span attributes (same as async)
292
+ span_attrs = {
293
+ "category": span_type, # span_type IS the category
294
+ "function.module": func_module,
295
+ "function.name": func_name,
296
+ "function.qualname": func_qualname,
297
+ }
298
+
299
+ if attributes:
300
+ span_attrs.update(attributes)
301
+
302
+ task_run_id = get_current_task_run_id()
303
+ if task_run_id:
304
+ span_attrs["hud.task_run_id"] = task_run_id
305
+
306
+ # Record function arguments if requested
307
+ if record_args and sig:
308
+ try:
309
+ bound_args = sig.bind(*args, **kwargs)
310
+ bound_args.apply_defaults()
311
+
312
+ args_dict = {}
313
+ for param_name, value in bound_args.arguments.items():
314
+ try:
315
+ if param_name in ("self", "cls"):
316
+ continue
317
+
318
+ args_dict[param_name] = _serialize_value(value)
319
+ except Exception:
320
+ args_dict[param_name] = "<serialization_error>"
321
+
322
+ if args_dict:
323
+ args_json = json.dumps(args_dict)
324
+ span_attrs["function.arguments"] = args_json
325
+ # Always set generic request field for consistency
326
+ span_attrs["request"] = args_json
327
+ except Exception as e:
328
+ logger.debug("Failed to record function arguments: %s", e)
329
+
330
+ with tracer.start_as_current_span(
331
+ span_name,
332
+ kind=span_kind,
333
+ attributes=span_attrs,
334
+ ) as span:
335
+ try:
336
+ # Execute the function
337
+ result = func(*args, **kwargs)
338
+
339
+ # Record result if requested
340
+ if record_result:
341
+ try:
342
+ serialized = _serialize_value(result)
343
+ result_json = json.dumps(serialized)
344
+ span.set_attribute("function.result", result_json)
345
+ # Always set generic result field for consistency
346
+ span.set_attribute("result", result_json)
347
+
348
+ # Also set result type for complex objects
349
+ if not isinstance(
350
+ result, str | int | float | bool | type(None) | list | tuple | dict
351
+ ):
352
+ span.set_attribute("function.result_type", type(result).__name__)
353
+ except Exception as e:
354
+ logger.debug("Failed to record function result: %s", e)
355
+
356
+ span.set_status(Status(StatusCode.OK))
357
+ return result
358
+
359
+ except Exception as e:
360
+ span.record_exception(e)
361
+ span.set_status(Status(StatusCode.ERROR, str(e)))
362
+ raise
363
+
364
+ # Choose wrapper based on function type
365
+ wrapper = async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
366
+
367
+ # Mark as instrumented
368
+ wrapper._hud_instrumented = True # type: ignore[attr-defined]
369
+ wrapper._hud_original = func # type: ignore[attr-defined]
370
+
371
+ return wrapper
372
+
373
+ # Handle usage with or without parentheses
374
+ if func is None:
375
+ # Called with arguments: @instrument(name="foo")
376
+ return decorator
377
+ else:
378
+ # Called without arguments: @instrument
379
+ return decorator(func)