first commit
This commit is contained in:
17
docs/README.md
Normal file
17
docs/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Docs
|
||||
|
||||
Esta pasta concentra a documentacao viva do projeto.
|
||||
|
||||
Arquivos:
|
||||
- `../README.md`: setup local, bootstrap do virtualenv, faixa de Python suportada, comandos de teste e estrutura de imagens/manifests Kubernetes
|
||||
- `refactor-plan.md`: alvo arquitetural e fases do refactor incremental
|
||||
- `refactor-log.md`: historico operacional das mudancas feitas no repositorio
|
||||
- `api-overview.md`: descricao da API, fluxos e responsabilidades atuais
|
||||
|
||||
Regra de uso durante o refactor:
|
||||
1. toda mudanca relevante de arquitetura deve atualizar `refactor-log.md`
|
||||
2. quando o comportamento publico mudar, atualizar `api-overview.md`
|
||||
3. se a ordem das etapas do refactor mudar, atualizar `refactor-plan.md`
|
||||
|
||||
Objetivo final:
|
||||
- chegar ao fim do refactor com uma base documental suficiente para gerar a documentacao final da API sem depender de reconstruir contexto depois
|
||||
728
docs/api-overview.md
Normal file
728
docs/api-overview.md
Normal file
@@ -0,0 +1,728 @@
|
||||
# API Overview
|
||||
|
||||
## Status
|
||||
|
||||
Documento vivo. Descreve o estado atual da API e sera atualizado ao longo do refactor.
|
||||
|
||||
## O que esta API faz
|
||||
|
||||
Esta API implementa um fluxo de voz-to-voz para TIA:
|
||||
- recebe conexao websocket do cliente
|
||||
- recebe audio PCM do cliente
|
||||
- encaminha audio para o agent via LiveKit
|
||||
- transcreve, processa a pipeline de negocio e sintetiza resposta
|
||||
- devolve audio para o cliente
|
||||
- finaliza a chamada e exporta resultado
|
||||
|
||||
## Componentes principais
|
||||
|
||||
### Bridge
|
||||
|
||||
Arquivo principal:
|
||||
- `app/ws_gateway/main.py`
|
||||
- `app/ws_gateway/voice_client.html`
|
||||
|
||||
Responsabilidades:
|
||||
- aceitar conexao em `/ws/agent`
|
||||
- receber `start`
|
||||
- montar contexto da chamada
|
||||
- despachar agent para uma room LiveKit
|
||||
- mandar audio do cliente para LiveKit
|
||||
- devolver audio do agent para o cliente
|
||||
- encerrar websocket ao receber `DONE`
|
||||
- servir um cliente web de teste em `/voice-client`
|
||||
|
||||
### Agent
|
||||
|
||||
Arquivos principais:
|
||||
- `app/livekit/main.py`
|
||||
- `app/livekit/runtime/call_runtime.py`
|
||||
- `app/livekit/runtime/state.py`
|
||||
- `app/livekit/runtime/commands.py`
|
||||
- `app/livekit/runtime/command_executor.py`
|
||||
- `app/livekit/runtime/scheduler.py`
|
||||
- `app/livekit/policies/`
|
||||
- `app/livekit/adapters/`
|
||||
|
||||
Responsabilidades:
|
||||
- entrar na room do LiveKit
|
||||
- configurar STT, VAD e TTS
|
||||
- receber transcricao final do usuario
|
||||
- chamar o backend de IA configurado
|
||||
- vocalizar resposta
|
||||
- tratar interrupcao, idle nudge e finalizacao
|
||||
|
||||
Organizacao atual:
|
||||
- `main.py` faz o wiring do agent e das dependencias
|
||||
- `CallRuntime` coordena o ciclo de vida da chamada
|
||||
- `CallState` concentra o estado mutavel da sessao
|
||||
- `commands.py` define os comandos internos para side effects
|
||||
- `RuntimeCommandExecutor` executa bridge, export, speech, pipeline e start da sessao
|
||||
- `TimerScheduler` concentra os timers nomeados do runtime
|
||||
- `InterruptPolicy`, `IdlePolicy` e `FinalizationPolicy` concentram regras operacionais da chamada
|
||||
- adapters encapsulam acesso ao backend de IA, speech, bridge e export
|
||||
|
||||
### Pipeline de negocio
|
||||
|
||||
Backends disponiveis no runtime websocket:
|
||||
- `remote_ws`:
|
||||
- `app/livekit/adapters/remote_agent_ws_adapter.py`
|
||||
- `remote_sse`:
|
||||
- `app/livekit/adapters/remote_agent_sse_adapter.py`
|
||||
|
||||
Responsabilidades:
|
||||
- preparar dados de atendimento
|
||||
- controlar os estagios da chamada ou delegar esse controle ao agent remoto
|
||||
- gerar a resposta textual por etapa
|
||||
- registrar metadados e finalizar a conversa
|
||||
|
||||
Selecao atual:
|
||||
- `AGENT_BACKEND=remote_ws` envia cada turno transcrito para `REMOTE_AGENT_WS_URL`
|
||||
- `AGENT_BACKEND=remote_sse` usa contratos especificos por agente:
|
||||
- `conta` usa `GET /agent/sse` para inicializar a sessao e `POST /agent/sse` para executar cada acao
|
||||
- `oferta` usa `POST /agent/execute` por turno e consome os eventos SSE `schedule_message`, `message` e `done`
|
||||
- `AGENT_BACKEND=remote_ws_fake` usa um fake interno no proprio processo, sem abrir websocket local
|
||||
- o fluxo do runtime local continua o mesmo: STT produz texto, o backend de IA devolve texto e o TTS vocaliza
|
||||
- o roteamento entre agentes acontece pelo campo `data.agent`
|
||||
- quando `AGENT_BACKEND=remote_ws`, o adapter escolhe a URL por `agent` se existirem:
|
||||
- `REMOTE_AGENT_WS_URL_CONTA`
|
||||
- `REMOTE_AGENT_WS_URL_OFERTA`
|
||||
- `REMOTE_AGENT_WS_URL_COBRANCA` ou `REMOTE_AGENT_WS_URL_COBRA`
|
||||
- quando `AGENT_BACKEND=remote_sse`, o adapter exige a URL especifica do `agent`, sem fallback generico:
|
||||
- `REMOTE_AGENT_SSE_URL_CONTA`
|
||||
- `REMOTE_AGENT_SSE_URL_OFERTA`
|
||||
- `REMOTE_AGENT_SSE_URL_COBRANCA` ou `REMOTE_AGENT_SSE_URL_COBRA`
|
||||
|
||||
Contrato remoto atual por turno:
|
||||
- `timestamp`
|
||||
- `agent`
|
||||
- `RouterCallKeyDay`
|
||||
- `RouterCallKey`
|
||||
- `ANI`
|
||||
- `GSM`
|
||||
- `callIdGed`
|
||||
- `ID_FATURA` somente para `agent=conta`
|
||||
- `text`
|
||||
- `protocol`
|
||||
- `stage`
|
||||
|
||||
Contrato especifico atual de `conta`:
|
||||
- request:
|
||||
- na abertura da sessao, o transporte envia query string com `ani`, `channelId` e `uraCallId`
|
||||
- `action: "chat"`
|
||||
- `payload.message`
|
||||
- `payload.message_id` no transporte SSE de `conta`, com o mesmo UUID enviado na query string
|
||||
- `payload.channel` no transporte websocket
|
||||
- `payload.interruption` e `payload.events` quando existirem
|
||||
- response:
|
||||
- `type: "ready"` para a primeira fala ou fala que abre uma janela de resposta do cliente
|
||||
- a fala de `ready` nao e interrompivel; se o cliente falar enquanto o audio do `ready` ainda estiver tocando, a transcricao e descartada e registrada em log
|
||||
- apos o fim do audio de `ready`, o runtime mantem uma janela protegida de 750ms para absorver atraso de playback do cliente; fala iniciada nessa janela tambem e descartada
|
||||
- depois dessa janela protegida, o runtime passa a esperar resposta do cliente
|
||||
- `type: "result"`
|
||||
- `action: "chat"`
|
||||
- `result.type: "final"` para respostas intermediarias
|
||||
- `result.content` como texto para TTS
|
||||
- `type: "feedback"` ou `result.type: "feedback"` para mensagens de acompanhamento enquanto o backend continua processando o turno atual
|
||||
- a fala de `feedback` nao e interrompivel, nao abre novo turno, nao espera resposta do cliente e nao arma timeout de silencio do cliente
|
||||
- se o cliente falar durante `feedback`, o runtime descarta a transcricao, registra a tentativa em log e continua aguardando a resposta final do backend
|
||||
- `feedback` nao dispara `metadata.wait_retry_messages`, como mensagens do tipo "Voce esta ai?"
|
||||
- para finalizacoes esperadas, o runtime aguarda uma janela curta de silencio estavel do cliente, fala o `result.content` e depois envia `stop`; se uma nova transcricao final chegar durante a espera, a finalizacao anterior e descartada em favor do novo turno
|
||||
- falas de finalizacao esperada (`resolvido`, `nao_resolvido`, `erro_no_match` etc.) nao sao interrompiveis; qualquer fala do cliente durante a finalizacao e descartada
|
||||
- nesses casos o runtime nao chama finalizacao remota adicional (`end`/`end_service_once`)
|
||||
- quando o `ready` vier com `metadata.wait_timeout_seconds`, o runtime aguarda esse tempo apos falar a mensagem de `ready`
|
||||
- se tambem vier `metadata.wait_retry_messages`, o runtime fala cada item do array a cada novo estouro de `wait_timeout_seconds`; depois do ultimo item, aguarda mais um intervalo igual e envia
|
||||
`stop_silencio_longo` com `reason: "no_user_response"`
|
||||
- sem `metadata.wait_retry_messages`, o comportamento continua sendo encerrar direto no primeiro estouro de `wait_timeout_seconds`
|
||||
- enquanto aguarda processamento depois do fim de fala do usuario, o runtime toca somente o audio local longo de conforto; no padrao atual, usa intervalo de 12s e no maximo 6 vezes
|
||||
- se a resposta do backend remoto nao chegar em 300s, o runtime encerra com `stop_agent_backend_unavailable`
|
||||
- se o participante do agent LiveKit desconectar sem `DONE`, o bridge tenta redispatch na mesma room; se o agent nao voltar, encerra com `stop_agent_runtime_unavailable`
|
||||
|
||||
Mapeamento de finalizacoes esperadas de `conta`:
|
||||
- `result.type: "resolvido"` -> `stop_resolvido_e_finalizado`
|
||||
- `result.type: "nao_resolvido"` -> `stop_nao_resolvido`
|
||||
- `result.type: "resolvido_outros_assuntos"` -> `stop_outro_assunto`
|
||||
- `result.type: "outros_assuntos"` -> `stop_outro_assunto`
|
||||
- `result.type: "erro_falha_sistema"` -> `stop_falha_sistema`
|
||||
- `result.type: "erro_no_match"` -> `stop_no_match`
|
||||
|
||||
Contrato SSE atual de `conta`:
|
||||
- o runtime guarda o `session_id` retornado pelo backend nos eventos `ready`
|
||||
- abre `GET /agent/sse?msisdn=...&invoice_id=...&ani=...&protocol_id=...&session_id=...&message_id=...&channelId=ura&uraCallId=...` no `prepare`
|
||||
- consome o stream de inicializacao ate `ready` e ate o termino do prefetch (`prefetch_done`, `prefetch_skipped` ou `prefetch_failed`) ou fechamento da resposta
|
||||
- envia cada turno com `POST /agent/sse?session_id=...&ani=...&protocol_id=...&message_id=...&channelId=ura&uraCallId=...`
|
||||
- `message_id` e um UUID gerado por turno e independente de `session_id`; nos turnos de acao, `session_id` continua sendo o identificador de sessao retornado pelo backend de contas
|
||||
- body do `POST`:
|
||||
- `action: "chat"`
|
||||
- `payload.message`
|
||||
- `payload.message_id` com o mesmo UUID do parametro `message_id` da query string
|
||||
- `payload.interruption` e `payload.events` quando existirem
|
||||
- a resposta do proprio `POST` e `text/event-stream`; o runtime consome `ready`, `progress`, `result` e `error` ate receber o resultado da acao
|
||||
|
||||
Contrato SSE atual de `oferta`:
|
||||
- `prepare` nao abre stream remoto; o primeiro turno do agente chama `POST /agent/execute`
|
||||
- o turno inicial envia `message: "inicio_atendimento"` para o backend remoto produzir a primeira fala
|
||||
- cada turno usa body:
|
||||
- `messageId` gerado por turno como UUID
|
||||
- `message` com `inicio_atendimento` no primeiro turno ou a transcricao do cliente nos demais
|
||||
- `context.protocolNumber` e `context.protocolo` vindos de `data.protocolo`
|
||||
- `context.gsm` vindo de `data.gsm`
|
||||
- `context.uraId` vindo de `data.callIdGed`
|
||||
- `context.callIdGed`, `context.ani`, `context.routerCallKey`, `context.routerCallKeyDay`, `context.agent` e `context.assetId` quando existirem
|
||||
- `context.sessionId` e enviado a partir de `data.session_id`/`data.sessionId` recebido no `start`; protocolo, `callIdGed` e `routerCallKey` nao sao usados como fallback de `sessionId`
|
||||
- headers enviados: `Accept: text/event-stream`, `Content-Type: application/json` e `Channel-id` vindo de `channelId` ou `ura`
|
||||
- eventos recebidos:
|
||||
- `schedule_message`: fala imediatamente `scheduledMessage.message`
|
||||
- `message`: fala `response`
|
||||
- `done`: encerra o stream do turno; a chamada so e finalizada quando o status recebido indicar encerramento terminal
|
||||
- de/para do `done.additionalInformations.service_status`:
|
||||
- `RESOLVED` -> `stop_resolvido_e_finalizado`
|
||||
- `UNRESOLVED` ou ausente -> `stop_nao_resolvido`
|
||||
- `RESOLVED_WITH_NEW_REQUEST` -> nao envia `stop`; a conversa permanece aberta para o novo assunto
|
||||
- `done.status=transferred` e reconhecido, mas ainda nao orquestra transferencia; nesta versao encerra como nao resolvido e preserva metadados de handover para evolucao futura
|
||||
|
||||
Endpoint dev fornecido para `oferta`:
|
||||
- host: `https://agt-ai-atendimento-ofertas-dev.internal.timbrasil.com.br`
|
||||
- execute: `https://agt-ai-atendimento-ofertas-dev.internal.timbrasil.com.br/agent/execute`
|
||||
- health: nao configurado por enquanto; a readiness nao deve derivar nem chamar `/health` para oferta ate essa rota ser fornecida
|
||||
- enquanto o certificado interno nao estiver confiavel no ambiente local, `REMOTE_AGENT_SSE_TLS_VERIFY_OFERTA=0` permite testar o fluxo ignorando a validacao TLS do `httpx`
|
||||
- pod de referencia: `tim-ai-atend-agnt-sales-65764fcf8d-xpzmb`
|
||||
- IP de referencia: `http://10.153.35.23`
|
||||
- portas: `80:31332/TCP`, `443:30635/TCP`
|
||||
|
||||
Origem desses campos:
|
||||
- o bridge extrai esses valores do `data` recebido em `WS /ws/agent`
|
||||
- o agent local apenas reaproveita esse contexto para chamar o websocket remoto
|
||||
- `protocol`/`protocolo` e identificador de negocio; `session_id` e identificador explicito de sessao recebido no `start`. O runtime nao preenche `session_id` com protocolo, `callIdGed` ou `routerCallKey`.
|
||||
|
||||
Configuracao opcional por chamada:
|
||||
- o cliente pode enviar `callConfig` no `start`, mas esse bloco e opcional
|
||||
- objetivo atual: testes, homologacao e overrides tecnicos por sessao
|
||||
- `callConfig.agentBackend` faz override do backend websocket para aquela chamada
|
||||
- `callConfig.agentBackend` tambem pode selecionar o transporte SSE para a sessao
|
||||
- `callConfig.stt` pode ajustar `provider`, `initialPrompt`, `configOverride` e `minProbSingleWord`
|
||||
- `callConfig.tts` pode ajustar `provider`, `voiceId` e `modelId`
|
||||
- valores aceitos hoje em `callConfig.agentBackend`:
|
||||
- `remote_ws`
|
||||
- `remote_sse`
|
||||
- `remote_ws_fake`
|
||||
- hoje os providers suportados no agent local sao:
|
||||
- STT: `internal_http` (`Sofya Batch` no cliente de teste), `fake`
|
||||
- TTS: `elevenlabs`, `azure`, `xai`, `fake`
|
||||
|
||||
## Endpoints atuais
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Retorna status simples de saude do bridge.
|
||||
|
||||
### `GET /health/resources`
|
||||
|
||||
Retorna o deep health do bridge com validacao dos recursos usados pelo `WS /ws/agent`.
|
||||
|
||||
Comportamento:
|
||||
- `200` quando os checks obrigatorios estao saudaveis
|
||||
- `503` quando algum recurso obrigatorio falha
|
||||
- inclui `active_connections`, `max_connections`, `cached`, `failed_resources` e `checks`
|
||||
- os checks atuais cobrem:
|
||||
- `agent_runtime`
|
||||
- `agent_backend`
|
||||
- `stt`
|
||||
- `tts`
|
||||
|
||||
### `GET /health/services`
|
||||
|
||||
Retorna o health consolidado dos servicos do TIA no formato consumido pela esteira de operacao.
|
||||
|
||||
Comportamento:
|
||||
- `200` quando todos os servicos monitorados estao saudaveis
|
||||
- `503` quando algum servico monitorado falha
|
||||
- `status` no corpo retorna `ok` ou `fail`
|
||||
- `htp_cod_status` preserva o status HTTP retornado pelo health do servico quando houver resposta HTTP
|
||||
- `hhtp_cod_desc` preserva a descricao retornada pelo health quando houver; se nao houver descricao no corpo, usa a reason phrase HTTP
|
||||
- ignora `AGENT_BACKEND=remote_ws_fake` e usa as rotas reais configuradas nos envs dos servicos
|
||||
- `checks` cobre apenas os servicos atualmente monitorados:
|
||||
- `agent_runtime`
|
||||
- `agent_backend.contas`: `REMOTE_AGENT_HEALTH_URL_CONTA`, `REMOTE_AGENT_HEALTH_URL_CONTAS` ou `REMOTE_AGENT_HEALTH_URL`
|
||||
- `agent_backend.oferta`: `REMOTE_AGENT_HEALTH_URL_OFERTA`, `REMOTE_AGENT_HEALTH_URL_OFERTAS` ou health derivado de `REMOTE_AGENT_SSE_URL_OFERTA`
|
||||
- `stt.sofya`: `STT_HEALTH_URL` ou health derivado de `STT_URL`
|
||||
- `tts.xAI`: provider xAI com `XAI_WEBSOCKET_URL`/`XAI_API_KEY`
|
||||
|
||||
Para checks sem resposta HTTP, como falha de conexao ou probe por WebSocket, a rota usa fallback `200`/`500` e a mensagem interna do erro ou sucesso.
|
||||
|
||||
Formato:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"checks": {
|
||||
"agent_runtime": {
|
||||
"htp_cod_status": 200,
|
||||
"hhtp_cod_desc": "SUCCESS"
|
||||
},
|
||||
"agent_backend": {
|
||||
"contas": {
|
||||
"htp_cod_status": 200,
|
||||
"hhtp_cod_desc": "SUCCESS"
|
||||
},
|
||||
"oferta": {
|
||||
"htp_cod_status": 200,
|
||||
"hhtp_cod_desc": "SUCCESS"
|
||||
}
|
||||
},
|
||||
"stt": {
|
||||
"sofya": {
|
||||
"htp_cod_status": 200,
|
||||
"hhtp_cod_desc": "SUCCESS"
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"xAI": {
|
||||
"htp_cod_status": 200,
|
||||
"hhtp_cod_desc": "SUCCESS"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /voice-client`
|
||||
|
||||
Cliente web de teste para capturar microfone, enviar audio para `WS /ws/agent`,
|
||||
reproduzir o audio do agent e configurar `STT`, `TTS` e `AGENT` por chamada.
|
||||
|
||||
### `WS /fake-agent/ws`
|
||||
|
||||
Websocket fake para homologacao local do backend remoto.
|
||||
|
||||
Uso esperado:
|
||||
- usar apenas para homologacao manual do contrato websocket fake
|
||||
- manter `agent=conta|oferta|cobranca`
|
||||
- deixar um cliente websocket externo chamar o fake diretamente quando precisar testar esse endpoint
|
||||
|
||||
Comportamento:
|
||||
- suporta o contrato `conta` com `action/payload`
|
||||
- suporta o contrato generico com `text/stage`
|
||||
- responde com progressao simples de stages
|
||||
- encerra quando recebe textos como `encerrar`, `obrigado` ou `tchau`
|
||||
|
||||
### `WS /ws/agent`
|
||||
|
||||
Fluxo principal de voz:
|
||||
1. cliente conecta
|
||||
2. envia mensagem `start`
|
||||
3. bridge valida capacidade e readiness dos recursos obrigatorios
|
||||
4. recebe `ready` ou `stop`
|
||||
5. se receber `ready`, envia audio binario
|
||||
6. recebe audio binario de resposta
|
||||
7. recebe `stop` ao fim da chamada ou em falha terminal
|
||||
|
||||
Contrato de inicio da chamada `type=start`:
|
||||
- a primeira mensagem de inicio deve ser um JSON textual com `type: "start"`; opcionalmente, `transferencia_session_id` pode chegar antes dela para informar somente o `session_id`
|
||||
- os campos de negocio devem ser enviados em `data`
|
||||
- chaves canonicas obrigatorias em `data`:
|
||||
- `agent`
|
||||
- `ani`
|
||||
- `gsm`
|
||||
- `session_id`
|
||||
- `routerCallKey`
|
||||
- `routerCallKeyDay`
|
||||
- `callIdGed`
|
||||
- chaves obrigatorias por agente:
|
||||
- `agentData.idFatura` quando `agent=conta`
|
||||
- `protocolo` quando `agent=oferta`
|
||||
- valores canonicos recomendados para `agent`:
|
||||
- `conta`
|
||||
- `oferta`
|
||||
- `cobranca`
|
||||
|
||||
Exemplo recomendado de `start` para `conta`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "start",
|
||||
"data": {
|
||||
"agent": "conta",
|
||||
"ani": "5511999990000",
|
||||
"gsm": "5511999990000",
|
||||
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"routerCallKeyDay": "20260409",
|
||||
"routerCallKey": "RCK-001",
|
||||
"callIdGed": "GED-123456",
|
||||
"agentData": {
|
||||
"idFatura": "FAT-123"
|
||||
}
|
||||
},
|
||||
"audioFormat": {
|
||||
"encoding": "linear16",
|
||||
"sampleRateHz": 16000,
|
||||
"channels": 1
|
||||
},
|
||||
"callConfig": {
|
||||
"agentBackend": "remote_ws"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Exemplo recomendado de `start` para `oferta`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "start",
|
||||
"data": {
|
||||
"agent": "oferta",
|
||||
"ani": "5511999990000",
|
||||
"gsm": "5511999990000",
|
||||
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"routerCallKeyDay": "20260409",
|
||||
"routerCallKey": "RCK-001",
|
||||
"callIdGed": "GED-123456",
|
||||
"protocolo": "PRT-20260409-0001"
|
||||
},
|
||||
"audioFormat": {
|
||||
"encoding": "linear16",
|
||||
"sampleRateHz": 16000,
|
||||
"channels": 1
|
||||
},
|
||||
"callConfig": {
|
||||
"agentBackend": "remote_sse"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Mensagem opcional aceita antes do `start` em cenarios de transferencia:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "transferencia_session_id",
|
||||
"data": {
|
||||
"session_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Se essa mensagem chegar antes do `start`, o bridge usa esse valor apenas para preencher `data.session_id` quando o `start` ainda nao trouxer o campo. Em producao, o caminho recomendado e enviar `session_id` diretamente dentro de `data` no `start`.
|
||||
|
||||
Contrato de audio:
|
||||
- apos o `ready`, o cliente deve enviar audio binario bruto em `PCM16/LINEAR16`, `16000 Hz`, `1 canal`
|
||||
- o `ready` informa os parametros operacionais atuais do bridge:
|
||||
- `session_id`: eco do `data.session_id` aceito para a chamada
|
||||
- `sample_rate: 16000`
|
||||
- `channels: 1`
|
||||
- `frame_ms: 20`
|
||||
- `bytes_per_frame: 640`
|
||||
- no contrato atual, `ready` significa que o bridge esta pronto para receber os bytes de audio do cliente
|
||||
- quando `agent_starts_conversation` esta habilitado, o agent mantem a entrada do usuario desativada no `RoomIO` desde antes do `StartSession` ate o fim da primeira mensagem; esse gate impede que fala ou backlog vindo da URA alcance VAD/STT durante o setup e a saudacao
|
||||
- o gate e liberado no fim do primeiro turno do agent e tambem em falha do pipeline, finalizacao ou timeout de setup; chamadas em que o usuario inicia a conversa nao usam esse bloqueio
|
||||
- `audioFormat` no `start` e opcional e hoje funciona como campo informativo/reservado
|
||||
- enviar outro codec, sample rate ou numero de canais nesse campo nao reconfigura o bridge atualmente
|
||||
- se houver necessidade de outro formato, homologar com a equipe de desenvolvimento antes da integracao
|
||||
|
||||
Mensagens devolvidas pelo servidor:
|
||||
- `ready` quando a sessao foi aceita e o bridge esta pronto para receber audio
|
||||
- audio binario PCM16 durante a resposta do agent
|
||||
- `stop` como mensagem terminal em qualquer encerramento do `WS /ws/agent`
|
||||
|
||||
Recuperacao do participante LiveKit do agent:
|
||||
- quando o participant identificado como agent desconecta e a chamada ainda nao terminou, o bridge faz redispatch do mesmo `AGENT_NAME` na mesma room
|
||||
- quando o novo participant entra, o bridge reenvia o controle `client_audio_enabled` e passa a consumir o audio desse novo participant
|
||||
- se o redispatch nao trouxer um novo agent dentro do timeout configurado, o bridge envia `stop_agent_runtime_unavailable` com `reason: "agent_disconnected"`
|
||||
|
||||
Contrato de `stop`:
|
||||
- toda mensagem terminal em `WS /ws/agent` usa:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "stop",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
- bloqueio antes do `ready`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "stop",
|
||||
"data": {
|
||||
"status": "stop_stt_unavailable",
|
||||
"reason": "resource_unhealthy",
|
||||
"resource": "stt",
|
||||
"failed_resources": ["stt"],
|
||||
"phase": "pre_ready"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- falha de recurso durante a sessao:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "stop",
|
||||
"data": {
|
||||
"status": "stop_agent_backend_unavailable",
|
||||
"reason": "resource_unhealthy",
|
||||
"resource": "agent_backend",
|
||||
"failed_resources": ["agent_backend"],
|
||||
"phase": "in_session"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- fim normal da chamada:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "stop",
|
||||
"data": {
|
||||
"status": "stop_resolvido_e_finalizado",
|
||||
"reason": "stage_done",
|
||||
"phase": "in_session"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- falha terminal durante a sessao:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "stop",
|
||||
"data": {
|
||||
"status": "stop_bridge_failed",
|
||||
"reason": "bridge_failed",
|
||||
"resource": "bridge",
|
||||
"phase": "in_session"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Status terminais atualmente usados em `WS /ws/agent`:
|
||||
- `stop_capacity_tia`
|
||||
- `stop_agent_runtime_unavailable`
|
||||
- `stop_agent_backend_unavailable`
|
||||
- `stop_stt_unavailable`
|
||||
- `stop_tts_unavailable`
|
||||
- `stop_resolvido_e_finalizado`
|
||||
- `stop_nao_resolvido`
|
||||
- `stop_falha_sistema`
|
||||
- `stop_no_match`
|
||||
- `stop_outro_assunto`
|
||||
- `stop_silencio_longo`
|
||||
- `stop_bridge_failed`
|
||||
|
||||
Regra de interpretacao:
|
||||
- `phase: "pre_ready"` indica bloqueio antes de a sessao aceitar audio
|
||||
- `phase: "in_session"` indica falha terminal ou encerramento depois do `ready`
|
||||
- os status de recurso podem aparecer nas duas fases, dependendo de quando a indisponibilidade foi detectada
|
||||
|
||||
Campos opcionais de testes e homologacao:
|
||||
- `audioFormat` pode ser enviado no `start`, mas hoje nao altera o pipeline de audio
|
||||
- `callConfig` e opcional e existe para testes, smoke test local e homologacao tecnica
|
||||
- para integracao produtiva com cliente externo, o contrato pode omitir `callConfig`
|
||||
|
||||
Configuracao de chamada para teste de carga com STT Sofya, agente fake e TTS xAI:
|
||||
|
||||
```json
|
||||
{
|
||||
"debugEvents": true,
|
||||
"callConfig": {
|
||||
"agentBackend": "remote_ws_fake",
|
||||
"agentFake": {
|
||||
"delayMs": 2500,
|
||||
"responses": "Primeira resposta simulada com tamanho intermediario;Segunda resposta simulada com tamanho intermediario;Resposta final encerrando o atendimento simulado"
|
||||
},
|
||||
"stt": {
|
||||
"provider": "internal_http",
|
||||
"disableVosk": true
|
||||
},
|
||||
"tts": {
|
||||
"provider": "xai"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Regras desse modo:
|
||||
- `agentFake.responses` contem de 2 a 10 frases separadas por `;`; espacos laterais sao removidos e cada frase deve ter de 40 a 180 caracteres
|
||||
- `agentFake.delayMs` e aplicado antes de cada resposta, usa `2500` por padrao e aceita valores de `0` a `180000`
|
||||
- a saudacao inicial continua sendo o `intro` normal; cada fala posterior reconhecida pelo STT consome uma resposta fake, sem usar o texto transcrito para escolher a resposta
|
||||
- respostas anteriores as duas ultimas usam `ARGUMENTATION`, a penultima usa `FORMALIZATION` e a ultima usa `DONE`; chamadas posteriores ao fim recebem novamente o mesmo resultado terminal
|
||||
- a sequencia e isolada por sessao
|
||||
- `debugEvents=true` publica pelo websocket somente eventos tecnicos e valores agregados de fala, STT Sofya, TTS xAI e sheds do Bridge; o conteudo integral da conversa nao e incluido nas metricas
|
||||
- o endpoint nao possui uma autorizacao adicional especifica para o fake: qualquer cliente ja autorizado a abrir `/ws/agent` pode selecionar `remote_ws_fake` por `callConfig`
|
||||
|
||||
### `WS /ws/text`
|
||||
|
||||
Fluxo de texto sem audio para testes e integracao basica.
|
||||
|
||||
### `WS /ws/text_stream`
|
||||
|
||||
Fluxo textual com resposta em streaming.
|
||||
|
||||
## Fluxo ponta a ponta atual
|
||||
|
||||
1. Cliente abre websocket em `/ws/agent`
|
||||
2. Bridge recebe `start` e faz parse do contexto da chamada
|
||||
3. Bridge valida capacidade e readiness dos recursos
|
||||
4. Bridge cria room/token e envia `ready`
|
||||
5. Cliente envia audio para o bridge
|
||||
6. Bridge publica audio no LiveKit
|
||||
7. Agent recebe audio, STT produz texto
|
||||
8. Agent chama o backend de IA configurado
|
||||
9. Agent usa TTS para vocalizar resposta
|
||||
10. Bridge devolve audio ao cliente
|
||||
11. Agent sinaliza `DONE` ou ocorre erro terminal
|
||||
12. Bridge envia `stop` e fecha a chamada
|
||||
|
||||
## Dependencias externas relevantes
|
||||
|
||||
- LiveKit
|
||||
- STT interno HTTP
|
||||
- Vosk
|
||||
- ElevenLabs
|
||||
- modelo LLM da pipeline
|
||||
- API de fidelizacao
|
||||
|
||||
## Timeline de chamada
|
||||
|
||||
O projeto agora gera uma timeline estruturada por chamada em formato `jsonl`.
|
||||
|
||||
Configuracao:
|
||||
- `CALL_TIMELINE_ENABLED=1` ativa a escrita da timeline
|
||||
- `CALL_TIMELINE_CONSOLE=1` replica os eventos tambem no stdout
|
||||
- `CALL_TIMELINE_DIR=./timeline` define o diretorio dos arquivos
|
||||
- `CALL_TIMELINE_QUEUE_MAX=10000` limita eventos pendentes para escrita assincrona
|
||||
- `CALL_LOG_QUEUE_MAX=20000` limita registros pendentes dos arquivos por chamada
|
||||
- `ASYNC_IO_WARNING_INTERVAL_S=60` limita a frequencia dos avisos de descarte/erro
|
||||
|
||||
A timeline e o arquivo de log por chamada sao gravados por threads de fundo para
|
||||
nao executar I/O de disco no event loop de audio. Quando uma fila atinge o limite,
|
||||
o evento e descartado em vez de bloquear o audio e um warning rate-limited registra
|
||||
o total acumulado. Os limites aceitos ficam entre `1` e `1000000`.
|
||||
|
||||
Os spans estruturados usam `BatchSpanProcessor`: `span.end()` apenas enfileira o
|
||||
span, enquanto o envio OTLP acontece em lote fora da thread chamadora. O provider
|
||||
faz flush e shutdown no encerramento normal do processo.
|
||||
|
||||
Fake remoto:
|
||||
- `remote_ws_fake` usa um fake interno em memoria e nao depende de `REMOTE_AGENT_WS_FAKE_URL`
|
||||
- o endpoint `ws://127.0.0.1:8000/fake-agent/ws` continua disponivel apenas para testes manuais do contrato websocket
|
||||
|
||||
Logs de websocket remoto:
|
||||
- o adapter `remote_ws` agora registra no stdout eventos `REMOTE_AGENT_WS_CONNECT_OPEN`, `REMOTE_AGENT_WS_CONNECT_OK`, `REMOTE_AGENT_WS_REQUEST`, `REMOTE_AGENT_WS_RESPONSE` e falhas `*_FAIL`
|
||||
- os logs incluem `instance` (hostname/pod), `agent`, `url`, `host`, `stage`, `protocol` e metadados do payload para facilitar comparar pods com erro de DNS/host
|
||||
|
||||
Mock de encerramento apos primeiro audio:
|
||||
- `MOCK_STOP_AFTER_FIRST_AUDIO_ENABLED=1` faz o bridge enviar o `stop` terminal configurado para o `reason` depois que o primeiro audio real do agente for entregue ao cliente e a saida ficar em silencio pelo intervalo configurado
|
||||
- `MOCK_STOP_AFTER_FIRST_AUDIO_SILENCE_S=0.35` controla quanto tempo de silencio o bridge espera antes de disparar o `stop`
|
||||
- `MOCK_STOP_AFTER_FIRST_AUDIO_REASON=stage_done` define o `reason` exato enviado no `stop`
|
||||
|
||||
Status terminais configuraveis por env:
|
||||
- `FINAL_STOP_STATUS_RESOLVED=stop_resolvido_e_finalizado`
|
||||
- `FINAL_STOP_STATUS_UNRESOLVED=stop_nao_resolvido`
|
||||
- `FINAL_STOP_STATUS_OTHER_SUBJECT=stop_outro_assunto`
|
||||
- `FINAL_STOP_STATUS_LONG_SILENCE=stop_silencio_longo`
|
||||
- `FINAL_STOP_DEFAULT_KIND=resolved` define o fallback quando o `reason` nao bater em nenhum valor configurado
|
||||
- `FINAL_STOP_REASON_RESOLVED=stage_done`
|
||||
- `FINAL_STOP_REASON_UNRESOLVED=nao_resolvido`
|
||||
- `FINAL_STOP_REASON_OTHER_SUBJECT=outro_assunto`
|
||||
- `FINAL_STOP_REASON_LONG_SILENCE=no_user_response`
|
||||
- o mapeamento agora usa comparacao exata do `reason`, sem aliases
|
||||
|
||||
Espera de resposta do backend remoto:
|
||||
- `REMOTE_AGENT_INFLIGHT_WAIT_INTERVAL_S=12`
|
||||
- `REMOTE_AGENT_INFLIGHT_WAIT_SHORT_AUDIO_DIR=src/app/livekit/assets/comfort/short`
|
||||
- `REMOTE_AGENT_INFLIGHT_WAIT_LONG_AUDIO_DIR=src/app/livekit/assets/comfort/long`
|
||||
- o primeiro audio de conforto usa um WAV aleatorio da pasta `short`; a partir do segundo, usa WAVs aleatorios da pasta `long`
|
||||
- o intervalo dos audios de conforto conta a partir do fim do audio anterior
|
||||
- `REMOTE_AGENT_INFLIGHT_WAIT_TIMEOUT_S=180`
|
||||
- `REMOTE_AGENT_INFLIGHT_WAIT_MAX_NOTICES=0` (`0` mantém os confortos sem limite de quantidade até o timeout)
|
||||
- `REMOTE_AGENT_INFLIGHT_WAIT_TEXT=Um momento, ainda estou consultando para te ajudar.`
|
||||
- este timeout e tecnico: limita quanto tempo o runtime aguarda o backend remoto processar um turno
|
||||
- ele e diferente do timeout de resposta do cliente configurado por `metadata.wait_timeout_seconds`
|
||||
- mensagens `feedback`, tanto top-level quanto `result.type: "feedback"`, usam a espera tecnica do backend, mas nao contam silencio do cliente nem disparam `metadata.wait_retry_messages`
|
||||
- em um turno normal, depois que o agent termina de falar e o cliente responde, o fim de fala detectado pelo VAD pode antecipar o primeiro conforto curto enquanto o STT conclui a transcricao
|
||||
- o conforto antecipado do VAD nao e agendado enquanto o agent esta falando; se outra fala do agent ocupar o `say_lock` depois do agendamento, o conforto tambem e abortado para nao sair colado ao fim da mensagem
|
||||
- se o cliente interromper uma fala interrompivel do agent, o VAD nao enfileira conforto atras dessa fala; o novo pipeline ainda pode usar os confortos normais caso o processamento demore
|
||||
- se o cliente falar por mais de `800ms` enquanto o backend ja processa outro turno e o agent esta em silencio, o conforto curto especulativo do VAD e suprimido e a transcricao abre uma interrupcao diferida
|
||||
- cada interrupcao diferida toca `src/app/livekit/assets/comfort/interruption/01.wav`, correspondente a "Ouvi o que voce falou, um instante", e invalida a resposta anterior quando ela chegar
|
||||
- no pipeline substituto, o conforto dedicado conta como o primeiro aviso ja consumido: o audio `short` normal nao toca logo depois; se o processamento continuar, o proximo aviso permitido e `long` e respeita o intervalo configurado
|
||||
- o estado de interrupcao e rearmado antes de iniciar o pipeline substituto: cada nova fala valida durante o novo processamento repete o conforto dedicado e substitui novamente a resposta em voo, sem descartar a nova transcricao nem deixar silencio na chamada
|
||||
- falas de ate `800ms` durante processamento sao tratadas como ruido ou backchannel curto e nao abrem interrupcao diferida
|
||||
- os logs principais desse fluxo sao `pre_backend_wait_notice_skipped` (`agent_speaking` ou `backend_processing`), `deferred_interruption_accumulated`, `deferred_interruption_dispatched` e os estagios TTS `AGENT_BACKEND_WAIT`/`INTERRUPTION_COMFORT`
|
||||
|
||||
Recuperacao do participant LiveKit do agent:
|
||||
- `AGENT_RECONNECT_ENABLED=1`
|
||||
- `AGENT_RECONNECT_MAX_ATTEMPTS=1`
|
||||
- `AGENT_RECONNECT_TIMEOUT_S=10`
|
||||
|
||||
Protecao de reenvio TTS:
|
||||
- `TTS_EMPTY_FRAME_RETRY_TIMEOUT_S=3` interrompe uma tentativa de TTS sem primeiro frame de audio ou com gap entre frames no meio da fala apos o timeout e reenvia o mesmo texto uma vez
|
||||
- antes do reenvio, toca `src/app/livekit/assets/comfort/fails/tts_fail_recovery.wav`
|
||||
- quando o reenvio tem sucesso, registra um unico `envio msg` com `http_cod_status=200` e `erro_msg=TTS_regerado`
|
||||
|
||||
STT fake:
|
||||
- `STT_PROVIDER=fake` dispensa `STT_URL`
|
||||
- usa `FAKE_STT_TRANSCRIPTS` como fila de falas por turno
|
||||
- `FAKE_STT_MODE=repeat_last|cycle` controla o comportamento ao consumir a fila
|
||||
|
||||
TTS fake:
|
||||
- `TTS_PROVIDER=fake` dispensa credenciais externas
|
||||
- gera audio PCM sintetico local para smoke tests do pipeline
|
||||
|
||||
Formato:
|
||||
- um arquivo por chamada
|
||||
- nome do arquivo baseado no `room`
|
||||
- eventos do `bridge` e do `agent` entram no mesmo arquivo
|
||||
- cada linha contem:
|
||||
- `ts`
|
||||
- `t_rel_ms`
|
||||
- `component`
|
||||
- `event`
|
||||
- `protocol`
|
||||
- `room`
|
||||
- campos especificos do evento
|
||||
|
||||
Eventos relevantes:
|
||||
- `bridge`:
|
||||
- `call_start`
|
||||
- `ready_sent`
|
||||
- `dispatch_started`
|
||||
- `livekit_room_connected`
|
||||
- `agent_join`
|
||||
- `client_audio_first_frame_received`
|
||||
- `client_audio_first_frame_published`
|
||||
- `client_audio_enabled`
|
||||
- `done_packet_received`
|
||||
- `stop_sent`
|
||||
- `call_end`
|
||||
- `agent`:
|
||||
- `call_start`
|
||||
- `room_enter`
|
||||
- `session_start_requested`
|
||||
- `stt_recognize_started`
|
||||
- `stt_http_completed`
|
||||
- `user_transcript_final`
|
||||
- `pipeline_run_started`
|
||||
- `pipeline_run_completed`
|
||||
- `remote_agent_request`
|
||||
- `remote_agent_response`
|
||||
- `tts_stage_started`
|
||||
- `tts_stage_result`
|
||||
- `interrupt_marked`
|
||||
- `finalize_started`
|
||||
- `finalize_completed`
|
||||
|
||||
Uso pratico:
|
||||
1. iniciar a chamada normalmente
|
||||
2. identificar no terminal o `room` ou o `protocol`
|
||||
3. abrir o arquivo correspondente em `./timeline`
|
||||
4. ler os eventos em ordem de `t_rel_ms`
|
||||
|
||||
## Ponto de atencao
|
||||
|
||||
Durante o refactor, este documento deve continuar descrevendo:
|
||||
- comportamento publico
|
||||
- contrato do websocket
|
||||
- responsabilidades de cada camada
|
||||
|
||||
Na versao final, ele deve evoluir para a documentacao oficial da API.
|
||||
612
docs/refactor-log.md
Normal file
612
docs/refactor-log.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# Refactor Log
|
||||
|
||||
Este arquivo registra o que mudou a cada etapa do refactor incremental.
|
||||
|
||||
Nota:
|
||||
- as entradas `000` a `010` foram escritas antes da consolidacao da estrutura atual do repositorio
|
||||
- por isso, varias delas ainda referenciam caminhos antigos sem o prefixo `src/`
|
||||
- a partir da entrada `011`, os caminhos refletem o layout atual da raiz + `src/`
|
||||
|
||||
## Como preencher
|
||||
|
||||
Para cada mudanca relevante, registrar:
|
||||
- data
|
||||
- objetivo
|
||||
- arquivos alterados
|
||||
- comportamento preservado
|
||||
- comportamento alterado
|
||||
- risco conhecido
|
||||
- validacao executada
|
||||
- proximo passo
|
||||
|
||||
## Entrada 000 - Baseline documental
|
||||
|
||||
- Data: 2026-03-27
|
||||
- Objetivo: criar uma base de documentacao viva para acompanhar o refactor incremental
|
||||
- Arquivos alterados:
|
||||
- `README.md`
|
||||
- `docs/README.md`
|
||||
- `docs/refactor-plan.md`
|
||||
- `docs/refactor-log.md`
|
||||
- `docs/api-overview.md`
|
||||
- Comportamento preservado: nenhum codigo de execucao foi alterado
|
||||
- Comportamento alterado: nenhum
|
||||
- Risco conhecido: a documentacao precisa ser mantida junto das mudancas, senao perde valor
|
||||
- Validacao executada: revisao manual do conteudo criado
|
||||
- Proximo passo: iniciar Fase 1 com extracao dos adapters de pipeline, speech, bridge e export
|
||||
|
||||
## Entrada 001 - Fase 1 / adapters extraidos
|
||||
|
||||
- Data: 2026-03-27
|
||||
- Objetivo: extrair boundaries de pipeline, speech, bridge e export sem mudar o comportamento publico da API
|
||||
- Arquivos alterados:
|
||||
- `app/livekit/main.py`
|
||||
- `app/livekit/adapters/pipeline_adapter.py`
|
||||
- `app/livekit/adapters/speech_service.py`
|
||||
- `app/livekit/adapters/bridge_gateway.py`
|
||||
- `app/livekit/adapters/export_service.py`
|
||||
- `docs/refactor-log.md`
|
||||
- Comportamento preservado:
|
||||
- contrato do websocket do bridge
|
||||
- fluxo STT -> pipeline -> TTS
|
||||
- notificacao `DONE` para o bridge
|
||||
- exportacao final de sessao
|
||||
- Comportamento alterado:
|
||||
- nenhum comportamento publico planejado
|
||||
- `app/livekit/main.py` passa a delegar responsabilidades para adapters
|
||||
- Risco conhecido:
|
||||
- como a extracao preserva a logica inline, ainda existe acoplamento forte no runtime
|
||||
- faltam testes automatizados de regressao para interrupcao, idle nudge e finalizacao
|
||||
- Validacao executada:
|
||||
- revisao manual do diff
|
||||
- validacao sintatica prevista apos a extracao
|
||||
- Proximo passo:
|
||||
- reduzir `nonlocal` no runtime com um `CallRuntime` explicito
|
||||
- preparar o terreno para policies e scheduler unificados
|
||||
|
||||
## Entrada 002 - Fase 2 / runtime explicito
|
||||
|
||||
- Data: 2026-03-27
|
||||
- Objetivo: encapsular o fluxo da chamada em um `CallRuntime` com `CallState` explicito, reduzindo estado implícito e `nonlocal` em `app/livekit/main.py`
|
||||
- Arquivos alterados:
|
||||
- `app/livekit/main.py`
|
||||
- `app/livekit/runtime/state.py`
|
||||
- `app/livekit/runtime/call_runtime.py`
|
||||
- `docs/refactor-log.md`
|
||||
- `docs/api-overview.md`
|
||||
- Comportamento preservado:
|
||||
- contrato do websocket do bridge
|
||||
- fluxo STT -> pipeline -> TTS
|
||||
- idle nudge, interrupcao e finalizacao continuam com a mesma logica operacional
|
||||
- exportacao e notificacao `DONE` continuam sendo disparadas pelo agent
|
||||
- Comportamento alterado:
|
||||
- `app/livekit/main.py` passa a ser majoritariamente wiring do session e construcao do runtime
|
||||
- o estado mutavel da chamada fica concentrado em `CallState`
|
||||
- Risco conhecido:
|
||||
- as regras ainda nao foram transformadas em policies puras; a logica segue acoplada ao runtime, apenas mais organizada
|
||||
- ainda faltam testes automatizados para cenarios concorrentes de fala, interrupcao e fechamento
|
||||
- Validacao executada:
|
||||
- revisao manual do diff
|
||||
- `PYTHONDONTWRITEBYTECODE=1 python3 -m py_compile app/livekit/main.py app/livekit/adapters/bridge_gateway.py app/livekit/adapters/export_service.py app/livekit/adapters/pipeline_adapter.py app/livekit/adapters/speech_service.py app/livekit/runtime/state.py app/livekit/runtime/call_runtime.py`
|
||||
- Proximo passo:
|
||||
- introduzir command/execution boundaries no runtime
|
||||
- preparar a migracao de interrupcao, idle e finalize para policies + scheduler
|
||||
|
||||
## Entrada 003 - Fase 3 / commands e executor
|
||||
|
||||
- Data: 2026-03-29
|
||||
- Objetivo: separar efeitos colaterais do `CallRuntime` por meio de comandos tipados e um executor dedicado
|
||||
- Arquivos alterados:
|
||||
- `app/livekit/main.py`
|
||||
- `app/livekit/runtime/commands.py`
|
||||
- `app/livekit/runtime/command_executor.py`
|
||||
- `app/livekit/runtime/call_runtime.py`
|
||||
- `docs/refactor-log.md`
|
||||
- `docs/api-overview.md`
|
||||
- Comportamento preservado:
|
||||
- contrato do websocket do bridge
|
||||
- fluxo STT -> pipeline -> TTS
|
||||
- sequencia de idle nudge, interrupcao e finalizacao
|
||||
- integracao com LiveKit, pipeline e export sem troca de contrato
|
||||
- Comportamento alterado:
|
||||
- `CallRuntime` deixa de chamar diretamente bridge/export/speech/pipeline para os principais side effects
|
||||
- side effects passam a trafegar por comandos (`commands.py`) executados por `RuntimeCommandExecutor`
|
||||
- Risco conhecido:
|
||||
- ainda existe conhecimento de regras dentro de `CallRuntime`; a extracao atual isola efeitos, mas nao transforma as regras em policies puras
|
||||
- `agent._ready`, `agent._run_lock` e coordenacao de fila ainda pertencem ao runtime
|
||||
- Validacao executada:
|
||||
- revisao manual do diff
|
||||
- validacao sintatica com `python3 -c 'from pathlib import Path; paths = [...]; [compile(Path(p).read_text(), p, "exec") for p in paths]'`
|
||||
- Proximo passo:
|
||||
- extrair policies de interrupcao, idle e finalizacao
|
||||
- introduzir scheduler unico para timers e reduzir logica condicional no runtime
|
||||
|
||||
## Entrada 004 - Fase 4 / policies e scheduler
|
||||
|
||||
- Data: 2026-03-29
|
||||
- Objetivo: mover regras de interrupcao, idle e finalize-once para policies dedicadas e centralizar timers em um scheduler unico
|
||||
- Arquivos alterados:
|
||||
- `app/livekit/main.py`
|
||||
- `app/livekit/policies/interrupt_policy.py`
|
||||
- `app/livekit/policies/idle_policy.py`
|
||||
- `app/livekit/policies/finalization_policy.py`
|
||||
- `app/livekit/runtime/scheduler.py`
|
||||
- `app/livekit/runtime/state.py`
|
||||
- `app/livekit/runtime/call_runtime.py`
|
||||
- `docs/refactor-log.md`
|
||||
- `docs/api-overview.md`
|
||||
- Comportamento preservado:
|
||||
- contrato do websocket do bridge
|
||||
- fluxo STT -> pipeline -> TTS
|
||||
- timers de idle nudge e idle close continuam ativos
|
||||
- finalizacao por `DONE`, `room_empty`, `shutdown_callback` e `no_user_response` continua existindo
|
||||
- Comportamento alterado:
|
||||
- `InterruptPolicy` passa a decidir interrupcao por stage e filtro de backchannel
|
||||
- `IdlePolicy` passa a concentrar regras de nudge/close
|
||||
- `FinalizationPolicy` passa a concentrar regras de finalize-once e room-empty
|
||||
- `TimerScheduler` passa a concentrar arm/cancel/token dos timers
|
||||
- `CallState` deixa de carregar estado interno de timers
|
||||
- Risco conhecido:
|
||||
- o `CallRuntime` ainda conhece a ordem operacional completa da chamada; as policies reduzem acoplamento, mas ainda nao existe reducer/event engine
|
||||
- `RuntimeConfig` ainda carrega campos hoje parcialmente sobrepostos pelas policies; isso pode ser simplificado em um corte futuro
|
||||
- Validacao executada:
|
||||
- revisao manual do diff
|
||||
- validacao sintatica com `python3 -c 'from pathlib import Path; paths = [...]; [compile(Path(p).read_text(), p, "exec") for p in paths]'`
|
||||
- Proximo passo:
|
||||
- avaliar se vale introduzir eventos/comandos mais declarativos no runtime ou parar aqui e estabilizar
|
||||
- se seguir, o proximo salto natural e um reducer/event engine ou a evolucao da integracao de IA (`llm_node` ou agente remoto)
|
||||
|
||||
## Entrada 005 - Estabilizacao / testes unitarios do runtime
|
||||
|
||||
- Data: 2026-03-29
|
||||
- Objetivo: adicionar cobertura automatizada para os cenarios mais criticos do runtime sem depender de LiveKit real
|
||||
- Arquivos alterados:
|
||||
- `tests/__init__.py`
|
||||
- `tests/livekit/__init__.py`
|
||||
- `tests/livekit/test_runtime.py`
|
||||
- `docs/refactor-log.md`
|
||||
- Comportamento preservado:
|
||||
- nenhum contrato publico da API foi alterado
|
||||
- nenhum fluxo de audio ou websocket foi modificado
|
||||
- Comportamento alterado:
|
||||
- o repositorio passa a ter uma suite unitária para policies, scheduler, command executor e cenarios críticos do `CallRuntime`
|
||||
- Risco conhecido:
|
||||
- os testes ainda usam doubles/fakes e nao substituem validacao integrada com LiveKit real
|
||||
- ainda faltam cenarios mais completos de timer real, callback de room e speech handle real
|
||||
- Validacao executada:
|
||||
- `python3 -m unittest tests.livekit.test_runtime -v`
|
||||
- `python3 -m unittest discover -s tests -v`
|
||||
- Proximo passo:
|
||||
- ampliar cobertura para speech handle real e integracao de callbacks com objetos LiveKit reais, se isso passar a ser area de regressao
|
||||
- ou encerrar a fase de estabilizacao e voltar a discutir a evolucao da camada de IA
|
||||
|
||||
## Entrada 006 - Backend remoto via websocket
|
||||
|
||||
- Data: 2026-03-29
|
||||
- Objetivo: permitir trocar a pipeline local por um agent remoto via websocket, preservando o fluxo STT -> IA -> TTS e o runtime atual
|
||||
- Arquivos alterados:
|
||||
- `app/livekit/main.py`
|
||||
- `app/livekit/adapters/agent_backend.py`
|
||||
- `app/livekit/adapters/backend_factory.py`
|
||||
- `app/livekit/adapters/pipeline_adapter.py`
|
||||
- `app/livekit/adapters/remote_agent_ws_adapter.py`
|
||||
- `app/livekit/runtime/command_executor.py`
|
||||
- `requirements.txt`
|
||||
- `tests/livekit/test_remote_agent_ws_adapter.py`
|
||||
- `docs/refactor-log.md`
|
||||
- `docs/api-overview.md`
|
||||
- Comportamento preservado:
|
||||
- contrato do websocket do bridge
|
||||
- fluxo de audio com LiveKit e TTS no agent local
|
||||
- runtime de interrupcao, idle nudge e finalizacao
|
||||
- backend `langgraph` segue como default
|
||||
- Comportamento alterado:
|
||||
- o backend da camada de IA passa a ser selecionavel por `AGENT_BACKEND`
|
||||
- quando `AGENT_BACKEND=remote_ws`, o agent envia cada turno transcrito para um websocket remoto e vocaliza a resposta retornada
|
||||
- a finalizacao pode opcionalmente buscar um `result` remoto via mensagem `type=end`
|
||||
- o bridge agora repassa `agent`, `RouterCallKeyDay`, `RouterCallKey`, `ANI`, `GSM` e `ID_FATURA` para o agent quando esses campos vierem no metadata do cliente
|
||||
- o adapter remoto roteia a chamada para endpoints diferentes de acordo com `agent` (`conta`, `oferta`, `cobranca`)
|
||||
- o agent `conta` passou a usar contrato proprio em websocket com `action/payload` na entrada e `result.content` na resposta
|
||||
- Risco conhecido:
|
||||
- o contrato do websocket remoto ainda e interno e precisa ser homologado com o agent externo real
|
||||
- o adapter atual usa conexao websocket por turno, nao uma sessao persistente
|
||||
- se o agent remoto nao devolver `stage`, a politica local dependera do ultimo stage conhecido ou do default configurado
|
||||
- o campo `timestamp` e gerado localmente no momento de cada turno; se o integrador exigir outro formato, isso ainda precisa ser alinhado
|
||||
- Validacao executada:
|
||||
- testes unitarios do adapter remoto e factory
|
||||
- execucao da suite `unittest`
|
||||
- Proximo passo:
|
||||
- homologar o contrato do websocket remoto com payload/resposta reais
|
||||
- decidir se a conexao remota deve continuar por turno ou evoluir para sessao persistente
|
||||
|
||||
## Entrada 007 - Cliente web de voz e call_config por chamada
|
||||
|
||||
- Data: 2026-03-30
|
||||
- Objetivo: disponibilizar um cliente web para testar o fluxo de voz pelo navegador e permitir overrides de `STT`, `TTS` e `AGENT` por chamada
|
||||
- Arquivos alterados:
|
||||
- `app/ws_gateway/main.py`
|
||||
- `app/ws_gateway/voice_client.html`
|
||||
- `app/ws_gateway/call_config.py`
|
||||
- `app/livekit/main.py`
|
||||
- `app/livekit/call_config.py`
|
||||
- `app/livekit/adapters/backend_factory.py`
|
||||
- `tests/config/test_call_config.py`
|
||||
- `docs/api-overview.md`
|
||||
- `docs/refactor-log.md`
|
||||
- Comportamento preservado:
|
||||
- fluxo principal de audio via `/ws/agent`
|
||||
- bridge continua recebendo `start`, audio binario e encerrando com `stop`
|
||||
- defaults de `STT`, `TTS` e backend continuam vindo de env quando nao houver override
|
||||
- Comportamento alterado:
|
||||
- `/voice-client` agora serve um cliente web para capturar microfone e ouvir o audio retornado
|
||||
- o `start` pode carregar `call_config` com overrides por chamada
|
||||
- o agent agora aplica overrides de backend, STT e TTS para a sessao corrente
|
||||
- Risco conhecido:
|
||||
- o cliente web usa `ScriptProcessorNode`, que e suficiente para homologacao mas nao e a opcao mais moderna da Web Audio API
|
||||
- os providers dinamicos suportados ainda sao os que ja existem no projeto (`internal_http` e `elevenlabs`)
|
||||
- Validacao executada:
|
||||
- testes puros de `call_config`
|
||||
- execucao da suite `unittest`
|
||||
- Proximo passo:
|
||||
- se o cliente web passar a ser usado em rotina de homologacao, vale migrar a captura/playback para `AudioWorklet`
|
||||
- se surgirem novos providers de STT/TTS, plugar nas factories por chamada
|
||||
|
||||
## Entrada 008 - Timeline estruturada por chamada
|
||||
|
||||
- Data: 2026-03-30
|
||||
- Objetivo: criar observabilidade ponta a ponta para entender o caminho `bridge -> livekit -> stt -> backend -> tts -> stop` por chamada
|
||||
- Arquivos alterados:
|
||||
- `app/utils/call_timeline.py`
|
||||
- `app/ws_gateway/main.py`
|
||||
- `app/livekit/main.py`
|
||||
- `app/livekit/runtime/call_runtime.py`
|
||||
- `app/livekit/adapters/backend_factory.py`
|
||||
- `app/livekit/adapters/pipeline_adapter.py`
|
||||
- `app/livekit/adapters/remote_agent_ws_adapter.py`
|
||||
- `app/livekit/adapters/bridge_gateway.py`
|
||||
- `app/providers/stt_internal_livekit.py`
|
||||
- `tests/utils/test_call_timeline.py`
|
||||
- `docs/api-overview.md`
|
||||
- `docs/refactor-log.md`
|
||||
- Comportamento preservado:
|
||||
- contrato publico de `WS /ws/agent`
|
||||
- fluxo de audio ja existente entre cliente, bridge, LiveKit e agent
|
||||
- backends de IA, STT e TTS seguem com a mesma responsabilidade funcional
|
||||
- Comportamento alterado:
|
||||
- bridge e agent agora escrevem uma timeline estruturada em `jsonl` compartilhada por chamada
|
||||
- a timeline usa o mesmo `timeline_id` e o mesmo `origin_unix_ms` entre os dois processos para manter a ordem relativa dos eventos
|
||||
- o bridge passou a registrar eventos como `ready_sent`, `dispatch_started`, `agent_join`, `client_audio_enabled`, `done_packet_received` e `call_end`
|
||||
- o agent passou a registrar eventos como `user_transcript_final`, `pipeline_run_started`, `pipeline_run_completed`, `tts_stage_started`, `interrupt_marked`, `finalize_started` e `finalize_completed`
|
||||
- o provider de STT agora registra etapas de reconhecimento (`stt_recognize_started`, `stt_vosk_completed`, `stt_http_completed`)
|
||||
- o backend remoto por websocket agora registra `remote_agent_request` e `remote_agent_response`
|
||||
- Risco conhecido:
|
||||
- a timeline registra payloads de request do backend remoto, o que aumenta a verbosidade e pode expor dados sensiveis em ambiente de debug
|
||||
- o arquivo `jsonl` e compartilhado entre dois processos locais; o uso atual com `flock` e suficiente para dev/homologacao, mas nao substitui observabilidade centralizada
|
||||
- Validacao executada:
|
||||
- `py_compile` dos arquivos alterados
|
||||
- teste unitario novo de timeline
|
||||
- execucao completa da suite `unittest`
|
||||
- Proximo passo:
|
||||
- decidir se a timeline deve continuar sempre habilitada em dev ou ficar atras de flag por ambiente
|
||||
- se a homologacao exigir, adicionar visualizador simples da timeline no cliente web
|
||||
|
||||
## Entrada 009 - Fake remoto para homologacao via UI
|
||||
|
||||
- Data: 2026-03-30
|
||||
- Objetivo: permitir testar o fluxo `STT -> remote_ws -> TTS` pela UI mesmo sem o agent remoto real estar de pe
|
||||
- Arquivos alterados:
|
||||
- `app/ws_gateway/fake_remote_agent.py`
|
||||
- `app/ws_gateway/main.py`
|
||||
- `app/ws_gateway/voice_client.html`
|
||||
- `app/livekit/adapters/backend_factory.py`
|
||||
- `tests/ws_gateway/test_fake_remote_agent.py`
|
||||
- `tests/livekit/test_remote_agent_ws_adapter.py`
|
||||
- `docs/api-overview.md`
|
||||
- `docs/refactor-log.md`
|
||||
- Comportamento preservado:
|
||||
- backend `remote_ws` continua exigindo URL real quando selecionado
|
||||
- o contrato do adapter remoto segue o mesmo
|
||||
- Comportamento alterado:
|
||||
- o `bridge` agora expoe `WS /fake-agent/ws`
|
||||
- a UI ganhou a opcao `remote_ws_fake`
|
||||
- quando `remote_ws_fake` e selecionado, o agent local aponta para `REMOTE_AGENT_WS_FAKE_URL` ou usa por padrao `ws://127.0.0.1:8000/fake-agent/ws`
|
||||
- o fake responde nos contratos `conta` e generico, com progressao simples de stages e encerramento por palavras-chave como `encerrar`, `obrigado` e `tchau`
|
||||
- Risco conhecido:
|
||||
- o fake nao simula comportamento de negocio real; ele serve apenas para homologacao tecnica do fluxo de voz
|
||||
- o fake e stateless por conexao, entao a progressao depende do `stage` enviado pelo agent local
|
||||
- Validacao executada:
|
||||
- testes unitarios do fake remoto
|
||||
- testes do backend fake no adapter remoto
|
||||
- execucao completa da suite `unittest`
|
||||
- Proximo passo:
|
||||
- se a homologacao pedir cenarios mais realistas, adicionar scripts por agent (`conta`, `oferta`, `cobranca`) com respostas configuraveis
|
||||
|
||||
## Template de nova entrada
|
||||
|
||||
- Data:
|
||||
- Objetivo:
|
||||
- Arquivos alterados:
|
||||
- Comportamento preservado:
|
||||
- Comportamento alterado:
|
||||
- Risco conhecido:
|
||||
- Validacao executada:
|
||||
- Proximo passo:
|
||||
|
||||
## Entrada 010 - Correcao do roteamento `remote_ws_fake`
|
||||
|
||||
- Data: 2026-03-30
|
||||
- Objetivo: garantir que a selecao `remote_ws_fake` na UI nao seja sobrescrita pela URL real configurada em `REMOTE_AGENT_WS_URL`
|
||||
- Arquivos alterados:
|
||||
- `app/livekit/adapters/backend_factory.py`
|
||||
- `app/livekit/adapters/remote_agent_ws_adapter.py`
|
||||
- `tests/livekit/test_remote_agent_ws_adapter.py`
|
||||
- `docs/api-overview.md`
|
||||
- `docs/refactor-log.md`
|
||||
- Comportamento preservado:
|
||||
- `remote_ws` continua usando `REMOTE_AGENT_WS_URL`
|
||||
- o adapter remoto continua sendo o mesmo para backend real e fake
|
||||
- Comportamento alterado:
|
||||
- `remote_ws_fake` agora prioriza sempre `REMOTE_AGENT_WS_FAKE_URL`, mesmo quando `REMOTE_AGENT_WS_URL` existe no ambiente
|
||||
- a timeline do adapter remoto agora diferencia `backend=remote_ws_fake` de `backend=remote_ws`
|
||||
- Risco conhecido:
|
||||
- aliases como `fake_remote_ws` e `ws_fake` continuam sendo tratados como `remote_ws_fake`, mas o label emitido na timeline fica canonico como `remote_ws_fake`
|
||||
- Validacao executada:
|
||||
- `python3 -m unittest tests.livekit.test_remote_agent_ws_adapter -v`
|
||||
- `python3 -m py_compile app/livekit/adapters/backend_factory.py app/livekit/adapters/remote_agent_ws_adapter.py tests/livekit/test_remote_agent_ws_adapter.py`
|
||||
- Proximo passo:
|
||||
- validar na chamada real da UI que o timeline e o request usam `ws://127.0.0.1:8000/fake-agent/ws`
|
||||
|
||||
## Entrada 011 - Reorganizacao da raiz e remocao de boilerplate
|
||||
|
||||
- Data: 2026-04-06
|
||||
- Objetivo: consolidar a estrutura real do projeto na raiz do repositorio, mantendo `src/app` e `src/agent` como codigo-fonte canonico
|
||||
- Arquivos alterados:
|
||||
- `README.md`
|
||||
- `makefile`
|
||||
- `Dockerfile`
|
||||
- `pytest.ini`
|
||||
- `.env.example`
|
||||
- `.gitignore`
|
||||
- `.dockerignore`
|
||||
- `requirements.txt`
|
||||
- `livekit.yaml`
|
||||
- `docs/*`
|
||||
- `tests/*`
|
||||
- `k8s/deployment.yaml`
|
||||
- remocao de `pyproject.toml`, `uv.lock`, `README copy.md`, `configs/config.example.yaml` e scripts herdados do boilerplate
|
||||
- Comportamento preservado:
|
||||
- `src/app` e `src/agent` permanecem como raiz do codigo de execucao
|
||||
- o bridge FastAPI continua em `src/app/ws_gateway/main.py`
|
||||
- o projeto continua usando `PYTHONPATH=src`
|
||||
- Comportamento alterado:
|
||||
- a raiz do repositorio passa a refletir o projeto real, e nao mais o boilerplate original
|
||||
- `requirements.txt` passa a ser a fonte principal de dependencias
|
||||
- artefatos de runtime (`logs`, `timeline`, `__pycache__`) deixam de ser versionados
|
||||
- Risco conhecido:
|
||||
- arquivos de infra legados fora do fluxo principal ainda poderiam carregar suposicoes antigas do boilerplate
|
||||
- a migracao para `requirements.txt` simplifica a operacao, mas remove o lockfile anterior
|
||||
- Validacao executada:
|
||||
- `python3 -m compileall src tests`
|
||||
- `python3 -m pytest tests/utils/test_call_timeline.py -q`
|
||||
- Proximo passo:
|
||||
- revisar a infra auxiliar restante e reduzir ruido operacional na raiz
|
||||
|
||||
## Entrada 012 - Simplificacao do compose local de observabilidade
|
||||
|
||||
- Data: 2026-04-06
|
||||
- Objetivo: transformar o `docker-compose.yml` em uma stack minima e coerente para Langfuse local
|
||||
- Arquivos alterados:
|
||||
- `docker-compose.yml`
|
||||
- Comportamento preservado:
|
||||
- a stack opcional de Langfuse continua disponivel para uso local
|
||||
- a aplicacao principal segue fora do compose, operada via `make`
|
||||
- Comportamento alterado:
|
||||
- remocao do servico `mongo`
|
||||
- consolidacao apenas de `langfuse-web`, `langfuse-worker`, `postgres`, `redis`, `clickhouse` e `minio`
|
||||
- reducao de duplicacao com anchors para configuracoes compartilhadas
|
||||
- Risco conhecido:
|
||||
- o compose continua sendo opcional e depende de configuracao explicita do projeto para apontar para Langfuse self-hosted
|
||||
- Validacao executada:
|
||||
- validacao sintatica do YAML com parser local
|
||||
- Proximo passo:
|
||||
- se a equipe mantiver uso frequente, considerar alvos dedicados no `makefile` para subir e derrubar a stack
|
||||
|
||||
## Entrada 013 - Provider TTS opcional desacoplado do import global
|
||||
|
||||
- Data: 2026-04-06
|
||||
- Objetivo: impedir que a falta do SDK do ElevenLabs quebrasse o import do modulo inteiro de TTS
|
||||
- Arquivos alterados:
|
||||
- `src/app/providers/tts.py`
|
||||
- `tests/providers/test_tts.py`
|
||||
- Comportamento preservado:
|
||||
- selecao de provider TTS por configuracao
|
||||
- suporte aos providers ja existentes
|
||||
- Comportamento alterado:
|
||||
- o SDK do ElevenLabs deixa de ser carregado no topo do modulo e passa a ser resolvido sob demanda
|
||||
- ausencia do SDK passa a gerar `missing_elevenlabs_sdk`, em vez de falha de import que derruba testes e providers nao relacionados
|
||||
- Risco conhecido:
|
||||
- o provider continua opcional, mas a disciplina de dependencias ainda segue concentrada em `requirements.txt`
|
||||
- Validacao executada:
|
||||
- `python3 -m pytest tests/providers/test_tts.py -q`
|
||||
- `python3 -m pytest tests/adapters/test_azure_tts.py -q`
|
||||
- Proximo passo:
|
||||
- continuar removendo acoplamentos desnecessarios entre providers e entrypoints
|
||||
|
||||
## Entrada 014 - Remocao de mocks e hardcodes dos pipelines de texto
|
||||
|
||||
- Data: 2026-04-06
|
||||
- Objetivo: tirar codigo de demonstracao do caminho produtivo dos endpoints de texto
|
||||
- Arquivos alterados:
|
||||
- `src/app/services/session_context.py`
|
||||
- `src/app/services/text_pipeline.py`
|
||||
- `src/app/services/text_pipeline_stream.py`
|
||||
- `src/app/ws_gateway/main.py`
|
||||
- `tests/services/test_session_context.py`
|
||||
- Comportamento preservado:
|
||||
- os endpoints `/ws/text` e `/ws/text_stream` continuam aceitando a mesma sessao de entrada
|
||||
- os pipelines continuam recebendo `mailing` e `intro`
|
||||
- Comportamento alterado:
|
||||
- remocao do uso de `app.models.mock`
|
||||
- remocao de protocolo e dados fixos hardcoded nos pipelines
|
||||
- introducao de extracao explicita de protocolo via `session_context`
|
||||
- Risco conhecido:
|
||||
- se existiam fluxos de dev que dependiam dos mocks antigos, eles passam a precisar de payloads reais
|
||||
- Validacao executada:
|
||||
- `python3 -m pytest -q`
|
||||
- resultado observado na etapa: `36 passed`
|
||||
- Proximo passo:
|
||||
- consolidar contratos duplicados entre gateway e runtime
|
||||
|
||||
## Entrada 015 - Unificacao de `call_config`
|
||||
|
||||
- Data: 2026-04-06
|
||||
- Objetivo: eliminar duplicacao entre as implementacoes de `call_config` do gateway e do runtime LiveKit
|
||||
- Arquivos alterados:
|
||||
- `src/app/common/call_config.py`
|
||||
- `src/app/ws_gateway/call_config.py`
|
||||
- `src/app/livekit/call_config.py`
|
||||
- `tests/config/test_call_config.py`
|
||||
- Comportamento preservado:
|
||||
- API publica de `build_call_config(...)`
|
||||
- API publica de `normalize_call_config(...)` e resolucao dos campos auxiliares
|
||||
- Comportamento alterado:
|
||||
- a normalizacao passa a ter um nucleo compartilhado em `src/app/common/call_config.py`
|
||||
- `ws_gateway` e `livekit` viram wrappers leves para o mesmo contrato
|
||||
- Risco conhecido:
|
||||
- qualquer evolucao futura de `call_config` passa a ter impacto compartilhado entre bridge e agent, o que e desejado, mas exige disciplina de compatibilidade
|
||||
- Validacao executada:
|
||||
- `python3 -m pytest tests/config/test_call_config.py -q`
|
||||
- `python3 -m pytest -q`
|
||||
- resultado observado na etapa: `36 passed`
|
||||
- Proximo passo:
|
||||
- iniciar a quebra incremental do monolito em `src/app/ws_gateway/main.py`
|
||||
|
||||
## Entrada 016 - Extracao do parsing inicial da sessao no bridge
|
||||
|
||||
- Data: 2026-04-06
|
||||
- Objetivo: remover do `ws_gateway/main.py` a logica de parsing da mensagem `start`, resolucao de mailing e montagem de `intro`/`nudge`
|
||||
- Arquivos alterados:
|
||||
- `src/app/ws_gateway/session_start.py`
|
||||
- `src/app/ws_gateway/main.py`
|
||||
- `tests/ws_gateway/test_session_start.py`
|
||||
- Comportamento preservado:
|
||||
- contrato de primeira mensagem `type=start`
|
||||
- geracao de `mailing`, `intro` e `nudge`
|
||||
- construcao do contexto do agente remoto a partir de metadata + mailing
|
||||
- Comportamento alterado:
|
||||
- criacao da dataclass `StartSessionContext`
|
||||
- centralizacao de `parse_start_payload`, `recv_start_message` e `build_remote_agent_context` em modulo proprio
|
||||
- `/ws/agent`, `/ws/text` e `/ws/text_stream` passam a consumir um contexto estruturado, e nao uma tuple solta
|
||||
- Risco conhecido:
|
||||
- o contrato de entrada continua dependente do payload do cliente; a extracao melhora testabilidade, mas nao redefine o protocolo
|
||||
- Validacao executada:
|
||||
- `python3 -m compileall src/app/ws_gateway src/app/services tests/ws_gateway`
|
||||
- `python3 -m pytest tests/ws_gateway/test_session_start.py -q`
|
||||
- `python3 -m pytest -q`
|
||||
- resultado observado na etapa: `39 passed`
|
||||
- Proximo passo:
|
||||
- extrair o bootstrap da chamada para reduzir ainda mais a responsabilidade do endpoint
|
||||
|
||||
## Entrada 017 - Extracao do bootstrap da chamada do bridge
|
||||
|
||||
- Data: 2026-04-06
|
||||
- Objetivo: separar a preparacao da chamada do runtime do endpoint `/ws/agent`
|
||||
- Arquivos alterados:
|
||||
- `src/app/ws_gateway/session_bootstrap.py`
|
||||
- `src/app/ws_gateway/main.py`
|
||||
- `tests/ws_gateway/test_session_bootstrap.py`
|
||||
- Comportamento preservado:
|
||||
- geracao de token de acesso ao LiveKit
|
||||
- montagem de `call_config`, `remote_agent_context`, `timeline` e `dispatch_metadata`
|
||||
- fallbacks de protocolo e telefone
|
||||
- Comportamento alterado:
|
||||
- introducao da dataclass `BridgeSessionBootstrap`
|
||||
- centralizacao de `room_name`, `identity`, `token`, `protocol`, `phone_number` e `dispatch_metadata` em um builder dedicado
|
||||
- `main.py` passa a consumir um pacote pronto de bootstrap
|
||||
- Risco conhecido:
|
||||
- o bootstrap ainda depende de envs e factories externas; a extracao organiza responsabilidade, mas nao muda a politica de configuracao
|
||||
- Validacao executada:
|
||||
- `python3 -m compileall src/app/ws_gateway tests/ws_gateway`
|
||||
- `python3 -m pytest tests/ws_gateway/test_session_bootstrap.py -q`
|
||||
- `python3 -m pytest -q`
|
||||
- resultado observado na etapa: `41 passed`
|
||||
- Proximo passo:
|
||||
- extrair o ciclo de vida da sessao LiveKit do endpoint
|
||||
|
||||
## Entrada 018 - Extracao do lifecycle da sessao LiveKit no bridge
|
||||
|
||||
- Data: 2026-04-06
|
||||
- Objetivo: remover do endpoint os handlers de participante, o processamento do pacote `DONE` e o watcher de encerramento
|
||||
- Arquivos alterados:
|
||||
- `src/app/ws_gateway/session_lifecycle.py`
|
||||
- `src/app/ws_gateway/main.py`
|
||||
- `tests/ws_gateway/test_session_lifecycle.py`
|
||||
- Comportamento preservado:
|
||||
- deteccao de entrada e saida do agente remoto
|
||||
- processamento do pacote `agent.stage` com `DONE`
|
||||
- envio de `stop` para o cliente ao final da chamada
|
||||
- Comportamento alterado:
|
||||
- criacao de `RoomLifecycleState` para concentrar `agent_participant` e `done_payload`
|
||||
- extracao de `register_room_lifecycle_handlers(...)`
|
||||
- extracao de `watch_call_done(...)`
|
||||
- Risco conhecido:
|
||||
- o fluxo ainda depende de coordenacao concorrente entre tasks do bridge; a extracao reduz tamanho do endpoint, mas nao muda o modelo de concorrencia
|
||||
- Validacao executada:
|
||||
- `python3 -m compileall src/app/ws_gateway tests/ws_gateway`
|
||||
- `python3 -m pytest tests/ws_gateway/test_session_lifecycle.py -q`
|
||||
- `python3 -m pytest -q`
|
||||
- resultado observado na etapa: `43 passed`
|
||||
- Proximo passo:
|
||||
- extrair a camada de intro/sinalizacao do bridge
|
||||
|
||||
## Entrada 019 - Extracao da intro TTS e da sinalizacao de audio no bridge
|
||||
|
||||
- Data: 2026-04-07
|
||||
- Objetivo: isolar do endpoint o trecho responsavel por intro TTS, liberacao do audio do cliente, sinalizacao `client_audio_enabled` e bootstrap do streaming do agente
|
||||
- Arquivos alterados:
|
||||
- `src/app/ws_gateway/session_audio.py`
|
||||
- `src/app/ws_gateway/main.py`
|
||||
- `tests/ws_gateway/test_session_audio.py`
|
||||
- Comportamento preservado:
|
||||
- intro sintetizada antes da liberacao do audio do cliente
|
||||
- emissao do controle `client_audio_enabled` para o agente
|
||||
- inicializacao do worker que consome audio do agente quando ele fica pronto
|
||||
- Comportamento alterado:
|
||||
- extracao de `play_intro_to_client(...)`
|
||||
- extracao de `notify_client_audio_enabled(...)`
|
||||
- extracao de `stream_agent_audio_when_ready(...)`
|
||||
- `main.py` passa a gerenciar explicitamente a task `t_control`, em vez de disparar a notificacao como fire-and-forget
|
||||
- ajuste de typing para evitar import-time acidental de `audioop` em testes unitarios do modulo novo
|
||||
- Risco conhecido:
|
||||
- o caminho quente de audio continua centralizado em `main.py`; ainda faltam as primitivas de transporte para o arquivo deixar de ser monolitico
|
||||
- Validacao executada:
|
||||
- `python3 -m compileall src/app/ws_gateway tests/ws_gateway`
|
||||
- `python3 -m pytest tests/ws_gateway/test_session_audio.py -q`
|
||||
- `python3 -m pytest -q`
|
||||
- resultado observado na etapa: `46 passed`
|
||||
- Proximo passo:
|
||||
- extrair as primitivas de transporte de audio e LiveKit (`ws_audio_receiver`, `publish_queue_to_livekit`, `connect_publish_livekit`, `stream_agent_audio_to_queue`)
|
||||
|
||||
## Entrada 020 - Validacao do Dockerfile para CI/CD
|
||||
|
||||
- Data: 2026-04-07
|
||||
- Objetivo: revisar o `Dockerfile` frente ao layout atual do projeto e corrigir o principal risco de compatibilidade para pipeline
|
||||
- Arquivos alterados:
|
||||
- `Dockerfile`
|
||||
- Comportamento preservado:
|
||||
- execucao do bridge via `uvicorn app.ws_gateway.main:app`
|
||||
- exposicao da porta `8000`
|
||||
- `PYTHONPATH=/app/src`
|
||||
- healthcheck em `/health`
|
||||
- Comportamento alterado:
|
||||
- troca da base de `python:3.13-slim` para `python:3.12-slim`
|
||||
- o ajuste foi necessario porque o projeto ainda depende de `audioop` em `src/app/ws_gateway/main.py` e `src/app/utils/background.py`
|
||||
- Risco conhecido:
|
||||
- o build real da imagem nao conseguiu ser concluido nesta maquina porque nem o daemon Docker nem a conexao efetiva do Podman estavam operacionais no momento da validacao
|
||||
- portanto, a validacao foi estrutural e de coerencia do arquivo, nao um smoke test completo de container
|
||||
- Validacao executada:
|
||||
- revisao manual de `Dockerfile`, `.dockerignore`, `requirements.txt`, `makefile` e `k8s/deployment.yaml`
|
||||
- tentativa de `docker build`, bloqueada por daemon indisponivel
|
||||
- tentativa de `podman build`, bloqueada por conexao local com a VM do Podman
|
||||
- Proximo passo:
|
||||
- executar um build real da imagem assim que houver runtime de containers disponivel no host ou direto no pipeline
|
||||
116
docs/refactor-plan.md
Normal file
116
docs/refactor-plan.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Refactor Plan
|
||||
|
||||
## Objetivo
|
||||
|
||||
Fazer um refactor incremental da camada de runtime de voz sem quebrar:
|
||||
- contrato do bridge websocket
|
||||
- integracao com LiveKit
|
||||
- pipeline atual de negocio
|
||||
- comportamento de interrupcao, idle nudge e finalizacao
|
||||
|
||||
## Problema atual
|
||||
|
||||
Hoje o arquivo `app/livekit/main.py` concentra responsabilidades demais:
|
||||
- wiring do AgentSession
|
||||
- timers
|
||||
- politica de interrupcao
|
||||
- idle nudge
|
||||
- finalizacao
|
||||
- chamada da pipeline
|
||||
- speak / wait_for_playout
|
||||
- notificacao ao bridge
|
||||
|
||||
Isso dificulta manutencao, teste e evolucao para alternativas futuras como:
|
||||
- `llm_node`
|
||||
- agente remoto via API
|
||||
- politica de chamada mais previsivel
|
||||
|
||||
## Direcao arquitetural
|
||||
|
||||
Separar o runtime em camadas:
|
||||
- adapters: wrappers dos componentes atuais (pipeline, speech, bridge, export)
|
||||
- runtime: coordenacao da chamada
|
||||
- domain: estado, eventos e comandos
|
||||
- policies: regras puras de interrupcao, idle e finalizacao
|
||||
- engine: reducer e scheduler
|
||||
|
||||
## Principios
|
||||
|
||||
- preservar comportamento antes de trocar mecanismo
|
||||
- extrair boundaries antes de mudar a orquestracao
|
||||
- isolar side effects
|
||||
- tornar finalizacao idempotente
|
||||
- introduzir estado explicito da chamada
|
||||
|
||||
## Fases sugeridas
|
||||
|
||||
### Fase 1: Boundaries
|
||||
|
||||
Extrair sem mudar comportamento:
|
||||
- `PipelineAdapter`
|
||||
- `SpeechService`
|
||||
- `BridgeGateway`
|
||||
- `ExportService`
|
||||
|
||||
Saida esperada:
|
||||
- `app/livekit/main.py` menor
|
||||
- regras ainda iguais as de hoje
|
||||
|
||||
### Fase 2: Runtime explicito
|
||||
|
||||
Introduzir:
|
||||
- `CallState`
|
||||
- `CallRuntime`
|
||||
- comandos e eventos
|
||||
|
||||
Saida esperada:
|
||||
- menor uso de `nonlocal`
|
||||
- caminhos de execucao mais faceis de seguir
|
||||
|
||||
### Fase 3: Policies e scheduler
|
||||
|
||||
Migrar:
|
||||
- interrupcao
|
||||
- idle nudge
|
||||
- idle close
|
||||
- finalize once
|
||||
|
||||
Saida esperada:
|
||||
- regras de chamada isoladas e testaveis
|
||||
|
||||
### Fase 4: Evolucao de IA
|
||||
|
||||
Avaliar depois da estabilizacao:
|
||||
- `llm_node`
|
||||
- agente remoto via API
|
||||
- SSE / websocket para agente externo
|
||||
|
||||
Saida esperada:
|
||||
- STT / IA / TTS com fronteiras mais limpas
|
||||
|
||||
## Fora de escopo inicial
|
||||
|
||||
- reescrever bridge
|
||||
- substituir pipeline de negocio
|
||||
- trocar protocolo websocket externo
|
||||
- mudar contrato de audio
|
||||
|
||||
## Definition of done por fase
|
||||
|
||||
### Fase 1
|
||||
- sem mudanca de contrato externo
|
||||
- sem mudanca de fluxo de audio
|
||||
- classes novas usadas pelo `main.py`
|
||||
|
||||
### Fase 2
|
||||
- estado da chamada centralizado
|
||||
- menos logica de coordenacao inline
|
||||
|
||||
### Fase 3
|
||||
- timers centralizados
|
||||
- finalizacao unica e previsivel
|
||||
- regras de interrupcao sem duplicacao
|
||||
|
||||
### Fase 4
|
||||
- nova integracao de IA desacoplada do fluxo manual atual
|
||||
- comparacao controlada com comportamento anterior
|
||||
416
docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md
Normal file
416
docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# Arquitetura TIA Regional com Pool xAI Pré-Aquecido
|
||||
|
||||
## 1. Introdução
|
||||
|
||||
Este documento descreve a evolução do TIA para operar TTS xAI em alta disponibilidade e alta volumetria usando réplicas Kubernetes distribuídas por região. A solução parte da implementação atual do TIA/LiveKit e mantém seu contrato de TTS, suas métricas de underflow/TTFB e sua lógica de proteção contra repetição após áudio parcial.
|
||||
|
||||
A mudança principal é mover a manutenção do conjunto de conexões WebSocket xAI para um **pool persistente por Pod TIA**, implementado como sidecar. Cada Pod regional pode manter até `XAI_POOL_SIZE` conexões xAI pré-aquecidas e prontas para uso. O TIA continua enxergando um endpoint compatível com xAI, porém local (`127.0.0.1`).
|
||||
|
||||
## 2. Dificuldade do modelo anterior
|
||||
|
||||
Na versão anterior, cada instância `OraclexAITTS` mantinha uma única conexão reutilizável. Como o LiveKit executa chamadas em processos de job, essas conexões são naturalmente distribuídas por chamadas/processos e não formam um pool global do Pod.
|
||||
|
||||
Isso cria alguns riscos em alta volumetria:
|
||||
|
||||
1. **Burst de handshakes WebSocket.** Muitas chamadas podem abrir conexões ao xAI praticamente ao mesmo tempo.
|
||||
2. **Capacidade não compartilhada.** Uma chamada pode manter uma conexão ociosa enquanto outra precisa abrir uma nova.
|
||||
3. **Ausência de backpressure no balanceador.** O LB enxerga o TIA como saudável mesmo quando a capacidade de TTS daquele Pod está totalmente ocupada.
|
||||
4. **Acoplamento entre sessão e conexão xAI.** A quantidade de chamadas pode virar, indiretamente, a quantidade de conexões abertas, mesmo quando apenas uma parte das chamadas está sintetizando naquele instante.
|
||||
5. **Falha regional afeta novas chamadas.** Sem readiness orientada à saúde/capacidade do xAI, novas sessões podem continuar chegando a uma réplica cuja região está degradada.
|
||||
|
||||
O histórico de testes do TTS já mostrou que problemas de estabelecimento de WebSocket e jitter podem se manifestar de forma regional e em função de carga. A arquitetura proposta transforma esses sinais em capacidade operacional do Pod.
|
||||
|
||||
## 3. Objetivos
|
||||
|
||||
A solução tem os seguintes objetivos:
|
||||
|
||||
- manter conexões OCI xAI abertas e pré-aquecidas;
|
||||
- reutilizar uma conexão entre diferentes sínteses;
|
||||
- reservar conexão upstream apenas durante uma utterance;
|
||||
- liberar a conexão imediatamente após `audio.done`;
|
||||
- impedir bursts de abertura de WebSockets no caminho crítico da chamada;
|
||||
- permitir TIA ativo/ativo em múltiplas regiões;
|
||||
- retirar automaticamente uma réplica saturada do balanceamento de novas conexões;
|
||||
- manter chamadas existentes durante draining/rollout;
|
||||
- renovar sockets antes do TTL de forma escalonada, evitando reconexão simultânea;
|
||||
- preservar o protocolo atual do `OraclexAITTS` e minimizar mudanças no código de voz;
|
||||
- permitir escalabilidade horizontal em Kubernetes.
|
||||
|
||||
## 4. Arquitetura proposta
|
||||
|
||||
```text
|
||||
Clientes / Telefonia
|
||||
|
|
||||
v
|
||||
Load Balancer / Service
|
||||
(novas conexões WebSocket)
|
||||
|
|
||||
+--------------------+--------------------+
|
||||
| |
|
||||
v v
|
||||
Deployment ORD Deployment IAD
|
||||
| |
|
||||
+--------+--------+ +--------+--------+
|
||||
| Pod TIA ORD | | Pod TIA IAD |
|
||||
| | | |
|
||||
| bridge | | bridge |
|
||||
| agent/livekit | | agent/livekit |
|
||||
| | | | | |
|
||||
| v | | v |
|
||||
| xAI pool sidecar| | xAI pool sidecar|
|
||||
| 50 WS warm | | 50 WS warm |
|
||||
+-------+---------+ +-------+---------+
|
||||
| |
|
||||
v v
|
||||
OCI xAI ORD OCI xAI IAD
|
||||
```
|
||||
|
||||
O mesmo `Service` Kubernetes seleciona Pods ORD e IAD. Cada deployment acrescenta o label `tia-region`, útil para métricas e operação, mas ambos compartilham `app=${APP_NAME}-regional`.
|
||||
|
||||
## 5. Componentes
|
||||
|
||||
### 5.1 Bridge TIA
|
||||
|
||||
Mantém o WebSocket de entrada e o comportamento existente do TIA. O código principal não precisa conhecer o endpoint xAI regional.
|
||||
|
||||
### 5.2 LiveKit Agent
|
||||
|
||||
Continua instanciando `OraclexAITTS`, porém passa a usar:
|
||||
|
||||
```text
|
||||
XAI_WEBSOCKET_URL=ws://127.0.0.1:18100/xai/v1/tts
|
||||
```
|
||||
|
||||
O adapter continua enviando:
|
||||
|
||||
```text
|
||||
text.clear
|
||||
text.delta
|
||||
text.done
|
||||
```
|
||||
|
||||
e recebendo:
|
||||
|
||||
```text
|
||||
audio.clear
|
||||
audio.delta
|
||||
audio.done
|
||||
```
|
||||
|
||||
Portanto, a lógica atual de streaming, TTFB, gaps, underflow e prevenção de replay permanece válida.
|
||||
|
||||
### 5.3 Sidecar `xai-pool`
|
||||
|
||||
Implementado em:
|
||||
|
||||
```text
|
||||
src/app/livekit/adapters/xai_pool_proxy.py
|
||||
```
|
||||
|
||||
Responsabilidades:
|
||||
|
||||
- abrir `XAI_POOL_SIZE` WebSockets no startup;
|
||||
- usar abertura em ondas controladas (`XAI_POOL_PREWARM_CONCURRENCY`);
|
||||
- manter os sockets vivos;
|
||||
- recuperar automaticamente slots desconectados;
|
||||
- renovar sockets antes do TTL;
|
||||
- aplicar jitter no refresh para não reconectar todos simultaneamente;
|
||||
- emprestar uma conexão a uma utterance;
|
||||
- devolver a conexão ao pool depois de `audio.done`;
|
||||
- expor health, readiness, status, drain e métricas.
|
||||
|
||||
### 5.4 OCI xAI regional
|
||||
|
||||
Cada deployment recebe seu endpoint próprio por `XAI_POOL_UPSTREAM_URL`.
|
||||
|
||||
Exemplo:
|
||||
|
||||
```text
|
||||
ORD -> wss://...us-chicago-1.../xai/v1/tts
|
||||
IAD -> wss://...us-ashburn-1.../xai/v1/tts
|
||||
```
|
||||
|
||||
As credenciais reais ficam somente no sidecar de pool. O container `agent` usa uma credencial local dummy porque se conecta apenas a `localhost`.
|
||||
|
||||
### 5.5 Kubernetes Service / Load Balancer
|
||||
|
||||
O Service seleciona todas as réplicas regionais. O WebSocket funciona normalmente através do LB: o balanceador escolhe um Pod durante o HTTP Upgrade e aquela conexão permanece no mesmo backend durante sua vida.
|
||||
|
||||
A estratégia de capacidade não tenta migrar uma conexão existente. Ela afeta **novas conexões**.
|
||||
|
||||
## 6. Pool compartilhado por Pod
|
||||
|
||||
Uma chamada não reserva um socket xAI por toda sua duração.
|
||||
|
||||
```text
|
||||
Call A falando ---------- sem TTS
|
||||
Call B ouvindo ---------- sem nova síntese
|
||||
Call C precisa falar ---- acquire WS #17
|
||||
text.clear
|
||||
text.delta
|
||||
text.done
|
||||
audio.delta...
|
||||
audio.done
|
||||
release WS #17
|
||||
```
|
||||
|
||||
Portanto, 200 chamadas podem coexistir em um Pod com pool de 50, desde que não existam mais de 50 sínteses concorrentes naquele instante.
|
||||
|
||||
Essa é a principal diferença entre dimensionar por **chamadas simultâneas** e por **utterances TTS simultâneas**.
|
||||
|
||||
## 7. Readiness orientada à capacidade
|
||||
|
||||
O sidecar expõe:
|
||||
|
||||
```text
|
||||
GET /healthz
|
||||
GET /readyz
|
||||
GET /pool/status
|
||||
GET /metrics
|
||||
POST /drain
|
||||
```
|
||||
|
||||
`/healthz` responde se o processo está vivo.
|
||||
|
||||
`/readyz` responde se o Pod deve aceitar **novas chamadas**.
|
||||
|
||||
Exemplo padrão:
|
||||
|
||||
```text
|
||||
XAI_POOL_SIZE=50
|
||||
XAI_POOL_UNAVAILABLE_FREE=2
|
||||
XAI_POOL_RECOVER_FREE=5
|
||||
```
|
||||
|
||||
Com o Pod inicialmente pronto:
|
||||
|
||||
```text
|
||||
free > 2 -> ready
|
||||
free <= 2 -> not ready (HTTP 503)
|
||||
```
|
||||
|
||||
Depois de sair da rotação, só retorna quando:
|
||||
|
||||
```text
|
||||
free >= 5 -> ready novamente
|
||||
```
|
||||
|
||||
Isso cria histerese e evita flapping de readiness em torno do limite.
|
||||
|
||||
Para a política estrita sugerida de somente sair quando todas estiverem ocupadas:
|
||||
|
||||
```text
|
||||
XAI_POOL_UNAVAILABLE_FREE=0
|
||||
XAI_POOL_RECOVER_FREE=5
|
||||
```
|
||||
|
||||
Em produção recomenda-se uma pequena reserva (por exemplo 2 a 5 conexões), pois ela absorve rajadas e reduz a chance de uma sessão recém-chegada não encontrar capacidade.
|
||||
|
||||
## 8. Como o LB redireciona tráfego
|
||||
|
||||
Em Kubernetes, um Pod é Ready apenas quando todos os containers que possuem readiness probe estão Ready.
|
||||
|
||||
O sidecar `xai-pool` possui uma probe em `/readyz`.
|
||||
|
||||
Quando a capacidade acaba:
|
||||
|
||||
```text
|
||||
xai-pool /readyz -> 503
|
||||
|
|
||||
v
|
||||
Pod becomes NotReady
|
||||
|
|
||||
v
|
||||
Pod removed from Service Endpoints
|
||||
|
|
||||
v
|
||||
LB stops sending NEW connections
|
||||
```
|
||||
|
||||
As conexões WebSocket já estabelecidas não são redirecionadas e continuam no Pod enquanto o processo continuar disponível.
|
||||
|
||||
## 9. Renovação escalonada das conexões
|
||||
|
||||
Manter 50 sockets abertos indefinidamente sem renovação é arriscado porque serviços upstream normalmente aplicam TTL e renovação de autorização.
|
||||
|
||||
A configuração padrão usa:
|
||||
|
||||
```text
|
||||
XAI_POOL_CONNECTION_TTL_S=540
|
||||
XAI_POOL_REFRESH_JITTER_S=45
|
||||
```
|
||||
|
||||
Cada conexão recebe um refresh deadline diferente:
|
||||
|
||||
```text
|
||||
WS01 -> ~501s
|
||||
WS02 -> ~527s
|
||||
WS03 -> ~509s
|
||||
...
|
||||
```
|
||||
|
||||
Somente conexões livres são renovadas. Isso evita um evento no qual 50 conexões expiram e fazem handshake simultaneamente.
|
||||
|
||||
## 10. Alta disponibilidade regional
|
||||
|
||||
Operação normal:
|
||||
|
||||
```text
|
||||
LB
|
||||
|-- ORD Pod 1 -> ready
|
||||
|-- ORD Pod 2 -> ready
|
||||
|-- IAD Pod 1 -> ready
|
||||
`-- IAD Pod 2 -> ready
|
||||
```
|
||||
|
||||
Se ORD perder saúde/capacidade xAI, os slots começam a falhar e a quantidade de conexões saudáveis/livres cai. Quando a readiness cruza o threshold, os Pods ORD saem dos endpoints e novas chamadas passam a ser atendidas pelos Pods IAD disponíveis.
|
||||
|
||||
Não há necessidade de alterar o cliente ou o LiveKit para escolher a região.
|
||||
|
||||
## 11. Escala horizontal
|
||||
|
||||
A capacidade teórica de pool é:
|
||||
|
||||
```text
|
||||
capacidade regional de sockets = replicas_region * XAI_POOL_SIZE
|
||||
```
|
||||
|
||||
Exemplo:
|
||||
|
||||
```text
|
||||
ORD: 2 pods x 25 sockets = 50
|
||||
IAD: 2 pods x 25 sockets = 50
|
||||
TOTAL = 100 sockets prewarmed
|
||||
```
|
||||
|
||||
ou, se a OCI conceder capacidade independente suficiente:
|
||||
|
||||
```text
|
||||
ORD: 2 pods x 50 = 100
|
||||
IAD: 2 pods x 50 = 100
|
||||
TOTAL = 200
|
||||
```
|
||||
|
||||
### Restrição crítica
|
||||
|
||||
`replicas * XAI_POOL_SIZE` **não pode ultrapassar o limite real concedido pela OCI para o endpoint/tenancy/região**.
|
||||
|
||||
Se a OCI disser que o limite 50 é global por endpoint, então duas réplicas de 50 no mesmo endpoint seriam incorretas. Nesse caso use, por exemplo:
|
||||
|
||||
```text
|
||||
2 replicas x 25 = 50 total
|
||||
```
|
||||
|
||||
ou obtenha endpoints/capacidades independentes.
|
||||
|
||||
## 12. Escala visual
|
||||
|
||||
```text
|
||||
Carga baixa
|
||||
=========
|
||||
LB
|
||||
|-- ORD-1 [pool 50: 10 leased / 40 free]
|
||||
`-- IAD-1 [pool 50: 8 leased / 42 free]
|
||||
|
||||
Carga aumenta
|
||||
=============
|
||||
LB
|
||||
|-- ORD-1 [47 leased / 3 free] READY
|
||||
`-- IAD-1 [30 leased /20 free] READY
|
||||
|
||||
ORD satura
|
||||
===========
|
||||
LB
|
||||
|-- ORD-1 [48 leased /2 free] NOT READY -> sem novas chamadas
|
||||
`-- IAD-1 [31 leased /19 free] READY -> recebe novas chamadas
|
||||
|
||||
ORD recupera
|
||||
=============
|
||||
ORD-1 chega a 45 leased /5 free
|
||||
/readyz volta a 200
|
||||
LB volta a considerá-lo para novas conexões
|
||||
```
|
||||
|
||||
## 13. Draining e rollout
|
||||
|
||||
O deployment usa:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
```
|
||||
|
||||
O sidecar executa no `preStop`:
|
||||
|
||||
```text
|
||||
POST /drain
|
||||
```
|
||||
|
||||
Isso torna `/readyz` imediatamente 503, removendo o Pod da entrada de novas conexões antes de sua finalização.
|
||||
|
||||
`terminationGracePeriodSeconds` deve ser compatível com a duração/grace desejada para as chamadas existentes.
|
||||
|
||||
## 14. Segurança
|
||||
|
||||
As credenciais OCI/xAI reais ficam no Secret indicado por `XAI_SECRET_NAME` e são montadas apenas no sidecar.
|
||||
|
||||
O agent conecta a localhost e não precisa conhecer a chave real do xAI.
|
||||
|
||||
Para produção recomenda-se evoluir para `OKE_WORKLOAD_IDENTITY` sempre que suportado pela política do ambiente, eliminando API keys estáticas.
|
||||
|
||||
## 15. Observabilidade
|
||||
|
||||
`GET /pool/status` retorna:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ready",
|
||||
"region": "ord",
|
||||
"configured": 50,
|
||||
"healthy": 50,
|
||||
"leased": 17,
|
||||
"free": 33,
|
||||
"total_acquires": 845,
|
||||
"total_acquire_timeouts": 0,
|
||||
"total_proxy_failures": 0
|
||||
}
|
||||
```
|
||||
|
||||
`GET /metrics` expõe métricas Prometheus simples:
|
||||
|
||||
- `tia_xai_pool_connections{state="healthy"}`
|
||||
- `tia_xai_pool_connections{state="leased"}`
|
||||
- `tia_xai_pool_connections{state="free"}`
|
||||
- `tia_xai_pool_acquires_total`
|
||||
- `tia_xai_pool_acquire_timeouts_total`
|
||||
- `tia_xai_pool_proxy_failures_total`
|
||||
|
||||
Estas métricas devem ser correlacionadas com as métricas já existentes no TIA, especialmente TTFB, gap e underflow.
|
||||
|
||||
## 16. O que esta versão resolve
|
||||
|
||||
| Problema | Solução |
|
||||
|---|---|
|
||||
| handshakes xAI no caminho crítico | pool pré-aquecido |
|
||||
| burst de WebSockets | prewarm em ondas + refresh com jitter |
|
||||
| conexão presa a uma chamada | lease somente durante utterance |
|
||||
| LB envia tráfego a Pod sem capacidade TTS | `/readyz` baseado no pool |
|
||||
| flapping de health | histerese unavailable/recover |
|
||||
| indisponibilidade regional | Deployments ORD/IAD no mesmo Service |
|
||||
| rollout derruba novas sessões | drain + RollingUpdate |
|
||||
| chave xAI em todos os processos | credencial real somente no sidecar |
|
||||
| observabilidade de capacidade | `/pool/status` + `/metrics` |
|
||||
|
||||
## 17. O que esta versão não resolve sozinha
|
||||
|
||||
A solução não cria capacidade de inferência no OCI xAI. Se todos os endpoints regionais terminarem no mesmo pool de inferência saturado, o TIA terá failover e melhor utilização de sockets, mas não multiplicará a capacidade real do modelo.
|
||||
|
||||
É necessário confirmar com a OCI:
|
||||
|
||||
1. limite de WebSockets por endpoint/região/tenancy;
|
||||
2. se endpoints dedicados possuem capacidade independente;
|
||||
3. se API servers/LB e inference workers são dedicados ou compartilhados;
|
||||
4. quais limites podem ser reservados/negociados para a TIM.
|
||||
32
docs/regional/CHANGELOG_IMPLEMENTACAO.md
Normal file
32
docs/regional/CHANGELOG_IMPLEMENTACAO.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Changelog — TIA Regional / xAI Pool
|
||||
|
||||
## Implementação adicionada
|
||||
|
||||
- sidecar `xai_pool_proxy` compatível com o protocolo WebSocket xAI usado pelo TIA;
|
||||
- pool pré-aquecido configurável por Pod (`XAI_POOL_SIZE`, default de exemplo 50);
|
||||
- acquire por utterance após `text.clear`;
|
||||
- release automático após `audio.done`;
|
||||
- recuperação de slots desconectados;
|
||||
- refresh escalonado por TTL + jitter;
|
||||
- prewarm em ondas para evitar burst de handshake;
|
||||
- `/healthz`, `/readyz`, `/pool/status`, `/metrics` e `/drain`;
|
||||
- readiness com histerese;
|
||||
- Deployments regionais ORD/IAD usando a mesma imagem;
|
||||
- credencial xAI real isolada no sidecar;
|
||||
- Service único para balancear Pods regionais;
|
||||
- RollingUpdate, PDB e HPA;
|
||||
- scripts de renderização, validação e deployment;
|
||||
- manuais de arquitetura, implantação e testes.
|
||||
|
||||
## Compatibilidade
|
||||
|
||||
O modo anterior permanece disponível. O deployment regional é opcional e não remove `XAI_WEBSOCKET_URL` direto para OCI xAI.
|
||||
|
||||
## Validações executadas na geração
|
||||
|
||||
- `py_compile` / `compileall` do novo sidecar: PASS;
|
||||
- parsing YAML dos templates Kubernetes: PASS;
|
||||
- renderização ORD/IAD via `envsubst`: PASS;
|
||||
- parsing YAML dos manifests renderizados: PASS.
|
||||
|
||||
Não foi executado teste real contra OCI xAI, pois depende das credenciais/endpoints do ambiente TIM/OCI. O manual descreve smoke, saturação, failover e stress test a executar em FQA.
|
||||
385
docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md
Normal file
385
docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md
Normal file
@@ -0,0 +1,385 @@
|
||||
# Deployment — TIA Regional com Pool xAI
|
||||
|
||||
## 1. Pré-requisitos
|
||||
|
||||
- cluster Kubernetes/OKE funcional;
|
||||
- `kubectl` configurado;
|
||||
- `envsubst` (`gettext`) instalado na máquina de deployment;
|
||||
- imagem TIA construída a partir desta versão;
|
||||
- ConfigMap `${APP_NAME}-config` já existente;
|
||||
- Secrets `${APP_NAME}-api-secrets`, `${APP_NAME}-google-sa-secret` e `shared-tls-secret` já existentes conforme deployment atual;
|
||||
- endpoint e credencial xAI de cada região;
|
||||
- quota xAI validada para o número total de WebSockets configurado.
|
||||
|
||||
## 2. Arquivos adicionados
|
||||
|
||||
```text
|
||||
src/app/livekit/adapters/xai_pool_proxy.py
|
||||
|
||||
k8s/regional/
|
||||
deployment-region.yaml
|
||||
service.yaml
|
||||
pdb.yaml
|
||||
hpa-region.yaml
|
||||
xai-secret.example.yaml
|
||||
regions.env.example
|
||||
|
||||
scripts/
|
||||
render-regional-k8s.sh
|
||||
validate-regional-k8s.sh
|
||||
deploy-regional-k8s.sh
|
||||
```
|
||||
|
||||
## 3. Construir a imagem
|
||||
|
||||
Use o pipeline existente ou Dockerfile atual do TIA. O novo sidecar utiliza a mesma imagem e apenas muda o módulo executado:
|
||||
|
||||
```text
|
||||
python -m app.livekit.adapters.xai_pool_proxy
|
||||
```
|
||||
|
||||
Exemplo local:
|
||||
|
||||
```bash
|
||||
docker build -f k8s/tia/Dockerfile \
|
||||
-t iad.ocir.io/<namespace>/tia:regional-xai-pool-v1 .
|
||||
```
|
||||
|
||||
Faça push para o registry usado pelo cluster.
|
||||
|
||||
## 4. Criar arquivo de ambiente de deployment
|
||||
|
||||
```bash
|
||||
cp k8s/regional/regions.env.example k8s/regional/regions.env
|
||||
```
|
||||
|
||||
Edite no mínimo:
|
||||
|
||||
```bash
|
||||
APP_NAME=tim-ai-atend-agnt-integ-tia
|
||||
K8S_NAMESPACE=...
|
||||
IMAGE_REPOSITORY=...
|
||||
IMAGE_TAG=...
|
||||
|
||||
ORD_XAI_UPSTREAM_URL=wss://...us-chicago-1.../xai/v1/tts
|
||||
ORD_XAI_SECRET_NAME=xai-ord-credentials
|
||||
|
||||
IAD_XAI_UPSTREAM_URL=wss://...us-ashburn-1.../xai/v1/tts
|
||||
IAD_XAI_SECRET_NAME=xai-iad-credentials
|
||||
```
|
||||
|
||||
## 5. Definir tamanho do pool
|
||||
|
||||
Para um Pod com 50 sockets:
|
||||
|
||||
```bash
|
||||
XAI_POOL_SIZE=50
|
||||
```
|
||||
|
||||
Readiness recomendada:
|
||||
|
||||
```bash
|
||||
XAI_POOL_UNAVAILABLE_FREE=2
|
||||
XAI_POOL_RECOVER_FREE=5
|
||||
```
|
||||
|
||||
Se desejar exatamente o comportamento "só sair quando os 50 estiverem ocupados":
|
||||
|
||||
```bash
|
||||
XAI_POOL_UNAVAILABLE_FREE=0
|
||||
XAI_POOL_RECOVER_FREE=5
|
||||
```
|
||||
|
||||
### Importante
|
||||
|
||||
Se houver `N` réplicas apontando para o mesmo endpoint:
|
||||
|
||||
```text
|
||||
sockets máximos = N * XAI_POOL_SIZE
|
||||
```
|
||||
|
||||
Nunca configure isso acima da quota xAI real.
|
||||
|
||||
## 6. Criar Secrets regionais
|
||||
|
||||
Não versione chaves reais.
|
||||
|
||||
Exemplo por linha de comando:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" create secret generic xai-ord-credentials \
|
||||
--from-literal=XAI_API_KEY='<ORD_KEY>' \
|
||||
--from-literal=OCI_COMPARTMENT_ID='<COMPARTMENT_OCID>'
|
||||
|
||||
kubectl -n "$K8S_NAMESPACE" create secret generic xai-iad-credentials \
|
||||
--from-literal=XAI_API_KEY='<IAD_KEY>' \
|
||||
--from-literal=OCI_COMPARTMENT_ID='<COMPARTMENT_OCID>'
|
||||
```
|
||||
|
||||
Para `API_KEY`, `OCI_COMPARTMENT_ID` pode ficar vazio se o fluxo upstream não o exigir.
|
||||
|
||||
Para produção, prefira OCI Vault/External Secrets/Workload Identity em vez de gravar chaves no repositório.
|
||||
|
||||
## 7. Renderizar manifests
|
||||
|
||||
```bash
|
||||
./scripts/render-regional-k8s.sh
|
||||
```
|
||||
|
||||
Saída:
|
||||
|
||||
```text
|
||||
k8s/regional/rendered/
|
||||
deployment-ord.yaml
|
||||
deployment-iad.yaml
|
||||
hpa-ord.yaml
|
||||
hpa-iad.yaml
|
||||
service.yaml
|
||||
pdb.yaml
|
||||
```
|
||||
|
||||
É possível escolher outro arquivo e diretório:
|
||||
|
||||
```bash
|
||||
./scripts/render-regional-k8s.sh ./minha-config.env /tmp/tia-regional
|
||||
```
|
||||
|
||||
## 8. Validar sem implantar
|
||||
|
||||
```bash
|
||||
./scripts/validate-regional-k8s.sh
|
||||
```
|
||||
|
||||
O script usa:
|
||||
|
||||
```bash
|
||||
kubectl apply --dry-run=client
|
||||
```
|
||||
|
||||
Revise também:
|
||||
|
||||
```bash
|
||||
kubectl diff -f k8s/regional/rendered/
|
||||
```
|
||||
|
||||
## 9. Implantar
|
||||
|
||||
```bash
|
||||
./scripts/deploy-regional-k8s.sh
|
||||
```
|
||||
|
||||
Ou manualmente:
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/regional/rendered/service.yaml
|
||||
kubectl apply -f k8s/regional/rendered/pdb.yaml
|
||||
kubectl apply -f k8s/regional/rendered/deployment-ord.yaml
|
||||
kubectl apply -f k8s/regional/rendered/deployment-iad.yaml
|
||||
kubectl apply -f k8s/regional/rendered/hpa-ord.yaml
|
||||
kubectl apply -f k8s/regional/rendered/hpa-iad.yaml
|
||||
```
|
||||
|
||||
## 10. Verificar rollout
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" get pods -l app=${APP_NAME}-regional -o wide
|
||||
```
|
||||
|
||||
Todos os containers devem ficar Ready:
|
||||
|
||||
```text
|
||||
bridge 1/1
|
||||
agent 1/1
|
||||
xai-pool 1/1
|
||||
```
|
||||
|
||||
Confira os deployments:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" rollout status deployment/${APP_NAME}-ord
|
||||
kubectl -n "$K8S_NAMESPACE" rollout status deployment/${APP_NAME}-iad
|
||||
```
|
||||
|
||||
## 11. Validar o pool
|
||||
|
||||
Escolha um Pod:
|
||||
|
||||
```bash
|
||||
POD=$(kubectl -n "$K8S_NAMESPACE" get pod \
|
||||
-l app=${APP_NAME}-regional,tia-region=ord \
|
||||
-o jsonpath='{.items[0].metadata.name}')
|
||||
```
|
||||
|
||||
Port-forward:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" port-forward "$POD" 18100:18100
|
||||
```
|
||||
|
||||
Em outro terminal:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:18100/healthz | jq
|
||||
curl -s http://127.0.0.1:18100/readyz | jq
|
||||
curl -s http://127.0.0.1:18100/pool/status | jq
|
||||
curl -s http://127.0.0.1:18100/metrics
|
||||
```
|
||||
|
||||
Resultado esperado após prewarm:
|
||||
|
||||
```json
|
||||
{
|
||||
"region": "ord",
|
||||
"configured": 50,
|
||||
"healthy": 50,
|
||||
"leased": 0,
|
||||
"free": 50,
|
||||
"status": "ready"
|
||||
}
|
||||
```
|
||||
|
||||
## 12. Logs
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" logs "$POD" -c xai-pool -f
|
||||
```
|
||||
|
||||
Eventos relevantes:
|
||||
|
||||
```text
|
||||
XAI_POOL_SLOT_OPENED
|
||||
XAI_POOL_PREWARM_FAILED
|
||||
XAI_POOL_SLOT_RECOVERY_FAILED
|
||||
XAI_POOL_STARTED
|
||||
```
|
||||
|
||||
## 13. Testar saturação/readiness
|
||||
|
||||
Objetivo: provar que o Pod sai da rotação para novas sessões quando o pool atinge o limite.
|
||||
|
||||
1. Gere sínteses concorrentes suficientes para ocupar o pool.
|
||||
2. Observe:
|
||||
|
||||
```bash
|
||||
watch -n 1 'curl -s http://127.0.0.1:18100/pool/status | jq'
|
||||
```
|
||||
|
||||
3. Quando `free <= XAI_POOL_UNAVAILABLE_FREE`, espere:
|
||||
|
||||
```bash
|
||||
curl -i http://127.0.0.1:18100/readyz
|
||||
```
|
||||
|
||||
Resultado:
|
||||
|
||||
```text
|
||||
HTTP/1.1 503 Service Unavailable
|
||||
```
|
||||
|
||||
4. Verifique o Pod:
|
||||
|
||||
```bash
|
||||
kubectl get pod "$POD"
|
||||
```
|
||||
|
||||
Ele deve ficar `NotReady` enquanto o sidecar estiver sem capacidade.
|
||||
|
||||
5. Quando conexões forem liberadas e `free >= XAI_POOL_RECOVER_FREE`, o `/readyz` volta a 200 e o Pod retorna aos endpoints.
|
||||
|
||||
## 14. Verificar endpoints do Service
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" get endpoints ${APP_NAME}-regional -o wide
|
||||
```
|
||||
|
||||
ou, em clusters novos:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" get endpointslice \
|
||||
-l kubernetes.io/service-name=${APP_NAME}-regional -o yaml
|
||||
```
|
||||
|
||||
Um Pod NotReady não deve ser usado para novas conexões do Service.
|
||||
|
||||
## 15. Testar failover regional
|
||||
|
||||
### Teste controlado de ORD
|
||||
|
||||
Coloque ORD em drain:
|
||||
|
||||
```bash
|
||||
ORD_POD=$(kubectl -n "$K8S_NAMESPACE" get pod \
|
||||
-l app=${APP_NAME}-regional,tia-region=ord \
|
||||
-o jsonpath='{.items[0].metadata.name}')
|
||||
|
||||
kubectl -n "$K8S_NAMESPACE" exec "$ORD_POD" -c xai-pool -- \
|
||||
python -c "import urllib.request; urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:18100/drain', method='POST')).read()"
|
||||
```
|
||||
|
||||
Confirme `/readyz=503` e gere **nova chamada**. Ela deve ser entregue a um Pod IAD ainda Ready.
|
||||
|
||||
Esse teste não deve ser interpretado como migração de uma chamada existente; somente novas conexões são rebalanceadas.
|
||||
|
||||
## 16. Testar recuperação
|
||||
|
||||
O drain manual é intencional e não é revertido. Para voltar o Pod, reinicie-o:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" delete pod "$ORD_POD"
|
||||
```
|
||||
|
||||
O novo Pod deve:
|
||||
|
||||
1. iniciar sidecar;
|
||||
2. pré-aquecer sockets;
|
||||
3. atingir `recover_free_threshold`;
|
||||
4. ficar Ready;
|
||||
5. entrar novamente nos endpoints.
|
||||
|
||||
## 17. Load Balancer e WebSocket
|
||||
|
||||
WebSocket é suportado porque a conexão começa como HTTP Upgrade. O LB escolhe o backend no handshake e mantém a conexão naquele Pod.
|
||||
|
||||
Não use sticky session como requisito de correção. A conexão WebSocket em si já é persistente ao backend selecionado. Em caso de perda do Pod, o cliente precisa reconectar.
|
||||
|
||||
Uma política equivalente a least-connections pode ser útil quando o LB permite configuração, mas o mecanismo primário de proteção desta arquitetura é a readiness baseada em capacidade real do TTS.
|
||||
|
||||
## 18. HPA
|
||||
|
||||
Os manifests incluem HPA por CPU como proteção inicial.
|
||||
|
||||
Atenção: aumentar réplicas também aumenta o número de sockets xAI pré-aquecidos. Portanto, HPA só deve ter `maxReplicas` maior que 1 se a quota xAI permitir:
|
||||
|
||||
```text
|
||||
HPA_MAX_REPLICAS * XAI_POOL_SIZE <= quota regional permitida
|
||||
```
|
||||
|
||||
Para evolução futura, prefira uma métrica customizada que considere chamadas/TTS ativos e também um controlador que respeite orçamento global de sockets.
|
||||
|
||||
## 19. Rollback
|
||||
|
||||
Para voltar ao modelo anterior:
|
||||
|
||||
1. redirecione o Service/LB para o deployment antigo;
|
||||
2. ou restaure o manifesto `k8s/tia/deployment.yaml` original;
|
||||
3. o código antigo `OraclexAITTS` continua presente e compatível com endpoint xAI direto.
|
||||
|
||||
A alteração não remove suporte ao modo anterior.
|
||||
|
||||
## 20. Checklist de produção
|
||||
|
||||
- [ ] confirmar quota real de WebSockets por endpoint/região/tenancy;
|
||||
- [ ] confirmar se ORD e IAD têm pools de capacidade independentes;
|
||||
- [ ] validar `XAI_POOL_SIZE * replicas` por região;
|
||||
- [ ] validar Secret/Vault;
|
||||
- [ ] medir tempo de prewarm dos 50 sockets;
|
||||
- [ ] observar taxa de erro de handshake;
|
||||
- [ ] validar refresh escalonado em janela > 10 minutos;
|
||||
- [ ] testar saturation -> `/readyz=503`;
|
||||
- [ ] testar recovery -> `/readyz=200`;
|
||||
- [ ] testar drain durante chamada ativa;
|
||||
- [ ] testar rollout sem perda de novas chamadas;
|
||||
- [ ] testar perda total de ORD e entrada em IAD;
|
||||
- [ ] correlacionar TTFB/gap/underflow com `pool_free`;
|
||||
- [ ] validar LB/Service com WebSocket real;
|
||||
- [ ] executar stress test com perfil semelhante ao tráfego de produção.
|
||||
151
docs/regional/TESTES_TIA_XAI_REGIONAL.md
Normal file
151
docs/regional/TESTES_TIA_XAI_REGIONAL.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Plano de Testes — TIA Regional / Pool xAI
|
||||
|
||||
## Objetivo
|
||||
|
||||
Validar separadamente capacidade do pool, comportamento do Kubernetes, failover regional, impacto de latência e resiliência do xAI.
|
||||
|
||||
## Camada 1 — Smoke
|
||||
|
||||
1. subir 1 Pod ORD com pool pequeno (`XAI_POOL_SIZE=3`);
|
||||
2. confirmar 3 conexões healthy;
|
||||
3. realizar uma chamada e uma síntese;
|
||||
4. confirmar que `leased` vai 0 -> 1 -> 0;
|
||||
5. confirmar que o socket upstream continua healthy após `audio.done`.
|
||||
|
||||
## Camada 2 — Prewarm
|
||||
|
||||
Subir com `XAI_POOL_SIZE=50` e medir:
|
||||
|
||||
- tempo total até Ready;
|
||||
- taxa de sucesso de handshake;
|
||||
- número máximo de handshakes simultâneos;
|
||||
- impacto de `XAI_POOL_PREWARM_CONCURRENCY` 2, 5 e 10.
|
||||
|
||||
Critério inicial: nenhuma rajada deve reproduzir os erros de conexão observados no modelo burst.
|
||||
|
||||
## Camada 3 — Saturação
|
||||
|
||||
Gerar mais utterances concorrentes que slots.
|
||||
|
||||
Esperado:
|
||||
|
||||
- leases nunca superam `XAI_POOL_SIZE`;
|
||||
- acquire espera até `XAI_POOL_ACQUIRE_TIMEOUT_S`;
|
||||
- readiness fica 503 quando free cruza threshold;
|
||||
- novas chamadas deixam de entrar naquele Pod;
|
||||
- chamadas existentes continuam.
|
||||
|
||||
## Camada 4 — Histerese
|
||||
|
||||
Com size=10:
|
||||
|
||||
```text
|
||||
UNAVAILABLE_FREE=2
|
||||
RECOVER_FREE=5
|
||||
```
|
||||
|
||||
Esperado:
|
||||
|
||||
- free=2 -> NotReady;
|
||||
- free=3/4 -> continua NotReady;
|
||||
- free=5 -> Ready.
|
||||
|
||||
## Camada 5 — Refresh
|
||||
|
||||
Use TTL curto em FQA:
|
||||
|
||||
```text
|
||||
XAI_POOL_CONNECTION_TTL_S=60
|
||||
XAI_POOL_REFRESH_JITTER_S=20
|
||||
```
|
||||
|
||||
Observe por 5 minutos.
|
||||
|
||||
Esperado:
|
||||
|
||||
- sockets são renovados individualmente;
|
||||
- não existe burst de 50 reconnects;
|
||||
- slots ocupados não são renovados no meio da síntese;
|
||||
- pool retorna ao tamanho configurado.
|
||||
|
||||
## Camada 6 — Falha upstream
|
||||
|
||||
Bloqueie ORD ou aponte temporariamente para endpoint inválido.
|
||||
|
||||
Esperado:
|
||||
|
||||
- slots ORD tornam-se unhealthy;
|
||||
- maintenance tenta recuperação;
|
||||
- readiness ORD cai;
|
||||
- Service deixa de enviar novas chamadas a ORD;
|
||||
- IAD continua Ready.
|
||||
|
||||
## Camada 7 — Latência
|
||||
|
||||
Compare três cenários:
|
||||
|
||||
A. TIA -> xAI direto sem pool
|
||||
|
||||
B. TIA -> localhost pool -> xAI com socket já aquecido
|
||||
|
||||
C. TIA -> localhost pool -> xAI durante recuperação/abertura de socket
|
||||
|
||||
Meça:
|
||||
|
||||
- `provider_ttfb_ms`;
|
||||
- `end_to_end_ttfb_ms`;
|
||||
- `max_audio_delta_gap_ms`;
|
||||
- `xai_underrun_estimado_ms`;
|
||||
- connect time do upstream;
|
||||
- `pool_free` e `pool_leased`.
|
||||
|
||||
Hipótese: o hop localhost adiciona latência desprezível frente ao TTFB do provider, enquanto remove handshake xAI do caminho crítico na situação normal.
|
||||
|
||||
## Camada 8 — Carga semelhante a produção
|
||||
|
||||
Evite somente burst C=200. Use sockets persistentes e concorrência de síntese representativa da operação real, seguindo a metodologia que produziu resultados reprodutíveis nos testes anteriores.
|
||||
|
||||
Rodar pelo menos:
|
||||
|
||||
```text
|
||||
20% da carga alvo
|
||||
50%
|
||||
80%
|
||||
100%
|
||||
120% por janela curta
|
||||
```
|
||||
|
||||
## Camada 9 — Rollout
|
||||
|
||||
Com chamadas ativas:
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment/<tia-ord>
|
||||
```
|
||||
|
||||
Validar:
|
||||
|
||||
- Pod antigo entra em drain;
|
||||
- não recebe novas conexões;
|
||||
- Pod novo preaquece antes de ficar Ready;
|
||||
- `maxUnavailable=0` preserva capacidade durante rollout.
|
||||
|
||||
## Camada 10 — Failover regional
|
||||
|
||||
1. ORD e IAD Ready;
|
||||
2. iniciar tráfego contínuo;
|
||||
3. tornar ORD NotReady;
|
||||
4. verificar novas chamadas em IAD;
|
||||
5. recuperar ORD;
|
||||
6. verificar reentrada progressiva.
|
||||
|
||||
## Evidências a guardar
|
||||
|
||||
- logs do sidecar;
|
||||
- `/pool/status` em intervalos de 1s;
|
||||
- métricas Prometheus;
|
||||
- logs TIA de TTFB/underflow;
|
||||
- quantidade de endpoints Ready por região;
|
||||
- distribuição de chamadas por Pod;
|
||||
- erros de WebSocket upstream;
|
||||
- timestamps de `audio.done`.
|
||||
193
docs/vad-pause-logs.md
Normal file
193
docs/vad-pause-logs.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# VAD pause logs
|
||||
|
||||
Este documento resume como analisar pausas de fala do usuario nos logs locais do agente LiveKit.
|
||||
|
||||
## Onde procurar
|
||||
|
||||
Os logs por chamada ficam em `logs/`.
|
||||
|
||||
O arquivo costuma trazer os identificadores principais logo no inicio:
|
||||
|
||||
```text
|
||||
CALL_START | room=dev-room-75a205ec | protocol=PRT-20260409-0001 | session_id=hf05d7c2-1a1a-42e9-8651-ccd6351faff4 | bridge=ws-bridge-dev-2755fb53
|
||||
```
|
||||
|
||||
Use principalmente:
|
||||
|
||||
- `room`: identifica a sala LiveKit e permite cruzar com `timeline/<room>.jsonl`.
|
||||
- `session_id`: identifica a conversa/sessao.
|
||||
- `message_id`: identifica cada turno de usuario ou resposta do agente.
|
||||
- `user_seq`: sequencia de falas finais do usuario.
|
||||
|
||||
## Evento principal: `vad_user_pause`
|
||||
|
||||
`vad_user_pause` indica que o wrapper de VAD registrou uma pausa relevante na fala do usuario.
|
||||
|
||||
Exemplo:
|
||||
|
||||
```text
|
||||
FLOW | step=vad_user_pause | decision=end_of_speech | silence_ms=704 | pause_min_ms=700 | speech_duration_ms=896 | min_interrupt_ms=1600 | eligible=False | probability=0.000 | raw_speech_ms=0 | raw_silence_ms=0
|
||||
```
|
||||
|
||||
Campos essenciais:
|
||||
|
||||
| Campo | Como usar |
|
||||
| --- | --- |
|
||||
| `decision` | Tipo de pausa detectada. `end_of_speech` e o principal para segmentacao real. `pause_threshold_reached` e um alerta intermediario. |
|
||||
| `silence_ms` | Silencio observado pelo VAD, em milissegundos. E o campo mais importante para pausa. |
|
||||
| `pause_min_ms` | Minimo de silencio necessario para fechar a fala. Vem de `LIVEKIT_VAD_MIN_SILENCE_DURATION_S`. |
|
||||
| `speech_duration_ms` | Duracao do trecho de fala fechado pelo VAD. Trechos muito baixos indicam fala picotada. |
|
||||
| `min_interrupt_ms` | Minimo de fala para considerar interrupcao do bot. Nao e o limite de pausa. |
|
||||
| `eligible` | Se a fala passou de `min_interrupt_ms`. Mais util para barge-in do que para pausa. |
|
||||
| `probability` | Probabilidade de fala no frame atual. Ajuda a entender ruido/atividade fraca. |
|
||||
| `raw_speech_ms` | Acumulo bruto usado pelo VAD para detectar inicio de fala. |
|
||||
| `raw_silence_ms` | Acumulo bruto usado pelo VAD para detectar silencio. |
|
||||
|
||||
## Como interpretar pausas
|
||||
|
||||
### Pausa que fechou fala
|
||||
|
||||
Priorize `decision=end_of_speech`.
|
||||
|
||||
Exemplo:
|
||||
|
||||
```text
|
||||
decision=end_of_speech | silence_ms=704 | pause_min_ms=700 | speech_duration_ms=896
|
||||
```
|
||||
|
||||
Leitura:
|
||||
|
||||
- o VAD fechou o trecho apos observar `704ms` de silencio;
|
||||
- o minimo configurado era `700ms`;
|
||||
- como passou apenas `4ms` do minimo, a configuracao esta bem sensivel;
|
||||
- se isso acontece varias vezes durante uma frase natural, a fala esta sendo segmentada cedo demais.
|
||||
|
||||
### Alerta intermediario
|
||||
|
||||
`decision=pause_threshold_reached` significa que o silencio passou do limite minimo antes do fechamento final.
|
||||
|
||||
Exemplo:
|
||||
|
||||
```text
|
||||
decision=pause_threshold_reached | silence_ms=960 | pause_min_ms=700 | speech_duration_ms=0 | raw_speech_ms=32
|
||||
```
|
||||
|
||||
Esse evento deve ser lido com cuidado quando:
|
||||
|
||||
- `speech_duration_ms=0`;
|
||||
- `raw_speech_ms` e muito baixo, como `32`;
|
||||
- `silence_ms` e muito alto, como dezenas de segundos.
|
||||
|
||||
Nesses casos, pode ser artefato de estado acumulado ou ruido antes de uma fala real. Para confirmar impacto na conversa, cruze com `stt_final`.
|
||||
|
||||
## Eventos auxiliares
|
||||
|
||||
### `vad_speech_start`
|
||||
|
||||
Indica inicio de fala detectado pelo VAD.
|
||||
|
||||
```text
|
||||
FLOW | step=vad_speech_start | speech_duration_ms=128 | min_interrupt_ms=1600
|
||||
```
|
||||
|
||||
Use para ver quando o sistema saiu de `listening` para `speaking`.
|
||||
|
||||
### `vad_speech_end`
|
||||
|
||||
Indica fechamento do trecho de fala.
|
||||
|
||||
```text
|
||||
FLOW | step=vad_speech_end | decision=too_short | speech_duration_ms=896 | eligible=False | silence_ms=704 | pause_min_ms=700
|
||||
```
|
||||
|
||||
Use junto com `vad_user_pause`. Se `speech_duration_ms` for baixo varias vezes, a fala pode estar sendo picotada.
|
||||
|
||||
### `vad_interrupt_check`
|
||||
|
||||
Indica que a fala atingiu duracao suficiente para interrupcao do bot.
|
||||
|
||||
```text
|
||||
FLOW | step=vad_interrupt_check | decision=eligible_by_duration | speech_duration_ms=1600 | min_interrupt_ms=1600
|
||||
```
|
||||
|
||||
Esse evento ajuda mais a analisar barge-in/interrupcao do bot do que pausa final de fala.
|
||||
|
||||
### `stt_final`
|
||||
|
||||
Mostra o texto final enviado como turno de usuario.
|
||||
|
||||
```text
|
||||
FLOW | step=stt_final | message_id=12345678-1234-4234-9234-123456789abc | user_seq=3 | text=Veio mais cara
|
||||
```
|
||||
|
||||
Use este evento para validar o efeito real da segmentacao. Se uma frase natural virou varios `stt_final`, o VAD/STT segmentou demais.
|
||||
|
||||
## Checklist de analise
|
||||
|
||||
1. Encontre o `CALL_START` e anote `room`, `session_id` e `protocol`.
|
||||
2. Filtre os eventos `vad_user_pause`.
|
||||
3. Priorize `decision=end_of_speech`.
|
||||
4. Compare `silence_ms` com `pause_min_ms`.
|
||||
5. Verifique se `speech_duration_ms` esta muito baixo.
|
||||
6. Cruze com os `stt_final` seguintes.
|
||||
7. Se uma frase esperada virou varias mensagens curtas, a segmentacao esta agressiva.
|
||||
|
||||
## Sinais de segmentacao agressiva
|
||||
|
||||
Exemplo de fala esperada:
|
||||
|
||||
```text
|
||||
por que a minha fatura veio mais cara
|
||||
```
|
||||
|
||||
Exemplo de saida segmentada:
|
||||
|
||||
```text
|
||||
stt_final | text=porque
|
||||
stt_final | text=a minha fatura
|
||||
stt_final | text=Veio mais cara
|
||||
```
|
||||
|
||||
Se isso vier acompanhado de pausas assim:
|
||||
|
||||
```text
|
||||
vad_user_pause | decision=end_of_speech | silence_ms=704 | pause_min_ms=700
|
||||
```
|
||||
|
||||
provavelmente o limite de pausa esta fechando a fala cedo demais.
|
||||
|
||||
## Parametro de ajuste
|
||||
|
||||
O limite principal e:
|
||||
|
||||
```env
|
||||
LIVEKIT_VAD_MIN_SILENCE_DURATION_S=0.7
|
||||
```
|
||||
|
||||
Ele aparece no log como:
|
||||
|
||||
```text
|
||||
pause_min_ms=700
|
||||
```
|
||||
|
||||
Aumentar esse valor tende a juntar mais frases, porque o VAD espera mais silencio antes de encerrar a fala. O custo e aumentar a latencia percebida: o agente demora um pouco mais para responder depois que o usuario termina.
|
||||
|
||||
Valores para teste manual:
|
||||
|
||||
- `0.85`: ajuste conservador.
|
||||
- `1.0`: tende a reduzir mais a segmentacao.
|
||||
- `1.2`: pode ajudar em fala pausada, mas pode deixar a conversa lenta.
|
||||
|
||||
## Regra pratica
|
||||
|
||||
Para pausa, olhe primeiro:
|
||||
|
||||
```text
|
||||
decision + silence_ms + pause_min_ms
|
||||
```
|
||||
|
||||
Para impacto na conversa, cruze com:
|
||||
|
||||
```text
|
||||
stt_final + user_seq + message_id
|
||||
```
|
||||
Reference in New Issue
Block a user