| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- 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
- };
- }
|