← Back to blog

Asterisk + AI via ARI: how to build a virtual voice assistant for business telephony

Asterisk + AI via ARI: how to build a virtual voice assistant for business telephony

Artificial intelligence is no longer just a chatbot on a website. Today, it can be quite easily integrated with a PBX to create a voice assistant that answers calls, communicates with callers using natural speech, works with a knowledge base, and transfers the call to a human when needed.

If you use Asterisk, there are several ways to build such a solution. One of the most interesting is connecting through ARI (Asterisk REST Interface), which gives developers significantly more control over the call than a traditional dialplan. Combined with a Node.js application, you can create a modern AI voicebot solution without having to build the entire telephony logic from scratch.

Why connect Asterisk with AI at all

A typical business scenario today looks straightforward: a customer calls the main number, the AI assistant greets them, identifies the reason for the call, answers basic questions, and if needed, transfers the call to the right department or specific extension.

Such an assistant can, for example:

  • answer frequently asked questions
  • handle calls outside business hours
  • provide information from a knowledge base
  • collect information from the caller before transferring
  • route calls based on conversation content
  • help an overloaded call center

The advantage is that this is no longer the old rigid IVR of the “press 1” variety. A modern AI voice assistant can communicate naturally, understand context, and be tailored to a specific company, service, or industry.

What are the integration options

When integrating AI into Asterisk, there are two main approaches in practice.

1. Connecting via SIP trunk to an external AI platform

The first option is to use an external voice AI service that connects to Asterisk as a SIP peer. Typically, this could be a connection to a cloud platform that handles speech recognition, voice synthesis, and dialog logic.

The advantage is relatively quick deployment. The disadvantage is usually less control over the call flow, higher dependency on a specific provider, and often more complex customization to the company’s specific processes.

This model makes sense where the priority is a quick start with less need for deeper development.

2. ARI + custom Node.js application

The second, technically more interesting path, is to use ARI. In this case, Asterisk hands the call to your application, which controls it via REST and WebSocket interfaces. In the Node.js application, you can then handle your own logic, communication with the AI model, connections to databases, CRM, and knowledge bases.

This solution brings key advantages:

  • full control over the call lifecycle
  • ability to precisely define the assistant’s role
  • easy integration with internal systems
  • flexible handling of transfers, queues, and fallback scenarios
  • ability to combine multiple AI services as needed

This is where Asterisk becomes a very strong foundation for developing your own AI voicebot.

To make ARI work, you need to enable the REST interface in Asterisk and create a user:

ini ari.conf
[general]
enabled = yes
pretty = yes
allowed_origins = *

[ai-demo-user]
type = user
password = tajne-heslo
read_only = no

And in the dialplan, hand the call over to the Stasis application:

ini extensions.conf
[ai-demo]
exten => _[+0-9A-Za-Z]!,1,NoOp(AI asistent)
same => n,Answer()
same => n,Wait(0.5)
same => n,Stasis(asterisk-ai)
same => n,Hangup()

How it works in practice

The architecture can be surprisingly straightforward.

  1. A call arrives in Asterisk.
  2. Asterisk hands the channel to the ARI application.
  3. The Node.js application starts controlling the call.
  4. Audio is sent to the AI layer based on the chosen design.
  5. The AI returns a response, which is converted to speech and played to the caller.

Based on the result, the application can:

  • continue the dialog
  • look up information in the knowledge base
  • trigger an action
  • or transfer the call to an operator

From the business perspective, it’s important that the assistant doesn’t have to be just a “talking FAQ.” If you give it the right role and data, it can function as a receptionist, helpdesk operator, order assistant, or first-level customer support.

The following Node.js application shows a complete implementation — from accepting a call via ARI, creating an ExternalMedia channel for RTP audio, to connecting to the AI Realtime API via WebSocket:

javascript asterisk-ai-bridge.js
"use strict";
require("dotenv").config();

const ari    = require("ari-client");
const WebSocket = require("ws");
const dgram  = require("dgram");
const fs     = require("fs");
const path   = require("path");

// ─── Konfigurace ────────────────────────────────────────────────────────────
const ts = () => new Date().toISOString().slice(11,23);

