Parcourir la source

ajustes atendimentos via whatsapp

leonardo il y a 1 mois
Parent
commit
c87efd8344

+ 5 - 0
.env.example

@@ -46,6 +46,11 @@ OLLAMA_VISION_MODEL=llava
 # chamada travada (modelo sobrecarregado, processo do Ollama pendurado) nunca resolve
 # nem rejeita — trava indefinidamente locks como o de avaliação em lote de atendimentos.
 OLLAMA_TIMEOUT_MS=180000
+# Timeout de inatividade (ms) para o streaming do chat (chatCompletionStream). Diferente
+# do OLLAMA_TIMEOUT_MS (que limita o total de uma chamada não-streaming), este reinicia a
+# cada chunk recebido — só dispara se o Ollama parar de mandar bytes no meio de uma
+# resposta, o que antes deixava a UI presa em "Gerando resposta..." para sempre.
+OLLAMA_STREAM_IDLE_TIMEOUT_MS=60000
 
 # Prefixos opcionais prependados ao texto antes de gerar embedding (útil para modelos
 # assimétricos que recomendam prefixo diferente para query de busca e passagem indexada,

+ 2 - 1
src/config/index.js

@@ -48,7 +48,8 @@ export const config = {
     embedPassagePrefix: process.env.OLLAMA_EMBED_PASSAGE_PREFIX ?? "",
     chatModel: process.env.OLLAMA_CHAT_MODEL ?? "llama3.1",
     visionModel: process.env.OLLAMA_VISION_MODEL ?? "llava:latest",
-    timeoutMs: Number(process.env.OLLAMA_TIMEOUT_MS ?? 180_000)
+    timeoutMs: Number(process.env.OLLAMA_TIMEOUT_MS ?? 180_000),
+    streamIdleTimeoutMs: Number(process.env.OLLAMA_STREAM_IDLE_TIMEOUT_MS ?? 60_000)
   },
   rag: {
     topK: Number(process.env.RAG_TOP_K ?? 8),

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

@@ -75,7 +75,7 @@ export const ChatController = {
     } catch (err) {
       if (!res.headersSent) {
         next(err);
-      } else if (err.name !== "AbortError") {
+      } else if (res.writable) {
         res.write(`data: ${JSON.stringify({ type: "error", error: err.message })}\n\n`);
         res.end();
       }

+ 12 - 0
src/factories/Server.factory.js

@@ -66,6 +66,18 @@ export class ServerFactory {
       iniciarConexaoSalva().catch((e) => console.error("[whatsapp] falha ao reconectar sessão salva:", e.message));
     });
 
+    // sem isso, EADDRINUSE (porta já ocupada por outro backend rodando) sobe como
+    // exceção não tratada — stack trace confuso e nada explica que é só outro processo
+    // já de pé; aqui a causa fica clara e o processo encerra igual, sem crash "seco"
+    server.on("error", (err) => {
+      if (err.code === "EADDRINUSE") {
+        console.error(`\n[server] Porta ${port} já está em uso — outro backend já deve estar rodando. Encerre-o antes de iniciar de novo.\n`);
+      } else {
+        console.error("[server] Falha ao iniciar o servidor:", err.message);
+      }
+      process.exit(1);
+    });
+
     this.app = app;
     return server;
   }

+ 70 - 45
src/services/ollamaClient.js

@@ -115,56 +115,81 @@ export async function chatCompletion({ messages, options, format, model, signal
 }
 
 export async function chatCompletionStream({ messages, onChunk, signal, options }) {
-  const res = await fetch(`${config.ollama.url}/api/chat`, {
-    method: "POST",
-    headers: { "content-type": "application/json" },
-    body: JSON.stringify({
-      model: config.ollama.chatModel,
-      messages,
-      stream: true,
-      ...(options && Object.keys(options).length > 0 && { options })
-    }),
-    signal
-  });
-
-  if (!res.ok) {
-    const text = await res.text().catch(() => "");
-    const err = new Error(`ollama_error:${res.status}:${text || res.statusText}`);
-    err.statusCode = 502;
-    throw err;
+  const idleTimeoutMs = config.ollama.streamIdleTimeoutMs;
+  const idleController = new AbortController();
+  let idleTimer = setTimeout(
+    () => idleController.abort(new Error(`ollama_stream_idle_timeout:${idleTimeoutMs}ms`)),
+    idleTimeoutMs
+  );
+  const resetIdleTimer = () => {
+    clearTimeout(idleTimer);
+    idleTimer = setTimeout(
+      () => idleController.abort(new Error(`ollama_stream_idle_timeout:${idleTimeoutMs}ms`)),
+      idleTimeoutMs
+    );
+  };
+  const onExternalAbort = () => idleController.abort(signal.reason);
+  if (signal) {
+    if (signal.aborted) idleController.abort(signal.reason);
+    else signal.addEventListener("abort", onExternalAbort, { once: true });
   }
 
-  const reader = res.body.getReader();
-  const decoder = new TextDecoder();
-  let fullContent = "";
-  let buffer = "";
-
-  const processLine = (line) => {
-    if (!line.trim()) return;
-    try {
-      const parsed = JSON.parse(line);
-      const delta = parsed?.message?.content ?? "";
-      if (delta) {
-        fullContent += delta;
-        onChunk(delta);
-      }
-    } catch {}
-  };
+  try {
+    const res = await fetch(`${config.ollama.url}/api/chat`, {
+      method: "POST",
+      headers: { "content-type": "application/json" },
+      body: JSON.stringify({
+        model: config.ollama.chatModel,
+        messages,
+        stream: true,
+        ...(options && Object.keys(options).length > 0 && { options })
+      }),
+      signal: idleController.signal
+    });
 
-  while (true) {
-    const { done, value } = await reader.read();
-    if (done) break;
-    
-    buffer += decoder.decode(value, { stream: true });
-    const lines = buffer.split("\n");
-    buffer = lines.pop() ?? "";
-    for (const line of lines) processLine(line);
-  }
+    if (!res.ok) {
+      const text = await res.text().catch(() => "");
+      const err = new Error(`ollama_error:${res.status}:${text || res.statusText}`);
+      err.statusCode = 502;
+      throw err;
+    }
 
-  buffer += decoder.decode();
-  processLine(buffer);
+    const reader = res.body.getReader();
+    const decoder = new TextDecoder();
+    let fullContent = "";
+    let buffer = "";
+
+    const processLine = (line) => {
+      if (!line.trim()) return;
+      try {
+        const parsed = JSON.parse(line);
+        const delta = parsed?.message?.content ?? "";
+        if (delta) {
+          fullContent += delta;
+          onChunk(delta);
+        }
+      } catch {}
+    };
 
-  return { content: fullContent };
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done) break;
+      resetIdleTimer();
+
+      buffer += decoder.decode(value, { stream: true });
+      const lines = buffer.split("\n");
+      buffer = lines.pop() ?? "";
+      for (const line of lines) processLine(line);
+    }
+
+    buffer += decoder.decode();
+    processLine(buffer);
+
+    return { content: fullContent };
+  } finally {
+    clearTimeout(idleTimer);
+    signal?.removeEventListener("abort", onExternalAbort);
+  }
 }
 
 export async function visionExtractFromImage({ imageBase64, prompt }) {