godcrm/backend/routes/v3/ai-agents/adminUpgradeController.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

181 lines
7 KiB
JavaScript

/**
* Admin Upgrade Controller
* POST /upgrade-agents-tables — Upgrade all Agents tables (ADR-091)
*/
import { Router } from 'express';
import { authenticate } from '../../../middleware/auth.js';
import { dbGet, dbRun, dbAll } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import { success, forbidden, error } from '../../../utils/response.js';
const router = Router();
/**
* POST /api/v3/ai/upgrade-agents-tables
* Upgrade all Agents tables to add missing columns (tags, vector, response_mode)
* and backfill existing agent rows with response_mode default.
* ADR-091 Phase 1 Task 2: response_mode on AI Agents table.
* Admin only
*/
router.post('/upgrade-agents-tables', authenticate, async (req, res) => {
try {
const userId = req.user?.id;
const userRole = req.user?.role;
// Only admins can upgrade
if (userRole !== 'admin' && userRole !== 'owner') {
return forbidden(res, 'Admin access required');
}
apiLogger.info({ userId }, 'Upgrading all Agents tables');
// Find all Agents tables — match various naming conventions
const agentsTables = await dbAll(`
SELECT ut.id, ut.name, p.name as project_name, s.name as space_name
FROM universal_tables ut
JOIN projects p ON ut.project_id = p.id
JOIN spaces s ON p.space_id = s.id
WHERE LOWER(ut.name) = 'agents'
OR LOWER(ut.name) = 'ai agents'
OR LOWER(ut.name) LIKE '%ai_agents%'
OR ut.name LIKE '%Agents%'
OR ut.name LIKE '%agents%'
`);
const results = [];
for (const table of agentsTables) {
try {
// Get existing columns
const existingColumns = await dbAll(
'SELECT column_name FROM table_columns WHERE table_id = ?',
[table.id]
);
const existingNames = new Set(existingColumns.map(c => c.column_name));
// Get max order_index
const maxOrderRow = await dbGet(
'SELECT MAX(order_index) as max_order FROM table_columns WHERE table_id = ?',
[table.id]
);
let nextOrder = (maxOrderRow?.max_order || 0) + 1;
const addedColumns = [];
// Check for tags column
if (!existingNames.has('tags')) {
const config = {
icon: '🏷️',
options: [
{ value: 'assistant', label: 'Assistant', color: '#3B82F6' },
{ value: 'builder', label: 'Builder', color: '#10B981' },
{ value: 'analyst', label: 'Analyst', color: '#8B5CF6' },
{ value: 'creative', label: 'Creative', color: '#F59E0B' },
{ value: 'code', label: 'Code', color: '#EF4444' },
{ value: 'data', label: 'Data', color: '#06B6D4' },
{ value: 'utility', label: 'Utility', color: '#6B7280' }
]
};
await dbRun(`
INSERT INTO table_columns (table_id, column_name, display_name, type, config, width, is_required, order_index, created_at, updated_at)
VALUES (?, 'tags', 'Tags', 'multi_select', ?, 150, 0, ?, datetime('now'), datetime('now'))
`, [table.id, JSON.stringify(config), nextOrder++]);
addedColumns.push('tags');
}
// Check for vector column
if (!existingNames.has('vector')) {
const config = {
icon: '🧬',
formula: '{{name}} | {{description}} | {{system_prompt}}',
agent_id: null,
auto_generate: true
};
await dbRun(`
INSERT INTO table_columns (table_id, column_name, display_name, type, config, width, is_required, order_index, created_at, updated_at)
VALUES (?, 'vector', 'Vector', 'vector', ?, 150, 0, ?, datetime('now'), datetime('now'))
`, [table.id, JSON.stringify(config), nextOrder++]);
addedColumns.push('vector');
}
// ADR-091 Phase 1 Task 2: Check for response_mode column
if (!existingNames.has('response_mode')) {
const config = {
icon: '💬',
options: [
{ value: 'always', label: 'Always respond' },
{ value: 'topic_only', label: 'Topic only' },
{ value: 'mention_only', label: 'Mention only' }
],
defaultValue: 'mention_only'
};
await dbRun(`
INSERT INTO table_columns (table_id, column_name, display_name, type, config, width, is_required, order_index, created_at, updated_at)
VALUES (?, 'response_mode', 'Response Mode', 'select', ?, 150, 0, ?, datetime('now'), datetime('now'))
`, [table.id, JSON.stringify(config), nextOrder++]);
addedColumns.push('response_mode');
}
// ADR-085: Check for group_chat_behavior column
if (!existingNames.has('group_chat_behavior')) {
const config = {
icon: '👥',
options: [
{ value: 'silent', label: 'Silent' },
{ value: 'topic_only', label: 'Topic only' },
{ value: 'respond_all', label: 'Respond to all' }
],
defaultValue: 'silent'
};
await dbRun(`
INSERT INTO table_columns (table_id, column_name, display_name, type, config, width, is_required, order_index, created_at, updated_at)
VALUES (?, 'group_chat_behavior', 'Group Chat Behavior', 'select', ?, 170, 0, ?, datetime('now'), datetime('now'))
`, [table.id, JSON.stringify(config), nextOrder++]);
addedColumns.push('group_chat_behavior');
}
// ADR-091: Backfill existing agent rows that lack response_mode in JSON data.
// Default is 'mention_only' — backward compatible with ADR-078 solo/group logic.
let backfilledRows = 0;
const backfillResult = await dbRun(`
UPDATE table_rows
SET data = jsonb_set(data::jsonb, '{response_mode}', '"mention_only"')
WHERE table_id = $1
AND (data::jsonb ->> 'response_mode') IS NULL
`, [table.id]);
backfilledRows = backfillResult?.changes || 0;
results.push({
tableId: table.id,
tableName: table.name,
spaceName: table.space_name,
addedColumns,
backfilledRows,
existingColumns: [...existingNames]
});
if (addedColumns.length > 0 || backfilledRows > 0) {
apiLogger.info({ tableId: table.id, addedColumns, backfilledRows }, 'Upgraded Agents table');
}
} catch (error) {
apiLogger.error({ err: error, tableId: table.id }, 'Failed to upgrade Agents table');
results.push({
tableId: table.id,
error: error.message
});
}
}
return success(res, {
upgraded: results.filter(r => (r.addedColumns?.length > 0) || (r.backfilledRows > 0)).length,
total: agentsTables.length,
results
});
} catch (err) {
apiLogger.error({ err }, 'Error upgrading Agents tables');
return error(res, 'UPGRADE_TABLES_ERROR', 'Failed to upgrade tables: ' + err.message, 500);
}
});
export default router;