This message was deleted.
# ask-ai
s
This message was deleted.
t
Thanks for reaching out, Arun. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Hi — welcome! Thanks for saying hello. How can I help you today? A few examples of things I can assist with: • Troubleshooting a room or connection issue (share your Room ID and a brief description) • SDK or API questions (which SDK and language are you using?) • Account, billing, or permissions questions • Guidance on implementing features like recording, relaying, or data messages If you’re not sure where to start, tell me what you’re trying to accomplish and I’ll suggest the next steps. , with interactive elements
c
import asyncio from datetime import datetime from dotenv import load_dotenv from livekit import api from livekit.rtc import DisconnectReason from livekit.agents import Agent, JobContext, JobProcess, WorkerOptions, cli, room_io from livekit.agents.beta.tools import EndCallTool {e}") agent = VoiceAgent(ctx, meta_data, variables, call_metadata, transfer_context, disconnect_tracker, inactivity_config) agent_data = meta_data['agent'] interruption_sensitivity = agent_data.get('interruption_sensitivity', 0.5) min_interruption_duration, false_interruption_timeout = get_interruption_params(interruption_sensitivity) if is_telephony and is_outbound: greeting = agent_data.get('greeting_message', OUTBOUND_MESSAGE) else: greeting = agent_data.get('greeting_message', WELCOME_MESSAGE) greeting = substitute_variables(greeting, variables) agent._greeting_message = greeting agent._pause_before_greeting = agent_data.get('pause_before_greeting', 0) eagerness = agent_data.get('response_eagerness', 1.0) min_ep_delay = 1.0 - eagerness session = AgentSession( vad=ctx.proc.userdata.get("vad"), min_endpointing_delay=min_ep_delay, preemptive_generation=False, min_interruption_duration=min_interruption_duration, false_interruption_timeout=false_interruption_timeout, ) try: pause_inactivity, resume_inactivity = inactivity_handler(session, ctx, agent._inactivity_config) except (ValueError, TypeError) as e: pause_inactivity, resume_inactivity = inactivity_handler(session, ctx) if agent._transfer_config is not None and is_telephony: agent.set_inactivity_controls(pause_inactivity, resume_inactivity) start_time = datetime.now() is_transfer_agent = agent._transfer_config is not None and is_telephony room_options = room_io.RoomOptions( delete_room_on_close=not is_transfer_agent, ) await session.start(agent=agent, room=ctx.room, room_options=room_options) voicemail_integration = None if is_telephony: llm_config = meta_data.get('llm', {}) voicemail_detector = VoicemailDetector( silence_threshold=2.0, phrase_detection_enabled=True, llm_detection_enabled=True, llm_config=llm_config, max_user_inputs_to_check=2 ) voicemail_integration = VoicemailIntegration(session, voicemail_detector) agent.set_voicemail_integration(voicemail_integration) await voicemail_integration.start_monitoring() async def on_session_end(): recording_url = None try: egress_list = await ctx.api.egress.list_egress( api.ListEgressRequest(room_name=ctx.room.name) ) for egress_info in egress_list.items: if egress_info.status == api.EgressStatus.EGRESS_ACTIVE: await ctx.api.egress.stop_egress( api.StopEgressRequest(egress_id=egress_info.egress_id) ) if hasattr(egress_info, 'file_results') and egress_info.file_results: for file_result in egress_info.file_results: if hasattr(file_result, 'filename') and file_result.filename: recording_url = f"gs://livkit_call_recordings/{file_result.filename}" break elif hasattr(egress_info, 'file') and egress_info.file: if hasattr(egress_info.file, 'filename') and egress_info.file.filename: recording_url = f"gs://livkit_call_recordings/{egress_info.file.filename}" if recording_url: break except Exception as e: pass voicemail_result = {} if voicemail_integration: voicemail_result = voicemail_integration.stop_monitoring() final_disconnect_reason = disconnect_tracker.get_final_disconnect_reason() is_voicemail_detected = voicemail_result.get('is_voicemail', False) if is_voicemail_detected: final_disconnect_reason = 999 raw_transcript = session.history.to_dict() if hasattr(session, "history") else {"items": []} end_call_data = build_end_call_payload( ctx=ctx, meta_data=meta_data, start_time=start_time, recording_url=recording_url, disconnect_reason=final_disconnect_reason, is_outbound=is_outbound, is_telephony=is_telephony, channel=channel, from_number=from_number if is_telephony else None, to_number=to_number if is_telephony else None, transcript=raw_transcript, voicemail_result=voicemail_result ) try: await end_call(end_call_data) except Exception as e: pass if not is_transfer_agent: try: await ctx.api.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name)) except Exception as e: pass ctx.add_shutdown_callback(on_session_end) def run(): cli.run_app( WorkerOptions( entrypoint_fnc=entrypoint, prewarm_fnc=prewarm, agent_name="ar-agent", num_idle_processes=4, port=8081, ) ) if name == "__main__": run() how todo warm tramfer of calls?
t
Thanks — I see you pasted your agent entrypoint and asked “how to do warm transfer of calls?” I can help you add a warm (agent-assisted) transfer into that flow. Quick summary of what’s available • Use the prebuilt Beta task called WarmTransferTask. It dials the human/supervisor via SIP, puts the caller on hold (plays hold audio), sends the human agent conversation context/history, and provides tools for the human to accept/decline or connect to the caller. • WarmTransferTask returns a WarmTransferResult (includes the human agent identity) and supports customization like hold audio and extra instructions. • Prerequisites: a provisioned SIP outbound trunk and telephony-enabled agent context (set the trunk ID in env var
LIVEKIT_SIP_OUTBOUND_TRUNK
or
SIP_OUTBOUND_TRUNK_ID
). Before I give a concrete code suggestion, two quick clarifying questions so I tailor the example to your setup: 1. Are you running a Python Agent in the LiveKit Agents Beta (yes/no)? 2. Do you want the transfer initiated from the AI agent (agent asks supervisor) or from an external trigger (e.g., UI button or supervisor request)? If you confirm “yes” to (1) and that you want the AI agent to start the transfer, here’s a concise example of how to call WarmTransferTask from your session flow and handle the result. Paste this into the place where you decide to transfer (for example inside your session logic before you call
await session.start(...)
ends or when agent decides to transfer):
Copy code
from livekit.agents.beta.workflows import WarmTransferTask

