> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# Integrazione LLM personalizzato

## Panoramica

L'[integrazione nativa con Twilio](/docs/it/eleven-agents/phone-numbers/twilio-integration/native-integration) di ElevenAgents copre il caso in cui ElevenLabs fornisce il modello LLM. Usa questa guida quando hai bisogno del pieno controllo del motore LLM sul tuo server — il tuo modello, pipeline RAG, instradamento delle function call o altri ragionamenti lato server — e l'agente si trova comunque su un numero di telefono Twilio.

La parte relativa al modello LLM personalizzato è fornita da [Speech Engine SDK](/docs/it/eleven-api/guides/cookbooks/speech-engine), che apre un WebSocket tra ElevenLabs e il tuo server, in modo che il tuo LLM possa trasmettere le risposte man mano che la chiamata procede. La parte relativa a Twilio usa [Media Streams](https://www.twilio.com/docs/voice/media-streams) per inoltrare l'audio della chiamata all'agente.

## Architettura

Speech Engine SDK espone due endpoint WebSocket nel sistema di conversazione dell'agente:

* Il **WebSocket del motore** viene eseguito sul tuo server. ElevenLabs si connette per inviare le trascrizioni e ricevere il testo generato dall'LLM.
* Il **WebSocket della conversazione** viene eseguito su ElevenLabs. I client vi si connettono per inviare audio e ricevere in risposta audio sintetizzato. Il bridge Twilio si connette tramite un URL firmato e inoltra l'audio μ-law in entrambe le direzioni.

Poiché Twilio Media Streams e Speech Engine usano entrambi `ulaw_8000`, il bridge inoltra audio codificato in base64 senza transcodifica.

```mermaid
sequenceDiagram
    participant Caller
    participant Twilio
    participant Bridge as Bridge Server
    participant EL as ElevenLabs (conversation WS)
    participant Brain as Brain Server

    Caller->>Twilio: Dial number
    Twilio->>Bridge: POST /incoming-call
    Bridge-->>Twilio: TwiML <Connect><Stream>
    Twilio->>Bridge: WebSocket /media-stream
    Bridge->>EL: Open conversation WebSocket (signed URL)

    loop Conversation
        Caller->>Twilio: Speak
        Twilio->>Bridge: media event (μ-law base64)
        Bridge->>EL: user_audio_chunk
        EL->>Brain: user_transcript
        Brain-->>EL: agent_response (streamed)
        EL->>Bridge: audio event (μ-law base64)
        Bridge->>Twilio: media event
        Twilio->>Caller: Play audio
    end
```

Il bridge e il server del motore possono essere eseguiti nello stesso processo, se è più comodo: l'esempio seguente li combina.

## Quando usare questo schema

Sia questa guida sia l'[integrazione nativa con Twilio](/docs/it/eleven-agents/phone-numbers/twilio-integration/native-integration) collocano un agente su un numero di telefono Twilio. La differenza sta in chi gestisce il modello LLM:

* **Integrazione nativa**: ElevenLabs fornisce il modello LLM, che configuri tramite l'agente. Più semplice.
* **LLM personalizzato tramite Speech Engine SDK** (questa guida): fornisci il modello LLM sul tuo server. Controllo completo sul modello, RAG, function call e logica di business. Più componenti da gestire.

Se la logica del tuo LLM rientra nella configurazione standard dell'agente, preferisci l'integrazione nativa. Usa questa guida quando il tuo motore deve eseguire codice sulla tua infrastruttura.

Questo schema usa Speech Engine SDK, che utilizza una connessione WebSocket per comunicare tra il tuo server e l'API ElevenLabs. Puoi anche usare la guida [LLM personalizzato](/docs/it/eleven-agents/customization/llm/custom-llm), che utilizza un endpoint HTTP compatibile con OpenAI anziché Speech Engine SDK.

La principale differenza tra i due è WebSocket rispetto alle richieste HTTP. L'uso di WebSocket consente di mantenere un'unica connessione invece di stabilire una nuova connessione HTTP per ogni turno, con possibili miglioramenti della latenza.

## Prerequisiti

* Un account [Twilio](https://www.twilio.com/) e un numero di telefono abilitato alle chiamate vocali.
* Una risorsa Speech Engine. Segui la [guida rapida di Speech Engine](/docs/it/eleven-api/guides/cookbooks/speech-engine) per crearne una e conoscere lo schema del server del motore.
* Un tunnel HTTPS pubblico (ad esempio, [ngrok](https://ngrok.com)). Twilio chiama il tuo bridge tramite Internet pubblico.
* Python 3.9+ o Node.js 18+.

## Configura l'agente per l'audio μ-law

Twilio Media Streams usa audio μ-law a 8 kHz. Configura Speech Engine affinché accetti ed emetta lo stesso formato, così il bridge non dovrà effettuare transcodifica.

**`configure_engine.py`**

```python title="configure_engine.py"
import asyncio
import os
from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])


async def update_engine():
    await elevenlabs.speech_engine.update(
        speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
        asr={"user_input_audio_format": "ulaw_8000"},
        tts={
            "model_id": "eleven_flash_v2",
            "agent_output_audio_format": "ulaw_8000",
        },
        speech_engine={
            "request_headers": {"x-api-key": os.environ["SHARED_SECRET"]},
        },
    )


asyncio.run(update_engine())
```

**`configure-engine.mts`**

```typescript title="configure-engine.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import "dotenv/config";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  asr: { userInputAudioFormat: "ulaw_8000" },
  tts: {
    modelId: "eleven_flash_v2",
    agentOutputAudioFormat: "ulaw_8000",
  },
  speechEngine: {
    requestHeaders: { "x-api-key": process.env.SHARED_SECRET! },
  },
});
```

`eleven_flash_v2` mantiene bassa la latenza della sintesi vocale, un aspetto importante in una chiamata telefonica. Il blocco `request_headers` indica a ElevenLabs di includere `x-api-key: <shared-secret>` in ogni connessione WebSocket del motore: il server del motore verifica l'header per assicurarsi che possa raggiungerlo solo il tuo Speech Engine.

## Crea il server bridge

Il bridge espone tre route:

* `POST /incoming-call` — webhook Twilio. Restituisce TwiML che indica a Twilio di aprire un Media Stream verso `/media-stream`.
* `GET /media-stream` — WebSocket di Twilio Media Streams. Inoltra l'audio da e verso il WebSocket della conversazione Speech Engine.
* `GET /ws` — WebSocket del motore. ElevenLabs si connette qui quando inizia una conversazione. Esegue il server standard `engine.serve()` / `engine.attach()`.

#### Installa le dipendenze

**`Python`**

```bash title="Python"
pip install "elevenlabs" "aiohttp" "twilio" "python-dotenv"
```

**`Node`**

```bash title="Node"
npm install @elevenlabs/elevenlabs-js express ws twilio dotenv openai
```

#### Genera un URL firmato per Speech Engine

Il bridge richiede un URL firmato ogni volta che arriva una nuova chiamata. L'URL incorpora l'ID di Speech Engine e una firma monouso, così il bridge non ha mai bisogno della chiave API non elaborata.

**`bridge.py`**

```python title="bridge.py"
from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])

async def signed_url() -> str:
    response = await elevenlabs.conversational_ai.conversations.get_signed_url(
        agent_id=os.environ["SPEECH_ENGINE_ID"],
    )
    return response.signed_url
```

**`bridge.mts`**

```typescript title="bridge.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

async function signedUrl(): Promise<string> {
  const response = await elevenlabs.conversationalAi.conversations.getSignedUrl({
    agentId: process.env.SPEECH_ENGINE_ID!,
  });
  return response.signedUrl;
}
```

#### Fornisci la risposta TwiML

Quando arriva una chiamata, Twilio invia una richiesta POST a `/incoming-call`. La risposta è TwiML che apre un Media Stream verso il WebSocket `/media-stream` del bridge.

**`bridge.py`**

```python title="bridge.py"
from aiohttp import web
from twilio.request_validator import RequestValidator

validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])


async def incoming_call(request: web.Request) -> web.Response:
    form = await request.post()
    signature = request.headers.get("X-Twilio-Signature", "")
    url = str(request.url)
    if not validator.validate(url, dict(form), signature):
        return web.Response(status=403, text="forbidden")

    host = request.headers.get("X-Forwarded-Host") or request.host
    twiml = (
        '<?xml version="1.0" encoding="UTF-8"?>'
        "<Response><Connect>"
        f'<Stream url="wss://{host}/media-stream"/>'
        "</Connect></Response>"
    )
    return web.Response(text=twiml, content_type="text/xml")
```

**`bridge.mts`**

```typescript title="bridge.mts"
import express from "express";
import twilio from "twilio";

const app = express();
app.use(express.urlencoded({ extended: false }));

app.post(
  "/incoming-call",
  twilio.webhook({ validate: true }),
  (req, res) => {
    const host = req.headers["x-forwarded-host"] ?? req.get("host");
    const twiml = `<?xml version="1.0" encoding="UTF-8"?>
      <Response>
        <Connect>
          <Stream url="wss://${host}/media-stream"/>
        </Connect>
      </Response>`;
    res.type("text/xml").send(twiml);
  },
);
```

`RequestValidator` (Python) e `twilio.webhook({ validate: true })` (Node) verificano l'header `X-Twilio-Signature` rispetto a `TWILIO_AUTH_TOKEN`. Senza convalida, chiunque su Internet pubblico potrebbe inviare una richiesta POST a `/incoming-call` e addebitare chiamate al tuo account.

#### Collega il Media Stream

Il Media Stream è un WebSocket che invia una sequenza di eventi JSON: `connected`, `start`, `media` (il payload audio) e `stop`. Il bridge apre un WebSocket della conversazione Speech Engine su `start` e inoltra l'audio in entrambe le direzioni finché lo stream non si chiude.

**`bridge.py`**

```python title="bridge.py" maxLines=0
import asyncio
import json

import aiohttp
from aiohttp import web


async def media_stream(request: web.Request) -> web.WebSocketResponse:
    twilio_ws = web.WebSocketResponse()
    await twilio_ws.prepare(request)

    stream_sid: str | None = None
    el_session: aiohttp.ClientSession | None = None
    el_ws: aiohttp.ClientWebSocketResponse | None = None
    pump_task: asyncio.Task | None = None

    async def pump_el_to_twilio(el: aiohttp.ClientWebSocketResponse):
        async for msg in el:
            if msg.type != aiohttp.WSMsgType.TEXT:
                continue
            event = json.loads(msg.data)
            etype = event.get("type")
            if etype == "audio":
                await twilio_ws.send_str(json.dumps({
                    "event": "media",
                    "streamSid": stream_sid,
                    "media": {"payload": event["audio_event"]["audio_base_64"]},
                }))
            elif etype == "interruption":
                await twilio_ws.send_str(json.dumps({
                    "event": "clear",
                    "streamSid": stream_sid,
                }))
            elif etype == "ping":
                event_id = event.get("ping_event", {}).get("event_id")
                await el.send_str(json.dumps({
                    "type": "pong", "event_id": event_id,
                }))

    try:
        async for msg in twilio_ws:
            if msg.type != aiohttp.WSMsgType.TEXT:
                continue
            event = json.loads(msg.data)

            if event["event"] == "start":
                stream_sid = event["start"]["streamSid"]
                el_session = aiohttp.ClientSession()
                el_ws = await el_session.ws_connect(await signed_url())
                await el_ws.send_str(json.dumps({
                    "type": "conversation_initiation_client_data",
                }))
                pump_task = asyncio.create_task(pump_el_to_twilio(el_ws))

            elif event["event"] == "media" and el_ws is not None:
                await el_ws.send_str(json.dumps({
                    "user_audio_chunk": event["media"]["payload"],
                }))

            elif event["event"] == "stop":
                break
    finally:
        if pump_task:
            pump_task.cancel()
        if el_ws and not el_ws.closed:
            await el_ws.close()
        if el_session and not el_session.closed:
            await el_session.close()

    return twilio_ws
```

**`bridge.mts`**

```typescript title="bridge.mts" maxLines=0
import { WebSocket, WebSocketServer } from "ws";
import { createServer } from "node:http";

const httpServer = createServer(app);
const wss = new WebSocketServer({ noServer: true });

httpServer.on("upgrade", (req, socket, head) => {
  if (req.url === "/media-stream") {
    wss.handleUpgrade(req, socket, head, (ws) => handleMediaStream(ws));
  } else {
    socket.destroy();
  }
});

async function handleMediaStream(twilioWs: WebSocket) {
  let streamSid: string | null = null;
  let elReady: Promise<WebSocket | null> | null = null;

  twilioWs.on("message", async (raw) => {
    const event = JSON.parse(raw.toString());

    if (event.event === "start") {
      streamSid = event.start.streamSid;
      // Convert rejection into a clean null + close so a failed signed-URL
      // fetch doesn't become an unhandled rejection on the next media event.
      elReady = openElevenLabsWebSocket(twilioWs, () => streamSid).catch((err) => {
        console.error("Failed to open Speech Engine conversation:", err);
        twilioWs.close();
        return null;
      });
    } else if (event.event === "media" && elReady) {
      const elWs = await elReady;
      if (!elWs) return;
      elWs.send(JSON.stringify({
        user_audio_chunk: event.media.payload,
      }));
    } else if (event.event === "stop") {
      twilioWs.close();
    }
  });

  twilioWs.on("close", async () => {
    (await elReady)?.close();
  });
}

async function openElevenLabsWebSocket(
  twilioWs: WebSocket,
  getStreamSid: () => string | null,
): Promise<WebSocket> {
  const elWs = new WebSocket(await signedUrl());
  await new Promise<void>((resolve, reject) => {
    elWs.once("open", () => resolve());
    elWs.once("error", reject);
  });
  elWs.send(JSON.stringify({
    type: "conversation_initiation_client_data",
  }));

  elWs.on("message", (raw) => {
    const event = JSON.parse(raw.toString());
    const streamSid = getStreamSid();
    if (event.type === "audio") {
      twilioWs.send(JSON.stringify({
        event: "media",
        streamSid,
        media: { payload: event.audio_event.audio_base_64 },
      }));
    } else if (event.type === "interruption") {
      twilioWs.send(JSON.stringify({ event: "clear", streamSid }));
    } else if (event.type === "ping") {
      elWs.send(JSON.stringify({
        type: "pong", event_id: event.ping_event?.event_id,
      }));
    }
  });

  return elWs;
}
```

L'evento `interruption` di Speech Engine attiva un evento `clear` nello stream Twilio, che elimina l'audio in buffer affinché l'interruzione vocale funzioni correttamente. All'evento `ping` viene risposto con `pong` per mantenere attivo il WebSocket della conversazione.

#### Esegui anche il server del motore

Il server del motore è il server Speech Engine standard mostrato nella [guida rapida](/docs/it/eleven-api/guides/cookbooks/speech-engine). L'unica aggiunta è la verifica del segreto condiviso durante l'upgrade WebSocket: accetta la connessione solo se `x-api-key` corrisponde al valore impostato in Speech Engine.

**`bridge.py`**

```python title="bridge.py" maxLines=0
import os

from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
SHARED_SECRET = os.environ["SHARED_SECRET"]


async def brain_ws(request: web.Request) -> web.WebSocketResponse:
    if request.headers.get("x-api-key") != SHARED_SECRET:
        return web.Response(status=401, text="unauthorized")

    ws = web.WebSocketResponse()
    await ws.prepare(request)

    engine = await elevenlabs.speech_engine.get(os.environ["SPEECH_ENGINE_ID"])
    session = engine.create_session(ws)

    async def on_transcript(transcript):
        # Replace this with your own LLM call; see the quickstart.
        await session.send_response("Hello, you've reached the demo.")

    session.on("user_transcript", on_transcript)
    await session.run()
    return ws


def make_app() -> web.Application:
    app = web.Application()
    app.router.add_post("/incoming-call", incoming_call)
    app.router.add_get("/media-stream", media_stream)
    app.router.add_get("/ws", brain_ws)
    return app


if __name__ == "__main__":
    web.run_app(make_app(), port=3001)
```

**`bridge.mts`**

```typescript title="bridge.mts" maxLines=0
httpServer.on("upgrade", async (req, socket, head) => {
  if (req.url === "/ws") {
    if (req.headers["x-api-key"] !== process.env.SHARED_SECRET) {
      socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
      socket.destroy();
      return;
    }
    // Hand off to engine.attach() — see the Speech Engine quickstart.
  } else if (req.url === "/media-stream") {
    wss.handleUpgrade(req, socket, head, (ws) => handleMediaStream(ws));
  } else {
    socket.destroy();
  }
});

httpServer.listen(3001);
```

Consulta la [guida rapida di Speech Engine](/docs/it/eleven-api/guides/cookbooks/speech-engine#server-setup) per l'implementazione completa di `on_transcript`, inclusa una chiamata LLM e una risposta in streaming.

## Indica a Twilio il bridge

#### Avvia il bridge e un tunnel pubblico

```bash
ngrok http 3001
python bridge.py
```

Prendi nota dell'URL `https://` visualizzato da ngrok: Twilio vi invierà richieste POST.

#### Aggiorna ws\_url di Speech Engine

Imposta `speech_engine.ws_url` sull'URL WebSocket pubblico dell'endpoint del tuo motore, così ElevenLabs sa dove connettersi.

```python
await elevenlabs.speech_engine.update(
    speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
    speech_engine={"ws_url": "wss://abc123.ngrok.io/ws"},
)
```

```typescript
await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  speechEngine: { wsUrl: "wss://abc123.ngrok.io/ws" },
});
```

#### Configura il numero Twilio

Nella console Twilio, apri la **Voice Configuration** del tuo numero di telefono:

* **A call comes in**: Webhook
* **URL**: `https://abc123.ngrok.io/incoming-call`
* **HTTP method**: POST

Se il numero è collegato a un Elastic SIP Trunk, scollegalo prima: un numero Twilio viene instradato a un trunk oppure a un webhook, non a entrambi.

#### Chiama il numero

Chiama il numero da qualsiasi telefono. L'agente risponderà; parla durante la chiamata e dovresti sentire la risposta dell'agente. Con il logging di debug abilitato, il bridge registra il SID della chiamata, l'ID della conversazione e il formato audio per ogni turno.

## Considerazioni per la produzione

* **Convalida del webhook**: convalida sempre `X-Twilio-Signature` su `/incoming-call`. L'esempio precedente usa la libreria di supporto di Twilio; non saltare questo passaggio.
* **Segreto condiviso**: applica il segreto condiviso sul WebSocket del motore. Senza di esso, chiunque indovini il tuo URL ngrok può connettersi e impersonare ElevenLabs.
* **Host stabile**: gli URL del piano gratuito di ngrok cambiano a ogni riavvio. Usa un dominio ngrok riservato o un hostname reale per non dover aggiornare `ws_url` di Speech Engine e il webhook Twilio dopo ogni riavvio.
* **Latenza**: ogni chiamata aggiunge due passaggi di rete al tempo al primo token dell'LLM. Usa un modello a bassa latenza e trasmetti le risposte in streaming per ridurre la latenza percepita.
* **Un processo o due**: l'esempio colloca il bridge e il motore sulla stessa porta, così un unico tunnel ngrok copre tutto. In produzione, puoi suddividerli in due servizi, purché ciascuno disponga di un URL pubblico.
* **Prompt injection**: l'input vocale di una chiamata telefonica è input utente non attendibile. Convalida le trascrizioni prima che influenzino chiamate agli strumenti o scritture nel database.

## Passaggi successivi

#### [Integrazione nativa con Twilio](/docs/it/eleven-agents/phone-numbers/twilio-integration/native-integration)

Usa il modello LLM fornito anziché uno personalizzato.

#### [Guida rapida di Speech Engine](/docs/it/eleven-api/guides/cookbooks/speech-engine)

Crea il server del motore dall'inizio alla fine con un LLM in streaming.

#### [LLM personalizzato (compatibile con OpenAI)](/docs/it/eleven-agents/customization/llm/custom-llm)

Un meccanismo LLM personalizzato alternativo che usa un endpoint HTTP compatibile con OpenAI.

#### [Riferimento SDK Python](/docs/it/eleven-api/resources/libraries/speech-engine/python-sdk-reference)

Classi, metodi ed eventi per l'SDK Python di Speech Engine.

#### [Riferimento SDK JavaScript](/docs/it/eleven-api/resources/libraries/speech-engine/javascript-sdk-reference)

Classi, metodi ed eventi per l'SDK JavaScript di Speech Engine.