hud-python 0.4.45__py3-none-any.whl → 0.5.13__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.
Files changed (282) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +70 -5
  3. hud/agents/base.py +238 -500
  4. hud/agents/claude.py +236 -247
  5. hud/agents/gateway.py +42 -0
  6. hud/agents/gemini.py +264 -0
  7. hud/agents/gemini_cua.py +324 -0
  8. hud/agents/grounded_openai.py +98 -100
  9. hud/agents/misc/integration_test_agent.py +51 -20
  10. hud/agents/misc/response_agent.py +48 -36
  11. hud/agents/openai.py +282 -296
  12. hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
  13. hud/agents/operator.py +199 -0
  14. hud/agents/resolver.py +70 -0
  15. hud/agents/tests/conftest.py +133 -0
  16. hud/agents/tests/test_base.py +300 -622
  17. hud/agents/tests/test_base_runtime.py +233 -0
  18. hud/agents/tests/test_claude.py +381 -214
  19. hud/agents/tests/test_client.py +9 -10
  20. hud/agents/tests/test_gemini.py +369 -0
  21. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  22. hud/agents/tests/test_openai.py +377 -140
  23. hud/agents/tests/test_operator.py +362 -0
  24. hud/agents/tests/test_resolver.py +192 -0
  25. hud/agents/tests/test_run_eval.py +179 -0
  26. hud/agents/types.py +148 -0
  27. hud/cli/__init__.py +493 -546
  28. hud/cli/analyze.py +43 -5
  29. hud/cli/build.py +699 -113
  30. hud/cli/debug.py +8 -5
  31. hud/cli/dev.py +889 -732
  32. hud/cli/eval.py +793 -667
  33. hud/cli/flows/dev.py +167 -0
  34. hud/cli/flows/init.py +191 -0
  35. hud/cli/flows/tasks.py +153 -56
  36. hud/cli/flows/templates.py +151 -0
  37. hud/cli/flows/tests/__init__.py +1 -0
  38. hud/cli/flows/tests/test_dev.py +126 -0
  39. hud/cli/init.py +60 -58
  40. hud/cli/pull.py +1 -1
  41. hud/cli/push.py +38 -13
  42. hud/cli/rft.py +311 -0
  43. hud/cli/rft_status.py +145 -0
  44. hud/cli/tests/test_analyze.py +5 -5
  45. hud/cli/tests/test_analyze_metadata.py +3 -2
  46. hud/cli/tests/test_analyze_module.py +120 -0
  47. hud/cli/tests/test_build.py +110 -8
  48. hud/cli/tests/test_build_failure.py +41 -0
  49. hud/cli/tests/test_build_module.py +50 -0
  50. hud/cli/tests/test_cli_init.py +6 -1
  51. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  52. hud/cli/tests/test_cli_root.py +140 -0
  53. hud/cli/tests/test_convert.py +361 -0
  54. hud/cli/tests/test_debug.py +12 -10
  55. hud/cli/tests/test_dev.py +197 -0
  56. hud/cli/tests/test_eval.py +251 -0
  57. hud/cli/tests/test_eval_bedrock.py +51 -0
  58. hud/cli/tests/test_init.py +124 -0
  59. hud/cli/tests/test_main_module.py +11 -5
  60. hud/cli/tests/test_mcp_server.py +12 -100
  61. hud/cli/tests/test_push.py +1 -1
  62. hud/cli/tests/test_push_happy.py +74 -0
  63. hud/cli/tests/test_push_wrapper.py +23 -0
  64. hud/cli/tests/test_registry.py +1 -1
  65. hud/cli/tests/test_utils.py +1 -1
  66. hud/cli/{rl → utils}/celebrate.py +14 -12
  67. hud/cli/utils/config.py +18 -1
  68. hud/cli/utils/docker.py +130 -4
  69. hud/cli/utils/env_check.py +9 -9
  70. hud/cli/utils/git.py +136 -0
  71. hud/cli/utils/interactive.py +39 -5
  72. hud/cli/utils/metadata.py +70 -1
  73. hud/cli/utils/runner.py +1 -1
  74. hud/cli/utils/server.py +2 -2
  75. hud/cli/utils/source_hash.py +3 -3
  76. hud/cli/utils/tasks.py +4 -1
  77. hud/cli/utils/tests/__init__.py +0 -0
  78. hud/cli/utils/tests/test_config.py +58 -0
  79. hud/cli/utils/tests/test_docker.py +93 -0
  80. hud/cli/utils/tests/test_docker_hints.py +71 -0
  81. hud/cli/utils/tests/test_env_check.py +74 -0
  82. hud/cli/utils/tests/test_environment.py +42 -0
  83. hud/cli/utils/tests/test_git.py +142 -0
  84. hud/cli/utils/tests/test_interactive_module.py +60 -0
  85. hud/cli/utils/tests/test_local_runner.py +50 -0
  86. hud/cli/utils/tests/test_logging_utils.py +23 -0
  87. hud/cli/utils/tests/test_metadata.py +49 -0
  88. hud/cli/utils/tests/test_package_runner.py +35 -0
  89. hud/cli/utils/tests/test_registry_utils.py +49 -0
  90. hud/cli/utils/tests/test_remote_runner.py +25 -0
  91. hud/cli/utils/tests/test_runner_modules.py +52 -0
  92. hud/cli/utils/tests/test_source_hash.py +36 -0
  93. hud/cli/utils/tests/test_tasks.py +80 -0
  94. hud/cli/utils/version_check.py +258 -0
  95. hud/cli/{rl → utils}/viewer.py +2 -2
  96. hud/clients/README.md +12 -11
  97. hud/clients/__init__.py +4 -3
  98. hud/clients/base.py +166 -26
  99. hud/clients/environment.py +51 -0
  100. hud/clients/fastmcp.py +13 -6
  101. hud/clients/mcp_use.py +45 -15
  102. hud/clients/tests/test_analyze_scenarios.py +206 -0
  103. hud/clients/tests/test_protocol.py +9 -3
  104. hud/datasets/__init__.py +23 -20
  105. hud/datasets/loader.py +326 -0
  106. hud/datasets/runner.py +198 -105
  107. hud/datasets/tests/__init__.py +0 -0
  108. hud/datasets/tests/test_loader.py +221 -0
  109. hud/datasets/tests/test_utils.py +315 -0
  110. hud/datasets/utils.py +270 -90
  111. hud/environment/__init__.py +52 -0
  112. hud/environment/connection.py +258 -0
  113. hud/environment/connectors/__init__.py +33 -0
  114. hud/environment/connectors/base.py +68 -0
  115. hud/environment/connectors/local.py +177 -0
  116. hud/environment/connectors/mcp_config.py +137 -0
  117. hud/environment/connectors/openai.py +101 -0
  118. hud/environment/connectors/remote.py +172 -0
  119. hud/environment/environment.py +835 -0
  120. hud/environment/integrations/__init__.py +45 -0
  121. hud/environment/integrations/adk.py +67 -0
  122. hud/environment/integrations/anthropic.py +196 -0
  123. hud/environment/integrations/gemini.py +92 -0
  124. hud/environment/integrations/langchain.py +82 -0
  125. hud/environment/integrations/llamaindex.py +68 -0
  126. hud/environment/integrations/openai.py +238 -0
  127. hud/environment/mock.py +306 -0
  128. hud/environment/router.py +263 -0
  129. hud/environment/scenarios.py +620 -0
  130. hud/environment/tests/__init__.py +1 -0
  131. hud/environment/tests/test_connection.py +317 -0
  132. hud/environment/tests/test_connectors.py +205 -0
  133. hud/environment/tests/test_environment.py +593 -0
  134. hud/environment/tests/test_integrations.py +257 -0
  135. hud/environment/tests/test_local_connectors.py +242 -0
  136. hud/environment/tests/test_scenarios.py +1086 -0
  137. hud/environment/tests/test_tools.py +208 -0
  138. hud/environment/types.py +23 -0
  139. hud/environment/utils/__init__.py +35 -0
  140. hud/environment/utils/formats.py +215 -0
  141. hud/environment/utils/schema.py +171 -0
  142. hud/environment/utils/tool_wrappers.py +113 -0
  143. hud/eval/__init__.py +67 -0
  144. hud/eval/context.py +727 -0
  145. hud/eval/display.py +299 -0
  146. hud/eval/instrument.py +187 -0
  147. hud/eval/manager.py +533 -0
  148. hud/eval/parallel.py +268 -0
  149. hud/eval/task.py +372 -0
  150. hud/eval/tests/__init__.py +1 -0
  151. hud/eval/tests/test_context.py +178 -0
  152. hud/eval/tests/test_eval.py +210 -0
  153. hud/eval/tests/test_manager.py +152 -0
  154. hud/eval/tests/test_parallel.py +168 -0
  155. hud/eval/tests/test_task.py +291 -0
  156. hud/eval/types.py +65 -0
  157. hud/eval/utils.py +194 -0
  158. hud/patches/__init__.py +19 -0
  159. hud/patches/mcp_patches.py +308 -0
  160. hud/patches/warnings.py +54 -0
  161. hud/samples/browser.py +4 -4
  162. hud/server/__init__.py +2 -1
  163. hud/server/low_level.py +2 -1
  164. hud/server/router.py +164 -0
  165. hud/server/server.py +567 -80
  166. hud/server/tests/test_mcp_server_integration.py +11 -11
  167. hud/server/tests/test_mcp_server_more.py +1 -1
  168. hud/server/tests/test_server_extra.py +2 -0
  169. hud/settings.py +45 -3
  170. hud/shared/exceptions.py +36 -10
  171. hud/shared/hints.py +26 -1
  172. hud/shared/requests.py +15 -3
  173. hud/shared/tests/test_exceptions.py +40 -31
  174. hud/shared/tests/test_hints.py +167 -0
  175. hud/telemetry/__init__.py +20 -19
  176. hud/telemetry/exporter.py +201 -0
  177. hud/telemetry/instrument.py +165 -253
  178. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  179. hud/telemetry/tests/test_exporter.py +258 -0
  180. hud/telemetry/tests/test_instrument.py +401 -0
  181. hud/tools/__init__.py +18 -2
  182. hud/tools/agent.py +223 -0
  183. hud/tools/apply_patch.py +639 -0
  184. hud/tools/base.py +54 -4
  185. hud/tools/bash.py +2 -2
  186. hud/tools/computer/__init__.py +36 -3
  187. hud/tools/computer/anthropic.py +2 -2
  188. hud/tools/computer/gemini.py +385 -0
  189. hud/tools/computer/hud.py +23 -6
  190. hud/tools/computer/openai.py +20 -21
  191. hud/tools/computer/qwen.py +434 -0
  192. hud/tools/computer/settings.py +37 -0
  193. hud/tools/edit.py +3 -7
  194. hud/tools/executors/base.py +4 -2
  195. hud/tools/executors/pyautogui.py +1 -1
  196. hud/tools/grounding/grounded_tool.py +13 -18
  197. hud/tools/grounding/grounder.py +10 -31
  198. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  199. hud/tools/jupyter.py +330 -0
  200. hud/tools/playwright.py +18 -3
  201. hud/tools/shell.py +308 -0
  202. hud/tools/tests/test_agent_tool.py +355 -0
  203. hud/tools/tests/test_apply_patch.py +718 -0
  204. hud/tools/tests/test_computer.py +4 -9
  205. hud/tools/tests/test_computer_actions.py +24 -2
  206. hud/tools/tests/test_jupyter_tool.py +181 -0
  207. hud/tools/tests/test_shell.py +596 -0
  208. hud/tools/tests/test_submit.py +85 -0
  209. hud/tools/tests/test_types.py +193 -0
  210. hud/tools/types.py +21 -1
  211. hud/types.py +194 -56
  212. hud/utils/__init__.py +2 -0
  213. hud/utils/env.py +67 -0
  214. hud/utils/hud_console.py +89 -18
  215. hud/utils/mcp.py +15 -58
  216. hud/utils/strict_schema.py +162 -0
  217. hud/utils/tests/test_init.py +1 -2
  218. hud/utils/tests/test_mcp.py +1 -28
  219. hud/utils/tests/test_pretty_errors.py +186 -0
  220. hud/utils/tests/test_tool_shorthand.py +154 -0
  221. hud/utils/tests/test_version.py +1 -1
  222. hud/utils/types.py +20 -0
  223. hud/version.py +1 -1
  224. hud_python-0.5.13.dist-info/METADATA +264 -0
  225. hud_python-0.5.13.dist-info/RECORD +305 -0
  226. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
  227. hud/agents/langchain.py +0 -261
  228. hud/agents/lite_llm.py +0 -72
  229. hud/cli/rl/__init__.py +0 -180
  230. hud/cli/rl/config.py +0 -101
  231. hud/cli/rl/display.py +0 -133
  232. hud/cli/rl/gpu.py +0 -63
  233. hud/cli/rl/gpu_utils.py +0 -321
  234. hud/cli/rl/local_runner.py +0 -595
  235. hud/cli/rl/presets.py +0 -96
  236. hud/cli/rl/remote_runner.py +0 -463
  237. hud/cli/rl/rl_api.py +0 -150
  238. hud/cli/rl/vllm.py +0 -177
  239. hud/cli/rl/wait_utils.py +0 -89
  240. hud/datasets/parallel.py +0 -687
  241. hud/misc/__init__.py +0 -1
  242. hud/misc/claude_plays_pokemon.py +0 -292
  243. hud/otel/__init__.py +0 -35
  244. hud/otel/collector.py +0 -142
  245. hud/otel/config.py +0 -181
  246. hud/otel/context.py +0 -570
  247. hud/otel/exporters.py +0 -369
  248. hud/otel/instrumentation.py +0 -135
  249. hud/otel/processors.py +0 -121
  250. hud/otel/tests/__init__.py +0 -1
  251. hud/otel/tests/test_processors.py +0 -197
  252. hud/rl/README.md +0 -30
  253. hud/rl/__init__.py +0 -1
  254. hud/rl/actor.py +0 -176
  255. hud/rl/buffer.py +0 -405
  256. hud/rl/chat_template.jinja +0 -101
  257. hud/rl/config.py +0 -192
  258. hud/rl/distributed.py +0 -132
  259. hud/rl/learner.py +0 -637
  260. hud/rl/tests/__init__.py +0 -1
  261. hud/rl/tests/test_learner.py +0 -186
  262. hud/rl/train.py +0 -382
  263. hud/rl/types.py +0 -101
  264. hud/rl/utils/start_vllm_server.sh +0 -30
  265. hud/rl/utils.py +0 -524
  266. hud/rl/vllm_adapter.py +0 -143
  267. hud/telemetry/job.py +0 -352
  268. hud/telemetry/replay.py +0 -74
  269. hud/telemetry/tests/test_replay.py +0 -40
  270. hud/telemetry/tests/test_trace.py +0 -63
  271. hud/telemetry/trace.py +0 -158
  272. hud/utils/agent_factories.py +0 -86
  273. hud/utils/async_utils.py +0 -65
  274. hud/utils/group_eval.py +0 -223
  275. hud/utils/progress.py +0 -149
  276. hud/utils/tasks.py +0 -127
  277. hud/utils/tests/test_async_utils.py +0 -173
  278. hud/utils/tests/test_progress.py +0 -261
  279. hud_python-0.4.45.dist-info/METADATA +0 -552
  280. hud_python-0.4.45.dist-info/RECORD +0 -228
  281. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
  282. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,16 @@
