diff --git a/sino-ia/orquestrador/brain.mjs b/sino-ia/orquestrador/brain.mjs index a35c943..0b8f255 100644 --- a/sino-ia/orquestrador/brain.mjs +++ b/sino-ia/orquestrador/brain.mjs @@ -63,6 +63,9 @@ export function buildSystemPrompt(agent, ragContext = '', ctx = {}) { } if (ctx.memoryBlock) parts.push(ctx.memoryBlock); if (ragContext) parts.push('\n' + ragContext); + // Correção que o ORQUESTRADOR injeta para refazer um turno que saiu errado (ver execucao-gate.mjs). + // Vai por último de propósito: é a última coisa que o modelo lê antes do histórico. + if (ctx.instrucaoDoSistema) parts.push('\n' + ctx.instrucaoDoSistema); return parts.join('\n'); } @@ -309,7 +312,7 @@ export function sanitizeBeatriz(text) { } // ---- pipeline ---- -export async function handleMessage(agent, { history = [], userMessage, knowledgeIndex = [], tools = [], toolHandlers = {}, contactPhone, memoryBlock, senderIsOwner, senderName, senderSegment, canal, encaminharWhatsApp }, llm = { provider: 'mock' }) { +export async function handleMessage(agent, { history = [], userMessage, knowledgeIndex = [], tools = [], toolHandlers = {}, contactPhone, memoryBlock, senderIsOwner, senderName, senderSegment, canal, encaminharWhatsApp, instrucaoDoSistema }, llm = { provider: 'mock' }) { let ragContext = '', ragHits = []; // toggle "Busca inteligente do treinamento" controla o RAG (default: ligado se não definido) const usarRag = agent.settings?.buscaInteligente !== false; @@ -317,7 +320,7 @@ export async function handleMessage(agent, { history = [], userMessage, knowledg ragHits = await search(knowledgeIndex, userMessage, { provider: llm.provider === 'openai' ? 'openai' : 'mock', apiKey: llm.apiKey, topK: 3 }); ragContext = contextBlock(ragHits); } - const system = buildSystemPrompt(agent, ragContext, { contactPhone, memoryBlock, senderIsOwner, senderName, senderSegment, canal, encaminharWhatsApp }); + const system = buildSystemPrompt(agent, ragContext, { contactPhone, memoryBlock, senderIsOwner, senderName, senderSegment, canal, encaminharWhatsApp, instrucaoDoSistema }); let out; const literais = []; // textos que as ferramentas mandaram entregar exatos (ver separarLiteral) diff --git a/sino-ia/orquestrador/inbound.mjs b/sino-ia/orquestrador/inbound.mjs index fdce703..3a65096 100644 --- a/sino-ia/orquestrador/inbound.mjs +++ b/sino-ia/orquestrador/inbound.mjs @@ -15,6 +15,7 @@ import { ehConsole, handleConsole, contatoAtivo, atrasoMsFor, buildAvisarDonoToo import { aplicarTravaAdmin } from './admin-gate.mjs'; import { aplicarIdentidadeRemetente } from './identidade-gate.mjs'; import { aplicarTravaConferencia } from './conferencia-gate.mjs'; +import { afirmacaoSemGravacao, INSTRUCAO_REEXECUCAO, FALA_DE_ESCAPE } from './execucao-gate.mjs'; import { destinatariosDeAviso, ehGrupo, comoDestino } from './avisos.mjs'; import { matchTrigger, isExitCommand, routeActive, routeExpired, expiresAt } from './routing.mjs'; import { getMediaBase64, sendPresence } from './channels/evolution.mjs'; @@ -496,7 +497,30 @@ export async function handleInbound(msg, { send, apiKey }) { const memoryBlock = memoriaOn ? memoryPromptBlock({ contactName: contact.name, contactMemory: admin ? '' : contact.memory }) : ''; // gravar agendamento exige que o extrato de conferência já tenha ido ao paciente (ver conferencia-gate.mjs) const handlersConferidos = aplicarTravaConferencia({ toolHandlers, mensagens: allMsgs }); - const out = await handleMessage(agent, { history, userMessage: userText, knowledgeIndex, tools, toolHandlers: handlersConferidos, contactPhone: contact.phone, memoryBlock, senderIsOwner: !!admin, senderName: admin?.nome, canal: channel.type, encaminharWhatsApp: channel.encaminharWhatsApp }, llm); + // `pensar` existe para o turno poder ser REFEITO com uma correção do sistema (ver a trava de + // execução mais abaixo) sem repetir esta lista de argumentos em dois lugares, que é onde os dois + // caminhos começariam a divergir em silêncio. + const pensar = (instrucaoDoSistema) => handleMessage(agent, { history, userMessage: userText, knowledgeIndex, tools, toolHandlers: handlersConferidos, contactPhone: contact.phone, memoryBlock, senderIsOwner: !!admin, senderName: admin?.nome, canal: channel.type, encaminharWhatsApp: channel.encaminharWhatsApp, instrucaoDoSistema }, llm); + let out = await pensar(); + + // TRAVA DE EXECUÇÃO (ver execucao-gate.mjs): a agente afirmou que agendou/remarcou e nenhuma + // ferramenta gravou nada. Refaz o turno UMA vez, com a correção no prompt, porque na maioria das + // vezes o que faltou foi a segunda chamada do protocolo e o modelo a faz quando avisado. Se ele + // insistir, a afirmação falsa não chega ao cliente: quem fala é o sistema. + const somarUso = (a, b) => ({ prompt: (a?.prompt || 0) + (b?.prompt || 0), completion: (a?.completion || 0) + (b?.completion || 0) }); + const afirmouSemGravar = afirmacaoSemGravacao(out); + if (afirmouSemGravar) { + console.log(`[execucao-gate] afirmação sem gravação em ${conv.id}: "${afirmouSemGravar.trecho.slice(0, 120)}" — refazendo o turno`); + const refeito = await pensar(INSTRUCAO_REEXECUCAO).catch((e) => { console.warn('[execucao-gate] refazer o turno falhou:', e.message); return null; }); + if (refeito) out = { ...refeito, usage: somarUso(out.usage, refeito.usage) }; + const insistiu = afirmacaoSemGravacao(out); + if (insistiu) { + console.error(`[execucao-gate] ${conv.id}: o modelo insistiu sem gravar, resposta trocada pelo literal de escape`); + out = { ...out, parts: [FALA_DE_ESCAPE], reply: FALA_DE_ESCAPE, literais: [FALA_DE_ESCAPE], execucaoBarrada: insistiu }; + } else { + console.log(`[execucao-gate] ${conv.id}: resolvido na segunda tentativa`); + } + } // contador de consumo (tokens) por agente if (out.usage) incrementDoc(`agents/${agent.id}`, { @@ -584,6 +608,27 @@ export async function handleInbound(msg, { send, apiKey }) { }).catch((e) => console.warn('[barramento] não marquei a conversa:', e.message)); } + // A TRAVA DE EXECUÇÃO DISPAROU E O MODELO INSISTIU. Diferente do barramento acima, aqui não existe + // o "resolveu sozinho 75 segundos depois": o cliente já recebeu o literal de escape e está + // esperando um retorno que só um humano pode dar. Por isso o aviso sai na hora, sem passe do + // scheduler, e a conversa fica marcada. + if (out.execucaoBarrada) { + await patchDoc(`conversations/${conv.id}`, { needsHuman: true, execucaoBarradaEm: now() }).catch(() => {}); + if (avisados.length) { + const texto = [ + '🚨 A IA disse que agendou, e não agendou', + '', + `Cliente: ${conv.contactName || contact.name || '-'}`, + `Contato: ${contact.phone || '-'}`, + '', + `O que ela ia mandar: "${out.execucaoBarrada.trecho}"`, + '', + 'O sistema barrou essa mensagem e respondeu que vai confirmar e retornar. NADA foi gravado: alguém precisa fechar na mão e falar com o cliente.', + ].join('\n'); + avisarDono({ channel, destinatarios: avisados, texto, workspaceId: conv.workspaceId }).catch((e) => console.warn('[execucao-gate] aviso:', e.message)); + } + } + // FATOS DO SISTEMA: o que as ferramentas confirmam que aconteceu de verdade, guardado na conversa. // A memória do contato é escrita a partir do TEXTO trocado, sem conferir contra o que o sistema // gravou — daí a memória da Marciana registrar "sexta-feira" para um exame de quinta (ver