creamy-tiger-14805
12/15/2025, 8:08 AMcreamy-tiger-14805
12/15/2025, 10:06 AMlively-winter-88668
12/15/2025, 10:43 AMcreamy-tiger-14805
12/15/2025, 10:48 AM@function_tool
async def transfer_call(self, context: RunContext, reason: str | None = None):
"""
Transfer the conversation to a human agent when appropriate.
Call this function when:
- Direct requests: "I want to speak to a human", "Connect me with a representative", "Transfer me to an agent"
- Technical limitations: The user needs something beyond your capabilities
- Complex issues: Problems requiring human judgment or access to systems beyond your reach
Before calling this function, try to assist the user first if the request is within your capabilities.
Do not call this function when:
- You can answer basic information requests yourself
- You can complete simple tasks within your capabilities
- The user hasn't explicitly requested human assistance for complex issues
EXAMPLE FLOWS:
Example 1 (explicit request):
User: "I want to talk to a real person instead of an AI. Please connect me to a human agent."
Assistant: "I understand you'd prefer to speak with a human agent. I'm arranging that connection for you now, and a human representative will assist you shortly."
[transfer_to_number function called]
Example 2 (technical limitation):
User: "I need to change my billing address and payment method for my order #12345."
Assistant: "I understand you need to update your billing information. Since this requires accessing your account details, I'll connect you with a human representative who can help you with that change safely."
[transfer_to_number function called]
Example 3 (DO NOT call):
User: "What's your refund policy?"
Assistant: "Our standard refund policy allows returns within 30 days of purchase with a receipt. Would you like more specific information about a particular product or situation?"
"""
transfer_to = self.dial_info["transfer_to"]
if not transfer_to:
await context.session.say("I'm sorry, I cannot transfer the call right now. I can help schedule a callback for you.")
# self._closing_task = asyncio.create_task(self.session.aclose())
raise Exception("human agent number not provided")
<http://logger.info|logger.info>(f"transferring call to {transfer_to}")
# let the message play fully before transferring
await context.session.say("I'll be transferring you to my team. Please hold on while I connect you.")
job_ctx = get_job_context()
try:
self.human_transfer_vars["attempted"] = True
self.human_transfer_vars["attempted_at"] = datetime.now(timezone.utc)
await job_ctx.api.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name=job_ctx.room.name,
sip_trunk_id=self.dial_info["sip_trunk_id"],
sip_call_to=transfer_to,
participant_identity=transfer_to,
# function blocks until user answers the call, or if the call fails
wait_until_answered=True,
)
)
participant = await job_ctx.wait_for_participant(identity=transfer_to)
<http://logger.info|logger.info>(f"participant joined: {participant.identity}")
self.human_transfer_vars["handover_at"] = datetime.now(timezone.utc)
await job_ctx.api.room.remove_participant(api.RoomParticipantIdentity(
room=job_ctx.room.name,
identity=job_ctx.room.local_participant.identity
))
<http://logger.info|logger.info>(f"Agent disconnected")
self.human_transfer_vars["agent_disconnected_at"] = datetime.now(timezone.utc)
self.human_transfer_vars["attempt_status"] = "success"
<http://logger.info|logger.info>(f"transferred call to {transfer_to}")
except Exception as e:
logger.error(f"error transferring call to {transfer_to}: {e}")
self.human_transfer_vars["error_message"] = str(e)
self.human_transfer_vars["attempt_status"] = "failed"
await context.session.say("I'm sorry, I cannot transfer the call right now. I will schedule a callback for you.")
self._closing_task = asyncio.create_task(self.session.aclose())
yes, i did see that example.
I did some changes as per my use case and it was working.
But recently, the agent has been calling the tool twice each time.lively-winter-88668
12/15/2025, 11:13 AMBut recently, the agent has been calling the tool twice each time.Was there anything that coincided with the start of the failures? An update to the code? An upgrade to a new package version? A redeploy which might have resulted in pulling in an updated package? As an absolute fail-safe you could test whether the tool had already been called by storing some flag in userData, https://docs.livekit.io/agents/build/agents-handoffs/#passing-state, and checking for that in the subsequent function call - but ideally that shouldn't be necessary.
creamy-tiger-14805
12/15/2025, 11:34 AMawait job_ctx.api.room.remove_participant(api.RoomParticipantIdentity(
room=job_ctx.room.name,
identity=job_ctx.room.local_participant.identity
))
<http://logger.info|logger.info>(f"Agent disconnected")
updated to
await self.session.aclose()
this let me have control over the room events even after agent disconnection.
except Exception as e:
logger.error(f"error transferring call to {transfer_to}: {e}")
self.human_transfer_vars["error_message"] = str(e)
self.human_transfer_vars["attempt_status"] = "failed"
await context.session.say("I'm sorry, I cannot transfer the call right now. I will schedule a callback for you.")
self._closing_task = asyncio.create_task(self.session.aclose())
Also I had a close session task in my exception block, which was later removed to let the agent remain in the conversation in case of failure to add a human agent.creamy-tiger-14805
12/15/2025, 11:47 AMcreamy-tiger-14805
12/15/2025, 1:05 PMlively-winter-88668
12/15/2025, 2:19 PMlively-winter-88668
12/16/2025, 7:32 AMcreamy-tiger-14805
12/17/2025, 12:48 PMlively-winter-88668
12/17/2025, 2:42 PMrefined-appointment-81829
12/17/2025, 2:58 PMcreamy-tiger-14805
12/17/2025, 7:59 PM