فهرست منبع

ajustes para trazer dados dos atendimentos.

leonardo 2 ماه پیش
والد
کامیت
955a1a0cc8

+ 97 - 21
src/chat/chatChain.js

@@ -9,7 +9,16 @@ const SYSTEM_PROMPT = [
   "Quando o CONTEXTO abaixo cobrir a pergunta, use-o e cite a fonte entre parênteses.",
   "Quando o CONTEXTO abaixo cobrir a pergunta, use-o e cite a fonte entre parênteses.",
   "Para perguntas gerais sem relação com documentos internos, responda normalmente com seu conhecimento.",
   "Para perguntas gerais sem relação com documentos internos, responda normalmente com seu conhecimento.",
   "Nunca invente dados específicos da empresa (processos, valores, nomes) que não estejam no CONTEXTO."
   "Nunca invente dados específicos da empresa (processos, valores, nomes) que não estejam no CONTEXTO."
-  
+
+].join("\n");
+
+const ATENDIMENTOS_SYSTEM_PROMPT = [
+  "Você é o Oráculo, assistente interno da Star, no modo de consulta a atendimentos de suporte ao cliente.",
+  "Responda em português brasileiro de forma natural, clara e objetiva.",
+  "O CONTEXTO abaixo contém trechos de conversas reais entre atendentes e clientes, extraídos do sistema de atendimento (ifbot); cada trecho traz o código do protocolo, setor, cliente e data.",
+  "Use o CONTEXTO para responder o que foi dito, reclamado, resolvido ou combinado em atendimentos específicos, citando o código do protocolo entre parênteses.",
+  "Nunca invente falas, nomes de clientes ou desfechos que não estejam no CONTEXTO.",
+  "Se a pergunta pedir estatística agregada (contagens, totais, rankings) que não dá para responder apenas lendo trechos de conversas, avise que esse tipo de análise não é suportado neste modo."
 ].join("\n");
 ].join("\n");
 
 
 function buildContextBlock(hits) {
 function buildContextBlock(hits) {
@@ -30,12 +39,18 @@ const EMPTY_CONTEXT_GUARD = [
   "Perguntas gerais podem ser respondidas normalmente."
   "Perguntas gerais podem ser respondidas normalmente."
 ].join("\n");
 ].join("\n");
 
 
-function buildMessages(message, context, history = []) {
+const ATENDIMENTOS_EMPTY_CONTEXT_GUARD = [
+  "Nenhuma conversa de atendimento foi encontrada para esta pergunta.",
+  "Informe que não há registros correspondentes na base de atendimentos indexada e não invente.",
+  "Sugira refinar por setor, período ou código de protocolo, se fizer sentido."
+].join("\n");
+
+function buildMessages(message, context, history = [], modeConfig = MODES.documentos) {
   return [
   return [
-    { role: "system", content: SYSTEM_PROMPT },
+    { role: "system", content: modeConfig.systemPrompt },
     ...(context
     ...(context
       ? [{ role: "system", content: `CONTEXTO:\n${context}` }]
       ? [{ role: "system", content: `CONTEXTO:\n${context}` }]
-      : [{ role: "system", content: EMPTY_CONTEXT_GUARD }]),
+      : [{ role: "system", content: modeConfig.emptyContextGuard }]),
     ...history.map((m) => ({ role: m.Role.toLowerCase(), content: m.Content })),
     ...history.map((m) => ({ role: m.Role.toLowerCase(), content: m.Content })),
     { role: "user", content: message }
     { role: "user", content: message }
   ];
   ];
@@ -169,11 +184,20 @@ const HYDE_SYSTEM_PROMPT = [
   "Responda em português brasileiro."
   "Responda em português brasileiro."
 ].join("\n");
 ].join("\n");
 
 
-export async function generateHydeDocument(query) {
+const ATENDIMENTOS_HYDE_SYSTEM_PROMPT = [
+  "Você simula um trecho plausível de conversa real entre um atendente de suporte e um cliente,",
+  "extraído de um sistema de atendimento via WhatsApp. Dada a pergunta abaixo, escreva um parágrafo",
+  "curto (3 a 6 frases) no estilo de uma troca de mensagens entre cliente e atendente, mencionando",
+  "o tipo de problema, sintomas ou solução relacionados à pergunta. Não inclua a pergunta,",
+  "saudações genéricas ou ressalvas de incerteza — o texto só será usado para busca por",
+  "similaridade, nunca mostrado ao usuário. Responda em português brasileiro."
+].join("\n");
+
+export async function generateHydeDocument(query, hydeSystemPrompt = HYDE_SYSTEM_PROMPT) {
   try {
   try {
     const { content } = await chatCompletion({
     const { content } = await chatCompletion({
       messages: [
       messages: [
-        { role: "system", content: HYDE_SYSTEM_PROMPT },
+        { role: "system", content: hydeSystemPrompt },
         { role: "user", content: query }
         { role: "user", content: query }
       ],
       ],
       options: { temperature: 0.2 }
       options: { temperature: 0.2 }
@@ -216,12 +240,12 @@ function shouldSkipHyde(originalMessage, searchQuery) {
   return false;
   return false;
 }
 }
 
 
-async function resolveEmbeddingQuery(searchQuery, originalMessage) {
+async function resolveEmbeddingQuery(searchQuery, originalMessage, hydeSystemPrompt = HYDE_SYSTEM_PROMPT) {
   if (!config.rag.hyde) return { embeddingQuery: searchQuery, embedRole: "query", hydeSkipped: false };
   if (!config.rag.hyde) return { embeddingQuery: searchQuery, embedRole: "query", hydeSkipped: false };
   if (shouldSkipHyde(originalMessage, searchQuery)) {
   if (shouldSkipHyde(originalMessage, searchQuery)) {
     return { embeddingQuery: searchQuery, embedRole: "query", hydeSkipped: true };
     return { embeddingQuery: searchQuery, embedRole: "query", hydeSkipped: true };
   }
   }
-  const hydeText = await generateHydeDocument(searchQuery);
+  const hydeText = await generateHydeDocument(searchQuery, hydeSystemPrompt);
   return hydeText
   return hydeText
     ? { embeddingQuery: hydeText, embedRole: "passage", hydeSkipped: false }
     ? { embeddingQuery: hydeText, embedRole: "passage", hydeSkipped: false }
     : { embeddingQuery: searchQuery, embedRole: "query", hydeSkipped: false };
     : { embeddingQuery: searchQuery, embedRole: "query", hydeSkipped: false };
@@ -239,6 +263,18 @@ const REWRITE_HYDE_SYSTEM_PROMPT = [
   "Não inclua explicações, texto extra ou markdown fora do JSON."
   "Não inclua explicações, texto extra ou markdown fora do JSON."
 ].join(" ");
 ].join(" ");
 
 
+const ATENDIMENTOS_REWRITE_HYDE_SYSTEM_PROMPT = [
+  'Você é um motor de busca interno. Responda APENAS com um objeto JSON no formato',
+  '{"rewrittenQuery": "...", "hydeDocument": "..."}.',
+  '"rewrittenQuery": reescreva a ÚLTIMA pergunta do usuário como uma query autocontida e específica',
+  "para busca em conversas de atendimento ao cliente, em linguagem natural, resolvendo",
+  "pronomes/referências via HISTÓRICO DA CONVERSA quando houver.",
+  '"hydeDocument": um parágrafo curto (3 a 6 frases) simulando um trecho plausível de conversa real',
+  "entre atendente e cliente de suporte (estilo WhatsApp) relacionado a essa pergunta, plausível mesmo",
+  "sem certeza, pois será usado apenas para busca por similaridade, nunca mostrado ao usuário.",
+  "Responda em português brasileiro. Não inclua explicações, texto extra ou markdown fora do JSON."
+].join(" ");
+
 function parseRewriteHydeResult(content, originalQuery) {
 function parseRewriteHydeResult(content, originalQuery) {
   try {
   try {
     const cleaned = String(content ?? "")
     const cleaned = String(content ?? "")
@@ -260,11 +296,11 @@ function parseRewriteHydeResult(content, originalQuery) {
   }
   }
 }
 }
 
 
-async function rewriteAndHyde(query, history) {
+async function rewriteAndHyde(query, history, rewriteHydeSystemPrompt = REWRITE_HYDE_SYSTEM_PROMPT) {
   try {
   try {
     const hasHistory = history.length > 0;
     const hasHistory = history.length > 0;
     const messages = [
     const messages = [
-      { role: "system", content: REWRITE_HYDE_SYSTEM_PROMPT },
+      { role: "system", content: rewriteHydeSystemPrompt },
       ...(hasHistory
       ...(hasHistory
         ? [{ role: "system", content: `HISTÓRICO DA CONVERSA:\n${history.map((m) => `${m.Role}: ${m.Content}`).join("\n")}` }]
         ? [{ role: "system", content: `HISTÓRICO DA CONVERSA:\n${history.map((m) => `${m.Role}: ${m.Content}`).join("\n")}` }]
         : []),
         : []),
@@ -280,11 +316,11 @@ async function rewriteAndHyde(query, history) {
   }
   }
 }
 }
 
 
-async function resolveSearchAndEmbedding(message, history, timer) {
+async function resolveSearchAndEmbedding(message, history, timer, modeConfig = MODES.documentos) {
   const bothEnabled = config.rag.queryRewrite && config.rag.hyde;
   const bothEnabled = config.rag.queryRewrite && config.rag.hyde;
 
 
   if (bothEnabled) {
   if (bothEnabled) {
-    const { rewrittenQuery, hydeDocument } = await rewriteAndHyde(message, history);
+    const { rewrittenQuery, hydeDocument } = await rewriteAndHyde(message, history, modeConfig.rewriteHydeSystemPrompt);
     timer?.mark("rewrite_hyde");
     timer?.mark("rewrite_hyde");
     const skip = shouldSkipHyde(message, rewrittenQuery) || !hydeDocument;
     const skip = shouldSkipHyde(message, rewrittenQuery) || !hydeDocument;
     if (skip) {
     if (skip) {
@@ -296,11 +332,39 @@ async function resolveSearchAndEmbedding(message, history, timer) {
 
 
   const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message, history) : message;
   const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message, history) : message;
   timer?.mark("rewrite");
   timer?.mark("rewrite");
-  const { embeddingQuery, embedRole, hydeSkipped } = await resolveEmbeddingQuery(searchQuery, message);
+  const { embeddingQuery, embedRole, hydeSkipped } = await resolveEmbeddingQuery(
+    searchQuery,
+    message,
+    modeConfig.hydeSystemPrompt
+  );
   timer?.mark("hyde", hydeSkipped ? "skipped" : undefined);
   timer?.mark("hyde", hydeSkipped ? "skipped" : undefined);
   return { searchQuery, embeddingQuery, embedRole, hydeSkipped };
   return { searchQuery, embeddingQuery, embedRole, hydeSkipped };
 }
 }
 
 
+const ATENDIMENTOS_FILTER = { must: [{ key: "metadata.type", match: { value: "atendimento" } }] };
+const DOCUMENTOS_FILTER = { must_not: [{ key: "metadata.type", match: { value: "atendimento" } }] };
+
+const MODES = {
+  documentos: {
+    systemPrompt: SYSTEM_PROMPT,
+    emptyContextGuard: EMPTY_CONTEXT_GUARD,
+    hydeSystemPrompt: HYDE_SYSTEM_PROMPT,
+    rewriteHydeSystemPrompt: REWRITE_HYDE_SYSTEM_PROMPT,
+    filter: DOCUMENTOS_FILTER
+  },
+  atendimentos: {
+    systemPrompt: ATENDIMENTOS_SYSTEM_PROMPT,
+    emptyContextGuard: ATENDIMENTOS_EMPTY_CONTEXT_GUARD,
+    hydeSystemPrompt: ATENDIMENTOS_HYDE_SYSTEM_PROMPT,
+    rewriteHydeSystemPrompt: ATENDIMENTOS_REWRITE_HYDE_SYSTEM_PROMPT,
+    filter: ATENDIMENTOS_FILTER
+  }
+};
+
+function resolveMode(mode) {
+  return MODES[mode] ?? MODES.documentos;
+}
+
 function createTimer() {
 function createTimer() {
   const t0 = Date.now();
   const t0 = Date.now();
   let last = t0;
   let last = t0;
@@ -318,19 +382,20 @@ function createTimer() {
   };
   };
 }
 }
 
 
-export async function answerWithContext({ message, conversationId, userId, options }) {
+export async function answerWithContext({ message, conversationId, userId, options, mode }) {
+  const modeConfig = resolveMode(mode);
   const timer = createTimer();
   const timer = createTimer();
   const history = await loadHistory(conversationId, userId);
   const history = await loadHistory(conversationId, userId);
   timer.mark("history");
   timer.mark("history");
-  const { searchQuery, embeddingQuery, embedRole } = await resolveSearchAndEmbedding(message, history, timer);
-  let hits = await searchDocs({ query: embeddingQuery, topK: config.rag.topK, embedRole });
+  const { searchQuery, embeddingQuery, embedRole } = await resolveSearchAndEmbedding(message, history, timer, modeConfig);
+  let hits = await searchDocs({ query: embeddingQuery, topK: config.rag.topK, embedRole, filter: modeConfig.filter });
   timer.mark("search");
   timer.mark("search");
   if (config.rag.rerank && hits.length > 1) {
   if (config.rag.rerank && hits.length > 1) {
     hits = await rerankHits(searchQuery, hits);
     hits = await rerankHits(searchQuery, hits);
     timer.mark("rerank");
     timer.mark("rerank");
   }
   }
   const context = buildContextBlock(hits);
   const context = buildContextBlock(hits);
-  const messages = buildMessages(message, context, history);
+  const messages = buildMessages(message, context, history, modeConfig);
   const completion = await chatCompletion({ messages, options: buildDefaultOptions(options) });
   const completion = await chatCompletion({ messages, options: buildDefaultOptions(options) });
   timer.mark("llm");
   timer.mark("llm");
   timer.log(`conversationId=${conversationId ?? "-"}`);
   timer.log(`conversationId=${conversationId ?? "-"}`);
@@ -341,13 +406,24 @@ export async function answerWithContext({ message, conversationId, userId, optio
   };
   };
 }
 }
 
 
-export async function answerWithContextStream({ message, conversationId, userId, options, onChunk, onSources, onStatus, signal }) {
+export async function answerWithContextStream({
+  message,
+  conversationId,
+  userId,
+  options,
+  mode,
+  onChunk,
+  onSources,
+  onStatus,
+  signal
+}) {
+  const modeConfig = resolveMode(mode);
   const timer = createTimer();
   const timer = createTimer();
   onStatus?.("buscando");
   onStatus?.("buscando");
   const history = await loadHistory(conversationId, userId);
   const history = await loadHistory(conversationId, userId);
   timer.mark("history");
   timer.mark("history");
-  const { searchQuery, embeddingQuery, embedRole } = await resolveSearchAndEmbedding(message, history, timer);
-  let hits = await searchDocs({ query: embeddingQuery, topK: config.rag.topK, embedRole });
+  const { searchQuery, embeddingQuery, embedRole } = await resolveSearchAndEmbedding(message, history, timer, modeConfig);
+  let hits = await searchDocs({ query: embeddingQuery, topK: config.rag.topK, embedRole, filter: modeConfig.filter });
   timer.mark("search");
   timer.mark("search");
   onStatus?.("encontrou", hits.length);
   onStatus?.("encontrou", hits.length);
 
 
@@ -360,7 +436,7 @@ export async function answerWithContextStream({ message, conversationId, userId,
   const sources = hitsToSources(hits);
   const sources = hitsToSources(hits);
   onSources?.(sources);
   onSources?.(sources);
   const context = buildContextBlock(hits);
   const context = buildContextBlock(hits);
-  const messages = buildMessages(message, context, history);
+  const messages = buildMessages(message, context, history, modeConfig);
   onStatus?.("gerando");
   onStatus?.("gerando");
   const completion = await chatCompletionStream({ messages, onChunk, signal, options: buildDefaultOptions(options) });
   const completion = await chatCompletionStream({ messages, onChunk, signal, options: buildDefaultOptions(options) });
   timer.mark("llm");
   timer.mark("llm");

+ 3 - 1
src/controllers/Chat.Controller.js

@@ -22,7 +22,8 @@ export const ChatController = {
         message: body.message,
         message: body.message,
         conversationId: body.conversationId,
         conversationId: body.conversationId,
         userId: req.userId,
         userId: req.userId,
-        options: body.options
+        options: body.options,
+        mode: body.mode
       });
       });
 
 
       await tryPersistMessages(body.conversationId, req.userId, body.message, result);
       await tryPersistMessages(body.conversationId, req.userId, body.message, result);
@@ -51,6 +52,7 @@ export const ChatController = {
         conversationId: body.conversationId,
         conversationId: body.conversationId,
         userId: req.userId,
         userId: req.userId,
         options: body.options,
         options: body.options,
+        mode: body.mode,
         signal: abortController.signal,
         signal: abortController.signal,
         onStatus: (stage, count) => {
         onStatus: (stage, count) => {
           const payload = count !== undefined ? { type: "status", stage, count } : { type: "status", stage };
           const payload = count !== undefined ? { type: "status", stage, count } : { type: "status", stage };

+ 1 - 0
src/middleware/schemas/Chat.Schema.js

@@ -5,6 +5,7 @@ export const chatBodySchema = z.object({
   conversationId: z.coerce.number().int().positive().optional(),
   conversationId: z.coerce.number().int().positive().optional(),
   sessionId: z.string().min(1).optional(),
   sessionId: z.string().min(1).optional(),
   model: z.string().min(1).optional(),
   model: z.string().min(1).optional(),
+  mode: z.enum(["documentos", "atendimentos"]).optional(),
   options: z
   options: z
     .object({
     .object({
       temperature: z.number().min(0).max(2).optional(),
       temperature: z.number().min(0).max(2).optional(),

+ 123 - 0
src/services/atendimentoRagService.js

@@ -0,0 +1,123 @@
+import { ingestDocuments } from "./ingestService.js";
+
+// mensagens do ifbot: Resposta 0 = cliente, 1 = atendente, 9 = sistema.
+// o campo Autor normalmente vem null; o nome do atendente é embutido no próprio
+// Body como HTML: "<b>Nome:</b><br/>texto da mensagem".
+const ATENDENTE_PREFIX_RE = /^\s*<b>\s*([^<:]+?)\s*:?\s*<\/b>\s*(?:<br\s*\/?>)?\s*/i;
+
+function decodeHtmlEntities(str) {
+  return str
+    .replace(/&nbsp;/g, " ")
+    .replace(/&amp;/g, "&")
+    .replace(/&lt;/g, "<")
+    .replace(/&gt;/g, ">")
+    .replace(/&quot;/g, '"')
+    .replace(/&#39;/g, "'");
+}
+
+function stripHtml(html) {
+  if (!html) return "";
+  return decodeHtmlEntities(
+    String(html)
+      .replace(/<br\s*\/?>/gi, "\n")
+      .replace(/<\/p>/gi, "\n")
+      .replace(/<[^>]+>/g, "")
+  )
+    .replace(/[ \t]+/g, " ")
+    .replace(/\n{2,}/g, "\n")
+    .trim();
+}
+
+function parseAtendenteBody(bodyHtml) {
+  const raw = String(bodyHtml ?? "");
+  const match = ATENDENTE_PREFIX_RE.exec(raw);
+  if (!match) return { nome: null, texto: stripHtml(raw) };
+  return { nome: match[1].trim(), texto: stripHtml(raw.slice(match[0].length)) };
+}
+
+function formatDateTime(value) {
+  if (!value) return null;
+  const d = value instanceof Date ? value : new Date(value);
+  if (Number.isNaN(d.getTime())) return null;
+  return new Intl.DateTimeFormat("pt-BR", {
+    timeZone: "America/Sao_Paulo",
+    day: "2-digit",
+    month: "2-digit",
+    year: "numeric",
+    hour: "2-digit",
+    minute: "2-digit"
+  }).format(d);
+}
+
+function formatMensagem(m, clienteNome) {
+  let label;
+  let texto;
+
+  if (m.Resposta === 1) {
+    const parsed = parseAtendenteBody(m.Body);
+    label = parsed.nome ? `Atendente (${parsed.nome})` : "Atendente";
+    texto = parsed.texto;
+  } else if (m.Resposta === 0) {
+    label = clienteNome ? `Cliente (${clienteNome})` : "Cliente";
+    texto = stripHtml(m.Body);
+  } else if (m.Resposta === 9) {
+    label = "Sistema";
+    texto = stripHtml(m.Body);
+  } else {
+    label = "Desconhecido";
+    texto = stripHtml(m.Body);
+  }
+
+  if (!texto && m.Tipodemidia && m.Tipodemidia !== "text") {
+    texto = `[mídia: ${m.Tipodemidia}]`;
+  }
+  if (!texto) return null;
+
+  const timestamp = formatDateTime(m.Timestamp);
+  return timestamp ? `[${timestamp}] ${label}: ${texto}` : `${label}: ${texto}`;
+}
+
+export function buildAtendimentoDocument({ atendimento, cliente, mensagens }) {
+  const ordenadas = [...mensagens].sort((a, b) => {
+    const ta = a.Timestamp ? new Date(a.Timestamp).getTime() : 0;
+    const tb = b.Timestamp ? new Date(b.Timestamp).getTime() : 0;
+    return ta - tb || (a.Id ?? 0) - (b.Id ?? 0);
+  });
+
+  const cabecalho = [
+    `Atendimento ${atendimento.Codigo}` +
+      (atendimento.Setor ? ` — Setor: ${atendimento.Setor}` : "") +
+      (atendimento.Status !== null && atendimento.Status !== undefined ? ` — Status: ${atendimento.Status}` : ""),
+    `Cliente: ${cliente?.Nome ?? "desconhecido"}${cliente?.Telefone ? ` (Tel: ${cliente.Telefone})` : ""}`,
+    atendimento.Abertura ? `Aberto em: ${formatDateTime(atendimento.Abertura)}` : null
+  ].filter(Boolean);
+
+  const linhasMensagens = ordenadas.map((m) => formatMensagem(m, cliente?.Nome)).filter(Boolean);
+
+  const text = [...cabecalho, "", ...linhasMensagens].join("\n").trim();
+
+  return {
+    id: `atendimento:${atendimento.Codigo}`,
+    text,
+    source: `atendimento:${atendimento.Codigo}`,
+    title: `Atendimento ${atendimento.Codigo} — ${cliente?.Nome ?? "cliente desconhecido"}`,
+    metadata: {
+      type: "atendimento",
+      codigo: atendimento.Codigo,
+      protocoloId: atendimento.Id,
+      setor: atendimento.Setor ?? null,
+      status: atendimento.Status ?? null,
+      clienteId: cliente?.Id ?? null,
+      clienteNome: cliente?.Nome ?? null,
+      abertura: atendimento.Abertura ? new Date(atendimento.Abertura).toISOString() : null,
+      totalMensagens: linhasMensagens.length
+    }
+  };
+}
+
+export async function indexAtendimento({ atendimento, cliente, mensagens }) {
+  if (!mensagens?.length) return { upserted: 0 };
+  const doc = buildAtendimentoDocument({ atendimento, cliente, mensagens });
+  if (!doc.text.trim()) return { upserted: 0 };
+  return ingestDocuments([doc]);
+}

+ 11 - 0
src/services/atendimentosService.js

@@ -9,6 +9,7 @@ import {
   protocoloMensagensSchema
   protocoloMensagensSchema
 } from "../middleware/schemas/Atendimento.Schema.js";
 } from "../middleware/schemas/Atendimento.Schema.js";
 import { assertPublicHttpsUrl } from "./ingestService.js";
 import { assertPublicHttpsUrl } from "./ingestService.js";
+import { indexAtendimento } from "./atendimentoRagService.js";
 import { BadRequestError } from "../shared/errors/index.js";
 import { BadRequestError } from "../shared/errors/index.js";
 
 
 const MAX_REDIRECTS = 5;
 const MAX_REDIRECTS = 5;
@@ -153,6 +154,16 @@ export async function salvarAtendimento({ payload, setor, sourceUrl, abertura, t
 
 
     await trx.commit();
     await trx.commit();
 
 
+    try {
+      await indexAtendimento({
+        atendimento: { Id: p.Id, Codigo: p.Codigo, Status: p.Status, Setor: setor ?? null, Abertura: toDate(abertura) },
+        cliente: { ...c, Telefone: clienteTelefone },
+        mensagens
+      });
+    } catch (err) {
+      console.error(`[atendimentos] falha ao indexar RAG (${p.Codigo}):`, err.message);
+    }
+
     return {
     return {
       protocoloId: p.Id,
       protocoloId: p.Id,
       codigo: p.Codigo,
       codigo: p.Codigo,

+ 3 - 2
src/services/searchService.js

@@ -2,7 +2,7 @@ import { config } from "../config/index.js";
 import { embedTexts } from "./ollamaClient.js";
 import { embedTexts } from "./ollamaClient.js";
 import { qdrant } from "./qdrantClient.js";
 import { qdrant } from "./qdrantClient.js";
 
 
-export async function searchDocs({ query, topK, embedRole = "query", minScore }) {
+export async function searchDocs({ query, topK, embedRole = "query", minScore, filter }) {
   const collectionName = config.qdrant.collection;
   const collectionName = config.qdrant.collection;
   const [vector] = await embedTexts([query], { role: embedRole });
   const [vector] = await embedTexts([query], { role: embedRole });
 
 
@@ -13,7 +13,8 @@ export async function searchDocs({ query, topK, embedRole = "query", minScore })
       limit: topK ?? config.rag.topK,
       limit: topK ?? config.rag.topK,
       score_threshold: minScore ?? config.rag.minScore,
       score_threshold: minScore ?? config.rag.minScore,
       with_payload: true,
       with_payload: true,
-      with_vector: false
+      with_vector: false,
+      ...(filter ? { filter } : {})
     });
     });
   } catch (err) {
   } catch (err) {
     const status = err?.status ?? err?.statusCode ?? err?.response?.status ?? null;
     const status = err?.status ?? err?.statusCode ?? err?.response?.status ?? null;