express-genix 3.0.1 → 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.
- package/README.md +74 -1
- package/index.js +160 -1
- package/lib/ai-cli.js +133 -0
- package/lib/features.js +2 -0
- package/lib/generator.js +57 -7
- package/package.json +1 -1
- package/templates/agents/graph.js.ejs +85 -0
- package/templates/config/ai.js.ejs +54 -0
- package/templates/controllers/aiController.js.ejs +102 -0
- package/templates/core/env.ejs +13 -0
- package/templates/core/env.example.ejs +13 -0
- package/templates/core/package.json.ejs +11 -3
- package/templates/mcp/mcp-config.json.ejs +9 -0
- package/templates/mcp/server.js.ejs +151 -0
- package/templates/middleware/auditLog.js.ejs +5 -3
- package/templates/routes/aiRoutes.js.ejs +126 -0
- package/templates/routes/index.js.ejs +2 -0
- package/templates/services/aiService.js.ejs +80 -0
package/README.md
CHANGED
|
@@ -42,6 +42,13 @@ A production-grade CLI tool that generates Express.js applications with best-in-
|
|
|
42
42
|
- Prometheus metrics (`/metrics` endpoint with prom-client)
|
|
43
43
|
- Audit logging middleware with request tracking and sensitive field redaction
|
|
44
44
|
|
|
45
|
+
**AI & Agent Integration**
|
|
46
|
+
- MCP Server — exposes your API as tools for AI agents (Claude Desktop, Cursor, Copilot)
|
|
47
|
+
- LangChain / LangGraph AI service with chat, streaming (SSE), prompt chains, and ReAct agent
|
|
48
|
+
- Provider-agnostic: OpenAI, Anthropic, Google Gemini, Ollama (local)
|
|
49
|
+
- AI-powered CLI: `express-genix ai "describe your app"` generates a project from natural language
|
|
50
|
+
- **Coda VS Code extension** — in-editor AI chat for Express Genix projects (auto-prompted on AI/MCP projects)
|
|
51
|
+
|
|
45
52
|
**Developer Experience**
|
|
46
53
|
- Interactive prompts — pick language, database, features via checkbox
|
|
47
54
|
- Winston or Pino logger (you choose)
|
|
@@ -84,7 +91,7 @@ You'll be prompted for:
|
|
|
84
91
|
1. **Project name**
|
|
85
92
|
2. **Language** — JavaScript or TypeScript
|
|
86
93
|
3. **Database** — MongoDB, PostgreSQL (Sequelize), PostgreSQL (Prisma), or None
|
|
87
|
-
4. **Features** — Auth, Rate Limiting, Swagger, Redis, Docker, CI/CD, WebSocket, Request ID, Email, File Uploads, Soft Deletes, Audit Logging, Prometheus Metrics, API Versioning, Background Jobs, GraphQL
|
|
94
|
+
4. **Features** — Auth, Rate Limiting, Swagger, Redis, Docker, CI/CD, WebSocket, Request ID, Email, File Uploads, Soft Deletes, Audit Logging, Prometheus Metrics, API Versioning, Background Jobs, GraphQL, MCP Server, AI/LLM Service
|
|
88
95
|
5. **Logger** — Winston or Pino
|
|
89
96
|
|
|
90
97
|
The CLI generates your project, installs dependencies, formats code, and creates an initial git commit.
|
|
@@ -111,6 +118,16 @@ express-genix add websocket # Adds Socket.io setup
|
|
|
111
118
|
express-genix add prisma # Adds Prisma schema, client config, migrations
|
|
112
119
|
```
|
|
113
120
|
|
|
121
|
+
### Generate from natural language (AI)
|
|
122
|
+
|
|
123
|
+
Requires `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in your environment.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
express-genix ai "a task manager API with auth, real-time updates, and email notifications"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The CLI interprets your description, maps it to features, shows the config for confirmation, then generates the project.
|
|
130
|
+
|
|
114
131
|
## Generated Project Structure
|
|
115
132
|
|
|
116
133
|
```
|
|
@@ -120,6 +137,8 @@ my-express-app/
|
|
|
120
137
|
│ ├── controllers/ # Route handlers (auth, user, admin, example)
|
|
121
138
|
│ ├── graphql/ # GraphQL type definitions & resolvers (if selected)
|
|
122
139
|
│ ├── jobs/ # BullMQ background workers (if selected)
|
|
140
|
+
│ ├── mcp/ # MCP server for AI agent tools (if selected)
|
|
141
|
+
│ ├── agents/ # LangGraph ReAct agent (if selected)
|
|
123
142
|
│ ├── middleware/ # Auth, RBAC, validation, error handling, uploads, metrics, audit log
|
|
124
143
|
│ ├── models/ # Database models (Mongoose/Sequelize/Prisma)
|
|
125
144
|
│ ├── routes/ # API route definitions with Swagger annotations
|
|
@@ -132,6 +151,7 @@ my-express-app/
|
|
|
132
151
|
├── migrations/ # Sequelize migrations (if PostgreSQL + Sequelize)
|
|
133
152
|
├── seeders/ # Sequelize seeders (if PostgreSQL + Sequelize)
|
|
134
153
|
├── uploads/ # File upload directory (if selected)
|
|
154
|
+
├── mcp.json # MCP client config for Claude Desktop (if selected)
|
|
135
155
|
├── .github/workflows/ # CI/CD pipeline (if selected)
|
|
136
156
|
├── .env # Auto-generated environment config
|
|
137
157
|
├── .env.example # Template for team sharing
|
|
@@ -189,6 +209,14 @@ my-express-app/
|
|
|
189
209
|
| GET | `/metrics` | Prometheus metrics (prom-client) |
|
|
190
210
|
| GET | `/graphql` | GraphQL Playground (Apollo Server) |
|
|
191
211
|
|
|
212
|
+
### AI / LLM (LangChain)
|
|
213
|
+
| Method | Endpoint | Description |
|
|
214
|
+
|--------|----------|-------------|
|
|
215
|
+
| POST | `/api/ai/chat` | Chat with AI (complete response) |
|
|
216
|
+
| POST | `/api/ai/stream` | Stream AI response (SSE) |
|
|
217
|
+
| POST | `/api/ai/chain` | Run a prompt template chain |
|
|
218
|
+
| POST | `/api/ai/agent` | Run LangGraph ReAct agent with tools |
|
|
219
|
+
|
|
192
220
|
## Available Scripts
|
|
193
221
|
|
|
194
222
|
```bash
|
|
@@ -212,6 +240,7 @@ npm run prisma:studio # Open Prisma Studio
|
|
|
212
240
|
express-genix init --skip-install # Skip npm install (useful for CI)
|
|
213
241
|
express-genix init --skip-cleanup # Skip auto-formatting (for debugging)
|
|
214
242
|
express-genix add <feature> # Add feature to existing project
|
|
243
|
+
express-genix ai "<description>" # Generate project from natural language
|
|
215
244
|
```
|
|
216
245
|
|
|
217
246
|
## Environment Variables
|
|
@@ -237,6 +266,14 @@ Generated `.env` includes auto-generated JWT secrets:
|
|
|
237
266
|
| `EMAIL_FROM` | Default sender address | — |
|
|
238
267
|
| `UPLOAD_MAX_SIZE` | Max file upload size in bytes | `5242880` (5 MB) |
|
|
239
268
|
| `UPLOAD_DIR` | Upload destination directory | `uploads` |
|
|
269
|
+
| `MCP_SERVER_NAME` | MCP server display name | project name |
|
|
270
|
+
| `AI_PROVIDER` | LLM provider (`openai`, `anthropic`, `ollama`) | `openai` |
|
|
271
|
+
| `AI_MODEL` | Model name | `gpt-4o-mini` |
|
|
272
|
+
| `AI_TEMPERATURE` | Sampling temperature | `0.7` |
|
|
273
|
+
| `AI_MAX_TOKENS` | Max response tokens | `2048` |
|
|
274
|
+
| `OPENAI_API_KEY` | OpenAI API key (if using OpenAI) | — |
|
|
275
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key (if using Anthropic) | — |
|
|
276
|
+
| `OLLAMA_BASE_URL` | Ollama server URL (if using Ollama) | `http://localhost:11434` |
|
|
240
277
|
|
|
241
278
|
## Docker
|
|
242
279
|
|
|
@@ -246,6 +283,42 @@ docker-compose up --build
|
|
|
246
283
|
|
|
247
284
|
The generated `docker-compose.yml` includes services for your app and its dependencies (MongoDB/PostgreSQL, Redis) with health checks, volumes, and a shared network. The `Dockerfile` uses multi-stage builds, runs as a non-root user, and only copies production dependencies.
|
|
248
285
|
|
|
286
|
+
## Coda VS Code Extension
|
|
287
|
+
|
|
288
|
+
When you select **AI** or **MCP** features, the CLI prompts you to install the **Coda** VS Code extension — an in-editor AI assistant that talks to your running project's AI endpoints.
|
|
289
|
+
|
|
290
|
+
### What it does
|
|
291
|
+
|
|
292
|
+
- **Sidebar chat** — a chat panel in the VS Code activity bar that streams responses from your project's `/ai/chat` and `/ai/stream` endpoints
|
|
293
|
+
- **`@coda` in Copilot Chat** — type `@coda` in the native Copilot Chat to interact with your project's AI agent
|
|
294
|
+
- **Agent mode** — toggle between Chat (streaming) and Agent (LangGraph tool-calling) modes
|
|
295
|
+
- **Auto-detection** — activates automatically when it detects an Express Genix project
|
|
296
|
+
- **Code actions** — copy or insert code blocks from AI responses directly into your editor
|
|
297
|
+
|
|
298
|
+
### Install manually
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Download from GitHub Releases
|
|
302
|
+
curl -sLO https://github.com/LambdaAI001/coda/releases/latest/download/coda-ai-0.1.0.vsix
|
|
303
|
+
|
|
304
|
+
# Install
|
|
305
|
+
code --install-extension coda-ai-0.1.0.vsix
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Or grab it from [github.com/LambdaAI001/coda/releases](https://github.com/LambdaAI001/coda/releases).
|
|
309
|
+
|
|
310
|
+
### Configuration
|
|
311
|
+
|
|
312
|
+
In VS Code Settings (search "Coda"):
|
|
313
|
+
|
|
314
|
+
| Setting | Default | Description |
|
|
315
|
+
|---------|---------|-------------|
|
|
316
|
+
| `coda.serverUrl` | `http://localhost:3000` | Your Express server URL |
|
|
317
|
+
| `coda.apiPrefix` | `/api/v1` | API prefix path |
|
|
318
|
+
| `coda.aiProvider` | `openai` | AI provider (openai, anthropic, gemini, ollama) |
|
|
319
|
+
| `coda.model` | *(server default)* | Model override |
|
|
320
|
+
| `coda.systemPrompt` | *Coding assistant prompt* | Default system prompt |
|
|
321
|
+
|
|
249
322
|
## Contributing
|
|
250
323
|
|
|
251
324
|
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
|
package/index.js
CHANGED
|
@@ -14,6 +14,50 @@ const prompt = inquirer.createPromptModule();
|
|
|
14
14
|
|
|
15
15
|
const generateSecret = (length = 64) => crypto.randomBytes(length).toString('hex');
|
|
16
16
|
|
|
17
|
+
const CODA_EXTENSION_ID = 'express-genix.coda-ai';
|
|
18
|
+
const CODA_VSIX_URL = 'https://github.com/LambdaAI001/coda/releases/latest/download/coda-ai-0.1.0.vsix';
|
|
19
|
+
|
|
20
|
+
async function promptCodaExtension() {
|
|
21
|
+
const inquirerPrompt = inquirer.createPromptModule();
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log('🤖 Your project includes AI/MCP features.');
|
|
24
|
+
console.log(' The Coda VS Code extension lets you chat with your AI agent');
|
|
25
|
+
console.log(' directly from VS Code — no extra setup needed.');
|
|
26
|
+
console.log('');
|
|
27
|
+
|
|
28
|
+
const { installCoda } = await inquirerPrompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'confirm',
|
|
31
|
+
name: 'installCoda',
|
|
32
|
+
message: 'Install the Coda AI VS Code extension?',
|
|
33
|
+
default: true,
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
if (!installCoda) return;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Check if VS Code CLI is available
|
|
41
|
+
execSync('code --version', { stdio: 'pipe' });
|
|
42
|
+
|
|
43
|
+
// Download .vsix from GitHub Releases and install
|
|
44
|
+
const os = require('os');
|
|
45
|
+
const tmpFile = path.join(os.tmpdir(), 'coda-ai.vsix');
|
|
46
|
+
console.log('\n📦 Downloading Coda AI extension...');
|
|
47
|
+
execSync(`curl -sL -o "${tmpFile}" "${CODA_VSIX_URL}"`, { stdio: 'pipe' });
|
|
48
|
+
console.log('📦 Installing...');
|
|
49
|
+
execSync(`code --install-extension "${tmpFile}"`, { stdio: 'pipe' });
|
|
50
|
+
// Clean up temp file
|
|
51
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
52
|
+
console.log('✅ Coda extension installed! Open your project in VS Code to use it.');
|
|
53
|
+
console.log(' Look for the Coda AI icon in the sidebar, or type @coda in Copilot Chat.');
|
|
54
|
+
} catch {
|
|
55
|
+
console.log('\n⚠️ Could not install automatically. Install it manually:');
|
|
56
|
+
console.log(' 1. Download from: https://github.com/LambdaAI001/coda/releases');
|
|
57
|
+
console.log(' 2. Then run: code --install-extension coda-ai-0.1.0.vsix');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
17
61
|
async function main() {
|
|
18
62
|
program
|
|
19
63
|
.name('express-genix')
|
|
@@ -83,6 +127,8 @@ async function main() {
|
|
|
83
127
|
{ name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
|
|
84
128
|
{ name: 'Background Jobs (BullMQ)', value: 'backgroundJobs' },
|
|
85
129
|
{ name: 'GraphQL (Apollo Server)', value: 'graphql' },
|
|
130
|
+
{ name: 'MCP Server (AI Agent Tools)', value: 'mcp' },
|
|
131
|
+
{ name: 'AI / LLM Service (LangChain)', value: 'ai' },
|
|
86
132
|
],
|
|
87
133
|
when: (ans) => ans.db !== 'none',
|
|
88
134
|
},
|
|
@@ -102,6 +148,8 @@ async function main() {
|
|
|
102
148
|
{ name: 'Prometheus Metrics', value: 'metrics' },
|
|
103
149
|
{ name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
|
|
104
150
|
{ name: 'GraphQL (Apollo Server)', value: 'graphql' },
|
|
151
|
+
{ name: 'MCP Server (AI Agent Tools)', value: 'mcp' },
|
|
152
|
+
{ name: 'AI / LLM Service (LangChain)', value: 'ai' },
|
|
105
153
|
],
|
|
106
154
|
when: (ans) => ans.db === 'none',
|
|
107
155
|
},
|
|
@@ -142,6 +190,8 @@ async function main() {
|
|
|
142
190
|
hasApiVersioning: features.includes('apiVersioning'),
|
|
143
191
|
hasBackgroundJobs: features.includes('backgroundJobs'),
|
|
144
192
|
hasGraphQL: features.includes('graphql'),
|
|
193
|
+
hasMCP: features.includes('mcp'),
|
|
194
|
+
hasAI: features.includes('ai'),
|
|
145
195
|
jwtSecret: generateSecret(),
|
|
146
196
|
jwtRefreshSecret: generateSecret(),
|
|
147
197
|
};
|
|
@@ -184,7 +234,7 @@ async function main() {
|
|
|
184
234
|
To get started:
|
|
185
235
|
cd ${config.projectName}
|
|
186
236
|
npm run dev
|
|
187
|
-
${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}${config.hasGraphQL ? `\nGraphQL Playground: http://localhost:3000/graphql` : ''}${config.hasMetrics ? `\nPrometheus Metrics: http://localhost:3000/metrics` : ''}
|
|
237
|
+
${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}${config.hasGraphQL ? `\nGraphQL Playground: http://localhost:3000/graphql` : ''}${config.hasMetrics ? `\nPrometheus Metrics: http://localhost:3000/metrics` : ''}${config.hasMCP ? `\nMCP Server: npm run mcp:start` : ''}${config.hasAI ? `\nAI Endpoints: http://localhost:3000/api${config.hasApiVersioning ? '/v1' : ''}/ai/chat` : ''}
|
|
188
238
|
Health Check: http://localhost:3000/health
|
|
189
239
|
|
|
190
240
|
Available scripts:
|
|
@@ -203,6 +253,11 @@ Configuration:
|
|
|
203
253
|
Features: ${features.join(', ') || 'base'}
|
|
204
254
|
`);
|
|
205
255
|
|
|
256
|
+
// Prompt to install Coda VS Code extension for AI/MCP projects
|
|
257
|
+
if (config.hasAI || config.hasMCP) {
|
|
258
|
+
await promptCodaExtension();
|
|
259
|
+
}
|
|
260
|
+
|
|
206
261
|
} catch (error) {
|
|
207
262
|
// Rollback: remove partial project directory on failure
|
|
208
263
|
if (fs.existsSync(projectDir)) {
|
|
@@ -245,6 +300,110 @@ Configuration:
|
|
|
245
300
|
}
|
|
246
301
|
});
|
|
247
302
|
|
|
303
|
+
program
|
|
304
|
+
.command('ai <description>')
|
|
305
|
+
.description('Generate a project from a natural language description (requires OPENAI_API_KEY or ANTHROPIC_API_KEY)')
|
|
306
|
+
.option('--skip-install', 'Skip npm install')
|
|
307
|
+
.option('--skip-cleanup', 'Skip post-generation cleanup')
|
|
308
|
+
.action(async (description, options) => {
|
|
309
|
+
const { runAiCommand } = require('./lib/ai-cli');
|
|
310
|
+
const aiConfig = await runAiCommand(description);
|
|
311
|
+
|
|
312
|
+
const inquirerPrompt = inquirer.createPromptModule();
|
|
313
|
+
const { confirmed } = await inquirerPrompt([
|
|
314
|
+
{
|
|
315
|
+
type: 'confirm',
|
|
316
|
+
name: 'confirmed',
|
|
317
|
+
message: 'Generate project with this configuration?',
|
|
318
|
+
default: true,
|
|
319
|
+
},
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
if (!confirmed) {
|
|
323
|
+
console.log('Cancelled.');
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const features = aiConfig.features || [];
|
|
328
|
+
const config = {
|
|
329
|
+
projectName: aiConfig.projectName,
|
|
330
|
+
language: aiConfig.language || 'javascript',
|
|
331
|
+
db: aiConfig.db || 'none',
|
|
332
|
+
logger: aiConfig.logger || 'winston',
|
|
333
|
+
hasDatabase: aiConfig.db !== 'none',
|
|
334
|
+
isNoDatabase: aiConfig.db === 'none',
|
|
335
|
+
isPrisma: aiConfig.db === 'prisma',
|
|
336
|
+
isTypescript: aiConfig.language === 'typescript',
|
|
337
|
+
hasAuth: features.includes('auth') && aiConfig.db !== 'none',
|
|
338
|
+
hasRateLimit: features.includes('rateLimit'),
|
|
339
|
+
hasSwagger: features.includes('swagger'),
|
|
340
|
+
hasDocker: features.includes('docker'),
|
|
341
|
+
hasCicd: features.includes('cicd'),
|
|
342
|
+
hasWebsocket: features.includes('websocket'),
|
|
343
|
+
hasRequestId: features.includes('requestId'),
|
|
344
|
+
hasRedis: features.includes('redis') && features.includes('auth') && aiConfig.db !== 'none',
|
|
345
|
+
hasEmail: features.includes('email') && aiConfig.db !== 'none',
|
|
346
|
+
hasFileUpload: features.includes('fileUpload'),
|
|
347
|
+
hasSoftDelete: features.includes('softDelete') && aiConfig.db !== 'none',
|
|
348
|
+
hasAuditLog: features.includes('auditLog'),
|
|
349
|
+
hasMetrics: features.includes('metrics'),
|
|
350
|
+
hasApiVersioning: features.includes('apiVersioning'),
|
|
351
|
+
hasBackgroundJobs: features.includes('backgroundJobs'),
|
|
352
|
+
hasGraphQL: features.includes('graphql'),
|
|
353
|
+
hasMCP: features.includes('mcp'),
|
|
354
|
+
hasAI: features.includes('ai'),
|
|
355
|
+
jwtSecret: generateSecret(),
|
|
356
|
+
jwtRefreshSecret: generateSecret(),
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const projectDir = path.join(process.cwd(), config.projectName);
|
|
360
|
+
|
|
361
|
+
if (fs.existsSync(projectDir)) {
|
|
362
|
+
console.error(`\n❌ Directory "${config.projectName}" already exists!`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const dbLabel = config.isNoDatabase ? 'no database' : config.db;
|
|
367
|
+
console.log(`\n🚀 Creating ${config.projectName} (${config.language}, ${dbLabel})...\n`);
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
await generateProject(config, projectDir, options);
|
|
371
|
+
|
|
372
|
+
if (!options.skipCleanup) {
|
|
373
|
+
await runPostGenerationCleanup(projectDir, config);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
execSync('git init', { cwd: projectDir, stdio: 'pipe' });
|
|
378
|
+
execSync('git add -A', { cwd: projectDir, stdio: 'pipe' });
|
|
379
|
+
execSync('git commit -m "Initial commit from express-genix ai"', {
|
|
380
|
+
cwd: projectDir,
|
|
381
|
+
stdio: 'pipe',
|
|
382
|
+
env: { ...process.env, GIT_COMMITTER_NAME: 'express-genix', GIT_COMMITTER_EMAIL: 'cli@express-genix.dev', GIT_AUTHOR_NAME: 'express-genix', GIT_AUTHOR_EMAIL: 'cli@express-genix.dev' },
|
|
383
|
+
});
|
|
384
|
+
} catch {
|
|
385
|
+
// Git not available
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
console.log(`\n✅ Project ${config.projectName} created from AI description!\n`);
|
|
389
|
+
console.log(` cd ${config.projectName}`);
|
|
390
|
+
console.log(' npm run dev\n');
|
|
391
|
+
|
|
392
|
+
// Prompt to install Coda VS Code extension for AI/MCP projects
|
|
393
|
+
if (config.hasAI || config.hasMCP) {
|
|
394
|
+
await promptCodaExtension();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (fs.existsSync(projectDir)) {
|
|
399
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
400
|
+
console.error('\n🧹 Rolled back partial project directory.');
|
|
401
|
+
}
|
|
402
|
+
console.error(`\n❌ Failed to create project: ${error.message}`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
248
407
|
await program.parseAsync(process.argv);
|
|
249
408
|
}
|
|
250
409
|
|
package/lib/ai-cli.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI CLI handler — interprets natural language project descriptions
|
|
3
|
+
* and maps them to express-genix configuration flags.
|
|
4
|
+
*
|
|
5
|
+
* Requires @langchain/openai or @langchain/anthropic to be installed.
|
|
6
|
+
* These are NOT bundled with express-genix to keep the CLI lightweight.
|
|
7
|
+
*
|
|
8
|
+
* Install with: npm install -g @langchain/core @langchain/openai
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SYSTEM_PROMPT = `You are an expert Express.js project configurator for the express-genix CLI tool.
|
|
12
|
+
Given a natural language description of a web application, output a JSON configuration object.
|
|
13
|
+
|
|
14
|
+
Available options:
|
|
15
|
+
- projectName: string (lowercase, hyphens only, no spaces)
|
|
16
|
+
- language: "javascript" | "typescript"
|
|
17
|
+
- db: "mongodb" | "postgresql" | "prisma" | "none"
|
|
18
|
+
- logger: "winston" | "pino"
|
|
19
|
+
- features: array of feature strings from this list:
|
|
20
|
+
- "auth" (JWT authentication — requires a database)
|
|
21
|
+
- "rateLimit" (rate limiting)
|
|
22
|
+
- "swagger" (Swagger/OpenAPI docs)
|
|
23
|
+
- "redis" (Redis token blacklist — requires auth)
|
|
24
|
+
- "docker" (Docker & Docker Compose)
|
|
25
|
+
- "cicd" (GitHub Actions CI/CD)
|
|
26
|
+
- "websocket" (WebSocket via Socket.io)
|
|
27
|
+
- "requestId" (request ID / correlation tracking)
|
|
28
|
+
- "email" (Nodemailer email service — requires database)
|
|
29
|
+
- "fileUpload" (Multer file uploads)
|
|
30
|
+
- "softDelete" (soft deletes — requires database + auth)
|
|
31
|
+
- "auditLog" (audit logging middleware)
|
|
32
|
+
- "metrics" (Prometheus metrics)
|
|
33
|
+
- "apiVersioning" (API versioning /api/v1)
|
|
34
|
+
- "backgroundJobs" (BullMQ background jobs)
|
|
35
|
+
- "graphql" (GraphQL via Apollo Server)
|
|
36
|
+
- "mcp" (MCP Server — exposes API as AI-agent tools)
|
|
37
|
+
- "ai" (LangChain/LangGraph AI service)
|
|
38
|
+
|
|
39
|
+
Rules:
|
|
40
|
+
- Always include "auth", "rateLimit", "swagger", "requestId" unless explicitly unwanted
|
|
41
|
+
- Include "docker" for production-ready apps
|
|
42
|
+
- Include "ai" if the description mentions AI, chatbot, LLM, or intelligence
|
|
43
|
+
- Include "mcp" if the description mentions AI agents, tools, or MCP
|
|
44
|
+
- Choose "prisma" for modern stacks, "mongodb" for documents, "postgresql" for relational
|
|
45
|
+
- Infer a short kebab-case project name from the description
|
|
46
|
+
- Output ONLY valid JSON. No explanation, no markdown fences.`;
|
|
47
|
+
|
|
48
|
+
const runAiCommand = async (description) => {
|
|
49
|
+
// Check for API key
|
|
50
|
+
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
51
|
+
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
52
|
+
const hasGemini = !!process.env.GOOGLE_API_KEY;
|
|
53
|
+
|
|
54
|
+
if (!hasOpenAI && !hasAnthropic && !hasGemini) {
|
|
55
|
+
console.error('❌ The AI command requires an API key.');
|
|
56
|
+
console.error(' Set one of these environment variables:');
|
|
57
|
+
console.error(' export OPENAI_API_KEY=sk-...');
|
|
58
|
+
console.error(' export ANTHROPIC_API_KEY=sk-ant-...');
|
|
59
|
+
console.error(' export GOOGLE_API_KEY=AI...');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Try to load LangChain (not bundled — must be installed separately)
|
|
64
|
+
let ChatModel;
|
|
65
|
+
try {
|
|
66
|
+
if (hasGemini) {
|
|
67
|
+
const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
|
|
68
|
+
ChatModel = new ChatGoogleGenerativeAI({
|
|
69
|
+
model: 'gemini-2.0-flash',
|
|
70
|
+
maxOutputTokens: 1024,
|
|
71
|
+
apiKey: process.env.GOOGLE_API_KEY,
|
|
72
|
+
});
|
|
73
|
+
} else if (hasAnthropic) {
|
|
74
|
+
const { ChatAnthropic } = require('@langchain/anthropic');
|
|
75
|
+
ChatModel = new ChatAnthropic({
|
|
76
|
+
modelName: 'claude-sonnet-4-20250514',
|
|
77
|
+
maxTokens: 1024,
|
|
78
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
const { ChatOpenAI } = require('@langchain/openai');
|
|
82
|
+
ChatModel = new ChatOpenAI({
|
|
83
|
+
modelName: 'gpt-4o-mini',
|
|
84
|
+
maxTokens: 1024,
|
|
85
|
+
openAIApiKey: process.env.OPENAI_API_KEY,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
console.error('❌ LangChain packages not found.');
|
|
90
|
+
console.error(' Install them globally to use the AI command:');
|
|
91
|
+
console.error(' npm install -g @langchain/core @langchain/openai @langchain/anthropic @langchain/google-genai');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { HumanMessage, SystemMessage } = require('@langchain/core/messages');
|
|
96
|
+
|
|
97
|
+
console.log('🤖 Analyzing your description...\n');
|
|
98
|
+
|
|
99
|
+
const response = await ChatModel.invoke([
|
|
100
|
+
new SystemMessage(SYSTEM_PROMPT),
|
|
101
|
+
new HumanMessage(description),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
let config;
|
|
105
|
+
try {
|
|
106
|
+
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
107
|
+
if (!jsonMatch) throw new Error('No JSON found in response');
|
|
108
|
+
config = JSON.parse(jsonMatch[0]);
|
|
109
|
+
} catch {
|
|
110
|
+
console.error('❌ Could not parse AI response. Try a clearer description.');
|
|
111
|
+
console.error(' Raw output:', response.content);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate required fields
|
|
116
|
+
if (!config.projectName || !config.language || !config.db) {
|
|
117
|
+
console.error('❌ AI response missing required fields. Try again.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Display interpreted config
|
|
122
|
+
console.log('📋 Interpreted configuration:\n');
|
|
123
|
+
console.log(` Project: ${config.projectName}`);
|
|
124
|
+
console.log(` Language: ${config.language}`);
|
|
125
|
+
console.log(` Database: ${config.db}`);
|
|
126
|
+
console.log(` Logger: ${config.logger || 'winston'}`);
|
|
127
|
+
console.log(` Features: ${(config.features || []).join(', ') || 'base'}`);
|
|
128
|
+
console.log('');
|
|
129
|
+
|
|
130
|
+
return config;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
module.exports = { runAiCommand };
|
package/lib/features.js
CHANGED
|
@@ -240,6 +240,8 @@ const inferConfig = (projectDir, pkg) => {
|
|
|
240
240
|
hasApiVersioning: false,
|
|
241
241
|
hasBackgroundJobs: !!(pkg.dependencies && pkg.dependencies.bullmq),
|
|
242
242
|
hasGraphQL: !!(pkg.dependencies && pkg.dependencies['@apollo/server']),
|
|
243
|
+
hasMCP: !!(pkg.dependencies && pkg.dependencies['@modelcontextprotocol/sdk']),
|
|
244
|
+
hasAI: !!(pkg.dependencies && pkg.dependencies['@langchain/core']),
|
|
243
245
|
logger: pkg.dependencies && pkg.dependencies.pino ? 'pino' : 'winston',
|
|
244
246
|
};
|
|
245
247
|
};
|
package/lib/generator.js
CHANGED
|
@@ -61,6 +61,14 @@ const createDirectoryStructure = (projectDir, config) => {
|
|
|
61
61
|
dirs.push('src/graphql');
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
if (config.hasMCP) {
|
|
65
|
+
dirs.push('src/mcp');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.hasAI) {
|
|
69
|
+
dirs.push('src/agents');
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
fs.mkdirSync(projectDir);
|
|
65
73
|
dirs.forEach((dir) => {
|
|
66
74
|
fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
|
|
@@ -144,6 +152,10 @@ const generateFiles = async (config, projectDir) => {
|
|
|
144
152
|
configFiles.push({ template: 'config/queue.js.ejs', output: `src/config/queue.${ext}` });
|
|
145
153
|
}
|
|
146
154
|
|
|
155
|
+
if (config.hasAI) {
|
|
156
|
+
configFiles.push({ template: 'config/ai.js.ejs', output: `src/config/ai.${ext}` });
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
// Route files
|
|
148
160
|
const routeFiles = [
|
|
149
161
|
{ template: 'routes/index.js.ejs', output: `src/routes/index.${ext}` },
|
|
@@ -202,6 +214,18 @@ const generateFiles = async (config, projectDir) => {
|
|
|
202
214
|
);
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
if (config.hasAI) {
|
|
218
|
+
serviceFiles.push(
|
|
219
|
+
{ template: 'services/aiService.js.ejs', output: `src/services/aiService.${ext}` }
|
|
220
|
+
);
|
|
221
|
+
controllerFiles.push(
|
|
222
|
+
{ template: 'controllers/aiController.js.ejs', output: `src/controllers/aiController.${ext}` }
|
|
223
|
+
);
|
|
224
|
+
routeFiles.push(
|
|
225
|
+
{ template: 'routes/aiRoutes.js.ejs', output: `src/routes/aiRoutes.${ext}` }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
205
229
|
if (config.hasFileUpload) {
|
|
206
230
|
routeFiles.push(
|
|
207
231
|
{ template: 'routes/uploadRoutes.js.ejs', output: `src/routes/uploadRoutes.${ext}` }
|
|
@@ -214,6 +238,35 @@ const generateFiles = async (config, projectDir) => {
|
|
|
214
238
|
);
|
|
215
239
|
}
|
|
216
240
|
|
|
241
|
+
// GraphQL files
|
|
242
|
+
const graphqlFiles = [];
|
|
243
|
+
|
|
244
|
+
if (config.hasGraphQL) {
|
|
245
|
+
graphqlFiles.push(
|
|
246
|
+
{ template: 'graphql/typeDefs.js.ejs', output: `src/graphql/typeDefs.${ext}` },
|
|
247
|
+
{ template: 'graphql/resolvers.js.ejs', output: `src/graphql/resolvers.${ext}` }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// MCP files
|
|
252
|
+
const mcpFiles = [];
|
|
253
|
+
|
|
254
|
+
if (config.hasMCP) {
|
|
255
|
+
mcpFiles.push(
|
|
256
|
+
{ template: 'mcp/server.js.ejs', output: `src/mcp/server.${ext}` },
|
|
257
|
+
{ template: 'mcp/mcp-config.json.ejs', output: 'mcp.json' }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Agent files
|
|
262
|
+
const agentFiles = [];
|
|
263
|
+
|
|
264
|
+
if (config.hasAI) {
|
|
265
|
+
agentFiles.push(
|
|
266
|
+
{ template: 'agents/graph.js.ejs', output: `src/agents/graph.${ext}` }
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
217
270
|
// Middleware files
|
|
218
271
|
const middlewareFiles = [
|
|
219
272
|
{ template: 'middleware/errorHandler.js.ejs', output: `src/middleware/errorHandler.${ext}` },
|
|
@@ -303,15 +356,12 @@ const generateFiles = async (config, projectDir) => {
|
|
|
303
356
|
...serviceFiles,
|
|
304
357
|
...utilFiles,
|
|
305
358
|
...testFiles,
|
|
359
|
+
...graphqlFiles,
|
|
360
|
+
...mcpFiles,
|
|
361
|
+
...agentFiles,
|
|
306
362
|
];
|
|
307
363
|
|
|
308
|
-
// GraphQL files
|
|
309
|
-
if (config.hasGraphQL) {
|
|
310
|
-
allFiles.push(
|
|
311
|
-
{ template: 'graphql/typeDefs.js.ejs', output: `src/graphql/typeDefs.${ext}` },
|
|
312
|
-
{ template: 'graphql/resolvers.js.ejs', output: `src/graphql/resolvers.${ext}` }
|
|
313
|
-
);
|
|
314
|
-
}
|
|
364
|
+
// GraphQL files (legacy — now handled above)
|
|
315
365
|
|
|
316
366
|
// Background job worker
|
|
317
367
|
if (config.hasBackgroundJobs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-genix",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Production-grade CLI to generate Express apps with JWT, RBAC, GraphQL, TypeScript, Prisma, MongoDB, PostgreSQL, file uploads, email, background jobs, and more",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -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 };
|
package/templates/core/env.ejs
CHANGED
|
@@ -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 (
|
|
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,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
|
/**
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { getModel } = require('../config/ai');
|
|
2
|
+
const { HumanMessage, SystemMessage, AIMessage } = require('@langchain/core/messages');
|
|
3
|
+
const { StringOutputParser } = require('@langchain/core/output_parsers');
|
|
4
|
+
const { ChatPromptTemplate } = require('@langchain/core/prompts');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SYSTEM_PROMPT = 'You are a helpful AI assistant.';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Send a chat message and get a complete response.
|
|
11
|
+
*/
|
|
12
|
+
const chat = async (message, options = {}) => {
|
|
13
|
+
const model = getModel(options);
|
|
14
|
+
const messages = [
|
|
15
|
+
new SystemMessage(options.systemPrompt || DEFAULT_SYSTEM_PROMPT),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
if (Array.isArray(options.history)) {
|
|
19
|
+
for (const msg of options.history) {
|
|
20
|
+
if (msg.role === 'user') messages.push(new HumanMessage(msg.content));
|
|
21
|
+
else if (msg.role === 'assistant') messages.push(new AIMessage(msg.content));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
messages.push(new HumanMessage(message));
|
|
26
|
+
|
|
27
|
+
const response = await model.invoke(messages);
|
|
28
|
+
|
|
29
|
+
logger.info('AI chat completed', {
|
|
30
|
+
provider: options.provider || process.env.AI_PROVIDER || 'openai',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
content: response.content,
|
|
35
|
+
model: response.response_metadata?.model || 'unknown',
|
|
36
|
+
usage: response.usage_metadata || null,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stream a chat response. Returns an async generator yielding text chunks.
|
|
42
|
+
*/
|
|
43
|
+
const stream = async function* (message, options = {}) {
|
|
44
|
+
const model = getModel(options);
|
|
45
|
+
const messages = [
|
|
46
|
+
new SystemMessage(options.systemPrompt || DEFAULT_SYSTEM_PROMPT),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(options.history)) {
|
|
50
|
+
for (const msg of options.history) {
|
|
51
|
+
if (msg.role === 'user') messages.push(new HumanMessage(msg.content));
|
|
52
|
+
else if (msg.role === 'assistant') messages.push(new AIMessage(msg.content));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
messages.push(new HumanMessage(message));
|
|
57
|
+
|
|
58
|
+
const streamResponse = await model.stream(messages);
|
|
59
|
+
|
|
60
|
+
for await (const chunk of streamResponse) {
|
|
61
|
+
if (chunk.content) {
|
|
62
|
+
yield chunk.content;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run a prompt template chain with variable substitution.
|
|
69
|
+
*/
|
|
70
|
+
const chain = async (template, variables = {}, options = {}) => {
|
|
71
|
+
const model = getModel(options);
|
|
72
|
+
const prompt = ChatPromptTemplate.fromTemplate(template);
|
|
73
|
+
const outputParser = new StringOutputParser();
|
|
74
|
+
const pipeline = prompt.pipe(model).pipe(outputParser);
|
|
75
|
+
|
|
76
|
+
const result = await pipeline.invoke(variables);
|
|
77
|
+
return result;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
module.exports = { chat, stream, chain };
|