adorable-bear-14321
05/13/2026, 9:56 AMtall-airline-16756
05/13/2026, 9:56 AMtall-airline-16756
05/13/2026, 9:56 AMmetrics_collected is deprecated and there’s no plugin instance for EOU. Good question.
Short answer: there is no plugin instance that emits raw EOU events. EOU decisions are part of the agent’s turn‑detection logic and aren’t exposed as a per‑plugin metrics_collected stream.
What you can do:
• Enable Agent Observability in your project settings (Settings → Data and Privacy). This records turn‑detection / EOU traces automatically.
• Inspect session replays in the dashboard to find the eou_detection trace span with prediction, confidence, context, and timing for debugging.
• Subscribe to session-level interruption metrics: while raw EOU scores aren’t emitted via a plugin, interruption/turn metrics (e.g., InterruptionMetrics) are available on the session event bus and reflect EOU-driven decisions. Example pattern:
def on_metrics_collected(ev):
if isinstance(ev.metrics, InterruptionMetrics):
logger.info(f"Interruption: {ev.metrics}")
session.on("metrics_collected", on_metrics_collected)
Limitations to note:
• Raw EOU model outputs (per‑utterance confidence/prediction) aren’t exposed through a plugin API; use Observability traces for those.
• EOU currently requires text/STT input (English only) and doesn’t work as a standalone plugin instance.
If you want, tell me:
• Which SDK and version you’re using, and
• Whether you need real‑time programmatic alerts (low latency) or post‑hoc debugging via traces,
and I’ll suggest the exact event pattern or dashboard steps tailored to your setup.
Sources: Improved end-of-turn model cuts Voice AI interruptions 39% | LiveKit | Streamline troubleshooting with Agent Observability | LiveKit | Adaptive interruption handling | LiveKit Documentation | Improved end-of-turn model cuts Voice AI interruptions 39% | LiveKit, with interactive elementsadorable-bear-14321
05/13/2026, 10:29 AMtall-airline-16756
05/13/2026, 10:30 AMmetrics_collected without repeating code. I can give a concise reusable pattern, but first: which language/SDK and version are you using (Python asyncio, Node, or other)? Also which plugin objects do you need to subscribe (LLM, TTS, STT, others)?
, with interactive elementsadorable-bear-14321
05/13/2026, 10:30 AMtall-airline-16756
05/13/2026, 10:30 AMllm, tts, stt, etc.) to metrics_collected without repeating code. It also shows how to filter by metric type and delegate to async handlers.
import asyncio
from typing import Callable, Iterable
# Generic wrapper factory: creates a sync callback that schedules the async handler
def make_metrics_callback(async_handler: Callable):
def callback(metrics):
asyncio.create_task(async_handler(metrics))
return callback
# Example async handlers for different metric types
async def handle_llm(metrics):
# process LLM metrics
logger.info(f"LLM metrics: {metrics}")
async def handle_tts(metrics):
# process TTS metrics
logger.info(f"TTS metrics: {metrics}")
async def handle_stt(metrics):
# process STT metrics
logger.info(f"STT metrics: {metrics}")
# Register multiple plugins with minimal repetition
def subscribe_metrics(plugins: Iterable, handler_map: dict):
"""
plugins: iterable of (name, plugin_instance) tuples or plugin instances where you can infer type
handler_map: mapping from plugin name (or type) -> async handler
"""
for key, plugin in plugins:
handler = handler_map.get(key)
if handler:
plugin.on("metrics_collected", make_metrics_callback(handler))
# Usage example (adapt keys to how you identify plugins)
plugins = [
("llm", session.llm),
("tts", session.tts),
("stt", session.stt),
]
handler_map = {
"llm": handle_llm,
"tts": handle_tts,
"stt": handle_stt,
}
subscribe_metrics(plugins, handler_map)
Notes and tips:
• Use the plugin key you prefer (string name, class name, or other identifier). Adjust plugins and handler_map accordingly.
• If multiple plugins share identical handling, point them to the same async handler to avoid duplication.
• For cross‑cutting logic (logging, metrics forwarding), wrap handlers or create a small middleware function to run shared behavior before/after per-plugin handling.
If you want, I can adapt this to:
• automatically infer plugin keys from plugin types,
• include filtering by metric subclass (e.g., only handle InterruptionMetrics), or
• show an example with structured logging/export to your telemetry sink. Which would you prefer?, with interactive elementsadorable-bear-14321
05/13/2026, 6:27 PMConversationItemAddedEvent.item.id to speech_id (from ev.metrics)tall-airline-16756
05/13/2026, 6:28 PMspeech_id but EOU now arrives in conversation_item_added without speech_id. Good question.
Short answer: use the per-turn metrics on the conversation item (especially e2e_latency) or correlate events by timestamps / conversation item id instead of speech_id. Below are two reliable approaches and a concise Python pattern.
Recommended (simple & accurate)
• Read e2e_latency and component fields (llm_node_ttft, tts_node_ttfb, etc.) from ev.item.metrics in conversation_item_added. e2e_latency already represents end‑to‑end latency for that assistant turn so you don’t need to stitch `speech_id`s.
• If you want the breakdown, ev.item.metrics includes component metrics so you can compute cumulative = llm_node_ttft + tts_node_ttfb + eou_delay (or just trust e2e_latency).
If you need to correlate separate plugin metrics_collected events with the conversation item
• Correlate by timestamps: capture metrics_collected timestamps (e.g., metric.created_at or time.time()) and match the closest plugin metrics to the conversation item timestamp window.
• Correlate by conversation item id (if present): use ev.item.id as an anchor if your plugin metrics include or can be mapped to that id.
• Use sequence/order heuristics only if traffic is low/serial (risky under concurrency).
Minimal Python pattern (timestamp correlation + fallback to e2e)
import time
from collections import defaultdict, deque
# small in-memory buffer keyed by speech_id or plugin type
plugin_buffer = defaultdict(deque) # {plugin: deque([(ts, metrics), ...])}
def plugin_callback(plugin_name):
def cb(metrics):
plugin_buffer[plugin_name].append((time.time(), metrics))
return cb
# subscribe: session.llm.on("metrics_collected", plugin_callback("llm")) etc.
@session.on("conversation_item_added")
def on_item(ev):
if ev.item.role != "assistant":
return
metrics = ev.item.metrics or {}
# preferred: use e2e_latency if present
e2e = metrics.get("e2e_latency")
if e2e is not None:
logger.info(f"turn e2e_latency={e2e}")
# optional: read breakdown fields if you want components
return
# fallback: approximate by timestamp matching
item_ts = ev.item.created_at_epoch or time.time()
def nearest(plugin):
candidates = plugin_buffer.get(plugin)
if not candidates:
return None
# pop old entries
while candidates and item_ts - candidates[0][0] > 10.0:
candidates.popleft()
# find closest (simple: rightmost <= item_ts)
for ts, m in reversed(candidates):
if ts <= item_ts:
return m
return candidates[-1][1] if candidates else None
llm_m = nearest("llm")
tts_m = nearest("tts")
# compute approximate cumulative if available
if llm_m and tts_m:
approx = llm_m.get("ttft", 0) + tts_m.get("ttfb", 0) + metrics.get("eou_delay", 0)
logger.info(f"approx cumulative={approx}")
Notes and tradeoffs
• Using e2e_latency is the most robust and simplest approach.
• Timestamp correlation works but can mis-map under concurrent/parallel turns; tune time windows and clean buffers.
• Conversation item id correlation is ideal if you can surface that id in plugin metrics.
If you want, tell me:
• a sample payload for your metrics_collected and conversation_item_added events and the SDK version — I’ll adapt the code to your exact field names and time formats. Sources: How to capture real-time agent transcriptions during conversations | Build Your First AI Voice Agent in Python: Complete Tutorial | LiveKit | Build Your First AI Voice Agent in Python: Complete Tutorial | LiveKit, with interactive elements