How can we implement AMD for Livekit agent? is the...
# ask-ai
d
How can we implement AMD for Livekit agent? is there is any funcationality in livekit that is used to implement AMD?
t
Thanks for reaching out, Zahra. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you're asking whether LiveKit has AMD (Answering Machine Detection) and how to implement it for a LiveKit agent. I can help — a quick clarification first: Do you want a simple silence-based AMD, an LLM/transcript-based AMD, or a combination? Also tell me which language/runtime you’re using (Node, Python, or another) so I can give a focused example. Sources: sip | livekit/livekit_composite | DeepWiki | How to detect and handle silence during calls | Agents telephony integration | LiveKit docs | examples/bank-ivr | livekit/livekit_composite | DeepWiki
d
I using python, and i want combination AMDs, also guide me which is best for real time seneriors
t
Thanks — I’ll walk you through a practical Python approach that combines silence-based and LLM-based AMD, and recommend what works best for real‑time scenarios. Which part should I deliver first? • A concise architecture + recommended thresholds and tradeoffs for real‑time AMD, or • A runnable Python example (agent loop, silence detector, transcript to LLM check, hangup) you can drop into your agent? Tell me which you prefer and I’ll provide that next.
d
A concise architecture + recommended thresholds and tradeoffs for real‑time AMD also tell how to implement
t
Thanks — I’m responding to your request for a concise architecture, recommended thresholds and tradeoffs for real‑time AMD, plus how to implement it in Python using a combination of silence detection and LLM-based transcript analysis. Architecture (concise) • Outbound call → Agent joins LiveKit room and starts two parallel components: a. Silence monitor: background task tracking last speech timestamp from
user_started_speaking
events. b. LLM transcript analyzer: collects the initial seconds of audio (or a short transcript) and asks an LLM if the answer is a human or voicemail greeting. • Decision logic: silence detection triggers a preliminary “possible voicemail” state after a threshold (recommended 5s). If preliminary triggered, run LLM check on the initial transcript (or first N seconds) to confirm. On confirmation, run voicemail flow (leave message & hang up). If LLM indicates human, continue call. • Hangup: use room disconnect/delete_room to cleanly end the session. Recommended thresholds and tradeoffs (real‑time) • Silence threshold: 5 seconds for initial detection. ◦ Tradeoff: shorter (2–3s) detects faster but increases false positives on humans who pause; longer (8–10s) reduces false positives but delays action and increases cost. • Transcript window: capture first 3–7 seconds of audio for LLM analysis. ◦ Tradeoff: shorter windows are faster and cheaper but may miss clear greeting cues; longer windows improve LLM accuracy but add latency/cost. • LLM confidence policy: require both silence + LLM confidence (or specific keyword patterns like “you’ve reached”, “leave a message”) to confirm voicemail. ◦ Tradeoff: stricter policies reduce false positives but increase missed voicemails. • Retry / grace: allow one short grace period (1–2s) after LLM check to catch late speech before hanging up. • Cost vs accuracy: LLM checks add compute cost and latency. For high-volume real-time systems, prefer a lightweight text-based classifier or keywords plus LLM only on ambiguous cases. How to implement in Python (step-by-step) 1. Instrument speech activity ◦ Listen for
user_started_speaking
(or equivalent event) and update
user_last_spoke_time = time.time()
when fired. 1. Start a background monitor task ◦ Every 0.5–1s check: if
time.time() - user_last_spoke_time >= SILENCE_THRESHOLD
and call is in “post-pickup” phase, mark preliminary voicemail candidate and trigger LLM analysis. 1. Capture initial audio / transcript ◦ Either request a short audio buffer (first 3–7s) or obtain real‑time transcription of initial speech. Send that transcript (or a short summary) to the LLM. 1. LLM analysis ◦ Ask a short prompt: “Is this a voicemail greeting or a live human response? Reply: voicemail/human and confidence score. Highlight phrases that indicate voicemail.” Include transcript text and request brief justification. ◦ If LLM returns “voicemail” with sufficient confidence OR contains voicemail keywords, proceed to voicemail flow. 1. Voicemail flow ◦ Optionally generate/record a short callback message via the agent (use TTS or pre-recorded message). ◦ Wait 0.5–2s to ensure message playback completes. ◦ Call your hangup routine. 1. Hangup routine ◦ Use LiveKit API to disconnect participants / delete the room (e.g.,
ctx.api.room.delete_room(...)
) to end the call cleanly. 1. Safety / logging ◦ Log decisions (silence timestamp, transcript, LLM result, final action) for tuning thresholds and auditing false positives. ◦ Implement metrics for false positive/negative rates, average detection time, and LLM cost. Minimal code pattern (pseudocode outline)
Copy code
SILENCE_THRESHOLD = 5
TRANSCRIPT_WINDOW = 5  # seconds

