Sari la conținut
megapromotingHai să discutăm

Expertiză · Agenți vocali

Un agent care vorbește — în pagina ta și la telefon — și care poate fi întrerupt.

Construim agenți vocali pe Kallina: rol scris, bază de cunoștințe aprobată, unelte pe care le cheamă în timpul convorbirii și o limită dincolo de care predau discuția unui om. Româna e limba implicită a agenților noi.

Construit dejaTrei implementări proprii, citate mai jos. (1) Platforma Kallina — 472 de funcții edge Deno și 559 de migrări de bază de date, numărate azi în depozit; `https://app.kallina.info` răspunde 200. (2) Componenta de pe acest site, `src/components/agents/KallinaVoiceAgent.tsx`, care implementează traseul corect de sesiune și documentează cele două moduri în care un agent vocal eșuează în tăcere. (3) Puntea `elevenlabs-bridge` și sesiunea `openai-realtime-session`, unde stau parametrii reali de întrerupere și de detecție a vorbirii. Rezerva pe care o spunem: partea de telefon depinde de o gazdă SIP, iar gazda prin care trec liniile noastre de test nu răspunde la data scrierii — vezi pagina de telefonie. Pe web, traseul e verificabil azi.

Un agent vocal, la noi, nu e „un bot care citește un scenariu”. Are patru lucruri scrise înainte de primul apel: un rol, o bază de cunoștințe aprobată de tine, un set de unelte pe care are voie să le cheme, și o limită dincolo de care predă discuția unui om. Ce iese din conversație nu e doar un transcript: apelul are durată, cost defalcat pe furnizor, înregistrare și analiză, toate legate de contul afacerii.

Motorul nu e unul singur, și asta e o decizie, nu un accident. În configurația agentului alegi între ElevenLabs Conversational AI, OpenAI Realtime și Gemini Live, sau lași „auto” și alege platforma după limbă, încărcare și disponibilitate. Peste el alegi separat creierul textual, dintr-un catalog în care fiecare model are latența și costul pe minut afișate în interfață. Când cineva întreabă „de ce răspunde încet”, răspunsul are cifre, nu păreri.

Partea pe care oamenii o simt cel mai tare e întreruperea. Pe OpenAI Realtime, detecția vorbirii e semantică, cu prag 0,5, o pernă de 300 ms înainte și 500 ms de liniște ca să considere că ai terminat de vorbit; comutatorul „permite întreruperi” e pornit implicit. Pe ElevenLabs, evenimentul de întrerupere e trimis mai departe centralei telefonice, ca să oprească redarea imediat. Iar timpul după care agentul consideră că tăcerea ta e un răspuns e configurabil per agent — pe un agent de vânzări scris de noi e strâns la 12 secunde, față de 15 implicit.

Agentul trăiește în două locuri diferite, cu constrângeri diferite. În pagina web intră cu o singură linie de script sau cu o componentă proprie, iar regula de aur e că pagina nu ține niciodată identificatorul agentului și nicio cheie de furnizor — cere platformei o sesiune și primește un URL semnat, de scurtă durată. La telefon intră printr-un trunk SIP, iar acolo constrângerile sunt ale operatorului și ale centralei, nu ale agentului; despre asta scriem separat, pe pagina de telefonie.

Ce cuprinde

Lucrarea, pe componente

Sesiunea se cere de la platformă, nu de la furnizorul de voce

Pagina trimite `POST /functions/v1/widget-voice-session` cu identificatorul public al widgetului și primește înapoi un URL semnat. Nu are niciodată identificatorul agentului și nicio cheie. Varianta greșită — `startSession({ agentId })` direct în HTML — ocolește complet stratul de sesiune, de credit și de jurnal al platformei, și expune agentul oricui deschide sursa paginii. E prima verificare pe care o facem pe orice implementare pe care o preluăm.

Microfonul se cere înainte de sesiune, deci un refuz nu costă nimic

Ordinea e fixă și contează: cere microfonul cu `getUserMedia({audio:true})`, și abia dacă omul acceptă cere sesiunea. Dacă vizitatorul refuză, nu se emite niciun URL semnat și nu se consumă niciun minut. Cererea de microfon se face în interiorul apăsării de buton, înainte de orice așteptare care ar putea depăși gestul — altfel browserul ignoră cererea.

Agentul poate fi întrerupt, și asta se reglează cu cifre

