Add function call latency tracking to LatencyBreakdown

This commit is contained in:
Mark Backman
2026-03-01 11:14:17 -05:00
parent ddba1b84a9
commit a738a4d82b
4 changed files with 179 additions and 6 deletions

View File

@@ -15,11 +15,13 @@ is measured. Optionally collects per-service latency breakdown metrics
import time
from collections import deque
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Dict, List, Optional
from pipecat.frames.frames import (
BotStartedSpeakingFrame,
ClientConnectedFrame,
FunctionCallInProgressFrame,
FunctionCallResultFrame,
InterruptionFrame,
MetricsFrame,
UserStoppedSpeakingFrame,
@@ -34,6 +36,19 @@ from pipecat.observers.base_observer import BaseObserver, FramePushed
from pipecat.processors.frame_processor import FrameDirection
@dataclass
class FunctionCallMetrics:
"""Latency for a single function call execution.
Parameters:
function_name: Name of the function that was called.
duration_secs: Time in seconds from execution start to result.
"""
function_name: str
duration_secs: float
@dataclass
class LatencyBreakdown:
"""Per-service latency breakdown for a single user-to-bot cycle.
@@ -52,11 +67,14 @@ class LatencyBreakdown:
VAD silence detection, STT finalization, and any turn analyzer
wait. ``None`` if no ``UserStoppedSpeakingFrame`` was observed
(e.g. no turn analyzer configured).
function_calls: Latency for each function call executed during
this cycle. Empty if no function calls occurred.
"""
ttfb: List[TTFBMetricsData] = field(default_factory=list)
text_aggregation: Optional[TextAggregationMetricsData] = None
user_turn_secs: Optional[float] = None
function_calls: List[FunctionCallMetrics] = field(default_factory=list)
class UserBotLatencyObserver(BaseObserver):
@@ -113,6 +131,8 @@ class UserBotLatencyObserver(BaseObserver):
# Per-cycle metric accumulators
self._ttfb: List[TTFBMetricsData] = []
self._text_aggregation: Optional[TextAggregationMetricsData] = None
self._function_call_starts: Dict[str, tuple[str, float]] = {}
self._function_call_metrics: List[FunctionCallMetrics] = []
self._register_event_handler("on_latency_measured")
self._register_event_handler("on_latency_breakdown")
@@ -171,6 +191,21 @@ class UserBotLatencyObserver(BaseObserver):
elif isinstance(data.frame, InterruptionFrame):
# Discard stale metrics from cancelled LLM/TTS cycles
self._reset_accumulators()
elif isinstance(data.frame, FunctionCallInProgressFrame):
self._function_call_starts[data.frame.tool_call_id] = (
data.frame.function_name,
time.time(),
)
elif isinstance(data.frame, FunctionCallResultFrame):
start = self._function_call_starts.pop(data.frame.tool_call_id, None)
if start is not None:
function_name, start_time = start
self._function_call_metrics.append(
FunctionCallMetrics(
function_name=function_name,
duration_secs=time.time() - start_time,
)
)
elif isinstance(data.frame, MetricsFrame):
self._handle_metrics_frame(data.frame)
elif isinstance(data.frame, BotStartedSpeakingFrame):
@@ -198,6 +233,7 @@ class UserBotLatencyObserver(BaseObserver):
ttfb=list(self._ttfb),
text_aggregation=self._text_aggregation,
user_turn_secs=self._user_turn,
function_calls=list(self._function_call_metrics),
)
await self._call_event_handler("on_latency_breakdown", breakdown)
self._reset_accumulators()
@@ -229,3 +265,5 @@ class UserBotLatencyObserver(BaseObserver):
self._ttfb = []
self._text_aggregation = None
self._user_turn = None
self._function_call_starts = {}
self._function_call_metrics = []