const CFG = {
ariUrl:        process.env.ARI_URL        || "http://127.0.0.1:8088",
ariUser:       process.env.ARI_USER       || "ai-demo-user",
ariPass:       process.env.ARI_PASS       || "tajne-heslo",
rtpPort:       parseInt(process.env.RTP_PORT || "12000"),
bridgeHost:    process.env.BRIDGE_HOST    || "127.0.0.1",
openaiKey:     process.env.OPENAI_API_KEY || "",
transcriptDir: process.env.TRANSCRIPT_DIR || "/cesta-k-transkriptum",
openaiModel:   "gpt-4o-realtime-preview",
openaiVoice:   "alloy",       // alloy | echo | fable | onyx | nova | shimmer
transferExt:   process.env.TRANSFER_EXT || "100",  // extenze pro přepojení
};

// Asterisk slin16 = 16 kHz, OpenAI pcm16 = 24 kHz  → resample poměr 3:2
const ASTERISK_SAMPLE_RATE = 8000;  // alaw @ 8 kHz
const OPENAI_SAMPLE_RATE   = 8000;  // g711_alaw @ 8 kHz
const FRAME_DURATION_MS    = 20;   // 20ms RTP frames
const ASTERISK_FRAME_SAMPLES = ASTERISK_SAMPLE_RATE * FRAME_DURATION_MS / 1000;
const OPENAI_FRAME_SAMPLES   = OPENAI_SAMPLE_RATE  * FRAME_DURATION_MS / 1000;
const OPENAI_CHUNK_SAMPLES   = Math.floor(OPENAI_FRAME_SAMPLES / 3) * 3;
const OPENAI_CHUNK_BYTES     = OPENAI_CHUNK_SAMPLES * 2;

// RTP header je 12 bytů
const RTP_HEADER_SIZE = 12;

// ─── Instrukce pro AI ────────────────────────────────────────────────────────
const AI_INSTRUCTIONS = `
Jsi hlasový asistent ve společnosti XYZ. Mluvíš VÝHRADNĚ česky.

Chování:
- Přivítej volajícího: "Dobrý den, dovolali jste se do společnosti XYZ, s čím vám mohu pomoci?"
- Odpovídej stručně, max 2-3 věty. Nezaplňuj ticho.
- Jsi přátelský, profesionální, věcný.
- Pokud se tě ptají na produkt, popis je níže.
- Když se tě zeptají kdo jsi, řekni: "Jsem AI asistent určen výhradně pro === DOPLŇTE ÚČEL ===."

O společnosti XYZ:
- PÁR VĚT O SPOLEČNOSTI

O našich produktech:
- PÁR VĚT O PRODUKTECH

Pokud volající chce mluvit s člověkem nebo nevíš odpověď:
- Řekni: "Přepojím vás na kolegu, okamžik prosím." a ukonči odpověď.
`.trim();

// ─── Resample: 16kHz → 24kHz (upsample 2:3 s lineární interpolací) ──────────
function resample16to24(buf16) {
const srcSamples = buf16.length / 2;
const groups = Math.floor(srcSamples / 2);
const dstSamples = groups * 3;
const out = Buffer.alloc(dstSamples * 2);
let outIdx = 0;
for (let g = 0; g < groups; g++) {
  const s0 = buf16.readInt16LE(g * 2 * 2);
  const s1 = buf16.readInt16LE((g * 2 + 1) * 2);
  out.writeInt16LE(s0, outIdx * 2);
  out.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(s0 + (s1 - s0) / 3))), (outIdx + 1) * 2);
  out.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(s0 + (s1 - s0) * 2 / 3))), (outIdx + 2) * 2);
  outIdx += 3;
}
return out;
}

// ─── Resample: 24kHz → 16kHz (decimace 3:2 s anti-aliasing) ─────────────────
function resample24to16(buf24) {
const srcSamples = buf24.length / 2;
const groups = Math.floor(srcSamples / 3);
const dstSamples = groups * 2;
const out = Buffer.alloc(dstSamples * 2);
let outIdx = 0;
for (let g = 0; g < groups; g++) {
  const i = g * 3;
  const s0 = buf24.readInt16LE(i * 2);
  const s1 = buf24.readInt16LE((i + 1) * 2);
  const s2 = buf24.readInt16LE((i + 2) * 2);
  const o0 = Math.round((s0 * 2 + s1) / 3);
  const o1 = Math.round((s1 + s2 * 2) / 3);
  out.writeInt16LE(Math.max(-32768, Math.min(32767, o0)), outIdx * 2);
  out.writeInt16LE(Math.max(-32768, Math.min(32767, o1)), (outIdx + 1) * 2);
  outIdx += 2;
}
return out;
}