Pe OpenAI Realtime: detecție semantică a vorbirii, prag 0,5, pernă 300 ms, 500 ms de liniște, întrerupere permisă implicit. Pe ElevenLabs: evenimentul `interruption` e transmis centralei ca să taie redarea. Peste asta, `turn_timeout` per agent decide după cât timp de tăcere agentul consideră că e rândul lui — implicit 15 secunde, strâns la 12 pe un agent de vânzări. Și un „nu-l pot întrerupe” raportat de un client are, de multe ori, altă cauză decât pragurile: răspunsuri prea lungi. Pe aceea o rezolvăm în prompt și în plafonul de tokeni, nu în detecția vorbirii.

Cheamă unelte în timpul convorbirii

Agentul nu e închis în ce știe. Poate chema funcții pe server în timp ce vorbește — pentru a citi o disponibilitate, a scrie o programare, a trimite un SMS sau a interoga un catalog. Uneltele sunt declarate per agent, cu parametri tipizați și cu un termen de execuție propriu, ca o unealtă lentă să nu blocheze conversația.

Predarea către om e parte din scenariu, nu o excepție

În mesagerie, preluarea de către un coleg are stare proprie pe fir — `handoff_status`, momentul preluării, motivul, cine e alocat, notele de rezolvare — implementată într-un modul dedicat de 619 linii. La telefon, transferul e făcut de centrală: transfer orb prin `##` și transfer asistat prin `*2`, cu apelul aterizând într-un context de dialplan scris pentru asta. Sunt două mecanisme diferite pentru că sunt două canale diferite; le scriem pe amândouă în scenariu.

Motorul și vocea se aleg explicit, nu implicit

Patru poziții de furnizor: `elevenlabs`, `openai-realtime`, `gemini-live` și `auto`. Pentru sinteza vocii în alte limbi decât engleza, implicit e `eleven_flash_v2_5` — 32 de limbi, în jur de 75 ms; varianta de calitate mai bună, `eleven_turbo_v2_5`, e în jur de 250 ms. Pe OpenAI Realtime sunt zece voci și motoare până la `gpt-realtime-2.1`. Alegerea e un câmp în configurația agentului, nu o presupunere din partea noastră.

Limbi

Configurația agentului acceptă 32 de limbi, harta internă de limbi a platformei are 41 de intrări, iar panoul de administrare e tradus în 20. Sunt trei cifre pentru trei lucruri diferite și nu se pot amesteca. Româna e limba implicită a unui agent nou.

Ce rămâne după apel

Transcript, înregistrare audio, durată, cost defalcat pe furnizor, plus o analiză de după apel și un rezumat scurt al conversației, generat pentru memoria clientului. Analiza nu e un scop în sine: pe ea se sprijină decizia dacă agentul a făcut ce trebuia sau dacă scenariul are o gaură.

Consumul se verifică înainte de acțiune, nu la factură

Înainte de orice operațiune care costă, platforma verifică creditul disponibil, iar consumul se scrie defalcat pe furnizor și pe agent. Poarta stă înaintea apelului. Practic: un agent scăpat într-o buclă nu poate consuma la nesfârșit fără să lovească într-o limită.

Cum arată

Traseul unei conversații: pagina cere microfonul, platforma emite o sesiune semnată, agentul vorbește și poate fi întrerupt, cheamă o unealtă, iar ce depășește rolul lui se predă unui om.

01

Alegem un singur traseu și îi scriem marginile

Un scenariu cu intrare clară și ieșire clară: apel primit la recepție, confirmare de comandă, verificare de disponibilitate. Scriem explicit ce nu are voie agentul să facă, ce nu are voie să spună și pe ce cale predă discuția unui om. Un agent fără margini scrise nu e mai capabil, e doar mai imprevizibil.

02

Construim agentul: rol, cunoștințe, unelte, limite

Promptul, baza de cunoștințe aprobată de tine, uneltele pe care le poate chema și pragurile de conversație — furnizor, voce, model textual, timp de tăcere, comportament la întrerupere. La final avem un agent configurat, nu un demo.

03

Îl punem pe canal și verificăm traseul fără să consumăm minute

Pe web: o linie de script sau o componentă proprie peste API-ul de pornire și oprire. Există un script care parcurge exact drumul unui browser — rândul din configurație, configurația widgetului, cererea de sesiune — și se oprește la URL-ul semnat, fără să deschidă conexiunea audio. Rulează pe un widget sau pe toate, întoarce cod de ieșire, deci poate intra într-o verificare programată. Pe telefon: numărul intră pe trunk, iar apelurile primite și cele ieșite se testează separat, pentru că se strică separat.

04

Măsurăm ce a ieșit și corectăm scenariul

