Files
B2BCall-dialer/docs/AI_PIPELINE.md
Matheus 91c0448dd4 feat(ai): pipeline assincrono — transcricao, analise, prompts (fase 20)
Sub-fase B do modulo de IA: novo servico apps/ai-worker (poll + FOR UPDATE
SKIP LOCKED) processa AIJob de TRANSCRIPTION/ANALYSIS disparados
automaticamente apos uma gravacao ficar disponivel, respeitando a cascata
de privacidade Tenant>Queue>Campaign e o entitlement do Plan. Transcricao
separa o WAV estereo em 2 canais (parser proprio, sem ffmpeg) e transcreve
cada perna independente; analise sempre redige dados sensiveis antes de
sair pro provider e valida o resultado contra o schema antes de persistir.
CRUD de AIPromptTemplate/AIPromptVersion em apps/api.

Testado ponta a ponta contra o worker real em Docker e Postgres real com
RLS (cascata de privacidade em 3 cenarios, WAV sintetico real no object
storage, claim/retry/dead-letter reais) — chamada de rede contra
OpenAI/Anthropic continua nunca exercitada (mesma restricao de rede desde
o Provider Layer). Detalhes em docs/AI_PIPELINE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
2026-08-28 16:39:38 -03:00

6.1 KiB

AI Pipeline (agente.md secao 104-116)

Sub-fase B da fase de IA: transforma o Provider Layer (PHASE 19, ver docs/AI_PROVIDERS.md) num pipeline assíncrono de verdade — transcrição e análise de chamadas, disparado sozinho depois que uma gravação fica disponível.

Onde cada peça mora

  • packages/ai/src/privacy.ts — cascata Tenant > Queue > Campaign (secao 122), pura, sem I/O.
  • packages/ai/src/retry.ts — backoff exponencial + dead-letter (secao 108), pura.
  • packages/ai/src/call-analysis-schema.ts — schema JSON + validação manual do resultado de análise (secao 112-113).
  • packages/ai/src/wav-stereo-split.ts — separa um WAV estéreo em dois WAV mono (canal 0 = perna onde record_session foi chamado/"cliente", canal 1 = perna bridgeada/"agente" — convenção não confirmada contra áudio real distinguível nesta sessão, só com tons sintéticos).
  • apps/freeswitch-events/src/ai-trigger.ts — decide, no momento em que uma gravação fica AVAILABLE, se vira um AIJob(TRANSCRIPTION). Chamado a partir de recording.ts logo depois do Recording.create.
  • apps/ai-worker/ — serviço novo (Docker, tsx direto, mesmo padrão de apps/predictive-dialer): tick a cada AI_WORKER_TICK_INTERVAL_MS (default 5s), reserva até AI_WORKER_JOBS_PER_TICK (default 5) jobs pendentes por tenant com FOR UPDATE SKIP LOCKED, processa TRANSCRIPTION/ANALYSIS, marca COMPLETED ou aplica retry/dead-letter.
  • apps/api/src/ai/ai-prompts.controller.ts — CRUD de AIPromptTemplate/AIPromptVersion (secao 114-116), mesma RLS híbrida GLOBAL/tenant dos providers/models.

Decisão de disparar (ou não) um job

Duas checagens independentes, as duas precisam passar (ai-trigger.ts):

  1. Entitlement do Plan (aiEnabled + aiTranscriptionEnabled) — "essa conta contratou IA?". Não-lançante via isFeatureEnabled (novo em packages/entitlements), porque isto é uma decisão em background, não uma requisição HTTP que deveria retornar 403.
  2. Cascata de privacidade Tenant > Queue > Campaign (secao 122) — "esse tenant/fila/campanha específica autorizou processar ESTA chamada?".

O job de ANALYSIS nunca é criado de cara — só depois que o job de TRANSCRIPTION correspondente completa com sucesso, reconsultando a mesma cascata (a config pode ter mudado entre os dois).

