there is one issue that i am facing. when i am sta...
# ask-ai
a
there is one issue that i am facing. when i am starting my agent it is not joining the room in the first call and then i am calling again and then there is no audio and then when i am calling again at that time the voice is coming from the other side and later on its workin fine. what could be the possible reasons for this
t
Thanks for reaching out, Aryan. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m sorry you’re hitting that flaky join/audio behavior. I’m responding to your description that the agent fails to join on the first call, has no audio on the second, and works on the third. Most likely causes and quick checks • Cold starts: If your agent can scale to zero (Build/free plan), the first call may be delayed while the agent starts. Check whether the agent process is starting on the first call and how long it takes. • Agent dispatch timing: Ensure the agent is being dispatched (explicit dispatch or correct
agent_name
in room config) and that dispatch completes before connecting the client. • Connection / lifecycle ordering: Verify you call
ctx.connect()
/ start the
RoomIO
(e.g.,
await room_io.start()
) before publishing or expecting audio. Delayed or out-of-order connects can drop the first utterance. • Preconnect buffering: If users speak before the agent is fully connected, early audio can be lost. Enable preconnect audio buffering on the client to capture initial speech. • Reused state or room name: Reusing the same room or session objects across calls can cause leftover tracks or routing conflicts. Try unique room names per call and ensure session/room cleanup. • Missing audio tracks: Confirm audio tracks are published/subscribed correctly on each attempt (no errors when starting audio). To pinpoint the issue I need a few details • Are you on LiveKit Cloud? If so, which plan (Build/free vs paid)? • How are you dispatching the agent — automatic or explicit (show the dispatch snippet or token config if possible)? • Relevant code snippets around the agent entrypoint: where you create/start the session, call
ctx.connect()
, and start
room_io
. • Logs from the first three attempts (agent startup logs, dispatch logs, and any errors). • Whether the client uses preconnect buffering and whether you reuse room names. If you share those, I’ll suggest exact fixes and code changes. Sources: Quotas and limits | LiveKit docs | Agent dispatch | LiveKit docs | Authentication | LiveKit docs | examples/bank-ivr | livekit/livekit_composite | DeepWiki | Job lifecycle | LiveKit docs
a
i am dispatchin the agent using dispath request and i have hosted livekitn on my own server on ec2 instance. this is my entry point # ✅ CRITICAL FIX: Connect to database in EACH worker process global db_manager # Connect if not already connected if not db_manager.connected: await db_manager.connect() if not db_manager.connected: logger.error("❌ Failed to connect to database in worker!") return logger.info(f"🚀 EMI Agent Worker JOB RECEIVED!") logger.info(f" - Room: {ctx.room.name}") logger.info(f" - DB Connected: {db_manager.connected}") if not ctx.room.name.startswith("emi-call-"): logger.info(f"⏭️ Skipping non-EMI room: {ctx.room.name}") return try: logger.info(f"🔌 Connecting to room: {ctx.room.name}") await ctx.connect() logger.info(f"✅ Connected successfully!") # ✅ OPTIMIZATION: Minimal delay before starting await asyncio.sleep(1.0) # ✅ CRITICAL FIX: Initialize with minimal defaults - will be overridden by metadata emi_data = EMIReminderInfo( language=INITIAL_LANGUAGE, borrower_name="Unknown", # Will be updated from metadata loan_id="unknown", emi_amount=0.0, due_date="Unknown", days_remaining=0, penalty_amount=0.0 ) # ✅ CRITICAL: Parse metadata from API server call_id = None # Initialize before try block to avoid UnboundLocalError if hasattr(ctx.room, 'metadata') and ctx.room.metadata: try: metadata = json.loads(ctx.room.metadata) logger.info(f"📋 Raw metadata: {metadata}") # Extract core fields phone_number = metadata.get("phone_number", "unknown") loanId = metadata.get("loan_id", "unknown") call_id = metadata.get("call_id") if not call_id: logger.error("❌ call_id not found in metadata") return # ✅ FIXED: Update EMI data from metadata (multiple possible formats) if 'emi_data' in metadata: # Format 1: emi_data as nested object emi_dict = metadata['emi_data'] for key, value in emi_dict.items(): if hasattr(emi_data, key): setattr(emi_data, key, value) # Handle camelCase to snake_case conversion for company_name elif key == 'companyName' and hasattr(emi_data, 'company_name'): if value: # Only set if value is not None/empty setattr(emi_data, 'company_name', value) logger.info(f"✅ Custom company name set: {value}") # ✅ CRITICAL: Also check for company_name (snake_case) in emi_data if 'company_name' in emi_dict and emi_dict['company_name']: emi_data.company_name = emi_dict['company_name'] logger.info(f"✅ Company name set from emi_data: {emi_dict['company_name']}") logger.info(f"✅ EMI data loaded from 'emi_data' key for {emi_data.borrower_name}") else: # Format 2: Fields at top level of metadata if 'borrower_name' in metadata or 'borrowerName' in metadata: emi_data.borrower_name = metadata.get('borrower_name') or metadata.get('borrowerName', 'Unknown') if 'emi_amount' in metadata or 'emiAmount' in metadata: emi_data.emi_amount = float(metadata.get('emi_amount') or metadata.get('emiAmount', 0)) if 'due_date' in metadata or 'dueDate' in metadata: emi_data.due_date = metadata.get('due_date') or metadata.get('dueDate', 'Unknown') if 'language' in metadata: emi_data.language = metadata.get('language', INITIAL_LANGUAGE) if 'company_name' in metadata or 'companyName' in metadata: company_name_value = metadata.get('company_name') or metadata.get('companyName', '') if company_name_value: emi_data.company_name = company_name_value logger.info(f"✅ Company name set from metadata: {company_name_value}") logger.info(f"✅ EMI data loaded from top-level metadata for {emi_data.borrower_name}") # Set phone and loan ID emi_data.phone_number = phone_number emi_data.loan_id = loanId logger.info(f"📞 Final EMI Data - Name: {emi_data.borrower_name}, Amount: {emi_data.emi_amount}, Due: {emi_data.due_date}") except Exception as e: logger.error(f"❌ Metadata parsing error: {e}") import traceback traceback.print_exc() agent = EMIReminderAgent(userdata=emi_data) agent.call_id = call_id stt_instance = get_stt(emi_data.language) tts_instance = get_tts(emi_data.language) logger.info(f"✅ TTS/STT ready") vad_instance = silero.VAD.load( min_speech_duration=0.15, min_silence_duration=0.75, prefix_padding_duration=0.15, max_buffered_speech=20.0, activation_threshold=0.75, sample_rate=8000, force_cpu=True ) logger.info(f"✅ Silero VAD configured: 450ms silence threshold, 0.4 activation (confirmation-optimized)") # Create LLM instance - AWS Bedrock Claude 3.5 Sonnet v2 # try: # llm_instance = get_llm_instance( # model="anthropic.claude-3-sonnet-20240229-v1:0", # region_name=os.getenv("AWS_REGION", "ap-south-1"), # temperature=0.3, # max_tokens=150 # ) # except Exception as e: # logger.error(f"❌ Failed to create Bedrock LLM: {e}") # logger.info("⚠️ Falling back to OpenAI GPT-4o") llm_instance = openai.LLM( model="gpt-4o", temperature=0.3, max_completion_tokens=150, parallel_tool_calls=True, tool_choice="auto", timeout=8.0, ) session = AgentSession( vad=vad_instance, stt=stt_instance, tts=tts_instance, llm=llm_instance, userdata=emi_data ) logger.info(f"🎤 Starting agent session...") await session.start( agent=agent, room=ctx.room, room_input_options=RoomInputOptions( audio_enabled=True, text_enabled=False, ), room_output_options=RoomOutputOptions( audio_enabled=True, ), ) logger.info(f"✅ EMI session completed successfully") except Exception as e: logger.error(f"❌ EMI agent error: {e}") import traceback traceback.print_exc() finally: # ✅ CRITICAL FIX: Wait for ALL writes before closing try: if db_manager.connected: queue_size = db_manager.write_queue.qsize() if queue_size > 0: logger.info(f"⏳ [CLEANUP] Waiting for {queue_size} pending writes...") await db_manager.wait_for_writes(timeout=15.0) logger.info("✅ [CLEANUP] All writes completed") else: logger.info("✅ [CLEANUP] No pending writes") # Small delay to ensure everything is flushed await asyncio.sleep(0.5) except Exception as e: logger.error(f"❌ [CLEANUP] Cleanup error: {e}") import traceback traceback.print_exc() if name == "__main__": required_vars = ['LIVEKIT_URL', 'LIVEKIT_API_KEY', 'LIVEKIT_API_SECRET'] missing_vars = [var for var in required_vars if not os.getenv(var)] if missing_vars: logger.error(f"❌ Missing required environment variables: {missing_vars}") sys.exit(1) logger.info(f"✅ Environment check passed") # ✅ Connect to database before starting worker async def startup(): await db_manager.connect() if not db_manager.connected: logger.error("❌ Failed to connect to database on startup!") sys.exit(1) asyncio.run(startup()) try: cli.run_app(server) except Exception as e: logger.error(f"❌ Failed to start agent worker: {e}") import traceback traceback.print_exc() sys.exit(1) finally: # ✅ Ensure writes are flushed before exit async def shutdown(): await db_manager.wait_for_writes(timeout=10.0) await db_manager.close() asyncio.run(shutdown()) and this is how i am dispatching agent through other file } await self.save_call_log(call_id, loan_id, initial_log) try: dispatch_lk_api = api.LiveKitAPI(self.livekit_url, self.api_key, self.api_secret) await dispatch_lk_api.agent_dispatch.create_dispatch( api.CreateAgentDispatchRequest( room=room_name, agent_name="emi-reminder-agent-test2", metadata=room_metadata, ) ) await dispatch_lk_api.aclose() logger.info(f"✅ Agent dispatched") except Exception as e: logger.warning(f"⚠️ Agent dispatch failed: {e}") agent_joined = await self.wait_for_agent_participant(lk_api, room_name, "emi-reminder-agent-test2") if not agent_joined: logger.error(f"Agent failed to join the room: {room_name}") await self.update_call_log(call_id, { "callOutcome": "agent_not_joined", "callStatus": "failed_agent_unavailable", "endedAt": datetime.utcnow(), "sipStatus": "CANCELLED", "sipErrorMessage": "Call cancelled - Agent unavailable" }) try: await lk_api.room.delete_room(api.DeleteRoomRequest(room=room_name))
t
Thanks — this is very helpful. I’m responding to your details about self-hosting on EC2 and dispatching with
create_dispatch
. Short checklist of highest-probability causes (based on your code) • Race between dispatch and caller join: you dispatch immediately; agent may start before the SIP caller is present, so it sees no participant/audio and exits or never links. • Worker/process lifecycle (DB connect): DB connect per worker must be reliable — if a worker blocks or fails to connect, the agent job can hang or die. • Connection sequencing / double connect: calling
await ctx.connect()
then
session.start(...)
can create race/duplication —
session.start
will handle connecting and subscriptions. • No explicit participant binding: if the agent joins before caller or room state hasn’t propagated, RoomIO may not auto-link to the SIP participant → no audio. • Premature room deletion: deleting the room on a perceived agent-failure can create inconsistent state for retries. Immediate, minimal changes to try (in order) 1. Delay dispatch until the caller is confirmed joined. ◦ Poll the room participants (via LiveKit API) and only call
create_dispatch
after you detect the SIP participant. 1. Remove the extra
await ctx.connect()
and rely on
await session.start(...)
to establish the agent connection. 2. Explicitly bind the agent to the caller participant when starting the session: ◦ Extract the SIP participant identity from
ctx.room.participants
and pass it in
RoomInputOptions
(e.g.,
participant_identity
or explicit
participant_kinds
). 1. Make DB connect idempotent and per-worker safe: ◦ Ensure each worker calls
db_manager.connect()
and waits until
db_manager.connected
before proceeding; add short retries. 1. Stop deleting the room immediately on a failed agent join. Wait for an explicit session end or a longer timeout before cleanup. 2. Add short stabilization delays where needed: ◦ After confirming caller joined, wait ~0.5–1.0s before dispatching/starting session to let SFU state propagate. 1. Enable client preconnect audio buffering (on the caller side) so early speech isn’t lost. Key logging/events to add (will make root cause obvious) • Log participant join events, track publications, and when the agent publishes/subscribes. • Log lifecycle steps: "caller detected", "dispatch requested", "agent process started", "session.start called", "agent linked to participant". • If agent exits, log exit reason/traceback and whether RoomIO had any linked participant. If you want, I can: • Provide the small code patches for (A) polling/wait-for-caller before dispatch, (B) removing
ctx.connect()
and explicitly binding participant in
RoomInputOptions
, and (C) a safe
ensure_db_connected()
helper. Which one should I give first?
a
1. Delay dispatch until the caller is confirmed joined. ◦ Poll the room participants (via LiveKit API) and only call
create_dispatch
after you detect the SIP participant. 2. Remove the extra
await ctx.connect()
and rely on
await session.start(...)
to establish the agent connection. 3. Explicitly bind the agent to the caller participant when starting the session: ◦ Extract the SIP participant identity from
ctx.room.participants
and pass it in
RoomInputOptions
(e.g.,
participant_identity
or explicit
participant_kinds
). 4. Make DB connect idempotent and per-worker safe: ◦ Ensure each worker calls
db_manager.connect()
and waits until
db_manager.connected
before proceeding; add short retries. 5. Stop deleting the room immediately on a failed agent join. Wait for an explicit session end or a longer timeout before cleanup. 6. Add short stabilization delays where needed: ◦ After confirming caller joined, wait ~0.5–1.0s before dispatching/starting session to let SFU state propagate. 7. Enable client preconnect the things u have mentiond what if agent never joins the room and the sip participant is still waitinh inside the room wouldnt that cuse an issue since its a cal ?
t
Good question — that exact worry is why you need explicit timeout, retry, and fallback behavior. Below are practical patterns to avoid leaving callers stranded while still giving the agent a fair chance to join. 1) Wait-with-timeout (recommended) • Start polling for the SIP participant, then dispatch the agent only when caller present. • After dispatch, wait for the agent to appear with a short timeout (e.g., 5–15s). • If timeout expires, take a fallback action (see options below). 2) Retry with backoff • If agent doesn’t join within timeout, retry dispatch 1–2 times with short exponential backoff (e.g., 1s, then 2–4s). • Limit retries to avoid indefinite waiting. 3) Fallbacks when agent never joins Choose one depending on product needs: • Failover to a human agent or IVR message (best UX). • Play a queued message and end call after informing caller. • Route caller to voicemail and record for later processing. • Mark call as “agent_unavailable” and release room so caller isn’t blocked. 4) Don't delete the room immediately • Never delete the room instantly on first failure. Instead: ◦ Keep room alive for a configured cooldown (e.g., 30–60s) to allow retries. ◦ After cooldown and retries fail, then perform cleanup/deletion. 5) Release resources & notify • When giving up, explicitly unpublish or hang up the SIP participant, or send an audio prompt explaining the issue before disconnecting. • Log and surface metrics (agent join latency, failure rate) so you can tune timeouts. 6) Example flow (concise) 1. Detect caller joined. 2. Dispatch agent. 3. Wait for agent join with timeout = 10s. ◦ If joined → start session. ◦ If not → retry dispatch (max 2 retries) with 1–2s backoff. ◦ After retries fail → run fallback (IVR/human/voicemail), wait short cooldown, then cleanup. 7) Tuning tips • Use lower timeouts (5–10s) in colocated environments; higher (15–20s) if network/EC2 cold starts suspected. • Increase retries if you see transient cold-starts; reduce if it increases caller wait complaints. • Instrument metrics (agent_join_time, retries, abandoned_calls) to pick optimal numbers. If you tell me which fallback behavior you prefer (human transfer, IVR message, voicemail, or immediate hangup), I can provide a small code snippet implementing the wait/retry/cooldown logic for your dispatch path.
a
why me dispatching the agent first and makin him jointhe room and then sip participant is joining causing a issue
t
Good question — and this is a common source of flakiness. Short answer: order matters because the agent needs a real participant (and their published audio tracks) to detect, subscribe to, and route audio. If the agent joins first, it often cannot find or bind to the SIP caller, so no audio flows and the agent may exit or idle. Key reasons this causes issues • Race condition for participant discovery: When the agent starts before the SIP participant exists, RoomIO may find no target participant to subscribe to. The agent then has nothing to listen to. • Missing track publications: The SIP caller only publishes audio after joining. If the agent has already completed its subscription logic, it may not re-subscribe automatically to later-published tracks. • Subscription/linking timing: Many agent session setups expect a participant identity or an existing participant list. Without that, the agent can’t attach input/output streams correctly. • Agent lifecycle behavior: Agents often start, evaluate room state, and exit if no usable participant/audio is present (to avoid wasting resources). That makes the first dispatch a no-op. • Self-hosted latency and cold starts: On EC2 you may have extra startup/network delay; dispatching too early increases chances the agent misses the caller. • Cleanup side effects: If your code treats "agent not joined" as failure and deletes the room, it compounds the problem for retries. What to do instead (brief) • Wait for the SIP participant to join before dispatching the agent. • Or dispatch only after detecting the caller, then wait (with a short timeout) for the agent to appear and explicitly bind the agent to the caller participant identity when starting the session. • Add small stabilization delay (0.5–1s) and limited retries if agent doesn’t join right away. If you want, I can show a small code snippet that (A) waits for the SIP participant, (B) dispatches with retries, and (C) then binds the agent to that participant. Which snippet would you like?
a
giv e me the whole flow what to do first and then how to do
t
Nice — I’ll give you a clear, ordered flow you can implement, plus concise code snippets for each step. This will prevent the race conditions you’re seeing (agent starting before the SIP caller) and include retries, timeouts, and clean cleanup. Summary flow (high-level) 1. Create or ensure room exists for the call. 2. Wait for the SIP caller to join (poll LiveKit room participants). 3. When caller present, wait a short stabilization delay (0.5–1.0s). 4. Dispatch the agent via
create_dispatch
. 5. Wait for the agent participant to join (timeout + retries). 6. Start the AgentSession and explicitly bind to the SIP participant identity. 7. Monitor session and participant events; on failures use a fallback (IVR/human/voicemail) after retries. 8. Clean up only after session end or fallback complete (don’t delete room immediately on first failure). Config choices (recommended) • Caller-wait timeout: 10s • Agent-join timeout: 10s per attempt • Dispatch retries: 1–2 • Stabilization delay: 0.5–1.0s • Overall caller cooldown before final cleanup: 30–60s Code snippets (Python-ish pseudocode; adapt to your codebase) 1) Helper: poll for caller (SIP participant)
Copy code
async def wait_for_sip_caller(lk_url, api_key, api_secret, room_name, timeout=10.0, poll_interval=1.0):
    api = api.LiveKitAPI(lk_url, api_key, api_secret)
    deadline = asyncio.get_event_loop().time() + timeout
    try:
        while asyncio.get_event_loop().time() < deadline:
            room_info = await api.room.get_room(api.RoomRequest(room=room_name))
            for p in room_info.participants:
                # ParticipantKind enum may differ by SDK; check SDK value for SIP
                if p.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP:
                    return p.identity
            await asyncio.sleep(poll_interval)
    finally:
        await api.aclose()
    return None