După primele conversații reale citim transcriptele și analiza de după apel, nu impresiile. Ce corectăm cel mai des nu e modelul, ci lungimea răspunsurilor și marginile scenariului — două lucruri care se schimbă în prompt, în aceeași zi.

IntrareVerificareDeciziePublicareNimic nu trece mai departe neverificat.ORDINEA E ARGUMENTUL
Traseul unei conversații: pagina cere microfonul, platforma emite o sesiune semnată, agentul vorbește și poate fi întrerupt, cheamă o unealtă, iar ce depășește rolul lui se predă unui om.

Datele

Ce atingem, unde stau și cât rămân

Întrebările pe care le pune oricine are un responsabil cu protecția datelor — puse aici înainte să le pună el.

Unde stau datele
PostgreSQL prin Supabase găzduit pe infrastructură proprie, cu 559 de migrări versionate, dintre care 195 ating politici de Row Level Security. Fișierele — înregistrări, documente de cunoștințe, mostre de voce — stau în depozite separate, cu calea începând cu identificatorul spațiului de lucru, ca izolarea să fie în structură, nu doar în interogare.
Ce nu ajunge niciodată în pagina ta
Cheia furnizorului de voce și identificatorul agentului. Pagina primește doar un URL semnat, de scurtă durată, emis de platformă. Consecința practică: cineva care citește sursa paginii tale nu poate folosi agentul tău pe cheltuiala ta.
Ce se înregistrează dintr-un apel
Audio, transcript, durată, numărul apelantului, rezultatul apelului și analiza de după. Pentru cazurile în care agentul ține minte clientul între apeluri, se salvează și un rezumat scurt al conversației anterioare. Toate sunt legate de un spațiu de lucru; nu există un depozit comun peste clienți.
Ștergerea la cerere există; ștergerea automată la termen încă nu
Spunem asta așa cum e, pentru că e diferența dintre o promisiune și o funcție. Ștergerea la cerere e implementată: o funcție dedicată șterge obiectele din depozitul de fișiere, cheamă procedura de ștergere din bază și invalidează sesiunile; există separat export de date și o funcție pentru datele de apel. Ce nu e implementat: termenele de retenție configurabile per spațiu de lucru. Sunt proiectate într-un document intern din aprilie, dar coloana de configurare nu apare în nicio migrare — deci azi ștergerea la termen se face prin procedură, nu prin ceas. Dacă proiectul tău are nevoie de ștergere automată, ea intră ca lucrare, nu ca setare.
Cine e informat că vorbește cu un agent
Anunțul că interlocutorul vorbește cu un sistem automat, temeiul înregistrării convorbirii și textul care se citește la începutul apelului se stabilesc împreună cu tine și intră în scenariu. Nu sunt o setare tehnică: sunt o decizie a operatorului de date, adică a ta.

Un caz

Butonul care nu făcea nimic — și nici măcar nu dădea eroare

Situația

Un agent vocal pus într-o pagină web. La apăsare nu se întâmpla absolut nimic: fără sunet, fără mesaj, fără eroare în consolă pe care un neinițiat s-o vadă. Genul de defect care se raportează ca „nu merge” și se caută o zi în locul greșit.

Ce am construit

Cauzele erau două, ambele tăcute prin natura lor. Prima: antetul `Permissions-Policy` al serverului conținea `microphone=()`, iar browserul refuză cererea de microfon fără să ridice ceva vizibil pentru utilizator. A doua: SDK-ul vocal era importat static, iar un modul care întoarce 404 la import static doboară tot scriptul — deci și cererea de configurare murea odată cu el, ceea ce face defectul să arate exact ca „nimic nu s-a întâmplat”. Am mutat importul SDK-ului în interiorul apăsării de buton, am pus cererea de microfon înaintea cererii de sesiune și am scris antetul serverului ca `microphone=(self)`, cu politica de conținut care permite explicit rețeaua de distribuție a SDK-ului.

Ce a ieșit

Astăzi, pe acest site, antetul servit în producție este `camera=(), microphone=(self), geolocation=(self)`, iar politica de securitate a conținutului enumeră explicit sursa SDK-ului. Un eșec nu mai e tăcut: refuzul microfonului produce un mesaj în limba română care spune ce anume trebuie schimbat, iar refuzul nu costă nimic, pentru că sesiunea nici nu se mai cere.

Ce nu spune cazul

Cele două cauze sunt de infrastructură, nu de agent. Dacă site-ul tău e servit de altcineva și antetele nu sunt sub controlul nostru, verificarea lor devine o condiție de început a lucrării, nu o surpriză de la final.

