chatChain.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { config } from "../config/index.js";
  2. import { chatCompletion, chatCompletionStream } from "../services/ollamaClient.js";
  3. import { getRecentMessages } from "../services/conversationsService.js";
  4. import { searchDocs } from "../services/searchService.js";
  5. const SYSTEM_PROMPT = [
  6. "Você é o Oráculo, assistente interno da Star.",
  7. "Responda em português brasileiro de forma natural, clara e completa.",
  8. "Quando o CONTEXTO abaixo cobrir a pergunta, use-o e cite a fonte entre parênteses.",
  9. "Para perguntas gerais sem relação com documentos internos, responda normalmente com seu conhecimento.",
  10. "Nunca invente dados específicos da empresa (processos, valores, nomes) que não estejam no CONTEXTO."
  11. ].join("\n");
  12. function buildContextBlock(hits) {
  13. const lines = [];
  14. hits.forEach((h, i) => {
  15. const score = h.score ? ` [relevância: ${(h.score * 100).toFixed(0)}%]` : "";
  16. const header = `## Fonte ${i + 1}${h.source ? ` — ${h.source}` : ""}${score}`;
  17. lines.push(header);
  18. lines.push(h.text);
  19. lines.push("");
  20. });
  21. return lines.join("\n").trim();
  22. }
  23. function buildMessages(message, context, history = []) {
  24. return [
  25. { role: "system", content: SYSTEM_PROMPT },
  26. ...(context ? [{ role: "system", content: `CONTEXTO:\n${context}` }] : []),
  27. ...history.map((m) => ({ role: m.Role.toLowerCase(), content: m.Content })),
  28. { role: "user", content: message }
  29. ];
  30. }
  31. function hitsToSources(hits) {
  32. return hits.map((h) => ({
  33. id: h.id,
  34. source: h.source,
  35. score: h.score,
  36. chunkIndex: h.chunkIndex,
  37. metadata: h.metadata
  38. }));
  39. }
  40. async function rewriteQuery(query) {
  41. try {
  42. const { content } = await chatCompletion({
  43. messages: [
  44. {
  45. role: "system",
  46. content:
  47. "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."
  48. },
  49. { role: "user", content: query }
  50. ]
  51. });
  52. return content?.trim() || query;
  53. } catch {
  54. return query;
  55. }
  56. }
  57. async function loadHistory(conversationId, userId) {
  58. if (!conversationId) return [];
  59. try {
  60. return await getRecentMessages(conversationId, userId, config.rag.historyLimit);
  61. } catch {
  62. return [];
  63. }
  64. }
  65. function buildDefaultOptions(override) {
  66. const defaults = {
  67. temperature: config.llm.temperature,
  68. top_p: config.llm.topP,
  69. num_predict: config.llm.numPredict,
  70. repeat_penalty: config.llm.repeatPenalty
  71. };
  72. return override && Object.keys(override).length > 0
  73. ? { ...defaults, ...override }
  74. : defaults;
  75. }
  76. export async function answerWithContext({ message, conversationId, userId, options }) {
  77. const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message;
  78. const [hits, history] = await Promise.all([
  79. searchDocs({ query: searchQuery, topK: config.rag.topK }),
  80. loadHistory(conversationId, userId)
  81. ]);
  82. const context = buildContextBlock(hits);
  83. const messages = buildMessages(message, context, history);
  84. const completion = await chatCompletion({ messages, options: buildDefaultOptions(options) });
  85. return {
  86. answer: completion.content,
  87. sources: hitsToSources(hits)
  88. };
  89. }
  90. export async function answerWithContextStream({ message, conversationId, userId, options, onChunk, signal }) {
  91. const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message;
  92. const [hits, history] = await Promise.all([
  93. searchDocs({ query: searchQuery, topK: config.rag.topK }),
  94. loadHistory(conversationId, userId)
  95. ]);
  96. const context = buildContextBlock(hits);
  97. const messages = buildMessages(message, context, history);
  98. const sources = hitsToSources(hits);
  99. const completion = await chatCompletionStream({ messages, onChunk, signal, options: buildDefaultOptions(options) });
  100. return {
  101. answer: completion.content,
  102. sources
  103. };
  104. }