chatChain.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import { config } from "../src/config/index.js";
  2. import { chatCompletion, chatCompletionStream } from "../src/services/ollamaClient.js";
  3. import { getRecentMessages } from "../src/services/conversationsService.js";
  4. import { searchDocs } from "../chat/searchChat.js";
  5. const SYSTEM_PROMPT = [
  6. "Você é o Oráculo, assistente interno da empresa Star.",
  7. "Responda sempre em português brasileiro de forma completa, clara e bem estruturada.",
  8. "",
  9. "FORMATO DAS RESPOSTAS:",
  10. "- Comece com uma breve introdução de contexto (1-2 frases).",
  11. "- Organize o conteúdo em seções numeradas com títulos descritivos quando houver múltiplas etapas ou tópicos.",
  12. "- Use sub-bullets (traço) para detalhar cada item dentro das seções.",
  13. "- Adicione callouts inline quando relevante: '**Nota:**', '**Atenção:**', '**Dica:**'.",
  14. "- Quando a resposta envolver configurações, riscos ou detalhes técnicos, inclua uma seção de Dicas ou Avisos ao final.",
  15. "- Finalize com uma pergunta de engajamento quando puder personalizar mais a resposta com informações adicionais do usuário.",
  16. "",
  17. "USO DO CONHECIMENTO:",
  18. "- Priorize sempre o CONTEXTO fornecido (documentos internos da empresa).",
  19. "- Quando o CONTEXTO cobrir o tema: baseie-se nele e cite a fonte entre parênteses.",
  20. "- 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.",
  21. "- Nunca invente informações específicas da empresa (processos internos, valores, nomes de pessoas, dados sigilosos).",
  22. "- 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.",
  23. "",
  24. "QUALIDADE:",
  25. "- Prefira respostas detalhadas e completas a respostas curtas.",
  26. "- Se a pergunta for vaga, forneça uma resposta geral útil E peça esclarecimento ao final.",
  27. "- Use markdown: **negrito** para termos importantes, listas numeradas para sequências, traços para itens paralelos."
  28. ].join("\n");
  29. function buildContextBlock(hits) {
  30. const lines = [];
  31. hits.forEach((h, i) => {
  32. const score = h.score ? ` [relevância: ${(h.score * 100).toFixed(0)}%]` : "";
  33. const header = `## Fonte ${i + 1}${h.source ? ` — ${h.source}` : ""}${score}`;
  34. lines.push(header);
  35. lines.push(h.text);
  36. lines.push("");
  37. });
  38. return lines.join("\n").trim();
  39. }
  40. function buildMessages(message, context, history = []) {
  41. return [
  42. { role: "system", content: SYSTEM_PROMPT },
  43. ...(context ? [{ role: "system", content: `CONTEXTO:\n${context}` }] : []),
  44. ...history.map((m) => ({ role: m.Role.toLowerCase(), content: m.Content })),
  45. { role: "user", content: message }
  46. ];
  47. }
  48. function hitsToSources(hits) {
  49. return hits.map((h) => ({
  50. id: h.id,
  51. source: h.source,
  52. score: h.score,
  53. chunkIndex: h.chunkIndex,
  54. metadata: h.metadata
  55. }));
  56. }
  57. async function rewriteQuery(query) {
  58. try {
  59. const { content } = await chatCompletion({
  60. messages: [
  61. {
  62. role: "system",
  63. content:
  64. "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."
  65. },
  66. { role: "user", content: query }
  67. ]
  68. });
  69. return content?.trim() || query;
  70. } catch {
  71. return query;
  72. }
  73. }
  74. async function loadHistory(conversationId) {
  75. if (!conversationId) return [];
  76. try {
  77. return await getRecentMessages(conversationId, 12);
  78. } catch {
  79. return [];
  80. }
  81. }
  82. function buildDefaultOptions(override) {
  83. const defaults = {
  84. temperature: config.llm.temperature,
  85. top_p: config.llm.topP,
  86. num_predict: config.llm.numPredict,
  87. repeat_penalty: config.llm.repeatPenalty
  88. };
  89. return override && Object.keys(override).length > 0
  90. ? { ...defaults, ...override }
  91. : defaults;
  92. }
  93. export async function answerWithContext({ message, conversationId, options }) {
  94. const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message;
  95. const hits = await searchDocs({ query: searchQuery, topK: config.rag.topK });
  96. const context = buildContextBlock(hits);
  97. const history = await loadHistory(conversationId);
  98. const messages = buildMessages(message, context, history);
  99. const completion = await chatCompletion({ messages, options: buildDefaultOptions(options) });
  100. return {
  101. answer: completion.content,
  102. sources: hitsToSources(hits)
  103. };
  104. }
  105. export async function answerWithContextStream({ message, conversationId, options, onChunk, signal }) {
  106. const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message;
  107. const hits = await searchDocs({ query: searchQuery, topK: config.rag.topK });
  108. const context = buildContextBlock(hits);
  109. const history = await loadHistory(conversationId);
  110. const messages = buildMessages(message, context, history);
  111. const sources = hitsToSources(hits);
  112. const completion = await chatCompletionStream({ messages, onChunk, signal, options: buildDefaultOptions(options) });
  113. return {
  114. answer: completion.content,
  115. sources
  116. };
  117. }