Întrebări

Ce ne întreabă oamenii înainte să sune

Cu ce vorbește, concret? Ce motor e în spate?

Alegi între ElevenLabs Conversational AI, OpenAI Realtime și Gemini Live, sau lași „auto” și alege platforma după limbă și disponibilitate. Pentru sinteza vocii în alte limbi decât engleza, implicit e `eleven_flash_v2_5` — 32 de limbi, în jur de 75 ms; dacă vrei calitate mai mare și accepți întârziere mai mare, `eleven_turbo_v2_5` e în jur de 250 ms. Pe OpenAI Realtime sunt zece voci, iar motorul merge până la `gpt-realtime-2.1`. Creierul textual se alege separat, dintr-un catalog în care fiecare model are latența și costul pe minut scrise lângă el.

Îl pot întrerupe când vorbește, sau trebuie să aștept să termine?

Poți. Pe OpenAI Realtime detecția vorbirii e semantică, cu prag 0,5, pernă de 300 ms și 500 ms de liniște, iar comutatorul de întrerupere e pornit implicit. Pe ElevenLabs, evenimentul de întrerupere e trimis centralei telefonice ca să oprească redarea. Dacă totuși ai senzația că „nu-l poți întrerupe”, cauza e de obicei alta: agentul răspunde în paragrafe, nu în propoziții. Aia se repară în prompt și în plafonul de tokeni, nu în praguri.

În câte limbi vorbește?

Configurația agentului acceptă 32 de limbi. Harta internă de limbi a platformei are 41 de intrări, iar panoul de administrare e tradus în 20 — trei cifre pentru trei lucruri diferite. Româna e limba implicită a unui agent nou. Pentru o limbă anume verificăm întâi ce combinație de model de voce și de furnizor o acoperă efectiv, pentru că nu toate modelele acoperă aceleași limbi.

Cum ajunge pe site-ul meu și ce trebuie să schimb la mine?

O linie de script cu identificatorul public al widgetului, sau componenta ta proprie peste `start()` / `stop()` / `toggle()`. Două lucruri trebuie să fie adevărate pe serverul tău, altfel butonul nu face nimic și nu apare nicio eroare: antetul `Permissions-Policy` trebuie să permită `microphone=(self)` — cu `microphone=()` browserul refuză microfonul în tăcere — și politica de securitate a conținutului trebuie să permită încărcarea SDK-ului de pe rețeaua de distribuție folosită. Le verificăm noi înainte de publicare.

Poate suna el primul, nu doar să răspundă?

Da, platforma are apelare în loturi, cu import de contacte, reîncercări și urmărire în timp real. Dar apelurile ieșite nu depind doar de noi: în registrul nostru de probleme e documentat un caz în care centrala operatorului a început să întoarcă `403 Forbidden` la toate ieșirile, cu configurația noastră neschimbată și cu apelurile primite funcționând normal. De aceea testăm ieșirile separat înainte să promitem o campanie de sunat, și de aceea nu vindem un volum de apeluri ieșite înainte de acel test.

Ce se întâmplă când agentul nu știe răspunsul?

Predă. Fiecare agent are limite scrise și o cale de transfer configurată. În mesagerie, firul trece în stare de preluare umană, cu motiv și cu alocare către un coleg. La telefon, centrala face transferul — orb sau asistat — către o extensie sau un număr extern. Ce depășește rolul agentului se oprește, nu se improvizează; asta e o alegere de proiectare, pentru că un agent care improvizează într-un domeniu reglementat e un risc, nu o economie.

Se înregistrează convorbirile? Cât se păstrează?

Da, se înregistrează, se transcriu și se analizează, iar totul e legat de spațiul tău de lucru. Cât se păstrează e o decizie a ta ca operator de date, nu o valoare implicită pe care ți-o impunem — și trebuie spus limpede că ștergerea automată la termen nu e azi o simplă setare în platformă: ștergerea la cerere e implementată, ștergerea la termen se face prin procedură. Dacă ai nevoie de un ceas automat, el se construiește în proiect.

Consumă bani dacă vizitatorul apasă butonul și apoi refuză microfonul?

Nu. Microfonul se cere înaintea sesiunii. Dacă omul refuză, cererea de sesiune nu se mai face deloc, deci nu se emite niciun URL semnat și nu se consumă nimic. Separat, înainte de orice operațiune care costă, platforma verifică creditul disponibil — poarta e înaintea apelului, nu pe factura de la sfârșitul lunii.

Pot avea azi un agent care răspunde la un număr de telefon fix?