// ─── RTP packet builder ───────────────────────────────────────────────────────
function buildRtpPacket(payload, seqNum, timestamp, ssrc) {
const header = Buffer.alloc(RTP_HEADER_SIZE);
header.writeUInt8(0x80, 0);         // V=2, P=0, X=0, CC=0
header.writeUInt8(8, 1);            // M=0, PT=8 (G.711 alaw)
header.writeUInt16BE(seqNum & 0xFFFF, 2);
header.writeUInt32BE(timestamp >>> 0, 4);
header.writeUInt32BE(ssrc >>> 0, 8);
return Buffer.concat([header, payload]);
}

// ─── Transcript helper ────────────────────────────────────────────────────────
function saveTranscript(callId, entries) {
try {
  fs.mkdirSync(CFG.transcriptDir, { recursive: true });
  const date = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
  const fname = path.join(CFG.transcriptDir, `${date}_${callId}.txt`);
  const lines = [
    `Hovor: ${callId}`,
    `Datum: ${new Date().toLocaleString("cs-CZ")}`,
    `${"─".repeat(60)}`,
    ...entries.map(e => `[${e.role.toUpperCase()}] ${e.text}`),
    `${"─".repeat(60)}`,
  ];
  fs.writeFileSync(fname, lines.join("\n"), "utf8");
  console.log(`[transcript] Uložen: ${fname}`);
} catch (err) {
  console.error("[transcript] Chyba při ukládání:", err.message);
}
}

