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
This commit is contained in:
78
docs/QUALITY_SCORECARDS.md
Normal file
78
docs/QUALITY_SCORECARDS.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user