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:
@@ -27,3 +27,9 @@ export {
|
||||
} from "./privacy";
|
||||
export { computeBackoffDelayMs, computeNextScheduledAt, isDeadLetter } from "./retry";
|
||||
export { splitStereoWav, type StereoSplitResult } from "./wav-stereo-split";
|
||||
export {
|
||||
QUALITY_EVALUATION_JSON_SCHEMA,
|
||||
validateQualityEvaluationResult,
|
||||
QualityEvaluationValidationError,
|
||||
type QualityEvaluationResult,
|
||||
} from "./quality-evaluation-schema";
|
||||
|
||||
61
packages/ai/src/quality-evaluation-schema.ts
Normal file
61
packages/ai/src/quality-evaluation-schema.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Schema do resultado de uma avaliação de QA automático (agente.md secao
|
||||
* 118): score 0-100 por chamada, score por critério do scorecard,
|
||||
* justificativa final — nunca o chain-of-thought do modelo ("Não
|
||||
* armazenar chain-of-thought do modelo", secao 118 explicitamente).
|
||||
*
|
||||
* Diferente de `call-analysis-schema.ts`, os nomes dos critérios variam
|
||||
* por `QualityScorecard` (cada tenant define os seus itens, secao 117) —
|
||||
* o schema não pode fixar as chaves de `criterionScores` de antemão, só a
|
||||
* FORMA (mapa string->0-100). O prompt (montado em
|
||||
* apps/ai-worker/src/process-scorecard.ts a partir dos itens do
|
||||
* scorecard) é quem diz ao provider quais critérios usar como chave.
|
||||
*/
|
||||
export const QUALITY_EVALUATION_JSON_SCHEMA = {
|
||||
type: "object",
|
||||
properties: {
|
||||
score: { type: "integer", minimum: 0, maximum: 100 },
|
||||
criterionScores: {
|
||||
type: "object",
|
||||
additionalProperties: { type: "integer", minimum: 0, maximum: 100 },
|
||||
},
|
||||
summaryJustification: { type: "string" },
|
||||
},
|
||||
required: ["score", "criterionScores"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
export interface QualityEvaluationResult {
|
||||
score: number;
|
||||
criterionScores: Record<string, number>;
|
||||
summaryJustification?: string;
|
||||
}
|
||||
|
||||
export class QualityEvaluationValidationError extends Error {}
|
||||
|
||||
function isCriterionScoresMap(value: unknown): value is Record<string, number> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
||||
return Object.values(value).every((v) => typeof v === "number" && v >= 0 && v <= 100);
|
||||
}
|
||||
|
||||
export function validateQualityEvaluationResult(data: unknown): QualityEvaluationResult {
|
||||
if (typeof data !== "object" || data === null) {
|
||||
throw new QualityEvaluationValidationError("Resultado de avaliacao nao e' um objeto");
|
||||
}
|
||||
const d = data as Record<string, unknown>;
|
||||
|
||||
if (typeof d.score !== "number" || d.score < 0 || d.score > 100) {
|
||||
throw new QualityEvaluationValidationError("score ausente ou fora do intervalo 0-100");
|
||||
}
|
||||
if (!isCriterionScoresMap(d.criterionScores)) {
|
||||
throw new QualityEvaluationValidationError("criterionScores ausente ou invalido (esperado mapa string->0-100)");
|
||||
}
|
||||
|
||||
return {
|
||||
score: Math.round(d.score),
|
||||
criterionScores: Object.fromEntries(
|
||||
Object.entries(d.criterionScores).map(([k, v]) => [k, Math.round(v as number)]),
|
||||
),
|
||||
summaryJustification: typeof d.summaryJustification === "string" ? d.summaryJustification : undefined,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user