# Example usage inside your async agent handler when you want to warm-transfer:
result = await WarmTransferTask(
    target_phone_number="<supervisor-phone-number>",   # required
    sip_trunk_id="<outbound-trunk-id>",                # required or rely on LIVEKIT_SIP_OUTBOUND_TRUNK env var
    chat_ctx=session.history,                           # pass conversation history/context
    extra_instructions="Supervisor: caller needs billing escalation",  # optional
    hold_audio=None,                                    # optional: defaults to built-in hold music
)

# result is a WarmTransferResult with info like human_agent_identity
if result and result.human_agent_identity:
    # transfer accepted/connected — handle cleanup or handoff
    # e.g., mark this session transferred and stop AI audio I/O
    pass
else:
    # declined or failed — resume as needed
    pass
Helpful implementation notes • Set
LIVEKIT_SIP_OUTBOUND_TRUNK
(or pass
sip_trunk_id
) to your outbound SIP trunk ID so the task can dial the supervisor. • You can customize
hold_audio
or
extra_instructions
to provide richer context to the human agent. • The task handles placing the caller on hold and merging the human agent into the call when they accept; your agent should stop its I/O and exit gracefully once the human is connected. • Ensure your SIP provider allows outbound calling and caller-ID passthrough if you need to preserve the original caller number. Would you like: • A version of the example adapted to where in your pasted code it should be called (point me to the exact function/line you want to modify), or • An example showing how to set the env var and test the transfer end-to-end with a test-number? Tell me which and I’ll provide the tailored code snippet and next steps. Sources: WarmTransferTask | LiveKit Documentation | Outbound Calling Script | LiveKit Documentation | Preserving Caller ID During SIP Transfers in LiveKit Agents, with interactive elements
c
import inspect import asyncio from typing import Dict, Any, Optional from livekit.agents import function_tool from livekit.agents.llm import ToolError from livekit.agents.beta.workflows import WarmTransferTask from .utils import get_sip_number def build_warm_transfer_tool( agent_ref, transfer_config: Dict[str, Any], transfer_context: Dict[str, Any], call_metadata: Optional[Dict[str, Any]] = None, ): """Build a warm transfer tool that lets the AI speak to the agent before connecting the caller.""" routing_type = transfer_config.get("routing_type", "static") if routing_type == "static": return _build_static_warm_transfer(agent_ref, transfer_config, transfer_context, call_metadata) return _build_dynamic_warm_transfer(agent_ref, transfer_config, transfer_context, call_metadata) def _get_caller_id(config: Dict[str, Any], call_metadata: Optional[Dict[str, Any]]) -> Optional[str]: """Determine caller ID based on config - use agent's number or preserve caller's number.""" if config.get("caller_id") == "agent": return get_sip_number(config, call_metadata) return call_metadata.get("from_number") if call_metadata else None def _build_transfer_description(config: Dict[str, Any], is_dynamic: bool = False) -> str: """Build the tool description for the LLM.""" base = config.get("description", "Transfer the call to a human agent") destination = config.get("transfer_to", "") rules = config.get("dynamic_routing_rules", "") desc = f"{base}. This is a WARM transfer - you will speak to the human agent first to explain the situation." if is_dynamic and rules: desc += f" {rules}" elif destination: desc += f" Transfer to: {destination}" return desc def _get_handoff_message(config: Dict[str, Any]) -> str: """Get the message to tell the caller before transferring.""" message = config.get("handoff_message", "I'm connecting you to a human agent now.") if config.get("handoff_message_type") == "prompt": return f"I'll transfer you to a human agent. {message}" return message async def _execute_warm_transfer( agent_ref, config: Dict[str, Any], transfer_context: Dict[str, Any], call_metadata: Optional[Dict[str, Any]], destination: str, ) -> None: """Execute the warm transfer flow: AI talks to agent first, then connects caller.""" ctx = transfer_context.get("ctx") sip_trunk_id = transfer_context.get("sip_trunk_id", "") if not sip_trunk_id: raise ToolError("Transfer not available: no outbound SIP trunk configured") caller_id = _get_caller_id(config, call_metadata) # Step 1: Tell caller we're transferring handoff_msg = _get_handoff_message(config) await agent_ref.session.say(handoff_msg, allow_interruptions=False) # Step 2: Dial the human agent warm_transfer = WarmTransferTask( room=ctx.room, destination=f"tel:{destination}" if not destination.startswith("sip:") else destination, sip_trunk_id=sip_trunk_id, caller_id=caller_id, ) await warm_transfer.start() try: # Step 3: Wait for agent to answer await warm_transfer.wait_for_answer(timeout=30) # Step 4: AI introduces itself and explains the situation to the human agent await agent_ref.session.say( "Hello! I'm an AI assistant handling a call. Let me quickly brief you on this caller before I connect you.", allow_interruptions=False ) # Step 5: Provide context about the caller context = _build_call_context(call_metadata) if context: await agent_ref.session.say(context, allow_interruptions=True) # Step 6: Final handoff message to agent, then connect the caller await agent_ref.session.say( "I'm now connecting the caller to you. They should be on the line momentarily.", allow_interruptions=False ) # Step 7: Bridge the calls and exit await warm_transfer.complete() agent_ref.session.shutdown() except asyncio.TimeoutError: await warm_transfer.cancel() raise ToolError("The agent did not answer within 30 seconds") except Exception as e: await warm_transfer.cancel() raise ToolError(f"Transfer failed: {e}") from e def _build_call_context(call_metadata: Optional[Dict[str, Any]]) -> str: """Build a concise summary of the call for the human agent.""" if not call_metadata: return "" parts = [] if call_metadata.get("from_number"): parts.append(f"Caller: {call_metadata['from_number']}") if call_metadata.get("direction"): parts.append(f"Direction: {call_metadata['direction']}") # Add any custom metadata for key, value in call_metadata.get("metadata", {}).items(): if value and str(value).strip(): parts.append(f"{key}: {value}") if parts: return "Caller info: " + ", ".join(parts) return "" def _build_static_warm_transfer(agent_ref, config, transfer_context, call_metadata): """Build warm transfer tool for static routing (fixed destination).""" description = _build_transfer_description(config) destination = config.get("transfer_to", "").strip() if not destination: raise ToolError("No transfer destination configured") async def transfer_call(): dest = destination.removeprefix("tel:").removeprefix("sip:") if not dest: raise ToolError("Invalid transfer destination") await _execute_warm_transfer(agent_ref, config, transfer_context, call_metadata, dest) transfer_call.name = "transfer_call" transfer_call.doc = description transfer_call.signature = inspect.Signature([]) transfer_call.annotations = {} return function_tool(transfer_call) def _build_dynamic_warm_transfer(agent_ref, config, transfer_context, call_metadata): """Build warm transfer tool for dynamic routing (AI chooses destination).""" description = _build_transfer_description(config, is_dynamic=True) parameters = [ inspect.Parameter( name="transfer_to", kind=inspect.Parameter.KEYWORD_ONLY, annotation=str, default=inspect.Parameter.empty, ) ] async def transfer_call(*, transfer_to: str): dest = transfer_to.strip().removeprefix("tel:").removeprefix("sip:") if not dest: raise ToolError("No transfer destination provided") await _execute_warm_transfer(agent_ref, config, transfer_context, call_metadata, dest) transfer_call.name = "transfer_call" transfer_call.doc = description transfer_call.signature = inspect.Signature(parameters) transfer_call.annotations = {"transfer_to": str} return function_tool(transfer_call) modifi this cod enot workign for warm transfer{ "message": "Agent functions retrieved successfully", "data": [ { "id": 36, "name": "transfer_call", "description": "Transfer the call to a human agent", "endpoint": "internal://transfer", "method": "POST", "headers": null, "query_params": null, "transfer_config": { "caller_id": "agent", "transfer_to": "+16193892389", "routing_type": "static", "transfer_type": "warm", "handoff_message": "The AI will determine the correct number based on these rules.\n\n", "transfer_method": "invite", "handoff_message_type": "prompt" }, "is_active": true, "timeout_seconds": 30, "speech_during_config": null, "speech_after_enabled": true, "end_instructions": null, "agent_id": 32, "client_id": 2, "created_at": "2026-03-22T114549.255Z", "updated_at": "2026-03-22T121548.965Z", "created_by": 2, "updated_by": 2 } ] }
t
Thanks — I can see the warm-transfer tool code you pasted and that it’s not working. I’ll help troubleshoot and suggest specific fixes, but first I need a couple quick clarifying details so I don’t miss the root cause. What I’m responding to: • Your custom warm-transfer tool (static and dynamic builders) that uses WarmTransferTask and the error: “modifi this cod enot workign for warm transfer”. • You included the transfer config for a static transfer to +16193892389 and caller_id set to "agent". Quick questions 1. What symptom or error do you see when you run this? (runtime exception stack trace, WarmTransferTask never answers, caller not bridged, task.start()/wait_for_answer hangs, etc.) 2. Is the SIP trunk configured and reachable? Confirm either: ◦ You set env var
LIVEKIT_SIP_OUTBOUND_TRUNK
or ◦
transfer_context["sip_trunk_id"]
contains the correct outbound trunk ID. 1. Are you running this inside a telephony-enabled agent session (i.e., caller is in a telephony room and
ctx.room
is valid)? 2. Any logs from WarmTransferTask (if you have them) or from LiveKit server around the time of the transfer? While you gather that, here are targeted code corrections and improvements you can apply now — these address common issues and some problems I see in your code. 1) Ensure WarmTransferTask usage matches the API • WarmTransferTask typically accepts
target_phone_number
(or
destination
depending on version) and
sip_trunk_id
. In your code you pass
room=ctx.room
and
destination=...
. Confirm the correct parameter names for your installed LiveKit Agents Beta release. • Safe approach: pass both the explicit
room
and
target_phone_number
(or
destination
) that the library expects. If your version expects
target_phone_number
, change the call to use that name. Example change (try this if your library expects
target_phone_number
):
Copy code
warm_transfer = WarmTransferTask(
    room=ctx.room,
    target_phone_number=f"tel:{destination}" if not destination.startswith("sip:") else destination,
    sip_trunk_id=sip_trunk_id,
    caller_id=caller_id,
)
2) Start / wait / complete flow — use start(), wait_for_answer(), then complete() • You call
await warm_transfer.start()
then
await warm_transfer.wait_for_answer(timeout=30)
which is correct. Make sure you handle the cases where answer never arrives and log returned state after start. Add defensive logging and state checks:
Copy code
await warm_transfer.start()
# log start result/state if available
try:
    await warm_transfer.wait_for_answer(timeout=30)