Resolução de provider/modelo

agente.md não especifica um algoritmo de seleção quando o tenant tem múltiplos providers/modelos com a mesma capability. Política adotada (apps/ai-worker/src/provider-resolution.ts): prefere BYOK do próprio tenant sobre o provider GLOBAL da plataforma; dentro de cada grupo, o primeiro AIModel habilitado com a capability pedida, por createdAt. Sem heurística de custo/qualidade ainda — revisar se precisar de algo mais rico (ex.: fallback em cadeia, menor custo).

Resolução de prompt template (análise)

Campaign.analysisPromptTemplateId sobrescreve quando setado; senão o primeiro AIPromptTemplate de purpose=ANALYSIS com versão ativa, preferindo um template do próprio tenant sobre o padrão GLOBAL. Se nada for encontrado, o job de ANALYSIS falha com uma mensagem clara ("nenhum template configurado") e segue o retry/dead-letter normal — não é um crash, é uma configuração pendente do admin do tenant.

Redação de dados sensíveis

O texto da transcrição sempre passa pelo SensitiveDataRedactor (secao 123) antes de sair pro provider.analyze(), independente do nível de privacidade — o nível de privacidade controla SE a análise roda, nunca O QUE é enviado quando roda. Defesa em profundidade.

Limitação conhecida: Campaign só pode opt-in, nunca opt-out

Campaign.aiTranscriptionEnabled/aiAnalysisEnabled são dois booleanos independentes (existiam desde a fase Campaigns, secao 63), não o mesmo enum de 3 níveis de Tenant/Queue. Uma campanha não consegue forçar AI_OFF explicitamente quando o tenant/fila já tem um nível mais permissivo — só pode ligar mais IA, nunca desligar. Como "opt-in" é a direção mais segura por padrão, aceitável por agora.

O que foi testado de verdade nesta sessão

Restrição de rede desta sessão continua valendo: só o servidor git é autorizado, nenhuma chamada real saiu pra OpenAI/Anthropic. Testado ao vivo, contra o container b2bcall-ai-worker rodando de verdade (não mocks) e Postgres real com RLS:

  • Cascata de privacidade + entitlement, 3 cenários reais: tenant permissivo → job criado; tenant.aiPrivacyLevel=AI_OFF → nenhum job; privacidade permitindo mas Plan.aiEnabled=false → nenhum job (o entitlement bloqueia mesmo quando a privacidade libera).
  • splitStereoWav com um buffer sintético: valores extremos (±32767/-32768), zero, negativo, todos de-interleaved corretamente; header malformado lança como esperado.
  • retry.ts/privacy.ts/call-analysis-schema.ts: backoff exponencial com teto, dead-letter no limite exato, cascata de privacidade nos 3 níveis, validação aceitando/rejeitando resultado de análise conforme schema.
  • Pipeline completo ponta a ponta: WAV estéreo sintético real gravado no object storage local, Recording real AVAILABLE, AIJob real reservado via FOR UPDATE SKIP LOCKED pelo worker rodando em Docker, download+split+resolução de provider bem-sucedidos, chamada de rede real tentada contra uma porta loopback fechada (http://127.0.0.1:1 — nunca sai da máquina), falha real (fetch failed), retry com backoff de 30s, dead-letter exatamente na 2ª tentativa configurada.
  • processAnalysisJob com uma transcrição semeada manualmente: resolveu template, resolveu provider, redigiu o texto, tentou a chamada real, falhou da mesma forma esperada.

Nunca exercitado: a chamada de rede de verdade contra a API da OpenAI ou Anthropic (mesma restrição desde o Provider Layer); o encadeamento automático TRANSCRIPTION→ANALYSIS após uma transcrição bem-sucedida (só dá pra testar a criação do job de ANALYSIS a partir de uma transcrição semeada manualmente, já que nenhuma transcrição real chega a completar sem rede); a convenção de canal 0/1 contra áudio real distinguível (só tons sintéticos).