express-genix 3.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ const { createReactAgent } = require('@langchain/langgraph/prebuilt');
2
+ const { tool } = require('@langchain/core/tools');
3
+ const { HumanMessage } = require('@langchain/core/messages');
4
+ const { z } = require('zod');
5
+ const { getModel } = require('../config/ai');
6
+
7
+ // --- Built-in Tools ---
8
+
9
+ const currentTimeTool = tool(
10
+ async () => new Date().toISOString(),
11
+ {
12
+ name: 'current_time',
13
+ description: 'Get the current date and time in ISO 8601 format',
14
+ schema: z.object({}),
15
+ }
16
+ );
17
+
18
+ const calculatorTool = tool(
19
+ async ({ expression }) => {
20
+ // Only allow digits and math operators — no code injection possible
21
+ const sanitized = expression.replace(/[^0-9+\-*/().%\s]/g, '');
22
+ if (sanitized !== expression.trim()) {
23
+ return 'Error: Expression contains invalid characters. Use only numbers and +, -, *, /, (, ), %';
24
+ }
25
+ try {
26
+ const result = new Function(`"use strict"; return (${sanitized})`)();
27
+ if (!isFinite(result)) return 'Error: Result is not a finite number';
28
+ return String(result);
29
+ } catch {
30
+ return 'Error: Could not evaluate expression';
31
+ }
32
+ },
33
+ {
34
+ name: 'calculator',
35
+ description: 'Evaluate a mathematical expression using standard operators: +, -, *, /, %',
36
+ schema: z.object({
37
+ expression: z.string().describe('Math expression, e.g. "2 + 3 * 4"'),
38
+ }),
39
+ }
40
+ );
41
+
42
+ /**
43
+ * Create a LangGraph ReAct agent with tool-calling capabilities.
44
+ *
45
+ * @param {Object} options
46
+ * @param {Array} options.tools - Additional @langchain/core tools to register
47
+ * @param {string} options.systemPrompt - System instruction for the agent
48
+ * @param {string} options.provider - AI provider (openai, anthropic, ollama)
49
+ * @param {string} options.model - Model name override
50
+ */
51
+ const createAgent = (options = {}) => {
52
+ const tools = [
53
+ currentTimeTool,
54
+ calculatorTool,
55
+ ...(options.tools || []),
56
+ ];
57
+
58
+ const model = getModel(options);
59
+
60
+ return createReactAgent({
61
+ llm: model,
62
+ tools,
63
+ messageModifier: options.systemPrompt || 'You are a helpful assistant. Use the provided tools when needed to answer questions accurately.',
64
+ });
65
+ };
66
+
67
+ /**
68
+ * Run the agent with a single message and return the final response.
69
+ */
70
+ const runAgent = async (message, options = {}) => {
71
+ const agent = createAgent(options);
72
+
73
+ const result = await agent.invoke({
74
+ messages: [new HumanMessage(message)],
75
+ });
76
+
77
+ const lastMessage = result.messages[result.messages.length - 1];
78
+
79
+ return {
80
+ content: lastMessage.content,
81
+ steps: result.messages.length,
82
+ };
83
+ };
84
+
85
+ module.exports = { createAgent, runAgent, currentTimeTool, calculatorTool };
@@ -0,0 +1,54 @@
1
+ const { ChatOpenAI } = require('@langchain/openai');
2
+ const { ChatAnthropic } = require('@langchain/anthropic');
3
+ const { ChatOllama } = require('@langchain/ollama');
4
+ const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
5
+
6
+ /**
7
+ * Create a LangChain chat model based on the configured provider.
8
+ * Supports OpenAI, Anthropic, Google Gemini, and Ollama (local).
9
+ */
10
+ const getModel = (options = {}) => {
11
+ const provider = (options.provider || process.env.AI_PROVIDER || 'openai').toLowerCase();
12
+ const temperature = options.temperature ?? parseFloat(process.env.AI_TEMPERATURE || '0.7');
13
+ const maxTokens = options.maxTokens ?? parseInt(process.env.AI_MAX_TOKENS || '2048', 10);
14
+
15
+ switch (provider) {
16
+ case 'openai':
17
+ return new ChatOpenAI({
18
+ modelName: options.model || process.env.AI_MODEL || 'gpt-4o-mini',
19
+ temperature,
20
+ maxTokens,
21
+ openAIApiKey: process.env.OPENAI_API_KEY,
22
+ });
23
+
24
+ case 'anthropic':
25
+ return new ChatAnthropic({
26
+ modelName: options.model || process.env.AI_MODEL || 'claude-sonnet-4-20250514',
27
+ temperature,
28
+ maxTokens,
29
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
30
+ });
31
+
32
+ case 'gemini':
33
+ return new ChatGoogleGenerativeAI({
34
+ model: options.model || process.env.AI_MODEL || 'gemini-2.0-flash',
35
+ temperature,
36
+ maxOutputTokens: maxTokens,
37
+ apiKey: process.env.GOOGLE_API_KEY,
38
+ });
39
+
40
+ case 'ollama':
41
+ return new ChatOllama({
42
+ model: options.model || process.env.AI_MODEL || 'llama3',
43
+ temperature,
44
+ baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
45
+ });
46
+
47
+ default:
48
+ throw new Error(
49
+ `Unsupported AI provider: "${provider}". Supported: openai, anthropic, gemini, ollama`
50
+ );
51
+ }
52
+ };
53
+
54
+ module.exports = { getModel };
@@ -0,0 +1,102 @@
1
+ const aiService = require('../services/aiService');
2
+ const { runAgent } = require('../agents/graph');
3
+ const { success, error } = require('../utils/response');
4
+
5
+ /**
6
+ * POST /ai/chat — Send a message, get a complete AI response.
7
+ */
8
+ const chatHandler = async (req, res, next) => {
9
+ try {
10
+ const { message, systemPrompt, history, provider, model, temperature, maxTokens } = req.body;
11
+
12
+ if (!message) {
13
+ return res.status(400).json(error('Message is required'));
14
+ }
15
+
16
+ const result = await aiService.chat(message, {
17
+ systemPrompt,
18
+ history,
19
+ provider,
20
+ model,
21
+ temperature,
22
+ maxTokens,
23
+ });
24
+
25
+ res.json(success(result));
26
+ } catch (err) {
27
+ next(err);
28
+ }
29
+ };
30
+
31
+ /**
32
+ * POST /ai/stream — Stream an AI response via Server-Sent Events.
33
+ */
34
+ const streamHandler = async (req, res, next) => {
35
+ try {
36
+ const { message, systemPrompt, history, provider, model, temperature, maxTokens } = req.body;
37
+
38
+ if (!message) {
39
+ return res.status(400).json(error('Message is required'));
40
+ }
41
+
42
+ res.setHeader('Content-Type', 'text/event-stream');
43
+ res.setHeader('Cache-Control', 'no-cache');
44
+ res.setHeader('Connection', 'keep-alive');
45
+
46
+ const generator = aiService.stream(message, {
47
+ systemPrompt,
48
+ history,
49
+ provider,
50
+ model,
51
+ temperature,
52
+ maxTokens,
53
+ });
54
+
55
+ for await (const chunk of generator) {
56
+ res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
57
+ }
58
+
59
+ res.write('data: [DONE]\n\n');
60
+ res.end();
61
+ } catch (err) {
62
+ next(err);
63
+ }
64
+ };
65
+
66
+ /**
67
+ * POST /ai/chain — Run a prompt template chain with variables.
68
+ */
69
+ const chainHandler = async (req, res, next) => {
70
+ try {
71
+ const { template, variables, provider, model } = req.body;
72
+
73
+ if (!template) {
74
+ return res.status(400).json(error('Template is required'));
75
+ }
76
+
77
+ const result = await aiService.chain(template, variables || {}, { provider, model });
78
+ res.json(success({ content: result }));
79
+ } catch (err) {
80
+ next(err);
81
+ }
82
+ };
83
+
84
+ /**
85
+ * POST /ai/agent — Run the LangGraph ReAct agent.
86
+ */
87
+ const agentHandler = async (req, res, next) => {
88
+ try {
89
+ const { message, systemPrompt, provider, model } = req.body;
90
+
91
+ if (!message) {
92
+ return res.status(400).json(error('Message is required'));
93
+ }
94
+
95
+ const result = await runAgent(message, { systemPrompt, provider, model });
96
+ res.json(success(result));
97
+ } catch (err) {
98
+ next(err);
99
+ }
100
+ };
101
+
102
+ module.exports = { chatHandler, streamHandler, chainHandler, agentHandler };
@@ -28,6 +28,19 @@ FRONTEND_URL=http://localhost:3000
28
28
  # File Uploads
