arcadiasuite/server/bi/routes.ts

872 lines
32 KiB
TypeScript

import type { Express, Request, Response } from "express";
import { db } from "../../db/index";
import {
dataSources, biDatasets, biCharts, biDashboards, biDashboardCharts,
backupJobs, backupArtifacts, automationLogs
} from "@shared/schema";
import { eq, desc, and, sql } from "drizzle-orm";
import { z } from "zod";
import { registerUploadRoutes } from "./upload";
import { registerStagingRoutes } from "./staging";
import OpenAI from "openai";
const dataSourceSchema = z.object({
name: z.string().min(1),
type: z.enum(["postgresql", "mysql", "mongodb", "sqlite", "internal"]),
host: z.string().optional(),
port: z.number().optional(),
database: z.string().optional(),
username: z.string().optional(),
password: z.string().optional(),
connectionString: z.string().optional(),
});
const datasetSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
dataSourceId: z.number().optional(),
queryType: z.enum(["table", "sql", "api"]).optional(),
tableName: z.string().optional(),
sqlQuery: z.string().optional(),
columns: z.string().optional(),
filters: z.string().optional(),
isPublic: z.string().optional(),
});
const chartSchema = z.object({
name: z.string().min(1),
datasetId: z.number(),
chartType: z.enum(["bar", "line", "pie", "area", "scatter", "table", "metric", "donut"]),
config: z.string().optional(),
xAxis: z.string().optional(),
yAxis: z.string().optional(),
groupBy: z.string().optional(),
aggregation: z.string().optional(),
colors: z.string().optional(),
});
const dashboardSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
layout: z.string().optional(),
isPublic: z.string().optional(),
});
const backupJobSchema = z.object({
name: z.string().min(1),
dataSourceId: z.number(),
backupType: z.enum(["full", "schema", "data", "incremental"]),
includeSchema: z.string().optional(),
includeTables: z.string().optional(),
excludeTables: z.string().optional(),
compressionType: z.string().optional(),
retentionDays: z.number().optional(),
storageLocation: z.string().optional(),
});
const SYSTEM_TABLE_CATEGORIES: Record<string, { category: string; description: string }> = {
users: { category: "Sistema", description: "Usuários do sistema" },
applications: { category: "Sistema", description: "Aplicações disponíveis" },
roles: { category: "Sistema", description: "Perfis de acesso" },
pc_clients: { category: "Process Compass", description: "Clientes de consultoria" },
pc_projects: { category: "Process Compass", description: "Projetos de consultoria" },
pc_tasks: { category: "Process Compass", description: "Tarefas de projetos" },
crm_clients: { category: "CRM", description: "Clientes do CRM" },
crm_contracts: { category: "CRM", description: "Contratos" },
crm_partners: { category: "CRM", description: "Parceiros" },
crm_opportunities: { category: "CRM", description: "Oportunidades de negócio" },
crm_leads: { category: "CRM", description: "Leads" },
crm_messages: { category: "CRM", description: "Mensagens enviadas" },
bi_datasets: { category: "BI", description: "Conjuntos de dados" },
bi_charts: { category: "BI", description: "Gráficos criados" },
knowledge_base: { category: "Conhecimento", description: "Base de conhecimento" },
manus_runs: { category: "Manus", description: "Execuções do agente" },
agent_tasks: { category: "Manus", description: "Tarefas do agente" },
whatsapp_messages: { category: "Comunicação", description: "Mensagens WhatsApp" },
conversations: { category: "Comunicação", description: "Conversas" },
};
export function registerBiRoutes(app: Express): void {
registerUploadRoutes(app);
registerStagingRoutes(app);
app.get("/api/bi/internal-tables", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tablesResult = await db.execute(sql`
SELECT
t.table_name,
(SELECT COUNT(*) FROM information_schema.columns c WHERE c.table_name = t.table_name AND c.table_schema = 'public') as column_count,
pg_total_relation_size(quote_ident(t.table_name)) as table_size
FROM information_schema.tables t
WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'
ORDER BY t.table_name
`);
const tables = (tablesResult.rows as any[]).map(row => {
const meta = SYSTEM_TABLE_CATEGORIES[row.table_name] || { category: "Outros", description: row.table_name };
return {
name: row.table_name,
columnCount: parseInt(row.column_count) || 0,
sizeBytes: parseInt(row.table_size) || 0,
category: meta.category,
description: meta.description,
};
});
res.json(tables);
} catch (error) {
console.error("Get internal tables error:", error);
res.status(500).json({ error: "Failed to get internal tables" });
}
});
app.get("/api/bi/internal-tables/:tableName/schema", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tableName = req.params.tableName.replace(/[^a-zA-Z0-9_]/g, '');
const columnsResult = await db.execute(sql`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = ${tableName} AND table_schema = 'public'
ORDER BY ordinal_position
`);
const countResult = await db.execute(sql.raw(`SELECT COUNT(*) as count FROM "${tableName}"`));
const rowCount = parseInt((countResult.rows[0] as any)?.count) || 0;
const previewResult = await db.execute(sql.raw(`SELECT * FROM "${tableName}" LIMIT 5`));
res.json({
tableName,
columns: columnsResult.rows,
rowCount,
preview: previewResult.rows,
});
} catch (error) {
console.error("Get table schema error:", error);
res.status(500).json({ error: "Failed to get table schema" });
}
});
app.post("/api/bi/internal-tables/:tableName/create-dataset", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tableName = req.params.tableName.replace(/[^a-zA-Z0-9_]/g, '');
const { name, description, columns } = req.body;
const [dataset] = await db.insert(biDatasets).values({
userId: req.user!.id,
name: name || tableName,
description: description || `Dataset criado a partir da tabela interna: ${tableName}`,
queryType: "table",
tableName: tableName,
columns: columns ? JSON.stringify(columns) : null,
}).returning();
res.json(dataset);
} catch (error) {
console.error("Create dataset from internal table error:", error);
res.status(500).json({ error: "Failed to create dataset" });
}
});
app.get("/api/bi/data-sources", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
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,
createdAt: dataSources.createdAt,
}).from(dataSources)
.where(eq(dataSources.userId, req.user!.id))
.orderBy(desc(dataSources.createdAt));
res.json(sources);
} catch (error) {
console.error("Get data sources error:", error);
res.status(500).json({ error: "Failed to get data sources" });
}
});
app.post("/api/bi/data-sources", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const parse = dataSourceSchema.safeParse(req.body);
if (!parse.success) {
return res.status(400).json({ error: "Invalid data", details: parse.error.errors });
}
const [source] = await db.insert(dataSources).values({
userId: req.user!.id,
...parse.data,
}).returning();
res.json(source);
} catch (error) {
console.error("Create data source error:", error);
res.status(500).json({ error: "Failed to create data source" });
}
});
app.post("/api/bi/data-sources/:id/test", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const [source] = await db.select().from(dataSources)
.where(and(eq(dataSources.id, id), eq(dataSources.userId, req.user!.id)));
if (!source) {
return res.status(404).json({ error: "Data source not found" });
}
await db.update(dataSources)
.set({ lastTestedAt: new Date(), isActive: "true" })
.where(eq(dataSources.id, id));
res.json({ success: true, message: "Conexão testada com sucesso" });
} catch (error) {
console.error("Test data source error:", error);
res.status(500).json({ error: "Failed to test connection" });
}
});
app.delete("/api/bi/data-sources/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
await db.delete(dataSources)
.where(and(eq(dataSources.id, id), eq(dataSources.userId, req.user!.id)));
res.json({ success: true });
} catch (error) {
console.error("Delete data source error:", error);
res.status(500).json({ error: "Failed to delete data source" });
}
});
app.get("/api/bi/datasets", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const datasets = await db.select().from(biDatasets)
.where(eq(biDatasets.userId, req.user!.id))
.orderBy(desc(biDatasets.updatedAt));
res.json(datasets);
} catch (error) {
console.error("Get datasets error:", error);
res.status(500).json({ error: "Failed to get datasets" });
}
});
app.post("/api/bi/datasets", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const parse = datasetSchema.safeParse(req.body);
if (!parse.success) {
return res.status(400).json({ error: "Invalid data", details: parse.error.errors });
}
const [dataset] = await db.insert(biDatasets).values({
userId: req.user!.id,
...parse.data,
}).returning();
res.json(dataset);
} catch (error) {
console.error("Create dataset error:", error);
res.status(500).json({ error: "Failed to create dataset" });
}
});
app.post("/api/bi/datasets/:id/execute", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const [dataset] = await db.select().from(biDatasets)
.where(and(eq(biDatasets.id, id), eq(biDatasets.userId, req.user!.id)));
if (!dataset) {
return res.status(404).json({ error: "Dataset not found" });
}
const tablesResult = await db.execute(sql`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
`);
const validTables = new Set((tablesResult.rows as any[]).map(r => r.table_name));
if (dataset.sqlQuery) {
const safeQueries = ['SELECT', 'WITH'];
const query = dataset.sqlQuery.trim().toUpperCase();
if (!safeQueries.some(sq => query.startsWith(sq))) {
return res.status(400).json({ error: "Only SELECT queries are allowed" });
}
const dangerousPatterns = [/;\s*(?:DROP|DELETE|UPDATE|INSERT|TRUNCATE|ALTER|CREATE)/i, /--/, /\/\*/];
if (dangerousPatterns.some(p => p.test(dataset.sqlQuery!))) {
return res.status(400).json({ error: "Query contains forbidden patterns" });
}
const result = await db.execute(sql.raw(dataset.sqlQuery));
res.json({ data: result.rows, columns: Object.keys(result.rows[0] || {}) });
} else if (dataset.tableName) {
if (!validTables.has(dataset.tableName)) {
return res.status(400).json({ error: "Invalid table name" });
}
const safeTableName = dataset.tableName.replace(/[^a-zA-Z0-9_]/g, '');
const result = await db.execute(sql`SELECT * FROM ${sql.identifier(safeTableName)} LIMIT 1000`);
res.json({ data: result.rows, columns: Object.keys(result.rows[0] || {}) });
} else {
res.json({ data: [], columns: [] });
}
} catch (error: any) {
console.error("Execute dataset error:", error);
res.status(500).json({ error: error.message || "Failed to execute dataset" });
}
});
app.delete("/api/bi/datasets/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
await db.delete(biDatasets)
.where(and(eq(biDatasets.id, id), eq(biDatasets.userId, req.user!.id)));
res.json({ success: true });
} catch (error) {
console.error("Delete dataset error:", error);
res.status(500).json({ error: "Failed to delete dataset" });
}
});
app.post("/api/bi/query", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { tableName, limit = 100 } = req.body;
if (!tableName) {
return res.status(400).json({ error: "Table name is required" });
}
const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, '');
const safeLimit = Math.min(Math.max(1, parseInt(limit)), 1000);
const result = await db.execute(sql.raw(`SELECT * FROM "${safeTableName}" LIMIT ${safeLimit}`));
res.json(result.rows || []);
} catch (error: any) {
console.error("Query table error:", error);
res.status(500).json({ error: error.message || "Failed to query table" });
}
});
app.get("/api/bi/charts", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const charts = await db.select().from(biCharts)
.where(eq(biCharts.userId, req.user!.id))
.orderBy(desc(biCharts.updatedAt));
res.json(charts);
} catch (error) {
console.error("Get charts error:", error);
res.status(500).json({ error: "Failed to get charts" });
}
});
app.post("/api/bi/charts", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const parse = chartSchema.safeParse(req.body);
if (!parse.success) {
return res.status(400).json({ error: "Invalid data", details: parse.error.errors });
}
const [chart] = await db.insert(biCharts).values({
userId: req.user!.id,
...parse.data,
}).returning();
res.json(chart);
} catch (error) {
console.error("Create chart error:", error);
res.status(500).json({ error: "Failed to create chart" });
}
});
app.delete("/api/bi/charts/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
await db.delete(biCharts)
.where(and(eq(biCharts.id, id), eq(biCharts.userId, req.user!.id)));
res.json({ success: true });
} catch (error) {
console.error("Delete chart error:", error);
res.status(500).json({ error: "Failed to delete chart" });
}
});
app.get("/api/bi/dashboards", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const dashboards = await db.select().from(biDashboards)
.where(eq(biDashboards.userId, req.user!.id))
.orderBy(desc(biDashboards.updatedAt));
res.json(dashboards);
} catch (error) {
console.error("Get dashboards error:", error);
res.status(500).json({ error: "Failed to get dashboards" });
}
});
app.get("/api/bi/dashboards/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const [dashboard] = await db.select().from(biDashboards)
.where(and(eq(biDashboards.id, id), eq(biDashboards.userId, req.user!.id)));
if (!dashboard) {
return res.status(404).json({ error: "Dashboard not found" });
}
const charts = await db.select({
chartId: biDashboardCharts.chartId,
positionX: biDashboardCharts.positionX,
positionY: biDashboardCharts.positionY,
width: biDashboardCharts.width,
height: biDashboardCharts.height,
chart: biCharts,
}).from(biDashboardCharts)
.innerJoin(biCharts, eq(biDashboardCharts.chartId, biCharts.id))
.where(eq(biDashboardCharts.dashboardId, id));
res.json({ ...dashboard, charts });
} catch (error) {
console.error("Get dashboard error:", error);
res.status(500).json({ error: "Failed to get dashboard" });
}
});
app.post("/api/bi/dashboards", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const parse = dashboardSchema.safeParse(req.body);
if (!parse.success) {
return res.status(400).json({ error: "Invalid data", details: parse.error.errors });
}
const [dashboard] = await db.insert(biDashboards).values({
userId: req.user!.id,
...parse.data,
}).returning();
res.json(dashboard);
} catch (error) {
console.error("Create dashboard error:", error);
res.status(500).json({ error: "Failed to create dashboard" });
}
});
app.post("/api/bi/dashboards/:id/charts", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const dashboardId = parseInt(req.params.id);
const { chartId, positionX, positionY, width, height } = req.body;
const [added] = await db.insert(biDashboardCharts).values({
dashboardId,
chartId,
positionX: positionX || 0,
positionY: positionY || 0,
width: width || 6,
height: height || 4,
}).returning();
res.json(added);
} catch (error) {
console.error("Add chart to dashboard error:", error);
res.status(500).json({ error: "Failed to add chart to dashboard" });
}
});
app.delete("/api/bi/dashboards/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
await db.delete(biDashboards)
.where(and(eq(biDashboards.id, id), eq(biDashboards.userId, req.user!.id)));
res.json({ success: true });
} catch (error) {
console.error("Delete dashboard error:", error);
res.status(500).json({ error: "Failed to delete dashboard" });
}
});
app.get("/api/bi/backup-jobs", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const jobs = await db.select({
job: backupJobs,
dataSource: {
name: dataSources.name,
type: dataSources.type,
},
}).from(backupJobs)
.leftJoin(dataSources, eq(backupJobs.dataSourceId, dataSources.id))
.where(eq(backupJobs.userId, req.user!.id))
.orderBy(desc(backupJobs.createdAt));
res.json(jobs.map(j => ({ ...j.job, dataSource: j.dataSource })));
} catch (error) {
console.error("Get backup jobs error:", error);
res.status(500).json({ error: "Failed to get backup jobs" });
}
});
app.post("/api/bi/backup-jobs", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const parse = backupJobSchema.safeParse(req.body);
if (!parse.success) {
return res.status(400).json({ error: "Invalid data", details: parse.error.errors });
}
const [job] = await db.insert(backupJobs).values({
userId: req.user!.id,
...parse.data,
}).returning();
res.json(job);
} catch (error) {
console.error("Create backup job error:", error);
res.status(500).json({ error: "Failed to create backup job" });
}
});
app.post("/api/bi/backup-jobs/:id/run", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const [job] = await db.select().from(backupJobs)
.where(and(eq(backupJobs.id, id), eq(backupJobs.userId, req.user!.id)));
if (!job) {
return res.status(404).json({ error: "Backup job not found" });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `backup_${job.name.replace(/\s/g, '_')}_${timestamp}.sql.gz`;
const [artifact] = await db.insert(backupArtifacts).values({
backupJobId: id,
filename,
filePath: `/backups/${filename}`,
status: "running",
}).returning();
setTimeout(async () => {
await db.update(backupArtifacts)
.set({ status: "completed", completedAt: new Date(), fileSize: Math.floor(Math.random() * 10000000) })
.where(eq(backupArtifacts.id, artifact.id));
}, 3000);
res.json({ success: true, artifactId: artifact.id, message: "Backup iniciado" });
} catch (error) {
console.error("Run backup job error:", error);
res.status(500).json({ error: "Failed to run backup" });
}
});
app.get("/api/bi/backup-artifacts", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const artifacts = await db.select({
artifact: backupArtifacts,
job: {
name: backupJobs.name,
},
}).from(backupArtifacts)
.innerJoin(backupJobs, eq(backupArtifacts.backupJobId, backupJobs.id))
.where(eq(backupJobs.userId, req.user!.id))
.orderBy(desc(backupArtifacts.startedAt))
.limit(50);
res.json(artifacts.map(a => ({ ...a.artifact, jobName: a.job.name })));
} catch (error) {
console.error("Get backup artifacts error:", error);
res.status(500).json({ error: "Failed to get backup artifacts" });
}
});
app.delete("/api/bi/backup-jobs/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
await db.delete(backupJobs)
.where(and(eq(backupJobs.id, id), eq(backupJobs.userId, req.user!.id)));
res.json({ success: true });
} catch (error) {
console.error("Delete backup job error:", error);
res.status(500).json({ error: "Failed to delete backup job" });
}
});
app.get("/api/bi/stats", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userId = req.user!.id;
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));
res.json({
dataSources: datasourceCount?.count || 0,
datasets: datasetCount?.count || 0,
charts: chartCount?.count || 0,
dashboards: dashboardCount?.count || 0,
backupJobs: backupCount?.count || 0,
});
} catch (error) {
console.error("Get BI stats error:", error);
res.status(500).json({ error: "Failed to get stats" });
}
});
app.get("/api/bi/tables", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const result = await db.execute(sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
`);
res.json(result.rows.map((r: any) => r.table_name));
} catch (error) {
console.error("Get tables error:", error);
res.status(500).json({ error: "Failed to get tables" });
}
});
app.get("/api/bi/tables/:name/columns", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tableName = req.params.name;
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
`);
res.json(result.rows);
} catch (error) {
console.error("Get columns error:", error);
res.status(500).json({ error: "Failed to get columns" });
}
});
app.post("/api/bi/ai-analyze", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { question, datasetId } = req.body;
if (!question || !datasetId) {
return res.status(400).json({ error: "Pergunta e dataset são obrigatórios" });
}
const [dataset] = await db.select().from(biDatasets).where(eq(biDatasets.id, datasetId)).limit(1);
if (!dataset || !dataset.tableName) {
return res.status(404).json({ error: "Dataset não encontrado" });
}
const tableNameClean = dataset.tableName.replace(/[^a-zA-Z0-9_]/g, '');
const dataResult = await db.execute(sql`SELECT * FROM ${sql.identifier(tableNameClean)} LIMIT 200`);
const sampleData = dataResult.rows as Record<string, any>[];
if (sampleData.length === 0) {
return res.status(400).json({ error: "Dataset vazio" });
}
let pandasAnalysis = null;
try {
const pandasRes = await fetch("http://localhost:8003/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: sampleData, question }),
});
if (pandasRes.ok) {
pandasAnalysis = await pandasRes.json();
}
} catch (e) {
console.log("Pandas service not available, continuing without");
}
const columns = Object.keys(sampleData[0] || {});
const numericColumns = columns.filter(col => {
const val = sampleData[0][col];
return typeof val === 'number' || (!isNaN(Number(val)) && val !== null && val !== '');
});
const openai = new OpenAI({
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
});
let pandasContext = "";
if (pandasAnalysis) {
pandasContext = `\n\nANÁLISE ESTATÍSTICA (via Pandas):
- Total de registros: ${pandasAnalysis.row_count}
- Colunas: ${pandasAnalysis.column_count}
ESTATÍSTICAS POR COLUNA:
${pandasAnalysis.columns.map((c: any) =>
`- ${c.name} (${c.dtype}): ${c.mean ? `média=${c.mean.toFixed(2)}, mediana=${c.median?.toFixed(2)}, desvio=${c.std?.toFixed(2)}` : `${c.unique_count} valores únicos`}`
).join('\n')}
${pandasAnalysis.correlations ? `CORRELAÇÕES: ${JSON.stringify(pandasAnalysis.correlations)}` : ''}
INSIGHTS AUTOMÁTICOS:
${pandasAnalysis.insights.join('\n- ')}`;
}
const systemPrompt = `Você é um analista de dados especializado em Business Intelligence.
Analise os dados fornecidos e responda à pergunta do usuário de forma detalhada e profissional.
IMPORTANTE: Sua resposta DEVE ser um JSON válido com a seguinte estrutura:
{
"answer": "Sua resposta detalhada à pergunta, incluindo números, percentuais e insights",
"insights": ["Insight 1", "Insight 2", "Insight 3"],
"suggestedChart": {
"type": "bar|line|pie|area|scatter",
"title": "Título do gráfico sugerido",
"xAxis": "nome_da_coluna_x",
"yAxis": "nome_da_coluna_y",
"groupBy": "nome_da_coluna_para_agrupar ou null",
"aggregation": "sum|count|avg|max|min"
}
}
Escolha o tipo de gráfico mais apropriado para visualizar a resposta:
- bar: para comparações entre categorias
- line: para tendências ao longo do tempo
- pie: para proporções de um todo
- area: para volumes ao longo do tempo
- scatter: para correlações entre variáveis`;
const userPrompt = `Dataset: ${dataset.name}
Colunas disponíveis: ${columns.join(", ")}
Colunas numéricas: ${numericColumns.join(", ")}
${pandasContext}
Amostra dos dados (primeiros 10 registros):
${JSON.stringify(sampleData.slice(0, 10), null, 2)}
Pergunta do usuário: ${question}`;
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt }
],
response_format: { type: "json_object" },
max_tokens: 2000,
});
const content = response.choices[0]?.message?.content || "{}";
let result;
try {
result = JSON.parse(content);
} catch {
result = { answer: content, insights: [], suggestedChart: null };
}
res.json({
success: true,
question,
datasetId,
datasetName: dataset.name,
pandasAnalysis: pandasAnalysis ? {
rowCount: pandasAnalysis.row_count,
insights: pandasAnalysis.insights,
suggestedCharts: pandasAnalysis.suggested_charts,
} : null,
...result,
});
} catch (error: any) {
console.error("AI Analysis error:", error);
res.status(500).json({ error: error.message || "Falha na análise" });
}
});
app.post("/api/bi/ai-create-chart", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { datasetId, chartConfig } = req.body;
const userId = req.user!.id;
if (!datasetId || !chartConfig) {
return res.status(400).json({ error: "Dados inválidos" });
}
const [chart] = await db.insert(biCharts).values({
userId,
datasetId,
name: chartConfig.title || "Gráfico IA",
chartType: chartConfig.type || "bar",
config: JSON.stringify({
xAxis: chartConfig.xAxis,
yAxis: chartConfig.yAxis,
groupBy: chartConfig.groupBy,
aggregation: chartConfig.aggregation,
colors: ["#c89b3c", "#4caf50", "#2196f3", "#ff9800", "#9c27b0"],
}),
}).returning();
res.json({ success: true, chart });
} catch (error: any) {
console.error("Create chart error:", error);
res.status(500).json({ error: error.message || "Falha ao criar gráfico" });
}
});
}