159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
import logging
|
|
import os
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
from livekit.agents import (
|
|
Agent,
|
|
AgentSession,
|
|
JobContext,
|
|
MetricsCollectedEvent,
|
|
cli,
|
|
inference,
|
|
metrics,
|
|
)
|
|
from livekit.agents.beta.workflows.dtmf_inputs import (
|
|
GetDtmfTask,
|
|
)
|
|
from livekit.agents.llm.tool_context import ToolError, function_tool
|
|
from livekit.agents.voice.events import RunContext
|
|
from livekit.agents.worker import AgentServer
|
|
|
|
logger = logging.getLogger("dtmf-agent")
|
|
|
|
load_dotenv()
|
|
|
|
|
|
DTMF_AGENT_DISPATCH_NAME = os.getenv("DTMF_AGENT_DISPATCH_NAME", "my-telephony-agent")
|
|
|
|
server = AgentServer()
|
|
|
|
|
|
class DtmfAgent(Agent):
|
|
def __init__(self) -> None:
|
|
super().__init__(
|
|
instructions=(
|
|
"You are Horizon Wireless's automated support assistant speaking with a caller over the phone. "
|
|
"Open with a brief, friendly greeting that mentions Horizon Wireless. "
|
|
"Explain that you'll first confirm the caller's account phone number by using the `ask_for_phone_number` tool. "
|
|
"After the phone number is confirmed, guide the caller through picking a support option with the `ask_for_service_options` tool. "
|
|
"Keep the tone professional, concise, and smoothly transition between steps."
|
|
),
|
|
)
|
|
|
|
self.phone_number: str | None = None
|
|
|
|
async def on_enter(self) -> None:
|
|
self.session.generate_reply(
|
|
instructions=(
|
|
"Greet the caller as Horizon Wireless's virtual assistant, briefly describe your role, "
|
|
"and let them know you'll collect their 10-digit phone number now."
|
|
)
|
|
)
|
|
|
|
@function_tool
|
|
async def ask_for_service_options(self, context: RunContext) -> str:
|
|
"""Ask user to which telephone service they want to use."""
|
|
|
|
if self.phone_number is None:
|
|
raise ToolError(
|
|
"Phone number not provided, you should ask for it via `ask_for_phone_number` tool"
|
|
)
|
|
|
|
while True:
|
|
try:
|
|
result = await GetDtmfTask(
|
|
num_digits=1,
|
|
chat_ctx=self.chat_ctx.copy(
|
|
exclude_instructions=True,
|
|
exclude_function_call=True,
|
|
exclude_handoff=True,
|
|
exclude_config_update=True,
|
|
),
|
|
extra_instructions=(
|
|
"Let the caller know they can choose one of three Horizon Wireless services: "
|
|
"press 1 to hear details about their current plan, press 2 to enable international data roaming, "
|
|
"or press 3 to explore upgrade options. Prompt them for a single digit and give them a moment to respond."
|
|
),
|
|
repeat_instructions=2,
|
|
)
|
|
except ToolError as e:
|
|
await self.session.generate_reply(instructions=e.message, allow_interruptions=False)
|
|
continue
|
|
|
|
if result.user_input == "1":
|
|
return "Your current plan is $100 per month"
|
|
elif result.user_input == "2":
|
|
return "International data roaming is enabled"
|
|
elif result.user_input == "3":
|
|
return "Your new plan is $150 per month"
|
|
|
|
await self.session.generate_reply(
|
|
instructions=(
|
|
"Apologize that the selection wasn't recognized, then remind the caller to enter 1 for plan details, "
|
|
"2 for international roaming, or 3 for upgrades."
|
|
),
|
|
allow_interruptions=False,
|
|
)
|
|
|
|
@function_tool
|
|
async def ask_for_phone_number(self, context: RunContext) -> str:
|
|
"""Ask user to provide a phone number."""
|
|
while True:
|
|
try:
|
|
result = await GetDtmfTask(
|
|
num_digits=10,
|
|
chat_ctx=self.chat_ctx.copy(
|
|
exclude_instructions=True,
|
|
exclude_function_call=True,
|
|
exclude_handoff=True,
|
|
exclude_config_update=True,
|
|
),
|
|
ask_for_confirmation=True,
|
|
extra_instructions=(
|
|
"Let the caller know you'll record their 10-digit account number and that they can speak or dial it. "
|
|
"Provide an example such as 415 555 0199, remind them you're keeping their information secure, "
|
|
"then capture the digits. Read the number back in grouped segments for confirmation and invite them to confirm or re-enter."
|
|
),
|
|
repeat_instructions=2,
|
|
)
|
|
except ToolError as e:
|
|
await self.session.generate_reply(instructions=e.message, allow_interruptions=False)
|
|
continue
|
|
|
|
break
|
|
|
|
self.phone_number = result.user_input
|
|
return f"User's phone number is {result.user_input}"
|
|
|
|
|
|
@server.rtc_session(agent_name=DTMF_AGENT_DISPATCH_NAME)
|
|
async def entrypoint(ctx: JobContext) -> None:
|
|
ctx.log_context_fields = {
|
|
"room": ctx.room.name,
|
|
}
|
|
|
|
session: AgentSession = AgentSession(
|
|
llm=inference.LLM("openai/gpt-4.1-mini"),
|
|
stt=inference.STT("deepgram/nova-3"),
|
|
tts=inference.TTS("inworld/inworld-tts-1"),
|
|
)
|
|
|
|
@session.on("metrics_collected")
|
|
def _on_metrics_collected(ev: MetricsCollectedEvent) -> None:
|
|
metrics.log_metrics(ev.metrics)
|
|
|
|
async def log_usage() -> None:
|
|
logger.info(f"Usage: {session.usage}")
|
|
|
|
ctx.add_shutdown_callback(log_usage)
|
|
|
|
await session.start(
|
|
agent=DtmfAgent(),
|
|
room=ctx.room,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli.run_app(server)
|