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)