# 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).