1
- """General-purpose instrumentation decorator for HUD telemetry.
1
+ """Instrumentation decorator for HUD telemetry.
2
2
 
3
- This module provides the instrument() decorator that users can use
4
- to instrument any function with OpenTelemetry spans.
3
+ This module provides a lightweight @instrument decorator that records
4
+ function calls and sends them to the HUD telemetry backend.
5
+
6
+ Usage:
7
+ @hud.instrument
8
+ async def my_function(arg1, arg2):
9
+ ...
10
+
11
+ # Within an eval context, calls are recorded and sent to HUD
12
+ async with env.eval("task") as ctx:
13
+ result = await my_function("a", "b")
5
14
  """
6
15
 
7
16
  from __future__ import annotations
@@ -11,14 +20,23 @@ import functools
11
20
  import inspect
12
21
  import json
13
22
  import logging
23
+ import time
24
+ import uuid
25
+ from datetime import UTC, datetime
14
26
  from typing import TYPE_CHECKING, Any, TypeVar, overload
15
27
 
16
28
  import pydantic_core
17
- from opentelemetry import trace
18
- from opentelemetry.trace import SpanKind, Status, StatusCode
19
29
 
20
- from hud.otel import configure_telemetry, is_telemetry_configured
21
- from hud.otel.context import get_current_task_run_id
30
+ from hud.telemetry.exporter import queue_span
31
+ from hud.types import TraceStep
32
+
33
+
34
+ def _get_trace_id() -> str | None:
35
+ """Lazy import to avoid circular dependency with eval.context."""
36
+ from hud.eval.context import get_current_trace_id
37
+
38
+ return get_current_trace_id()
39
+
22
40
 
23
41
  if TYPE_CHECKING:
24
42
  from collections.abc import Awaitable, Callable
@@ -31,53 +49,43 @@ logger = logging.getLogger(__name__)
31
49
 
32
50
 
33
51
  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
52
+ """Serialize a value for recording."""
46
53
  if isinstance(value, str | int | float | bool | type(None)):
47
54
  return value
48
55
 
49
- # For collections, we need to limit size first
50
56
  if isinstance(value, list | tuple):
51
57
  value = value[:max_items] if len(value) > max_items else value
52
58
  elif isinstance(value, dict) and len(value) > max_items:
53
59
  value = dict(list(value.items())[:max_items])
54
60
 
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
61
  try:
62
- # Convert to JSON bytes then back to Python objects
63
- # This ensures we get JSON-serializable types
64
62
  json_bytes = pydantic_core.to_json(value, fallback=str)
65
63
  return json.loads(json_bytes)
66
64
  except Exception:
67
- # Fallback if pydantic_core fails somehow
68
65
  return f"<{type(value).__name__}>"
69
66
 
70
67
 
68
+ def _now_iso() -> str:
69
+ """Get current time as ISO-8601 string."""
70
+ return datetime.now(UTC).isoformat().replace("+00:00", "Z")
71
+
72
+
73
+ def _normalize_trace_id(trace_id: str) -> str:
74
+ """Normalize trace_id to 32-character hex string."""
75
+ clean = trace_id.replace("-", "")
76
+ return clean[:32].ljust(32, "0")
77
+
78
+
71
79
  @overload
72
80
  def instrument(
73
81
  func: None = None,
74
82
  *,
75
83
  name: str | None = None,
76
- span_type: str = "function",
77
- attributes: dict[str, Any] | None = None,
84
+ category: str = "function",
85
+ span_type: str | None = None,
86
+ internal_type: str | None = None,
78
87
  record_args: bool = True,
79
88
  record_result: bool = True,
80
- span_kind: SpanKind = SpanKind.INTERNAL,
81
89
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
82
90
 
83
91
 
@@ -86,11 +94,11 @@ def instrument(
86
94
  func: Callable[P, R],
87
95
  *,
88
96
  name: str | None = None,
89
- span_type: str = "function",
90
- attributes: dict[str, Any] | None = None,
97
+ category: str = "function",
98
+ span_type: str | None = None,
99
+ internal_type: str | None = None,
91
100
  record_args: bool = True,
92
101
  record_result: bool = True,
93
- span_kind: SpanKind = SpanKind.INTERNAL,
94
102
  ) -> Callable[P, R]: ...
95
103
 
96
104
 
@@ -99,11 +107,11 @@ def instrument(
99
107
  func: Callable[P, Awaitable[R]],
100
108
  *,
101
109
  name: str | None = None,
102
- span_type: str = "function",
103
- attributes: dict[str, Any] | None = None,
110
+ category: str = "function",
111
+ span_type: str | None = None,
112
+ internal_type: str | None = None,
104
113
  record_args: bool = True,
105
114
  record_result: bool = True,
106
- span_kind: SpanKind = SpanKind.INTERNAL,
107
115
  ) -> Callable[P, Awaitable[R]]: ...
108
116
 
109
117
 
@@ -111,269 +119,173 @@ def instrument(
111
119
  func: Callable[..., Any] | None = None,
112
120
  *,
113
121
  name: str | None = None,
114
- span_type: str = "function",
115
- attributes: dict[str, Any] | None = None,
122
+ category: str = "function",
123
+ span_type: str | None = None,
124
+ internal_type: str | None = None,
116
125
  record_args: bool = True,
117
126
  record_result: bool = True,
118
- span_kind: SpanKind = SpanKind.INTERNAL,
119
127
  ) -> Callable[..., Any]:
120
- """Instrument a function to emit OpenTelemetry spans.
128
+ """Instrument a function to record spans within eval context.
121
129
 
122
- This decorator wraps any function to automatically create spans for
123
- observability. It works with both sync and async functions.
130
+ This decorator records function calls as spans and sends them to the HUD API.
124
131
 
125
132
  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
+ func: The function to instrument
134
+ name: Custom span name (defaults to module.function)
135
+ category: Span category (e.g., "agent", "tool", "function", "mcp")
136
+ span_type: Alias for category (deprecated, use category instead)
137
+ internal_type: Internal span type (e.g., "user-message")
138
+ record_args: Whether to record function arguments
139
+ record_result: Whether to record function result
133
140
 
134
141
  Returns:
135
- The instrumented function that emits spans
142
+ The instrumented function
136
143
 
137
144
  Examples:
138
- # Basic usage - defaults to category="function"
139
145
  @hud.instrument
140
146
  async def process_data(items: list[str]) -> dict:
141
147
  return {"count": len(items)}
142
148
 
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)
149
+ @hud.instrument(category="agent")
150
+ async def call_model(messages: list) -> str:
151
+ return await model.generate(messages)
171
152
  """
