rerankService.js 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import { config } from "../config/index.js";
  2. import { chatCompletion } from "./ollamaClient.js";
  3. // módulo-folha (sem importar de chatChain.js/atendimentosQueryService.js), pra poder ser
  4. // reusado tanto no rerank de documentos quanto no de atendimentos sem dependência circular
  5. // (mesmo motivo de hydeService.js).
  6. const RERANK_SYSTEM_PROMPT = [
  7. "Você reordena trechos de documentos internos por relevância à pergunta do usuário.",
  8. 'Responda APENAS com um objeto JSON no formato {"order": [...]}, contendo TODOS os índices',
  9. '(0-based) de cada trecho, em ordem decrescente de relevância. Exemplo: {"order": [2,0,3,1]}.',
  10. "Não inclua explicações, texto extra ou markdown."
  11. ].join("\n");
  12. const RERANK_SNIPPET_LENGTH = 300;
  13. function buildRerankPrompt(query, hits) {
  14. const listing = hits
  15. .map((h, i) => `[${i}] ${h.text.slice(0, RERANK_SNIPPET_LENGTH)}`)
  16. .join("\n\n");
  17. return `Pergunta: ${query}\n\nTrechos:\n${listing}`;
  18. }
  19. function parseRerankOrder(content, len) {
  20. try {
  21. const cleaned = String(content ?? "")
  22. .trim()
  23. .replace(/^```json\s*/i, "")
  24. .replace(/^```\s*/, "")
  25. .replace(/```$/, "")
  26. .trim();
  27. const parsed = JSON.parse(cleaned);
  28. const arr = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.order) ? parsed.order : null;
  29. if (!arr) return null;
  30. const seen = new Set();
  31. const valid = [];
  32. for (const v of arr) {
  33. const idx = Number(v);
  34. if (Number.isInteger(idx) && idx >= 0 && idx < len && !seen.has(idx)) {
  35. seen.add(idx);
  36. valid.push(idx);
  37. }
  38. }
  39. if (valid.length === 0) return null;
  40. for (let i = 0; i < len; i++) if (!seen.has(i)) valid.push(i);
  41. return valid;
  42. } catch {
  43. return null;
  44. }
  45. }
  46. export async function rerankHits(query, hits, { topN, numCtx = config.llm.numCtx } = {}) {
  47. if (hits.length <= 1) return hits;
  48. const limit = topN ?? hits.length;
  49. try {
  50. const { content } = await chatCompletion({
  51. messages: [
  52. { role: "system", content: RERANK_SYSTEM_PROMPT },
  53. { role: "user", content: buildRerankPrompt(query, hits) }
  54. ],
  55. format: "json",
  56. options: { temperature: 0.1, num_ctx: numCtx, num_predict: config.llm.auxNumPredict }
  57. });
  58. const order = parseRerankOrder(content, hits.length);
  59. if (!order) console.warn("[rerankService] rerankHits: resposta não parseável, mantendo ordem original");
  60. const reordered = order ? order.map((i) => hits[i]) : hits;
  61. return reordered.slice(0, limit);
  62. } catch (err) {
  63. console.warn("[rerankService] rerankHits falhou, mantendo ordem original:", err.message);
  64. return hits.slice(0, limit);
  65. }
  66. }