161 lines
5.8 KiB
Python
161 lines
5.8 KiB
Python
import logging
|
|
import os
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
from livekit.agents import (
|
|
Agent,
|
|
AgentServer,
|
|
AgentSession,
|
|
JobContext,
|
|
cli,
|
|
room_io,
|
|
)
|
|
from livekit.agents.beta.workflows import WarmTransferTask
|
|
from livekit.agents.llm import ToolError, function_tool
|
|
from livekit.plugins import noise_cancellation
|
|
|
|
logger = logging.getLogger("warm-transfer")
|
|
|
|
load_dotenv()
|
|
|
|
# ensure the following variables/env vars are set
|
|
SIP_TRUNK_ID = os.getenv("LIVEKIT_SIP_OUTBOUND_TRUNK") # "ST_abcxyz"
|
|
SUPERVISOR_PHONE_NUMBER = os.getenv("LIVEKIT_SUPERVISOR_PHONE_NUMBER") # "+12003004000"
|
|
SIP_NUMBER = os.getenv("LIVEKIT_SIP_NUMBER") # "+15005006000" - caller ID shown to supervisor
|
|
|
|
|
|
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:
|
|
assert SIP_TRUNK_ID is not None
|
|
assert SUPERVISOR_PHONE_NUMBER is not None
|
|
|
|
result = await WarmTransferTask(
|
|
sip_call_to=SUPERVISOR_PHONE_NUMBER,
|
|
sip_trunk_id=SIP_TRUNK_ID,
|
|
sip_number=SIP_NUMBER,
|
|
chat_ctx=self.chat_ctx,
|
|
# to reach an extension behind an IVR, pass DTMF tones to send once
|
|
# answered, e.g. dtmf="wwww1234#" (each `w` pauses ~0.5s):
|
|
# dtmf=SUPERVISOR_EXTENSION,
|
|
# give up if the supervisor doesn't pick up within 25s:
|
|
# ringing_timeout=25,
|
|
# add extra instructions for summarization
|
|
# you can also customize the entire instructions by overriding the `get_instructions` method
|
|
extra_instructions=SUMMARY_INSTRUCTIONS,
|
|
)
|
|
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={"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()
|
|
|
|
|
|
server = AgentServer()
|
|
|
|
|
|
@server.rtc_session(agent_name="sip-inbound")
|
|
async def entrypoint(ctx: JobContext):
|
|
session = AgentSession(
|
|
llm="openai/gpt-4.1-mini",
|
|
stt="deepgram/nova-3:en",
|
|
tts="cartesia/sonic-3:9626c31c-bec5-4cca-baa8-f8ba9e84c8bc",
|
|
)
|
|
|
|
support_agent = SupportAgent()
|
|
|
|
await session.start(
|
|
agent=support_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
|
|
),
|
|
)
|
|
|
|
|
|
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"""
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 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)
|