user_last_spoke_time = time.time()
voicemail_candidate = False

async def on_user_started_speaking(event):
    global user_last_spoke_time, voicemail_candidate
    user_last_spoke_time = time.time()
    voicemail_candidate = False

async def silence_monitor(ctx):
    while ctx.call_active:
        if time.time() - user_last_spoke_time >= SILENCE_THRESHOLD and not voicemail_candidate:
            voicemail_candidate = True
            transcript = await capture_initial_transcript(ctx, seconds=TRANSCRIPT_WINDOW)
            llm_result = await analyze_transcript_with_llm(transcript)
            if is_voicemail(llm_result):
                await leave_message_and_hangup(ctx)
            else:
                voicemail_candidate = False
        await asyncio.sleep(0.5)
Points to tune and test • Start with SILENCE_THRESHOLD = 5s and TRANSCRIPT_WINDOW = 3–5s. Run A/B tests to measure false positives and detection latency. • If traffic is high, only call the LLM for transcripts that contain likely voicemail keywords to reduce cost. • Add a short grace period after LLM-confirmed voicemail detection before hangup to ensure message playback isn't cut off. I can provide: • A runnable Python example that hooks into LiveKit events and an LLM prompt template, or • A ready-to-run snippet showing how to capture audio, get a transcript, call the LLM, and delete the room. Which would you like next? Sources: examples/voice_agents | livekit/livekit_composite | DeepWiki
d
• A runnable Python example (agent loop, silence detector, transcript to LLM check, hangup) you can drop into your agent? also give me this and tell me how can i implement this in my existing livekit agent
t
Thanks — I’ll provide a runnable Python example you can drop into your agent plus concise integration steps. What I’m responding to: your request for a Python agent loop that combines silence detection + transcript→LLM check and then hangs up when a voicemail is confirmed. Prerequisites • Your agent already connects to LiveKit, receives
user_started_speaking
events (or equivalent), can capture/stream audio or receive transcripts, and can call an LLM (or an LLM API wrapper). • You have access to LiveKit server API to delete rooms or disconnect participants. Runnable example (async Python — replace placeholder functions with your app’s implementations)
Copy code
import asyncio
import time
from typing import Optional

SILENCE_THRESHOLD = 5.0       # seconds before we consider voicemail candidate
TRANSCRIPT_WINDOW = 5.0       # seconds of initial audio to transcribe/send to LLM
GRACE_PERIOD = 1.0            # wait before hangup to ensure message playback

# --- Replace these with your implementations ---
async def capture_initial_transcript(ctx, seconds: float) -> str:
    """
    Collect the first `seconds` seconds of audio or transcript from the remote party.
    If you have streaming transcription, grab that buffer; otherwise request a short recording.
    """
    # Example placeholder: return existing buffer or call your transcription service
    return await ctx.get_transcript(seconds)

async def call_llm_classify(transcript: str) -> dict:
    """
    Call your LLM. Return a dict like {"label":"voicemail"/"human", "confidence":0.85, "reason":"matched 'you've reached'"}
    """
    # Placeholder: implement actual HTTP/gRPC call to your LLM and parse result
    return await your_llm_client.classify_voicemail(transcript)

async def hangup_call(ctx):
    """
    Use LiveKit API to end the call / delete room.
    """
    # Example placeholder: call LiveKit admin API to delete the room
    await ctx.api.room.delete_room(room=ctx.room.name)

# --- End replaceable functions ----------------------------------------

