-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat: twilio connector warm transfer #6283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
7897c11
feat: twilio connector warm transfer
anunaym14 89a7f05
fix: devin review comments
anunaym14 8486913
error on missing twilio package
anunaym14 d2c0cca
Update livekit-agents/livekit/agents/beta/workflows/warm_transfer.py
anunaym14 bcad3c9
Merge remote-tracking branch 'origin/main' into am/twilio-warm-transfer
anunaym14 74e2c4a
fix: validate Twilio credentials in TwilioConnectorWarmTransferTask
anunaym14 16f4c2a
fix: mark supervisor identity log field as pii
anunaym14 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import logging | ||
| from collections.abc import Callable | ||
|
|
||
| from livekit.agents import ( | ||
| Agent, | ||
| AgentServer, | ||
| AgentSession, | ||
| JobContext, | ||
| cli, | ||
| room_io, | ||
| ) | ||
| from livekit.agents.beta.workflows import WarmTransferResult | ||
| from livekit.agents.llm import ToolError, function_tool | ||
| from livekit.plugins import noise_cancellation | ||
|
|
||
| logger = logging.getLogger("warm-transfer") | ||
|
|
||
|
|
||
| class SupportAgent(Agent): | ||
| def __init__(self) -> None: | ||
| super().__init__(instructions=INSTRUCTIONS) | ||
|
|
||
| async def on_enter(self): | ||
| self.session.generate_reply() | ||
|
|
||
| @function_tool | ||
| async def transfer_to_human(self) -> None: | ||
| """Called when the user asks to speak to a human agent. This will put the user on | ||
| hold while the supervisor is connected. | ||
|
|
||
| Ensure that the user has confirmed that they wanted to be transferred. Do not start transfer | ||
| until the user has confirmed. | ||
| Examples on when the tool should be called: | ||
| ---- | ||
| - User: Can I speak to your supervisor? | ||
| - Assistant: Yes of course. | ||
| ---- | ||
| - Assistant: I'm unable to help with that, would you like to speak to a human agent? | ||
| - User: Yes please. | ||
| ---- | ||
| """ | ||
|
|
||
| logger.info("tool called to transfer to human") | ||
| await self.session.say( | ||
| "Please hold while I connect you to a human agent.", allow_interruptions=False | ||
| ) | ||
| try: | ||
| result = await self._start_transfer() | ||
| except ToolError as e: | ||
| logger.error(f"failed to transfer to supervisor with tool error: {e}") | ||
| raise e | ||
| except Exception as e: | ||
| logger.exception("failed to transfer to supervisor") | ||
| raise ToolError(f"failed to transfer to supervisor with error: {e}") from e | ||
|
|
||
| logger.info( | ||
| "transfer to supervisor successful", | ||
| extra={"lk.pii.supervisor_identity": result.human_agent_identity}, | ||
| ) | ||
| await self.session.say( | ||
| "you are on the line with my supervisor. I'll be hanging up now.", | ||
| allow_interruptions=False, | ||
| ) | ||
| self.session.shutdown() | ||
|
|
||
| # implemented per transport (SIP, Twilio connector, ...) | ||
| async def _start_transfer(self) -> WarmTransferResult: | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| def run(create_agent: Callable[[], Agent]) -> None: | ||
| server = AgentServer() | ||
|
|
||
| @server.rtc_session(agent_name="sip-inbound") | ||
| async def entrypoint(ctx: JobContext) -> None: | ||
| session = AgentSession( | ||
| llm="openai/gpt-4.1-mini", | ||
| stt="deepgram/nova-3:en", | ||
| tts="cartesia/sonic-3:9626c31c-bec5-4cca-baa8-f8ba9e84c8bc", | ||
| ) | ||
| await session.start( | ||
| agent=create_agent(), | ||
| room=ctx.room, | ||
| room_options=room_io.RoomOptions( | ||
| audio_input=room_io.AudioInputOptions( | ||
| # enable Krisp BVC noise cancellation | ||
| noise_cancellation=noise_cancellation.BVCTelephony(), | ||
| ), | ||
| delete_room_on_close=False, # keep the room open for the customer and supervisor | ||
| ), | ||
| ) | ||
|
|
||
| # this example requires explicit dispatch using named agents | ||
| # supervisor will be placed in a separate room, and we do not want it to dispatch the default agent | ||
| cli.run_app(server) | ||
|
|
||
|
|
||
| INSTRUCTIONS = """ | ||
| # Personality | ||
|
|
||
| You are friendly and helpful, with a welcoming personality | ||
| You're naturally curious, empathetic, and intuitive, always aiming to deeply understand the user's intent by actively listening. | ||
|
|
||
| # Environment | ||
|
|
||
| You are engaged in a live, spoken dialogue over the phone. | ||
| There are no other ways of communication with the user (no chat, text, visual, etc) | ||
|
|
||
| # Tone | ||
|
|
||
| Your responses are warm, measured, and supportive, typically 1-2 sentences to maintain a comfortable pace. | ||
| You speak with gentle, thoughtful pacing, using pauses (marked by "...") when appropriate to let emotional moments breathe. | ||
| You naturally include subtle conversational elements like "Hmm," "I see," and occasional rephrasing to sound authentic. | ||
| You actively acknowledge feelings ("That sounds really difficult...") and check in regularly ("How does that resonate with you?"). | ||
| You vary your tone to match the user's emotional state, becoming calmer and more deliberate when they express distress. | ||
|
|
||
| # Identity | ||
|
|
||
| You are a customer support agent for LiveKit. | ||
|
|
||
| # Transferring to a human | ||
|
|
||
| In some cases, the user may ask to speak to a human agent. This could happen when you are unable to answer their question. | ||
| When such is requested, you would always confirm with the user before initiating the transfer. | ||
| """ | ||
|
|
||
|
|
||
| SUMMARY_INSTRUCTIONS = """ | ||
| Introduce the conversation from your perspective as the AI assistant who participated in this call: | ||
|
|
||
| WHO you're talking to (name, role, company if mentioned) | ||
| WHY they contacted you (goal, problem, request) | ||
| WHY a human agent is requested or needed at this point | ||
| Brief summary in 100-200 characters from a first-person perspective""" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import os | ||
|
|
||
| from dotenv import load_dotenv | ||
| from support_agent import SUMMARY_INSTRUCTIONS, SupportAgent, run | ||
|
|
||
| from livekit.agents.beta.workflows import TwilioConnectorWarmTransferTask, WarmTransferResult | ||
|
|
||
| load_dotenv() | ||
|
|
||
| # LiveKit credentials (read from env by the SDK): LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET | ||
| # Twilio REST credentials + caller ID: | ||
| TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID") # "ACxxxx..." | ||
| TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN") # "xxxx..." | ||
| TWILIO_FROM_NUMBER = os.getenv( | ||
| "TWILIO_FROM_NUMBER" | ||
| ) # "+15005006000" - your Twilio number, shown to supervisor | ||
| SUPERVISOR_PHONE_NUMBER = os.getenv("LIVEKIT_SUPERVISOR_PHONE_NUMBER") # "+12003004000" | ||
| # This example places the outbound call with the Twilio REST SDK (pip install twilio): | ||
| # connect_twilio_call returns a connect_url that we hand to Twilio in <Stream url=...> | ||
| # TwiML; Twilio then streams the supervisor's call audio back to the connector. | ||
|
|
||
|
|
||
| class TwilioSupportAgent(SupportAgent): | ||
| async def _start_transfer(self) -> WarmTransferResult: | ||
| assert SUPERVISOR_PHONE_NUMBER is not None | ||
| assert TWILIO_FROM_NUMBER is not None | ||
| return await TwilioConnectorWarmTransferTask( | ||
| SUPERVISOR_PHONE_NUMBER, | ||
| twilio_from_number=TWILIO_FROM_NUMBER, | ||
| twilio_account_sid=TWILIO_ACCOUNT_SID, | ||
| twilio_auth_token=TWILIO_AUTH_TOKEN, | ||
| chat_ctx=self.chat_ctx, | ||
| # give up if the supervisor doesn't pick up within 25s: | ||
| # ringing_timeout=25, | ||
| # add extra instructions for summarization | ||
| extra_instructions=SUMMARY_INSTRUCTIONS, | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| run(TwilioSupportAgent) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.