godcrm/backend/routes/v3/ai-agents/analyticsController.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

192 lines
6.9 KiB
JavaScript

/**
* Analytics & Logs Controller
* POST /process-prompt, GET /logs/:spaceId, GET /analytics/:spaceId
*/
import { Router } from 'express';
import { dbGet, dbAll } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import { success, error, badRequest } from '../../../utils/response.js';
import { safeParseJSON } from './shared.js';
const router = Router();
/**
* POST /api/v3/ai/process-prompt
*/
router.post('/process-prompt', async (req, res) => {
try {
const { spaceId, promptTemplate, variables = {} } = req.body;
if (!promptTemplate) return badRequest(res, 'Prompt template is required');
const variablePattern = /\{\{(\w+)\.(\w+)\}\}/g;
let processedPrompt = promptTemplate;
const resolvedVariables = {};
const matches = [...promptTemplate.matchAll(variablePattern)];
for (const match of matches) {
const [fullMatch, tableName, columnName] = match;
const variableKey = `${tableName}.${columnName}`;
if (variables[variableKey] !== undefined) {
processedPrompt = processedPrompt.replace(fullMatch, String(variables[variableKey]));
resolvedVariables[variableKey] = variables[variableKey];
continue;
}
if (spaceId) {
try {
const table = await dbGet(`
SELECT ut.id FROM universal_tables ut
JOIN projects p ON ut.project_id = p.id
WHERE p.space_id = ? AND (ut.name LIKE ? OR LOWER(ut.name) LIKE ?)
LIMIT 1
`, [spaceId, `%${tableName}%`, `%${tableName.toLowerCase()}%`]);
if (table) {
const row = await dbGet(`SELECT data FROM table_rows WHERE table_id = ? ORDER BY created_at DESC LIMIT 1`, [table.id]);
if (row) {
const data = safeParseJSON(row.data, {});
const value = data[columnName] || data[columnName.toLowerCase()];
if (value !== undefined) {
processedPrompt = processedPrompt.replace(fullMatch, String(value));
resolvedVariables[variableKey] = value;
}
}
}
} catch (e) {
apiLogger.warn({ err: e.message }, 'Failed to resolve variable ${variableKey}');
}
}
}
return success(res, {
original: promptTemplate, processed: processedPrompt,
variables: resolvedVariables,
unresolvedCount: (processedPrompt.match(variablePattern) || []).length
});
} catch (err) {
apiLogger.error({ err }, 'Error processing prompt');
return error(res, 'PROMPT_PROCESS_ERROR', 'Failed to process prompt template', 500);
}
});
/**
* GET /api/v3/ai/logs/:spaceId
*/
router.get('/logs/:spaceId', async (req, res) => {
try {
const { spaceId } = req.params;
const { limit = 50, offset = 0, agent, status } = req.query;
const table = await dbGet(`
SELECT ut.id FROM universal_tables ut
JOIN projects p ON ut.project_id = p.id
WHERE p.space_id = ? AND (
ut.name LIKE '%Run Logs%' OR ut.name LIKE '%Message Logs%' OR
ut.name LIKE '%Chat History%' OR ut.name LIKE '%Logs%'
)
ORDER BY CASE
WHEN ut.name LIKE '%Run Logs%' THEN 1
WHEN ut.name LIKE '%Chat History%' THEN 2
ELSE 3
END LIMIT 1
`, [spaceId]);
if (!table) return success(res, { logs: [], total: 0 });
let query = `SELECT * FROM table_rows WHERE table_id = ?`;
const params = [table.id];
if (agent) { query += ` AND JSON_EXTRACT(data, '$.agent') = ?`; params.push(agent); }
if (status) { query += ` AND JSON_EXTRACT(data, '$.status') = ?`; params.push(status); }
query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`;
params.push(Number(limit), Number(offset));
const rows = await dbAll(query, params);
const countResult = await dbGet(`SELECT COUNT(*) as total FROM table_rows WHERE table_id = ?`, [table.id]);
const logs = rows.map(row => ({ id: row.id, ...safeParseJSON(row.data, {}), createdAt: row.created_at }));
return success(res, { logs, total: countResult.total, limit: Number(limit), offset: Number(offset) });
} catch (err) {
apiLogger.error({ err }, 'Error fetching AI logs');
return error(res, 'FETCH_LOGS_ERROR', 'Failed to fetch logs', 500);
}
});
/**
* GET /api/v3/ai/analytics/:spaceId
*/
router.get('/analytics/:spaceId', async (req, res) => {
try {
const { spaceId } = req.params;
const table = await dbGet(`
SELECT ut.id FROM universal_tables ut
JOIN projects p ON ut.project_id = p.id
WHERE p.space_id = ? AND (
ut.name LIKE '%Usage Analytics%' OR ut.name LIKE '%AI Analytics%' OR ut.name LIKE '%Аналитика%'
) LIMIT 1
`, [spaceId]);
if (!table) {
const logsTable = await dbGet(`
SELECT ut.id FROM universal_tables ut
JOIN projects p ON ut.project_id = p.id
WHERE p.space_id = ? AND (
ut.name LIKE '%Run Logs%' OR ut.name LIKE '%Message Logs%' OR
ut.name LIKE '%Chat History%' OR ut.name LIKE '%Logs%'
)
ORDER BY CASE
WHEN ut.name LIKE '%Run Logs%' THEN 1
WHEN ut.name LIKE '%Chat History%' THEN 2
ELSE 3
END LIMIT 1
`, [spaceId]);
if (!logsTable) return success(res, { analytics: [], summary: {} });
const rows = await dbAll(`SELECT data FROM table_rows WHERE table_id = ?`, [logsTable.id]);
const stats = { totalCalls: rows.length, totalTokens: 0, totalCost: 0, successRate: 0, avgResponseTime: 0 };
let successCount = 0;
let totalTime = 0;
rows.forEach(row => {
const data = safeParseJSON(row.data, {});
stats.totalTokens += (data.tokens_in || 0) + (data.tokens_out || 0);
stats.totalCost += data.cost || 0;
if (data.status === 'success') successCount++;
totalTime += data.response_time || 0;
});
stats.successRate = rows.length > 0 ? (successCount / rows.length) * 100 : 0;
stats.avgResponseTime = rows.length > 0 ? totalTime / rows.length : 0;
return success(res, { analytics: [], summary: stats });
}
const rows = await dbAll(`SELECT data FROM table_rows WHERE table_id = ?`, [table.id]);
const analytics = rows.map(row => safeParseJSON(row.data, {}));
const summary = analytics.reduce((acc, row) => {
acc.totalCalls += row.calls || 0;
acc.totalTokens += row.tokens_used || 0;
acc.totalCost += row.cost || 0;
acc.totalSuccessful += row.successful || 0;
acc.totalErrors += row.errors || 0;
return acc;
}, { totalCalls: 0, totalTokens: 0, totalCost: 0, totalSuccessful: 0, totalErrors: 0 });
summary.successRate = summary.totalCalls > 0 ? (summary.totalSuccessful / summary.totalCalls) * 100 : 0;
return success(res, { analytics, summary });
} catch (err) {
apiLogger.error({ err }, 'Error fetching analytics');
return error(res, 'FETCH_ANALYTICS_ERROR', 'Failed to fetch analytics', 500);
}
});
export default router;