Files
2026-07-13 13:39:38 +08:00

142 lines
4.5 KiB
Python

import logging
import os
from dotenv import load_dotenv
from livekit import api, rtc
from livekit.agents import (
AMD,
NOT_GIVEN,
Agent,
AgentServer,
AgentSession,
JobContext,
cli,
inference,
)
logger = logging.getLogger("basic-agent")
load_dotenv()
class MyAgent(Agent):
def __init__(self) -> None:
super().__init__(
instructions=(
"You are reaching out to a customer with a phone call. You might encounter voice mail prompt or IVR systems. The goal is to reach to a human."
),
)
server = AgentServer()
@server.rtc_session()
async def entrypoint(ctx: JobContext):
ctx.log_context_fields = {
"room": ctx.room.name,
}
session = AgentSession(
stt=inference.STT("deepgram/nova-3", language="multi"),
llm=inference.LLM("openai/gpt-4.1-mini"),
tts=inference.TTS("cartesia/sonic-3", voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc"),
preemptive_generation=True,
)
await session.start(
agent=MyAgent(),
room=ctx.room,
)
phone_number = os.getenv("SIP_PHONE_NUMBER")
participant_identity = os.getenv("SIP_PARTICIPANT_IDENTITY")
outbound_trunk_id = os.getenv("SIP_OUTBOUND_TRUNK_ID")
# focus the session on the callee before AMD starts so audio recognition
# doesn't push frames from any pre-existing participant into AMD's pipeline
if not session.room_io:
raise RuntimeError(
"session room_io is unavailable. Make sure you use dev or start commands"
)
if participant_identity:
session.room_io.set_participant(participant_identity)
async with AMD(
session,
participant_identity=participant_identity or NOT_GIVEN,
) as detector:
# start running amd before the SIP participant joins to avoid audio loss
if phone_number and outbound_trunk_id and participant_identity:
logger.info(f"creating SIP participant for {participant_identity}")
await ctx.api.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name=ctx.room.name,
sip_trunk_id=outbound_trunk_id,
sip_call_to=phone_number,
participant_identity=participant_identity,
wait_until_answered=True,
)
)
participant = await ctx.wait_for_participant(identity=participant_identity)
logger.info(
"participant joined",
extra={
"actual_identity": participant.identity,
"expected_identity": participant_identity,
"kind": participant.kind,
"audio_tracks_subscribed": [
pub.sid
for pub in participant.track_publications.values()
if pub.subscribed and pub.kind == rtc.TrackKind.KIND_AUDIO
],
},
)
result = await detector.execute()
logger.info(f"AMD result: {result}")
if result.category == "human" or result.category == "uncertain":
logger.info(
"human answered the call or amd is uncertain, proceeding with normal conversation",
extra={"transcript": result.transcript},
)
elif result.category == "machine-ivr":
logger.info(
"ivr menu detected, starting navigation",
extra={"transcript": result.transcript},
)
elif result.category == "machine-vm":
logger.info(
"voicemail detected, leaving a message",
extra={"transcript": result.transcript},
)
speech_handle = session.generate_reply(
instructions=(
"You've reached voicemail. Leave a brief message asking "
"the customer to call back."
),
)
await speech_handle.wait_for_playout()
ctx.shutdown("voicemail detected")
elif result.category == "machine-unavailable":
logger.info("mailbox unavailable, ending call", extra={"transcript": result.transcript})
ctx.shutdown("mailbox unavailable")
async def hangup():
await ctx.api.room.delete_room(
api.DeleteRoomRequest(
room=ctx.room.name,
)
)
ctx.add_shutdown_callback(hangup)
if __name__ == "__main__":
cli.run_app(server)