import { config } from "../config/index.js"; import { chatCompletion, chatCompletionStream } from "../services/ollamaClient.js"; import { getRecentMessages } from "../services/conversationsService.js"; import { searchDocs } from "../services/searchService.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"); function buildContextBlock(hits) { const lines = []; hits.forEach((h, i) => { const score = h.score ? ` [relevância: ${(h.score * 100).toFixed(0)}%]` : ""; const header = `## Fonte ${i + 1}${h.source ? ` — ${h.source}` : ""}${score}`; lines.push(header); lines.push(h.text); lines.push(""); }); return lines.join("\n").trim(); } function buildMessages(message, context, history = []) { return [ { role: "system", content: SYSTEM_PROMPT }, ...(context ? [{ role: "system", content: `CONTEXTO:\n${context}` }] : []), ...history.map((m) => ({ role: m.Role.toLowerCase(), content: m.Content })), { role: "user", content: message } ]; } function hitsToSources(hits) { return hits.map((h) => ({ id: h.id, source: h.source, score: h.score, chunkIndex: h.chunkIndex, metadata: h.metadata })); } async function rewriteQuery(query) { try { const { content } = await chatCompletion({ messages: [ { role: "system", content: "Você é um motor de busca interno. Reescreva a pergunta abaixo de forma mais específica e completa para busca em documentos internos de empresa. Responda APENAS com a query reescrita, sem explicações ou formatação extra." }, { role: "user", content: query } ] }); return content?.trim() || query; } catch { return query; } } async function loadHistory(conversationId, userId) { if (!conversationId) return []; try { return await getRecentMessages(conversationId, userId, config.rag.historyLimit); } catch { return []; } } function buildDefaultOptions(override) { const defaults = { temperature: config.llm.temperature, top_p: config.llm.topP, num_predict: config.llm.numPredict, repeat_penalty: config.llm.repeatPenalty }; return override && Object.keys(override).length > 0 ? { ...defaults, ...override } : defaults; } export async function answerWithContext({ message, conversationId, userId, options }) { const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message; const [hits, history] = await Promise.all([ searchDocs({ query: searchQuery, topK: config.rag.topK }), loadHistory(conversationId, userId) ]); const context = buildContextBlock(hits); const messages = buildMessages(message, context, history); const completion = await chatCompletion({ messages, options: buildDefaultOptions(options) }); return { answer: completion.content, sources: hitsToSources(hits) }; } export async function answerWithContextStream({ message, conversationId, userId, options, onChunk, signal }) { const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message; const [hits, history] = await Promise.all([ searchDocs({ query: searchQuery, topK: config.rag.topK }), loadHistory(conversationId, userId) ]); const context = buildContextBlock(hits); const messages = buildMessages(message, context, history); const sources = hitsToSources(hits); const completion = await chatCompletionStream({ messages, onChunk, signal, options: buildDefaultOptions(options) }); return { answer: completion.content, sources }; }