CRUD de QualityScorecard/Item (criterios por tenant, sem lista fixa hardcoded), novo AIJobType.SCORECARD_EVALUATION encadeado junto com ANALYSIS a partir da transcricao (mesma decisao de privacidade + exige scorecard habilitado). apps/ai-worker avalia contra todos os scorecards habilitados do tenant, prompt montado dinamicamente a partir dos itens de cada um, nunca guarda chain-of-thought do modelo (so' o resultado final validado). GET /reports/ai-dashboard agrega CallAIAnalysis+QualityEvaluation do periodo (score medio, sentimento, assuntos/objecoes, compliance alerts, ranking de agentes). Testado ponta a ponta contra o ai-worker real e Postgres real com RLS (scorecard real via API, prompt montado a partir dos itens reais, job real reservado via SKIP LOCKED, retry+dead-letter corretos) — chamada de rede real contra OpenAI/Anthropic continua nunca exercitada. Detalhes em docs/QUALITY_SCORECARDS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
4.0 KiB
Scorecards de QA / QA automático (agente.md secao 117-119)
Sub-fase C da fase de IA: avalia cada chamada transcrita contra os critérios de qualidade que o próprio tenant define, sem lista fixa hardcoded (secao 117 sugere Saudação/Identificação/Empatia/etc. como exemplo, nunca como enum).
Modelo de dados (já existia desde a migration ai_module)
QualityScorecard— um conjunto de critérios, por tenant, comenabled(soft delete, mesmo padrão do resto do sistema).QualityScorecardItem— um critério:name,weight,description?,evaluationPrompt?.QualityEvaluation— o resultado de avaliar UMA chamada contra UM scorecard:score(0-100),criterionScores(Json, mapa nome-do-critério → 0-100),summaryJustification. Nunca guarda o chain-of-thought do modelo (secao 118, explícito) — só o resultado final validado.
CRUD
apps/api/src/quality/quality-scorecards.controller.ts — mesma RLS por
tenant do resto do sistema (sem hierarquia GLOBAL/BYOK aqui, scorecard é
sempre do tenant). POST /quality/scorecards cria o scorecard e seus
itens numa tacada só (nested create); GET lista os habilitados;
DELETE /:id desabilita (soft delete).
Pipeline (apps/ai-worker/src/process-scorecard.ts)
Novo AIJobType.SCORECARD_EVALUATION (migration
20260828194146_ai_job_scorecard_evaluation, só ALTER TYPE ... ADD VALUE, sem mudança de RLS). Encadeado a partir de
process-transcription.ts junto com o job de ANALYSIS — mesma
decisão de privacidade (precisa de allowsAnalysis), só que também exige
pelo menos 1 QualityScorecard habilitado pro tenant (senão nem cria o
job).
Ao processar: busca TODOS os scorecards habilitados do tenant (não só o
primeiro) e gera uma QualityEvaluation por scorecard. O prompt é
montado dinamicamente a partir dos itens de cada scorecard
(buildScorecardPrompt) — o JSON Schema mandado pro provider
(QUALITY_EVALUATION_JSON_SCHEMA, packages/ai/src/ quality-evaluation-schema.ts) só define a FORMA da resposta (score +
mapa de criterionScores), não as chaves específicas, já que os critérios
variam por scorecard. O texto da transcrição passa pelo
SensitiveDataRedactor antes de sair, igual à análise normal.
Dashboard de IA (GET /reports/ai-dashboard, secao 119)
Agrega CallAIAnalysis + QualityEvaluation do período: chamadas
analisadas, score médio (avgQualityScore, de CallAIAnalysis — quão
bem o modelo achou que a ligação foi — E avgScorecardScore, de
QualityEvaluation — a nota formal contra os scorecards; a especificação
só pede "score médio" sem dizer qual dos dois conceitos, então mostra os
dois em vez de escolher um), sentimento, principais assuntos/objeções
(contagem de frequência sobre os arrays já persistidos), compliance
alerts, ranking de agentes por agentScore médio (top 5 / bottom 5).
O que foi testado de verdade
Mesma restrição de rede desta sessão (só o servidor git é autorizado).
Testado ao vivo contra o b2bcall-ai-worker real e Postgres real com RLS:
scorecard real criado com 2 critérios via QualityScorecardsController
(nested create), processScorecardJob chamado diretamente contra uma
transcrição semeada — montou o prompt a partir dos itens reais, resolveu
provider, redigiu o texto, tentou a chamada de rede real (loopback
fechado), falhou como esperado; job SCORECARD_EVALUATION real criado e
reservado pelo worker via FOR UPDATE SKIP LOCKED, retry com backoff,
dead-letter exatamente na tentativa configurada. GET /reports/ai-dashboard e POST /quality/scorecards confirmados
respondendo (401 sem token, 200 esperado com token — não testado com um
usuário autenticado real nesta rodada, mesmo padrão de outras fases onde
o roteamento/guard já foi provado em fases anteriores).
Nunca exercitado: chamada de rede real contra OpenAI/Anthropic (mesma
restrição de todo o módulo de IA); avgScorecardScore calculado a partir
de uma QualityEvaluation real (só via dead-letter, nunca completou sem
rede).