Files
B2BCall-dialer/docs/QUALITY_SCORECARDS.md
Matheus d4e2513764 feat(ai): scorecards de QA, avaliacao automatica, dashboard (fase 21)
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
2026-08-28 16:49:35 -03:00

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, com enabled (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).