brave-island-45242
12/15/2025, 5:57 PMtall-airline-16756
12/15/2025, 5:58 PMtall-airline-16756
12/15/2025, 5:58 PMuser_input_transcribed events.
• For full/finished transcripts, use session history endpoints or ctx.make_session_report() to retrieve the transcript with speaker identities.
If you’d like, tell me:
• Which STT plugin you’re using (or if you want recommendations),
• Whether you need real-time transcripts or post-call/exported transcripts,
and I’ll give exact steps or example payloads for your setup.
Sources: Text and transcriptions | LiveKit docs | Speechmatics STT plugin guide | LiveKit Docs | Agent-assisted warm transfer | LiveKit docs | Observability data hooks | LiveKit docsbrave-island-45242
12/15/2025, 6:00 PMtall-airline-16756
12/15/2025, 6:00 PMuser_input_transcribed events (they include participant identity).
• Post-call: retrieve session history or call ctx.make_session_report() for the full transcript with speaker attribution.
If you want, tell me which STT plugin and agent/event flow you’re using and I’ll give concrete steps or example payloads to ensure accurate multi-speaker transcripts.brave-island-45242
12/15/2025, 6:33 PMtall-airline-16756
12/15/2025, 6:33 PMdeepgram/nova-2-medical) or AssemblyAI streaming. Configure your AgentSession or STT inference to use one of these models so you remain compliant.
Concrete checklist to ensure accurate, compliant multi-speaker transcripts
• Keep the agent in the room during/after the warm transfer (don’t filter it out).
• Do NOT set a fixed participant filter in RoomIO (so it listens to all participants).
• Ensure each participant publishes their own mic track and has a distinct participant ID.
• Use a HIPAA-compliant STT model (Deepgram/AssemblyAI) and enable diarization if available.
• For realtime events, consume user_input_transcribed (includes participant identity). For full transcripts, use session history or ctx.make_session_report().
If you want, tell me:
• which STT model you prefer (Deepgram or AssemblyAI),
• and whether you want a realtime or post-call transcript—
and I’ll give the exact config/code snippet to set this up in your agent session.
Sources: Agents Framework | livekit/livekit_composite | DeepWiki | How to set a fixed participant for agent audio processing | HIPAA Eligible Products and Services | LiveKit | Deepgram STT | LiveKit docs | Agent-assisted warm transfer | LiveKit docsbrave-island-45242
12/16/2025, 9:36 PMfrom typing import Any
import os
import json
from datetime import datetime, UTC
from livekit import agents, rtc
from livekit.agents import AgentServer,AgentSession, Agent, room_io
from livekit.plugins import noise_cancellation, silero
from livekit.plugins.turn_detector.multilingual import MultilingualModel
from livekit.agents import llm
from livekit.agents import stt
from livekit.agents import tts
from livekit.plugins import cartesia
from livekit.plugins import deepgram
from livekit.plugins import google
from livekit.plugins import silero
from app.config import application_settings
from app.di import wire_application_dependencies
from app.logging_config import setup_logging, get_logger
from app.utils.vertex_ai_setup import get_available_location
from app.utils.vertex_ai_setup import setup_vertex_ai_auth
setup_logging(application_settings)
logger = get_logger(__name__)
def get_pipeline_config() -> dict[str, Any]:
"""Get the pipeline configuration for STT, LLM, and TTS."""
settings = application_settings
project_id = setup_vertex_ai_auth(settings)
google_llm_model = "gemini-2.5-flash"
google_llm_location = get_available_location(google_llm_model)
return {
"stt": stt.FallbackAdapter(
[
deepgram.STT(
model="nova-3",
language="multi",
punctuate=True,
sample_rate=16000,
),
]
),
"llm": llm.FallbackAdapter(
[
google.LLM(
model=google_llm_model,
location=google_llm_location,
project=project_id,
vertexai=True,
),
]
),
"tts": tts.FallbackAdapter(
[
cartesia.TTS(
model="sonic-3",
voice="f786b574-daa5-4673-aa0c-cbe3e8534c02",
),
]
),
}
class Assistant(Agent):
def __init__(self) -> None:
super().__init__(
instructions="""You are a helpful voice AI assistant.
You eagerly assist users with their questions by providing information from your extensive knowledge.
Your responses are concise, to the point, and without any complex formatting or punctuation including emojis, asterisks, or other symbols.
You are curious, friendly, and have a sense of humor.""",
)
server = AgentServer()
@server.rtc_session()
async def my_agent(ctx: agents.JobContext) -> None:
pipeline_config = get_pipeline_config()
session = AgentSession(
stt=pipeline_config["stt"],
llm=pipeline_config["llm"],
tts=pipeline_config["tts"],
vad=silero.VAD.load(),
turn_detection=MultilingualModel(),
)
await session.start(
room=ctx.room,
agent=Assistant(),
room_options=room_io.RoomOptions(
audio_input=room_io.AudioInputOptions(
noise_cancellation=lambda params: noise_cancellation.BVCTelephony() if params.participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP else noise_cancellation.BVC(),
),
),
)
await session.generate_reply(
instructions="Greet the user and offer your assistance."
)
# Wait until shutdown
async def shutdown_callback() -> None:
<http://logger.info|logger.info>(f"Starting shutdown callback for room: {ctx.room.name}")
now_str = datetime.now().strftime("%Y-%m-%d_%H-%M")
outputs_dir = os.path.join(os.path.dirname(__file__), f"monitoring_outputs/{now_str}")
os.makedirs(outputs_dir, exist_ok=True)
# Save session history (agent's speech and user responses)
history_copy = session.history.copy().to_dict()
for item in history_copy["items"]:
if "created_at" in item and isinstance(item["created_at"], int | float):
item["created_at"] = datetime.fromtimestamp(item["created_at"], UTC).isoformat()
output_path = os.path.join(outputs_dir, f"{ctx.room.name}_history.json")
with open(output_path, "w", encoding="utf-8") as f:
json.dump(history_copy, f, ensure_ascii=False, indent=2)
<http://logger.info|logger.info>(f"Saved history to: {output_path}")
ctx.add_shutdown_callback(shutdown_callback)
if __name__ == "__main__":
<http://logger.info|logger.info>(f"Running in environment: {application_settings.ENVIRONMENT}")
wire_application_dependencies()
<http://logger.info|logger.info>("Dependency injection container initialized")
agents.cli.run_app(server)
and the transcript is:
{
"items": [
{
"id": "item_e0e947ceb699",
"type": "agent_handoff",
"new_agent_id": "assistant"
},
{
"id": "item_038da04da878",
"type": "message",
"role": "assistant",
"content": [
"Hello there I'm ready to help How can I assist you today"
],
"interrupted": false,
"extra": {},
"metrics": {
"started_speaking_at": 1765920808.026667,
"stopped_speaking_at": 1765920810.510167,
"llm_node_ttft": 1.1261196129926248,
"tts_node_ttfb": 0.5151044739905046
}
},
{
"id": "item_77880dde83fa",
"type": "message",
"role": "user",
"content": [
"Hi. I'm participant one. Just wait for participant two to join the conversation."
],
"interrupted": false,
"transcript_confidence": 0.98498535,
"extra": {},
"metrics": {
"started_speaking_at": 1765920812.9020004,
"stopped_speaking_at": 1765920818.6017575,
"transcription_delay": 0.5538489818572998,
"end_of_turn_delay": 0.6049089431762695,
"on_user_turn_completed_delay": 0.0000026149937184527516
}
},
{
"id": "item_fb8473986917",
"type": "message",
"role": "assistant",
"content": [
"Alright I'll be here waiting patiently for participant two to join"
],
"interrupted": false,
"extra": {},
"metrics": {
"started_speaking_at": 1765920820.1736605,
"stopped_speaking_at": 1765920823.193607,
"llm_node_ttft": 0.5869958319963189,
"tts_node_ttfb": 0.3776853090093937,
"e2e_latency": 1.5719029903411865
}
},
{
"id": "item_1e010890fc59",
"type": "message",
"role": "user",
"content": [
"Were you able to hear participant too?"
],
"interrupted": false,
"transcript_confidence": 0.9980469,
"extra": {},
"metrics": {
"started_speaking_at": 1765920846.1034784,
"stopped_speaking_at": 1765920848.0024707,
"transcription_delay": 0.8463616371154785,
"end_of_turn_delay": 0.9183995723724365,
"on_user_turn_completed_delay": 0.00000214400643017143
}
},
{
"id": "item_4fddef70cc35",
"type": "message",
"role": "assistant",
"content": [
"No I haven't heard from them yet I'm still waiting for them to type something"
],
"interrupted": false,
"extra": {},
"metrics": {
"started_speaking_at": 1765920850.6065447,
"stopped_speaking_at": 1765920853.5796337,
"llm_node_ttft": 1.025421081998502,
"tts_node_ttfb": 0.6590590020059608,
"e2e_latency": 2.604074001312256
}
}
]
}
As you can see an agent will not listen to all participantstall-airline-16756
12/16/2025, 9:36 PMaudio_input is enabled (you set it in RoomOptions; that’s good) and no code later disables audio for new participants.
• Turn-detection / VAD behavior: your turn_detection and vad can cause the session to treat only the first detected speaker as the active user if configured to single-turn focus. Check whether turn detection is filtering input to a single “current” participant.
• Participant identity mapping: confirm each participant has a distinct participant ID and your STT pipeline preserves participant metadata so transcripts are attributed.
Concrete checks and changes to try
1. Confirm RoomOptions has no participant filter:
◦ You passed RoomOptions but did not set participant anywhere in room_io.RoomOptions — good. Double-check no default elsewhere sets a fixed participant.
1. Force the session to listen to all participants (explicit):
await session.start(
room=ctx.room,
agent=Assistant(),
room_options=room_io.RoomOptions(
audio_input=room_io.AudioInputOptions(...),
io=room_io.IOOptions(listen_to_all_participants=True)
),
)
(If listen_to_all_participants is not available in your SDK version, skip and instead ensure no participant filter is set.)
1. Verify second participant publishes mic and is unmuted:
◦ On the client side confirm getUserMedia is running and the track is published.
◦ Check LiveKit room participant list and published tracks in your dashboard/logs.
1. Inspect turn_detection/VAD behavior:
◦ Temporarily disable turn_detection=MultilingualModel() and/or vad=silero.VAD.load() and rerun to see if both voices are transcribed. If that fixes it, we can tune turn detection settings to allow multiple concurrent speakers.
1. Capture event logs:
◦ Log `user_published_track`/participant-joined events and any RoomIO subscription actions to confirm the agent is subscribed to incoming audio tracks from participant two.
If you want, share:
• The client-side code/steps you used to join participant two (so I can check they actually published an audio track), and
• Any agent logs around participant join/publish events.
Tell me which you want to try first (explicitly forcing listen-to-all, disabling turn-detection, or sharing client logs) and I’ll give the exact code/log commands for that path.brave-island-45242
12/16/2025, 9:39 PMtall-airline-16756
12/16/2025, 9:40 PMparticipant identity, the agent will only process that participant’s audio — that is the explicit way to restrict listening. <cite sourceid="358ae8e8-8d4f-410b-8c2b-872bf35362dc"/>
Why your test likely heard only participant one
• A participant filter (explicit or implicit) was applied somewhere: either RoomIO was configured elsewhere to a fixed participant identity, or agent subscriptions were limited after start.
• The second participant may not have published an active mic track, or their track was not subscribed to by the agent.
• Turn-detection / VAD settings can make the session treat only one active speaker as the “current” input; that can appear as if the agent only hears the first participant.
What to check now (quick verification steps) 1) Confirm no participant filter is set anywhere:
• Search your codebase for RoomIO/RoomOptions usage and look for any participant or filtering option being set.
2) Confirm participant two published an active mic track:
• On the client, ensure getUserMedia is active and the track is published/unmuted.
• Inspect the LiveKit room participant list and published tracks (logs or dashboard).
3) Re-run with turn-detection/VAD disabled:
• Temporarily remove vad=silero.VAD.load() and turn_detection=MultilingualModel() from AgentSession and test again.
4) Explicitly start the session with an all-listen option (if available) or ensure no participant set:
await session.start(
room=ctx.room,
agent=Assistant(),
room_options=room_io.RoomOptions(
audio_input=room_io.AudioInputOptions(...),
# ensure no participant filter here
),
)
5) Log subscription/join events:
• Add logging for participant-joined, track-published, and agent subscription actions to confirm the agent subscribes to participant two’s audio.
If you want, I can:
• Show the exact places in your SDK/docs that state AgentSession listens to all participants by default, or
• Review the client-side join/publish code you used for participant two to confirm the mic track is actually published.
Which would you prefer?