ermis-voice 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ["3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python }}
19
+ - run: pip install -e ".[dev]"
20
+ - run: ruff check .
21
+ - run: ruff format --check .
22
+ - run: python -m pytest -q
@@ -0,0 +1,36 @@
1
+ # Publishes to PyPI when a GitHub release is published (trusted publishing, no token in the repo).
2
+ # One-time setup on PyPI: add a pending publisher for project "ermis-voice" with
3
+ # owner Ermisai, repository ermis-voice-sdk, workflow publish.yml and environment pypi.
4
+ name: Publish to PyPI
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: pip install build
19
+ - run: python -m build
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment: pypi
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .env
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0a1 (prévia)
4
+
5
+ - Primeira versão pública do SDK Python da Ermis Voice API: sessões (`twilio`, `browser`, `websocket`), canal de controle com eventos tipados, `decide`, `extract_slot`, mensagens de espera (`WaitingOptions`, com frases por intenção e pausa antes da resposta), `transcription_priority`, TwiML pronto e exceções por código de erro.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ermis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.5
2
+ Name: ermis-voice
3
+ Version: 0.1.0a1
4
+ Summary: SDK Python da Ermis Voice API: crie sessões de voz e conduza a conversa pelo canal de controle.
5
+ Project-URL: Homepage, https://github.com/Ermisai/ermis-voice-sdk
6
+ Project-URL: Documentation, https://github.com/Ermisai/ermis-voice-sdk/blob/main/docs/api.md
7
+ Project-URL: Issues, https://github.com/Ermisai/ermis-voice-sdk/issues
8
+ Author: Ermis
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ermis,speech,telephony,twilio,voice,voice-agent
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Natural Language :: Portuguese (Brazilian)
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Communications :: Telephony
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: websockets>=13
22
+ Provides-Extra: dev
23
+ Requires-Dist: fastapi>=0.110; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
25
+ Requires-Dist: pytest>=8.0; extra == 'dev'
26
+ Requires-Dist: python-multipart>=0.0.9; extra == 'dev'
27
+ Requires-Dist: ruff==0.8.4; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Ermis Voice SDK para Python
31
+
32
+ [![CI](https://github.com/Ermisai/ermis-voice-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Ermisai/ermis-voice-sdk/actions/workflows/ci.yml)
33
+
34
+ > **Prévia (0.1.0a1).** A Voice API ainda está em desenvolvimento; nomes e campos podem mudar até o lançamento.
35
+
36
+ O SDK liga o **seu backend** à **Ermis Voice API**. Ele cria sessões, abre o canal de controle e entrega os eventos da ligação como objetos Python, com métodos para mandar o agente agir.
37
+
38
+ ## O que é de quem
39
+
40
+ | | A **Voice API** (nos servidores da Ermis) | O **SDK** (no seu servidor) | **Você** |
41
+ |---|---|---|---|
42
+ | Áudio da ligação | Recebe do Twilio/navegador, detecta fala e interrupções | Não toca no áudio (exceto no transporte `websocket`, onde só repassa quadros) | Conecta o áudio à Ermis (TwiML, navegador) |
43
+ | O que o cliente disse | Transcreve e avalia (`transcript`, `assessment`) | Converte em `Transcript` | Decide o que fazer |
44
+ | O que o cliente quer | Classifica intenção, extrai dados (`decision`, `slot`) | `Decision`; `decide()` e `extract_slot()` viram chamadas com retorno | Usa para escolher a resposta |
45
+ | Resposta | Sintetiza e fala, para no barge-in | `speak()`, `say()` | Escreve o texto (seu roteiro ou sua IA) |
46
+ | Espera | Fala frases de espera e aplica o tempo limite | `WaitingOptions`, `thinking()` | Configura as frases |
47
+ | Cobrança | Mede por segundo, desconta créditos | `usage()`, `balance()`, exceções de crédito | — |
48
+
49
+ Resumindo: **a Voice API conduz a ligação; o SDK é o controle remoto; a inteligência do negócio é sua.** Tudo o que o SDK faz também pode ser feito com HTTP e WebSocket puros, seguindo a [documentação da API](https://github.com/Ermisai/ermis-voice-sdk/blob/main/docs/api.md). O SDK só tira o trabalho repetitivo: autenticação, montagem do JSON, ids de correlação, tipos, erros e o TwiML.
50
+
51
+ ## Instalação
52
+
53
+ ```bash
54
+ pip install ermis-voice # Python 3.10+
55
+ export ERMIS_API_KEY=ek_test_...
56
+ ```
57
+
58
+ O pacote se chama `ermis-voice` no pip e é importado como `ermis`:
59
+
60
+ ```python
61
+ from ermis import Ermis
62
+ ```
63
+
64
+ Para instalar direto do GitHub (por exemplo, uma versão ainda não publicada no PyPI):
65
+
66
+ ```bash
67
+ pip install "git+https://github.com/Ermisai/ermis-voice-sdk.git"
68
+ ```
69
+
70
+ Durante a prévia, a chave e a URL da Voice API são entregues pela equipe da Ermis. Defina `ERMIS_API_URL` se ela for diferente de `https://api.ermis.ai`.
71
+
72
+ Chaves `ek_test_…` usam o Sandbox; `ek_live_…`, produção. **Use a chave só no servidor.**
73
+
74
+ ## Início rápido (Twilio)
75
+
76
+ ```python
77
+ import asyncio
78
+
79
+ from fastapi import FastAPI, Form, Response
80
+ from ermis import Ermis, SessionStarted, Decision
81
+
82
+ app = FastAPI()
83
+ ermis = Ermis() # lê ERMIS_API_KEY
84
+
85
+ @app.post("/twilio/voz")
86
+ async def ligacao(CallSid: str = Form(...)):
87
+ session = await ermis.sessions.create(transport="twilio", metadata={"call_sid": CallSid})
88
+ asyncio.create_task(conversar(session))
89
+ return Response(session.twiml(), media_type="application/xml") # o áudio vai direto para a Ermis
90
+
91
+ async def conversar(session):
92
+ async with ermis.connect(session) as call:
93
+ async for event in call:
94
+ if isinstance(event, SessionStarted):
95
+ await call.speak("Olá! Como posso ajudar?", calibrate=True)
96
+ elif isinstance(event, Decision):
97
+ await call.speak(await sua_ia(event))
98
+ ```
99
+
100
+ Exemplo completo, com respostas rápidas, confirmação, desistência e despedida: [`examples/atendente_twilio.py`](https://github.com/Ermisai/ermis-voice-sdk/blob/main/examples/atendente_twilio.py).
101
+
102
+ ## Referência
103
+
104
+ ### `Ermis(api_key=None, *, base_url=None, timeout=10.0, http_client=None)`
105
+
106
+ Cliente assíncrono. Sem `api_key`, lê `ERMIS_API_KEY`; sem `base_url`, lê `ERMIS_API_URL` ou usa `https://api.ermis.ai`. Feche com `await ermis.aclose()` ou use `async with Ermis() as ermis`.
107
+
108
+ | Método | Chamada da API | Retorno |
109
+ |---|---|---|
110
+ | `await ermis.sessions.create(**opções)` | `POST /v1/voice/sessions` | `Session` |
111
+ | `await ermis.sessions.get(session_id)` | `GET /v1/voice/sessions/{id}` | `dict` |
112
+ | `await ermis.sessions.end(session_id)` | `DELETE /v1/voice/sessions/{id}` | `None` |
113
+ | `await ermis.usage(from_=, to=, group_by="day")` | `GET /v1/usage` | `dict` |
114
+ | `await ermis.balance()` | `GET /v1/account/balance` | `dict` |
115
+ | `ermis.connect(session_or_control_url)` | WebSocket `control_url` | `Call` (use com `async with`) |
116
+
117
+ **Opções de `sessions.create`** (todas opcionais; o que você não passar usa o padrão da API):
118
+ `transport` (`"twilio"`, `"browser"`, `"websocket"`), `audio_format` (`"mulaw_8k"`, `"pcm16_48k"`), `language`, `voice`, `auto_decide`, `transcription_priority` (`"balanced"`, `"latency"`, `"accuracy"`), `vad=VadOptions(min_silence_ms=…)`, `barge_in=BargeInOptions(enabled=…, min_speech_ms=…)`, `waiting=WaitingOptions(…)`, `max_duration_s`, `metadata`.
119
+
120
+ ```python
121
+ from ermis import WaitingOptions
122
+
123
+ waiting = WaitingOptions(
124
+ filler_after_ms=800, # None desliga a frase de espera
125
+ filler_messages=["Certo, um momento."],
126
+ filler_by_intent={"complaint": ["Entendo, sinto muito por isso."]}, # frase pela intenção da fala
127
+ gap_after_ms=600, # pausa antes da sua resposta; None desliga
128
+ patience_after_ms=3500,
129
+ patience_every_ms=8000,
130
+ patience_messages=["Ainda estou verificando…"],
131
+ timeout_ms=20000, # None: espera sem limite
132
+ timeout_message="Desculpe, não consegui essa informação agora.",
133
+ timeout_action="apologize", # ou "end"
134
+ )
135
+ ```
136
+
137
+ ### `Session`
138
+
139
+ `session_id`, `audio_url`, `control_url`, `session_token`, e:
140
+
141
+ - `session.twiml()`: TwiML pronto para o transporte `twilio`, com o token escapado.
142
+ - `session.browser_credentials()`: `{"audio_url", "session_token"}`, o que entregar ao navegador no transporte `browser`. A chave de API nunca sai do servidor.
143
+
144
+ ### `Call`: o canal de controle
145
+
146
+ `async for event in call` recebe os eventos até `SessionEnded`. Os comandos podem ser chamados a qualquer momento, inclusive enquanto você trata um evento.
147
+
148
+ | Método | Mensagem enviada | Retorno |
149
+ |---|---|---|
150
+ | `await call.speak(text, *, voice=, allow_barge_in=, calibrate=, id=)` | `speak` | o `id` da fala, na hora |
151
+ | `await call.say(text, **mesmas_opções)` | `speak` e espera | `True` se falou tudo, `False` se o cliente interrompeu |
152
+ | `await call.interrupt()` | `interrupt` | — |
153
+ | `await call.decide(text, custom_intents=[CustomIntent(key, label, description)])` | `decide` | `Decision` |
154
+ | `await call.extract_slot(text, name, type="text", *, description=)` | `extract_slot` | `SlotResult` |
155
+ | `await call.thinking(message=None, *, timeout_ms=None)` | `thinking` | — |
156
+ | `await call.done_processing()` | `done_processing` | — |
157
+ | `await call.end()` | `end` | — |
158
+ | `await call.send_audio(frame)` | quadro binário (só `websocket`) | — |
159
+ | `await call.playback_finished(speak_id)` | `playback_finished` (só `websocket`) | — |
160
+ | `await call.send(dict)` | qualquer mensagem crua | — |
161
+
162
+ `decide()` e `extract_slot()` geram o `id`, esperam a resposta com esse `id` (padrão: 10 s) e devolvem o resultado. Essas respostas **não** aparecem na iteração. Depois do fim, `call.ended` guarda o `SessionEnded`.
163
+
164
+ ### Eventos
165
+
166
+ | Classe | Evento da API | Campos |
167
+ |---|---|---|
168
+ | `SessionStarted` | `session.started` | `session_id`, `limits` |
169
+ | `SpeechStarted` / `SpeechEnded` | `speech.started` / `speech.ended` | `duration_ms` |
170
+ | `Transcript` | `transcript` | `utterance_id`, `text`, `uncertain`, `assessment`, `latency_ms` |
171
+ | `Decision` | `decision` | `intent`, `action`, `is_escape`, `complexity`, `custom_intent`, `extracted`, `utterance_id`, `id` |
172
+ | `SlotResult` | `slot` | `id`, `value`, `status`, `found` |
173
+ | `SpeakStarted` / `SpeakDone` | `speak.started` / `speak.done` | `id`, `chars` |
174
+ | `BargeIn` | `barge_in` | `id` |
175
+ | `WaitingSpoken` | `waiting` | `utterance_id`, `kind`, `text` |
176
+ | `ReplyTimeout` | `reply_timeout` | `utterance_id`, `action` |
177
+ | `Usage` | `usage` | `billed_seconds`, `credits`, `balance` |
178
+ | `WarningEvent` | `warning` | `code`, `message` |
179
+ | `ErrorEvent` | `error` | `code`, `message` |
180
+ | `SessionEnded` | `session.ended` | `reason`, `billed_seconds`, `credits` |
181
+ | `Audio` | quadro binário (só `websocket`) | `data` |
182
+ | `UnknownEvent` | qualquer outro | — |
183
+
184
+ Todo evento tem `type` e `raw` (o JSON original). Eventos novos da API chegam como `UnknownEvent` em vez de quebrar o seu código.
185
+
186
+ `Transcript.assessment` traz `ignore`, `ask_to_repeat`, `unproductive`, `confirm` e o atalho `usable` (nem `ignore`, nem `ask_to_repeat`, nem `unproductive`).
187
+
188
+ ### Erros
189
+
190
+ O mesmo erro chega como status HTTP ou código de fechamento do WebSocket; o SDK levanta a mesma exceção nos dois casos. Todas herdam de `ErmisError` (`message`, `code`, `status`).
191
+
192
+ | Exceção | HTTP / WebSocket |
193
+ |---|---|
194
+ | `AuthenticationError` | 401 / 4001 |
195
+ | `InsufficientCreditsError` | 402 / 4002 |
196
+ | `ChannelLimitError` | 429 / 4029 |
197
+ | `InvalidRequestError` | 400 / 4400 (também quando `decide`/`extract_slot` recebem `error`) |
198
+ | `SessionTimeoutError` | 4008 (60 s sem áudio) |
199
+ | `MaxDurationError` | 4013 |
200
+ | `APIError` | 500 / 1011 |
201
+ | `ConnectionClosedError` | o canal fechou antes de `session.ended` |
202
+
203
+ ## Outros transportes
204
+
205
+ **Navegador.** Crie a sessão com `transport="browser", audio_format="pcm16_48k"` e envie `session.browser_credentials()` ao navegador. O navegador abre `audio_url?session_token=…` e troca áudio direto com a Ermis; o seu backend conduz a conversa com `ermis.connect(session)`, como no Twilio.
206
+
207
+ **Seu próprio WebSocket.** Com `transport="websocket"`, o mesmo `Call` leva áudio e controle: envie com `call.send_audio(frame)`, receba `Audio` na iteração e avise o fim de cada fala com `call.playback_finished(speak_id)`.
208
+
209
+ ## Documentação
210
+
211
+ - [Referência da Voice API](https://github.com/Ermisai/ermis-voice-sdk/blob/main/docs/api.md): sessões, canal de controle, decisões, mensagens de espera, áudio e erros.
212
+ - Na plataforma da Ermis: **Documentação API → Ermis Voice** e a aba **Ligação** da Área de Teste, para conversar com o agente pelo microfone.
213
+
214
+ ## Desenvolvimento
215
+
216
+ ```bash
217
+ uv venv && uv pip install -e ".[dev]"
218
+ .venv/bin/python -m pytest -q
219
+ ```
220
+
221
+ Os testes rodam contra uma Voice API simulada (REST com `httpx.MockTransport`, controle com um servidor WebSocket local).
222
+
223
+ ## Licença
224
+
225
+ MIT. Veja [LICENSE](https://github.com/Ermisai/ermis-voice-sdk/blob/main/LICENSE).
@@ -0,0 +1,196 @@
1
+ # Ermis Voice SDK para Python
2
+
3
+ [![CI](https://github.com/Ermisai/ermis-voice-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Ermisai/ermis-voice-sdk/actions/workflows/ci.yml)
4
+
5
+ > **Prévia (0.1.0a1).** A Voice API ainda está em desenvolvimento; nomes e campos podem mudar até o lançamento.
6
+
7
+ O SDK liga o **seu backend** à **Ermis Voice API**. Ele cria sessões, abre o canal de controle e entrega os eventos da ligação como objetos Python, com métodos para mandar o agente agir.
8
+
9
+ ## O que é de quem
10
+
11
+ | | A **Voice API** (nos servidores da Ermis) | O **SDK** (no seu servidor) | **Você** |
12
+ |---|---|---|---|
13
+ | Áudio da ligação | Recebe do Twilio/navegador, detecta fala e interrupções | Não toca no áudio (exceto no transporte `websocket`, onde só repassa quadros) | Conecta o áudio à Ermis (TwiML, navegador) |
14
+ | O que o cliente disse | Transcreve e avalia (`transcript`, `assessment`) | Converte em `Transcript` | Decide o que fazer |
15
+ | O que o cliente quer | Classifica intenção, extrai dados (`decision`, `slot`) | `Decision`; `decide()` e `extract_slot()` viram chamadas com retorno | Usa para escolher a resposta |
16
+ | Resposta | Sintetiza e fala, para no barge-in | `speak()`, `say()` | Escreve o texto (seu roteiro ou sua IA) |
17
+ | Espera | Fala frases de espera e aplica o tempo limite | `WaitingOptions`, `thinking()` | Configura as frases |
18
+ | Cobrança | Mede por segundo, desconta créditos | `usage()`, `balance()`, exceções de crédito | — |
19
+
20
+ Resumindo: **a Voice API conduz a ligação; o SDK é o controle remoto; a inteligência do negócio é sua.** Tudo o que o SDK faz também pode ser feito com HTTP e WebSocket puros, seguindo a [documentação da API](https://github.com/Ermisai/ermis-voice-sdk/blob/main/docs/api.md). O SDK só tira o trabalho repetitivo: autenticação, montagem do JSON, ids de correlação, tipos, erros e o TwiML.
21
+
22
+ ## Instalação
23
+
24
+ ```bash
25
+ pip install ermis-voice # Python 3.10+
26
+ export ERMIS_API_KEY=ek_test_...
27
+ ```
28
+
29
+ O pacote se chama `ermis-voice` no pip e é importado como `ermis`:
30
+
31
+ ```python
32
+ from ermis import Ermis
33
+ ```
34
+
35
+ Para instalar direto do GitHub (por exemplo, uma versão ainda não publicada no PyPI):
36
+
37
+ ```bash
38
+ pip install "git+https://github.com/Ermisai/ermis-voice-sdk.git"
39
+ ```
40
+
41
+ Durante a prévia, a chave e a URL da Voice API são entregues pela equipe da Ermis. Defina `ERMIS_API_URL` se ela for diferente de `https://api.ermis.ai`.
42
+
43
+ Chaves `ek_test_…` usam o Sandbox; `ek_live_…`, produção. **Use a chave só no servidor.**
44
+
45
+ ## Início rápido (Twilio)
46
+
47
+ ```python
48
+ import asyncio
49
+
50
+ from fastapi import FastAPI, Form, Response
51
+ from ermis import Ermis, SessionStarted, Decision
52
+
53
+ app = FastAPI()
54
+ ermis = Ermis() # lê ERMIS_API_KEY
55
+
56
+ @app.post("/twilio/voz")
57
+ async def ligacao(CallSid: str = Form(...)):
58
+ session = await ermis.sessions.create(transport="twilio", metadata={"call_sid": CallSid})
59
+ asyncio.create_task(conversar(session))
60
+ return Response(session.twiml(), media_type="application/xml") # o áudio vai direto para a Ermis
61
+
62
+ async def conversar(session):
63
+ async with ermis.connect(session) as call:
64
+ async for event in call:
65
+ if isinstance(event, SessionStarted):
66
+ await call.speak("Olá! Como posso ajudar?", calibrate=True)
67
+ elif isinstance(event, Decision):
68
+ await call.speak(await sua_ia(event))
69
+ ```
70
+
71
+ Exemplo completo, com respostas rápidas, confirmação, desistência e despedida: [`examples/atendente_twilio.py`](https://github.com/Ermisai/ermis-voice-sdk/blob/main/examples/atendente_twilio.py).
72
+
73
+ ## Referência
74
+
75
+ ### `Ermis(api_key=None, *, base_url=None, timeout=10.0, http_client=None)`
76
+
77
+ Cliente assíncrono. Sem `api_key`, lê `ERMIS_API_KEY`; sem `base_url`, lê `ERMIS_API_URL` ou usa `https://api.ermis.ai`. Feche com `await ermis.aclose()` ou use `async with Ermis() as ermis`.
78
+
79
+ | Método | Chamada da API | Retorno |
80
+ |---|---|---|
81
+ | `await ermis.sessions.create(**opções)` | `POST /v1/voice/sessions` | `Session` |
82
+ | `await ermis.sessions.get(session_id)` | `GET /v1/voice/sessions/{id}` | `dict` |
83
+ | `await ermis.sessions.end(session_id)` | `DELETE /v1/voice/sessions/{id}` | `None` |
84
+ | `await ermis.usage(from_=, to=, group_by="day")` | `GET /v1/usage` | `dict` |
85
+ | `await ermis.balance()` | `GET /v1/account/balance` | `dict` |
86
+ | `ermis.connect(session_or_control_url)` | WebSocket `control_url` | `Call` (use com `async with`) |
87
+
88
+ **Opções de `sessions.create`** (todas opcionais; o que você não passar usa o padrão da API):
89
+ `transport` (`"twilio"`, `"browser"`, `"websocket"`), `audio_format` (`"mulaw_8k"`, `"pcm16_48k"`), `language`, `voice`, `auto_decide`, `transcription_priority` (`"balanced"`, `"latency"`, `"accuracy"`), `vad=VadOptions(min_silence_ms=…)`, `barge_in=BargeInOptions(enabled=…, min_speech_ms=…)`, `waiting=WaitingOptions(…)`, `max_duration_s`, `metadata`.
90
+
91
+ ```python
92
+ from ermis import WaitingOptions
93
+
94
+ waiting = WaitingOptions(
95
+ filler_after_ms=800, # None desliga a frase de espera
96
+ filler_messages=["Certo, um momento."],
97
+ filler_by_intent={"complaint": ["Entendo, sinto muito por isso."]}, # frase pela intenção da fala
98
+ gap_after_ms=600, # pausa antes da sua resposta; None desliga
99
+ patience_after_ms=3500,
100
+ patience_every_ms=8000,
101
+ patience_messages=["Ainda estou verificando…"],
102
+ timeout_ms=20000, # None: espera sem limite
103
+ timeout_message="Desculpe, não consegui essa informação agora.",
104
+ timeout_action="apologize", # ou "end"
105
+ )
106
+ ```
107
+
108
+ ### `Session`
109
+
110
+ `session_id`, `audio_url`, `control_url`, `session_token`, e:
111
+
112
+ - `session.twiml()`: TwiML pronto para o transporte `twilio`, com o token escapado.
113
+ - `session.browser_credentials()`: `{"audio_url", "session_token"}`, o que entregar ao navegador no transporte `browser`. A chave de API nunca sai do servidor.
114
+
115
+ ### `Call`: o canal de controle
116
+
117
+ `async for event in call` recebe os eventos até `SessionEnded`. Os comandos podem ser chamados a qualquer momento, inclusive enquanto você trata um evento.
118
+
119
+ | Método | Mensagem enviada | Retorno |
120
+ |---|---|---|
121
+ | `await call.speak(text, *, voice=, allow_barge_in=, calibrate=, id=)` | `speak` | o `id` da fala, na hora |
122
+ | `await call.say(text, **mesmas_opções)` | `speak` e espera | `True` se falou tudo, `False` se o cliente interrompeu |
123
+ | `await call.interrupt()` | `interrupt` | — |
124
+ | `await call.decide(text, custom_intents=[CustomIntent(key, label, description)])` | `decide` | `Decision` |
125
+ | `await call.extract_slot(text, name, type="text", *, description=)` | `extract_slot` | `SlotResult` |
126
+ | `await call.thinking(message=None, *, timeout_ms=None)` | `thinking` | — |
127
+ | `await call.done_processing()` | `done_processing` | — |
128
+ | `await call.end()` | `end` | — |
129
+ | `await call.send_audio(frame)` | quadro binário (só `websocket`) | — |
130
+ | `await call.playback_finished(speak_id)` | `playback_finished` (só `websocket`) | — |
131
+ | `await call.send(dict)` | qualquer mensagem crua | — |
132
+
133
+ `decide()` e `extract_slot()` geram o `id`, esperam a resposta com esse `id` (padrão: 10 s) e devolvem o resultado. Essas respostas **não** aparecem na iteração. Depois do fim, `call.ended` guarda o `SessionEnded`.
134
+
135
+ ### Eventos
136
+
137
+ | Classe | Evento da API | Campos |
138
+ |---|---|---|
139
+ | `SessionStarted` | `session.started` | `session_id`, `limits` |
140
+ | `SpeechStarted` / `SpeechEnded` | `speech.started` / `speech.ended` | `duration_ms` |
141
+ | `Transcript` | `transcript` | `utterance_id`, `text`, `uncertain`, `assessment`, `latency_ms` |
142
+ | `Decision` | `decision` | `intent`, `action`, `is_escape`, `complexity`, `custom_intent`, `extracted`, `utterance_id`, `id` |
143
+ | `SlotResult` | `slot` | `id`, `value`, `status`, `found` |
144
+ | `SpeakStarted` / `SpeakDone` | `speak.started` / `speak.done` | `id`, `chars` |
145
+ | `BargeIn` | `barge_in` | `id` |
146
+ | `WaitingSpoken` | `waiting` | `utterance_id`, `kind`, `text` |
147
+ | `ReplyTimeout` | `reply_timeout` | `utterance_id`, `action` |
148
+ | `Usage` | `usage` | `billed_seconds`, `credits`, `balance` |
149
+ | `WarningEvent` | `warning` | `code`, `message` |
150
+ | `ErrorEvent` | `error` | `code`, `message` |
151
+ | `SessionEnded` | `session.ended` | `reason`, `billed_seconds`, `credits` |
152
+ | `Audio` | quadro binário (só `websocket`) | `data` |
153
+ | `UnknownEvent` | qualquer outro | — |
154
+
155
+ Todo evento tem `type` e `raw` (o JSON original). Eventos novos da API chegam como `UnknownEvent` em vez de quebrar o seu código.
156
+
157
+ `Transcript.assessment` traz `ignore`, `ask_to_repeat`, `unproductive`, `confirm` e o atalho `usable` (nem `ignore`, nem `ask_to_repeat`, nem `unproductive`).
158
+
159
+ ### Erros
160
+
161
+ O mesmo erro chega como status HTTP ou código de fechamento do WebSocket; o SDK levanta a mesma exceção nos dois casos. Todas herdam de `ErmisError` (`message`, `code`, `status`).
162
+
163
+ | Exceção | HTTP / WebSocket |
164
+ |---|---|
165
+ | `AuthenticationError` | 401 / 4001 |
166
+ | `InsufficientCreditsError` | 402 / 4002 |
167
+ | `ChannelLimitError` | 429 / 4029 |
168
+ | `InvalidRequestError` | 400 / 4400 (também quando `decide`/`extract_slot` recebem `error`) |
169
+ | `SessionTimeoutError` | 4008 (60 s sem áudio) |
170
+ | `MaxDurationError` | 4013 |
171
+ | `APIError` | 500 / 1011 |
172
+ | `ConnectionClosedError` | o canal fechou antes de `session.ended` |
173
+
174
+ ## Outros transportes
175
+
176
+ **Navegador.** Crie a sessão com `transport="browser", audio_format="pcm16_48k"` e envie `session.browser_credentials()` ao navegador. O navegador abre `audio_url?session_token=…` e troca áudio direto com a Ermis; o seu backend conduz a conversa com `ermis.connect(session)`, como no Twilio.
177
+
178
+ **Seu próprio WebSocket.** Com `transport="websocket"`, o mesmo `Call` leva áudio e controle: envie com `call.send_audio(frame)`, receba `Audio` na iteração e avise o fim de cada fala com `call.playback_finished(speak_id)`.
179
+
180
+ ## Documentação
181
+
182
+ - [Referência da Voice API](https://github.com/Ermisai/ermis-voice-sdk/blob/main/docs/api.md): sessões, canal de controle, decisões, mensagens de espera, áudio e erros.
183
+ - Na plataforma da Ermis: **Documentação API → Ermis Voice** e a aba **Ligação** da Área de Teste, para conversar com o agente pelo microfone.
184
+
185
+ ## Desenvolvimento
186
+
187
+ ```bash
188
+ uv venv && uv pip install -e ".[dev]"
189
+ .venv/bin/python -m pytest -q
190
+ ```
191
+
192
+ Os testes rodam contra uma Voice API simulada (REST com `httpx.MockTransport`, controle com um servidor WebSocket local).
193
+
194
+ ## Licença
195
+
196
+ MIT. Veja [LICENSE](https://github.com/Ermisai/ermis-voice-sdk/blob/main/LICENSE).