clean-horse-59133
12/15/2025, 5:25 PMtall-airline-16756
12/15/2025, 5:25 PMtall-airline-16756
12/15/2025, 5:25 PMspeech_id emitted by each component (EOU, STT, LLM, TTS). It links all events for a single user turn.
• Collect these metrics on the metrics_collected event and index them by speech_id (in memory, DB, or metrics store).
Minimal implementation outline
1. Subscribe to metrics events:
@session.on("metrics_collected")
def on_metrics(ev: MetricsCollectedEvent):
for m in ev.metrics:
if getattr(m, "speech_id", None):
store_metric(m.speech_id, m)
1. For each speech_id aggregate key metrics:
◦ EOU: end_of_utterance_delay, transcription_delay
◦ STT: audio_duration, duration
◦ LLM: ttft (time to first token), duration
◦ TTS: ttfb (time to first byte), audio_duration
1. Compute an end‑to‑end turn latency example:
◦ total_latency = EOU.end_of_utterance_delay + LLM.ttft + TTS.ttfb
Useful helpers
• Use a dictionary or small DB keyed by speech_id to accumulate metrics until the turn completes.
• UsageCollector (or similar) can accumulate summaries across turns and sessions.
Observability & debugging
• Agent Insights in the dashboard provides Transcript, Trace, and Logs views to inspect per-turn timelines and identify bottlenecks.
Quick question to tailor this: Do you want a production-ready data model (DB schema + retention/aggregation rules) or a lightweight in-memory collector for real-time dashboards?
Sources: Observability data hooks | LiveKit docs | https://blog.livekit.io/streamline-troubleshooting-with-agent-observability/#/portal/signup|clean-horse-59133
12/15/2025, 5:27 PMtall-airline-16756
12/15/2025, 5:27 PMmetrics_collected metric will include a speech_id. Here's how to handle that and still map metrics to turns.
When speech_id appears
• speech_id is added to metrics emitted by turn-related components: End‑of‑Utterance (EOU), STT (when used), LLM, and TTS. Those are the primary metrics you should rely on for per‑turn correlation.
Why some metrics lack speech_id
• Some metrics are session- or system-level (transport, CPU, memory, network, etc.) and aren't tied to a specific turn, so they won’t include speech_id.
• A metric may be emitted before a speech_id is assigned (early events) or after turn finalization, depending on timing.
How to correlate metrics when speech_id is missing
• Timestamps: use metric timestamps to join session-level metrics to the nearest turn window (e.g., between EOU start and response complete).
• Request/trace identifiers: if a request_id or trace/span is present, use it to link metrics across services.
• Custom turn IDs: emit or attach your own turn identifier at the earliest point you can (for example, generate a turn_id when speech detection starts) and include it with any events you control.
• Heuristics: associate a session metric to the active turn if its timestamp falls within that turn’s start/end interval.
Practical collection strategy
1. Index all metrics that include speech_id by speech_id.
2. For metrics without speech_id, attempt these in order:
◦ Match on `request_id`/trace id.
◦ Assign to the nearest speech_id by timestamp (define a max gap).
◦ Store as session-level metrics and surface them separately in dashboards.
1. Persist a turn record keyed by speech_id (or your turn_id) containing timestamps for start, EOU, LLM start/finish, and TTS start/finish. Use this timeline to map probables for any unmapped metrics.
If you want, I can:
• Suggest a concrete schema for storing per-turn timelines and unmapped metrics.
• Provide code for timestamp-based matching with gap thresholds. Which would be more helpful?clean-horse-59133
12/15/2025, 5:29 PMtall-airline-16756
12/15/2025, 5:29 PMspeech_id. Use that speech_id to link the EOU event to the corresponding turn and correlate it with STT, LLM, and TTS metrics that share the same speech_id.
If you’d like, I can show a short example of extracting speech_id from EOU events and storing a turn timeline.clean-horse-59133
12/15/2025, 5:30 PMtall-airline-16756
12/15/2025, 5:31 PMSTTMetrics) do not include speech_id. Here are practical ways to correlate STT to a turn and a short example.
How to map STT -> turn
• request_id: STT includes request_id. If other components (or your code) propagate the same request_id through the pipeline, use it to link STT to the turn.
• Timestamps: match STT `timestamp`/`audio_duration` to the turn timeline (e.g., between turn start and EOU) with a configurable max gap.
• Custom turn_id: generate a turn_id when speech detection starts and attach it to any outgoing requests you control (STT requests, LLM calls, TTS), then index metrics by that ID.
• Heuristics fallback: if no IDs match, assign STT to the nearest speech_id by timestamp, and mark it as inferred.
Minimal example (timestamp + request_id fallback)
# stored by speech_id: {speech_id: {start_ts, eou_ts, ...}}
# incoming stt: STTMetrics with request_id, timestamp, audio_duration
def map_stt_to_turn(stt):
# 1) try request_id -> speech_id mapping
speech_id = request_id_to_speech_id.get(stt.request_id)
if speech_id:
return speech_id
# 2) timestamp match: find active turn window containing stt.timestamp
for sid, t in turns.items():
if t['start_ts'] <= stt.timestamp <= t['eou_ts'] + MAX_POST_EOU_GAP:
return sid
# 3) nearest-turn fallback
nearest = min(turns.items(), key=lambda it: abs(it[1]['eou_ts'] - stt.timestamp))
if abs(nearest[1]['eou_ts'] - stt.timestamp) < MAX_NEAREST_GAP:
return nearest[0]
return None # treat as session-level STT metric
Recommendation
• If you can, propagate request_id or add a turn_id to STT requests — this is the most reliable. Otherwise use timestamp windowing with sane gap thresholds and mark inferred mappings.
Would you like a suggested DB schema for storing turns and unmapped STT metrics or a ready-to-use matching implementation?clean-horse-59133
12/15/2025, 5:32 PMtall-airline-16756
12/15/2025, 5:32 PMspeech_id to items in chat_ctx. Below are direct options and a concise example to implement the mapping.
Options to map speech_id -> chat_ctx
• Capture speech_id at transcription time:
◦ Listen to user_input_transcribed (or equivalent) which includes speech_id and the transcript; attach that speech_id to the chat message metadata when adding it to chat_ctx.
• Use `conversation_item_added`:
◦ When a conversation item is committed, add speech_id into the item’s metadata (if the event provides it) or update the item with the speech_id you captured earlier.
• Maintain an external mapping:
◦ Keep a dictionary keyed by speech_id storing the chat item id, transcript, timestamps, and any metadata.
• Timestamp correlation fallback:
◦ If speech_id isn’t present on the chat item, match by timestamps (transcript timestamp ↔️ turn start/EOU) with a configurable max gap and mark the mapping as inferred.
Minimal implementation example
1. Capture transcription -> store mapping:
speech_to_ctx = {}
@session.on("user_input_transcribed")
def on_user_input_transcribed(ev: UserInputTranscribedEvent):
# ev.speech_id, ev.transcript, ev.timestamp
# create chat item and store mapping
chat_item = add_to_chat_ctx(text=ev.transcript, metadata={"speech_id": ev.speech_id})
speech_to_ctx[ev.speech_id] = {"chat_item_id": chat_item.id, "timestamp": ev.timestamp}
1. Correlate metrics -> look up chat item:
@session.on("metrics_collected")
def on_metrics_collected(ev: MetricsCollectedEvent):
for m in ev.metrics:
sid = getattr(m, "speech_id", None)
if sid and sid in speech_to_ctx:
chat_item_id = speech_to_ctx[sid]["chat_item_id"]
store_latency_for_chat_item(chat_item_id, m)
else:
# fallback: try request_id mapping or timestamp matching
handle_unmapped_metric(m)
Practical tips
• Prefer embedding speech_id directly into chat item metadata when creating the item—this is the most reliable mapping.
• Propagate request_id across STT/LLM/TTS calls if you need an alternate stable key.
• Persist the speech_id ↔️ chat_item_id mapping in your DB so dashboards and post‑session analytics can join metrics to chat history.
• When using timestamp heuristics, store explicit start/EOU timestamps for each turn to reduce false matches.
If you want, I can provide:
• A suggested DB schema for persisting mappings, or
• A timestamp-based matching function with configurable gap thresholds. Which do you prefer? Sources: Observability data hooks | LiveKit docs | Events and error handling | LiveKit docs