# IA — Provider Layer Agente.md secao 95-103. Primeira peça do módulo de IA: a abstração de provider (nunca hardcoda OpenAI no domínio), os adapters OpenAI/Anthropic, capabilities, e o cadastro de providers/modelos (global + BYOK). O pipeline que de fato aciona isso depois de uma chamada terminar (transcrição, análise, jobs assíncronos, prompts, scorecards, usage metering — secao 104-124) é a próxima fase, ver TODO.md. ## Schema completo do módulo de IA, código parcial O schema desta migration (`ai_module`) já cobre TODAS as tabelas das secções 95-124 (`ai_providers`, `ai_models`, `ai_prompt_templates`/ `ai_prompt_versions`, `ai_jobs`, `call_transcriptions`/ `call_transcript_segments`, `call_ai_analyses`, `quality_scorecards`/ `quality_scorecard_items`/`quality_evaluations`, `ai_usage_records`) de uma vez — mais barato revisar o desenho relacional inteiro (FKs entre Call/CallAttempt/Campaign) numa única passada do que fatiar em várias migrations pequenas que se emendam. O **código** que usa essas tabelas é que vem em fases separadas — esta fase só implementa Provider/Model. ## `packages/ai` — `AIProvider` (secao 96) ```typescript interface AIProvider { getCapabilities(): Promise; validateCredentials(): Promise; transcribe?(params): Promise; analyze?(params): Promise; summarize?(text): Promise; structuredGenerate?(prompt, schema): Promise>; } ``` Nenhum código fora de `packages/ai` conhece o formato de request/response de uma API de IA específica. ### `OpenAIProvider`/`AnthropicProvider` (secao 97-98) Usam `fetch` nativo direto contra a "OpenAI API"/"Anthropic API" (secao 98: nomenclatura da API, nunca "ChatGPT" — o domínio não fica amarrado ao nome do produto de consumidor) — sem SDK oficial, pra manter o request/ response inteiramente visível no nosso próprio código (relevante já que potencialmente manda dados de cliente pra fora, secao 122-123). - `transcribe` (só OpenAI — Anthropic não tem endpoint de áudio, secao 102: "nem todo provider terá todas as capacidades"): `POST /audio/ transcriptions`, `response_format=verbose_json` pra pegar segmentos com timestamp. - `analyze`/`structuredGenerate`: OpenAI via `response_format: json_schema` (Structured Outputs); Anthropic via "tool use" forçado (`tool_choice` apontando pra uma tool única cujo `input_schema` é o JSON Schema pedido) — a técnica documentada da própria Anthropic pra output estruturado confiável antes de terem suporte nativo equivalente ao da OpenAI. **Nunca exercitados nesta sessão** — a autorização de rede desta sessão cobre só o servidor git do repositório (restrição definida desde o primeiro pedido do usuário), então nenhuma chamada real saiu daqui. Implementados seguindo o contrato documentado de cada API o mais fiel possível; revisar contra as APIs reais antes de confiar em produção — mesmo padrão de honestidade já usado pra `S3ObjectStorageProvider` (fase Recording) e o caminho PSTN real (fase Predictive Dialer). ### `SensitiveDataRedactor` (secao 123) Mascara CPF/CNPJ/telefone/cartão/email em texto livre antes de mandar pra um provider, quando a política de privacidade exigir. Testado com casos reais (CPF/CNPJ formatados, telefone com parênteses, email, número de cartão) — todos os 5 tipos corretamente mascarados, frase sem dado sensível passa intacta. **Achado real testando**: um `\b` logo antes de um `\(?` opcional (regex de telefone) falha em casar quando o caractere anterior também não é de palavra (ex.: espaço seguido de `(`) — `\b` exige uma transição palavra/não-palavra dos dois lados, e nem "espaço" nem "(" são caracteres de palavra. Isso deixava o `(` de fora do match, vazando um parêntese solto no texto redigido (nenhum dado sensível vazava de verdade, só um caractere de formatação). Corrigido trocando o `\b` inicial por `(? 403 Tenant A cria provider TENANT (BYOK, Anthropic) -> sucesso, encryptedApiKey nunca aparece na resposta (só apiKeyPreview) providerType não suportado ("google") -> 403 com mensagem clara Platform admin cria provider GLOBAL (OpenAI) -> sucesso, tenantId null Tenant A lista -> ve' o proprio BYOK + o GLOBAL (2 providers) Tenant B lista -> ve' SO' o GLOBAL (1 provider) — BYOK do Tenant A nunca vaza, confirmando a RLS hibrida Tenant B tenta apagar o BYOK do Tenant A (por id direto) -> 404 (RLS esconde a linha antes mesmo do controller decidir) Tenant B tenta apagar o provider GLOBAL -> 403 (nao e' platform admin) Tenant A cria um AIModel sob o provider GLOBAL -> tenantId herdado como null, aparece na lista de AMBOS os tenants (modelo de provider global e' global tambem) Apos a correcao do soft delete: apagar modelo -> 204, apagar o provider GLOBAL que tinha esse modelo -> 204 (antes falhava com FK), lista fica vazia pros dois tenants ``` typecheck do workspace inteiro limpo. ## O que falta - Pipeline assíncrono pós-CALL_ENDED (secao 106-108: `ai_jobs`, retry com backoff exponencial, dead-letter) — schema pronto, worker não implementado ainda. - Transcrição de verdade (secao 109-111: `call_transcriptions`/ `call_transcript_segments`, mapeamento de speaker via canal estéreo) — schema pronto, nada chama `provider.transcribe()` ainda. - Análise da chamada (secao 112-113), prompts por tenant/campanha (secao 114-116), scorecards/QA automático (secao 117-118) — schema pronto, nada implementado. - Privacidade em cascata Campaign > Queue > Tenant (secao 122) — `Tenant.aiPrivacyLevel`/`Queue.aiPrivacyLevel` existem no schema, função de resolução ainda não escrita (só faz sentido junto com o pipeline). - Usage metering (secao 124) — `AIUsageRecord` existe, nada grava nele ainda (só faz sentido junto com transcrição/análise reais). - `ParseUUIDPipe`/validação de UUID em `@Param("id")` — um id malformado na URL retorna 500 (erro genérico do Prisma) em vez de 400. Mesmo padrão em TODOS os controllers deste projeto, não é regressão desta fase — registrado aqui porque foi onde apareceu durante o teste, mas o fix (se vier a acontecer) é um retrofit em todo o `apps/api`, fora do escopo desta fase.