From 1f07455c7cec92337a3e576d3f5b6da8436002d1 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 29 May 2026 12:20:31 -0400 Subject: [PATCH] fix: skip empty LiveKit function tool spans --- .../livekit_agents/test_livekit_agents.py | 7 ++- .../integrations/livekit_agents/tracing.py | 43 +++++++++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/py/src/braintrust/integrations/livekit_agents/test_livekit_agents.py b/py/src/braintrust/integrations/livekit_agents/test_livekit_agents.py index 4576bff2..afa48881 100644 --- a/py/src/braintrust/integrations/livekit_agents/test_livekit_agents.py +++ b/py/src/braintrust/integrations/livekit_agents/test_livekit_agents.py @@ -409,6 +409,7 @@ def _assert_openai_voice_turn_spans(logs, *, speech_text, outer_parent_name): _assert_eou_spans(logs) _assert_stt_spans(logs, speech_text) _assert_session_spans(logs) + _assert_no_function_tool_spans_without_tools(logs) _assert_custom_metrics_stay_in_metadata(logs) if outer_parent_name: @@ -478,13 +479,17 @@ def _assert_session_spans(logs): _assert_no_metrics(session_logs, "prompt_tokens", "completion_tokens", "tokens") +def _assert_no_function_tool_spans_without_tools(logs): + assert not _spans_named(logs, "function_tool") + + def _assert_voice_turn_parenting(logs, outer_parent_name): outer_parent_log = _single_span(logs, outer_parent_name) session_log = _single_span(logs, "livekit_agent_session") assert session_log.get("span_parents") == [outer_parent_log.get("span_id")] assert "end" in session_log.get("metrics", {}) - for child_name in ("function_tool", "llm_request_run", "user_speaking", "vad_endpointing"): + for child_name in ("llm_request_run", "user_speaking", "vad_endpointing"): child_log = _single_span(logs, child_name) assert child_log.get("span_parents") == [session_log.get("span_id")], child_name assert _single_span(logs, "llm_request_run")["span_attributes"]["type"].value == "task" diff --git a/py/src/braintrust/integrations/livekit_agents/tracing.py b/py/src/braintrust/integrations/livekit_agents/tracing.py index caab9a4f..7adfab31 100644 --- a/py/src/braintrust/integrations/livekit_agents/tracing.py +++ b/py/src/braintrust/integrations/livekit_agents/tracing.py @@ -218,18 +218,43 @@ async def traced_llm_stream_run(wrapped: Any, instance: Any, args: tuple[Any, .. async def traced_execute_tools_task(wrapped: Any, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: session = kwargs.get("session") parent = getattr(session, _SESSION_PARENT_ATTR, None) - result = await _traced_async_span( - "function_tool", - SpanTypeAttribute.TOOL, - wrapped, - args, - kwargs, - parent=parent if isinstance(parent, str) else None, - event_builder=lambda: _tool_execution_event(kwargs.get("tool_output")), - ) + parent_arg = parent if isinstance(parent, str) else None + start_time = time.time() + try: + with _without_current_span(parent_arg): + result = await wrapped(*args, **kwargs) + except Exception as exc: + end_time = time.time() + event = _tool_execution_event(kwargs.get("tool_output")) + if event: + _log_function_tool_span(event, parent=parent_arg, start_time=start_time, end_time=end_time, error=exc) + raise + end_time = time.time() + event = _tool_execution_event(kwargs.get("tool_output")) + if event: + _log_function_tool_span(event, parent=parent_arg, start_time=start_time, end_time=end_time) return result +def _log_function_tool_span( + event: dict[str, Any], + *, + parent: str | None, + start_time: float, + end_time: float, + error: Exception | None = None, +) -> None: + span = start_span( + name="function_tool", + type=SpanTypeAttribute.TOOL, + parent=parent, + start_time=start_time, + set_current=False, + ) + span.log(error=error, **event) + span.end(end_time=end_time) + + async def _traced_async_span( name: str, span_type: str,