2) Dispatch agent with retries and wait for agent join
Copy code
async def dispatch_agent_and_wait(lk_url, api_key, api_secret, room_name, agent_name,
                                  metadata, max_retries=2, join_timeout=10.0):
    dispatch_api = api.LiveKitAPI(lk_url, api_key, api_secret)
    try:
        for attempt in range(1, max_retries + 1):
            await dispatch_api.agent_dispatch.create_dispatch(
                api.CreateAgentDispatchRequest(room=room_name, agent_name=agent_name, metadata=metadata)
            )
            # wait for agent participant
            joined = await wait_for_agent_participant(dispatch_api, room_name, agent_name, timeout=join_timeout)
            if joined:
                return True
            # backoff before retry
            await asyncio.sleep(1.0 * attempt)
        return False
    finally:
        await dispatch_api.aclose()

async def wait_for_agent_participant(api_client, room_name, agent_name, timeout=10.0, poll_interval=0.5):
    deadline = asyncio.get_event_loop().time() + timeout
    while asyncio.get_event_loop().time() < deadline:
        try:
            room_info = await api_client.room.get_room(api.RoomRequest(room=room_name))
            for p in room_info.participants:
                if p.identity == agent_name:
                    return True
        except Exception:
            pass
        await asyncio.sleep(poll_interval)
    return False