172
- # Don't configure telemetry at decoration time - wait until first call
173
- # This allows users to configure alternative backends before importing agents
153
+ effective_category = span_type if span_type is not None else category
174
154
 
175
155
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
176
- # Check if already instrumented
177
156
  if hasattr(func, "_hud_instrumented"):
178
- logger.debug("Function %s already instrumented, skipping", func.__name__)
179
157
  return func
180
158
 
181
- # Get function metadata
182
159
  func_module = getattr(func, "__module__", "unknown")
183
160
  func_name = getattr(func, "__name__", "unknown")
184
161
  func_qualname = getattr(func, "__qualname__", func_name)
185
-
186
- # Determine span name
187
162
  span_name = name or f"{func_module}.{func_qualname}"
188
163
 
189
- # Get function signature for argument parsing
190
164
  try:
191
165
  sig = inspect.signature(func)
192
166
  except (ValueError, TypeError):
193
167
  sig = None
194
168
 
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
169
+ def _build_span(
170
+ task_run_id: str,
171
+ args: tuple[Any, ...],
172
+ kwargs: dict[str, Any],
173
+ start_time: str,
174
+ end_time: str,
175
+ result: Any = None,
176
+ error: str | None = None,
177
+ ) -> dict[str, Any]:
178
+ """Build a HudSpan-compatible span record."""
179
+ # Build attributes using TraceStep
180
+ attributes = TraceStep(
181
+ task_run_id=task_run_id,
182
+ category=effective_category,
183
+ type="CLIENT",
184
+ start_timestamp=start_time,
185
+ end_timestamp=end_time,
186
+ )
187
+
188
+ # Record arguments as request
222
189
  if record_args and sig:
