Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
348 lines
11 KiB
JavaScript
348 lines
11 KiB
JavaScript
/**
|
|
* Labs AI Integration Routes
|
|
* Handles AI agents, providers, templates endpoints
|
|
* Both v4 (direct DB) and legacy (universal tables) versions
|
|
*/
|
|
import { Router } from 'express';
|
|
import { dbGet, dbRun, dbAll } from '../../../database/connection.js';
|
|
import { apiLogger } from '../../../utils/logger.js';
|
|
import { success, created, notFound, badRequest } from '../../../utils/response.js';
|
|
import { generateId } from './_helpers.js';
|
|
import { getAgentTableIds } from '../../../services/agentTableResolver.js';
|
|
|
|
const router = Router();
|
|
|
|
// === V4 AI INTEGRATION ENDPOINTS ===
|
|
|
|
/**
|
|
* GET /api/v3/labs/ai/agents
|
|
* Get all AI agents from GOD CRM AI Pack
|
|
*/
|
|
router.get('/ai/agents', async (req, res) => {
|
|
try {
|
|
const agents = await dbAll(`
|
|
SELECT a.*, o.name as operator_name, o.integration_key, o.default_model
|
|
FROM ai_agents a
|
|
LEFT JOIN ai_operators o ON a.operator_id = o.id
|
|
ORDER BY a.name
|
|
`);
|
|
success(res, agents);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to get AI agents');
|
|
badRequest(res, 'Failed to get AI agents');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v3/labs/ai/providers
|
|
* Get all AI providers/operators from GOD CRM AI Pack
|
|
*/
|
|
router.get('/ai/providers', async (req, res) => {
|
|
try {
|
|
const providers = await dbAll(`
|
|
SELECT id, name, description, integration_key, default_model,
|
|
supported_models, created_at
|
|
FROM ai_operators
|
|
ORDER BY name
|
|
`);
|
|
success(res, providers);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to get AI providers');
|
|
badRequest(res, 'Failed to get AI providers');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v3/labs/ai/templates
|
|
* Get AI templates (placeholder for MindWorkflow integration)
|
|
*/
|
|
router.get('/ai/templates', async (req, res) => {
|
|
try {
|
|
// For now, return empty array - will be populated when MindWorkflow sync is implemented
|
|
const templates = [];
|
|
success(res, templates);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to get AI templates');
|
|
badRequest(res, 'Failed to get AI templates');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v3/labs/ai/templates/sync
|
|
* Sync templates from MindWorkflow (placeholder)
|
|
*/
|
|
router.post('/ai/templates/sync', async (req, res) => {
|
|
try {
|
|
// Placeholder for MindWorkflow template sync
|
|
apiLogger.info('AI templates sync requested (not yet implemented)');
|
|
success(res, {
|
|
synced: 0,
|
|
message: 'Template sync not yet implemented'
|
|
});
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to sync AI templates');
|
|
badRequest(res, 'Failed to sync AI templates');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v3/labs/ai/templates/:templateId/create-agent
|
|
* Create agent from template (placeholder)
|
|
*/
|
|
router.post('/ai/templates/:templateId/create-agent', async (req, res) => {
|
|
try {
|
|
const { templateId } = req.params;
|
|
const { operator_id, custom_name } = req.body;
|
|
|
|
// Placeholder for agent creation from template
|
|
apiLogger.info({ templateId, operator_id, custom_name }, 'Agent creation from template requested (not yet implemented)');
|
|
|
|
// Return mock agent for now
|
|
const mockAgent = {
|
|
id: Date.now(),
|
|
name: custom_name || `Agent from Template ${templateId}`,
|
|
description: 'Created from MindWorkflow template',
|
|
operator_id,
|
|
created_at: new Date().toISOString()
|
|
};
|
|
|
|
created(res, mockAgent);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to create agent from template');
|
|
badRequest(res, 'Failed to create agent from template');
|
|
}
|
|
});
|
|
|
|
// === LEGACY AI ENDPOINTS (universal tables system) ===
|
|
|
|
// GET /ai/agents (legacy - via universal tables)
|
|
// NOTE: This is registered after the v4 version above.
|
|
// Express uses the first matching route, so v4 version takes precedence.
|
|
// These legacy routes are kept for backward compatibility but won't be reached
|
|
// unless the v4 routes are removed. They are included here to preserve the
|
|
// exact same route registration order as the original file.
|
|
|
|
// Legacy: GET /ai/agents (via universal tables)
|
|
router.get('/ai/agents', async (req, res) => {
|
|
try {
|
|
// ADR-164 Phase 3a: resolve the AI Agents table via the marker-aware helper
|
|
// (earliest table id preserves the old `ORDER BY ut.id ASC LIMIT 1`).
|
|
const agentTableIds = await getAgentTableIds();
|
|
const table = agentTableIds.length ? { id: Math.min(...agentTableIds) } : null;
|
|
|
|
if (!table) {
|
|
return success(res, []);
|
|
}
|
|
|
|
// Get agents from table_rows
|
|
const agents = await dbAll(`
|
|
SELECT tr.id, tr.data
|
|
FROM table_rows tr
|
|
WHERE tr.table_id = ?
|
|
ORDER BY tr.id
|
|
`, [table.id]);
|
|
|
|
// Parse and format agent data
|
|
const formattedAgents = agents.map(row => {
|
|
const data = typeof row.data === 'string' ? JSON.parse(row.data) : row.data;
|
|
return {
|
|
id: row.id,
|
|
name: data.name || 'Unnamed Agent',
|
|
description: data.description || '',
|
|
system_prompt: data.system_prompt || '',
|
|
operator_id: data.operator_id || null
|
|
};
|
|
});
|
|
|
|
success(res, formattedAgents);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to get AI agents');
|
|
badRequest(res, 'Failed to get AI agents');
|
|
}
|
|
});
|
|
|
|
// Legacy: GET /ai/providers (via universal tables)
|
|
router.get('/ai/providers', async (req, res) => {
|
|
try {
|
|
// Find AI Operators table through universal tables system
|
|
const table = await dbGet(`
|
|
SELECT ut.id
|
|
FROM universal_tables ut
|
|
WHERE ut.name = 'AI Operators' OR ut.name = 'ai_operators'
|
|
ORDER BY ut.id ASC
|
|
LIMIT 1
|
|
`);
|
|
|
|
if (!table) {
|
|
return success(res, []);
|
|
}
|
|
|
|
// Get providers from table_rows
|
|
const providers = await dbAll(`
|
|
SELECT tr.id, tr.data
|
|
FROM table_rows tr
|
|
WHERE tr.table_id = ?
|
|
ORDER BY tr.id
|
|
`, [table.id]);
|
|
|
|
// Parse and format provider data
|
|
const formattedProviders = providers.map(row => {
|
|
const data = typeof row.data === 'string' ? JSON.parse(row.data) : row.data;
|
|
return {
|
|
id: row.id,
|
|
name: data.name || 'Unnamed Provider',
|
|
description: data.description || ''
|
|
};
|
|
});
|
|
|
|
success(res, formattedProviders);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to get AI providers');
|
|
badRequest(res, 'Failed to get AI providers');
|
|
}
|
|
});
|
|
|
|
// Legacy: GET /ai/templates - List AI templates
|
|
router.get('/ai/templates', async (req, res) => {
|
|
try {
|
|
const templates = await dbAll('SELECT * FROM labs_ai_templates ORDER BY name');
|
|
success(res, templates);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to get AI templates');
|
|
badRequest(res, 'Failed to get AI templates');
|
|
}
|
|
});
|
|
|
|
// Legacy: POST /ai/templates/:id/create-agent - Create agent from template
|
|
router.post('/ai/templates/:id/create-agent', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { operator_id, custom_name } = req.body;
|
|
|
|
const template = await dbGet('SELECT * FROM labs_ai_templates WHERE id = $1', [id]);
|
|
if (!template) {
|
|
return notFound(res, 'Template');
|
|
}
|
|
|
|
// ADR-164 Phase 3a: resolve the AI Agents table via the marker-aware helper
|
|
// (earliest table id preserves the old `ORDER BY ut.id ASC LIMIT 1`).
|
|
const agentTableIds = await getAgentTableIds();
|
|
const table = agentTableIds.length ? { id: Math.min(...agentTableIds) } : null;
|
|
|
|
if (!table) {
|
|
return badRequest(res, 'AI Agents table not found');
|
|
}
|
|
|
|
// Parse settings and routing_config
|
|
const settings = typeof template.settings === 'string'
|
|
? JSON.parse(template.settings)
|
|
: template.settings || {};
|
|
const routingConfig = typeof template.routing_config === 'string'
|
|
? JSON.parse(template.routing_config)
|
|
: template.routing_config || {};
|
|
|
|
// Create agent data
|
|
const agentData = {
|
|
name: custom_name || template.name,
|
|
description: template.description,
|
|
system_prompt: template.system_prompt,
|
|
user_prompt: template.user_prompt_example,
|
|
operator_id: operator_id || null,
|
|
settings: {
|
|
...settings,
|
|
routing_config: routingConfig,
|
|
source_template: template.mindworkflow_id
|
|
}
|
|
};
|
|
|
|
// Insert into table_rows
|
|
const result = await dbRun(`
|
|
INSERT INTO table_rows (table_id, base_id, data, created_at, updated_at)
|
|
VALUES ($1, $2, $3, datetime('now'), datetime('now'))
|
|
`, [
|
|
table.id,
|
|
generateId('agent'),
|
|
JSON.stringify(agentData)
|
|
]);
|
|
|
|
const agentRowId = result.lastInsertRowid || result.insertId;
|
|
|
|
// Update template reference
|
|
if (agentRowId) {
|
|
await dbRun('UPDATE labs_ai_templates SET ai_agent_id = $1 WHERE id = $2', [agentRowId, id]);
|
|
}
|
|
|
|
// Return the created agent
|
|
const agent = {
|
|
id: agentRowId,
|
|
...agentData
|
|
};
|
|
|
|
created(res, agent, 'Agent created from template');
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to create agent from template');
|
|
badRequest(res, 'Failed to create agent');
|
|
}
|
|
});
|
|
|
|
// Legacy: POST /ai/templates/sync - Sync MindWorkflow templates
|
|
router.post('/ai/templates/sync', async (req, res) => {
|
|
try {
|
|
const mindworkflowTemplates = [
|
|
{
|
|
mindworkflow_id: 'planner_llm',
|
|
name: 'Strategic Planner',
|
|
category: 'text_to_text',
|
|
description: 'Long-form reasoning model for planning multi-step creative pipelines',
|
|
system_prompt: 'You are a senior creative strategist. Respond with numbered steps, each containing a goal, reasoning, and deliverable.',
|
|
user_prompt_example: 'Draft a 5 step plan for launching a brand on social media.',
|
|
settings: JSON.stringify({ model: 'gpt-4o-mini', temperature: 0.4 }),
|
|
routing_config: JSON.stringify({ outputs: [{ id: 'text', type: 'text', enabled: true }] })
|
|
},
|
|
{
|
|
mindworkflow_id: 'tone_refiner',
|
|
name: 'Tone Refiner',
|
|
category: 'text_to_text',
|
|
description: 'Refines copy to match brand voice and tone guidelines',
|
|
system_prompt: 'You are an expert copywriter. Refine the given text to match the specified tone.',
|
|
settings: JSON.stringify({ model: 'gpt-4o-mini', temperature: 0.3 })
|
|
},
|
|
{
|
|
mindworkflow_id: 'code_assistant',
|
|
name: 'Code Assistant',
|
|
category: 'text_to_text',
|
|
description: 'Helps with code generation, review, and debugging',
|
|
system_prompt: 'You are an expert developer. Help with code tasks.',
|
|
settings: JSON.stringify({ model: 'gpt-4o-mini', temperature: 0.2 })
|
|
}
|
|
];
|
|
|
|
let synced = 0;
|
|
for (const tpl of mindworkflowTemplates) {
|
|
const existing = await dbGet(
|
|
'SELECT id FROM labs_ai_templates WHERE mindworkflow_id = $1',
|
|
[tpl.mindworkflow_id]
|
|
);
|
|
|
|
if (!existing) {
|
|
await dbRun(`
|
|
INSERT INTO labs_ai_templates
|
|
(mindworkflow_id, name, category, description, system_prompt, user_prompt_example, settings, routing_config, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())
|
|
`, [
|
|
tpl.mindworkflow_id, tpl.name, tpl.category, tpl.description,
|
|
tpl.system_prompt, tpl.user_prompt_example, tpl.settings, tpl.routing_config || '{}'
|
|
]);
|
|
synced++;
|
|
}
|
|
}
|
|
|
|
const templates = await dbAll('SELECT * FROM labs_ai_templates ORDER BY name');
|
|
success(res, { synced, total: templates.length, templates });
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to sync templates');
|
|
badRequest(res, 'Failed to sync templates');
|
|
}
|
|
});
|
|
|
|
export default router;
|