// ─── Hlavní session pro jeden hovor ──────────────────────────────────────────
async function handleCall(ariClient, channel) {
const callId = channel.id.slice(0, 8);
console.log(`\n[${callId}] Nový hovor: ${channel.caller?.number || "unknown"}`);

const transcript = [];

// ── 1. Odpověz na hovor ──────────────────────────────────────────────────
await channel.answer();
console.log(`[${callId}] Hovor přijat`);

// ── 2. Otevři UDP socket pro příjem RTP z Asterisku ──────────────────────
const udpSocket = dgram.createSocket("udp4");
await new Promise((res, rej) => {
  udpSocket.bind(CFG.rtpPort, "0.0.0.0", err => err ? rej(err) : res());
});
console.log(`[${callId}] UDP RTP socket naslouchá na portu ${CFG.rtpPort}`);

// ── 3. Vytvoř ExternalMedia kanál v Asterisku ────────────────────────────
let extChannel;
try {
  extChannel = await ariClient.channels.externalMedia({
    app: "asterisk-ai",
    external_host: `${CFG.bridgeHost}:${CFG.rtpPort}`,
    format: "alaw",
    direction: "both",
  });
} catch (err) {
  console.error(`[${callId}] ExternalMedia selhalo:`, err.message);
  udpSocket.close();
  await channel.hangup().catch(() => {});
  return;
}
console.log(`[${callId}] ExternalMedia kanál: ${extChannel.id}`);

let asteriskRtpHost = extChannel.channelvars?.UNICASTRTP_LOCAL_ADDRESS || CFG.bridgeHost;
let asteriskRtpPort = parseInt(extChannel.channelvars?.UNICASTRTP_LOCAL_PORT || "0");

if (!asteriskRtpPort && extChannel.id) {
  const vars = await ariClient.channels.getChannelVar({
    channelId: extChannel.id,
    variable: "UNICASTRTP_LOCAL_PORT",
  }).catch(() => null);
  if (vars) asteriskRtpPort = parseInt(vars.value);

  const varsAddr = await ariClient.channels.getChannelVar({
    channelId: extChannel.id,
    variable: "UNICASTRTP_LOCAL_ADDRESS",
  }).catch(() => null);
  if (varsAddr) asteriskRtpHost = varsAddr.value;
}

// ── 4. Vytvoř bridge a připoj oba kanály ─────────────────────────────────
const bridge = await ariClient.bridges.create({ type: "mixing" });
await bridge.addChannel({ channel: [channel.id, extChannel.id] });
console.log(`[${callId}] Bridge vytvořen: ${bridge.id}`);

// ── 5. Připoj WebSocket na OpenAI Realtime API ───────────────────────────
const openaiWs = new WebSocket(
  `wss://api.openai.com/v1/realtime?model=${CFG.openaiModel}`,
  {
    headers: {
      Authorization: `Bearer ${CFG.openaiKey}`,
      "OpenAI-Beta": "realtime=v1",
    },
  }
);

let rtpSeq = Math.floor(Math.random() * 65535);
let rtpTimestamp = Math.floor(Math.random() * 0xFFFFFFFF);
const rtpSsrc = Math.floor(Math.random() * 0xFFFFFFFF);

let audioBuffer = Buffer.alloc(0);
let flushMode = false;
let aiSpeaking = false;
let transferPending = false;
let active = true;
let sessionReady = false;

const RTP_FRAME_BYTES = 160;
const JITTER_PREFILL = 6;
let jitterReady = false;
let bufferWasFilled = false;
let totalAudioReceived = 0;
let silenceTimer = null;
const SILENCE_MS = 800;

// ── RTP output pacing timer (1 paket každých 20ms) ───────────────────────
const pacingTimer = setInterval(() => {
  if (!active || !asteriskRtpPort) return;

  if (!jitterReady) {
    if (audioBuffer.length < RTP_FRAME_BYTES * JITTER_PREFILL) return;
    jitterReady = true;
  }

  if (audioBuffer.length < RTP_FRAME_BYTES) {
    if (!flushMode || !bufferWasFilled) {
      if (bufferWasFilled && asteriskRtpPort) {
        const cn = Buffer.alloc(RTP_FRAME_BYTES, 0xD5);
        const pkt = buildRtpPacket(cn, rtpSeq, rtpTimestamp, rtpSsrc);
        rtpSeq = (rtpSeq + 1) & 0xFFFF;
        rtpTimestamp = (rtpTimestamp + 160) >>> 0;
        udpSocket.send(pkt, asteriskRtpPort, asteriskRtpHost);
      }
      return;
    }
    if (audioBuffer.length > 0) {
      const frame = Buffer.alloc(RTP_FRAME_BYTES, 0xD5);
      audioBuffer.copy(frame, 0);
      audioBuffer = Buffer.alloc(0);
      const pkt = buildRtpPacket(frame, rtpSeq, rtpTimestamp, rtpSsrc);
      rtpSeq = (rtpSeq + 1) & 0xFFFF;
      rtpTimestamp = (rtpTimestamp + 160) >>> 0;
      udpSocket.send(pkt, asteriskRtpPort, asteriskRtpHost);
    }
    aiSpeaking = false;
    jitterReady = false;
    flushMode = false;
    bufferWasFilled = false;

    if (transferPending) {
      transferPending = false;
      console.log(`${ts()} [${callId}] Přepojuji na ${CFG.transferExt}`);
      channel.continueInDialplan({
        context: "default",
        extension: CFG.transferExt,
        priority: 1,
      }).catch(err => console.error(`[${callId}] Transfer error:`, err.message));
    }
    return;
  }

  if (flushMode) jitterReady = true;

  const frame = audioBuffer.slice(0, RTP_FRAME_BYTES);
  audioBuffer = audioBuffer.slice(RTP_FRAME_BYTES);
  const pkt = buildRtpPacket(frame, rtpSeq, rtpTimestamp, rtpSsrc);
  rtpSeq = (rtpSeq + 1) & 0xFFFF;
  rtpTimestamp = (rtpTimestamp + 160) >>> 0;
  udpSocket.send(pkt, asteriskRtpPort, asteriskRtpHost);
}, 20);

// ── OpenAI WS events ──────────────────────────────────────────────────────
openaiWs.on("open", () => {
  console.log(`[${callId}] OpenAI WS připojen`);

  openaiWs.send(JSON.stringify({
    type: "session.update",
    session: {
      modalities: ["audio", "text"],
      instructions: AI_INSTRUCTIONS,
      voice: CFG.openaiVoice,
      input_audio_format: "g711_alaw",
      output_audio_format: "g711_alaw",
      input_audio_transcription: {
        model: "whisper-1",
        language: "cs",
      },
      turn_detection: {
        type: "server_vad",
        threshold: 0.9,
        prefix_padding_ms: 200,
        silence_duration_ms: 700,
      },
      temperature: 0.7,
      max_response_output_tokens: 4096,
    },
  }));

  const silence = Buffer.alloc(160, 0xD5);
  openaiWs.send(JSON.stringify({
    type: "input_audio_buffer.append",
    audio: silence.toString("base64"),
  }));
});

openaiWs.on("message", (raw) => {
  if (!active) return;
  let evt;
  try { evt = JSON.parse(raw); } catch { return; }

  switch (evt.type) {
    case "response.audio.delta": {
      if (!evt.delta) break;
      const chunk = Buffer.from(evt.delta, "base64");
      audioBuffer = Buffer.concat([audioBuffer, chunk]);
      totalAudioReceived += chunk.length;
      bufferWasFilled = true;
      break;
    }

    case "response.created": {
      aiSpeaking = true;
      if (openaiWs.readyState === WebSocket.OPEN) {
        openaiWs.send(JSON.stringify({ type: "input_audio_buffer.clear" }));
      }
      break;
    }

    case "response.audio.done": {
      flushMode = true;
      totalAudioReceived = 0;
      break;
    }

    case "response.output_item.done": {
      const content = evt.item?.content;
      if (content) {
        const textBlock = content.find(c => c.type === "audio" && c.transcript);
        const text = textBlock?.transcript || "";
        if (text) {
          console.log(`${ts()} [${callId}] AI: ${text}`);
          transcript.push({ role: "assistant", text });
          const lower = text.toLowerCase();
          if (lower.includes("přepoj") || lower.includes("přepojím") || lower.includes("přepojuji")) {
            transferPending = true;
            console.log(`${ts()} [${callId}] Transfer pending → ${CFG.transferExt}`);
          }
        }
      }
      break;
    }

    case "conversation.item.input_audio_transcription.completed": {
      const text = evt.transcript || "";
      if (text && text.trim()) {
        console.log(`${ts()} [${callId}] Volající: ${text}`);
        transcript.push({ role: "caller", text });
      }
      break;
    }

    case "session.created":
      console.log(`${ts()} [${callId}] Session: ${evt.type}`);
      break;

    case "session.updated":
      console.log(`${ts()} [${callId}] Session: ${evt.type}`);
      sessionReady = true;
      openaiWs.send(JSON.stringify({
        type: "response.create",
        response: {
          modalities: ["audio", "text"],
          instructions: "Pozdrav volajícího. Řekni: Dobrý den, dovolali jste se do společnosti XYZ.",
        }
      }));
      break;

    case "error":
      console.error(`[${callId}] OpenAI chyba:`, evt.error?.message);
      break;

    case "input_audio_buffer.speech_started":
      console.log(`${ts()} [${callId}] !! SPEECH_STARTED (VAD)`);
      break;
    case "input_audio_buffer.speech_stopped":
      console.log(`${ts()} [${callId}] speech_stopped`);
      break;
    case "response.cancelled":
      console.log(`${ts()} [${callId}] !! RESPONSE CANCELLED`);
      break;
  }
});

openaiWs.on("error", (err) => {
  console.error(`[${callId}] OpenAI WS error:`, err.message);
});

openaiWs.on("close", () => {
  console.log(`[${callId}] OpenAI WS uzavřen`);
});

// ── 6. Audio z Asterisku → OpenAI ─────────────────────────────────────────
udpSocket.on("message", (msg) => {
  if (!active || !sessionReady || openaiWs.readyState !== WebSocket.OPEN) return;
  if (msg.length <= RTP_HEADER_SIZE) return;
  if (aiSpeaking) return;

  const alawData = msg.slice(RTP_HEADER_SIZE);
  openaiWs.send(JSON.stringify({
    type: "input_audio_buffer.append",
    audio: alawData.toString("base64"),
  }));
});

// ── 7. Cleanup při zavěšení ───────────────────────────────────────────────
const cleanup = async (reason) => {
  if (!active) return;
  active = false;
  console.log(`${ts()} [${callId}] Hovor ukončen: ${reason}`);

  if (transcript.length > 0) {
    saveTranscript(callId, transcript);
  }

  if (silenceTimer) clearTimeout(silenceTimer);
  clearInterval(pacingTimer);
  openaiWs.close();
  udpSocket.close();

  try { await bridge.destroy(); } catch {}
  try { await extChannel.hangup(); } catch {}
};

channel.on("StasisEnd",   () => cleanup("StasisEnd"));
channel.on("ChannelHangupRequest", () => cleanup("HangupRequest"));

setTimeout(() => {
  if (active) {
    cleanup("timeout");
    channel.hangup().catch(() => {});
  }
}, 10 * 60 * 1000);
}