223
190
  try:
224
191
  bound_args = sig.bind(*args, **kwargs)
225
192
  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
-
193
+ args_dict = {
194
+ k: _serialize_value(v)
195
+ for k, v in bound_args.arguments.items()
196
+ if k not in ("self", "cls")
197
+ }
239
198
  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
199
+ attributes.request = args_dict
244
200
  except Exception as e:
245
- logger.debug("Failed to record function arguments: %s", e)
201
+ logger.debug("Failed to serialize args: %s", e)
246
202
 
247
- with tracer.start_as_current_span(
248
- span_name,
249
- kind=span_kind,
250
- attributes=span_attrs,
251
- ) as span:
203
+ # Record result
204
+ if record_result and result is not None and error is None:
252
205
  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
-
206
+ attributes.result = _serialize_value(result)
276
207
  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,
208
+ logger.debug("Failed to serialize result: %s", e)
209
+
210
+ # Build span
211
+ span_id = uuid.uuid4().hex[:16]
212
+ span: dict[str, Any] = {
213
+ "name": span_name,
214
+ "trace_id": _normalize_trace_id(task_run_id),
215
+ "span_id": span_id,
216
+ "parent_span_id": None,
217
+ "start_time": start_time,
218
+ "end_time": end_time,
219
+ "status_code": "ERROR" if error else "OK",
220
+ "status_message": error,
221
+ "attributes": attributes.model_dump(mode="json", exclude_none=True),
222
+ "exceptions": [{"message": error}] if error else None,
297
223
  }
