Så byggde vi
en kollega i telefonen.
Ett vanligt telefonnummer, en liten Python-server och en AI som pratar svenska. Här är delarna bakom Elkens VVS Demo — och hur samtal, AI och sms hänger ihop.
Python · Flask · SQLite · 46elks · ElevenLabs
Du ringer. Koden tar vid.
- 01 · TelefonenKunden ringer ett vanligt mobilnummer.
- 02 · 46elksKopplar vidare till ditt WebSocket-nummer.
- 03 · Din serverSkickar ljud åt båda håll och sparar ärenden.
- 04 · ElevenLabsLyssnar, ställer frågor och svarar med en svensk röst.
När ärendet är klart
ElevenLabs → submit_case → SQLite → 46elks SMS → inringaren
Om kunden rättar via sms
Inringaren → 46elks webhook → rättelse på senaste skickade ärendet
I demot får inringaren se det sms som en hantverkare skulle kunna få i en riktig tjänst. Inget arbete bokas.
Starta med en liten server.
Du behöver ett 46elks-konto, ett ElevenLabs-konto med tillgång till Agents, Python 3.11 eller senare och en server med HTTPS. Vår server är skriven i Python med Flask för webben, SQLite för ärenden och en separat process för ljudbryggan. Guiden beskriver den driftsatta lösningen; filnamnen nedan visar hur vi delat upp ansvaret.
app.pyWebb, databas, sms och inkommande rättelser.
voice_agent.pyLjudbryggan mellan 46elks och ElevenLabs.
agent_config.pyRöst, instruktioner och AI-verktyg.
provision.pySkapar och konfigurerar resurser via API.
static/Sajten, helt i HTML, CSS och JavaScript.
Ett mobilnummer. Ett WebSocket-nummer.
Mobilnumret är det kunden ringer och får sms från. WebSocket-numret kopplar röstsamtalet till din kod. De är två olika resurser på samma 46elks-konto.
Fyll i ELKS_API_USER och ELKS_API_PASS i .env. För ett subkonto används dess ID och aktuella secret. Skapa även separata slumpade värden för VOICE_TOKEN, SMS_WEBHOOK_TOKEN och ADMIN_TOKEN:
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'Vårt konfigurationsskript använder 46elks API för att koppla ihop numren. Motsvarande inställningar för din egen server är:
# Mobilnumrets voice_start
{"connect": "+46DITT_WEBSOCKET_NUMMER"}
# WebSocket-numrets websocket_url
wss://din-domän.se/ws/DITT_VOICE_TOKENGe AI:n ett tydligt jobb.
ElevenLabs sköter taligenkänning, röst och dialog. Här använder vi den svenska rösten Anders snackar och Gemini 2.5 Flash genom ElevenLabs. Du kan välja en annan tillgänglig röst genom dess voice ID.
Sätt ELEVENLABS_API_KEY och ELEVENLABS_VOICE_ID. Nyckeln behöver läsa röster samt läsa och skriva Agents. Vårt skript skapar agenten via ElevenLabs API och sparar verktygs-ID och agent-ID i .env.
Du är AI-svararen hos Elkens VVS. Fråga bara efter det som saknas: vad som hänt, plats, namn och när kunden vill bli uppringd. Använd inkommande nummer automatiskt. Upprepa inte ärendet. Spara när du har tillräckligt med uppgifter och avsluta sedan samtalet.
Det är en förenklad version av instruktionerna i agent_config.py. Den riktiga prompten hanterar också saknade uppgifter, sms-resultat och misslyckat sparande. Agenten lovar inga priser eller tider.
AI:n får ett verktyg som heter submit_case. Det tar emot namn, problem, plats, brådska, återringningsnummer och tid. Vår server lägger själv till samtals-ID och väljer sms-mottagaren från samtalets metadata.
Två WebSockets, samma ljudformat.
46elks öppnar en anslutning till vår server och skickar ett hello med samtals-ID och inkommande nummer. Servern hämtar en tillfälligt signerad URL till den privata ElevenLabs-agenten. API-nycklarna stannar på servern.
# 46elks → vår server
{"t": "hello", "callid": "c...", "from": "+467..."}
# Vår server → 46elks
{"t": "sending", "format": "pcm_24000"}
{"t": "listening", "format": "pcm_24000"}Båda tjänsterna konfigureras för PCM 24 kHz. Ljudet skickas som base64 i JSON, så ingen omsampling behövs. När kunden avbryter AI:n skickar bryggan interrupt till 46elks och kastar gamla ljudpaket.
Spara först. Skicka sedan.
När AI:n anropar submit_case validerar servern uppgifterna och sparar ärendet i SQLite. Samma samtals-ID kan inte skapa dubbla ärenden. Sms skickas från demots mobilnummer till numret som ringde.
requests.post(
"https://api.46elks.com/a1/sms",
auth=(api_user, api_password),
data={
"from": demo_phone,
"to": incoming_caller,
"message": summary,
},
timeout=15,
)Demot reserverar en av fem sms-platser per inkommande nummer och svensk kalenderdag i samma databastransaktion som ärendet. Därmed håller gränsen även om flera samtal blir klara samtidigt eller processen startas om.
En osäker nätverkstimeout förbrukar också en plats: meddelandet kan redan ha accepterats. Vi skickar därför inte automatiskt igen. AI:n får ett faktiskt resultat från servern och får inte säga att sms skickats om det misslyckades.
Aktivera med SMS_ENABLED=1. Numret du visar på sajten styrs av DEMO_PHONE och PHONE_READY.
Ett sms som går att svara på.
En avsändare som bara heter ”AI-svarare” går inte att svara till. Därför använder vi samma mobilnummer som kunden ringde. Sms:et säger att kunden kan svara om något är fel.
# Mobilnumrets sms_url
https://din-domän.se/sms/DITT_SMS_WEBHOOK_TOKEN
# 46elks POST:ar formulärdata
id=s...&from=+467...&to=+467...&message=Rätt adress är...Servern kopplar svaret till avsändarens senaste skickade ärende och sparar det som en rättelse. Originalet bevaras, så att man kan se både vad AI:n uppfattade och vad kunden rättade. Dubbla leveranser ignoreras med hjälp av sms-ID.
Ärende-API:t visar rättelserna i corrections. När rättelsen sparats skickar servern en kort sms-bekräftelse till kunden. Den delar dagens fem sms-platser med ärendesammanfattningarna. Dubbla webhookleveranser ger inte dubbla bekräftelser. Läs om inkommande sms ↗
”Hej då” måste också lägga på.
En prompt som säger ”avsluta vänligt” räcker inte. Vi ger agenten ElevenLabs systemverktyg end_call och instruerar den att använda det när ärendet är klart eller kunden vill avsluta.
# Efter att ElevenLabs avslutat
{"t": "sync"} # Vänta tills sista ljudet spelats färdigt
# 46elks svarar med sync
{"t": "bye"} # Lägg på telefonsamtaletBryggan väntar högst tolv sekunder på ljudkvittot och avslutar sedan. Kunden får höra avslutsfrasen, och samtalet lämnas inte öppet i tystnad.
Kör som en tjänst. Provring.
Kör webben med Gunicorn och rösten i en separat process. Appens lokala standardportar är 8110 och 8111. Bakom Nginx ska både HTTP och WebSocket proxas, med giltigt HTTPS-certifikat för din domän.
# Terminal 1
.venv/bin/gunicorn --bind 127.0.0.1:8110 app:app
# Terminal 2
.venv/bin/python voice_agent.py
# Automatiska tester (inga riktiga sms)
.venv/bin/python -m unittest -vVi använder systemd för att starta processerna automatiskt och Nginx som ingång för HTTPS och WebSocket. Katalog, användare, domän och portar konfigureras för servern. Kör med en egen tjänsteanvändare och se till att den kan skriva både i datakatalogen och SQLite-filen.
Vårt första integrationstest skapade databasen som root. Rösttjänsten kunde då inte spara riktiga ärenden. Vi rättade ägarskapet och kör nu databastester som tjänsteanvändaren. Ett test är mest användbart när det kör med samma förutsättningar som tjänsten.
- Ring från en riktig mobil: hörs rösten åt båda håll och fungerar avbrott?
- Lämna ett ärende: sparas det och kommer sms från rätt nummer?
- Svara med en rättelse: hamnar den på rätt ärende?
- Kontrollera att AI:n verkligen lägger på efter avslutet.
- Först därefter: visa numret publikt med
PHONE_READY=1.
Privata ärenden kräver ADMIN_TOKEN. Ljud spelas inte in av vår app; ElevenLabs-agentens inspelning är avstängd och dess konversationsdata sparas i sju dagar. Lokala ärenden och sms-rättelser behöver en egen rutin för gallring.
Samma grund.
Din egen verksamhet.
Byt företag, instruktioner och röst. För en riktig hantverkare kan sammanfattningen gå till företagaren i stället för inringaren. Anpassa då mottagarlogik och informationen till kunden innan du tar emot riktiga ärenden.
Till 46elks dokumentation ↗