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
79 lines
4.0 KiB
Markdown
79 lines
4.0 KiB
Markdown
# 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).
|