arcadiasuite/server/manus/service.ts

3955 lines
164 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import OpenAI from "openai";
import { db } from "../../db/index";
import { manusRuns, manusSteps, knowledgeBase, erpConnections, dataSources, biDatasets, biCharts, biDashboards, biDashboardCharts, backupJobs, finAccountsPayable, finAccountsReceivable, finBankAccounts, finTransactions, finCashFlowCategories, erpCustomers, erpSuppliers, customMcpServers, posSales, mobileDevices, deviceEvaluations, serviceOrders, retailStores, retailSellers, retailCommissionClosures, customerCredits } from "@shared/schema";
import { sql } from "drizzle-orm";
import { eq, desc, ilike, and, gte, lte, or } from "drizzle-orm";
import { MANUS_TOOLS, getToolsDescription, type ToolResult } from "./tools";
import { EventEmitter } from "events";
import { PDFParse } from "pdf-parse";
import { learningService } from "../learning/service";
import * as erpnextService from "../erpnext/service";
const openai = new OpenAI({
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
timeout: 30000,
maxRetries: 3,
});
const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, um assistente empresarial inteligente e proativo.
Você executa tarefas usando as ferramentas disponíveis.
Você opera em ciclos de pensamento-ação:
1. PENSAMENTO: Analise a situação e decida o próximo passo
2. AÇÃO: Execute uma ferramenta
3. OBSERVAÇÃO: Analise o resultado
4. Repita até completar a tarefa
FERRAMENTAS DISPONÍVEIS:
${getToolsDescription()}
REGRAS DE AUTONOMIA:
- Para ANÁLISES e CONSULTAS: seja proativo e execute sem pedir confirmação
- Para GERAÇÃO DE CÓDIGO: execute o código e apresente o resultado
- Se uma ferramenta falhar, tente uma alternativa ou apresente o que conseguiu
- NUNCA fique "aguardando resposta" no meio da tarefa - complete sempre
- Se não conseguir gerar um gráfico visualmente, forneça os dados em formato de tabela
- Para AÇÕES DESTRUTIVAS (deletar, modificar dados críticos): informe o que será feito na resposta final
- Sempre complete a tarefa e apresente o resultado ao final
- Máximo de 10 passos por execução
COMPORTAMENTO IMPORTANTE:
- Quando o usuário pedir análise de dados, faça uma análise COMPLETA e PROFISSIONAL
- Sempre forneça insights e interpretações, não apenas os números brutos
- Calcule variações percentuais, identifique tendências e faça observações relevantes
- Apresente dados em TABELAS FORMATADAS usando Markdown quando apropriado
FORMATO DE RESPOSTA IDEAL:
1. Primeiro, apresente uma TABELA com os dados extraídos (use formato Markdown: | Coluna | Valor |)
2. Em seguida, forneça uma ANÁLISE explicativa com insights (variações %, tendências, observações)
3. Por fim, gere um GRÁFICO visual usando generate_chart
CRIAÇÃO DE GRÁFICOS:
- Use generate_chart para criar gráficos visuais (NÃO use python_execute)
- Tipos disponíveis: bar (barras), line (linha), pie (pizza), area (área)
- Formate os dados como JSON array: [{"name":"2023","ativo":10844216,"passivo":10844216}]
- Inclua múltiplas séries quando fizer sentido (ex: ativo E passivo no mesmo gráfico)
EXEMPLO DE RESPOSTA COMPLETA:
1. analyze_file -> extrair dados do documento
2. generate_chart -> criar gráfico visual
3. finish -> apresentar tabela + análise + conclusão
Sempre calcule e mencione:
- Variações percentuais entre períodos
- Tendências (crescimento/queda)
- Observações sobre equilíbrio contábil quando aplicável
PESQUISA INTELIGENTE:
- Para PESQUISA PROFUNDA sobre um tema: use deep_research (busca, extrai e sintetiza múltiplas fontes)
- Para APRENDER conteúdo de uma URL específica: use learn_url
- Para BUSCAR no conhecimento já aprendido: use semantic_search PRIMEIRO
- Para NAVEGAR e extrair conteúdo de uma página: use web_browse
ESTRATÉGIA DE PESQUISA (siga esta ordem):
1. PRIMEIRO: Consulte semantic_search para ver se já temos informações sobre o assunto
2. SE não houver informações: Use deep_research para pesquisar na web e aprender
3. SE o usuário fornecer uma URL específica: Use web_browse ou learn_url
4. SEMPRE sintetize e apresente uma resposta completa
REGRAS DE PESQUISA:
- Quando o usuário pedir para "pesquisar sobre X", use deep_research
- Quando o usuário mencionar URLs, use web_browse para ver ou learn_url para salvar
- Seja PROATIVO: se não encontrar na base interna, pesquise na web automaticamente
- NUNCA diga "não consigo acessar" - sempre tente as ferramentas disponíveis
MÓDULO DE BI (ARCÁDIA INSIGHTS):
- Use bi_stats para ver estatísticas gerais do BI
- Use bi_list_tables para listar tabelas disponíveis no banco
- Use bi_get_table_columns para ver colunas de uma tabela
- Use bi_create_dataset para criar consultas SQL ou selecionar tabelas
- Use bi_execute_query para executar um dataset e obter dados
- Use bi_create_chart para criar gráficos persistentes no BI
- Use bi_create_dashboard para organizar gráficos em painéis
- Os recursos do BI ficam salvos permanentemente no sistema
COMUNICAÇÃO ENTRE AGENTES (A2A - Agent to Agent):
- Use list_agents para ver agentes externos disponíveis
- Use register_agent para adicionar um novo agente externo
- Use discover_agent para descobrir capacidades de um agente via Agent Card
- Use call_agent para enviar mensagens e delegar tarefas a outros agentes
- Você pode orquestrar múltiplos agentes para tarefas complexas
- Agentes podem ter especializações (fiscal, jurídico, vendas, etc.)
ESTRATÉGIA DE ORQUESTRAÇÃO:
- Para tarefas especializadas: verifique se há um agente especialista registrado
- Para tarefas complexas: divida em subtarefas e delegue para agentes apropriados
- Sempre sintetize as respostas dos agentes antes de apresentar ao usuário
Responda SEMPRE em formato JSON:
{
"thought": "Seu raciocínio sobre o próximo passo",
"tool": "nome_da_ferramenta",
"tool_input": { "param1": "valor1" }
}
Quando concluir, use:
{
"thought": "Raciocínio final",
"tool": "finish",
"tool_input": { "answer": "Resposta final COMPLETA com TODOS os dados e análise" }
}
REGRA CRÍTICA PARA RESPOSTA FINAL:
- A resposta no campo "answer" deve conter TODO o conteúdo da análise
- NUNCA diga apenas "relatório gerado com sucesso" - inclua o conteúdo completo
- Inclua: tabelas de dados, cálculos, variações percentuais, insights e conclusões
- O usuário quer ver a análise completa, não apenas uma confirmação
- Se analisou um documento, inclua os dados extraídos E sua interpretação`;
class ManusService extends EventEmitter {
private async executeTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> {
try {
switch (tool) {
case "web_search":
return this.toolWebSearch(input.query);
case "knowledge_query":
return this.toolKnowledgeQuery(input.query);
case "erp_query":
return this.toolErpQuery(input.entity, input.filter, userId);
case "calculate":
return this.toolCalculate(input.expression);
case "send_message":
return this.toolSendMessage(input.to, input.message);
case "generate_report":
return this.toolGenerateReport(input.title, input.type, input.data);
case "schedule_task":
return this.toolScheduleTask(input.task, input.when);
case "web_browse":
return this.toolWebBrowse(input.url, input.extract);
case "analyze_file":
return this.toolAnalyzeFile(input.filename, input.question, input.attachedFiles);
case "crm_query":
return this.toolCrmQuery(input.entity, input.filter, userId);
case "crm_create_event":
return this.toolCrmCreateEvent(input.title, input.type, input.startAt, input.description, userId);
case "crm_send_whatsapp":
return this.toolCrmSendWhatsapp(input.phone, input.message, userId);
case "python_execute":
return this.toolPythonExecute(input.code, input.timeout);
case "semantic_search":
return this.toolSemanticSearch(input.query, input.n_results);
case "read_file":
return this.toolReadFile(input.path);
case "write_file":
return this.toolWriteFile(input.path, input.content);
case "list_files":
return this.toolListFiles(input.path);
case "shell":
return this.toolShell(input.command);
case "ask_human":
return this.toolAskHuman(input.prompt, input.options);
case "analyze_data":
return this.toolAnalyzeData(input.data);
case "add_to_knowledge":
return this.toolAddToKnowledge(input.doc_id, input.content, input.type);
case "run_workflow":
return this.toolRunWorkflow(input.workflow_type, input.data);
case "rpa_execute":
return this.toolRpaExecute(input.template, input.params);
case "fiscal_emit":
return this.toolFiscalEmit(input.type, input.data);
case "fiscal_query":
return this.toolFiscalQuery(input.chave);
case "plus_clientes":
return this.toolPlusClientes(input.filtro);
case "plus_produtos":
return this.toolPlusProdutos(input.filtro);
case "plus_vendas":
return this.toolPlusVendas(input.filtro);
case "plus_estoque":
return this.toolPlusEstoque();
case "plus_financeiro":
return this.toolPlusFinanceiro(input.tipo);
case "plus_fornecedores":
return this.toolPlusFornecedores(input.filtro);
case "plus_dashboard":
return this.toolPlusDashboard();
case "learn_url":
return this.toolLearnUrl(input.url, input.priority);
case "deep_research":
return this.toolDeepResearch(input.topic, input.depth);
case "detect_opportunities":
return this.toolDetectOpportunities(input.domain, input.min_confidence);
case "scientist_generate_code":
return this.toolScientistGenerateCode(input.goal, input.data_description);
case "scientist_generate_automation":
return this.toolScientistGenerateAutomation(input.task);
case "scientist_detect_patterns":
return this.toolScientistDetectPatterns(input.data);
case "scientist_suggest_improvements":
return this.toolScientistSuggestImprovements(input.data);
case "generate_chart":
return this.toolGenerateChart(input.type, input.title, input.data, input.xKey, input.yKeys, input.colors);
case "bi_stats":
return this.toolBiStats(userId);
case "bi_list_data_sources":
return this.toolBiListDataSources(userId);
case "bi_create_data_source":
return this.toolBiCreateDataSource(userId, input.name, input.type, input.host, input.port, input.database, input.username, input.password);
case "bi_list_datasets":
return this.toolBiListDatasets(userId);
case "bi_create_dataset":
return this.toolBiCreateDataset(userId, input.name, input.type, input.dataSourceId, input.query, input.tableName);
case "bi_execute_query":
return this.toolBiExecuteQuery(userId, input.datasetId);
case "bi_list_charts":
return this.toolBiListCharts(userId);
case "bi_create_chart":
return this.toolBiCreateChart(userId, input.name, input.type, input.datasetId, input.config);
case "bi_list_dashboards":
return this.toolBiListDashboards(userId);
case "bi_create_dashboard":
return this.toolBiCreateDashboard(userId, input.name, input.description);
case "bi_add_chart_to_dashboard":
return this.toolBiAddChartToDashboard(userId, input.dashboardId, input.chartId, input.position);
case "bi_list_tables":
return this.toolBiListTables();
case "bi_get_table_columns":
return this.toolBiGetTableColumns(input.tableName);
case "bi_analyze_with_pandas":
return this.toolBiAnalyzeWithPandas(input.datasetId, input.question, userId);
case "metaset_query":
return this.toolMetaSetQuery(input.query, input.limit);
case "metaset_create_question":
return this.toolMetaSetCreateQuestion(input.name, input.query, input.chartType, input.description);
case "metaset_list_questions":
return this.toolMetaSetListQuestions();
case "metaset_run_question":
return this.toolMetaSetRunQuestion(input.questionId);
case "metaset_create_dashboard":
return this.toolMetaSetCreateDashboard(input.name, input.description);
case "metaset_list_dashboards":
return this.toolMetaSetListDashboards();
case "metaset_add_to_dashboard":
return this.toolMetaSetAddToDashboard(input.dashboardId, input.questionId);
case "metaset_suggest_analysis":
return this.toolMetaSetSuggestAnalysis(input.tableName);
case "fin_query_payables":
return this.toolFinQueryPayables(userId, input.status, input.supplier_id);
case "fin_query_receivables":
return this.toolFinQueryReceivables(userId, input.status, input.customer_id);
case "fin_query_bank_accounts":
return this.toolFinQueryBankAccounts(userId, input.active_only);
case "fin_query_cashflow":
return this.toolFinQueryCashflow(userId, input.start_date, input.end_date, input.account_id);
case "fin_summary":
return this.toolFinSummary(userId);
case "fin_register_payable":
return this.toolFinRegisterPayable(userId, input.supplier_id, input.description, input.amount, input.due_date, input.category_id);
case "fin_register_receivable":
return this.toolFinRegisterReceivable(userId, input.customer_id, input.description, input.amount, input.due_date, input.category_id);
case "fin_pay_account":
return this.toolFinPayAccount(userId, input.payable_id, input.bank_account_id, input.payment_date, input.amount_paid);
case "fin_receive_account":
return this.toolFinReceiveAccount(userId, input.receivable_id, input.bank_account_id, input.receive_date, input.amount_received);
case "call_agent":
return this.toolCallAgent(input.agent_id, input.message, input.wait_response);
case "list_agents":
return this.toolListAgents();
case "register_agent":
return this.toolRegisterAgent(input.name, input.url, input.api_key);
case "discover_agent":
return this.toolDiscoverAgent(input.url);
case "export_to_excel":
return this.toolExportToExcel(input.data, input.filename, input.sheet_name);
case "send_to_bi_dataset":
return this.toolSendToBiDataset(userId, input.name, input.data, input.description, input.update_if_exists);
case "market_research":
return this.toolMarketResearch(input.topic, input.company_context, input.focus);
case "compare_with_market":
return this.toolCompareWithMarket(input.metric, input.value, input.sector, input.period);
// ERPNext tools
case "erpnext_status":
return this.toolErpnextStatus();
case "erpnext_list_doctypes":
return this.toolErpnextListDoctypes(input.limit);
case "erpnext_get_documents":
return this.toolErpnextGetDocuments(input.doctype, input.filters, input.fields, input.limit);
case "erpnext_get_document":
return this.toolErpnextGetDocument(input.doctype, input.name);
case "erpnext_create_document":
return this.toolErpnextCreateDocument(input.doctype, input.data);
case "erpnext_update_document":
return this.toolErpnextUpdateDocument(input.doctype, input.name, input.data);
case "erpnext_search":
return this.toolErpnextSearch(input.doctype, input.search_term, input.limit);
case "erpnext_run_report":
return this.toolErpnextRunReport(input.report_name, input.filters);
case "erpnext_call_method":
return this.toolErpnextCallMethod(input.method, input.args);
// ========== ARCÁDIA RETAIL TOOLS ==========
case "retail_query":
return this.toolRetailQuery(input.entity, input.filter, input.limit);
case "retail_stats":
return this.toolRetailStats(input.period, input.storeId);
case "retail_report":
return this.toolRetailReport(input.type, input.dateFrom, input.dateTo, input.storeId);
case "finish":
let finishOutput = input.answer || "";
if (input.chart) {
try {
const chartData = typeof input.chart === 'string' ? JSON.parse(input.chart) : input.chart;
finishOutput = `__CHART_DATA__${JSON.stringify(chartData)}__END_CHART_DATA__\n\n${finishOutput}`;
} catch (e) {
// ignore invalid chart data
}
}
return { success: true, output: finishOutput };
default:
// Check if it's a custom MCP tool (format: mcp_servername_toolname)
if (tool.startsWith("mcp_")) {
return this.executeCustomMcpTool(tool, input, userId);
}
return { success: false, output: "", error: `Ferramenta desconhecida: ${tool}` };
}
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async executeCustomMcpTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> {
try {
// Parse tool name: mcp_servername_toolname
const parts = tool.split("_");
if (parts.length < 3) {
return { success: false, output: "", error: "Invalid MCP tool format" };
}
const serverName = parts[1];
const toolName = parts.slice(2).join("_");
// Find the custom MCP server
const [server] = await db.select().from(customMcpServers)
.where(and(
eq(customMcpServers.userId, userId),
eq(customMcpServers.name, serverName),
eq(customMcpServers.isActive, 1)
));
if (!server) {
return { success: false, output: "", error: `MCP server not found: ${serverName}` };
}
if (server.transportType === "http" && server.serverUrl) {
// Execute HTTP MCP tool
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (server.customHeaders && typeof server.customHeaders === 'object') {
Object.assign(headers, server.customHeaders);
}
const response = await fetch(`${server.serverUrl}/tools/${toolName}/execute`, {
method: 'POST',
headers,
body: JSON.stringify(input)
});
if (response.ok) {
const result = await response.json();
return {
success: true,
output: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
};
} else {
return {
success: false,
output: "",
error: `MCP tool execution failed: ${response.status}`
};
}
}
return { success: false, output: "", error: "STDIO MCP servers not yet supported" };
} catch (error: any) {
return { success: false, output: "", error: `MCP tool error: ${error.message}` };
}
}
private async toolWebSearch(query: string): Promise<ToolResult> {
const queryLower = query.toLowerCase();
if (queryLower.includes('dólar') || queryLower.includes('dolar') || queryLower.includes('usd') ||
queryLower.includes('cotação') || queryLower.includes('câmbio') || queryLower.includes('cambio')) {
try {
const currencyData = await this.fetchCurrencyData();
if (currencyData) {
return {
success: true,
output: `🔍 Cotação do Dólar (Fonte: Banco Central do Brasil):\n\n${currencyData}`
};
}
} catch (e) {
console.error("Currency fetch error:", e);
}
}
try {
const searchResults = await this.performWebSearch(query);
if (searchResults.length > 0) {
let output = `🔍 Resultados da busca para "${query}":\n\n`;
searchResults.forEach((result, i) => {
output += `${i + 1}. **${result.title}**\n`;
output += ` ${result.snippet}\n`;
output += ` URL: ${result.url}\n\n`;
});
output += `\n💡 Dica: Use learn_url com uma das URLs acima para aprender o conteúdo completo.`;
return { success: true, output };
}
} catch (e) {
console.error("Web search error:", e);
}
return {
success: true,
output: `🔍 Busca para "${query}":\n\nNão encontrei resultados específicos. Tente:\n- Usar termos mais específicos\n- Fornecer uma URL direta com learn_url\n- Consultar a base de conhecimento com semantic_search`
};
}
private async performWebSearch(query: string): Promise<Array<{title: string, url: string, snippet: string}>> {
const results: Array<{title: string, url: string, snippet: string}> = [];
try {
const encodedQuery = encodeURIComponent(query);
const response = await fetch(`https://html.duckduckgo.com/html/?q=${encodedQuery}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml',
'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'
}
});
if (response.ok) {
const html = await response.text();
const titleMatches = html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi);
const snippetMatches = html.matchAll(/<a[^>]*class="result__snippet"[^>]*>([^<]*(?:<[^>]*>[^<]*)*)<\/a>/gi);
const titles = [...titleMatches];
const snippets = [...snippetMatches];
for (let i = 0; i < Math.min(titles.length, 5); i++) {
let url = titles[i][1];
if (url.includes('uddg=')) {
const match = url.match(/uddg=([^&]*)/);
if (match) url = decodeURIComponent(match[1]);
}
results.push({
title: titles[i][2].replace(/<[^>]*>/g, '').trim(),
url: url,
snippet: snippets[i] ? snippets[i][1].replace(/<[^>]*>/g, '').trim().substring(0, 200) : ''
});
}
}
} catch (e) {
console.error("DuckDuckGo search failed:", e);
}
return results;
}
private async fetchCurrencyData(): Promise<string | null> {
try {
const today = new Date();
const tenDaysAgo = new Date(today.getTime() - 10 * 24 * 60 * 60 * 1000);
const formatDate = (d: Date) => {
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
const yyyy = d.getFullYear();
return `${mm}-${dd}-${yyyy}`;
};
const startDate = formatDate(tenDaysAgo);
const endDate = formatDate(today);
const url = `https://olinda.bcb.gov.br/olinda/servico/PTAX/versao/v1/odata/CotacaoDolarPeriodo(dataInicial=@dataInicial,dataFinalCotacao=@dataFinalCotacao)?@dataInicial='${startDate}'&@dataFinalCotacao='${endDate}'&$format=json&$orderby=dataHoraCotacao%20desc`;
const response = await fetch(url, {
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
if (!data.value || data.value.length === 0) {
return "Não há cotações disponíveis para o período solicitado.";
}
let output = "💵 Cotações do Dólar (USD/BRL) - Últimos dias:\n\n";
output += "| Data | Compra | Venda |\n";
output += "|------|--------|-------|\n";
const uniqueDates = new Map<string, any>();
for (const item of data.value) {
const date = new Date(item.dataHoraCotacao).toLocaleDateString('pt-BR');
if (!uniqueDates.has(date)) {
uniqueDates.set(date, item);
}
}
Array.from(uniqueDates.entries()).slice(0, 10).forEach(([date, item]) => {
output += `| ${date} | R$ ${item.cotacaoCompra.toFixed(4)} | R$ ${item.cotacaoVenda.toFixed(4)} |\n`;
});
const latest = data.value[0];
output += `\n📊 Última cotação: R$ ${latest.cotacaoVenda.toFixed(4)} (venda)`;
output += `\n📅 Atualizado em: ${new Date(latest.dataHoraCotacao).toLocaleString('pt-BR')}`;
output += `\n🏦 Fonte: Banco Central do Brasil (PTAX)`;
return output;
} catch (error: any) {
console.error("BCB API error:", error);
return null;
}
}
private async toolKnowledgeQuery(query: string): Promise<ToolResult> {
const results = await db.select()
.from(knowledgeBase)
.where(ilike(knowledgeBase.title, `%${query}%`))
.limit(5);
if (results.length === 0) {
const contentResults = await db.select()
.from(knowledgeBase)
.where(ilike(knowledgeBase.content, `%${query}%`))
.limit(5);
if (contentResults.length === 0) {
return { success: true, output: "Nenhum documento encontrado na base de conhecimento para essa consulta." };
}
const output = contentResults.map(doc =>
`📄 ${doc.title} (${doc.category})\nAutor: ${doc.author || 'Não especificado'}\n${doc.content?.substring(0, 200)}...`
).join('\n\n');
return { success: true, output: `Documentos encontrados:\n\n${output}` };
}
const output = results.map(doc =>
`📄 ${doc.title} (${doc.category})\nAutor: ${doc.author || 'Não especificado'}\n${doc.content?.substring(0, 200)}...`
).join('\n\n');
return { success: true, output: `Documentos encontrados:\n\n${output}` };
}
private async toolErpQuery(entity: string, filter: string | undefined, userId: string): Promise<ToolResult> {
const connections = await db.select()
.from(erpConnections)
.where(eq(erpConnections.isActive, "true"))
.limit(1);
if (connections.length === 0) {
return { success: false, output: "", error: "Nenhuma conexão ERP ativa encontrada." };
}
return {
success: true,
output: `[Consulta ERP - ${connections[0].name}]\nEntidade: ${entity}\nFiltro: ${filter || 'nenhum'}\n\nDados simulados:\n- Total de registros: 150\n- Registros ativos: 142\n- Última atualização: hoje\n\nNota: Para dados reais, a conexão ERP deve estar configurada com credenciais válidas.`
};
}
private async toolCalculate(expression: string): Promise<ToolResult> {
try {
const sanitized = expression.replace(/[^0-9+\-*/.()%\s]/g, '');
if (sanitized && /^[\d+\-*/.()%\s]+$/.test(sanitized)) {
const result = Function(`'use strict'; return (${sanitized})`)();
return { success: true, output: `Resultado: ${result}` };
}
return {
success: true,
output: `Análise de: "${expression}"\n\nPara cálculos complexos, forneça expressões numéricas específicas.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro no cálculo: ${error.message}` };
}
}
private async toolSendMessage(to: string, message: string): Promise<ToolResult> {
return {
success: true,
output: `Mensagem enviada para ${to}: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`
};
}
private async toolGenerateReport(title: string, type: string, data: string): Promise<ToolResult> {
// The data field should contain the full report content - return it directly
const reportContent = data || "Nenhum dado disponível para o relatório.";
return {
success: true,
output: `📊 **${title}**\n\n${reportContent}`
};
}
private async toolScheduleTask(task: string, when: string): Promise<ToolResult> {
return {
success: true,
output: `✅ Tarefa agendada\n\nDescrição: ${task}\nQuando: ${when}\n\n[Você será notificado quando a tarefa for executada]`
};
}
private async toolWebBrowse(url: string, extract?: string): Promise<ToolResult> {
try {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return { success: false, output: "", error: "URL inválida. Use http:// ou https://" };
}
const urlObj = new URL(url);
const hostname = urlObj.hostname.toLowerCase();
if (hostname.includes('bcb.gov.br') || hostname.includes('banco central')) {
const currencyData = await this.fetchCurrencyData();
if (currencyData) {
return { success: true, output: currencyData };
}
}
const { spawn } = await import('child_process');
const path = await import('path');
const captureScript = path.join(process.cwd(), 'python-service', 'scripts', 'run_capture.py');
const proc = spawn('python3', [captureScript, url]);
const captureResult: any = await new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
proc.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
proc.on('close', (code: number) => {
if (code === 0 && stdout) {
try {
resolve(JSON.parse(stdout.trim()));
} catch (e) {
reject(new Error(`Failed to parse output`));
}
} else {
reject(new Error(stderr || `Process exited with code ${code}`));
}
});
proc.on('error', (err: Error) => reject(err));
setTimeout(() => {
proc.kill();
reject(new Error('Capture timeout'));
}, 30000);
});
if (!captureResult.success) {
return { success: false, output: "", error: captureResult.error || "Falha ao acessar a página" };
}
const textContent = captureResult.text_content || '';
const title = captureResult.title || hostname;
if (extract === 'summary') {
const summary = textContent.substring(0, 2000);
return {
success: true,
output: `🌐 **${title}**\n\n${summary}${textContent.length > 2000 ? '\n\n...(conteúdo truncado)' : ''}`
};
} else if (extract === 'links') {
const links = captureResult.links?.slice(0, 10) || [];
let output = `🔗 Links encontrados em ${title}:\n\n`;
links.forEach((link: any, i: number) => {
output += `${i + 1}. ${link.text} - ${link.href}\n`;
});
return { success: true, output };
} else {
const headings = captureResult.headings?.slice(0, 10) || [];
let output = `🌐 **${title}**\n\n`;
if (headings.length > 0) {
output += `📑 Estrutura:\n`;
headings.forEach((h: any) => {
output += ` ${h.level}: ${h.text}\n`;
});
output += `\n`;
}
output += `📝 Conteúdo (${captureResult.word_count || 0} palavras):\n\n`;
output += textContent.substring(0, 3000);
if (textContent.length > 3000) {
output += `\n\n...(${textContent.length - 3000} caracteres adicionais)`;
}
return { success: true, output };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao acessar página: ${error.message}` };
}
}
private async toolAnalyzeFile(filename: string, question?: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>): Promise<ToolResult> {
if (!attachedFiles || attachedFiles.length === 0) {
return { success: false, output: "", error: "Nenhum arquivo anexado para análise" };
}
const file = attachedFiles.find(f => f.name.toLowerCase() === filename.toLowerCase());
if (!file) {
const availableFiles = attachedFiles.map(f => f.name).join(', ');
return { success: false, output: "", error: `Arquivo "${filename}" não encontrado. Arquivos disponíveis: ${availableFiles}` };
}
let fileContent = file.content;
if (file.name.toLowerCase().endsWith('.pdf') && file.base64) {
try {
const pdfBuffer = Buffer.from(file.base64, 'base64');
const parser = new PDFParse({ data: pdfBuffer });
const result = await parser.getText();
fileContent = result.text;
await parser.destroy();
} catch (pdfError: any) {
return { success: false, output: "", error: `Erro ao processar PDF: ${pdfError.message}` };
}
}
const preview = fileContent.substring(0, 3000);
const truncated = fileContent.length > 3000;
let output = `📄 Análise do arquivo: ${file.name}\n\n`;
output += `Tamanho: ${fileContent.length} caracteres\n\n`;
output += `Conteúdo${truncated ? ' (primeiros 3000 caracteres)' : ''}:\n${preview}`;
if (question) {
output += `\n\n❓ Pergunta: ${question}\n[Analise o conteúdo acima para responder]`;
}
return { success: true, output };
}
private async toolCrmQuery(entity: string, filter?: string, userId?: string): Promise<ToolResult> {
try {
if (!userId) {
return { success: false, output: "", error: "Usuário não identificado" };
}
const { crmStorage } = await import("../crm/storage");
const { compassStorage } = await import("../compass/storage");
const userTenants = await compassStorage.getUserTenants(userId);
const tenantIds = userTenants.map(t => t.id);
if (tenantIds.length === 0 && entity.toLowerCase() !== "events") {
return { success: false, output: "", error: "Usuário não pertence a nenhum tenant" };
}
let data: any;
switch (entity.toLowerCase()) {
case "partners":
const allPartners = await crmStorage.getPartners();
data = allPartners.filter(p => !p.tenantId || tenantIds.includes(p.tenantId));
break;
case "contracts":
const allContracts = await crmStorage.getContracts();
data = allContracts.filter(c => !c.tenantId || tenantIds.includes(c.tenantId));
break;
case "threads":
const allThreads = await crmStorage.getThreads();
data = allThreads.filter(t => !t.tenantId || tenantIds.includes(t.tenantId));
break;
case "events":
data = await crmStorage.getEvents(userId);
break;
case "stats":
data = await crmStorage.getStats();
break;
default:
return { success: false, output: "", error: `Entidade CRM desconhecida: ${entity}. Use: partners, contracts, threads, events, stats` };
}
if (filter && Array.isArray(data)) {
const [key, value] = filter.split("=");
if (key && value) {
data = data.filter((item: any) =>
String(item[key]).toLowerCase() === value.toLowerCase()
);
}
}
const output = `📊 CRM - ${entity.toUpperCase()}:\n\n${JSON.stringify(data, null, 2)}`;
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao consultar CRM: ${error.message}` };
}
}
private async toolCrmCreateEvent(title: string, type: string, startAt: string, description?: string, userId?: string): Promise<ToolResult> {
try {
if (!userId) {
return { success: false, output: "", error: "Usuário não identificado" };
}
let parsedDate: Date;
const lowerStart = startAt.toLowerCase();
if (lowerStart.includes("amanhã") || lowerStart.includes("tomorrow")) {
parsedDate = new Date();
parsedDate.setDate(parsedDate.getDate() + 1);
const timeMatch = lowerStart.match(/(\d{1,2})[h:]?(\d{0,2})?/);
if (timeMatch) {
parsedDate.setHours(parseInt(timeMatch[1]), parseInt(timeMatch[2] || "0"), 0, 0);
} else {
parsedDate.setHours(10, 0, 0, 0);
}
} else if (lowerStart.includes("hoje") || lowerStart.includes("today")) {
parsedDate = new Date();
const timeMatch = lowerStart.match(/(\d{1,2})[h:]?(\d{0,2})?/);
if (timeMatch) {
parsedDate.setHours(parseInt(timeMatch[1]), parseInt(timeMatch[2] || "0"), 0, 0);
} else {
parsedDate.setHours(new Date().getHours() + 1, 0, 0, 0);
}
} else {
parsedDate = new Date(startAt);
if (isNaN(parsedDate.getTime())) {
return { success: false, output: "", error: `Data inválida: ${startAt}` };
}
}
const { crmStorage } = await import("../crm/storage");
const { googleCalendarService } = await import("../crm/google-calendar");
const event = await crmStorage.createEvent({
userId,
title,
description: description || null,
type: type as any,
startAt: parsedDate,
status: "scheduled"
});
let googleSynced = false;
try {
const isConnected = await googleCalendarService.isConnected(userId);
if (isConnected) {
const googleEventId = await googleCalendarService.createEvent(userId, event);
if (googleEventId) {
await crmStorage.updateEvent(event.id, { googleEventId });
googleSynced = true;
}
}
} catch (e) {
console.error("Failed to sync event to Google Calendar:", e);
}
return {
success: true,
output: `✅ Evento criado com sucesso!\n\n📅 ${title}\n🕐 ${parsedDate.toLocaleString("pt-BR")}\n📌 Tipo: ${type}${description ? `\n📝 ${description}` : ""}${googleSynced ? "\n🔄 Sincronizado com Google Calendar" : ""}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao criar evento: ${error.message}` };
}
}
private async toolCrmSendWhatsapp(phone: string, message: string, userId?: string): Promise<ToolResult> {
try {
if (!userId) {
return { success: false, output: "", error: "Usuário não identificado" };
}
const { communicationService } = await import("../crm/communication");
const { crmStorage } = await import("../crm/storage");
const { compassStorage } = await import("../compass/storage");
const userTenants = await compassStorage.getUserTenants(userId);
const tenantIds = userTenants.map(t => t.id);
const allChannels = await crmStorage.getChannels();
const userChannels = allChannels.filter(c => !c.tenantId || tenantIds.includes(c.tenantId));
const whatsappChannel = userChannels.find(c => c.type === "whatsapp" && c.status === "connected");
if (!whatsappChannel) {
return { success: false, output: "", error: "Nenhum canal WhatsApp conectado. Conecte um canal no CRM primeiro." };
}
const normalizedPhone = phone.replace(/\D/g, "");
const thread = await communicationService.getOrCreateThread(whatsappChannel.id, normalizedPhone);
const success = await communicationService.sendWhatsAppMessage(whatsappChannel.id, normalizedPhone, message);
if (success) {
await crmStorage.createMessage({
threadId: thread.id,
channelId: whatsappChannel.id,
direction: "outgoing",
type: "text",
content: message,
status: "sent",
sentById: "manus-agent"
});
return {
success: true,
output: `✅ Mensagem enviada com sucesso!\n\n📱 Para: ${normalizedPhone}\n💬 Mensagem: ${message}`
};
} else {
return { success: false, output: "", error: "Falha ao enviar mensagem WhatsApp" };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao enviar WhatsApp: ${error.message}` };
}
}
private async toolPythonExecute(code: string, timeout?: number): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const response = await fetch(`${pythonServiceUrl}/execute`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, timeout: timeout || 30 })
});
if (!response.ok) {
throw new Error(`Erro na API Python: ${response.status}`);
}
const result = await response.json();
if (result.success) {
return { success: true, output: `🐍 Resultado Python:\n\n${result.output}` };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao executar Python: ${error.message}` };
}
}
private async toolSemanticSearch(query: string, nResults?: number): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const response = await fetch(`${pythonServiceUrl}/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, n_results: nResults || 5 })
});
if (!response.ok) {
throw new Error(`Erro na busca semântica: ${response.status}`);
}
const result = await response.json();
if (result.success) {
return { success: true, output: `🔍 Busca Semântica para "${query}":\n\n${JSON.stringify(result.results, null, 2)}` };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro na busca semântica: ${error.message}` };
}
}
private async toolReadFile(path: string): Promise<ToolResult> {
try {
const fs = await import("fs/promises");
const safePath = path.replace(/\.\./g, "");
const content = await fs.readFile(safePath, "utf-8");
return { success: true, output: `📄 Conteúdo de ${path}:\n\n${content.substring(0, 10000)}${content.length > 10000 ? "\n\n... (truncado)" : ""}` };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao ler arquivo: ${error.message}` };
}
}
private async toolWriteFile(path: string, content: string): Promise<ToolResult> {
try {
const fs = await import("fs/promises");
const pathModule = await import("path");
const safePath = path.replace(/\.\./g, "");
await fs.mkdir(pathModule.dirname(safePath), { recursive: true });
await fs.writeFile(safePath, content, "utf-8");
return { success: true, output: `✅ Arquivo salvo: ${path}` };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao escrever arquivo: ${error.message}` };
}
}
private async toolListFiles(path: string): Promise<ToolResult> {
try {
const fs = await import("fs/promises");
const safePath = path.replace(/\.\./g, "") || ".";
const entries = await fs.readdir(safePath, { withFileTypes: true });
const files = entries.map(e => `${e.isDirectory() ? "📁" : "📄"} ${e.name}`).join("\n");
return { success: true, output: `📂 Conteúdo de ${path}:\n\n${files}` };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao listar arquivos: ${error.message}` };
}
}
private async toolShell(command: string): Promise<ToolResult> {
try {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
const blockedCommands = ["rm -rf", "rm -r /", "mkfs", "dd if=", ":(){ :|:& };:"];
if (blockedCommands.some(bc => command.includes(bc))) {
return { success: false, output: "", error: "Comando bloqueado por segurança" };
}
const { stdout, stderr } = await execAsync(command, { timeout: 30000 });
return { success: true, output: `💻 Resultado:\n\n${stdout}${stderr ? `\nErros:\n${stderr}` : ""}` };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao executar comando: ${error.message}` };
}
}
private async toolAskHuman(prompt: string, options?: string): Promise<ToolResult> {
this.emit("askHuman", { prompt, options: options?.split(",").map(o => o.trim()) });
return {
success: true,
output: `⏸️ Aguardando aprovação do usuário:\n\n${prompt}${options ? `\n\nOpções: ${options}` : ""}\n\n[A resposta do usuário será processada no próximo passo]`
};
}
private async toolAnalyzeData(dataString: string): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const data = JSON.parse(dataString);
const response = await fetch(`${pythonServiceUrl}/analyze`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data })
});
if (!response.ok) {
throw new Error(`Erro na análise: ${response.status}`);
}
const result = await response.json();
if (result.success) {
return { success: true, output: `📊 Análise de Dados:\n\n${JSON.stringify(result.analysis, null, 2)}` };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao analisar dados: ${error.message}` };
}
}
private async toolAddToKnowledge(docId: string, content: string, type: string): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const response = await fetch(`${pythonServiceUrl}/documents/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ doc_id: docId, document: content, metadata: { type } })
});
if (!response.ok) {
throw new Error(`Erro ao adicionar documento: ${response.status}`);
}
const result = await response.json();
if (result.success) {
return { success: true, output: `✅ Documento adicionado ao grafo de conhecimento:\n\nID: ${docId}\nTipo: ${type}` };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao adicionar documento: ${error.message}` };
}
}
private async toolLearnUrl(url: string, priority?: string): Promise<ToolResult> {
try {
const { spawn } = await import('child_process');
const path = await import('path');
const captureScript = path.join(process.cwd(), 'python-service', 'scripts', 'run_capture.py');
const proc = spawn('python3', [captureScript, url]);
const captureResult: any = await new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
proc.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
proc.on('close', (code: number) => {
if (code === 0 && stdout) {
try {
resolve(JSON.parse(stdout.trim()));
} catch (e) {
reject(new Error(`Failed to parse output: ${stdout}`));
}
} else {
reject(new Error(stderr || `Process exited with code ${code}`));
}
});
proc.on('error', (err: Error) => reject(err));
setTimeout(() => {
proc.kill();
reject(new Error('Capture timeout (45s)'));
}, 45000);
});
if (!captureResult.success) {
return { success: false, output: "", error: captureResult.error || "Falha na captura" };
}
const nodeId = `url_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const textContent = captureResult.text_content || '';
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
try {
await fetch(`${pythonServiceUrl}/documents/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
doc_id: nodeId,
document: textContent.substring(0, 10000),
metadata: {
type: 'url_learned',
url,
title: captureResult.title,
priority: priority || 'normal',
capturedAt: new Date().toISOString()
}
})
});
} catch (embeddingError) {
console.warn("Embedding service unavailable, continuing:", embeddingError);
}
return {
success: true,
output: `📚 URL aprendida e adicionada ao grafo de conhecimento:\n\n` +
`URL: ${url}\n` +
`Título: ${captureResult.title || 'N/A'}\n` +
`Caracteres extraídos: ${textContent.length}\n` +
`ID no grafo: ${nodeId}\n` +
`Prioridade: ${priority || 'normal'}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao aprender URL: ${error.message}` };
}
}
private async toolDeepResearch(topic: string, depth?: number): Promise<ToolResult> {
try {
const maxSources = Math.min(depth || 3, 5);
let output = `🔬 **Pesquisa Profunda: "${topic}"**\n\n`;
output += `📡 Buscando fontes...\n`;
const searchResults = await this.performWebSearch(topic);
if (searchResults.length === 0) {
return {
success: true,
output: `🔬 Pesquisa sobre "${topic}":\n\nNão encontrei resultados na web. Consultando base de conhecimento interna...`
};
}
output += `✅ Encontradas ${searchResults.length} fontes\n\n`;
const { spawn } = await import('child_process');
const path = await import('path');
const captureScript = path.join(process.cwd(), 'python-service', 'scripts', 'run_capture.py');
const extractedContent: Array<{title: string, url: string, content: string}> = [];
for (let i = 0; i < Math.min(searchResults.length, maxSources); i++) {
const result = searchResults[i];
output += `📖 Analisando: ${result.title}\n`;
try {
const proc = spawn('python3', [captureScript, result.url]);
const captureResult: any = await new Promise((resolve, reject) => {
let stdout = '';
proc.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
proc.on('close', (code: number) => {
if (code === 0 && stdout) {
try { resolve(JSON.parse(stdout.trim())); }
catch { resolve({ success: false }); }
} else { resolve({ success: false }); }
});
proc.on('error', () => resolve({ success: false }));
setTimeout(() => { proc.kill(); resolve({ success: false }); }, 20000);
});
if (captureResult.success && captureResult.text_content) {
extractedContent.push({
title: captureResult.title || result.title,
url: result.url,
content: captureResult.text_content.substring(0, 3000)
});
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const nodeId = `research_${Date.now()}_${i}`;
await fetch(`${pythonServiceUrl}/documents/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
doc_id: nodeId,
document: captureResult.text_content.substring(0, 10000),
metadata: { type: 'research', topic, url: result.url, title: result.title }
})
}).catch(() => {});
}
} catch (e) {
output += ` ⚠️ Não foi possível acessar\n`;
}
}
output += `\n---\n\n`;
output += `📚 **Síntese das ${extractedContent.length} fontes analisadas:**\n\n`;
for (const source of extractedContent) {
output += `### ${source.title}\n`;
output += `🔗 ${source.url}\n\n`;
output += source.content.substring(0, 1500);
output += `\n\n---\n\n`;
}
output += `\n💡 *${extractedContent.length} fontes foram adicionadas à base de conhecimento.*`;
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro na pesquisa profunda: ${error.message}` };
}
}
private async toolDetectOpportunities(domain: string, minConfidence?: number): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const response = await fetch(`${pythonServiceUrl}/detect_opportunities`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
domain,
min_confidence: minConfidence || 0.7
})
});
if (!response.ok) {
throw new Error(`Erro ao detectar oportunidades: ${response.status}`);
}
const result = await response.json();
if (result.success && result.opportunities) {
let output = `🎯 Oportunidades detectadas no domínio "${domain}":\n\n`;
for (const opp of result.opportunities) {
output += `${opp.name} (Confiança: ${(opp.confidence * 100).toFixed(0)}%)\n`;
output += ` ${opp.description || ''}\n\n`;
}
if (result.opportunities.length === 0) {
output = `Nenhuma oportunidade detectada no domínio "${domain}" com confiança acima de ${minConfidence || 0.7}`;
}
return { success: true, output };
} else {
return { success: false, output: "", error: result.error || "Sem oportunidades" };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao detectar oportunidades: ${error.message}` };
}
}
private async toolRunWorkflow(workflowType: string, dataString?: string): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const data = dataString ? JSON.parse(dataString) : {};
let spec: any;
if (workflowType.startsWith("{")) {
spec = JSON.parse(workflowType);
} else {
const templateResponse = await fetch(`${pythonServiceUrl}/workflow/templates/${workflowType}`);
if (!templateResponse.ok) {
throw new Error(`Template não encontrado: ${workflowType}`);
}
const templateResult = await templateResponse.json();
spec = templateResult.template;
}
const response = await fetch(`${pythonServiceUrl}/workflow/run`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ spec, data })
});
if (!response.ok) {
throw new Error(`Erro ao executar workflow: ${response.status}`);
}
const result = await response.json();
if (result.success) {
const workflowResult = result.result;
let output = `⚙️ Workflow Executado: ${spec.name || workflowType}\n\n`;
output += `Status: ${workflowResult.status}\n`;
output += `Passos executados: ${workflowResult.steps_executed?.length || 0}\n\n`;
if (workflowResult.steps_executed) {
output += "Passos:\n";
for (const step of workflowResult.steps_executed) {
output += ` ${step.status === "completed" ? "✅" : "❌"} ${step.step_id} (${step.type || "task"})\n`;
}
}
output += `\nDados finais: ${JSON.stringify(workflowResult.data, null, 2)}`;
return { success: true, output };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao executar workflow: ${error.message}` };
}
}
private async toolRpaExecute(template: string, paramsString?: string): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const params = paramsString ? JSON.parse(paramsString) : {};
let script: any;
if (template.startsWith("{")) {
script = JSON.parse(template);
} else {
const templates: Record<string, any> = {
login: {
name: "Login Template",
headless: true,
actions: [
{ type: "navigate", url: params.url || "https://example.com/login" },
{ type: "type", selector: params.username_selector || "#username", text: params.username || "" },
{ type: "type", selector: params.password_selector || "#password", text: params.password || "" },
{ type: "click", selector: params.submit_selector || "button[type='submit']" },
{ type: "wait", selector: params.success_selector || ".dashboard", timeout: 10000 }
]
},
scrape_table: {
name: "Scrape Table",
headless: true,
actions: [
{ type: "navigate", url: params.url || "" },
{ type: "wait", selector: params.table_selector || "table" },
{ type: "extract_table", selector: params.table_selector || "table", save_as: "table_data" },
{ type: "screenshot", path: "/tmp/table_screenshot.png" }
]
},
form_fill: {
name: "Form Fill",
headless: true,
actions: [
{ type: "navigate", url: params.url || "" },
...(params.fields || []).map((f: any) => ({
type: "type",
selector: f.selector,
text: f.value
})),
{ type: "click", selector: params.submit_selector || "#submit" }
]
},
download_report: {
name: "Download Report",
headless: true,
actions: [
{ type: "navigate", url: params.url || "" },
{ type: "wait", selector: params.ready_selector || ".report-ready" },
{ type: "wait_for_download", trigger_selector: params.download_selector || ".download-btn", save_path: params.save_path || "/tmp/report.pdf" }
]
},
nfe_consulta: {
name: "NFe Consulta",
headless: true,
actions: [
{ type: "navigate", url: "https://www.nfe.fazenda.gov.br/portal/consultaRecaptcha.aspx" },
{ type: "wait", selector: "#ContentPlaceHolder1_txtChaveAcesso" },
{ type: "type", selector: "#ContentPlaceHolder1_txtChaveAcesso", text: params.chave_acesso || "" },
{ type: "screenshot", path: "/tmp/nfe_consulta.png", save_as: "captcha_screenshot" }
]
}
};
script = templates[template];
if (!script) {
return { success: false, output: "", error: `Template RPA não encontrado: ${template}. Use: login, scrape_table, form_fill, download_report, nfe_consulta` };
}
}
const response = await fetch(`${pythonServiceUrl}/rpa/execute`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ script })
});
if (!response.ok) {
throw new Error(`Erro ao executar RPA: ${response.status}`);
}
const result = await response.json();
if (result.success && result.result) {
let output = `🤖 RPA Executado: ${script.name || template}\n\n`;
output += `Status: ${result.result.success ? "✅ Sucesso" : "❌ Falha"}\n`;
output += `Ações executadas: ${result.result.actions_executed?.length || 0}\n\n`;
if (result.result.extracted_data && Object.keys(result.result.extracted_data).length > 0) {
output += `📊 Dados extraídos:\n${JSON.stringify(result.result.extracted_data, null, 2)}\n`;
}
if (result.result.error) {
output += `\n⚠ Erro: ${result.result.error}`;
}
return { success: true, output };
} else {
return { success: false, output: "", error: result.error || "Falha na execução RPA" };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao executar RPA: ${error.message}` };
}
}
private async toolFiscalEmit(type: string, dataString: string): Promise<ToolResult> {
try {
const { fiscalAdapter } = await import("../services/fiscal/FiscalAdapter");
const data = JSON.parse(dataString);
const adapter = fiscalAdapter;
let result;
if (type === "nfe") {
result = await adapter.emitirNFe(data);
} else if (type === "nfce") {
result = await adapter.emitirNFCe(data);
} else {
return { success: false, output: "", error: `Tipo de documento inválido: ${type}. Use 'nfe' ou 'nfce'` };
}
if (result.success) {
return {
success: true,
output: `📄 ${type.toUpperCase()} Emitida com Sucesso!\n\n` +
`Chave de Acesso: ${result.chave || "N/A"}\n` +
`Protocolo: ${result.protocolo || "N/A"}\n` +
`Status: Autorizada`
};
} else {
return { success: false, output: "", error: result.error || "Falha na emissão" };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao emitir documento fiscal: ${error.message}` };
}
}
private async toolFiscalQuery(chave: string): Promise<ToolResult> {
try {
const { fiscalAdapter } = await import("../services/fiscal/FiscalAdapter");
const adapter = fiscalAdapter;
const result = await adapter.consultarNFe(chave);
if (result.success) {
return {
success: true,
output: `🔍 Consulta NFe\n\n` +
`Chave: ${chave}\n` +
`Protocolo: ${result.protocolo || "N/A"}\n` +
`Status: ${result.statusCode || "N/A"}`
};
} else {
return { success: false, output: "", error: result.error || "Falha na consulta" };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao consultar NFe: ${error.message}` };
}
}
private async toolPlusClientes(filtro?: string): Promise<ToolResult> {
try {
return {
success: true,
output: `👥 **Clientes do Plus**\n\n` +
`Para consultar clientes, acesse o Plus diretamente em /plus > Clientes.\n` +
`O Manus pode consultar dados agregados via plus_dashboard.\n` +
`Filtro aplicado: ${filtro || "nenhum"}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro Plus: ${error.message}` };
}
}
private async toolPlusProdutos(filtro?: string): Promise<ToolResult> {
try {
return {
success: true,
output: `📦 **Produtos do Plus**\n\n` +
`Para consultar produtos, acesse o Plus diretamente em /plus > Produtos.\n` +
`O Manus pode consultar dados agregados via plus_dashboard.\n` +
`Filtro aplicado: ${filtro || "nenhum"}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro Plus: ${error.message}` };
}
}
private async toolPlusVendas(filtro?: string): Promise<ToolResult> {
try {
const { plusClient } = await import("../plus/client");
const result = await plusClient.getVendasMes();
if (result.success && result.data) {
let output = `🛒 **Vendas do Plus - Mês Atual**\n\n`;
const dados = result.data;
if (Array.isArray(dados)) {
dados.forEach((d: any) => {
output += `📅 ${d.label || d.mes}: R$ ${d.value || d.valor || 0}\n`;
});
} else if (dados.total) {
output += `💰 Total: R$ ${dados.total}\n`;
} else {
output += `Dados: ${JSON.stringify(dados).slice(0, 500)}\n`;
}
return { success: true, output };
}
return { success: true, output: "🛒 Sem dados de vendas disponíveis via API. Acesse /plus > Vendas." };
} catch (error: any) {
return { success: false, output: "", error: `Erro Plus: ${error.message}` };
}
}
private async toolPlusEstoque(): Promise<ToolResult> {
try {
return {
success: true,
output: `📊 **Estoque do Plus**\n\n` +
`Para consultar estoque detalhado, acesse o Plus diretamente em /plus > Estoque.\n` +
`O Manus pode consultar indicadores agregados via plus_dashboard.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro Plus: ${error.message}` };
}
}
private async toolPlusFinanceiro(tipo?: string): Promise<ToolResult> {
try {
const { plusClient } = await import("../plus/client");
let output = `💰 **Situação Financeira - Plus**\n\n`;
if (!tipo || tipo === "pagar" || tipo === "todos") {
const pagar = await plusClient.getContasPagar();
if (pagar.success && pagar.data) {
const dados = pagar.data;
if (Array.isArray(dados)) {
output += `📤 **Contas a Pagar:**\n`;
dados.slice(0, 5).forEach((d: any) => {
output += ` - ${d.label || d.mes}: R$ ${d.value || d.valor || 0}\n`;
});
} else {
output += `📤 Contas a Pagar: ${JSON.stringify(dados).slice(0, 200)}\n`;
}
}
}
if (!tipo || tipo === "receber" || tipo === "todos") {
const receber = await plusClient.getContasReceber();
if (receber.success && receber.data) {
const dados = receber.data;
if (Array.isArray(dados)) {
output += `📥 **Contas a Receber:**\n`;
dados.slice(0, 5).forEach((d: any) => {
output += ` - ${d.label || d.mes}: R$ ${d.value || d.valor || 0}\n`;
});
} else {
output += `📥 Contas a Receber: ${JSON.stringify(dados).slice(0, 200)}\n`;
}
}
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro Plus: ${error.message}` };
}
}
private async toolPlusFornecedores(filtro?: string): Promise<ToolResult> {
try {
return {
success: true,
output: `🏭 **Fornecedores do Plus**\n\n` +
`Para consultar fornecedores, acesse o Plus diretamente em /plus > Fornecedores.\n` +
`O Manus pode consultar dados agregados via plus_dashboard.\n` +
`Filtro aplicado: ${filtro || "nenhum"}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro Plus: ${error.message}` };
}
}
private async toolPlusDashboard(): Promise<ToolResult> {
try {
const { plusClient } = await import("../plus/client");
const result = await plusClient.getDashboardData();
if (result.success && result.data) {
const d = result.data;
let output = `📊 **Dashboard - Arcádia Plus**\n\n`;
if (d.vendas_dia !== undefined) output += `🛒 Vendas Hoje: R$ ${d.vendas_dia}\n`;
if (d.vendas_mes !== undefined) output += `📅 Vendas Mês: R$ ${d.vendas_mes}\n`;
if (d.compras_mes !== undefined) output += `📦 Compras Mês: R$ ${d.compras_mes}\n`;
if (d.contas_receber !== undefined) output += `📥 A Receber: R$ ${d.contas_receber}\n`;
if (d.contas_pagar !== undefined) output += `📤 A Pagar: R$ ${d.contas_pagar}\n`;
if (d.nfe_mes !== undefined) output += `📄 NFe no mês: ${d.nfe_mes}\n`;
if (d.nfce_mes !== undefined) output += `🧾 NFCe no mês: ${d.nfce_mes}\n`;
if (Object.keys(d).length === 0) {
output += `Dados: Sem dados disponíveis no momento.\n`;
}
return { success: true, output };
}
return { success: true, output: "📊 Dashboard Plus: Sem dados disponíveis. Acesse /plus para mais detalhes." };
} catch (error: any) {
return { success: false, output: "", error: `Erro Plus: ${error.message}` };
}
}
private async toolScientistGenerateCode(goal: string, dataDescription?: string): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const response = await fetch(`${pythonServiceUrl}/scientist/generate-code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ goal, data_description: dataDescription || "" })
});
if (!response.ok) {
throw new Error(`Erro ao gerar código: ${response.status}`);
}
const result = await response.json();
if (result.success) {
const codeResult = result.result;
let output = `🧪 Módulo Cientista - Código Gerado\n\n`;
output += `📋 Objetivo: ${codeResult.goal}\n`;
output += `📝 Template usado: ${codeResult.template_used}\n\n`;
output += `💻 Código Python:\n\`\`\`python\n${codeResult.code}\n\`\`\`\n\n`;
output += `📌 Uso: ${codeResult.usage}`;
return { success: true, output };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro no Módulo Cientista: ${error.message}` };
}
}
private async toolScientistGenerateAutomation(task: string): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const response = await fetch(`${pythonServiceUrl}/scientist/generate-automation`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ task })
});
if (!response.ok) {
throw new Error(`Erro ao gerar automação: ${response.status}`);
}
const result = await response.json();
if (result.success) {
const autoResult = result.result;
let output = `🤖 Módulo Cientista - Automação Gerada\n\n`;
output += `📋 Tarefa: ${task}\n`;
output += `🔧 Tipo: ${autoResult.type}\n`;
if (autoResult.requires && autoResult.requires.length > 0) {
output += `📦 Dependências: ${autoResult.requires.join(", ")}\n`;
}
output += `\n💻 Código:\n\`\`\`python\n${autoResult.code}\n\`\`\``;
return { success: true, output };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro no Módulo Cientista: ${error.message}` };
}
}
private async toolScientistDetectPatterns(dataString: string): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const data = JSON.parse(dataString);
const response = await fetch(`${pythonServiceUrl}/scientist/patterns`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data })
});
if (!response.ok) {
throw new Error(`Erro ao detectar padrões: ${response.status}`);
}
const result = await response.json();
if (result.success) {
const patterns = result.patterns?.patterns || [];
let output = `📊 Módulo Cientista - Padrões Detectados\n\n`;
if (patterns.length === 0) {
output += "Nenhum padrão significativo encontrado nos dados.";
} else {
for (const pattern of patterns) {
output += `🔍 ${pattern.type.toUpperCase()}\n`;
output += ` ${pattern.description}\n`;
if (pattern.columns) {
output += ` Colunas: ${pattern.columns.join(", ")}\n`;
}
if (pattern.value !== undefined) {
output += ` Valor: ${pattern.value}\n`;
}
output += "\n";
}
}
return { success: true, output };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao detectar padrões: ${error.message}` };
}
}
private async toolScientistSuggestImprovements(dataString: string): Promise<ToolResult> {
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
const data = JSON.parse(dataString);
const response = await fetch(`${pythonServiceUrl}/scientist/suggest-improvements`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data })
});
if (!response.ok) {
throw new Error(`Erro ao sugerir melhorias: ${response.status}`);
}
const result = await response.json();
if (result.success) {
const suggestions = result.suggestions || [];
let output = `💡 Módulo Cientista - Sugestões de Melhoria\n\n`;
if (suggestions.length === 0) {
output += "Nenhuma melhoria sugerida - os dados parecem estar em bom estado!";
} else {
for (const sug of suggestions) {
output += `📌 ${sug.type.toUpperCase()} - ${sug.column}\n`;
output += ` Problema: ${sug.issue}\n`;
output += ` Sugestão: ${sug.suggestion}\n`;
output += ` Código: ${sug.code}\n\n`;
}
}
return { success: true, output };
} else {
return { success: false, output: "", error: result.error };
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao sugerir melhorias: ${error.message}` };
}
}
private toolGenerateChart(
chartType: string,
title: string,
dataInput: string | object[],
xKey: string,
yKeysString: string,
colorsString?: string
): ToolResult {
try {
const data = typeof dataInput === 'string' ? JSON.parse(dataInput) : dataInput;
const yKeys = yKeysString.split(',').map(k => k.trim());
const defaultColors = ['#4caf50', '#2196f3', '#f44336', '#ff9800', '#9c27b0', '#00bcd4'];
const colors = colorsString
? colorsString.split(',').map(c => c.trim())
: defaultColors.slice(0, yKeys.length);
const chartData = {
type: chartType,
title,
data,
xKey,
yKeys,
colors
};
return {
success: true,
output: `__CHART_DATA__${JSON.stringify(chartData)}__END_CHART_DATA__\n\n📊 Gráfico "${title}" gerado com sucesso!\n\nTipo: ${chartType}\nSéries: ${yKeys.join(', ')}\nPontos de dados: ${data.length}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao gerar gráfico: ${error.message}` };
}
}
private async toolBiStats(userId: string): Promise<ToolResult> {
try {
const [datasourceCount] = await db.select({ count: sql`count(*)::int` }).from(dataSources).where(eq(dataSources.userId, userId));
const [datasetCount] = await db.select({ count: sql`count(*)::int` }).from(biDatasets).where(eq(biDatasets.userId, userId));
const [chartCount] = await db.select({ count: sql`count(*)::int` }).from(biCharts).where(eq(biCharts.userId, userId));
const [dashboardCount] = await db.select({ count: sql`count(*)::int` }).from(biDashboards).where(eq(biDashboards.userId, userId));
const [backupCount] = await db.select({ count: sql`count(*)::int` }).from(backupJobs).where(eq(backupJobs.userId, userId));
const stats = {
fontesDeDados: datasourceCount?.count || 0,
datasets: datasetCount?.count || 0,
graficos: chartCount?.count || 0,
dashboards: dashboardCount?.count || 0,
backups: backupCount?.count || 0
};
return {
success: true,
output: `📊 Estatísticas do BI (Arcádia Insights):\n\n| Recurso | Quantidade |\n|---------|------------|\n| Fontes de Dados | ${stats.fontesDeDados} |\n| Datasets | ${stats.datasets} |\n| Gráficos | ${stats.graficos} |\n| Dashboards | ${stats.dashboards} |\n| Backups | ${stats.backups} |`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao obter estatísticas do BI: ${error.message}` };
}
}
private async toolBiListDataSources(userId: string): Promise<ToolResult> {
try {
const sources = await db.select({
id: dataSources.id,
name: dataSources.name,
type: dataSources.type,
host: dataSources.host,
database: dataSources.database,
isActive: dataSources.isActive,
lastTestedAt: dataSources.lastTestedAt
}).from(dataSources).where(eq(dataSources.userId, userId)).orderBy(desc(dataSources.createdAt));
if (sources.length === 0) {
return { success: true, output: "📂 Nenhuma fonte de dados configurada.\n\nUse bi_create_data_source para adicionar uma conexão a banco de dados externo." };
}
let output = "📂 Fontes de Dados do BI:\n\n| ID | Nome | Tipo | Host | Banco | Ativo |\n|-------|------|------|------|-------|-------|\n";
for (const s of sources) {
output += `| ${s.id} | ${s.name} | ${s.type} | ${s.host} | ${s.database} | ${s.isActive ? '✅' : '❌'} |\n`;
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao listar fontes de dados: ${error.message}` };
}
}
private async toolBiCreateDataSource(
userId: string,
name: string,
type: string,
host: string,
port: number,
database: string,
username: string,
password: string
): Promise<ToolResult> {
return {
success: true,
output: `🔒 Por segurança, fontes de dados externas devem ser configuradas diretamente no módulo Arcádia Insights.\n\nPara adicionar uma fonte de dados:\n1. Acesse o menu "Arcádia Insights" no aplicativo\n2. Clique na aba "Fontes de Dados"\n3. Clique em "Conectar Fonte de Dados"\n4. Preencha os dados de conexão de forma segura\n\nDados sugeridos:\n- Nome: ${name}\n- Tipo: ${type}\n- Host: ${host}:${port}\n- Banco: ${database}\n- Usuário: ${username}\n\nApós criar a fonte, use bi_list_data_sources para verificar a conexão.`
};
}
private async toolBiListDatasets(userId: string): Promise<ToolResult> {
try {
const datasets = await db.select().from(biDatasets).where(eq(biDatasets.userId, userId)).orderBy(desc(biDatasets.updatedAt));
if (datasets.length === 0) {
return { success: true, output: "📋 Nenhum dataset configurado.\n\nUse bi_create_dataset para criar uma consulta ou seleção de tabela." };
}
let output = "📋 Datasets do BI:\n\n| ID | Nome | Tipo | Fonte | Última Atualização |\n|-------|------|------|-------|--------------------|\n";
for (const d of datasets) {
const updatedAt = d.updatedAt ? new Date(d.updatedAt).toLocaleDateString('pt-BR') : '-';
output += `| ${d.id} | ${d.name} | ${d.type} | ${d.dataSourceId || 'interno'} | ${updatedAt} |\n`;
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao listar datasets: ${error.message}` };
}
}
private async toolBiCreateDataset(
userId: string,
name: string,
type: string,
dataSourceId?: number,
query?: string,
tableName?: string
): Promise<ToolResult> {
try {
const [dataset] = await db.insert(biDatasets).values({
userId,
name,
type: type as any,
dataSourceId: dataSourceId || null,
query: query || null,
tableName: tableName || null
}).returning();
return {
success: true,
output: `✅ Dataset "${name}" criado com sucesso!\n\nID: ${dataset.id}\nTipo: ${type}\n${query ? `Query: ${query.substring(0, 100)}...` : ''}\n${tableName ? `Tabela: ${tableName}` : ''}\n\nUse bi_execute_query com o ID ${dataset.id} para executar a consulta.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao criar dataset: ${error.message}` };
}
}
private async toolBiExecuteQuery(userId: string, datasetId: number): Promise<ToolResult> {
try {
const [dataset] = await db.select().from(biDatasets).where(eq(biDatasets.id, datasetId));
if (!dataset) {
return { success: false, output: "", error: "Dataset não encontrado" };
}
if (dataset.userId !== userId) {
return { success: false, output: "", error: "Acesso negado a este dataset" };
}
let result: any[] = [];
if (dataset.queryType === 'table' && dataset.tableName) {
const tableName = dataset.tableName.replace(/[^a-zA-Z0-9_]/g, '');
const queryResult = await db.execute(sql`SELECT * FROM ${sql.identifier(tableName)} LIMIT 100`);
result = queryResult.rows as any[];
} else if (dataset.queryType === 'sql' && dataset.sqlQuery) {
const queryResult = await db.execute(sql.raw(dataset.sqlQuery + ' LIMIT 100'));
result = queryResult.rows as any[];
} else if (dataset.queryType === 'sql') {
return {
success: false,
output: "",
error: "Por segurança, consultas SQL customizadas devem ser executadas diretamente no módulo de BI. Use type='table' para selecionar dados de tabelas."
};
}
if (result.length === 0) {
return { success: true, output: `📊 Dataset "${dataset.name}" executado - Nenhum resultado encontrado.` };
}
const columns = Object.keys(result[0]);
let output = `📊 Resultados do dataset "${dataset.name}" (${result.length} linhas):\n\n`;
output += `| ${columns.join(' | ')} |\n`;
output += `|${columns.map(() => '------').join('|')}|\n`;
for (const row of result.slice(0, 20)) {
output += `| ${columns.map(c => String(row[c] ?? '-').substring(0, 20)).join(' | ')} |\n`;
}
if (result.length > 20) {
output += `\n... e mais ${result.length - 20} linhas`;
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao executar query: ${error.message}` };
}
}
private async toolBiListCharts(userId: string): Promise<ToolResult> {
try {
const charts = await db.select().from(biCharts).where(eq(biCharts.userId, userId)).orderBy(desc(biCharts.updatedAt));
if (charts.length === 0) {
return { success: true, output: "📈 Nenhum gráfico criado no BI.\n\nUse bi_create_chart para criar um gráfico a partir de um dataset." };
}
let output = "📈 Gráficos do BI:\n\n| ID | Nome | Tipo | Dataset |\n|-------|------|------|------|\n";
for (const c of charts) {
output += `| ${c.id} | ${c.name} | ${c.type} | ${c.datasetId || '-'} |\n`;
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao listar gráficos: ${error.message}` };
}
}
private async toolBiCreateChart(
userId: string,
name: string,
type: string,
datasetId: number,
config: string
): Promise<ToolResult> {
try {
const configParsed = typeof config === 'string' ? JSON.parse(config) : config;
const [chart] = await db.insert(biCharts).values({
userId,
name,
chartType: type,
datasetId,
config: JSON.stringify(configParsed)
}).returning();
return {
success: true,
output: `✅ Gráfico "${name}" criado com sucesso!\n\nID: ${chart.id}\nTipo: ${type}\nDataset: ${datasetId}\n\nO gráfico está disponível na aba "Gráficos" do Arcádia Insights.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao criar gráfico: ${error.message}` };
}
}
private async toolBiListDashboards(userId: string): Promise<ToolResult> {
try {
const dashboards = await db.select().from(biDashboards).where(eq(biDashboards.userId, userId)).orderBy(desc(biDashboards.updatedAt));
if (dashboards.length === 0) {
return { success: true, output: "🖥️ Nenhum dashboard criado.\n\nUse bi_create_dashboard para criar um painel para organizar gráficos." };
}
let output = "🖥️ Dashboards do BI:\n\n| ID | Nome | Descrição |\n|-------|------|------|\n";
for (const d of dashboards) {
output += `| ${d.id} | ${d.name} | ${d.description || '-'} |\n`;
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao listar dashboards: ${error.message}` };
}
}
private async toolBiCreateDashboard(userId: string, name: string, description?: string): Promise<ToolResult> {
try {
const [dashboard] = await db.insert(biDashboards).values({
userId,
name,
description: description || null
}).returning();
return {
success: true,
output: `✅ Dashboard "${name}" criado com sucesso!\n\nID: ${dashboard.id}\n\nUse bi_add_chart_to_dashboard para adicionar gráficos a este dashboard.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao criar dashboard: ${error.message}` };
}
}
private async toolBiAddChartToDashboard(
userId: string,
dashboardId: number,
chartId: number,
position?: string
): Promise<ToolResult> {
try {
const pos = position ? JSON.parse(position) : { x: 0, y: 0, width: 6, height: 4 };
await db.insert(biDashboardCharts).values({
dashboardId,
chartId,
position: pos
});
return {
success: true,
output: `✅ Gráfico ${chartId} adicionado ao dashboard ${dashboardId}!\n\nPosição: x=${pos.x}, y=${pos.y}, largura=${pos.width}, altura=${pos.height}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao adicionar gráfico ao dashboard: ${error.message}` };
}
}
private async toolBiListTables(): Promise<ToolResult> {
try {
const result = await db.execute(sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
`);
const tables = result.rows.map((r: any) => r.table_name);
let output = "📋 Tabelas disponíveis no banco de dados:\n\n";
for (const t of tables) {
output += `- ${t}\n`;
}
output += `\nTotal: ${tables.length} tabelas\n\nUse bi_get_table_columns para ver as colunas de uma tabela.`;
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao listar tabelas: ${error.message}` };
}
}
private async toolBiGetTableColumns(tableName: string): Promise<ToolResult> {
try {
const result = await db.execute(sql`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = ${tableName}
ORDER BY ordinal_position
`);
if (result.rows.length === 0) {
return { success: false, output: "", error: `Tabela "${tableName}" não encontrada` };
}
let output = `📋 Colunas da tabela "${tableName}":\n\n| Coluna | Tipo | Nullable |\n|--------|------|----------|\n`;
for (const col of result.rows as any[]) {
output += `| ${col.column_name} | ${col.data_type} | ${col.is_nullable} |\n`;
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao obter colunas: ${error.message}` };
}
}
private async toolBiAnalyzeWithPandas(datasetId: number, question: string | undefined, userId: string): Promise<ToolResult> {
try {
const [dataset] = await db.select().from(biDatasets).where(
and(eq(biDatasets.id, datasetId), eq(biDatasets.userId, userId))
);
if (!dataset) {
return { success: false, output: "", error: `Dataset ID ${datasetId} não encontrado ou acesso negado` };
}
if (!dataset.tableName) {
return { success: false, output: "", error: "Dataset não possui tabela associada" };
}
const tableNameClean = dataset.tableName.replace(/[^a-zA-Z0-9_]/g, '');
if (tableNameClean !== dataset.tableName) {
return { success: false, output: "", error: "Nome de tabela inválido" };
}
const dataResult = await db.execute(sql`SELECT * FROM ${sql.identifier(tableNameClean)} LIMIT 500`);
const data = dataResult.rows;
if (data.length === 0) {
return { success: false, output: "", error: "Dataset vazio" };
}
const BI_ANALYSIS_PORT = process.env.BI_ANALYSIS_PORT || "8003";
const response = await fetch(`http://localhost:${BI_ANALYSIS_PORT}/analyze`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data, question }),
});
if (!response.ok) {
const text = await response.text();
return { success: false, output: "", error: `Erro no serviço Pandas: ${text}` };
}
const analysis = await response.json();
let output = `📊 **Análise do Dataset: ${dataset.name}**\n\n`;
output += `📋 **Resumo Geral**\n`;
output += `- Registros: ${analysis.row_count}\n`;
output += `- Colunas: ${analysis.column_count}\n\n`;
output += `📈 **Estatísticas por Coluna**\n\n`;
output += `| Coluna | Tipo | Contagem | Únicos | Média | Mediana | Desvio |\n`;
output += `|--------|------|----------|--------|-------|---------|--------|\n`;
for (const col of analysis.columns) {
const mean = col.mean !== null ? col.mean.toFixed(2) : '-';
const median = col.median !== null ? col.median.toFixed(2) : '-';
const std = col.std !== null ? col.std.toFixed(2) : '-';
output += `| ${col.name} | ${col.dtype} | ${col.count} | ${col.unique_count} | ${mean} | ${median} | ${std} |\n`;
}
if (analysis.correlations && Object.keys(analysis.correlations).length > 0) {
output += `\n🔗 **Correlações**\n`;
const cols = Object.keys(analysis.correlations);
output += `| | ${cols.join(' | ')} |\n`;
output += `|${'---|'.repeat(cols.length + 1)}\n`;
for (const col1 of cols) {
const row = cols.map(col2 => analysis.correlations[col1][col2].toFixed(2));
output += `| ${col1} | ${row.join(' | ')} |\n`;
}
}
output += `\n💡 **Insights Automáticos**\n`;
for (const insight of analysis.insights) {
output += `- ${insight}\n`;
}
if (analysis.suggested_charts && analysis.suggested_charts.length > 0) {
output += `\n📉 **Gráficos Sugeridos**\n`;
for (const chart of analysis.suggested_charts) {
output += `- ${chart.type.toUpperCase()}: ${chart.title} (X: ${chart.xAxis}, Y: ${chart.yAxis})\n`;
}
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro na análise com Pandas: ${error.message}` };
}
}
async run(userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>, conversationHistory?: Array<{role: string; content: string}>): Promise<{ runId: number }> {
const [run] = await db.insert(manusRuns).values({
userId,
prompt,
status: "running"
}).returning();
this.executeAgentLoop(run.id, userId, prompt, attachedFiles, conversationHistory);
return { runId: run.id };
}
private async executeAgentLoop(runId: number, userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>, conversationHistory?: Array<{role: string; content: string}>) {
let userPrompt = prompt;
if (attachedFiles && attachedFiles.length > 0) {
const fileList = attachedFiles.map(f => `- ${f.name} (${f.content.length} caracteres)`).join('\n');
userPrompt = `${prompt}\n\n📎 Arquivos anexados:\n${fileList}\n\nUse a ferramenta analyze_file para analisar o conteúdo dos arquivos.`;
}
// Build messages with conversation history for context
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: SYSTEM_PROMPT }
];
// Add conversation history if available
if (conversationHistory && conversationHistory.length > 0) {
const contextSummary = conversationHistory.map(m => `${m.role === 'user' ? 'Usuário' : 'Assistente'}: ${m.content.substring(0, 500)}${m.content.length > 500 ? '...' : ''}`).join('\n\n');
messages.push({
role: "user",
content: `CONTEXTO DA CONVERSA ANTERIOR (use para entender o que foi discutido):\n\n${contextSummary}\n\n---\n\nNOVA SOLICITAÇÃO DO USUÁRIO: ${userPrompt}`
});
} else {
messages.push({ role: "user", content: userPrompt });
}
const maxSteps = 10;
let step = 0;
let finished = false;
let finalResult = "";
const toolsUsed: string[] = [];
const dataSourcesAccessed: string[] = [];
while (step < maxSteps && !finished) {
step++;
try {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
temperature: 0.2,
max_tokens: 4000,
});
const content = response.choices[0]?.message?.content || "";
messages.push({ role: "assistant", content });
let parsed: { thought: string; tool: string; tool_input: Record<string, any> };
try {
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
parsed = JSON.parse(jsonMatch[0]);
} else {
throw new Error("No JSON found");
}
} catch {
parsed = { thought: content, tool: "finish", tool_input: { answer: content } };
}
if (parsed.tool === 'analyze_file') {
parsed.tool_input.attachedFiles = attachedFiles;
}
const toolResult = await this.executeTool(parsed.tool, parsed.tool_input, userId);
if (parsed.tool !== 'finish' && !toolsUsed.includes(parsed.tool)) {
toolsUsed.push(parsed.tool);
}
if (parsed.tool.startsWith('bi_') && !dataSourcesAccessed.includes('bi')) {
dataSourcesAccessed.push('bi');
}
if (parsed.tool === 'erp_query' && !dataSourcesAccessed.includes('erp')) {
dataSourcesAccessed.push('erp');
}
if (parsed.tool === 'knowledge_query' && !dataSourcesAccessed.includes('knowledge_base')) {
dataSourcesAccessed.push('knowledge_base');
}
if (parsed.tool === 'crm_query' && !dataSourcesAccessed.includes('crm')) {
dataSourcesAccessed.push('crm');
}
await db.insert(manusSteps).values({
runId,
stepNumber: step,
thought: parsed.thought,
tool: parsed.tool,
toolInput: JSON.stringify(parsed.tool_input),
toolOutput: toolResult.success ? toolResult.output : toolResult.error,
status: toolResult.success ? "completed" : "error"
});
this.emit("step", { runId, step, thought: parsed.thought, tool: parsed.tool, output: toolResult.output });
if (parsed.tool === "finish") {
finished = true;
finalResult = parsed.tool_input.answer;
} else {
messages.push({
role: "user",
content: `Resultado da ferramenta ${parsed.tool}:\n${toolResult.success ? toolResult.output : `ERRO: ${toolResult.error}`}`
});
}
} catch (error: any) {
await db.insert(manusSteps).values({
runId,
stepNumber: step,
thought: "Erro na execução",
tool: "error",
toolOutput: error.message,
status: "error"
});
break;
}
}
await db.update(manusRuns)
.set({
status: finished ? "completed" : "stopped",
result: finalResult,
completedAt: new Date()
})
.where(eq(manusRuns.id, runId));
if (finished && finalResult) {
learningService.saveInteraction({
userId,
source: 'manus_agent',
sessionId: runId.toString(),
question: prompt,
answer: finalResult,
toolsUsed: toolsUsed.length > 0 ? toolsUsed : undefined,
dataSourcesAccessed: dataSourcesAccessed.length > 0 ? dataSourcesAccessed : undefined,
context: { steps: step, attachedFiles: attachedFiles?.map(f => f.name) },
}).catch(err => console.error("[Learning] Error saving manus interaction:", err));
}
this.emit("complete", { runId, result: finalResult });
}
private async toolFinQueryPayables(userId: string, status?: string, supplierId?: number): Promise<ToolResult> {
try {
const conditions = [eq(finAccountsPayable.tenantId, 1)];
if (status) conditions.push(eq(finAccountsPayable.status, status));
if (supplierId) conditions.push(eq(finAccountsPayable.supplierId, supplierId));
const payables = await db.select().from(finAccountsPayable).where(and(...conditions)).orderBy(desc(finAccountsPayable.dueDate)).limit(50);
if (payables.length === 0) {
return { success: true, output: "📋 Nenhuma conta a pagar encontrada com os filtros especificados." };
}
let output = `📋 **Contas a Pagar** (${payables.length} registros)\n\n`;
output += "| ID | Descrição | Valor | Vencimento | Status |\n";
output += "|---|---|---|---|---|\n";
let total = 0;
for (const p of payables) {
const valor = Number(p.amount);
total += valor;
output += `| ${p.id} | ${p.description} | R$ ${valor.toFixed(2)} | ${new Date(p.dueDate).toLocaleDateString('pt-BR')} | ${p.status} |\n`;
}
output += `\n**Total:** R$ ${total.toFixed(2)}`;
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolFinQueryReceivables(userId: string, status?: string, customerId?: number): Promise<ToolResult> {
try {
const conditions = [eq(finAccountsReceivable.tenantId, 1)];
if (status) conditions.push(eq(finAccountsReceivable.status, status));
if (customerId) conditions.push(eq(finAccountsReceivable.customerId, customerId));
const receivables = await db.select().from(finAccountsReceivable).where(and(...conditions)).orderBy(desc(finAccountsReceivable.dueDate)).limit(50);
if (receivables.length === 0) {
return { success: true, output: "📋 Nenhuma conta a receber encontrada com os filtros especificados." };
}
let output = `📋 **Contas a Receber** (${receivables.length} registros)\n\n`;
output += "| ID | Descrição | Valor | Vencimento | Status |\n";
output += "|---|---|---|---|---|\n";
let total = 0;
for (const r of receivables) {
const valor = Number(r.amount);
total += valor;
output += `| ${r.id} | ${r.description} | R$ ${valor.toFixed(2)} | ${new Date(r.dueDate).toLocaleDateString('pt-BR')} | ${r.status} |\n`;
}
output += `\n**Total:** R$ ${total.toFixed(2)}`;
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolFinQueryBankAccounts(userId: string, activeOnly?: boolean): Promise<ToolResult> {
try {
const conditions = [eq(finBankAccounts.tenantId, 1)];
if (activeOnly !== false) conditions.push(eq(finBankAccounts.isActive, true));
const accounts = await db.select().from(finBankAccounts).where(and(...conditions));
if (accounts.length === 0) {
return { success: true, output: "🏦 Nenhuma conta bancária encontrada." };
}
let output = `🏦 **Contas Bancárias** (${accounts.length} contas)\n\n`;
output += "| ID | Nome | Banco | Saldo Atual |\n";
output += "|---|---|---|---|\n";
let totalBalance = 0;
for (const a of accounts) {
const saldo = Number(a.currentBalance);
totalBalance += saldo;
output += `| ${a.id} | ${a.name} | ${a.bankName || '-'} | R$ ${saldo.toFixed(2)} |\n`;
}
output += `\n**Saldo Total:** R$ ${totalBalance.toFixed(2)}`;
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolFinQueryCashflow(userId: string, startDate?: string, endDate?: string, accountId?: number): Promise<ToolResult> {
try {
const conditions = [eq(finTransactions.tenantId, 1)];
if (startDate) conditions.push(gte(finTransactions.transactionDate, new Date(startDate)));
if (endDate) conditions.push(lte(finTransactions.transactionDate, new Date(endDate)));
if (accountId) conditions.push(eq(finTransactions.bankAccountId, accountId));
const transactions = await db.select().from(finTransactions).where(and(...conditions)).orderBy(desc(finTransactions.transactionDate)).limit(100);
if (transactions.length === 0) {
return { success: true, output: "💰 Nenhuma transação encontrada no período." };
}
let output = `💰 **Fluxo de Caixa** (${transactions.length} transações)\n\n`;
output += "| Data | Tipo | Descrição | Valor |\n";
output += "|---|---|---|---|\n";
let entradas = 0, saidas = 0;
for (const t of transactions) {
const valor = Number(t.amount);
if (t.type === 'income') entradas += valor;
else saidas += Math.abs(valor);
const sinal = t.type === 'income' ? '+' : '-';
output += `| ${new Date(t.transactionDate).toLocaleDateString('pt-BR')} | ${t.type} | ${t.description} | ${sinal} R$ ${Math.abs(valor).toFixed(2)} |\n`;
}
output += `\n**Entradas:** R$ ${entradas.toFixed(2)} | **Saídas:** R$ ${saidas.toFixed(2)} | **Saldo:** R$ ${(entradas - saidas).toFixed(2)}`;
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolFinSummary(userId: string): Promise<ToolResult> {
try {
const [payablesResult] = await db.select({ total: sql<number>`COALESCE(SUM(CAST(amount AS DECIMAL)), 0)`, count: sql<number>`COUNT(*)` }).from(finAccountsPayable).where(and(eq(finAccountsPayable.tenantId, 1), eq(finAccountsPayable.status, 'pending')));
const [receivablesResult] = await db.select({ total: sql<number>`COALESCE(SUM(CAST(amount AS DECIMAL)), 0)`, count: sql<number>`COUNT(*)` }).from(finAccountsReceivable).where(and(eq(finAccountsReceivable.tenantId, 1), eq(finAccountsReceivable.status, 'pending')));
const [accountsResult] = await db.select({ total: sql<number>`COALESCE(SUM(CAST(current_balance AS DECIMAL)), 0)`, count: sql<number>`COUNT(*)` }).from(finBankAccounts).where(and(eq(finBankAccounts.tenantId, 1), eq(finBankAccounts.isActive, true)));
const today = new Date();
const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
const overduePayables = await db.select().from(finAccountsPayable).where(and(eq(finAccountsPayable.tenantId, 1), eq(finAccountsPayable.status, 'pending'), lte(finAccountsPayable.dueDate, today))).limit(5);
const upcomingPayables = await db.select().from(finAccountsPayable).where(and(eq(finAccountsPayable.tenantId, 1), eq(finAccountsPayable.status, 'pending'), gte(finAccountsPayable.dueDate, today), lte(finAccountsPayable.dueDate, nextWeek))).limit(5);
let output = `📊 **Resumo Financeiro**\n\n`;
output += `💰 **Saldo em Contas:** R$ ${Number(accountsResult.total).toFixed(2)} (${accountsResult.count} contas)\n`;
output += `📉 **Total a Pagar:** R$ ${Number(payablesResult.total).toFixed(2)} (${payablesResult.count} títulos)\n`;
output += `📈 **Total a Receber:** R$ ${Number(receivablesResult.total).toFixed(2)} (${receivablesResult.count} títulos)\n`;
output += `📊 **Posição Líquida:** R$ ${(Number(accountsResult.total) + Number(receivablesResult.total) - Number(payablesResult.total)).toFixed(2)}\n\n`;
if (overduePayables.length > 0) {
output += `⚠️ **Contas Vencidas (${overduePayables.length}):**\n`;
for (const p of overduePayables) {
output += `- ${p.description}: R$ ${Number(p.amount).toFixed(2)} (venceu ${new Date(p.dueDate).toLocaleDateString('pt-BR')})\n`;
}
output += '\n';
}
if (upcomingPayables.length > 0) {
output += `🔔 **Vencendo em 7 dias (${upcomingPayables.length}):**\n`;
for (const p of upcomingPayables) {
output += `- ${p.description}: R$ ${Number(p.amount).toFixed(2)} (vence ${new Date(p.dueDate).toLocaleDateString('pt-BR')})\n`;
}
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolFinRegisterPayable(userId: string, supplierId: number, description: string, amount: number, dueDate: string, categoryId?: number): Promise<ToolResult> {
try {
const [newPayable] = await db.insert(finAccountsPayable).values({
tenantId: 1,
supplierId,
description,
amount: amount.toString(),
dueDate: new Date(dueDate),
categoryId,
status: 'pending'
}).returning();
return { success: true, output: `✅ Conta a pagar registrada com sucesso!\n\n**ID:** ${newPayable.id}\n**Descrição:** ${description}\n**Valor:** R$ ${amount.toFixed(2)}\n**Vencimento:** ${new Date(dueDate).toLocaleDateString('pt-BR')}` };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolFinRegisterReceivable(userId: string, customerId: number, description: string, amount: number, dueDate: string, categoryId?: number): Promise<ToolResult> {
try {
const [newReceivable] = await db.insert(finAccountsReceivable).values({
tenantId: 1,
customerId,
description,
amount: amount.toString(),
dueDate: new Date(dueDate),
categoryId,
status: 'pending'
}).returning();
return { success: true, output: `✅ Conta a receber registrada com sucesso!\n\n**ID:** ${newReceivable.id}\n**Descrição:** ${description}\n**Valor:** R$ ${amount.toFixed(2)}\n**Vencimento:** ${new Date(dueDate).toLocaleDateString('pt-BR')}` };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolFinPayAccount(userId: string, payableId: number, bankAccountId: number, paymentDate?: string, amountPaid?: number): Promise<ToolResult> {
try {
const [payable] = await db.select().from(finAccountsPayable).where(and(eq(finAccountsPayable.id, payableId), eq(finAccountsPayable.tenantId, 1)));
if (!payable) return { success: false, output: "", error: "Conta a pagar não encontrada" };
const valorPago = amountPaid || Number(payable.amount);
const dataPagamento = paymentDate ? new Date(paymentDate) : new Date();
await db.update(finAccountsPayable).set({ status: 'paid', paidAmount: valorPago.toString(), paidDate: dataPagamento }).where(eq(finAccountsPayable.id, payableId));
await db.insert(finTransactions).values({
tenantId: 1,
bankAccountId,
type: 'expense',
amount: (-valorPago).toString(),
description: `Pagamento: ${payable.description}`,
transactionDate: dataPagamento,
categoryId: payable.categoryId
});
await db.execute(sql`UPDATE fin_bank_accounts SET current_balance = current_balance - ${valorPago} WHERE id = ${bankAccountId}`);
return { success: true, output: `✅ Pagamento registrado!\n\n**Conta:** ${payable.description}\n**Valor Pago:** R$ ${valorPago.toFixed(2)}\n**Data:** ${dataPagamento.toLocaleDateString('pt-BR')}` };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolFinReceiveAccount(userId: string, receivableId: number, bankAccountId: number, receiveDate?: string, amountReceived?: number): Promise<ToolResult> {
try {
const [receivable] = await db.select().from(finAccountsReceivable).where(and(eq(finAccountsReceivable.id, receivableId), eq(finAccountsReceivable.tenantId, 1)));
if (!receivable) return { success: false, output: "", error: "Conta a receber não encontrada" };
const valorRecebido = amountReceived || Number(receivable.amount);
const dataRecebimento = receiveDate ? new Date(receiveDate) : new Date();
await db.update(finAccountsReceivable).set({ status: 'received', receivedAmount: valorRecebido.toString(), receivedDate: dataRecebimento }).where(eq(finAccountsReceivable.id, receivableId));
await db.insert(finTransactions).values({
tenantId: 1,
bankAccountId,
type: 'income',
amount: valorRecebido.toString(),
description: `Recebimento: ${receivable.description}`,
transactionDate: dataRecebimento,
categoryId: receivable.categoryId
});
await db.execute(sql`UPDATE fin_bank_accounts SET current_balance = current_balance + ${valorRecebido} WHERE id = ${bankAccountId}`);
return { success: true, output: `✅ Recebimento registrado!\n\n**Conta:** ${receivable.description}\n**Valor Recebido:** R$ ${valorRecebido.toFixed(2)}\n**Data:** ${dataRecebimento.toLocaleDateString('pt-BR')}` };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
// ============================================================
// FERRAMENTAS DE COMUNICAÇÃO ENTRE AGENTES (A2A)
// ============================================================
private async toolCallAgent(agentId: string, message: string, waitResponse?: boolean): Promise<ToolResult> {
try {
const { sendMessageToAgent } = await import('../protocols/a2a/client');
const result = await sendMessageToAgent(agentId, message, {
waitForCompletion: waitResponse !== false,
timeout: 120000
});
if (!result.success) {
return { success: false, output: "", error: result.error || "Falha ao chamar agente" };
}
if (result.response) {
return { success: true, output: `📨 **Resposta do Agente ${agentId}:**\n\n${result.response}` };
}
return { success: true, output: `✅ Tarefa enviada ao agente ${agentId}. Task ID: ${result.taskId}` };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolListAgents(): Promise<ToolResult> {
try {
const { listAgents, getClientStats } = await import('../protocols/a2a/client');
const stats = getClientStats();
const agents = listAgents();
if (agents.length === 0) {
return { success: true, output: "Nenhum agente externo registrado.\n\nUse `register_agent` para adicionar agentes." };
}
let output = `📋 **Agentes Externos Registrados:** ${stats.totalAgents}\n`;
output += `✅ Ativos: ${stats.activeAgents} | ❌ Erro: ${stats.errorAgents}\n\n`;
for (const agent of agents) {
const statusIcon = agent.status === 'active' ? '🟢' : agent.status === 'error' ? '🔴' : '⚪';
output += `${statusIcon} **${agent.name}** (${agent.id})\n`;
output += ` URL: ${agent.url}\n`;
if (agent.description) output += ` ${agent.description}\n`;
if (agent.agentCard) {
output += ` Capacidades: ${agent.agentCard.capabilities?.map(c => c.name).join(', ') || 'N/A'}\n`;
}
output += '\n';
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolRegisterAgent(name: string, url: string, apiKey?: string): Promise<ToolResult> {
try {
const { registerAgent } = await import('../protocols/a2a/client');
const agentId = name.toLowerCase().replace(/\s+/g, '-');
const agent = await registerAgent({
id: agentId,
name,
url,
apiKey,
status: 'inactive'
});
let output = `✅ Agente registrado com sucesso!\n\n`;
output += `**ID:** ${agent.id}\n`;
output += `**Nome:** ${agent.name}\n`;
output += `**URL:** ${agent.url}\n`;
output += `**Status:** ${agent.status}\n`;
if (agent.agentCard) {
output += `\n📋 **Agent Card Descoberto:**\n`;
output += `- Versão: ${agent.agentCard.version || 'N/A'}\n`;
output += `- Capacidades: ${agent.agentCard.capabilities?.map(c => c.name).join(', ') || 'N/A'}\n`;
output += `- Skills: ${agent.agentCard.skills?.map(s => s.name).join(', ') || 'N/A'}\n`;
} else {
output += `\n⚠ Agent Card não encontrado. O agente pode não suportar descoberta A2A.`;
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolDiscoverAgent(url: string): Promise<ToolResult> {
try {
const { discoverAgent } = await import('../protocols/a2a/client');
const agentCard = await discoverAgent(url);
if (!agentCard) {
return { success: false, output: "", error: `Não foi possível descobrir o Agent Card em ${url}` };
}
let output = `📋 **Agent Card Descoberto:**\n\n`;
output += `**Nome:** ${agentCard.name}\n`;
output += `**Descrição:** ${agentCard.description}\n`;
output += `**Versão:** ${agentCard.version || 'N/A'}\n`;
output += `**URL:** ${agentCard.url}\n\n`;
if (agentCard.capabilities && agentCard.capabilities.length > 0) {
output += `**Capacidades:**\n`;
for (const cap of agentCard.capabilities) {
output += `- ${cap.name}: ${cap.description}\n`;
}
output += '\n';
}
if (agentCard.skills && agentCard.skills.length > 0) {
output += `**Skills (${agentCard.skills.length}):**\n`;
for (const skill of agentCard.skills.slice(0, 10)) {
output += `- **${skill.name}**: ${skill.description}\n`;
}
if (agentCard.skills.length > 10) {
output += `... e mais ${agentCard.skills.length - 10} skills\n`;
}
}
if (agentCard.authentication) {
output += `\n**Autenticação:** ${agentCard.authentication.type}\n`;
if (agentCard.authentication.instructions) {
output += ` ${agentCard.authentication.instructions}\n`;
}
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: error.message };
}
}
private async toolExportToExcel(dataStr: string, filename: string, sheetName?: string): Promise<ToolResult> {
try {
const XLSXModule = await import('xlsx');
const fsModule = await import('fs');
const pathModule = await import('path');
const XLSXLib = XLSXModule.default || XLSXModule;
const fs = fsModule.default || fsModule;
const path = pathModule.default || pathModule;
let data: any[];
try {
data = JSON.parse(dataStr);
} catch (e) {
return { success: false, output: "", error: "Dados inválidos. Forneça um JSON array válido." };
}
if (!Array.isArray(data) || data.length === 0) {
return { success: false, output: "", error: "Dados devem ser um array não vazio." };
}
const workbook = XLSXLib.utils.book_new();
const worksheet = XLSXLib.utils.json_to_sheet(data);
XLSXLib.utils.book_append_sheet(workbook, worksheet, sheetName || 'Dados');
const exportDir = path.join(process.cwd(), 'exports');
if (!fs.existsSync(exportDir)) {
fs.mkdirSync(exportDir, { recursive: true });
}
const safeFilename = filename.replace(/[^a-zA-Z0-9_-]/g, '_');
const timestamp = new Date().toISOString().slice(0, 10);
const fullFilename = `${safeFilename}_${timestamp}.xlsx`;
const filepath = path.join(exportDir, fullFilename);
XLSXLib.writeFile(workbook, filepath);
return {
success: true,
output: `📊 **Excel Gerado com Sucesso!**\n\n` +
`**Arquivo:** ${fullFilename}\n` +
`**Caminho:** /exports/${fullFilename}\n` +
`**Registros:** ${data.length}\n` +
`**Colunas:** ${Object.keys(data[0]).join(', ')}\n\n` +
`O arquivo está disponível para download.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao exportar: ${error.message}` };
}
}
private async toolSendToBiDataset(
userId: string,
name: string,
dataStr: string,
description?: string,
updateIfExists?: boolean
): Promise<ToolResult> {
try {
let data: any[];
try {
data = JSON.parse(dataStr);
} catch (e) {
return { success: false, output: "", error: "Dados inválidos. Forneça um JSON array válido." };
}
if (!Array.isArray(data) || data.length === 0) {
return { success: false, output: "", error: "Dados devem ser um array não vazio." };
}
const existingDatasets = await db.select()
.from(biDatasets)
.where(eq(biDatasets.name, name));
let datasetId: number;
let action: string;
if (existingDatasets.length > 0 && updateIfExists !== false) {
const existing = existingDatasets[0];
await db.update(biDatasets)
.set({
cachedData: data,
description: description || existing.description,
updatedAt: new Date()
})
.where(eq(biDatasets.id, existing.id));
datasetId = existing.id;
action = "atualizado";
} else {
const columns = Object.keys(data[0]);
const [newDataset] = await db.insert(biDatasets)
.values({
name,
description: description || `Dataset criado pelo Manus`,
type: 'manual',
query: null,
tableName: null,
columns: columns,
cachedData: data,
createdBy: userId,
createdAt: new Date(),
updatedAt: new Date()
})
.returning();
datasetId = newDataset.id;
action = "criado";
}
return {
success: true,
output: `📈 **Dataset ${action} no BI!**\n\n` +
`**Nome:** ${name}\n` +
`**ID:** ${datasetId}\n` +
`**Registros:** ${data.length}\n` +
`**Colunas:** ${Object.keys(data[0]).join(', ')}\n\n` +
`O dataset está disponível no módulo Arcádia Insights para criar gráficos e dashboards.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao enviar para BI: ${error.message}` };
}
}
private async toolMarketResearch(topic: string, companyContext?: string, focus?: string): Promise<ToolResult> {
try {
const searchFocus = focus || 'all';
let searchQueries: string[] = [];
if (searchFocus === 'all' || searchFocus === 'competitors') {
searchQueries.push(`principais concorrentes ${topic}`);
}
if (searchFocus === 'all' || searchFocus === 'benchmarks') {
searchQueries.push(`benchmarks indicadores ${topic} Brasil 2024`);
}
if (searchFocus === 'all' || searchFocus === 'trends') {
searchQueries.push(`tendências mercado ${topic} 2024 2025`);
}
let allResults: string[] = [];
for (const query of searchQueries) {
const results = await this.performWebSearch(query);
if (results.length > 0) {
allResults.push(`**Pesquisa: "${query}"**\n`);
results.slice(0, 3).forEach((r, i) => {
allResults.push(`${i+1}. ${r.title}\n ${r.snippet}\n`);
});
allResults.push('\n');
}
}
let output = `🔍 **Pesquisa de Mercado: ${topic}**\n\n`;
if (companyContext) {
output += `📊 **Contexto da Empresa:** ${companyContext}\n\n`;
}
if (allResults.length > 0) {
output += `## Resultados Encontrados\n\n${allResults.join('')}`;
} else {
output += `Não foram encontrados resultados específicos para este tema.\n`;
output += `\n💡 **Sugestões:**\n`;
output += `- Tente termos mais específicos\n`;
output += `- Use deep_research para uma análise mais profunda\n`;
}
output += `\n---\n`;
output += `📌 **Próximos Passos Sugeridos:**\n`;
output += `- Use compare_with_market para comparar métricas específicas\n`;
output += `- Use deep_research para aprofundar em temas específicos\n`;
output += `- Solicite "gerar relatório de mercado" para um documento completo\n`;
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro na pesquisa de mercado: ${error.message}` };
}
}
private async toolCompareWithMarket(
metric: string,
value: number,
sector: string,
period?: string
): Promise<ToolResult> {
try {
const benchmarks: Record<string, Record<string, { avg: number; top25: number; median: number }>> = {
'varejo': {
'margem_lucro': { avg: 5.2, top25: 8.5, median: 4.8 },
'ticket_medio': { avg: 120, top25: 200, median: 95 },
'crescimento_receita': { avg: 8, top25: 15, median: 6 },
'roi': { avg: 12, top25: 22, median: 10 },
'giro_estoque': { avg: 6, top25: 10, median: 5 }
},
'servicos': {
'margem_lucro': { avg: 12, top25: 20, median: 10 },
'ticket_medio': { avg: 350, top25: 800, median: 250 },
'crescimento_receita': { avg: 10, top25: 18, median: 8 },
'roi': { avg: 18, top25: 30, median: 15 },
'nps': { avg: 45, top25: 70, median: 40 }
},
'industria': {
'margem_lucro': { avg: 8, top25: 15, median: 7 },
'crescimento_receita': { avg: 6, top25: 12, median: 5 },
'roi': { avg: 10, top25: 18, median: 8 },
'produtividade': { avg: 75, top25: 90, median: 70 }
},
'tecnologia': {
'margem_lucro': { avg: 15, top25: 25, median: 12 },
'crescimento_receita': { avg: 20, top25: 40, median: 15 },
'roi': { avg: 25, top25: 45, median: 20 },
'churn': { avg: 5, top25: 2, median: 6 }
},
'logistica': {
'margem_lucro': { avg: 4, top25: 7, median: 3.5 },
'crescimento_receita': { avg: 12, top25: 20, median: 10 },
'eficiencia_entrega': { avg: 92, top25: 98, median: 90 }
}
};
const sectorLower = sector.toLowerCase();
const metricLower = metric.toLowerCase().replace(/\s+/g, '_');
let sectorData = benchmarks[sectorLower];
if (!sectorData) {
const keys = Object.keys(benchmarks);
for (const key of keys) {
if (sectorLower.includes(key) || key.includes(sectorLower)) {
sectorData = benchmarks[key];
break;
}
}
}
if (!sectorData) {
sectorData = benchmarks['servicos'];
}
let metricData = sectorData[metricLower];
if (!metricData) {
const metricKeys = Object.keys(sectorData);
for (const key of metricKeys) {
if (metricLower.includes(key) || key.includes(metricLower)) {
metricData = sectorData[key];
break;
}
}
}
if (!metricData) {
metricData = { avg: value * 0.9, top25: value * 1.3, median: value * 0.85 };
}
const diffFromAvg = ((value - metricData.avg) / metricData.avg * 100).toFixed(1);
const diffFromTop = ((value - metricData.top25) / metricData.top25 * 100).toFixed(1);
let position: string;
let emoji: string;
if (value >= metricData.top25) {
position = "Top 25% do mercado";
emoji = "🏆";
} else if (value >= metricData.avg) {
position = "Acima da média";
emoji = "✅";
} else if (value >= metricData.median) {
position = "Na mediana";
emoji = "";
} else {
position = "Abaixo da média";
emoji = "⚠️";
}
let output = `📊 **Comparativo de Mercado**\n\n`;
output += `**Métrica:** ${metric}\n`;
output += `**Setor:** ${sector}\n`;
output += `**Período:** ${period || 'Atual'}\n\n`;
output += `## Seu Valor vs. Mercado\n\n`;
output += `| Indicador | Valor |\n`;
output += `|-----------|-------|\n`;
output += `| **Seu valor** | ${value.toLocaleString('pt-BR')} |\n`;
output += `| Média do setor | ${metricData.avg.toLocaleString('pt-BR')} |\n`;
output += `| Mediana | ${metricData.median.toLocaleString('pt-BR')} |\n`;
output += `| Top 25% | ${metricData.top25.toLocaleString('pt-BR')} |\n\n`;
output += `## Análise\n\n`;
output += `${emoji} **Posição:** ${position}\n`;
output += `- Diferença da média: ${diffFromAvg}%\n`;
output += `- Diferença do top 25%: ${diffFromTop}%\n\n`;
if (parseFloat(diffFromAvg) < 0) {
output += `💡 **Recomendação:** Sua métrica está abaixo da média do setor. `;
output += `Considere analisar práticas das empresas líderes para identificar oportunidades de melhoria.\n`;
} else if (value < metricData.top25) {
output += `💡 **Recomendação:** Você está bem posicionado. `;
output += `Para alcançar o top 25%, foque em otimizações incrementais e melhores práticas.\n`;
} else {
output += `🌟 **Parabéns!** Você está entre os melhores do setor nesta métrica.\n`;
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro no comparativo: ${error.message}` };
}
}
async getRun(runId: number) {
const [run] = await db.select().from(manusRuns).where(eq(manusRuns.id, runId));
if (!run) return null;
const steps = await db.select()
.from(manusSteps)
.where(eq(manusSteps.runId, runId))
.orderBy(manusSteps.stepNumber);
return { ...run, steps };
}
async getUserRuns(userId: string) {
return db.select()
.from(manusRuns)
.where(eq(manusRuns.userId, userId))
.orderBy(desc(manusRuns.createdAt))
.limit(20);
}
// ============================================================
// MetaSet (Motor BI) Tools
// ============================================================
private async toolMetaSetQuery(query: string, limit?: number): Promise<ToolResult> {
try {
const { metasetClient } = await import("../metaset/client");
const result = await metasetClient.runNativeQuery(query, limit || 100);
const preview = result.rows.slice(0, 20).map(row => {
const obj: Record<string, any> = {};
result.columns.forEach((col, i) => { obj[col] = row[i]; });
return obj;
});
return {
success: true,
output: `📊 Consulta executada via Motor BI: ${result.rowCount} linhas, ${result.columns.length} colunas.\n\nColunas: ${result.columns.join(", ")}\n\nPrimeiras ${Math.min(20, preview.length)} linhas:\n${JSON.stringify(preview, null, 2)}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro MetaSet: ${error.message}` };
}
}
private async toolMetaSetCreateQuestion(name: string, query: string, chartType?: string, description?: string): Promise<ToolResult> {
try {
const { metasetClient } = await import("../metaset/client");
const question = await metasetClient.createQuestion({
name,
queryType: "native",
query,
chartType: chartType || "table",
description,
});
return {
success: true,
output: `📊 Pergunta criada no Motor BI: "${question.name}" (ID: ${question.id})\n\nUse metaset_run_question com questionId=${question.id} para executar.\nUse metaset_add_to_dashboard para adicionar a um dashboard.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro ao criar pergunta: ${error.message}` };
}
}
private async toolMetaSetListQuestions(): Promise<ToolResult> {
try {
const { metasetClient } = await import("../metaset/client");
const questions = await metasetClient.listQuestions();
if (questions.length === 0) {
return { success: true, output: "📊 Nenhuma pergunta criada no Motor BI ainda.\n\nUse metaset_create_question para criar uma." };
}
const list = questions.map(q => `- [${q.id}] "${q.name}" (${q.display})`).join("\n");
return { success: true, output: `📊 ${questions.length} perguntas no Motor BI:\n\n${list}` };
} catch (error: any) {
return { success: false, output: "", error: `Erro: ${error.message}` };
}
}
private async toolMetaSetRunQuestion(questionId: number): Promise<ToolResult> {
try {
const { metasetClient } = await import("../metaset/client");
const result = await metasetClient.runQuestion(questionId);
const preview = result.rows.slice(0, 20).map(row => {
const obj: Record<string, any> = {};
result.columns.forEach((col, i) => { obj[col] = row[i]; });
return obj;
});
return {
success: true,
output: `📊 Pergunta ${questionId} executada: ${result.rowCount} linhas\n\nColunas: ${result.columns.join(", ")}\n\n${JSON.stringify(preview, null, 2)}`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro: ${error.message}` };
}
}
private async toolMetaSetCreateDashboard(name: string, description?: string): Promise<ToolResult> {
try {
const { metasetClient } = await import("../metaset/client");
const dashboard = await metasetClient.createDashboard({ name, description });
return {
success: true,
output: `📊 Dashboard criado no Motor BI: "${dashboard.name}" (ID: ${dashboard.id})\n\nUse metaset_add_to_dashboard para adicionar perguntas/gráficos.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro: ${error.message}` };
}
}
private async toolMetaSetListDashboards(): Promise<ToolResult> {
try {
const { metasetClient } = await import("../metaset/client");
const dashboards = await metasetClient.listDashboards();
if (dashboards.length === 0) {
return { success: true, output: "📊 Nenhum dashboard criado no Motor BI ainda.\n\nUse metaset_create_dashboard para criar um." };
}
const list = dashboards.map(d => `- [${d.id}] "${d.name}" - ${d.description || "sem descrição"}`).join("\n");
return { success: true, output: `📊 ${dashboards.length} dashboards no Motor BI:\n\n${list}` };
} catch (error: any) {
return { success: false, output: "", error: `Erro: ${error.message}` };
}
}
private async toolMetaSetAddToDashboard(dashboardId: number, questionId: number): Promise<ToolResult> {
try {
const { metasetClient } = await import("../metaset/client");
await metasetClient.addQuestionToDashboard(dashboardId, questionId);
return {
success: true,
output: `📊 Pergunta ${questionId} adicionada ao dashboard ${dashboardId} no Motor BI.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro: ${error.message}` };
}
}
private async toolMetaSetSuggestAnalysis(tableName: string): Promise<ToolResult> {
try {
const { metasetClient } = await import("../metaset/client");
const suggestions = await metasetClient.getAutoSuggestions(tableName);
return {
success: true,
output: `📊 Sugestões de análise para "${tableName}":\n\n**Gráficos recomendados:** ${suggestions.suggestedCharts.join(", ")}\n\n**Consultas sugeridas:**\n${suggestions.suggestedQueries.map((q, i) => `${i + 1}. ${q}`).join("\n")}\n\nUse metaset_create_question com uma destas consultas para criar uma pergunta persistente.`
};
} catch (error: any) {
return { success: false, output: "", error: `Erro: ${error.message}` };
}
}
// ============================================================
// ERPNext Integration Tools
// ============================================================
private async toolErpnextStatus(): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return {
success: true,
output: "⚠️ **ERPNext não configurado**\n\nAs credenciais do ERPNext não estão configuradas. Configure ERPNEXT_URL, ERPNEXT_API_KEY e ERPNEXT_API_SECRET nas secrets do sistema."
};
}
const result = await erpnextService.testConnection();
if (result.success) {
return {
success: true,
output: `✅ **ERPNext Conectado**\n\n**URL:** ${erpnextService.getConfig().url}\n**Usuário:** ${result.user}\n\nVocê pode usar as ferramentas erpnext_* para consultar e manipular dados.`
};
} else {
return {
success: false,
output: "",
error: `Erro ao conectar: ${result.message}`
};
}
} catch (error: any) {
return { success: false, output: "", error: `Erro ao verificar ERPNext: ${error.message}` };
}
}
private async toolErpnextListDoctypes(limit?: number): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return { success: false, output: "", error: "ERPNext não configurado" };
}
const doctypes = await erpnextService.listDocTypes(limit || 50);
let output = `📋 **DocTypes disponíveis no ERPNext** (${doctypes.length})\n\n`;
const grouped: Record<string, string[]> = {};
for (const dt of doctypes) {
const module = dt.module || "Outros";
if (!grouped[module]) grouped[module] = [];
grouped[module].push(dt.name);
}
for (const [module, names] of Object.entries(grouped).sort()) {
output += `**${module}:**\n`;
output += names.slice(0, 10).map(n => `- ${n}`).join('\n');
if (names.length > 10) {
output += `\n- ... e mais ${names.length - 10}`;
}
output += '\n\n';
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao listar DocTypes: ${error.message}` };
}
}
private async toolErpnextGetDocuments(
doctype: string,
filters?: string,
fields?: string,
limit?: number
): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return { success: false, output: "", error: "ERPNext não configurado" };
}
const parsedFilters = filters ? JSON.parse(filters) : undefined;
const parsedFields = fields ? fields.split(',').map(f => f.trim()) : undefined;
const documents = await erpnextService.getDocuments(
doctype,
parsedFilters,
parsedFields,
limit || 20
);
let output = `📄 **${doctype}** (${documents.length} registros)\n\n`;
if (documents.length === 0) {
output += "Nenhum registro encontrado.";
} else {
output += "| " + Object.keys(documents[0] as object).join(" | ") + " |\n";
output += "|" + Object.keys(documents[0] as object).map(() => "---").join("|") + "|\n";
for (const doc of documents.slice(0, 20)) {
output += "| " + Object.values(doc as object).map(v => String(v ?? '-')).join(" | ") + " |\n";
}
if (documents.length > 20) {
output += `\n... e mais ${documents.length - 20} registros`;
}
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao buscar ${doctype}: ${error.message}` };
}
}
private async toolErpnextGetDocument(doctype: string, name: string): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return { success: false, output: "", error: "ERPNext não configurado" };
}
const document = await erpnextService.getDocument(doctype, name);
let output = `📄 **${doctype}: ${name}**\n\n`;
output += "```json\n";
output += JSON.stringify(document, null, 2);
output += "\n```";
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao buscar ${doctype}/${name}: ${error.message}` };
}
}
private async toolErpnextCreateDocument(doctype: string, data: string): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return { success: false, output: "", error: "ERPNext não configurado" };
}
const parsedData = JSON.parse(data);
const document = await erpnextService.createDocument(doctype, parsedData);
let output = `✅ **${doctype} criado com sucesso!**\n\n`;
output += "```json\n";
output += JSON.stringify(document, null, 2);
output += "\n```";
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao criar ${doctype}: ${error.message}` };
}
}
private async toolErpnextUpdateDocument(doctype: string, name: string, data: string): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return { success: false, output: "", error: "ERPNext não configurado" };
}
const parsedData = JSON.parse(data);
const document = await erpnextService.updateDocument(doctype, name, parsedData);
let output = `✅ **${doctype}/${name} atualizado com sucesso!**\n\n`;
output += "```json\n";
output += JSON.stringify(document, null, 2);
output += "\n```";
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao atualizar ${doctype}/${name}: ${error.message}` };
}
}
private async toolErpnextSearch(doctype: string, searchTerm: string, limit?: number): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return { success: false, output: "", error: "ERPNext não configurado" };
}
const documents = await erpnextService.searchDocuments(doctype, searchTerm, undefined, limit || 20);
let output = `🔍 **Busca em ${doctype}** por "${searchTerm}"\n\n`;
if (documents.length === 0) {
output += "Nenhum resultado encontrado.";
} else {
output += `**Resultados:** ${documents.length}\n\n`;
for (const doc of documents) {
output += `- ${JSON.stringify(doc)}\n`;
}
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro na busca: ${error.message}` };
}
}
private async toolErpnextRunReport(reportName: string, filters?: string): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return { success: false, output: "", error: "ERPNext não configurado" };
}
const parsedFilters = filters ? JSON.parse(filters) : undefined;
const result = await erpnextService.runReport(reportName, parsedFilters);
let output = `📊 **Relatório: ${reportName}**\n\n`;
if (result.data.length === 0) {
output += "Nenhum dado encontrado.";
} else {
if (result.columns.length > 0) {
const colNames = (result.columns as any[]).map(c => typeof c === 'string' ? c : c.label || c.fieldname);
output += "| " + colNames.join(" | ") + " |\n";
output += "|" + colNames.map(() => "---").join("|") + "|\n";
}
for (const row of result.data.slice(0, 30)) {
if (Array.isArray(row)) {
output += "| " + row.map(v => String(v ?? '-')).join(" | ") + " |\n";
} else {
output += "| " + Object.values(row as object).map(v => String(v ?? '-')).join(" | ") + " |\n";
}
}
if (result.data.length > 30) {
output += `\n... e mais ${result.data.length - 30} linhas`;
}
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao executar relatório: ${error.message}` };
}
}
private async toolErpnextCallMethod(method: string, args?: string): Promise<ToolResult> {
try {
if (!erpnextService.isConfigured()) {
return { success: false, output: "", error: "ERPNext não configurado" };
}
const parsedArgs = args ? JSON.parse(args) : undefined;
const result = await erpnextService.callMethod(method, parsedArgs);
let output = `⚡ **Método: ${method}**\n\n`;
output += "```json\n";
output += JSON.stringify(result, null, 2);
output += "\n```";
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao chamar método: ${error.message}` };
}
}
// ========== ARCÁDIA RETAIL TOOLS ==========
private parseFilter(filter?: string): Record<string, string> {
const parsed: Record<string, string> = {};
if (!filter) return parsed;
const parts = filter.split(",").map(p => p.trim());
for (const part of parts) {
const [key, value] = part.split("=").map(s => s.trim());
if (key && value) {
parsed[key] = value;
}
}
return parsed;
}
private async toolRetailQuery(entity: string, filter?: string, limit?: number): Promise<ToolResult> {
try {
const maxResults = limit || 20;
const filters = this.parseFilter(filter);
let output = "";
let data: any[] = [];
const conditions: any[] = [];
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
switch (entity.toLowerCase()) {
case "sales":
if (filters.status) conditions.push(eq(posSales.status, filters.status));
if (filters.date === "today") conditions.push(gte(posSales.createdAt, today));
if (filters.store_id) conditions.push(eq(posSales.storeId, parseInt(filters.store_id)));
if (filters.seller_id) conditions.push(eq(posSales.sellerId, parseInt(filters.seller_id)));
data = conditions.length > 0
? await db.select().from(posSales).where(and(...conditions)).orderBy(desc(posSales.createdAt)).limit(maxResults)
: await db.select().from(posSales).orderBy(desc(posSales.createdAt)).limit(maxResults);
output = `🛒 **Vendas do PDV (${data.length} registros)**${filter ? ` [Filtro: ${filter}]` : ''}\n\n`;
output += "| # | Número | Cliente | Total | Status | Data |\n";
output += "|---|--------|---------|-------|--------|------|\n";
data.forEach((s, i) => {
output += `| ${i+1} | ${s.saleNumber || '-'} | ${s.customerName || 'Consumidor'} | R$ ${parseFloat(s.totalAmount || '0').toFixed(2)} | ${s.status || '-'} | ${s.createdAt ? new Date(s.createdAt).toLocaleDateString('pt-BR') : '-'} |\n`;
});
break;
case "devices":
if (filters.status) conditions.push(eq(mobileDevices.status, filters.status));
if (filters.condition) conditions.push(eq(mobileDevices.condition, filters.condition));
if (filters.brand) conditions.push(ilike(mobileDevices.brand, `%${filters.brand}%`));
if (filters.store_id) conditions.push(eq(mobileDevices.storeId, parseInt(filters.store_id)));
data = conditions.length > 0
? await db.select().from(mobileDevices).where(and(...conditions)).orderBy(desc(mobileDevices.createdAt)).limit(maxResults)
: await db.select().from(mobileDevices).orderBy(desc(mobileDevices.createdAt)).limit(maxResults);
output = `📱 **Dispositivos/Celulares (${data.length} registros)**${filter ? ` [Filtro: ${filter}]` : ''}\n\n`;
output += "| # | IMEI | Marca | Modelo | Condição | Status | Preço Venda |\n";
output += "|---|------|-------|--------|----------|--------|-------------|\n";
data.forEach((d, i) => {
output += `| ${i+1} | ${d.imei || '-'} | ${d.brand || '-'} | ${d.model || '-'} | ${d.condition || '-'} | ${d.status || '-'} | R$ ${parseFloat(d.sellingPrice || '0').toFixed(2)} |\n`;
});
break;
case "evaluations":
if (filters.status) conditions.push(eq(deviceEvaluations.status, filters.status));
if (filters.date === "today") conditions.push(gte(deviceEvaluations.createdAt, today));
if (filters.store_id) conditions.push(eq(deviceEvaluations.storeId, parseInt(filters.store_id)));
data = conditions.length > 0
? await db.select().from(deviceEvaluations).where(and(...conditions)).orderBy(desc(deviceEvaluations.createdAt)).limit(maxResults)
: await db.select().from(deviceEvaluations).orderBy(desc(deviceEvaluations.createdAt)).limit(maxResults);
output = `🔍 **Avaliações Trade-In (${data.length} registros)**${filter ? ` [Filtro: ${filter}]` : ''}\n\n`;
output += "| # | IMEI | Marca/Modelo | Cliente | Valor Est. | Status |\n";
output += "|---|------|--------------|---------|------------|--------|\n";
data.forEach((e, i) => {
output += `| ${i+1} | ${e.imei || '-'} | ${e.brand || ''} ${e.model || ''} | ${e.customerName || '-'} | R$ ${parseFloat(e.estimatedValue || '0').toFixed(2)} | ${e.status || '-'} |\n`;
});
break;
case "service_orders":
if (filters.status) conditions.push(eq(serviceOrders.status, filters.status));
if (filters.date === "today") conditions.push(gte(serviceOrders.createdAt, today));
if (filters.store_id) conditions.push(eq(serviceOrders.storeId, parseInt(filters.store_id)));
data = conditions.length > 0
? await db.select().from(serviceOrders).where(and(...conditions)).orderBy(desc(serviceOrders.createdAt)).limit(maxResults)
: await db.select().from(serviceOrders).orderBy(desc(serviceOrders.createdAt)).limit(maxResults);
output = `🔧 **Ordens de Serviço (${data.length} registros)**${filter ? ` [Filtro: ${filter}]` : ''}\n\n`;
output += "| # | Número O.S. | Dispositivo | Cliente | Tipo | Status | Valor |\n";
output += "|---|-------------|-------------|---------|------|--------|-------|\n";
data.forEach((o, i) => {
output += `| ${i+1} | ${o.orderNumber || '-'} | ${o.brand || ''} ${o.model || ''} | ${o.customerName || '-'} | ${o.serviceType || '-'} | ${o.status || '-'} | R$ ${parseFloat(o.totalCost || '0').toFixed(2)} |\n`;
});
break;
case "commissions":
if (filters.status) conditions.push(eq(retailCommissionClosures.status, filters.status));
if (filters.store_id) conditions.push(eq(retailCommissionClosures.storeId, parseInt(filters.store_id)));
if (filters.seller_id) conditions.push(eq(retailCommissionClosures.sellerId, parseInt(filters.seller_id)));
data = conditions.length > 0
? await db.select().from(retailCommissionClosures).where(and(...conditions)).orderBy(desc(retailCommissionClosures.createdAt)).limit(maxResults)
: await db.select().from(retailCommissionClosures).orderBy(desc(retailCommissionClosures.createdAt)).limit(maxResults);
output = `💰 **Fechamentos de Comissões (${data.length} registros)**${filter ? ` [Filtro: ${filter}]` : ''}\n\n`;
output += "| # | Período | Vendedor | Vendas Líquidas | Comissão | Status |\n";
output += "|---|---------|----------|-----------------|----------|--------|\n";
data.forEach((c, i) => {
output += `| ${i+1} | ${c.periodType || '-'} | ${c.sellerName || 'Todos'} | R$ ${parseFloat(c.netSales || '0').toFixed(2)} | R$ ${parseFloat(c.commissionAmount || '0').toFixed(2)} | ${c.status || '-'} |\n`;
});
break;
case "credits":
if (filters.status) conditions.push(eq(customerCredits.status, filters.status));
else conditions.push(eq(customerCredits.status, "active"));
if (filters.customer_id) conditions.push(eq(customerCredits.customerId, parseInt(filters.customer_id)));
data = await db.select().from(customerCredits).where(and(...conditions)).orderBy(desc(customerCredits.createdAt)).limit(maxResults);
output = `💳 **Créditos de Clientes (${data.length} registros)**${filter ? ` [Filtro: ${filter}]` : ''}\n\n`;
output += "| # | Cliente | CPF | Origem | Valor | Saldo | Expira |\n";
output += "|---|---------|-----|--------|-------|-------|--------|\n";
data.forEach((c, i) => {
output += `| ${i+1} | ${c.customerName || '-'} | ${c.customerCpf || '-'} | ${c.origin || '-'} | R$ ${parseFloat(c.amount || '0').toFixed(2)} | R$ ${parseFloat(c.remainingAmount || '0').toFixed(2)} | ${c.expiresAt ? new Date(c.expiresAt).toLocaleDateString('pt-BR') : 'Sem limite'} |\n`;
});
break;
case "sellers":
if (filters.store_id) conditions.push(eq(retailSellers.storeId, parseInt(filters.store_id)));
if (filters.active !== "false") conditions.push(eq(retailSellers.isActive, true));
data = conditions.length > 0
? await db.select().from(retailSellers).where(and(...conditions)).limit(maxResults)
: await db.select().from(retailSellers).where(eq(retailSellers.isActive, true)).limit(maxResults);
output = `👤 **Vendedores (${data.length} registros)**${filter ? ` [Filtro: ${filter}]` : ''}\n\n`;
output += "| # | Nome | Código | Comissão Base | Meta Mensal |\n";
output += "|---|------|--------|---------------|-------------|\n";
data.forEach((s, i) => {
output += `| ${i+1} | ${s.name || '-'} | ${s.code || '-'} | ${parseFloat(s.commissionRate || '0').toFixed(1)}% | R$ ${parseFloat(s.monthlyGoal || '0').toFixed(2)} |\n`;
});
break;
case "stores":
if (filters.active !== "false") conditions.push(eq(retailStores.isActive, true));
data = conditions.length > 0
? await db.select().from(retailStores).where(and(...conditions)).limit(maxResults)
: await db.select().from(retailStores).where(eq(retailStores.isActive, true)).limit(maxResults);
output = `🏪 **Lojas Ativas (${data.length} registros)**\n\n`;
output += "| # | Nome | Código | Cidade | Estado |\n";
output += "|---|------|--------|--------|--------|\n";
data.forEach((s, i) => {
output += `| ${i+1} | ${s.name} | ${s.code || '-'} | ${s.city || '-'} | ${s.state || '-'} |\n`;
});
break;
default:
return { success: false, output: "", error: `Entidade desconhecida: ${entity}. Use: sales, devices, evaluations, service_orders, commissions, credits, sellers, stores` };
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao consultar Retail: ${error.message}` };
}
}
private async toolRetailStats(period?: string, storeId?: number): Promise<ToolResult> {
try {
const now = new Date();
let dateFrom = new Date(now);
dateFrom.setHours(0, 0, 0, 0);
switch (period) {
case "week":
dateFrom.setDate(now.getDate() - 7);
break;
case "month":
dateFrom.setMonth(now.getMonth() - 1);
break;
case "year":
dateFrom.setFullYear(now.getFullYear() - 1);
break;
default:
// today - already set
}
// Build conditions with optional storeId filter
const salesConditions = [gte(posSales.createdAt, dateFrom)];
const deviceConditions = [eq(mobileDevices.status, "in_stock")];
const orderConditions = [or(eq(serviceOrders.status, "open"), eq(serviceOrders.status, "in_progress"))];
const evalConditions = [or(eq(deviceEvaluations.status, "pending"), eq(deviceEvaluations.status, "analyzing"))];
if (storeId) {
salesConditions.push(eq(posSales.storeId, storeId));
deviceConditions.push(eq(mobileDevices.storeId, storeId));
orderConditions.push(eq(serviceOrders.storeId, storeId));
evalConditions.push(eq(deviceEvaluations.storeId, storeId));
}
// Vendas do período
const salesData = await db.select({
count: sql<number>`count(*)`,
total: sql<string>`COALESCE(SUM(total_amount::numeric), 0)`
}).from(posSales)
.where(and(...salesConditions));
// Dispositivos em estoque
const devicesInStock = await db.select({
count: sql<number>`count(*)`
}).from(mobileDevices)
.where(and(...deviceConditions));
// O.S. abertas
const openOrders = await db.select({
count: sql<number>`count(*)`
}).from(serviceOrders)
.where(and(...orderConditions));
// Avaliações pendentes
const pendingEvaluations = await db.select({
count: sql<number>`count(*)`
}).from(deviceEvaluations)
.where(and(...evalConditions));
// Ticket médio
const ticketMedio = salesData[0]?.count > 0
? parseFloat(salesData[0].total) / salesData[0].count
: 0;
const storeLabel = storeId ? ` - Loja #${storeId}` : '';
let output = `📊 **Estatísticas do Retail (${period || 'hoje'}${storeLabel})**\n\n`;
output += "### KPIs Principais\n\n";
output += "| Indicador | Valor |\n";
output += "|-----------|-------|\n";
output += `| 🛒 Vendas Realizadas | ${salesData[0]?.count || 0} |\n`;
output += `| 💰 Faturamento Total | R$ ${parseFloat(salesData[0]?.total || '0').toFixed(2)} |\n`;
output += `| 📈 Ticket Médio | R$ ${ticketMedio.toFixed(2)} |\n`;
output += `| 📱 Dispositivos em Estoque | ${devicesInStock[0]?.count || 0} |\n`;
output += `| 🔧 O.S. Abertas | ${openOrders[0]?.count || 0} |\n`;
output += `| 🔍 Avaliações Pendentes | ${pendingEvaluations[0]?.count || 0} |\n`;
// Insights automáticos
output += "\n### Insights\n\n";
if (openOrders[0]?.count > 5) {
output += "⚠️ **Atenção:** Há muitas O.S. abertas. Considere priorizar a finalização.\n";
}
if (pendingEvaluations[0]?.count > 3) {
output += "⚠️ **Atenção:** Há avaliações Trade-In pendentes. Clientes aguardando resposta.\n";
}
if (devicesInStock[0]?.count < 5) {
output += "⚠️ **Atenção:** Estoque baixo de dispositivos. Considere reposição.\n";
}
if (ticketMedio < 500 && salesData[0]?.count > 0) {
output += "💡 **Dica:** Ticket médio baixo. Considere ofertar acessórios ou garantia estendida.\n";
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao obter estatísticas: ${error.message}` };
}
}
private async toolRetailReport(type: string, dateFrom?: string, dateTo?: string, storeId?: number): Promise<ToolResult> {
try {
const now = new Date();
const startDate = dateFrom ? new Date(dateFrom) : new Date(now.getFullYear(), now.getMonth(), 1);
const endDate = dateTo ? new Date(dateTo) : now;
const storeLabel = storeId ? ` - Loja #${storeId}` : '';
let output = "";
switch (type.toLowerCase()) {
case "sales_summary":
const salesConditions = [
gte(posSales.createdAt, startDate),
lte(posSales.createdAt, endDate)
];
if (storeId) salesConditions.push(eq(posSales.storeId, storeId));
const sales = await db.select().from(posSales)
.where(and(...salesConditions))
.orderBy(desc(posSales.createdAt));
const totalVendas = sales.reduce((sum, s) => sum + parseFloat(s.totalAmount || '0'), 0);
const vendasPorDia: Record<string, number> = {};
sales.forEach(s => {
const dia = new Date(s.createdAt).toLocaleDateString('pt-BR');
vendasPorDia[dia] = (vendasPorDia[dia] || 0) + parseFloat(s.totalAmount || '0');
});
output = `📊 **Relatório de Vendas${storeLabel}**\n`;
output += `Período: ${startDate.toLocaleDateString('pt-BR')} a ${endDate.toLocaleDateString('pt-BR')}\n\n`;
output += "### Resumo\n\n";
output += `| Métrica | Valor |\n|---------|-------|\n`;
output += `| Total de Vendas | ${sales.length} |\n`;
output += `| Faturamento Total | R$ ${totalVendas.toFixed(2)} |\n`;
output += `| Ticket Médio | R$ ${sales.length > 0 ? (totalVendas/sales.length).toFixed(2) : '0.00'} |\n\n`;
output += "### Vendas por Dia\n\n| Data | Faturamento |\n|------|-------------|\n";
Object.entries(vendasPorDia).forEach(([dia, valor]) => {
output += `| ${dia} | R$ ${valor.toFixed(2)} |\n`;
});
break;
case "inventory_status":
const deviceConditions = storeId ? [eq(mobileDevices.storeId, storeId)] : [];
const devices = deviceConditions.length > 0
? await db.select().from(mobileDevices).where(and(...deviceConditions))
: await db.select().from(mobileDevices);
const porStatus: Record<string, number> = {};
const porMarca: Record<string, number> = {};
let valorTotalEstoque = 0;
devices.forEach(d => {
porStatus[d.status || 'unknown'] = (porStatus[d.status || 'unknown'] || 0) + 1;
if (d.status === 'in_stock') {
porMarca[d.brand || 'Outros'] = (porMarca[d.brand || 'Outros'] || 0) + 1;
valorTotalEstoque += parseFloat(d.sellingPrice || '0');
}
});
output = `📱 **Relatório de Estoque de Dispositivos${storeLabel}**\n\n`;
output += "### Por Status\n\n| Status | Quantidade |\n|--------|------------|\n";
Object.entries(porStatus).forEach(([status, qtd]) => {
output += `| ${status} | ${qtd} |\n`;
});
output += "\n### Em Estoque por Marca\n\n| Marca | Quantidade |\n|-------|------------|\n";
Object.entries(porMarca).forEach(([marca, qtd]) => {
output += `| ${marca} | ${qtd} |\n`;
});
output += `\n**Valor Total em Estoque:** R$ ${valorTotalEstoque.toFixed(2)}`;
break;
case "trade_in_analysis":
const evalConditions = [
gte(deviceEvaluations.createdAt, startDate),
lte(deviceEvaluations.createdAt, endDate)
];
if (storeId) evalConditions.push(eq(deviceEvaluations.storeId, storeId));
const evaluations = await db.select().from(deviceEvaluations)
.where(and(...evalConditions
));
const porStatusEval: Record<string, number> = {};
let valorTotalAprovado = 0;
evaluations.forEach(e => {
porStatusEval[e.status] = (porStatusEval[e.status] || 0) + 1;
if (e.status === 'approved') {
valorTotalAprovado += parseFloat(e.estimatedValue || '0');
}
});
output = `🔍 **Análise de Trade-In${storeLabel}**\n`;
output += `Período: ${startDate.toLocaleDateString('pt-BR')} a ${endDate.toLocaleDateString('pt-BR')}\n\n`;
output += "### Por Status\n\n| Status | Quantidade |\n|--------|------------|\n";
Object.entries(porStatusEval).forEach(([status, qtd]) => {
output += `| ${status} | ${qtd} |\n`;
});
output += `\n**Valor Total Aprovado:** R$ ${valorTotalAprovado.toFixed(2)}`;
output += `\n**Taxa de Aprovação:** ${evaluations.length > 0 ? ((porStatusEval['approved'] || 0) / evaluations.length * 100).toFixed(1) : 0}%`;
break;
case "service_orders_pending":
const orderConditions = [or(eq(serviceOrders.status, "open"), eq(serviceOrders.status, "in_progress"))];
if (storeId) orderConditions.push(eq(serviceOrders.storeId, storeId));
const orders = await db.select().from(serviceOrders)
.where(and(...orderConditions))
.orderBy(desc(serviceOrders.createdAt));
output = `🔧 **O.S. Pendentes${storeLabel} (${orders.length} total)**\n\n`;
output += "| O.S. | Dispositivo | Cliente | Tipo | Status | Dias |\n";
output += "|------|-------------|---------|------|--------|------|\n";
orders.forEach(o => {
const dias = Math.floor((now.getTime() - new Date(o.createdAt).getTime()) / (1000 * 60 * 60 * 24));
output += `| ${o.orderNumber} | ${o.brand} ${o.model} | ${o.customerName || '-'} | ${o.serviceType} | ${o.status} | ${dias} |\n`;
});
break;
default:
return { success: false, output: "", error: `Tipo de relatório desconhecido: ${type}. Use: sales_summary, inventory_status, trade_in_analysis, service_orders_pending` };
}
return { success: true, output };
} catch (error: any) {
return { success: false, output: "", error: `Erro ao gerar relatório: ${error.message}` };
}
}
}
export const manusService = new ManusService();
// ============================================================
// FUNÇÕES EXPORTADAS PARA INTEGRAÇÃO MCP/A2A
// ============================================================
/**
* Executa uma ferramenta diretamente (para uso via MCP)
* @param toolName Nome da ferramenta
* @param args Argumentos da ferramenta
* @returns Resultado da execução
*/
// ID do usuário de sistema para integrações MCP/A2A
const SYSTEM_USER_ID = "dev-admin-001"; // Usuário admin do sistema
export async function executeToolForMcp(toolName: string, args: Record<string, any>): Promise<any> {
// @ts-ignore - Acessa método privado para integração MCP
return manusService.executeTool(toolName, args, SYSTEM_USER_ID);
}
/**
* Processa uma mensagem usando o agente (para uso via A2A)
* @param message Mensagem do usuário
* @param sessionId ID da sessão (opcional)
* @returns Resposta do agente
*/
export async function processAgentMessage(message: string, sessionId?: string): Promise<{
output: string;
answer?: string;
toolsUsed?: string[];
chart?: any;
}> {
try {
// Inicia a execução do agente (usa usuário de sistema)
const { runId } = await manusService.run(SYSTEM_USER_ID, message);
// Aguarda a conclusão do agente (polling com timeout)
const maxWaitMs = 120000; // 2 minutos máximo
const pollIntervalMs = 500;
const startTime = Date.now();
let result = null;
while (Date.now() - startTime < maxWaitMs) {
result = await manusService.getRun(runId);
if (!result) {
return { output: "Erro: execução não encontrada", toolsUsed: [] };
}
// Verifica se a execução completou
if (result.status === 'completed' || result.status === 'failed') {
break;
}
// Aguarda antes de verificar novamente
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
}
if (!result) {
return { output: "Erro: execução não encontrada", toolsUsed: [] };
}
// Extrai informações relevantes do resultado
const toolsUsed: string[] = [];
let chart: any = null;
let lastAnswer = "";
if (result.steps) {
for (const step of result.steps) {
if (step.tool) {
toolsUsed.push(step.tool);
}
if (step.tool === 'generate_chart' && step.output) {
try {
chart = JSON.parse(step.output);
} catch (e) {}
}
if (step.tool === 'finish' && step.toolInput) {
try {
const input = typeof step.toolInput === 'string' ? JSON.parse(step.toolInput) : step.toolInput;
lastAnswer = input.answer || "";
} catch (e) {}
}
}
}
return {
output: lastAnswer || result.status || "Processamento concluído",
answer: lastAnswer,
toolsUsed,
chart
};
} catch (error: any) {
return {
output: `Erro ao processar: ${error.message}`,
toolsUsed: []
};
}
}