29
29
  UPLOAD_DIR=uploads
30
30
  UPLOAD_MAX_SIZE=5242880
31
+ <% } %><% if (hasMCP) { %>
32
+ # MCP Server
33
+ MCP_SERVER_NAME=<%= projectName %>
34
+ <% } %><% if (hasAI) { %>
35
+ # AI Service (LangChain)
36
+ AI_PROVIDER=openai
37
+ AI_MODEL=gpt-4o-mini
38
+ AI_TEMPERATURE=0.7
39
+ AI_MAX_TOKENS=2048
40
+ OPENAI_API_KEY=
41
+ # ANTHROPIC_API_KEY=
42
+ # GOOGLE_API_KEY=
43
+ # OLLAMA_BASE_URL=http://localhost:11434
31
44
  <% } %><% if (db === 'mongodb') { %>
32
45
  # Database
33
46
  MONGO_URI=mongodb://localhost:27017/<%= projectName %>
@@ -28,6 +28,19 @@ FRONTEND_URL=http://localhost:3000
28
28
  # File Uploads
29
29
  UPLOAD_DIR=uploads
30
30
  UPLOAD_MAX_SIZE=5242880
31
+ <% } %><% if (hasMCP) { %>
32
+ # MCP Server
33
+ MCP_SERVER_NAME=<%= projectName %>
34
+ <% } %><% if (hasAI) { %>
35
+ # AI Service (LangChain)
36
+ AI_PROVIDER=openai
37
+ AI_MODEL=gpt-4o-mini
38
+ AI_TEMPERATURE=0.7
39
+ AI_MAX_TOKENS=2048
40
+ OPENAI_API_KEY=
41
+ # ANTHROPIC_API_KEY=
42
+ # GOOGLE_API_KEY=
43
+ # OLLAMA_BASE_URL=http://localhost:11434
31
44
  <% } %><% if (db === 'mongodb') { %>
32
45
  # Database
33
46
  MONGO_URI=mongodb://localhost:27017/<%= projectName %>
@@ -18,7 +18,8 @@
18
18
  "db:migrate": "npx sequelize-cli db:migrate",
19
19
  "db:migrate:undo": "npx sequelize-cli db:migrate:undo",
20
20
  "db:seed": "npx sequelize-cli db:seed:all",
21
- "db:create": "npx sequelize-cli db:create"<% } %>
21
+ "db:create": "npx sequelize-cli db:create"<% } %><% if (hasMCP) { %>,
22
+ "mcp:start": "node src/mcp/server.js"<% } %>
22
23
  },