// ─── Start ARI klienta ────────────────────────────────────────────────────────
ari.connect(CFG.ariUrl, CFG.ariUser, CFG.ariPass, (err, client) => {
if (err) {
  console.error("Nelze se připojit k Asterisk ARI:", err.message);
  process.exit(1);
}

console.log("Asterisk - AI Bridge spuštěn");
console.log(`  ARI:     ${CFG.ariUrl}`);
console.log(`  RTP:     ${CFG.bridgeHost}:${CFG.rtpPort}`);
console.log(`  Modely:  ${CFG.openaiModel} / whisper-1`);
console.log("─".repeat(50));

client.on("StasisStart", (evt, channel) => {
  if (channel.name.startsWith("UnicastRTP")) return;
  handleCall(client, channel).catch(err => {
    console.error(`[${channel.id.slice(0,8)}] Neočekávaná chyba:`, err.message);
  });
});

client.start("asterisk-ai");
});

process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
});

Role, knowledge base, and business context

The strength of an AI assistant doesn’t just lie in its ability to speak. What matters is what it knows and what role it has.

Every quality voice bot should have clearly defined:

  • how it should introduce itself
  • what communication style to use
  • what information it can provide
  • when to hand the call to a human
  • what it must never claim or promise

On top of that sits the knowledge base. It can contain, for example:

  • product and service information
  • business hours
  • price lists
  • internal procedures
  • answers to frequently asked questions
  • contact and routing rules

