feat(ai): provider layer — abstracao, OpenAI/Anthropic, global+BYOK
Fecha agente.md secao 95-103. Primeira peca do modulo de IA — a
abstracao de provider, os adapters OpenAI/Anthropic, capabilities, e o
cadastro de providers/modelos (global + BYOK). O pipeline que aciona
isso depois de uma chamada terminar (transcricao, analise, jobs
assincronos, prompts, scorecards, usage metering — secao 104-124) fica
pra proxima fase.
## Schema completo do modulo de IA numa unica migration
Todas as tabelas das secoes 95-124 de uma vez (ai_providers/ai_models/
ai_prompt_templates+versions/ai_jobs/call_transcriptions+segments/
call_ai_analyses/quality_scorecards+items+evaluations/ai_usage_records) —
mais barato revisar o desenho relacional inteiro numa unica passada do
que fatiar em migrations pequenas que se emendam. O codigo que usa essas
tabelas vem em fases separadas, so' Provider/Model nesta.
## packages/ai
Interface AIProvider (secao 96: nunca hardcoda OpenAI no dominio).
OpenAIProvider/AnthropicProvider (secao 97-98, nomenclatura "OpenAI API"/
"Anthropic API", nunca "ChatGPT") via fetch nativo direto contra cada API
— sem SDK oficial, request/response inteiramente visivel no proprio
codigo (relevante ja' que manda dado de cliente pra fora, secao 122-123).
transcribe so' na OpenAI (Anthropic nao tem endpoint de audio, secao 102:
"nem todo provider tem todas as capacidades"); analyze/structuredGenerate
via Structured Outputs na OpenAI e "tool use" forcado na Anthropic.
SensitiveDataRedactor (secao 123): CPF/CNPJ/telefone/email/cartao.
**Nunca exercitados contra rede real** — esta sessao so' tem autorizacao
de rede pro servidor git (restricao definida desde o primeiro pedido do
usuario). Mesmo padrao de honestidade ja' usado pro S3ObjectStorageProvider
e o caminho PSTN real.
## ai_providers/ai_models — global vs. BYOK
scope=GLOBAL (platform admin, tenant_id null) visivel de qualquer tenant;
scope=TENANT (BYOK) so' do dono. RLS hibrida (tenant_id = current OR
tenant_id IS NULL, mesma tecnica de tenant_memberships no login); quem
pode ESCREVER num GLOBAL e' decidido na camada de servico
(isPlatformUser), nao pela RLS. Key nunca reexposta (so' apiKeyPreview).
## Dois bugs reais achados testando esta fase
- Delete de provider fazia hard delete, bloqueado por FK quando um
AIModel (mesmo soft-deleted) ainda referenciava — inconsistente com o
resto do sistema (tudo soft delete). Corrigido; GET /ai/providers
tambem nao filtrava desabilitados, corrigido junto.
- SensitiveDataRedactor: \b antes de \(? opcional falha quando o char
anterior tambem nao e' de palavra (espaco + "("), vazando um parenteses
solto (nenhum dado sensivel de verdade vazava). Corrigido com (?<!\w).
Verificado ponta a ponta com 2 tenants + platform admin: GLOBAL so'
platform admin cria/apaga, BYOK isolado por RLS (tenant B nunca ve' BYOK
do tenant A, 404 em id direto), modelo de provider GLOBAL visivel dos
dois tenants, redactor com 5 tipos de dado sensivel todos corretos.
typecheck do workspace inteiro limpo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
This commit is contained in:
40
TODO.md
40
TODO.md
@@ -464,10 +464,44 @@
|
||||
- [ ] Chamadas manuais/internas não são gravadas — só o caminho do
|
||||
discador tem originate próprio
|
||||
- [ ] `max_recording_storage_gb` (Plan) existe mas não é aplicado
|
||||
- [ ] Transcrição (secao 95+, fase IA)
|
||||
- [ ] Transcrição (secao 95+, fase IA) — implementado parcialmente na
|
||||
PHASE 19 (só o Provider Layer; pipeline/transcrição real na PHASE 20)
|
||||
|
||||
## PHASE 19+ — ver `agente.md` seções 95 em diante (AI, Billing, Frontend,
|
||||
Security, Tests)
|
||||
## PHASE 19 — IA: Provider Layer (agente.md secao 95-103)
|
||||
- [x] Schema completo do módulo de IA numa única migration (todas as
|
||||
tabelas das secoes 95-124: providers/models/prompts/jobs/
|
||||
transcriptions/analyses/scorecards/usage) — código só pro Provider
|
||||
Layer nesta fase, resto vem em fases separadas
|
||||
- [x] `packages/ai`: interface `AIProvider` (secao 96, nunca hardcoda
|
||||
OpenAI no domínio), `OpenAIProvider`/`AnthropicProvider` (secao
|
||||
97-98, nomenclatura "OpenAI API"/"Anthropic API" nunca "ChatGPT"),
|
||||
`SensitiveDataRedactor` (secao 123, CPF/CNPJ/telefone/email/cartão)
|
||||
- [x] **Nunca exercitados contra rede real**: adapters OpenAI/Anthropic —
|
||||
esta sessão só tem autorização de rede pro servidor git (restrição
|
||||
desde o primeiro pedido do usuário)
|
||||
- [x] `ai_providers`/`ai_models`: global (platform admin, visível de
|
||||
qualquer tenant) vs. BYOK (secao 100-101) — RLS híbrida (`tenant_id
|
||||
= current OR tenant_id IS NULL`, mesma técnica de
|
||||
`tenant_memberships` no login), escrita em GLOBAL exige
|
||||
`isPlatformUser` na camada de serviço, key nunca reexposta
|
||||
(`apiKeyPreview`)
|
||||
- [x] **Bug real, achado no teste desta fase**: delete de provider fazia
|
||||
hard delete, bloqueado por FK quando um `AIModel` (mesmo
|
||||
soft-deleted) ainda referenciava — inconsistente com o resto do
|
||||
sistema (tudo soft delete). Corrigido; `GET /ai/providers` também
|
||||
não filtrava desabilitados, corrigido junto.
|
||||
- [x] **Bug real, achado no teste do redactor**: `\b` antes de `\(?`
|
||||
opcional falha quando o char anterior também não é de palavra
|
||||
(espaço + `(`) — vazava um parêntese solto. Corrigido com `(?<!\w)`.
|
||||
- [x] Testado ponta a ponta com 2 tenants + platform admin: GLOBAL só
|
||||
platform admin cria/apaga, BYOK isolado por RLS (tenant B nunca vê
|
||||
BYOK do tenant A, 404 em id direto), modelo de provider GLOBAL
|
||||
visível dos dois tenants, redactor com 5 tipos de dado sensível
|
||||
- [ ] Pipeline assíncrono pós-CALL_ENDED, transcrição/análise/prompts/
|
||||
scorecards/usage metering reais — PHASE 20
|
||||
|
||||
## PHASE 20+ — ver `agente.md` seções 104 em diante (Transcrição, Análise,
|
||||
Scorecards, Usage Metering, Billing, Frontend, Security, Tests)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user