Bladeren bron

melhorias backend

leonardo 2 maanden geleden
bovenliggende
commit
0a83c8a623

+ 37 - 13
chat/chatChain.js

@@ -4,23 +4,35 @@ import { getRecentMessages } from "../src/services/conversationsService.js";
 import { searchDocs } from "../chat/searchChat.js";
 
 const SYSTEM_PROMPT = [
-  "Você é um assistente de atendimento interno da empresa Star.",
-  "Seu papel é responder perguntas dos colaboradores com base nos documentos internos fornecidos no CONTEXTO.",
+  "Você é o Oráculo, assistente interno da empresa Star.",
+  "Responda sempre em português brasileiro de forma completa, clara e bem estruturada.",
   "",
-  "Regras:",
-  "- Responda sempre em português brasileiro.",
-  "- Baseie-se exclusivamente no CONTEXTO quando ele estiver disponível.",
-  '- Se a informação não estiver no CONTEXTO, diga claramente: "Não encontrei essa informação na base de conhecimento."',
-  "- Nunca invente informações ou suponha dados que não estejam no CONTEXTO.",
-  "- Seja direto, objetivo e use bullet points quando a resposta tiver múltiplos itens.",
-  "- Se a pergunta for vaga, peça esclarecimento antes de responder.",
-  "- Ao citar informações, mencione a fonte (nome do documento) quando disponível."
+  "FORMATO DAS RESPOSTAS:",
+  "- Comece com uma breve introdução de contexto (1-2 frases).",
+  "- Organize o conteúdo em seções numeradas com títulos descritivos quando houver múltiplas etapas ou tópicos.",
+  "- Use sub-bullets (traço) para detalhar cada item dentro das seções.",
+  "- Adicione callouts inline quando relevante: '**Nota:**', '**Atenção:**', '**Dica:**'.",
+  "- Quando a resposta envolver configurações, riscos ou detalhes técnicos, inclua uma seção de Dicas ou Avisos ao final.",
+  "- Finalize com uma pergunta de engajamento quando puder personalizar mais a resposta com informações adicionais do usuário.",
+  "",
+  "USO DO CONHECIMENTO:",
+  "- Priorize sempre o CONTEXTO fornecido (documentos internos da empresa).",
+  "- Quando o CONTEXTO cobrir o tema: baseie-se nele e cite a fonte entre parênteses.",
+  "- Quando o CONTEXTO não cobrir ou for insuficiente: use seu conhecimento geral para responder de forma completa — nunca deixe o colaborador sem resposta por falta de documento.",
+  "- Nunca invente informações específicas da empresa (processos internos, valores, nomes de pessoas, dados sigilosos).",
+  "- Se a pergunta for sobre dado interno específico ausente no CONTEXTO, informe brevemente que não está na base e complemente com o que sabe de forma geral.",
+  "",
+  "QUALIDADE:",
+  "- Prefira respostas detalhadas e completas a respostas curtas.",
+  "- Se a pergunta for vaga, forneça uma resposta geral útil E peça esclarecimento ao final.",
+  "- Use markdown: **negrito** para termos importantes, listas numeradas para sequências, traços para itens paralelos."
 ].join("\n");
 
 function buildContextBlock(hits) {
   const lines = [];
   hits.forEach((h, i) => {
-    const header = `# Fonte ${i + 1}${h.source ? ` (${h.source})` : ""}`;
+    const score = h.score ? ` [relevância: ${(h.score * 100).toFixed(0)}%]` : "";
+    const header = `## Fonte ${i + 1}${h.source ? ` — ${h.source}` : ""}${score}`;
     lines.push(header);
     lines.push(h.text);
     lines.push("");
@@ -74,13 +86,25 @@ async function loadHistory(conversationId) {
   }
 }
 
+function buildDefaultOptions(override) {
+  const defaults = {
+    temperature: config.llm.temperature,
+    top_p: config.llm.topP,
+    num_predict: config.llm.numPredict,
+    repeat_penalty: config.llm.repeatPenalty
+  };
+  return override && Object.keys(override).length > 0
+    ? { ...defaults, ...override }
+    : defaults;
+}
+
 export async function answerWithContext({ message, conversationId, options }) {
   const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message;
   const hits = await searchDocs({ query: searchQuery, topK: config.rag.topK });
   const context = buildContextBlock(hits);
   const history = await loadHistory(conversationId);
   const messages = buildMessages(message, context, history);
-  const completion = await chatCompletion({ messages, options });
+  const completion = await chatCompletion({ messages, options: buildDefaultOptions(options) });
 
   return {
     answer: completion.content,
@@ -96,7 +120,7 @@ export async function answerWithContextStream({ message, conversationId, options
   const messages = buildMessages(message, context, history);
   const sources = hitsToSources(hits);
 
-  const completion = await chatCompletionStream({ messages, onChunk, signal, options });
+  const completion = await chatCompletionStream({ messages, onChunk, signal, options: buildDefaultOptions(options) });
 
   return {
     answer: completion.content,

+ 6 - 0
src/config/index.js

@@ -32,5 +32,11 @@ export const config = {
     chunkSize: Number(process.env.RAG_CHUNK_SIZE ?? 900),
     chunkOverlap: Number(process.env.RAG_CHUNK_OVERLAP ?? 150),
     queryRewrite: process.env.RAG_QUERY_REWRITE === "true"
+  },
+  llm: {
+    temperature: Number(process.env.LLM_TEMPERATURE ?? 0.7),
+    topP: Number(process.env.LLM_TOP_P ?? 0.9),
+    numPredict: Number(process.env.LLM_NUM_PREDICT ?? 2048),
+    repeatPenalty: Number(process.env.LLM_REPEAT_PENALTY ?? 1.1)
   }
 };

+ 6 - 0
src/middleware/requireUser.js

@@ -0,0 +1,6 @@
+export function requireUser(req, res, next) {
+  const userId = req.user?.sub;
+  if (!userId) return res.status(401).json({ error: "unauthorized" });
+  req.userId = userId;
+  next();
+}

+ 11 - 20
src/routes/conversations.js

@@ -7,39 +7,34 @@ import {
   updateConversationTitle,
   deleteConversation
 } from "../services/conversationsService.js";
+import { requireUser } from "../middleware/requireUser.js";
 
 export const conversationsRouter = Router();
 
-conversationsRouter.get("/", async (req, res, next) => {
+conversationsRouter.get("/", requireUser, async (req, res, next) => {
   try {
-    const userId = req.user?.sub;
-    if (!userId) return res.status(401).json({ error: "unauthorized" });
-    const items = await listConversations(userId);
+    const items = await listConversations(req.userId);
     res.json({ items });
   } catch (err) {
     next(err);
   }
 });
 
-conversationsRouter.post("/", async (req, res, next) => {
+conversationsRouter.post("/", requireUser, async (req, res, next) => {
   try {
-    const userId = req.user?.sub;
-    if (!userId) return res.status(401).json({ error: "unauthorized" });
     const title = z.string().max(200).catch("Nova conversa").parse(req.body?.title);
-    const conv = await createConversation(userId, title);
+    const conv = await createConversation(req.userId, title);
     res.json(conv);
   } catch (err) {
     next(err);
   }
 });
 
-conversationsRouter.get("/:id/messages", async (req, res, next) => {
+conversationsRouter.get("/:id/messages", requireUser, async (req, res, next) => {
   try {
-    const userId = req.user?.sub;
-    if (!userId) return res.status(401).json({ error: "unauthorized" });
     const conversationId = Number(req.params.id);
     if (!conversationId) return res.status(400).json({ error: "invalid_id" });
-    const msgs = await getConversationMessages(conversationId, userId);
+    const msgs = await getConversationMessages(conversationId, req.userId);
     if (!msgs) return res.status(404).json({ error: "not_found" });
     res.json({ items: msgs });
   } catch (err) {
@@ -47,27 +42,23 @@ conversationsRouter.get("/:id/messages", async (req, res, next) => {
   }
 });
 
-conversationsRouter.patch("/:id", async (req, res, next) => {
+conversationsRouter.patch("/:id", requireUser, async (req, res, next) => {
   try {
-    const userId = req.user?.sub;
-    if (!userId) return res.status(401).json({ error: "unauthorized" });
     const conversationId = Number(req.params.id);
     if (!conversationId) return res.status(400).json({ error: "invalid_id" });
     const title = z.string().min(1).max(200).parse(req.body?.title);
-    await updateConversationTitle(conversationId, userId, title);
+    await updateConversationTitle(conversationId, req.userId, title);
     res.json({ ok: true });
   } catch (err) {
     next(err);
   }
 });
 
-conversationsRouter.delete("/:id", async (req, res, next) => {
+conversationsRouter.delete("/:id", requireUser, async (req, res, next) => {
   try {
-    const userId = req.user?.sub;
-    if (!userId) return res.status(401).json({ error: "unauthorized" });
     const conversationId = Number(req.params.id);
     if (!conversationId) return res.status(400).json({ error: "invalid_id" });
-    await deleteConversation(conversationId, userId);
+    await deleteConversation(conversationId, req.userId);
     res.json({ ok: true });
   } catch (err) {
     next(err);

+ 3 - 4
src/routes/documents.js

@@ -1,12 +1,12 @@
 import { Router } from "express";
 import { z } from "zod";
 import { listDocuments, deleteDocumentsBySource } from "../services/documentsService.js";
+import { requireUser } from "../middleware/requireUser.js";
 
 export const documentsRouter = Router();
 
-documentsRouter.get("/", async (req, res, next) => {
+documentsRouter.get("/", requireUser, async (req, res, next) => {
   try {
-    if (!req.user?.sub) return res.status(401).json({ error: "unauthorized" });
     const limit = z.coerce.number().int().positive().max(200).catch(50).parse(req.query.limit);
     const offset = z.coerce.number().int().nonnegative().optional().parse(req.query.offset);
     const result = await listDocuments({ limit, offset });
@@ -16,9 +16,8 @@ documentsRouter.get("/", async (req, res, next) => {
   }
 });
 
-documentsRouter.delete("/source/:source", async (req, res, next) => {
+documentsRouter.delete("/source/:source", requireUser, async (req, res, next) => {
   try {
-    if (!req.user?.sub) return res.status(401).json({ error: "unauthorized" });
     const source = req.params.source;
     if (!source) {
       return res.status(400).json({ error: "source_required" });

+ 4 - 6
src/routes/ingest.js

@@ -2,6 +2,7 @@ import { Router } from "express";
 import { z } from "zod";
 import multer from "multer";
 import { extractDocumentsFromUpload, ingestDocuments, fetchUrlText } from "../../chat/ingest.js";
+import { requireUser } from "../middleware/requireUser.js";
 
 export const ingestRouter = Router();
 
@@ -26,9 +27,8 @@ const ingestUrlSchema = z.object({
   source: z.string().min(1).optional()
 });
 
-ingestRouter.post("/", async (req, res, next) => {
+ingestRouter.post("/", requireUser, async (req, res, next) => {
   try {
-    if (!req.user?.sub) return res.status(401).json({ error: "unauthorized" });
     const body = ingestBodySchema.parse(req.body);
     const result = await ingestDocuments(body.documents);
     res.json(result);
@@ -37,9 +37,8 @@ ingestRouter.post("/", async (req, res, next) => {
   }
 });
 
-ingestRouter.post("/file", upload.single("file"), async (req, res, next) => {
+ingestRouter.post("/file", requireUser, upload.single("file"), async (req, res, next) => {
   try {
-    if (!req.user?.sub) return res.status(401).json({ error: "unauthorized" });
     const f = req.file;
     if (!f?.buffer) {
       res.status(400).json({ error: "file_required" });
@@ -67,9 +66,8 @@ ingestRouter.post("/file", upload.single("file"), async (req, res, next) => {
   }
 });
 
-ingestRouter.post("/url", async (req, res, next) => {
+ingestRouter.post("/url", requireUser, async (req, res, next) => {
   try {
-    if (!req.user?.sub) return res.status(401).json({ error: "unauthorized" });
     const body = ingestUrlSchema.parse(req.body);
     const text = await fetchUrlText(body.url);
     if (!text) {

+ 2 - 2
src/routes/search.js

@@ -1,6 +1,7 @@
 import { Router } from "express";
 import { z } from "zod";
 import { searchDocs } from "../../chat/searchChat.js";
+import { requireUser } from "../middleware/requireUser.js";
 
 export const searchRouter = Router();
 
@@ -9,9 +10,8 @@ const searchBodySchema = z.object({
   topK: z.number().int().positive().optional()
 });
 
-searchRouter.post("/", async (req, res, next) => {
+searchRouter.post("/", requireUser, async (req, res, next) => {
   try {
-    if (!req.user?.sub) return res.status(401).json({ error: "unauthorized" });
     const body = searchBodySchema.parse(req.body);
     const results = await searchDocs({
       query: body.query,

+ 10 - 8
src/services/conversationsService.js

@@ -37,15 +37,17 @@ export async function getRecentMessages(conversationId, limit = 12) {
 }
 
 export async function addMessage(conversationId, { role, content, sources = null }) {
-  await db("messages").insert({
-    ConversationId: conversationId,
-    Role: role,
-    Content: content,
-    Sources: sources ? JSON.stringify(sources) : null
+  await db.transaction(async (trx) => {
+    await trx("messages").insert({
+      ConversationId: conversationId,
+      Role: role,
+      Content: content,
+      Sources: sources ? JSON.stringify(sources) : null
+    });
+    await trx("conversations")
+      .where({ Id: conversationId })
+      .update({ UpdatedAt: trx.fn.now() });
   });
-  await db("conversations")
-    .where({ Id: conversationId })
-    .update({ UpdatedAt: db.fn.now() });
 }
 
 export async function updateConversationTitle(conversationId, userId, title) {