|
|
@@ -0,0 +1,87 @@
|
|
|
+import { config } from "../config/index.js";
|
|
|
+import { chatCompletion } from "./ollamaClient.js";
|
|
|
+import { WhatsappConversa } from "../models/WhatsappConversa.model.js";
|
|
|
+import { WhatsappMensagem } from "../models/WhatsappMensagem.model.js";
|
|
|
+import { NotFoundError } from "../shared/errors/index.js";
|
|
|
+
|
|
|
+const SYSTEM_PROMPT = [
|
|
|
+ "Você é um atendente de suporte da star internet, respondendo pelo WhatsApp.",
|
|
|
+ "Seja cordial, objetivo e resolutivo: entenda o problema, faça as perguntas necessarias,",
|
|
|
+ "explique os passos com clareza e conduza ate a solução ou o encaminhamento correto.",
|
|
|
+ "Responda em português brasileiro.",
|
|
|
+ "Gere APENAS o texto da mensagem que seria enviada ao cliente agora, sem comentarios,",
|
|
|
+ "explicações ou marcações extras - nada alem da resposta em si."
|
|
|
+].join(" ");
|
|
|
+
|
|
|
+function recortarHistorico(mensagensAsc, maxChars) {
|
|
|
+ let total = 0;
|
|
|
+ const selecionadas = [];
|
|
|
+ for (let i = mensagensAsc.length -1; i >=0 ; i-=1){
|
|
|
+ const texto = mensagensAsc[i].Corpo || "";
|
|
|
+ total += texto.length;
|
|
|
+ if (total > maxChars && selecionadas.length > 0) break;
|
|
|
+ selecionadas.unshift(mensagensAsc[i]);
|
|
|
+ }
|
|
|
+ return selecionadas;
|
|
|
+}
|
|
|
+
|
|
|
+function paraTextoLLM(m) {
|
|
|
+ if (m.Corpo && m.Corpo.trim()) return m.Corpo.trim();
|
|
|
+ if (m.TipoMidia) return `[mídia: ${m.TipoMidia}]`;
|
|
|
+ return "[mensagem vazia]";
|
|
|
+}
|
|
|
+
|
|
|
+export async function gerarSugestaoResposta(conversaId) {
|
|
|
+ const conversa = await WhatsappConversa.query().findById(conversaId);
|
|
|
+ if (!conversa) throw new NotFoundError("conversa_not_found");
|
|
|
+
|
|
|
+ const recentes = await WhatsappMensagem.query()
|
|
|
+ .where("ConversaId", conversaId)
|
|
|
+ .orderBy("Timestamp", "desc")
|
|
|
+ .orderBy("Id", "desc")
|
|
|
+ .limit(config.atendenteSugestao.historyLimit);
|
|
|
+
|
|
|
+
|
|
|
+ const historico = recentes.reverse();
|
|
|
+
|
|
|
+ if (!historico.length || historico[historico.length -1].Direcao !== "entrada") {
|
|
|
+ return { sugestao: null };
|
|
|
+ }
|
|
|
+
|
|
|
+ const recorte = recortarHistorico(historico, config.atendenteSugestao.maxChars);
|
|
|
+
|
|
|
+ const messages = [
|
|
|
+ { role: "system", content: SYSTEM_PROMPT },
|
|
|
+ ...recorte.map((m) => ({
|
|
|
+ role: m.Direcao === "entrada" ? "user" : "assistant",
|
|
|
+ content: paraTextoLLM(m)
|
|
|
+ }))
|
|
|
+ ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ const { content } = await chatCompletion({
|
|
|
+ messages,
|
|
|
+ model: config.atendenteSugestao.model,
|
|
|
+ options: {
|
|
|
+ temperature: config.llm.temperature,
|
|
|
+ top_p: config.llm.topP,
|
|
|
+ num_predict: config.llm.numPredict,
|
|
|
+ repeat_penalty: config.llm.repeatPenalty,
|
|
|
+ num_ctx: config.llm.numCtx
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return { sugestao : content.trim() || null };
|
|
|
+ } catch (err) {
|
|
|
+ const msg = typeof err?.message === "string" ? err.message : "";
|
|
|
+ if (msg.startsWith("ollama_model_not_found") || msg.startsWith("ollama_error:404")){
|
|
|
+ console.warn("[atendenteSugentao] modelo indisponivel:", msg);
|
|
|
+ const amigavel = new Error(
|
|
|
+ "Sugestão indisponivel: o modelo star-atendente ainda não foi publicado no Ollama"
|
|
|
+ );
|
|
|
+ amigavel.statusCode = 503;
|
|
|
+ throw amigavel;
|
|
|
+ }
|
|
|
+ throw err;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|