Augury Logo

Augury

Voice Agents: Sådan Byggede Vi en HR-assistent med Twilio og OpenAI Realtime

Cover Image for Voice Agents: Sådan Byggede Vi en HR-assistent med Twilio og OpenAI Realtime
Kasper Kristian RasmussenKasper Kristian Rasmussen

Voice Agents: Sådan Byggede Vi en HR-assistent med Twilio og OpenAI Realtime

Voice agents—AI der taler og lytter i realtid—har taget et spring fremad med modeller som OpenAI Realtime API. I denne artikel gennemgår vi hvordan vi byggede en telefonbaseret HR-assistent i projektet HelpDeskAI, med kun omkring 130 linjer kode.

Hvad Er En Voice Agent?

En voice agent er en AI der kan:

  • Tale (TTS – text-to-speech)
  • Lytte og forstå (STT – speech-to-text)
  • Tænke og handle (LLM med tools)
  • Samtale i realtid med lav latenstid

Tidligere krævede det typisk at samle flere tjenester (Whisper, en LLM, en TTS-leverandør, telephony) og bygge en kompleks pipeline. Nu kan OpenAI Realtime API håndtere tale-ind, tænkning og tale-ud i et samlet flow. Det gør implementationen markant simplere.

Arkitektur: Fra Telefon til AI

Overordnet flow:

Telefonopkald (PSTN)
    ↓
Twilio Voice (webhook)
    ↓  TwiML med <Stream url="wss://.../media-stream"/>
Twilio åbner WebSocket → /media-stream
    ↓
TwilioRealtimeTransportLayer (adapter)
    ↓  konverterer Twilio Media Streams ↔ OpenAI Realtime WebSocket
RealtimeSession (RealtimeAgent + tools + guardrails)
    ↓
OpenAI Realtime API

1. Indkommende Opkald (Twilio Webhook)

Når nogen ringer, kalder Twilio vores webhook (/incoming-call). Vi returnerer TwiML der forbinder opkaldet til en WebSocket-URL:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <Stream url="wss://din-server.dk/media-stream" />
  </Connect>
</Response>

Twilio åbner herefter en WebSocket til vores /media-stream-endpoint og streamer audio i begge retninger.

2. Transport Layer: Twilio ↔ OpenAI

Twilio bruger Media Streams med et binært protokol. OpenAI Realtime API bruger sin egen WebSocket-format. TwilioRealtimeTransportLayer fra @openai/agents-extensions fungerer som adapter mellem dem:

  • Konverterer lydformater
  • Håndterer Twilio mark-events til at fange afbrydelser (brugertaler mens AI taler)
  • Holder forbindelsen mellem Twilio og OpenAI Realtime API

STT og TTS sker begge i OpenAI Realtime modellen—der er ingen separate tjenester.

3. Agenten: RealtimeAgent med Tools

Vi definerer en RealtimeAgent med instruktioner og tools:

const agent = new RealtimeAgent({
  name: "HR-assistent",
  instructions: `Du er HR-assistent for organisationen. 
    Du hjælper med spørgsmål om job og ledige stillinger, kontakt, 
    kurser, selvbetjening og generelle HR-henvendelser. Svar altid på dansk. 
    Hold svar korte og venlige. Henvis til hjemmesiden til selvbetjening.`,
  tools: [getVacancyInfoTool, getContactInfoTool, getSelfServiceInfoTool],
});

Tools giver agenten mulighed for at hente konkrete data:

Tool Formål
get_vacancy_info Information om ledige stillinger
get_contact_info Kontaktoplysninger og åbningstider
get_self_service_info Selvbetjening, guides, kurser, råd (med topic-parameter)

Når opkalderen fx spørger "Hvordan ansøger jeg til en stilling?", kalder agenten get_vacancy_info og svarer baseret på det.

4. Guardrails: Sikkerhed omkring output

Agenten må ikke love ansættelse eller dele privat information. Vi bruger output guardrails:

const guardrails = [
  {
    name: "HR blocklist",
    async execute({ agentOutput }) {
      const blocklistTerms = [
        "du er ansat",
        "du fik jobbet",
        "vi lover",
        "garanteret stilling",
        "privat telefon",
        "personlig email",
      ];
      const triggered = blocklistTerms.some((term) =>
        agentOutput.toLowerCase().includes(term),
      );
      return {
        tripwireTriggered: triggered,
        outputInfo: { blocklistTermsInOutput: triggered },
      };
    },
  },
];

Hvis agentens output indeholder disse termer, kan man logge det eller stoppe responsen.

5. WebSocket-Handler: Sammenmontering

Selve forbindelsen sættes op i WebSocket-handleren:

fastify.get("/media-stream", { websocket: true }, async (connection) => {
  const transport = new TwilioRealtimeTransportLayer({
    twilioWebSocket: connection,
  });
  const session = new RealtimeSession(agent, {
    transport,
    outputGuardrails: guardrails,
  });
  await session.connect({ apiKey: OPENAI_API_KEY });
});

RealtimeSession forbinder agenten, transporten og guardrails til OpenAI Realtime API.

Tech Stack

Lag Teknologi
HTTP/WebSocket server Fastify v5
Telefoni Twilio Voice + Media Streams
Voice AI OpenAI Realtime API via @openai/agents
Transport TwilioRealtimeTransportLayer (@openai/agents-extensions)
Validering Zod

Deployment og Krav

  • Server på en port (fx 5050)
  • HTTPS/WSS offentligt endpoint (fx via ngrok eller cloud)
  • Twilio Voice webhook: https://din-host/incoming-call
  • OPENAI_API_KEY med adgang til Realtime API
  • Node.js 22+ anbefales

Konklusion

Voice agents behøver ikke være komplekse. Med Twilio Media Streams og OpenAI Realtime API kan man bygge en telefonbaseret AI-assistent med:

  • Ingen separat STT/TTS
  • Få linjer kode (~130)
  • Tools til strukturerede data
  • Guardrails til mere sikker output

Projektet HelpDeskAI viser en konkret implementation til HR-support—et mønster der kan genbruges til kundeservice, booking, FAQ og lignende.


Vil du udforske voice agents eller andre AI-løsninger til din virksomhed? Kontakt os og lad os snakke om hvad der giver mening for dig.