3) Start agent session binding to caller participant
Copy code
# In your agent worker entrypoint (ctx available)
# 1. Ensure db connected per-worker (ensure_db_connected helper)
# 2. Do NOT call await ctx.connect() separately if session.start handles it.

# find SIP participant identity from ctx.room.participants
caller_identity = None
for pid, participant in ctx.room.participants.items():
    if participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP:
        caller_identity = participant.identity
        break

if not caller_identity:
    logger.error("No SIP caller found in room at agent start")
    # optionally retry a short time, then exit

# small stabilization delay
await asyncio.sleep(0.5)

# build session and pass explicit participant binding
session = AgentSession(...)

await session.start(
    agent=agent,
    room=ctx.room,
    room_input_options=RoomInputOptions(
        audio_enabled=True,
        participant_kinds=[rtc.ParticipantKind.PARTICIPANT_KIND_SIP],
        participant_identity=caller_identity,  # explicit binding
    ),
    room_output_options=RoomOutputOptions(audio_enabled=True),
)
4) Wait-with-timeout / fallback flow (caller-facing safety)
Copy code
# Orchestrator flow when initiating the call:
caller_identity = await wait_for_sip_caller(...)
if not caller_identity:
    # Caller never joined: run fallback (hangup or voicemail) immediately
    run_fallback("caller_not_joined")
    return

