# Abrir conversa (WebRTC)

Da documentação da API do Lero: https://lero.studio/docs#conversa. Tudo num arquivo só: https://lero.studio/docs/llms-full.txt.

`POST /v1/agents/conversation`

O signaling da conversa ao vivo: você manda a SDP offer do seu client WebRTC e recebe a SDP answer. Depois, o áudio e o data channel `chat` vão peer-to-peer entre o seu client e o agente (STT, LLM e TTS em streaming, com barge-in e turn detection). Sem `versao`, `llm` e `voz`, vale o recomendado. Com `instrucoes`, o agente vira o seu: o system prompt substitui a persona do Lero. Passo a passo em [Conversational Agent: passo a passo](#agente-guia).

Autenticação: chave.

Corpo do pedido (JSON):

- `type` (string, obrigatório): Sempre `offer`.
- `sdp` (string, obrigatório): A SDP offer inteira, com o ICE completo (non-trickle), até 20 mil caracteres.
- `versao` (string, opcional): A versão do agente, um id do `GET /v1/agents`. Recomendado: `v0.4`.
- `llm` (string, opcional): O LLM, um id do `GET /v1/agents`. Recomendado: `oss120b`.
- `voz` (string, opcional): `rio`, `sp` ou `niteroi`. Ignorada na versão de voz única (`vozes: false`).
- `instrucoes` (string, opcional): System prompt, até 30 mil caracteres. Substitui a persona do Lero; as voice rules (nada de markdown, código ou URL na fala) vão junto.
- `conhecimento` (string | array, opcional): Knowledge base (RAG): texto ou `[{titulo, texto}]`, até 50 itens e 60 mil caracteres. O agente consulta e não lê em voz alta.
- `abertura` (string, opcional): First message: como o agente abre a ligação. É uma instrução, não a frase exata.
- `palavras` (string[], opcional): STT keywords: até 40 nomes e termos para reconhecer.
- `silencio_s` (false | number[], opcional): Silence prompts. Desligado com `instrucoes`; `[20, 40, 60]` liga (2 toques e a despedida).
- `pesquisa` (boolean, opcional): Web search tool. Desligada com `instrucoes`.
- `max_tokens` (integer, opcional): Tamanho máximo de cada resposta, de 60 a 1000 (padrão 260).

Responses:

- `200`: A SDP answer: aplique com `setRemoteDescription`.
- `400`: `voz_invalida`
- `400`: `pedido_invalido`
- `401`: `sem_chave`
- `401`: `chave_invalida`
- `401`: `chave_vencida`
- `400`: `oferta_invalida`
- `400`: `versao_invalida`
- `400`: `llm_invalido`
- `400`: `roteiro_invalido`
- `502`: `sem_bot`
- `503`: `desligado`

cURL:

```bash
curl -X POST "https://app.lero.studio/api/v1/agents/conversation" \
  -H "Authorization: Bearer $LERO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "offer",
    "sdp": "v=0\r\no=- ...",
    "versao": "v0.4",
    "llm": "oss120b",
    "voz": "rio",
    "instrucoes": "Você é a recepção da Clínica Exemplo. Marca consultas e tira dúvidas de preço.",
    "conhecimento": [{ "titulo": "Preços", "texto": "Consulta: R$ 250..." }]
  }'
```

JavaScript:

```javascript
// browser: mic and speaker. In production, send the offer to YOUR backend, which calls Lero with the key.
const pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
const chat = pc.createDataChannel("chat");                 // 1. data channel BEFORE the offer
chat.onmessage = (e) => console.log(JSON.parse(e.data));   // events: estado, fala, latencia

const mic = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true } });
mic.getTracks().forEach((t) => pc.addTrack(t, mic));       // 2. audio up
const audio = new Audio(); audio.autoplay = true;
pc.ontrack = (e) => { audio.srcObject = e.streams[0]; };   //    agent voice down

await pc.setLocalDescription(await pc.createOffer());      // 3. offer with complete ICE
await new Promise((ok) => {
  if (pc.iceGatheringState === "complete") return ok();
  pc.onicegatheringstatechange = () => pc.iceGatheringState === "complete" && ok();
  setTimeout(ok, 2500);
});

const res = await fetch("https://app.lero.studio/api/v1/agents/conversation", {      // 4. signaling: offer → answer
  method: "POST",
  headers: { "Authorization": `Bearer ${LERO_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ type: "offer", sdp: pc.localDescription.sdp, versao: "v0.4", llm: "oss120b", voz: "rio" }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.erro}`);
await pc.setRemoteDescription(data);                       // done: the call starts

// hang up: pc.close(); mic.getTracks().forEach((t) => t.stop());
```

Python:

```python
# headless (QA bots, tests, AI agents): pip install aiortc requests
import asyncio, json, os

import requests
from aiortc import RTCPeerConnection, RTCSessionDescription
from aiortc.contrib.media import MediaPlayer, MediaRecorder


async def main():
    pc = RTCPeerConnection()
    chat = pc.createDataChannel("chat")
    chat.on("message", lambda m: print(json.loads(m)))       # transcript and state
    pc.addTrack(MediaPlayer("pergunta.wav").audio)           # what the "person" says
    rec = MediaRecorder("resposta.wav")                      # what the agent says
    pc.on("track", lambda t: rec.addTrack(t))
    await pc.setLocalDescription(await pc.createOffer())     # returns with complete ICE
    r = requests.post(
        "https://app.lero.studio/api/v1/agents/conversation",
        headers={"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"},
        json={"type": "offer", "sdp": pc.localDescription.sdp, "versao": "v0.4", "llm": "oss120b"},
        timeout=20,
    )
    r.raise_for_status()
    await pc.setRemoteDescription(RTCSessionDescription(**r.json()))
    await rec.start()
    await asyncio.sleep(30)
    await rec.stop()
    await pc.close()


asyncio.run(main())
```

Resposta (200):

```json
{
  "type": "answer",
  "sdp": "v=0\r\no=- ..."
}
```