23
24
  "dependencies": {
24
25
  "express": "^4.21.0",
@@ -31,7 +32,7 @@
31
32
  "swagger-ui-express": "^5.0.1"<% } %><% if (hasAuth) { %>,
32
33
  "jsonwebtoken": "^9.0.2",
33
34
  "bcryptjs": "^2.4.3",
34
- "validator": "^13.12.0",
35
+ "validator": "^13.12.0"<% } %><% if (hasAuth || hasMCP || hasAI) { %>,
35
36
  "zod": "^3.23.0"<% } %><% if (db === 'mongodb') { %>,
36
37
  "mongoose": "^8.8.0"<% } %><% if (db === 'postgresql') { %>,
37
38
  "pg": "^8.13.0",
@@ -46,7 +47,14 @@
46
47
  "@apollo/server": "^4.11.0",
47
48
  "graphql": "^16.9.0",
48
49
  "graphql-tag": "^2.12.6"<% } %><% if (hasBackgroundJobs) { %>,
49
- "bullmq": "^5.12.0"<% } %><% if (logger === 'winston') { %>,
50
+ "bullmq": "^5.12.0"<% } %><% if (hasMCP) { %>,
51
+ "@modelcontextprotocol/sdk": "^1.12.0"<% } %><% if (hasAI) { %>,
52
+ "@langchain/core": "^0.3.0",
53
+ "@langchain/openai": "^0.5.0",
54
+ "@langchain/anthropic": "^0.3.0",
55
+ "@langchain/google-genai": "^0.2.0",
56
+ "@langchain/ollama": "^0.1.0",
57
+ "@langchain/langgraph": "^0.2.0"<% } %><% if (logger === 'winston') { %>,
50
58
  "winston": "^3.15.0"<% } %><% if (logger === 'pino') { %>,
51
59
  "pino": "^9.5.0",
52
60
  "pino-pretty": "^13.0.0"<% } %>
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "<%= projectName %>": {
4
+ "command": "node",
5
+ "args": ["src/mcp/server.js"],
6
+ "cwd": "/path/to/<%= projectName %>"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,151 @@
1
+ require('dotenv').config();
2
+
3
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
4
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
5
+ const { z } = require('zod');
6
+ <% if (hasDatabase) { %>const db = require('../config/database');
7
+ <% } %><% if (hasAuth) { %>const userService = require('../services/userService');
8
+ <% } %><% if (!hasAuth && !isNoDatabase) { %>const exampleService = require('../services/exampleService');
9
+ <% } %><% if (hasEmail) { %>const emailService = require('../services/emailService');
10
+ <% } %>
11
+
12
+ const server = new McpServer({
13
+ name: process.env.MCP_SERVER_NAME || '<%= projectName %>',
14
+ version: '1.0.0',
15
+ });
16
+
17
+ <% if (hasAuth) { %>
18
+ // --- User Management Tools ---
19
+
20
+ server.tool(
21
+ 'list_users',
22
+ 'List all registered users with pagination',
23
+ {
24
+ page: z.number().optional().describe('Page number (default: 1)'),
25
+ limit: z.number().optional().describe('Items per page (default: 20)'),
26
+ },
27
+ async ({ page = 1, limit = 20 }) => {
28
+ const result = await userService.findAll({ page, limit });
29
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
30
+ }
31
+ );
32
+
33
+ server.tool(
34
+ 'get_user',
35
+ 'Get a user by their ID',
36
+ {
37
+ userId: z.string().describe('The user ID to look up'),
38
+ },
39
+ async ({ userId }) => {
40
+ const user = await userService.findById(userId);
41
+ if (!user) {
42
+ return { content: [{ type: 'text', text: 'User not found' }], isError: true };
43
+ }
44
+ const { password, ...safeUser } = user.toJSON ? user.toJSON() : user;
45
+ return { content: [{ type: 'text', text: JSON.stringify(safeUser, null, 2) }] };
46
+ }
47
+ );
48
+
49
+ server.tool(
50
+ 'register_user',
51
+ 'Register a new user account',
52
+ {
53
+ username: z.string().describe('Username for the new account'),
54
+ email: z.string().email().describe('Email address'),
55
+ password: z.string().min(8).describe('Password (min 8 characters)'),
56
+ },
57
+ async ({ username, email, password }) => {
58
+ const bcrypt = require('bcryptjs');
59
+ const existing = await userService.findByEmail(email);
60
+ if (existing) {
61
+ return { content: [{ type: 'text', text: 'A user with this email already exists' }], isError: true };
62
+ }
63
+ const hashedPassword = await bcrypt.hash(password, 12);
64
+ const user = await userService.create({ username, email, password: hashedPassword });
65
+ return {
66
+ content: [{
67
+ type: 'text',
68
+ text: JSON.stringify({ id: user.id, username: user.username, email: user.email, role: user.role || 'user' }, null, 2),
69
+ }],
70
+ };
71
+ }
72
+ );
73
+ <% } %>
74
+ <% if (!hasAuth) { %>
75
+ // --- Example CRUD Tools ---
76
+
77
+ server.tool(
78
+ 'list_examples',
79
+ 'List all example items with pagination',
80
+ {
81
+ page: z.number().optional().describe('Page number (default: 1)'),
82
+ limit: z.number().optional().describe('Items per page (default: 20)'),
83
+ },
84
+ async ({ page = 1, limit = 20 }) => {
85
+ const result = await exampleService.getAll({ page, limit });
86
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
87
+ }
88
+ );
89
+
90
+ server.tool(
91
+ 'create_example',
92
+ 'Create a new example item',
93
+ {
94
+ title: z.string().describe('Item title'),
95
+ description: z.string().optional().describe('Item description'),
96
+ },
97
+ async ({ title, description }) => {
98
+ const item = await exampleService.create({ title, description });
99
+ return { content: [{ type: 'text', text: JSON.stringify(item, null, 2) }] };
100
+ }
101
+ );
102
+ <% } %>
103
+ <% if (hasEmail) { %>
104
+ // --- Email Tools ---
105
+
106
+ server.tool(
107
+ 'send_email',
108
+ 'Send an email to a recipient',
109
+ {
110
+ to: z.string().email().describe('Recipient email address'),
111
+ subject: z.string().describe('Email subject line'),
112
+ html: z.string().describe('Email body in HTML format'),
113
+ },
114
+ async ({ to, subject, html }) => {
115
+ const info = await emailService.sendMail({ to, subject, html });
116
+ return { content: [{ type: 'text', text: `Email sent successfully (ID: ${info.messageId})` }] };
117
+ }
118
+ );
119
+ <% } %>
120
+
121
+ // --- Health Check Tool ---
122
+
123
+ server.tool(
124
+ 'health_check',
125
+ 'Check the application health and status',
126
+ {},
127
+ async () => {
128
+ const health = {
129
+ status: 'OK',
130
+ uptime: `${Math.floor(process.uptime())}s`,
131
+ memoryUsage: `${Math.round(process.memoryUsage().rss / 1024 / 1024)} MB`,
132
+ timestamp: new Date().toISOString(),
133
+ };
134
+ return { content: [{ type: 'text', text: JSON.stringify(health, null, 2) }] };
135
+ }
136
+ );
137
+
138
+ // --- Start MCP Server ---
139
+
140
+ const main = async () => {
141
+ <% if (hasDatabase) { %> await db.connect();
142
+ <% } %>
143
+ const transport = new StdioServerTransport();
144
+ await server.connect(transport);
145
+ console.error(`MCP server "${process.env.MCP_SERVER_NAME || '<%= projectName %>'}" running on stdio`);
146
+ };
147
+
148
+ main().catch((err) => {
149
+ console.error('Failed to start MCP server:', err);
150
+ process.exit(1);
151
+ });
@@ -1,4 +1,4 @@
1
- const logger = require('../utils/logger');
1
+ const { logger } = require('../utils/logger');
2
2
 
3
3
  /**
4
4
  * Audit Logging Middleware
@@ -47,10 +47,12 @@ const auditLog = (action) => (req, res, next) => {
47
47
  }
48
48
  }
49
49
 
50
+ const message = `${auditEntry.method} ${auditEntry.path} ${auditEntry.statusCode} ${auditEntry.duration}`;
51
+
50
52
  if (res.statusCode >= 400) {
51
- logger.warn(auditEntry);
53
+ logger.warn(message, auditEntry);
52
54
  } else {
53
- logger.info(auditEntry);
55
+ logger.info(message, auditEntry);
54
56
  }
55
57
 
56
58
  originalEnd.apply(res, args);
@@ -0,0 +1,126 @@
1
+ const express = require('express');
2
+ const { chatHandler, streamHandler, chainHandler, agentHandler } = require('../controllers/aiController');
3
+
4
+ const router = express.Router();
5
+
6
+ /**
7
+ * @swagger
8
+ * /api<% if (hasApiVersioning) { %>/v1<% } %>/ai/chat:
9
+ * post:
10
+ * summary: Send a chat message to the AI
11
+ * tags: [AI]
12
+ * requestBody:
13
+ * required: true
14
+ * content:
15
+ * application/json:
16
+ * schema:
17
+ * type: object
18
+ * required: [message]
19
+ * properties:
20
+ * message:
21
+ * type: string
22
+ * example: "What is Node.js?"
23
+ * systemPrompt:
24
+ * type: string
25
+ * example: "You are a helpful coding assistant."
26
+ * history:
27
+ * type: array
28
+ * items:
29
+ * type: object
30
+ * properties:
31
+ * role:
32
+ * type: string
33
+ * enum: [user, assistant]
34
+ * content:
35
+ * type: string
36
+ * provider:
37
+ * type: string
38
+ * enum: [openai, anthropic, gemini, ollama]
39
+ * model:
40
+ * type: string
41
+ * responses:
42
+ * 200:
43
+ * description: AI response
44
+ */
45
+ router.post('/chat', chatHandler);
46
+
47
+ /**
48
+ * @swagger
49
+ * /api<% if (hasApiVersioning) { %>/v1<% } %>/ai/stream:
50
+ * post:
51
+ * summary: Stream an AI response via Server-Sent Events
52
+ * tags: [AI]
53
+ * requestBody:
54
+ * required: true
55
+ * content:
56
+ * application/json:
57
+ * schema:
58
+ * type: object
59
+ * required: [message]
60
+ * properties:
61
+ * message:
62
+ * type: string
63
+ * responses:
64
+ * 200:
65
+ * description: SSE stream of AI response chunks
66
+ */
67
+ router.post('/stream', streamHandler);
68
+
69
+ /**
70
+ * @swagger
71
+ * /api<% if (hasApiVersioning) { %>/v1<% } %>/ai/chain:
72
+ * post:
73
+ * summary: Run a prompt template chain with variable substitution
74
+ * tags: [AI]
75
+ * requestBody:
76
+ * required: true
77
+ * content:
78
+ * application/json:
79
+ * schema:
80
+ * type: object
81
+ * required: [template]
82
+ * properties:
83
+ * template:
84
+ * type: string
85
+ * example: "Summarize the following text: {text}"
86
+ * variables:
87
+ * type: object
88
+ * example: { "text": "Node.js is a JavaScript runtime..." }
89
+ * provider:
90
+ * type: string
91
+ * enum: [openai, anthropic, gemini, ollama]
92
+ * responses:
93
+ * 200:
94
+ * description: Chain result
95
+ */
96
+ router.post('/chain', chainHandler);
97
+
98
+ /**
99
+ * @swagger
100
+ * /api<% if (hasApiVersioning) { %>/v1<% } %>/ai/agent:
101
+ * post:
102
+ * summary: Run the LangGraph ReAct agent with tool calling
103
+ * tags: [AI]
104
+ * requestBody:
105
+ * required: true
106
+ * content:
107
+ * application/json:
108
+ * schema:
109
+ * type: object
110
+ * required: [message]
111
+ * properties:
112
+ * message:
113
+ * type: string
114
+ * example: "What time is it right now?"
115
+ * systemPrompt:
116
+ * type: string
117
+ * provider:
118
+ * type: string
119
+ * enum: [openai, anthropic, gemini, ollama]
120
+ * responses:
121
+ * 200:
122
+ * description: Agent response with tool-use steps
123
+ */
124
+ router.post('/agent', agentHandler);
125
+
126
+ module.exports = router;
@@ -5,6 +5,7 @@ const adminRoutes = require('./adminRoutes');
5
5
  <% } else { %>const exampleRoutes = require('./exampleRoutes');
6
6
  <% } %><% if (hasFileUpload) { %>const uploadRoutes = require('./uploadRoutes');
7
7
  <% } %><% if (hasBackgroundJobs) { %>const jobRoutes = require('./jobRoutes');
8
+ <% } %><% if (hasAI) { %>const aiRoutes = require('./aiRoutes');
8
9
  <% } %>
9
10
 
10
11
  const router = express.Router();
@@ -15,6 +16,7 @@ router.use('/admin', adminRoutes);
15
16
  <% } else { %>router.use('/examples', exampleRoutes);
16
17
  <% } %><% if (hasFileUpload) { %>router.use('/uploads', uploadRoutes);
17
18
  <% } %><% if (hasBackgroundJobs) { %>router.use('/jobs', jobRoutes);
19
+ <% } %><% if (hasAI) { %>router.use('/ai', aiRoutes);
18
20
  <% } %>
19
21
 
20
22
  /**