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.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +70 -5
- hud/agents/base.py +238 -500
- hud/agents/claude.py +236 -247
- hud/agents/gateway.py +42 -0
- hud/agents/gemini.py +264 -0
- hud/agents/gemini_cua.py +324 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +48 -36
- hud/agents/openai.py +282 -296
- hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
- hud/agents/operator.py +199 -0
- hud/agents/resolver.py +70 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +381 -214
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +377 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_resolver.py +192 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/agents/types.py +148 -0
- hud/cli/__init__.py +493 -546
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +699 -113
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +889 -732
- hud/cli/eval.py +793 -667
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/pull.py +1 -1
- hud/cli/push.py +38 -13
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +110 -8
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push.py +1 -1
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +70 -1
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_git.py +142 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +45 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +326 -0
- hud/datasets/runner.py +198 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +52 -0
- hud/environment/connection.py +258 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +137 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +835 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +263 -0
- hud/environment/scenarios.py +620 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +205 -0
- hud/environment/tests/test_environment.py +593 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +242 -0
- hud/environment/tests/test_scenarios.py +1086 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +727 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +187 -0
- hud/eval/manager.py +533 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +372 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +291 -0
- hud/eval/types.py +65 -0
- hud/eval/utils.py +194 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +308 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +165 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +18 -2
- hud/tools/agent.py +223 -0
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +36 -3
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_agent_tool.py +355 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +194 -56
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +89 -18
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.13.dist-info/METADATA +264 -0
- hud_python-0.5.13.dist-info/RECORD +305 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
hud/telemetry/instrument.py
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Instrumentation decorator for HUD telemetry.
|
|
2
2
|
|
|
3
|
-
This module provides
|
|
4
|
-
|
|
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.
|
|
21
|
-
from hud.
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
128
|
+
"""Instrument a function to record spans within eval context.
|
|
121
129
|
|
|
122
|
-
This decorator
|
|
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
|
|
127
|
-
name: Custom span name (defaults to
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
|
201
|
+
logger.debug("Failed to serialize args: %s", e)
|
|
246
202
|
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
286
|
+
return decorator(func)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
__all__ = [
|
|
290
|
+
"instrument",
|
|
291
|
+
]
|