Production-readiness cycle. Classifier was excessively conservative (real Run A: ACTIONABLE_NOW/UNASSIGNED_ACTIONABLE both 0) and made concrete semantic mistakes on real tickets: - #34419: technician "Rafael Brito" (internal) classified as WAITING_VENDOR waiting_for -- an internal technician is not a vendor. - #37389: ticket had concrete open tasks ("Realizar um inventário das máquinas", "Alinhar o dia...") but was classified INSUFFICIENT_CONTEXT. - Generic next_action ("Revisar chamado") repeated across unrelated tickets, proving nothing about real actionability. Rewrote the system prompt (n8n/nodes/prepare-ai-prompt.node.js) with: practical actionability definition ("suporte consegue fazer algo util agora", explicit inclusion list); contatar-vs-aguardar distinction with examples; WAITING_* requires positive evidence (action already taken AND external dependency still open); technician-vs-vendor distinction citing the TECNICO context field; STATUS Planejado alone != blocked/scheduled; SCHEDULED requires an explicit date/window; mandatory chronological interpretation (most recent apontamento wins over a superseded old blocker); next_action must be concrete, with explicit accepted/rejected examples; INSUFFICIENT_CONTEXT bar raised (ask "is there a diagnostic step available even without full info" first). n8n/nodes/prepare-ai-prompt.node.test.js: 10 new tests asserting each rule's text is still present in the prompt (deterministic proxy -- behavioral proof comes from the real DEV run, inspected manually). logic/classification.js: added AI_CLASSIFIER_VERSION=3 (stamped on every valid classification); added a deterministic safety net (not just a prompt hope) -- isWaitingForInternalTechnician() rejects WAITING_VENDOR when waiting_for matches the ticket's own TECNICO field, routing to a new fallback_reason 'ai_semantic_conflict' instead of accepting a known-wrong semantic classification; added next_action genericness rejection (isGenericNextAction) for ACTIONABLE_NOW/UNASSIGNED_ACTIONABLE only. n8n/nodes/validate-ai-response.node.js: now passes the ticket into validateAiClassification() so the technician guard has the facts it needs. logic/cache.js: added buildCacheKey(contextHash, classifierVersion) -> "<hash>:v<version>", stored in the same last_context_hash column (no schema migration) -- bumping AI_CLASSIFIER_VERSION now forces a cache miss on every ticket without manually clearing the Data Table. n8n/nodes/decide-needs-ai.node.js / prepare-memoria-analise-record.node.js updated to compute/persist the composite key. logic/config.js: AI_MAX_CANDIDATES default raised 30->60 (now equals CANDIDATE_POOL_SIZE) -- no candidate should go unanalyzed by an artificial cap now that Run B already proved the cache makes repeat runs cheap; AI_FALLBACK stays reserved for real provider failures. Also added SEND_ENABLED (default false) and ENVIRONMENT config fields for the real Slack send gate (used by logic/slack.js, next commit). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
85 lines
7.3 KiB
JavaScript
85 lines
7.3 KiB
JavaScript
// @requires normalize.js context.js
|
|
const VALID_CLASSIFICATIONS = [
|
|
'ACTIONABLE_NOW', 'UNASSIGNED_ACTIONABLE', 'WAITING_REQUESTER', 'WAITING_VENDOR',
|
|
'WAITING_APPROVAL', 'SCHEDULED', 'DEPENDENCY_BLOCKED', 'NEEDS_COORDINATION',
|
|
'PROJECT_LONG_TERM', 'DUPLICATE_OR_OBSOLETE', 'INSUFFICIENT_CONTEXT',
|
|
];
|
|
|
|
const systemPrompt = [
|
|
'Voce e um analisador operacional de chamados de TI (GLPI). Voce NAO e um atendente e NAO conversa com o usuario.',
|
|
'Use SOMENTE as informacoes fornecidas abaixo sobre o chamado. Nunca invente fatos.',
|
|
'Distinga evidencia (o que esta escrito no chamado) de inferencia (sua conclusao).',
|
|
'SLA vencido sozinho NAO e evidencia de que existe uma acao executavel agora.',
|
|
'Idade do chamado sozinha NAO e evidencia de acionabilidade.',
|
|
'',
|
|
'=== O QUE SIGNIFICA ACIONAVEL (ACTIONABLE_NOW / UNASSIGNED_ACTIONABLE) ===',
|
|
'ACTIONABLE_NOW significa: existe pelo menos uma acao concreta que um integrante do suporte pode executar AGORA para fazer o chamado avancar. NAO precisa significar que o chamado sera resolvido integralmente hoje -- precisa significar que o suporte consegue fazer algo util agora.',
|
|
'Isso INCLUI, entre outras: executar diagnostico; testar equipamento; verificar configuracao; solicitar informacao ao cliente (quando ainda NAO foi solicitada); entrar em contato com fornecedor (quando ainda NAO foi contatado); marcar/agendar visita; combinar horario; atribuir tecnico; realizar inventario; validar cenario; executar instalacao; iniciar migracao; cobrar informacao que ainda NAO foi solicitada; atualizar configuracao; reproduzir problema; levantar requisitos; ou qualquer outro proximo passo concreto sob controle do suporte.',
|
|
'',
|
|
'=== CONTATAR != AGUARDAR (diferenca critica) ===',
|
|
'Precisar contatar alguem (cliente ou fornecedor) que AINDA NAO foi contatado e ACIONAVEL AGORA: ex. "Precisamos perguntar ao cliente qual horario pode realizar a manutencao" -> ACTIONABLE_NOW, next_action "Contatar cliente para combinar horario". "Precisamos pedir orcamento ao fornecedor" -> ACTIONABLE_NOW.',
|
|
'Ja ter contatado e estar esperando resposta e WAITING_*: ex. "Cliente ja foi contatado em 23/08 e estamos aguardando resposta" -> WAITING_REQUESTER. "Orcamento ja solicitado; aguardando retorno do fornecedor" -> WAITING_VENDOR.',
|
|
'So classifique WAITING_REQUESTER, WAITING_VENDOR ou WAITING_APPROVAL quando existir evidencia POSITIVA no historico de que (1) a acao necessaria ja foi executada pelo suporte E (2) existe uma dependencia externa ainda aberta. Sem essa evidencia dupla, NAO assuma espera -- procure se existe uma acao ainda nao realizada primeiro.',
|
|
'',
|
|
'=== TECNICO INTERNO NAO E FORNECEDOR ===',
|
|
'O campo TECNICO do chamado lista responsaveis/tecnicos internos da equipe de suporte. Se uma pendencia depende de alguem listado nesse campo, NUNCA classifique como WAITING_VENDOR (fornecedor externo) -- normalmente e ACTIONABLE_NOW (se o proprio suporte pode agir) ou UNASSIGNED_ACTIONABLE (se depende de atribuicao). WAITING_VENDOR e reservado para fornecedores/terceiros genuinamente externos a equipe.',
|
|
'',
|
|
'=== STATUS "PLANEJADO" NAO SIGNIFICA BLOQUEADO ===',
|
|
'STATUS Planejado sozinho NAO significa WAITING_REQUESTER, WAITING_VENDOR nem SCHEDULED. Se existe tarefa ou apontamento descrevendo um passo operacional disponivel agora, o chamado pode ser ACTIONABLE_NOW mesmo estando "Planejado".',
|
|
'Use SCHEDULED apenas quando existir data, horario, janela ou evento futuro explicito no chamado (ou evidencia inequivoca de que nenhuma acao pode/deve ocorrer antes dessa data).',
|
|
'',
|
|
'=== CRONOLOGIA E OBRIGATORIA ===',
|
|
'Interprete os apontamentos em ordem temporal -- o mais recente relevante prevalece sobre um bloqueio antigo ja superado. Exemplo: "20/08 aguardando fornecedor" seguido de "22/08 pecas chegaram" seguido de "23/08 falta instalar" -> ACTIONABLE_NOW (instalar pecas), NAO WAITING_VENDOR. Nao se prenda a um estado antigo se um apontamento mais novo o superou.',
|
|
'',
|
|
'=== NEXT_ACTION DEVE SER CONCRETO ===',
|
|
'Para ACTIONABLE_NOW/UNASSIGNED_ACTIONABLE, next_action precisa nomear a acao real: "Realizar inventario das maquinas", "Contatar cliente para combinar recolha", "Testar nobreak com carga", "Atribuir tecnico para visita", "Iniciar migracao dos e-mails", "Validar configuracao da impressora", "Solicitar dados de acesso ao cliente".',
|
|
'NUNCA aceite como next_action frases genericas que nao provam acionabilidade nenhuma: "Revisar chamado", "Dar andamento", "Analisar situacao", "Verificar chamado", "Acompanhar". Se voce so consegue pensar numa frase generica dessas, reconsidere a classificacao -- provavelmente falta evidencia concreta.',
|
|
'',
|
|
'=== QUANDO USAR INSUFFICIENT_CONTEXT ===',
|
|
'Antes de classificar como INSUFFICIENT_CONTEXT, pergunte-se: existe alguma acao concreta que o suporte consegue executar mesmo sem informacao completa? Falta de diagnostico nao significa falta de acao -- ex. "nao sabemos por que o notebook nao conecta" ainda pode ser ACTIONABLE_NOW com next_action "Realizar diagnostico de conectividade".',
|
|
'Use INSUFFICIENT_CONTEXT somente quando realmente nao for possivel determinar um proximo passo razoavelmente especifico -- nem mesmo um passo de diagnostico/investigacao.',
|
|
'',
|
|
'Identifique sempre: existe acao executavel hoje? quem pode executar? existe bloqueio? quem segura o bloqueio (interno ou externo)? qual a proxima acao concreta? existe data conhecida para revisao? existe evidencia nos apontamentos, em ordem cronologica, que justifique a conclusao?',
|
|
`Responda em JSON estrito com os campos: classification (uma de: ${VALID_CLASSIFICATIONS.join(', ')}), actionable_today (boolean), confidence (0 a 1), blocker_type, waiting_for, next_action (curto, poucas palavras, sem paragrafo, deve ser uma acao concreta quando ACTIONABLE_NOW/UNASSIGNED_ACTIONABLE), suggested_action_type, business_risk (low|medium|high), reason (uma frase), evidence (lista curta de evidencias textuais encontradas, em ordem cronologica quando relevante).`,
|
|
].join('\n');
|
|
|
|
return $input.all().map((item) => {
|
|
const data = item.json;
|
|
const userPrompt = buildAiContextText(data.ticket, data.config.aiMaxContextChars);
|
|
|
|
const requestBody = {
|
|
model: data.config.aiModel,
|
|
temperature: 0.1,
|
|
response_format: {
|
|
type: 'json_schema',
|
|
json_schema: {
|
|
name: 'ticket_classification',
|
|
strict: true,
|
|
schema: {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
classification: { type: 'string', enum: VALID_CLASSIFICATIONS },
|
|
actionable_today: { type: 'boolean' },
|
|
confidence: { type: 'number' },
|
|
blocker_type: { type: ['string', 'null'] },
|
|
waiting_for: { type: ['string', 'null'] },
|
|
next_action: { type: 'string' },
|
|
suggested_action_type: { type: 'string' },
|
|
business_risk: { type: 'string' },
|
|
reason: { type: 'string' },
|
|
evidence: { type: 'array', items: { type: 'string' } },
|
|
},
|
|
required: ['classification', 'actionable_today', 'confidence', 'blocker_type', 'waiting_for', 'next_action', 'suggested_action_type', 'business_risk', 'reason', 'evidence'],
|
|
},
|
|
},
|
|
},
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: userPrompt },
|
|
],
|
|
};
|
|
|
|
return { json: { ...data, requestBody } };
|
|
});
|