Depinde de o piesă care nu e agentul: gazda SIP prin care intră apelul. Partea de agent e gata și se poate arăta în pagină imediat. Partea de telefon cere un trunk funcțional de la operator și o centrală care să răspundă; la data scrierii, gazda prin care trec liniile noastre de test nu răspunde, iar noi nu prezentăm ca disponibil ceva ce nu putem porni în fața ta. Pentru un proiect real, telefonia se dimensionează separat — vezi pagina de telefonie și contact center.

Pe ce se sprijină fiecare afirmație de mai sus (21 surse)
  1. app.kallina.info răspunde 200 azi; supabase.kallina.info răspunde la funcțiile edgehttps://app.kallina.info/ · 2026-09-06
  2. 472 de funcții edge Deno și 559 de migrări de bază de date în depozitul Kallina (ls | wc -l = 559):
  3. 195 din cele 559 de migrări conțin `ENABLE ROW LEVEL SECURITY` (grep -rl 'ENABLE ROW LEVEL SECURITY' | wc -l = 195):
  4. Contractul de embed: sesiune cerută platformei cu widget_id, niciodată startSession({agentId}); microfonul cerut înainte de sesiune; SDK importat dinamic în interiorul apăsăriiKallinaVoiceAgent.tsx:5-30, 57-92
  5. Patru poziții de furnizor vocal: elevenlabs, openai-realtime, gemini-live, autoconstants.ts:233-238
  6. TTS implicit non-engleză = eleven_flash_v2_5 (32 de limbi, ~75 ms); eleven_turbo_v2_5 ~250 msconstants.ts:201-207
  7. Zece voci OpenAI Realtime; motoare până la gpt-realtime-2.1constants.ts:210-231
  8. Detecție semantică a vorbirii: threshold 0.5, prefix_padding_ms 300, silence_duration_ms 500; interrupt_response implicit adevăratindex.ts:128-137
  9. Evenimentul `interruption` de la ElevenLabs e transmis centralei Asterisk ca să oprească redareaindex.ts:720-726
  10. turn_timeout e per agent și se sincronizează la furnizor; pe un agent de vânzări e strâns la 12,0 secunde, față de 15 implicitindex.ts:261; 68-71
  11. 32 de limbi în configurația agentului, 41 în harta de limbi, 20 de traduceri de panou; `ro` e limba implicită (20 fișiere):297-335
  12. Preluarea de către om are stare proprie pe fir: handoff_status, handoff_at, handoff_reason, assigned_agent_id, resolution_notes — modul de 619 linii (repository 128 + routes 253 + service 238 = 619 linii):handoff.repository.js:8-45
  13. Transfer orb prin `##` și transfer asistat prin `*2`, cu context de aterizare în dialplanextensions-transfer.conf:1-40
  14. Ștergerea la cerere e implementată (obiecte din storage → RPC execute_erasure → invalidare sesiuni); există și export de date și o funcție pentru datele de apel (356):1-10
  15. Retenția configurabilă per spațiu de lucru NU e implementată: `retention_settings` / `recording_days` apar doar în documentul de proiectare din aprilie, în nicio migraremigrations → zero rezultate:24-45
  16. Izolarea fișierelor e pe cale, cu identificatorul spațiului de lucru ca prim segment: call-recordings, knowledge-documents, voice-samplesCALL-DATA-ISOLATION.md:31-36
  17. Antetele servite azi în producție pe megapromoting.com permit microfonul și sursa SDK-uluihttps://www.megapromoting.com/ · 2026-09-06
  18. Apelurile ieșite pot fi blocate de operator cu 403 Forbidden, cu apelurile primite funcționale și cu configurația noastră neschimbatăREGISTRU-PROBLEME.md:IM-3
  19. Rezumatul conversației pentru memoria clientului se generează în 1-2 propoziții, în românăindex.ts:230-237
  20. Scriptul de verificare a traseului vocal există pe disc și se oprește la URL-ul semnat, fără să deschidă conexiunea audioverify-voice-path.mjs:
  21. Campanii de apeluri ieșite: 1–10 apeluri simultane, import CSV, reîncercări, follow-up SMS, urmărire în timp real cu pauză/reluare/oprireREADME.md:80-83

Citările interne arată numele fișierului și linia. Calea completă rămâne în depozitul nostru; o putem parcurge împreună, la cerere.

Ce ai vrea să funcționeze mai bine?

Povestește-ne despre procesul tău. Împreună stabilim ce merită construit, ce putem conecta și cum verificăm rezultatul.

Hai să discutăm