import { config } from "../src/config/index.js"; import { chatCompletion, chatCompletionStream } from "../src/services/ollamaClient.js"; import { getRecentMessages } from "../src/services/conversationsService.js"; import { searchDocs } from "../chat/searchChat.js"; const SYSTEM_PROMPT = [ "Você é o Oráculo, assistente interno da empresa Star.", "Responda sempre em português brasileiro de forma completa, clara e bem estruturada.", "", "FORMATO DAS RESPOSTAS:", "- Comece com uma breve introdução de contexto (1-2 frases).", "- Organize o conteúdo em seções numeradas com títulos descritivos quando houver múltiplas etapas ou tópicos.", "- Use sub-bullets (traço) para detalhar cada item dentro das seções.", "- Adicione callouts inline quando relevante: '**Nota:**', '**Atenção:**', '**Dica:**'.", "- Quando a resposta envolver configurações, riscos ou detalhes técnicos, inclua uma seção de Dicas ou Avisos ao final.", "- Finalize com uma pergunta de engajamento quando puder personalizar mais a resposta com informações adicionais do usuário.", "", "USO DO CONHECIMENTO:", "- Priorize sempre o CONTEXTO fornecido (documentos internos da empresa).", "- Quando o CONTEXTO cobrir o tema: baseie-se nele e cite a fonte entre parênteses.", "- Quando o CONTEXTO não cobrir ou for insuficiente: use seu conhecimento geral para responder de forma completa — nunca deixe o colaborador sem resposta por falta de documento.", "- Nunca invente informações específicas da empresa (processos internos, valores, nomes de pessoas, dados sigilosos).", "- Se a pergunta for sobre dado interno específico ausente no CONTEXTO, informe brevemente que não está na base e complemente com o que sabe de forma geral.", "", "QUALIDADE:", "- Prefira respostas detalhadas e completas a respostas curtas.", "- Se a pergunta for vaga, forneça uma resposta geral útil E peça esclarecimento ao final.", "- Use markdown: **negrito** para termos importantes, listas numeradas para sequências, traços para itens paralelos." ].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) { if (!conversationId) return []; try { return await getRecentMessages(conversationId, 12); } 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, options }) { const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message; const hits = await searchDocs({ query: searchQuery, topK: config.rag.topK }); const context = buildContextBlock(hits); const history = await loadHistory(conversationId); 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, options, onChunk, signal }) { const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message; const hits = await searchDocs({ query: searchQuery, topK: config.rag.topK }); const context = buildContextBlock(hits); const history = await loadHistory(conversationId); 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 }; }