class AMDController:
    def __init__(self, ctx):
        self.ctx = ctx
        self.user_last_spoke = time.time()
        self.voicemail_candidate = False
        self._monitor_task: Optional[asyncio.Task] = None
        self.call_active = True

    async def start(self):
        # Start monitor loop
        self._monitor_task = asyncio.create_task(self._silence_monitor())

    async def stop(self):
        self.call_active = False
        if self._monitor_task:
            self._monitor_task.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await self._monitor_task

    async def on_user_started_speaking(self, _event=None):
        # Call this when you receive user_started_speaking event
        self.user_last_spoke = time.time()
        self.voicemail_candidate = False

    async def _silence_monitor(self):
        # Loop checks for silence and triggers LLM classification
        while self.call_active and self.ctx.call_connected:
            elapsed = time.time() - self.user_last_spoke
            if elapsed >= SILENCE_THRESHOLD and not self.voicemail_candidate:
                self.voicemail_candidate = True
                # Capture transcript for the initial window
                transcript = await capture_initial_transcript(self.ctx, TRANSCRIPT_WINDOW)
                # Optional quick keyword heuristic to avoid LLM costs
                if self._keyword_heuristic(transcript):
                    llm_result = {"label": "voicemail", "confidence": 0.99, "reason": "keywords"}
                else:
                    llm_result = await call_llm_classify(transcript)

                if self._is_voicemail(llm_result):
                    # Optionally leave a short message or TTS here before hangup
                    await asyncio.sleep(GRACE_PERIOD)
                    await hangup_call(self.ctx)
                    self.call_active = False
                    return
                else:
                    # false alarm — reset and continue monitoring
                    self.voicemail_candidate = False
                    self.user_last_spoke = time.time()
            await asyncio.sleep(0.5)

    def _keyword_heuristic(self, transcript: str) -> bool:
        if not transcript:
            return False
        lowered = transcript.lower()
        keywords = ["you've reached", "leave a message", "please leave", "no one is available", "voicemail"]
        return any(k in lowered for k in keywords)

    def _is_voicemail(self, llm_result: dict) -> bool:
        label = llm_result.get("label", "").lower()
        confidence = float(llm_result.get("confidence", 0.0))
        # Require silence + LLM label "voicemail" with threshold confidence
        return label == "voicemail" and confidence >= 0.7
How to integrate this into your existing LiveKit agent 1. Hook event handlers ◦ When your agent connects to a room and call is active, create an
AMDController(ctx)
instance and call
await amd.start()
. ◦ Wire your existing
user_started_speaking
(or VAD) event handler to call
await amd.on_user_started_speaking(event)
. ◦ Ensure
ctx.get_transcript(seconds)
or your equivalent transcription call captures the initial audio buffer. 1. Provide implementations for the placeholder functions ◦ `capture_initial_transcript`: if you have streaming STT (e.g., WebSocket or service callbacks), keep a ring buffer of the first N seconds after pickup. Otherwise request a short recorded snippet from your telephony integration. ◦ `call_llm_classify`: implement a small prompt that asks the LLM to return a simple JSON with label/confidence/reason. Example prompt: "Given this transcript, reply with JSON {label: 'voicemail'|'human', confidence:0-1, reason: '...'}. Transcript: '...'" ◦ `hangup_call`: call the LiveKit admin API to delete the room or use your existing call-end logic. 1. Tune thresholds and fallbacks ◦ Start with the constants shown (SILENCE_THRESHOLD=5, TRANSCRIPT_WINDOW=5, confidence >= 0.7). ◦ Add the keyword heuristic first to reduce LLM calls; only call LLM when keywords absent or ambiguous. ◦ Add logging/metrics for decisions so you can iterate. 1. Safety and UX ◦ Add a short grace period (GRACE_PERIOD) before hangup to ensure any agent playback or TTS completes. ◦ Consider playing a short identification message before hanging up so you capture voicemail intent if needed. ◦ Ensure your hangup routine cleans up resources and records the outcome. Testing checklist • Simulate live human (longer back-and-forth) and verify AMD does not trigger. • Simulate voicemail greeting with keywords and verify hangup after detection. • Test edge cases: very short human response, delayed greeting, noisy lines. • Track false-positive and false-negative counts and iterate thresholds. If you want, I can: • Provide a concrete LLM prompt template and example response parser. • Convert the placeholders into a full example using a specific STT or LLM client you use (mention which ones). Which would you like next?