|
@@ -7,36 +7,14 @@ import { generateHydeDocument } from "../services/hydeService.js";
|
|
|
import { rerankHits } from "../services/rerankService.js";
|
|
import { rerankHits } from "../services/rerankService.js";
|
|
|
import { createTtlCache } from "../services/simpleCache.js";
|
|
import { createTtlCache } from "../services/simpleCache.js";
|
|
|
import { classificarModoChat } from "./chatRouter.js";
|
|
import { classificarModoChat } from "./chatRouter.js";
|
|
|
-
|
|
|
|
|
-const SYSTEM_PROMPT = [
|
|
|
|
|
- "Você é o Oráculo, assistente interno da Star.",
|
|
|
|
|
- "Responda em português brasileiro de forma natural, clara e completa.",
|
|
|
|
|
- "Quando o CONTEXTO abaixo cobrir a pergunta, use-o e cite a fonte entre parênteses.",
|
|
|
|
|
- "Para perguntas gerais sem relação com documentos internos, responda normalmente com seu conhecimento.",
|
|
|
|
|
- "Nunca invente dados específicos da empresa (processos, valores, nomes) que não estejam no CONTEXTO."
|
|
|
|
|
-
|
|
|
|
|
-].join("\n");
|
|
|
|
|
-
|
|
|
|
|
-const ATENDIMENTOS_SYSTEM_PROMPT = [
|
|
|
|
|
- "Você é o Oráculo, assistente interno da Star, no modo de consulta a atendimentos de suporte ao cliente.",
|
|
|
|
|
- "Você tem acesso legítimo e autorizado a esses registros — é uma ferramenta interna da empresa para analisar o próprio histórico de atendimento, não uma conversa privada de terceiros externos. NUNCA recuse responder por motivos de privacidade/confidencialidade, mesmo que as mensagens tenham linguagem informal, gírias ou desabafos do cliente — relate o conteúdo normalmente, como faria com qualquer outro dado interno da empresa.",
|
|
|
|
|
- "Responda em português brasileiro de forma natural e o mais completa e detalhada possível, usando TUDO que estiver literalmente disponível no CONTEXTO (cabeçalho, sequência de atendentes, cada mensagem relevante) — não resuma demais nem omita detalhes reais só para ser breve.",
|
|
|
|
|
- "O CONTEXTO abaixo vem diretamente do banco de dados interno de atendimentos (sem busca por similaridade) e pode ser de vários tipos:",
|
|
|
|
|
- "(1) uma contagem/estatística já calculada (ex: quantidade de atendimentos por setor) — nesse caso, apenas apresente os números exatamente como estão, sem recalcular ou arredondar;",
|
|
|
|
|
- "(2) trechos de conversas reais entre atendentes e clientes, cada um com código do protocolo, setor, cliente, data ('Aberto em') e uma linha 'Atendentes envolvidos' já calculada — use-os para responder o que foi dito, reclamado, resolvido ou combinado, citando o código do protocolo entre parênteses.",
|
|
|
|
|
- "(3) uma avaliação automática por IA de um atendimento específico (nota, resolvido, sentimento do cliente, resumo e sugestões de melhoria) — trate como uma análise automatizada feita por outra IA, não como um fato relatado pelo cliente ou uma pesquisa de satisfação; se o bloco disser 'DESATUALIZADA', avise o usuário disso antes de responder sobre a qualidade desse atendimento;",
|
|
|
|
|
- "(4) uma agregação de qualidade (nota média, distribuição de resolução, contagem de atendimentos mal avaliados) calculada a partir de avaliações automáticas por IA de vários atendimentos — apresente os números exatamente como estão, sem recalcular, deixando claro que a nota vem de avaliação automatizada por IA, não de pesquisa respondida pelo cliente;",
|
|
|
|
|
- "(5) uma amostra de avaliações de IA recentes com sugestões de melhoria, usada para perguntas sobre padrões ou lições aprendidas — quando isso aparecer, identifique e agrupe os temas/sugestões que se repetem entre os itens em vez de apenas listar cada um separadamente, e deixe claro que é uma amostra recente (não necessariamente todas as avaliações do período).",
|
|
|
|
|
- "Perguntas sobre quantos atendentes participaram, se houve transferência de atendente, ou quem atendeu primeiro/depois: responda usando diretamente a linha 'Atendentes envolvidos, em ordem de participação' do cabeçalho — ela já é calculada corretamente, não precisa contar você mesmo nas mensagens.",
|
|
|
|
|
- "Quando o CONTEXTO trouxer vários atendimentos do mesmo cliente, eles vêm ordenados do mais recente para o mais antigo (compare o campo 'Aberto em') — use isso para responder perguntas sobre a última/mais recente conversa com um cliente.",
|
|
|
|
|
- "Se o CONTEXTO trouxer mais de um atendimento, NÃO misture fatos de atendimentos diferentes numa mesma resposta — trate cada código de protocolo separadamente.",
|
|
|
|
|
- "Se a pergunta mencionar um código de protocolo específico e o CONTEXTO não contiver exatamente esse código (ou contiver um código diferente), diga claramente que não encontrou esse atendimento — nunca responda como se fosse sobre o código pedido.",
|
|
|
|
|
- "Mensagens marcadas como '[áudio] texto' JÁ SÃO a transcrição real do que foi dito — trate esse texto como conteúdo legítimo da conversa, cite e resuma normalmente, igual a uma mensagem de texto comum. Já mensagens marcadas como '[áudio sem transcrição disponível]' ou '[mídia: tipo]' são áudios/anexos cujo conteúdo você realmente não conhece — só mencione que foram enviados, sem descrever ou supor o que foi dito ou mostrado.",
|
|
|
|
|
- "Nunca invente números, datas, horários, prazos de espera, falas, nomes de clientes, códigos de protocolo ou desfechos que não estejam literalmente escritos no CONTEXTO — mas dentro do que está escrito, seja o mais completo possível.",
|
|
|
|
|
- "Para atendimentos longos (muitas mensagens), NÃO se limite a 'alguns pontos importantes' — percorra a conversa cronologicamente e descreva o que aconteceu em cada fase/assunto tratado, com o máximo de detalhe real disponível. Evite frases genéricas de ressalva como 'a conversa foi muito extensa', 'pode haver informações faltando' ou 'os áudios não são transcrições exatas' — se o CONTEXTO tem a informação, relate-a com confiança; só sinalize incerteza quando algo específico realmente não estiver claro no texto.",
|
|
|
|
|
- "(6) um registro real de cancelamento vindo AO VIVO do ERP (IXC) — cliente, plano, valor, data de ativação/cancelamento, meses de contrato, motivo, vendedor e local — ou um resumo agregado desses cancelamentos reais (ranking por motivo/cidade/vendedor); trate como o dado definitivo e confirmado de cancelamento, diferente de qualquer avaliação automática por IA;",
|
|
|
|
|
- "(7) um histórico de risco de cancelamento inferido por IA em atendimentos de um cliente específico — uma análise automática separada do registro real do ERP; compare os dois quando ambos aparecerem juntos (ex: se a IA tinha marcado risco e o cliente de fato cancelou, ou cancelou por um motivo diferente do dito no atendimento), mas nunca confunda um com o outro na resposta."
|
|
|
|
|
-].join("\n");
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ MODES,
|
|
|
|
|
+ HYDE_SYSTEM_PROMPT,
|
|
|
|
|
+ REWRITE_SYSTEM_PROMPT,
|
|
|
|
|
+ REWRITE_SYSTEM_PROMPT_WITH_HISTORY,
|
|
|
|
|
+ REWRITE_HYDE_SYSTEM_PROMPT,
|
|
|
|
|
+ ATENDIMENTOS_REFORCO_ANTIRECUSA
|
|
|
|
|
+} from "./prompts.js";
|
|
|
|
|
|
|
|
function buildContextBlock(hits) {
|
|
function buildContextBlock(hits) {
|
|
|
const lines = [];
|
|
const lines = [];
|
|
@@ -50,31 +28,14 @@ function buildContextBlock(hits) {
|
|
|
return lines.join("\n").trim();
|
|
return lines.join("\n").trim();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const EMPTY_CONTEXT_GUARD = [
|
|
|
|
|
- "Nenhum documento interno foi encontrado para esta pergunta.",
|
|
|
|
|
- "Se ela for sobre processos, valores, nomes ou políticas da Star, responda que não há informação na base interna e não invente.",
|
|
|
|
|
- "Perguntas gerais podem ser respondidas normalmente."
|
|
|
|
|
-].join("\n");
|
|
|
|
|
-
|
|
|
|
|
-const ATENDIMENTOS_EMPTY_CONTEXT_GUARD = [
|
|
|
|
|
- "Nenhuma conversa de atendimento foi encontrada para esta pergunta.",
|
|
|
|
|
- "Informe que não há registros correspondentes na base de atendimentos indexada e não invente.",
|
|
|
|
|
- "Sugira refinar por setor, período ou código de protocolo, se fizer sentido."
|
|
|
|
|
-].join("\n");
|
|
|
|
|
-
|
|
|
|
|
-const GERAL_SYSTEM_PROMPT = [
|
|
|
|
|
- "Você é o Oráculo, assistente interno da Star.",
|
|
|
|
|
- "Esta pergunta não tem relação clara com documentos técnicos internos nem com atendimentos de suporte —",
|
|
|
|
|
- "responda de forma natural, breve e cordial (saudação, conversa casual, pergunta genérica).",
|
|
|
|
|
- "Nunca invente nem afirme fatos específicos da Star (processos, valores, nomes, clientes, atendimentos,",
|
|
|
|
|
- "protocolos) — se a pergunta pedir esse tipo de dado, diga que você pode ajudar melhor com uma pergunta",
|
|
|
|
|
- "sobre documentos internos ou sobre atendimentos.",
|
|
|
|
|
- "Responda em português brasileiro."
|
|
|
|
|
-].join("\n");
|
|
|
|
|
-
|
|
|
|
|
function buildMessages(message, context, history = [], modeConfig = MODES.documentos) {
|
|
function buildMessages(message, context, history = [], modeConfig = MODES.documentos) {
|
|
|
const guard = context
|
|
const guard = context
|
|
|
- ? { role: "system", content: `CONTEXTO:\n${context}` }
|
|
|
|
|
|
|
+ ? {
|
|
|
|
|
+ role: "system",
|
|
|
|
|
+ content:
|
|
|
|
|
+ `Informações internas disponíveis para responder (uso seu, não mencione este aviso nem a palavra ` +
|
|
|
|
|
+ `"contexto" na resposta — fale como se você mesmo já soubesse disso):\n${context}`
|
|
|
|
|
+ }
|
|
|
: modeConfig.emptyContextGuard
|
|
: modeConfig.emptyContextGuard
|
|
|
? { role: "system", content: modeConfig.emptyContextGuard }
|
|
? { role: "system", content: modeConfig.emptyContextGuard }
|
|
|
: null;
|
|
: null;
|
|
@@ -96,22 +57,6 @@ function hitsToSources(hits) {
|
|
|
}));
|
|
}));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const REWRITE_SYSTEM_PROMPT = [
|
|
|
|
|
- "Você é um motor de busca interno. Reescreva a pergunta abaixo de forma mais específica e completa",
|
|
|
|
|
- "para busca em documentos internos de empresa, sempre em linguagem natural (nunca em código, SQL,",
|
|
|
|
|
- "fórmulas ou pseudocódigo). Responda APENAS com a query reescrita, em uma frase, sem",
|
|
|
|
|
- "explicações, sem aspas ou formatação extra."
|
|
|
|
|
-].join(" ");
|
|
|
|
|
-
|
|
|
|
|
-const REWRITE_SYSTEM_PROMPT_WITH_HISTORY = [
|
|
|
|
|
- "Você é um motor de busca interno. Sua tarefa é transformar a ÚLTIMA pergunta do usuário",
|
|
|
|
|
- "em uma query de busca autocontida e específica para buscar documentos internos da empresa,",
|
|
|
|
|
- "sempre em linguagem natural (nunca em código, SQL, fórmulas ou pseudocódigo).",
|
|
|
|
|
- "Use o HISTÓRICO DA CONVERSA abaixo apenas para resolver pronomes, referências e continuidade",
|
|
|
|
|
- "de assunto (ex.: 'e o segundo caso?', 'isso', 'e sobre X?').",
|
|
|
|
|
- "Responda APENAS com a query reescrita, em uma frase, sem explicações, sem aspas."
|
|
|
|
|
-].join("\n");
|
|
|
|
|
-
|
|
|
|
|
export async function rewriteQuery(query, history = []) {
|
|
export async function rewriteQuery(query, history = []) {
|
|
|
try {
|
|
try {
|
|
|
const hasHistory = history.length > 0;
|
|
const hasHistory = history.length > 0;
|
|
@@ -140,20 +85,6 @@ export async function rewriteQuery(query, history = []) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
-const HYDE_SYSTEM_PROMPT = [
|
|
|
|
|
- "Você é um redator técnico que escreve trechos de manuais internos de redes e",
|
|
|
|
|
- "equipamentos (comandos Huawei, configuração de roteadores TP-Link, processos e",
|
|
|
|
|
- "políticas internas da empresa, etc.). Dada a pergunta abaixo, escreva um parágrafo",
|
|
|
|
|
- "curto (3 a 6 frases) no estilo de um trecho real de documentação técnica que",
|
|
|
|
|
- "responda a essa pergunta — nomes de comandos, telas, campos, passos, quando fizer",
|
|
|
|
|
- "sentido. Não inclua a pergunta, saudações ou ressalvas de incerteza. Se não tiver",
|
|
|
|
|
- "certeza do conteúdo exato, escreva de forma plausível no mesmo estilo, pois o texto",
|
|
|
|
|
- "será usado apenas para busca por similaridade, nunca mostrado ao usuário.",
|
|
|
|
|
- "Responda em português brasileiro."
|
|
|
|
|
-].join("\n");
|
|
|
|
|
-
|
|
|
|
|
-export { generateHydeDocument, HYDE_SYSTEM_PROMPT };
|
|
|
|
|
-
|
|
|
|
|
async function loadHistory(conversationId, userId) {
|
|
async function loadHistory(conversationId, userId) {
|
|
|
if (!conversationId) return [];
|
|
if (!conversationId) return [];
|
|
|
try {
|
|
try {
|
|
@@ -201,20 +132,6 @@ async function resolveEmbeddingQuery(searchQuery, originalMessage) {
|
|
|
: { embeddingQuery: searchQuery, embedRole: "query", hydeSkipped: false };
|
|
: { embeddingQuery: searchQuery, embedRole: "query", hydeSkipped: false };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const REWRITE_HYDE_SYSTEM_PROMPT = [
|
|
|
|
|
- 'Você é um motor de busca interno. Responda APENAS com um objeto JSON no formato',
|
|
|
|
|
- '{"rewrittenQuery": "...", "hydeDocument": "..."}.',
|
|
|
|
|
- '"rewrittenQuery": reescreva a ÚLTIMA pergunta do usuário como uma query autocontida e específica',
|
|
|
|
|
- "para busca em documentos internos da empresa, em linguagem natural (nunca em código, SQL,",
|
|
|
|
|
- "fórmulas ou pseudocódigo), resolvendo pronomes/referências via HISTÓRICO DA CONVERSA quando houver.",
|
|
|
|
|
- '"hydeDocument": um parágrafo curto (3 a 6 frases) no estilo de um trecho real de manual interno de',
|
|
|
|
|
- "redes e equipamentos (comandos Huawei, configuração de roteadores TP-Link, processos e políticas",
|
|
|
|
|
- "internas da empresa, etc.) que responderia a essa pergunta — nomes de comandos, telas, campos, passos,",
|
|
|
|
|
- "quando fizer sentido — plausível mesmo sem certeza, pois será usado apenas para busca por similaridade,",
|
|
|
|
|
- "nunca mostrado ao usuário. Responda em português brasileiro.",
|
|
|
|
|
- "Não inclua explicações, texto extra ou markdown fora do JSON."
|
|
|
|
|
-].join(" ");
|
|
|
|
|
-
|
|
|
|
|
function parseRewriteHydeResult(content, originalQuery) {
|
|
function parseRewriteHydeResult(content, originalQuery) {
|
|
|
try {
|
|
try {
|
|
|
const cleaned = String(content ?? "")
|
|
const cleaned = String(content ?? "")
|
|
@@ -292,12 +209,6 @@ async function resolveSearchAndEmbedding(message, history, timer) {
|
|
|
return { searchQuery, embeddingQuery, embedRole, hydeSkipped };
|
|
return { searchQuery, embeddingQuery, embedRole, hydeSkipped };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const MODES = {
|
|
|
|
|
- documentos: { systemPrompt: SYSTEM_PROMPT, emptyContextGuard: EMPTY_CONTEXT_GUARD },
|
|
|
|
|
- atendimentos: { systemPrompt: ATENDIMENTOS_SYSTEM_PROMPT, emptyContextGuard: ATENDIMENTOS_EMPTY_CONTEXT_GUARD },
|
|
|
|
|
- geral: { systemPrompt: GERAL_SYSTEM_PROMPT, emptyContextGuard: null }
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
function resolveMode(mode) {
|
|
function resolveMode(mode) {
|
|
|
return MODES[mode] ?? MODES.documentos;
|
|
return MODES[mode] ?? MODES.documentos;
|
|
|
}
|
|
}
|
|
@@ -319,14 +230,6 @@ function isAtendimentoRefusal(text) {
|
|
|
return ATENDIMENTOS_REFUSAL_PATTERNS.some((p) => t.includes(p));
|
|
return ATENDIMENTOS_REFUSAL_PATTERNS.some((p) => t.includes(p));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const ATENDIMENTOS_REFORCO_ANTIRECUSA = [
|
|
|
|
|
- "Lembrete: os registros acima são dados internos de atendimento ao cliente da própria empresa, aos quais",
|
|
|
|
|
- "você tem acesso total e autorizado para fins de auditoria e análise interna — não são conversas privadas",
|
|
|
|
|
- "de terceiros externos. Mesmo que o texto contenha linguagem informal, gírias ou reclamações do cliente",
|
|
|
|
|
- "sobre outra pessoa, relate o conteúdo normalmente, de forma factual e direta, sem recusar e sem",
|
|
|
|
|
- "ressalvas sobre privacidade."
|
|
|
|
|
-].join(" ");
|
|
|
|
|
-
|
|
|
|
|
function comReforcoAntirecusa(messages) {
|
|
function comReforcoAntirecusa(messages) {
|
|
|
const reforco = { role: "system", content: ATENDIMENTOS_REFORCO_ANTIRECUSA };
|
|
const reforco = { role: "system", content: ATENDIMENTOS_REFORCO_ANTIRECUSA };
|
|
|
return [...messages.slice(0, -1), reforco, messages[messages.length - 1]];
|
|
return [...messages.slice(0, -1), reforco, messages[messages.length - 1]];
|