diff --git a/hud/agents/misc/response_agent.py b/hud/agents/misc/response_agent.py index 617db6a0..4142a624 100644 --- a/hud/agents/misc/response_agent.py +++ b/hud/agents/misc/response_agent.py @@ -6,6 +6,7 @@ from openai import AsyncOpenAI from hud.settings import settings +from hud.telemetry import instrument logger = logging.getLogger(__name__) @@ -64,6 +65,11 @@ def __init__( self.model = model self.system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT + @instrument( + category="agent", + name="response_agent", + internal_type="user-message", + ) async def determine_response(self, agent_message: str) -> ResponseType: """ Determine whether the agent should stop or continue based on its message. @@ -86,6 +92,7 @@ async def determine_response(self, agent_message: str) -> ResponseType: ], temperature=0.1, max_tokens=5, + extra_headers={"Trace-Id": ""}, ) response_text = response.choices[0].message.content diff --git a/hud/eval/instrument.py b/hud/eval/instrument.py index c2767f78..5d97cf87 100644 --- a/hud/eval/instrument.py +++ b/hud/eval/instrument.py @@ -69,7 +69,8 @@ def _httpx_request_hook(request: Any) -> None: headers = _get_trace_headers() if headers is not None: for key, value in headers.items(): - request.headers[key] = value + if key.lower() not in {k.lower() for k in request.headers}: + request.headers[key] = value logger.debug("Added trace headers to request: %s", url_str) # Auto-inject API key if not present or invalid (prefer contextvar, fallback to settings) @@ -149,7 +150,8 @@ async def on_request_start( trace_headers = _get_trace_headers() if trace_headers is not None: for key, value in trace_headers.items(): - params.headers[key] = value + if key.lower() not in {k.lower() for k in params.headers}: + params.headers[key] = value logger.debug("Added trace headers to aiohttp request: %s", url_str) api_key = _get_api_key() diff --git a/hud/telemetry/instrument.py b/hud/telemetry/instrument.py index 204f11bd..e5a8aba4 100644 --- a/hud/telemetry/instrument.py +++ b/hud/telemetry/instrument.py @@ -83,6 +83,7 @@ def instrument( name: str | None = None, category: str = "function", span_type: str | None = None, + internal_type: str | None = None, record_args: bool = True, record_result: bool = True, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ... @@ -95,6 +96,7 @@ def instrument( name: str | None = None, category: str = "function", span_type: str | None = None, + internal_type: str | None = None, record_args: bool = True, record_result: bool = True, ) -> Callable[P, R]: ... @@ -107,6 +109,7 @@ def instrument( name: str | None = None, category: str = "function", span_type: str | None = None, + internal_type: str | None = None, record_args: bool = True, record_result: bool = True, ) -> Callable[P, Awaitable[R]]: ... @@ -118,6 +121,7 @@ def instrument( name: str | None = None, category: str = "function", span_type: str | None = None, + internal_type: str | None = None, record_args: bool = True, record_result: bool = True, ) -> Callable[..., Any]: @@ -130,6 +134,7 @@ def instrument( name: Custom span name (defaults to module.function) category: Span category (e.g., "agent", "tool", "function", "mcp") span_type: Alias for category (deprecated, use category instead) + internal_type: Internal span type (e.g., "user-message") record_args: Whether to record function arguments record_result: Whether to record function result @@ -204,7 +209,7 @@ def _build_span( # Build span span_id = uuid.uuid4().hex[:16] - span = { + span: dict[str, Any] = { "name": span_name, "trace_id": _normalize_trace_id(task_run_id), "span_id": span_id, @@ -216,6 +221,8 @@ def _build_span( "attributes": attributes.model_dump(mode="json", exclude_none=True), "exceptions": [{"message": error}] if error else None, } + if internal_type: + span["internal_type"] = internal_type return span @functools.wraps(func)