224
+ if internal_type:
225
+ span["internal_type"] = internal_type
226
+ return span
298
227
 
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
228
+ @functools.wraps(func)
229
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
230
+ task_run_id = _get_trace_id()
231
+ start_time = _now_iso()
232
+ start_perf = time.perf_counter()
233
+ error: str | None = None
234
+ result: Any = None
235
+
236
+ try:
237
+ result = await func(*args, **kwargs)
238
+ return result
239
+ except Exception as e:
240
+ error = f"{type(e).__name__}: {e}"
241
+ raise
242
+ finally:
243
+ end_time = _now_iso()
244
+ duration_ms = (time.perf_counter() - start_perf) * 1000
245
+
246
+ if task_run_id:
247
+ span = _build_span(
248
+ task_run_id, args, kwargs, start_time, end_time, result, error
249
+ )
250
+ queue_span(span)
251
+ logger.debug("Span: %s (%.2fms)", span_name, duration_ms)
358
252
 
359
- except Exception as e:
360
- span.record_exception(e)
361
- span.set_status(Status(StatusCode.ERROR, str(e)))
362
- raise
253
+ @functools.wraps(func)
254
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
255
+ task_run_id = _get_trace_id()
256
+ start_time = _now_iso()
257
+ start_perf = time.perf_counter()
258
+ error: str | None = None
259
+ result: Any = None
260
+
261
+ try:
262
+ result = func(*args, **kwargs)
263
+ return result
264
+ except Exception as e:
265
+ error = f"{type(e).__name__}: {e}"
266
+ raise
267
+ finally:
268
+ end_time = _now_iso()
269
+ duration_ms = (time.perf_counter() - start_perf) * 1000
270
+
271
+ if task_run_id:
272
+ span = _build_span(
273
+ task_run_id, args, kwargs, start_time, end_time, result, error
274
+ )
275
+ queue_span(span)
276
+ logger.debug("Span: %s (%.2fms)", span_name, duration_ms)
363
277
 
364
- # Choose wrapper based on function type
365
278
  wrapper = async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
366
-
367
- # Mark as instrumented
368
279
  wrapper._hud_instrumented = True # type: ignore[attr-defined]
369
280
  wrapper._hud_original = func # type: ignore[attr-defined]
370
281
 
371
282
  return wrapper
372
283
 
373
- # Handle usage with or without parentheses
374
284
  if func is None:
375
- # Called with arguments: @instrument(name="foo")
376
285
  return decorator
377
- else:
378
- # Called without arguments: @instrument
379
- return decorator(func)
286
+ return decorator(func)
287
+
288
+
289
+ __all__ = [
290
+ "instrument",
291
+ ]