|
@@ -6,6 +6,7 @@ import { resolverContextoAtendimentos } from "../services/atendimentosQueryServi
|
|
|
import { generateHydeDocument } from "../services/hydeService.js";
|
|
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";
|
|
|
|
|
|
|
|
const SYSTEM_PROMPT = [
|
|
const SYSTEM_PROMPT = [
|
|
|
"Você é o Oráculo, assistente interno da Star.",
|
|
"Você é o Oráculo, assistente interno da Star.",
|
|
@@ -61,12 +62,25 @@ const ATENDIMENTOS_EMPTY_CONTEXT_GUARD = [
|
|
|
"Sugira refinar por setor, período ou código de protocolo, se fizer sentido."
|
|
"Sugira refinar por setor, período ou código de protocolo, se fizer sentido."
|
|
|
].join("\n");
|
|
].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
|
|
|
|
|
+ ? { role: "system", content: `CONTEXTO:\n${context}` }
|
|
|
|
|
+ : modeConfig.emptyContextGuard
|
|
|
|
|
+ ? { role: "system", content: modeConfig.emptyContextGuard }
|
|
|
|
|
+ : null;
|
|
|
return [
|
|
return [
|
|
|
{ role: "system", content: modeConfig.systemPrompt },
|
|
{ role: "system", content: modeConfig.systemPrompt },
|
|
|
- ...(context
|
|
|
|
|
- ? [{ role: "system", content: `CONTEXTO:\n${context}` }]
|
|
|
|
|
- : [{ role: "system", content: modeConfig.emptyContextGuard }]),
|
|
|
|
|
|
|
+ ...(guard ? [guard] : []),
|
|
|
...history.map((m) => ({ role: m.Role.toLowerCase(), content: m.Content })),
|
|
...history.map((m) => ({ role: m.Role.toLowerCase(), content: m.Content })),
|
|
|
{ role: "user", content: message }
|
|
{ role: "user", content: message }
|
|
|
];
|
|
];
|
|
@@ -280,7 +294,8 @@ async function resolveSearchAndEmbedding(message, history, timer) {
|
|
|
|
|
|
|
|
const MODES = {
|
|
const MODES = {
|
|
|
documentos: { systemPrompt: SYSTEM_PROMPT, emptyContextGuard: EMPTY_CONTEXT_GUARD },
|
|
documentos: { systemPrompt: SYSTEM_PROMPT, emptyContextGuard: EMPTY_CONTEXT_GUARD },
|
|
|
- atendimentos: { systemPrompt: ATENDIMENTOS_SYSTEM_PROMPT, emptyContextGuard: ATENDIMENTOS_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) {
|
|
@@ -343,15 +358,20 @@ function createTimer() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export async function answerWithContext({ message, conversationId, userId, options, mode, isAdmin = false }) {
|
|
export async function answerWithContext({ message, conversationId, userId, options, mode, isAdmin = false }) {
|
|
|
- const modeConfig = resolveMode(mode);
|
|
|
|
|
const timer = createTimer();
|
|
const timer = createTimer();
|
|
|
const history = await loadHistory(conversationId, userId);
|
|
const history = await loadHistory(conversationId, userId);
|
|
|
timer.mark("history");
|
|
timer.mark("history");
|
|
|
|
|
|
|
|
|
|
+ const resolvedMode = mode ?? (await classificarModoChat(message, history));
|
|
|
|
|
+ timer.mark("route", mode ? "explicit" : resolvedMode);
|
|
|
|
|
+ const modeConfig = resolveMode(resolvedMode);
|
|
|
|
|
+
|
|
|
let hits;
|
|
let hits;
|
|
|
- if (mode === "atendimentos") {
|
|
|
|
|
|
|
+ if (resolvedMode === "atendimentos") {
|
|
|
hits = await resolverContextoAtendimentos(message, history, { isAdmin });
|
|
hits = await resolverContextoAtendimentos(message, history, { isAdmin });
|
|
|
timer.mark("atendimentos_query");
|
|
timer.mark("atendimentos_query");
|
|
|
|
|
+ } else if (resolvedMode === "geral") {
|
|
|
|
|
+ hits = [];
|
|
|
} else {
|
|
} else {
|
|
|
const { searchQuery, embeddingQuery, embedRole } = await resolveSearchAndEmbedding(message, history, timer);
|
|
const { searchQuery, embeddingQuery, embedRole } = await resolveSearchAndEmbedding(message, history, timer);
|
|
|
hits = await searchDocs({ query: embeddingQuery, topK: config.rag.topK, embedRole });
|
|
hits = await searchDocs({ query: embeddingQuery, topK: config.rag.topK, embedRole });
|
|
@@ -371,12 +391,12 @@ export async function answerWithContext({ message, conversationId, userId, optio
|
|
|
const context = buildContextBlock(hits);
|
|
const context = buildContextBlock(hits);
|
|
|
const messages = buildMessages(message, context, history, modeConfig);
|
|
const messages = buildMessages(message, context, history, modeConfig);
|
|
|
const modeDefaults =
|
|
const modeDefaults =
|
|
|
- mode === "atendimentos"
|
|
|
|
|
|
|
+ resolvedMode === "atendimentos"
|
|
|
? { temperature: ATENDIMENTOS_TEMPERATURE, num_ctx: config.llm.atendimentosNumCtx }
|
|
? { temperature: ATENDIMENTOS_TEMPERATURE, num_ctx: config.llm.atendimentosNumCtx }
|
|
|
: {};
|
|
: {};
|
|
|
const finalOptions = buildDefaultOptions(options, modeDefaults);
|
|
const finalOptions = buildDefaultOptions(options, modeDefaults);
|
|
|
const completion =
|
|
const completion =
|
|
|
- mode === "atendimentos"
|
|
|
|
|
|
|
+ resolvedMode === "atendimentos"
|
|
|
? await chatCompletionAtendimentos(messages, finalOptions)
|
|
? await chatCompletionAtendimentos(messages, finalOptions)
|
|
|
: await chatCompletion({ messages, options: finalOptions });
|
|
: await chatCompletion({ messages, options: finalOptions });
|
|
|
timer.mark("llm");
|
|
timer.mark("llm");
|
|
@@ -400,17 +420,22 @@ export async function answerWithContextStream({
|
|
|
onStatus,
|
|
onStatus,
|
|
|
signal
|
|
signal
|
|
|
}) {
|
|
}) {
|
|
|
- const modeConfig = resolveMode(mode);
|
|
|
|
|
const timer = createTimer();
|
|
const timer = createTimer();
|
|
|
onStatus?.("buscando");
|
|
onStatus?.("buscando");
|
|
|
const history = await loadHistory(conversationId, userId);
|
|
const history = await loadHistory(conversationId, userId);
|
|
|
timer.mark("history");
|
|
timer.mark("history");
|
|
|
|
|
|
|
|
|
|
+ const resolvedMode = mode ?? (await classificarModoChat(message, history));
|
|
|
|
|
+ timer.mark("route", mode ? "explicit" : resolvedMode);
|
|
|
|
|
+ const modeConfig = resolveMode(resolvedMode);
|
|
|
|
|
+
|
|
|
let hits;
|
|
let hits;
|
|
|
- if (mode === "atendimentos") {
|
|
|
|
|
|
|
+ if (resolvedMode === "atendimentos") {
|
|
|
hits = await resolverContextoAtendimentos(message, history, { isAdmin });
|
|
hits = await resolverContextoAtendimentos(message, history, { isAdmin });
|
|
|
timer.mark("atendimentos_query");
|
|
timer.mark("atendimentos_query");
|
|
|
onStatus?.("encontrou", hits.length);
|
|
onStatus?.("encontrou", hits.length);
|
|
|
|
|
+ } else if (resolvedMode === "geral") {
|
|
|
|
|
+ hits = [];
|
|
|
} else {
|
|
} else {
|
|
|
const { searchQuery, embeddingQuery, embedRole } = await resolveSearchAndEmbedding(message, history, timer);
|
|
const { searchQuery, embeddingQuery, embedRole } = await resolveSearchAndEmbedding(message, history, timer);
|
|
|
hits = await searchDocs({ query: embeddingQuery, topK: config.rag.topK, embedRole });
|
|
hits = await searchDocs({ query: embeddingQuery, topK: config.rag.topK, embedRole });
|
|
@@ -439,14 +464,14 @@ export async function answerWithContextStream({
|
|
|
const messages = buildMessages(message, context, history, modeConfig);
|
|
const messages = buildMessages(message, context, history, modeConfig);
|
|
|
onStatus?.("gerando");
|
|
onStatus?.("gerando");
|
|
|
const modeDefaults =
|
|
const modeDefaults =
|
|
|
- mode === "atendimentos"
|
|
|
|
|
|
|
+ resolvedMode === "atendimentos"
|
|
|
? { temperature: ATENDIMENTOS_TEMPERATURE, num_ctx: config.llm.atendimentosNumCtx }
|
|
? { temperature: ATENDIMENTOS_TEMPERATURE, num_ctx: config.llm.atendimentosNumCtx }
|
|
|
: {};
|
|
: {};
|
|
|
const finalOptions = buildDefaultOptions(options, modeDefaults);
|
|
const finalOptions = buildDefaultOptions(options, modeDefaults);
|
|
|
// modo atendimentos precisa checar recusa antes de mostrar a resposta ao usuário,
|
|
// modo atendimentos precisa checar recusa antes de mostrar a resposta ao usuário,
|
|
|
// então gera a resposta inteira primeiro (sem streaming real) e emite como um chunk só.
|
|
// então gera a resposta inteira primeiro (sem streaming real) e emite como um chunk só.
|
|
|
const completion =
|
|
const completion =
|
|
|
- mode === "atendimentos"
|
|
|
|
|
|
|
+ resolvedMode === "atendimentos"
|
|
|
? await chatCompletionAtendimentos(messages, finalOptions, signal).then((c) => {
|
|
? await chatCompletionAtendimentos(messages, finalOptions, signal).then((c) => {
|
|
|
onChunk?.(c.content);
|
|
onChunk?.(c.content);
|
|
|
return c;
|
|
return c;
|