except asyncio.TimeoutError:
    await warm_transfer.cancel()
    raise ToolError("The agent did not answer within 30 seconds")

# optionally inspect warm_transfer.result or warm_transfer.state here
3) Use correct methods to bridge/complete • You call
await warm_transfer.complete()
and then
agent_ref.session.shutdown()
. Confirm
complete()
is the correct method to bridge the human agent and caller in your installed version. Some versions may use
warm_transfer.connect()
or return a
WarmTransferResult
to act upon. If
complete()
returns a result, capture it. Example:
Copy code
transfer_result = await warm_transfer.complete()
# inspect transfer_result for success/human_agent_identity
4) Don’t call session.say after the human answers on the dial leg • In your flow you use
agent_ref.session.say(...)
to speak to the human agent after wait_for_answer. That works only if the AI still has a media path to the human leg. If WarmTransferTask created a separate telephony leg for the human, speaking via the original session may not reach them. Instead: ◦ Use the transfer task’s API to send audio to the human leg (if provided), or ◦ Provide the context text as
extra_instructions
when creating WarmTransferTask so it appears to the human agent automatically. So either: • Pass context in `extra_instructions`:
Copy code
warm_transfer = WarmTransferTask(
    room=ctx.room,
    target_phone_number=...,
    sip_trunk_id=sip_trunk_id,
    caller_id=caller_id,
    extra_instructions=context  # brief summary for human
)
or • Use the tool methods that inject TTS into the human dial leg (if available in your version). 5) Ensure caller_id format is supported •
_get_caller_id
returns either agent SIP number or original caller number. Make sure the
caller_id
format matches your SIP trunk/provider expectations (E.164 for PSTN, or SIP URI like
sip:...
for SIP destinations). If your trunk rejects the caller id, the outbound call may fail. Log the final caller_id used. 6) Properly shutdown/cleanup • After bridge/complete, call
await warm_transfer.close()
or similar if provided, and then
await agent_ref.session.shutdown()
only after the bridge is confirmed. Avoid
session.shutdown()
if you still need to record or check egress — ensure graceful ordering. 7) Minor code fixes / robustness • Use
.startswith("tel:")
checks consistently (you remove prefixes then later assume plain numbers). Example:
Copy code
dest = destination
if dest.startswith("tel:"):
    dest = dest[len("tel:"):]
elif dest.startswith("sip:"):
    # keep sip: form if needed
    pass
# then build target param accordingly
• Ensure
transfer_call.__name__
and signatures are correct (you already set them — good). 8) Add logging to capture errors from WarmTransferTask • Wrap start/wait/complete in try/except and log the full exception and any
warm_transfer
state. This will make the actual failure clear (SIP reject, network, missing trunk ID, timed out, etc). What I can do next for you • If you paste the exact error/stack trace or logs from when you run the transfer, I’ll point to the exact line to change. • If you tell me which version of livekit-agents you’re using (pip show livekit-agents) I’ll give a drop-in code snippet matched to that version’s WarmTransferTask signature. • I can produce a ready-to-paste patch of your file with the parameter-name fixes, extra_instructions usage, added logging, and robust error handling. Which of those would you like me to do next? If you can share the runtime error or confirm the library version, I’ll provide the precise fix. , with interactive elements
c
give full code
t
I don't have the answer you're looking for. You could also try asking your question: • in one of the other Slack channels or • to https://deepwiki.com/livekit/livekit_composite which is trained on all LiveKit source code If you find the answer, please post it here to help others!
c
give full code