# short stabilization
await asyncio.sleep(0.8)

# dispatch and wait for agent
agent_joined = await dispatch_agent_and_wait(..., max_retries=2, join_timeout=10.0)
if not agent_joined:
    # Agent never joined: attempt fallback (IVR, human transfer, voicemail)
    run_fallback("agent_unavailable")
    # keep room alive for cooldown then cleanup (30s)
    await asyncio.sleep(30.0)
    await lk_api.room.delete_room(api.DeleteRoomRequest(room=room_name))
    return

# Agent joined => proceed (agent worker will start session and handle call)
5) DB connect helper for worker processes
Copy code
async def ensure_db_connected(db_manager, max_wait=5.0):
    start = asyncio.get_event_loop().time()
    if not db_manager.connected:
        await db_manager.connect()
    while not db_manager.connected and (asyncio.get_event_loop().time() - start) < max_wait:
        await asyncio.sleep(0.1)
    return db_manager.connected
6) Logging & events to add (minimal) • Log when caller detected, when dispatch requested, when agent process starts, when agent joins, when tracks are published, and when session ends. • Add event handlers for participant connected/disconnected and track_published to trace audio routing. 7) Cleanup rules (safe) • Never delete room immediately on first agent join failure. • After final fallback/policy decision, end call politely (play IVR message or transfer), wait short cooldown, then delete room. Final notes • The key fixes: wait for caller before dispatch, bind agent explicitly to the SIP participant when starting the session, and implement timeouts/retries/fallbacks. • Adjust timeouts based on observed agent cold-starts on your EC2 instance — if you see longer startup times, increase agent-join timeout. If you want, I can: • Convert these snippets into a ready-to-drop patch for your exact files (entrypoint + dispatch caller), or • Provide a single combined orchestrator function that implements the full flow end-to-end. Which would you prefer?