It’s precisely the combination of role + knowledge base + telephony logic that turns an ordinary AI model into a true virtual assistant.

Where ARI makes the most sense

The Asterisk dialplan is great for classic telecommunications scenarios. But when you want to conduct a dynamic dialog, work with context, or make real-time decisions based on AI responses, ARI becomes a much more natural choice.

A custom Node.js application also enables you to add features such as:

  • conversation history
  • caller intent evaluation
  • automatic transfer based on conversation content
  • integration with ticketing systems or CRM
  • logging and analytics
  • combining multiple models and services

This is exactly the moment when a PBX transforms into an intelligent communication platform.

It’s not complicated if you have the right foundation

At first glance, combining Asterisk, ARI, Node.js, and an AI model may seem complex. In reality, though, it’s nothing unachievable. Once you have a working Asterisk, a basic ARI application, and a well-designed call scenario, you can quite quickly create a first usable prototype.

And that’s exactly what makes this technology interesting: there’s no need to build a massive enterprise solution. Even a smaller company can today create its own voice AI assistant that answers calls, guides customers, and saves operators’ time.

Deployment as a systemd service

For production use, it’s advisable to run the application as a systemd service that automatically restarts on failure:

ini asterisk-ai-bridge.service
[Unit]
Description=Asterisk AI Bridge
After=network.target asterisk.service

[Service]
Type=simple
User=asterisk
WorkingDirectory=/opt/asterisk-ai-bridge/voipsun-ai-bridge
ExecStart=/usr/bin/node bridge.js
Restart=always
RestartSec=5
EnvironmentFile=/opt/asterisk-ai-bridge/voipsun-ai-bridge/.env

[Install]
WantedBy=multi-user.target

Configuration values are loaded from the .env file:

bash .env
# OpenAI API klíč
OPENAI_API_KEY=sk-proj-

# Asterisk ARI přihlašovací údaje
ARI_URL=http://127.0.0.1:8088
ARI_USER=ai-demo-user
ARI_PASS=tajne-heslo

# UDP port pro RTP audio příjem z Asterisku (libovolný volný port)
RTP_PORT=12000

# Adresa tohoto serveru (kde běží bridge) - viditelná pro Asterisk
BRIDGE_HOST=127.0.0.1

# Adresář pro ukládání transkriptů
TRANSCRIPT_DIR=/cesta-k-transkriptum

Download source code (ZIP)

Conclusion

Connecting Asterisk with AI via ARI opens a very practical path to taking business telephony to the next level. Whether you choose a direct cloud SIP connection or a custom ARI application in Node.js, the result can be an intelligent voice assistant that understands the business, knows its data, and can lead a meaningful dialog with the customer.

AI in telephony is no longer a futuristic toy. It’s a tool that can be deployed today — and in many cases, surprisingly easily.