arcadiasuite/server/replit_integrations/chat/routes.ts

532 lines
20 KiB
TypeScript

import type { Express, Request, Response } from "express";
import OpenAI from "openai";
import { chatStorage } from "./storage";
import { buildPromptWithContext } from "./prompt";
import { compassStorage } from "../../compass/storage";
import { PDFParse } from "pdf-parse";
import { learningService } from "../../learning/service";
const openai = new OpenAI({
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
});
export function registerChatRoutes(app: Express): void {
app.get("/api/conversations", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userId = req.user!.id;
const conversations = await chatStorage.getUserConversations(userId);
res.json(conversations);
} catch (error) {
console.error("Error fetching conversations:", error);
res.status(500).json({ error: "Failed to fetch conversations" });
}
});
app.get("/api/conversations/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const userId = req.user!.id;
const conversation = await chatStorage.getConversation(id);
if (!conversation) {
return res.status(404).json({ error: "Conversation not found" });
}
if (conversation.userId !== userId) {
return res.status(403).json({ error: "Access denied" });
}
const messages = await chatStorage.getMessagesByConversation(id);
res.json({ ...conversation, messages });
} catch (error) {
console.error("Error fetching conversation:", error);
res.status(500).json({ error: "Failed to fetch conversation" });
}
});
app.post("/api/conversations", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userId = req.user!.id;
const { title } = req.body;
const conversation = await chatStorage.createConversation(userId, title || "Nova Conversa");
res.status(201).json(conversation);
} catch (error) {
console.error("Error creating conversation:", error);
res.status(500).json({ error: "Failed to create conversation" });
}
});
app.patch("/api/conversations/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const userId = req.user!.id;
const { title } = req.body;
if (!title || typeof title !== "string" || title.trim().length === 0) {
return res.status(400).json({ error: "Title is required" });
}
const conversation = await chatStorage.getConversation(id);
if (!conversation) {
return res.status(404).json({ error: "Conversation not found" });
}
if (conversation.userId !== userId) {
return res.status(403).json({ error: "Access denied" });
}
const updated = await chatStorage.updateConversation(id, title.trim());
res.json(updated);
} catch (error) {
console.error("Error updating conversation:", error);
res.status(500).json({ error: "Failed to update conversation" });
}
});
app.delete("/api/conversations/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const userId = req.user!.id;
const conversation = await chatStorage.getConversation(id);
if (!conversation) {
return res.status(404).json({ error: "Conversation not found" });
}
if (conversation.userId !== userId) {
return res.status(403).json({ error: "Access denied" });
}
await chatStorage.deleteConversation(id);
res.status(204).send();
} catch (error) {
console.error("Error deleting conversation:", error);
res.status(500).json({ error: "Failed to delete conversation" });
}
});
app.post("/api/conversations/:id/messages", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const conversationId = parseInt(req.params.id);
const userId = req.user!.id;
const { content, fileContent, fileName, fileBase64, projectId, tenantId } = req.body;
const conversation = await chatStorage.getConversation(conversationId);
if (!conversation) {
return res.status(404).json({ error: "Conversation not found" });
}
if (conversation.userId !== userId) {
return res.status(403).json({ error: "Access denied" });
}
let processedFileContent = fileContent;
if (fileName && fileBase64 && fileName.toLowerCase().endsWith('.pdf')) {
try {
const pdfBuffer = Buffer.from(fileBase64, 'base64');
const parser = new PDFParse({ data: pdfBuffer });
const result = await parser.getText();
processedFileContent = result.text;
await parser.destroy();
} catch (pdfError) {
console.error("Error parsing PDF:", pdfError);
processedFileContent = "[Erro ao processar PDF - arquivo pode estar corrompido ou protegido]";
}
}
let messageContent = content;
if (fileName && (processedFileContent || fileContent)) {
messageContent = `[Arquivo anexado: ${fileName}]\n\n${content}`;
}
const userMessage = await chatStorage.createMessage(conversationId, "user", messageContent);
if (fileName && (processedFileContent || fileContent)) {
await chatStorage.createAttachment(userMessage.id, fileName, "text", processedFileContent || fileContent);
}
const knowledgeResults = await chatStorage.searchKnowledgeBase(content);
let knowledgeContext = "";
if (knowledgeResults.length > 0) {
knowledgeContext = knowledgeResults.map(kb =>
`---\nDocumento: ${kb.title}\nAutor: ${kb.author}\nCategoria: ${kb.category}\nConteúdo: ${kb.content}\n---`
).join("\n\n");
}
let diagnosticContext: any = undefined;
const diagnosticKeywords = ['canvas', 'swot', 'pdca', 'processo', 'requisito', 'diagnóstico', 'análise', 'forças', 'fraquezas', 'oportunidades', 'ameaças', 'melhoria', 'ciclo', 'modelo de negócio', 'bmca', 'proposta de valor'];
const shouldFetchDiagnostics = diagnosticKeywords.some(keyword =>
content.toLowerCase().includes(keyword)
);
if (shouldFetchDiagnostics && tenantId) {
try {
const isMember = await compassStorage.isUserInTenant(userId, tenantId);
if (isMember) {
let validProjectId: number | undefined = undefined;
if (projectId) {
const project = await compassStorage.getProject(projectId, tenantId);
if (project) {
validProjectId = projectId;
}
}
diagnosticContext = {};
const tenant = await compassStorage.getTenant(tenantId);
if (tenant) {
diagnosticContext.tenantName = tenant.name;
}
if (validProjectId) {
const project = await compassStorage.getProject(validProjectId, tenantId);
if (project) {
diagnosticContext.projectName = project.name;
}
const canvasBlocks = await compassStorage.getCanvasBlocks(validProjectId);
if (canvasBlocks.length > 0) {
diagnosticContext.canvas = canvasBlocks;
}
const swotAnalyses = await compassStorage.getSwotAnalyses(validProjectId);
if (swotAnalyses.length > 0) {
const allSwotItems: any[] = [];
for (const analysis of swotAnalyses) {
const items = await compassStorage.getSwotItems(analysis.id);
allSwotItems.push(...items);
}
diagnosticContext.swot = { analyses: swotAnalyses, items: allSwotItems };
}
const processes = await compassStorage.getProcesses(validProjectId);
if (processes.length > 0) {
const allSteps: any[] = [];
for (const process of processes) {
const steps = await compassStorage.getProcessSteps(process.id);
allSteps.push(...steps);
}
diagnosticContext.processes = { processes, steps: allSteps };
}
}
const pdcaCycles = await compassStorage.getPdcaCycles(tenantId, validProjectId);
if (pdcaCycles.length > 0) {
const allActions: any[] = [];
for (const cycle of pdcaCycles) {
const actions = await compassStorage.getPdcaActions(cycle.id);
allActions.push(...actions);
}
diagnosticContext.pdca = { cycles: pdcaCycles, actions: allActions };
}
const requirements = await compassStorage.getRequirements(tenantId, validProjectId);
if (requirements.length > 0) {
diagnosticContext.requirements = requirements;
}
if (Object.keys(diagnosticContext).filter(k => k !== 'tenantName' && k !== 'projectName').length === 0) {
diagnosticContext = undefined;
}
}
} catch (e) {
console.error("Error fetching diagnostic context:", e);
}
}
const systemPrompt = buildPromptWithContext(knowledgeContext, processedFileContent || fileContent, diagnosticContext);
const messages = await chatStorage.getMessagesByConversation(conversationId);
const chatMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...messages.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
];
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const stream = await openai.chat.completions.create({
model: "gpt-4o",
messages: chatMessages,
stream: true,
max_tokens: 4096,
});
let fullResponse = "";
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || "";
if (content) {
fullResponse += content;
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
let interactionId: number | undefined;
try {
interactionId = await learningService.saveInteraction({
userId,
tenantId: tenantId || undefined,
source: 'agent_chat',
sessionId: conversationId.toString(),
question: content,
answer: fullResponse,
context: diagnosticContext ? { diagnostics: true } : undefined,
dataSourcesAccessed: knowledgeResults.length > 0 ? ['knowledge_base'] : undefined,
});
} catch (err) {
console.error("[Learning] Error saving chat interaction:", err);
}
const assistantMessage = await chatStorage.createMessage(conversationId, "assistant", fullResponse, interactionId);
res.write(`data: ${JSON.stringify({ done: true, messageId: assistantMessage.id, interactionId })}\n\n`);
res.end();
} catch (error) {
console.error("Error sending message:", error);
if (res.headersSent) {
res.write(`data: ${JSON.stringify({ error: "Failed to send message" })}\n\n`);
res.end();
} else {
res.status(500).json({ error: "Failed to send message" });
}
}
});
app.get("/api/knowledge-base", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const entries = await chatStorage.getAllKnowledgeBase();
res.json(entries);
} catch (error) {
console.error("Error fetching knowledge base:", error);
res.status(500).json({ error: "Failed to fetch knowledge base" });
}
});
app.post("/api/knowledge-base", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const { title, content, author, category, source } = req.body;
if (!title || !content || !author || !category) {
return res.status(400).json({ error: "Missing required fields: title, content, author, category" });
}
const entry = await chatStorage.createKnowledgeBaseEntry({ title, content, author, category, source });
res.status(201).json(entry);
} catch (error) {
console.error("Error creating knowledge base entry:", error);
res.status(500).json({ error: "Failed to create knowledge base entry" });
}
});
app.delete("/api/knowledge-base/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const id = parseInt(req.params.id);
await chatStorage.deleteKnowledgeBaseEntry(id);
res.status(204).send();
} catch (error) {
console.error("Error deleting knowledge base entry:", error);
res.status(500).json({ error: "Failed to delete knowledge base entry" });
}
});
app.post("/api/agent/chat", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { message, conversationHistory = [] } = req.body;
if (!message || typeof message !== "string") {
return res.status(400).json({ error: "Message is required" });
}
const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [
{
role: "system",
content: `Você é o Arcádia Agent, um assistente inteligente de negócios especializado em gestão de projetos, metodologias ágeis (Scrum/Kanban), consultoria empresarial e análise de dados. Você ajuda profissionais a:
- Gerenciar tarefas e sprints de projetos
- Aplicar metodologias ágeis corretamente
- Analisar problemas de gestão e propor soluções
- Criar estimativas de esforço e custo
- Melhorar processos e produtividade
Responda de forma clara, objetiva e prática. Use exemplos quando apropriado.`
}
];
for (const msg of conversationHistory) {
if (msg.role === "user" || msg.role === "assistant") {
messages.push({ role: msg.role, content: msg.content });
}
}
messages.push({ role: "user", content: message });
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages,
max_tokens: 2048,
temperature: 0.7,
});
const responseContent = completion.choices[0]?.message?.content || "Desculpe, não consegui gerar uma resposta.";
res.json({ response: responseContent });
} catch (error) {
console.error("Error in agent chat:", error);
res.status(500).json({ error: "Failed to process chat request" });
}
});
// Save Manus result to conversation history (idempotent via runId)
app.post("/api/conversations/:id/manus-result", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const conversationId = parseInt(req.params.id);
const userId = req.user!.id;
const { userMessage, assistantMessage, runId, includeUserMessage } = req.body;
const conversation = await chatStorage.getConversation(conversationId);
if (!conversation) {
return res.status(404).json({ error: "Conversation not found" });
}
if (conversation.userId !== userId) {
return res.status(403).json({ error: "Access denied" });
}
// Check if this runId was already saved (idempotency) using metadata pattern
const existingMessages = await chatStorage.getMessagesByConversation(conversationId);
const alreadySaved = existingMessages.some(m =>
m.role === "assistant" && m.content?.startsWith(assistantMessage?.substring(0, 100))
);
if (alreadySaved && runId) {
return res.json({ success: true, alreadySaved: true });
}
// Only save user message if explicitly requested (for new conversations)
if (includeUserMessage && userMessage) {
await chatStorage.createMessage(conversationId, "user", userMessage);
}
// Save assistant message (clean, no markers)
const savedMessage = await chatStorage.createMessage(
conversationId,
"assistant",
assistantMessage
);
res.json({ success: true, messageId: savedMessage.id });
} catch (error) {
console.error("Error saving Manus result:", error);
res.status(500).json({ error: "Failed to save result" });
}
});
// Unified Agent endpoint - auto-detects if message needs tools (Manus) or simple chat
app.post("/api/agent/unified", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { content, conversationId, attachedFiles } = req.body;
const userId = req.user!.id;
if (!content || typeof content !== "string") {
return res.status(400).json({ error: "Content is required" });
}
// Keywords that indicate the message needs Manus tools
const toolKeywords = [
// Actions
'gerar relatório', 'criar relatório', 'exportar', 'enviar para',
'pesquisar', 'buscar na web', 'calcular', 'analisar dados',
'comparar com', 'pesquisa de mercado', 'agendar', 'criar gráfico',
// ERP/BI
'consultar erp', 'consulta erp', 'dados do erp', 'vendas', 'estoque',
'notas fiscais', 'clientes', 'fornecedores', 'financeiro',
'dataset', 'bi', 'dashboard',
// Knowledge
'base de conhecimento', 'documentos', 'aprender',
// Excel
'excel', 'planilha', 'xlsx',
// Complex analysis with files
'analisar arquivo', 'processar arquivo', 'extrair dados'
];
// Check if message contains tool keywords or has attached files
const contentLower = content.toLowerCase();
const needsTools = toolKeywords.some(kw => contentLower.includes(kw)) ||
(attachedFiles && attachedFiles.length > 0 &&
(contentLower.includes('analis') || contentLower.includes('process') ||
contentLower.includes('relat') || contentLower.includes('export')));
res.json({
mode: needsTools ? 'manus' : 'chat',
needsTools
});
} catch (error) {
console.error("Error in unified agent:", error);
res.status(500).json({ error: "Failed to process request" });
}
});
}