Async answering machine detection using LLM

Overview

This module provides asynchronous answering machine detection (AMD) capabilities using a Large Language Model. It can detect and classify responses into four categories:

  1. Human – Spoken by a real person
  2. IVR – Automated menu systems
  3. AMD – Generic automated announcements
  4. Voicemail – Messages asking to leave a voicemail

The implementation uses an async block pattern to process incoming audio without blocking the main conversation flow.

How It Works

  1. The library initializes an async child block to handle the classification
  2. Conversation audio is continuously analyzed for potential IVR/AMD patterns
  3. When potential patterns are detected, audio is sent to the LLM for classification
  4. Results are processed asynchronously, allowing the conversation to continue uninterrupted
  5. When IVR/AMD/Voicemail is detected, the system can automatically exit or take custom actions

Usage

Create a file asyncamd.dsl with the following content and import it in your main DSL file:

library context { // Output variables to track detection results output ivrDetected: boolean = false; output ivrDetectedHistory: string = ""; output ivrDetectedReply: string = ""; // Configuration variables ivrChildBlock: string = ""; lastIvrHistory: string = ""; input exitOnIVR: boolean = true; // LLM prompt for classification amdPrompt: string = `Given the conversation history as {{history}}, focus ONLY on the callee's very first response after the caller's initial statement or question. Categorize this single reply into ONEand only oneof the following four types: 1. HumanSpoken by a person, showing interactive intent (e.g., questions, answers, small talk, or personalized remarks relevant to the request). 2. IVRAutomated menu options/instructions for input (e.g., "Press 1 for sales. Press 2 for support," or "Say the department you want"). 3. AMDA generic automated or pre-recorded announcement about status or availability (e.g., "office is closed," "no one is available," or "please try again later") that does NOT mention leaving a message. 4. VoicemailAny message (personal or automated) that mentions, asks, or instructs you to leave a voicemail (e.g., "leave a message after the tone" or "record your message"). Review ONLY the first callee reply, disregarding any conversation that follows. Return EXACTLY one of these four words as your answer: Human, IVR, AMD, or Voicemail.`; } // Initialize the async child block when starting do { set $ivrChildBlock = (blockcall ivr_capturer($amdPrompt)).id; } // Analyze potential IVR/AMD speech patterns digression potential_ivr { conditions { on #getVisitCount("ivr_full_sentence") < 3 tags: onpotential; } do { var text = #getMessageTextV2().text; $.RequestIvrUpdate(text); return; } } // Process complete sentences for IVR/AMD detection preprocessor digression ivr_full_sentence { conditions { on #getVisitCount("ivr_full_sentence") < 3; } do { var text = #getMessageTextV2().text; $.RequestIvrUpdate(text, true); return; } } // Helper function to request IVR/AMD classification function RequestIvrUpdate(history: string, force: boolean = false): boolean { // Skip duplicate requests if (history == $this.lastIvrHistory) { return false; } // Optimization: only process substantial utterances unless forced if (!force) { if (history.length() < 30) { // Skip short sentences return false; } if (history.startsWith($this.lastIvrHistory) && history.length() - $this.lastIvrHistory.length() < 10) { // Skip minor updates return false; } } set $this.lastIvrHistory = history; var childId = $this.ivrChildBlock; var fullHistory = #getFormattedTranscription({history_length: 4}); if (!fullHistory.includes(history)) { set fullHistory = fullHistory + "\nhuman: " + history; } var request = { history: fullHistory }; #sendMessageToAsyncBlock(childId, "Content", request); return true; } // Define response message type type AsyncIvrResponse = { messageType: "Content"; sourceRouteId: string; targetRouteId: string; content: { isIvr: boolean; history: string; reply: string; }; }; // Handle classification results from the async block when blockMessage handle_ivr_update do { if (#isBlockMessage("Content") && (#getAsyncBlockMessage() as { sourceRouteId: string; })?.sourceRouteId == $ivrChildBlock) { var message = #getAsyncBlockMessage() as AsyncIvrResponse; if (message is null) { return; } var content = message.content; set $ivrDetected = $ivrDetected || content.isIvr; if ($ivrDetected) { set $ivrDetectedHistory = content.history; set $ivrDetectedReply = content.reply; #log("Detected IVR, exiting"); if ($exitOnIVR) { exit; } } return; } } // Async block for LLM-based classification async block ivr_capturer(amdPrompt: string) { type AsyncIvrRequest = { messageType: "Content"; sourceRouteId: string; targetRouteId: string; content: { history: string; }; }; digression @exit_dig { conditions { on true tags: onclosed; } do { exit; } } start node root { do { wait *; } } digression exit_when_parent_block_exit { conditions { on #isBlockMessage("Terminated") tags: onblock; } do { exit; } } digression handle_block_content_message { conditions { on #isBlockMessage("Content") tags: onblock; } do { var message = #getAsyncBlockMessage() as AsyncIvrRequest; if (message is null) { return; } var content = message.content; var history = content.history; // Define JSON Schema for structured output var schema = { name: "result", strict: true, description: "The result of the AMD detection", schema: { @type: "object", properties: { result: { @type: "string", description: "The result of the AMD detection", enum: ["Human", "IVR", "AMD", "Voicemail"] } }, additionalProperties: false, required: ["result"] } }.toString(); // Call LLM with structured output format var ask = #askGPT($amdPrompt, { model: "openai/gpt-4.1-nano", function_call: "none", history_length: 0, save_response_in_history: false, response_format: schema }, promptName: "ivr_capturer"); // Parse and process the result var response = (ask.responseText.parseJSON() as { result: string; })?.result ?? "Human"; if (response == "IVR" || response == "AMD" || response == "Voicemail") { #sendMessageToAsyncBlock(message.sourceRouteId, "Content", { isIvr: true, history: history, reply: ask.responseText } ); } return; } } }

Configuration Options

You can customize the behavior by setting these parameters:

  • exitOnIVR - Set to false if you want to continue the conversation after detection
  • amdPrompt - Modify the prompt to customize the detection logic

Troubleshooting

If detection is not working as expected:

  1. Check the logs for classification results
  2. Review the ivrDetectedReply variable to see exact LLM responses
  3. Adjust the prompt to better match your use case
  4. Try different LLM models for improved performance
Found a mistake? Let us know.

Enroll in beta

Request invite to our private Beta program for developers to join the waitlist. No spam, we promise.