mojulo 0.0.0 → 0.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 +53 -4
- package/lib/audit-logger-new.js +11 -0
- package/lib/auth/gate.js +25 -0
- package/lib/auth/service.js +17 -0
- package/lib/auth/session.js +63 -0
- package/lib/builder/chat-processor.js +607 -0
- package/lib/builder/composer-bridge.js +82 -0
- package/lib/builder/evaluator.js +159 -0
- package/lib/builder/executor.js +252 -0
- package/lib/builder/index.js +48 -0
- package/lib/builder/session.js +248 -0
- package/lib/builder/system-prompt.js +422 -0
- package/lib/builder/tone-presets.js +75 -0
- package/lib/builder/tool-executors.js +1418 -0
- package/lib/builder/tools.js +338 -0
- package/lib/builder/validators.js +239 -0
- package/lib/composer/composer.js +225 -0
- package/lib/composer/index.js +40 -0
- package/lib/composer/protocols/00_base.txt +19 -0
- package/lib/composer/protocols/01_knowledge.txt +9 -0
- package/lib/composer/protocols/02_form-gathering.txt +32 -0
- package/lib/composer/protocols/03_appointments.txt +16 -0
- package/lib/composer/protocols/04_triage.txt +15 -0
- package/lib/composer/protocols/05_optical-read.txt +22 -0
- package/lib/composer/response-builder.js +98 -0
- package/lib/config-builder.js +650 -0
- package/lib/db/ids.js +10 -0
- package/lib/db/index.js +179 -0
- package/lib/db/repositories/apiKeys.js +72 -0
- package/lib/db/repositories/auditLogs.js +12 -0
- package/lib/db/repositories/botSpaces.js +12 -0
- package/lib/db/repositories/builderSessions.js +312 -0
- package/lib/db/repositories/deploymentEvents.js +12 -0
- package/lib/db/repositories/deployments.js +385 -0
- package/lib/db/repositories/documents.js +68 -0
- package/lib/db/repositories/mcpJobs.js +84 -0
- package/lib/deployers/bot-fleet.js +110 -0
- package/lib/deployers/bot-proxy.js +72 -0
- package/lib/deployers/build.js +89 -0
- package/lib/deployers/cloud-deploy.js +310 -0
- package/lib/deployers/docker.js +439 -0
- package/lib/deployers/fly.js +432 -0
- package/lib/deployers/index.js +38 -0
- package/lib/deployment-auth.js +36 -0
- package/lib/document-parser.js +171 -0
- package/lib/embedder/chunker.js +93 -0
- package/lib/embedder/local.js +101 -0
- package/lib/embedder/preview-rag.js +93 -0
- package/lib/envelope-schema.js +54 -0
- package/lib/fleet/scoped-sql.js +342 -0
- package/lib/form-schema-config/base.js +135 -0
- package/lib/form-schema-config/index.js +286 -0
- package/lib/form-schema-config/locales/af-ZA.js +153 -0
- package/lib/form-schema-config/locales/ar-AE.js +142 -0
- package/lib/form-schema-config/locales/ar-SA.js +164 -0
- package/lib/form-schema-config/locales/de-DE.js +152 -0
- package/lib/form-schema-config/locales/en-AU.js +161 -0
- package/lib/form-schema-config/locales/en-CA.js +115 -0
- package/lib/form-schema-config/locales/en-GB.js +132 -0
- package/lib/form-schema-config/locales/en-IN.js +219 -0
- package/lib/form-schema-config/locales/en-MY.js +171 -0
- package/lib/form-schema-config/locales/en-NG.js +198 -0
- package/lib/form-schema-config/locales/en-PH.js +186 -0
- package/lib/form-schema-config/locales/en-SG.js +153 -0
- package/lib/form-schema-config/locales/en-US.js +138 -0
- package/lib/form-schema-config/locales/es-ES.js +171 -0
- package/lib/form-schema-config/locales/es-MX.js +193 -0
- package/lib/form-schema-config/locales/fr-CA.js +138 -0
- package/lib/form-schema-config/locales/fr-FR.js +155 -0
- package/lib/form-schema-config/locales/hi-IN.js +219 -0
- package/lib/form-schema-config/locales/it-IT.js +157 -0
- package/lib/form-schema-config/locales/ja-JP.js +169 -0
- package/lib/form-schema-config/locales/ko-KR.js +140 -0
- package/lib/form-schema-config/locales/nl-NL.js +149 -0
- package/lib/form-schema-config/locales/pt-BR.js +168 -0
- package/lib/form-schema-config/locales/zh-CN.js +172 -0
- package/lib/form-schema-config/locales/zh-HK.js +142 -0
- package/lib/form-structure-schema.js +191 -0
- package/lib/llm-providers.js +828 -0
- package/lib/markdown.js +197 -0
- package/lib/mcp/catalysts/appointment-to-calendar.md +84 -0
- package/lib/mcp/catalysts/conversations-to-channel-digest.md +104 -0
- package/lib/mcp/catalysts/document-extract-to-store.md +92 -0
- package/lib/mcp/catalysts/knowledge-gap-miner.md +96 -0
- package/lib/mcp/catalysts/loader.js +144 -0
- package/lib/mcp/catalysts/qualify-lead-to-crm.md +83 -0
- package/lib/mcp/catalysts/scan-conversations-for-signal.md +92 -0
- package/lib/mcp/catalysts/submission-to-ticket.md +83 -0
- package/lib/mcp/catalysts/submissions-to-warehouse.md +103 -0
- package/lib/mcp/catalysts/weekly-submissions-digest.md +82 -0
- package/lib/mcp/jobs.js +64 -0
- package/lib/mcp/server.js +184 -0
- package/lib/mcp/session-binding.js +130 -0
- package/lib/mcp/tools/build.js +123 -0
- package/lib/mcp/tools/catalysts.js +477 -0
- package/lib/mcp/tools/context.js +325 -0
- package/lib/mcp/tools/fleet.js +391 -0
- package/lib/mcp/tools/jobs-tools.js +240 -0
- package/lib/mcp/tools/operate.js +314 -0
- package/lib/preview/build-preview-config.js +136 -0
- package/lib/rate-limiter.js +11 -0
- package/lib/resolve-api-key.js +142 -0
- package/lib/storage/index.js +40 -0
- package/messages/de.json +2136 -0
- package/messages/en.json +2136 -0
- package/messages/es.json +2136 -0
- package/messages/fr.json +2136 -0
- package/messages/it.json +2136 -0
- package/messages/ja.json +2136 -0
- package/messages/ko.json +2136 -0
- package/messages/nl.json +2136 -0
- package/messages/pl.json +2136 -0
- package/messages/pt.json +2136 -0
- package/messages/ru.json +2136 -0
- package/messages/uk.json +2136 -0
- package/messages/zh.json +2136 -0
- package/package.json +61 -5
- package/scripts/mcp-config.mjs +162 -0
- package/scripts/mcp-stdio-loader.mjs +42 -0
- package/scripts/mcp-stdio.mjs +108 -0
- package/scripts/mojulo-paths.mjs +48 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import fsp from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import archiver from 'archiver';
|
|
5
|
+
import { composeInstructions } from '../composer/composer.js';
|
|
6
|
+
import { downloadToBuffer } from '../storage/index.js';
|
|
7
|
+
|
|
8
|
+
const LITE_TEMPLATE_PATH =
|
|
9
|
+
process.env.LITE_TEMPLATE_PATH ||
|
|
10
|
+
path.resolve(process.cwd(), '..', 'lite-template');
|
|
11
|
+
|
|
12
|
+
const ARTIFACTS_DIR =
|
|
13
|
+
process.env.ARTIFACTS_DIR || path.join(process.cwd(), 'data', 'artifacts');
|
|
14
|
+
|
|
15
|
+
const BOT_DEFAULT_PORT = process.env.BOT_DEFAULT_PORT || '3000';
|
|
16
|
+
|
|
17
|
+
// Prebuilt bot image published by .github/workflows/publish-bot-image.yml.
|
|
18
|
+
// Pin an exact version per release — never ship :latest to users.
|
|
19
|
+
const BOT_IMAGE =
|
|
20
|
+
process.env.BOT_IMAGE || 'ghcr.io/zombico/mojulo-bot:0.5.1';
|
|
21
|
+
|
|
22
|
+
// Escape hatch for users who can't reach ghcr.io (air-gapped networks,
|
|
23
|
+
// firewalls). Set MOJULO_OFFLINE_BUILD=1 on the control plane and the
|
|
24
|
+
// emitted artifact bundles the full source + Dockerfile and builds locally.
|
|
25
|
+
const OFFLINE_BUILD = process.env.MOJULO_OFFLINE_BUILD === '1';
|
|
26
|
+
|
|
27
|
+
// Used only in offline-build mode (the npm package's deploy path never
|
|
28
|
+
// reads lite-template). Drops local-state dirs and the `config` dir that
|
|
29
|
+
// the deploy flow writes inline a few lines below.
|
|
30
|
+
const TEMPLATE_EXCLUDES = new Set([
|
|
31
|
+
'node_modules',
|
|
32
|
+
'.next',
|
|
33
|
+
'.env',
|
|
34
|
+
'.DS_Store',
|
|
35
|
+
'data',
|
|
36
|
+
'documents',
|
|
37
|
+
'config',
|
|
38
|
+
'test',
|
|
39
|
+
'integration',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
async function ensureDir(dir) {
|
|
43
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Strip path-traversal characters and normalize the originalName so it's safe
|
|
47
|
+
// to write under documents/. Falls back to a deterministic stub when the
|
|
48
|
+
// upload's name was empty or only-separators after sanitization.
|
|
49
|
+
function sanitizeDocFilename(name) {
|
|
50
|
+
const base = (name || '').split(/[\\/]/).pop() || '';
|
|
51
|
+
const cleaned = base.replace(/\.\./g, '_').replace(/^\.+/, '_').trim();
|
|
52
|
+
return cleaned || 'document';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function copyTemplateFiles(srcRoot, dstRoot, excludes) {
|
|
56
|
+
const entries = await fsp.readdir(srcRoot, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (excludes.has(entry.name)) continue;
|
|
59
|
+
const srcPath = path.join(srcRoot, entry.name);
|
|
60
|
+
const dstPath = path.join(dstRoot, entry.name);
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
await ensureDir(dstPath);
|
|
63
|
+
await copyTemplateFiles(srcPath, dstPath, excludes);
|
|
64
|
+
} else {
|
|
65
|
+
await fsp.copyFile(srcPath, dstPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildDockerCompose(botName) {
|
|
71
|
+
const imageOrBuild = OFFLINE_BUILD
|
|
72
|
+
? `build: .
|
|
73
|
+
image: mojulo/bot:local`
|
|
74
|
+
: `image: ${BOT_IMAGE}`;
|
|
75
|
+
|
|
76
|
+
return `version: '3.8'
|
|
77
|
+
|
|
78
|
+
services:
|
|
79
|
+
${botName}:
|
|
80
|
+
${imageOrBuild}
|
|
81
|
+
container_name: ${botName}
|
|
82
|
+
ports:
|
|
83
|
+
- "${BOT_DEFAULT_PORT}:3000"
|
|
84
|
+
env_file:
|
|
85
|
+
- .env
|
|
86
|
+
volumes:
|
|
87
|
+
- ./data:/data
|
|
88
|
+
- ./config:/app/config
|
|
89
|
+
- ./documents:/app/documents
|
|
90
|
+
restart: unless-stopped
|
|
91
|
+
healthcheck:
|
|
92
|
+
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
|
93
|
+
interval: 30s
|
|
94
|
+
timeout: 3s
|
|
95
|
+
retries: 3
|
|
96
|
+
start_period: 10s
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildEnvExample(llmConfig) {
|
|
101
|
+
const provider = llmConfig.provider || 'anthropic';
|
|
102
|
+
return [
|
|
103
|
+
'# Mojulo-Lite bot runtime env.',
|
|
104
|
+
'# The builder baked your selected LLM provider + model into config/config.json,',
|
|
105
|
+
`# so you only need to paste the matching API key below before running 'docker compose up'.`,
|
|
106
|
+
'',
|
|
107
|
+
`LLM_PROVIDER=${provider}`,
|
|
108
|
+
'',
|
|
109
|
+
'# Uncomment and set the key for the provider you selected:',
|
|
110
|
+
'# OPENAI_API_KEY=',
|
|
111
|
+
'# ANTHROPIC_API_KEY=',
|
|
112
|
+
'',
|
|
113
|
+
'# AWS Bedrock (only if provider=bedrock)',
|
|
114
|
+
'# AWS_REGION=us-east-1',
|
|
115
|
+
'# AWS_ACCESS_KEY_ID=',
|
|
116
|
+
'# AWS_SECRET_ACCESS_KEY=',
|
|
117
|
+
'',
|
|
118
|
+
'# Admin API key for protected endpoints',
|
|
119
|
+
`MOJULO_API_KEY=`,
|
|
120
|
+
'',
|
|
121
|
+
].join('\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildReadme(botName, enabledProtocols = {}, hasEmbeddings = false) {
|
|
125
|
+
const protocols = Object.entries(enabledProtocols)
|
|
126
|
+
.filter(([, v]) => v)
|
|
127
|
+
.map(([k]) => `- ${k}`)
|
|
128
|
+
.join('\n') || '- (base only)';
|
|
129
|
+
|
|
130
|
+
const vectorSection = hasEmbeddings ? `
|
|
131
|
+
## Vector RAG (this bot)
|
|
132
|
+
|
|
133
|
+
The corpus embeddings ship in \`config/embeddings.json\`; the embedding
|
|
134
|
+
model (multilingual-e5-small, ONNX) ships in \`models/\`. User queries
|
|
135
|
+
are embedded in-process, then cosine similarity runs locally against
|
|
136
|
+
the baked corpus.
|
|
137
|
+
|
|
138
|
+
- No factory dependency at runtime — the bot is fully self-contained.
|
|
139
|
+
- If the model files in \`models/\` are missing or corrupt, queries fail
|
|
140
|
+
loudly.
|
|
141
|
+
|
|
142
|
+
` : '';
|
|
143
|
+
|
|
144
|
+
const formsSection = enabledProtocols.formGathering ? `
|
|
145
|
+
## Form submissions
|
|
146
|
+
|
|
147
|
+
Completed forms are captured to the bot's local SQLite database
|
|
148
|
+
(\`data/conversation.db\`, table \`form_submissions\`) on every completion,
|
|
149
|
+
independent of any webhook configuration. The webhook (\`formSendHome\`) is
|
|
150
|
+
attempted in addition when configured; failures are recorded but never block
|
|
151
|
+
local capture.
|
|
152
|
+
|
|
153
|
+
Admin endpoints (require \`x-mojulo-api-key: <MOJULO_API_KEY>\`):
|
|
154
|
+
|
|
155
|
+
- \`GET /api/forms\` — list submissions. Query params: \`conversationId\`,
|
|
156
|
+
\`since\` (ISO timestamp), \`limit\` (default 100, max 1000).
|
|
157
|
+
- \`GET /api/forms/:id\` — fetch one submission.
|
|
158
|
+
- \`GET /api/forms/export\` — CSV export with the same filters. Includes a
|
|
159
|
+
UTF-8 BOM so non-Latin field values render correctly in Excel.
|
|
160
|
+
` : '';
|
|
161
|
+
|
|
162
|
+
const quickStart = OFFLINE_BUILD
|
|
163
|
+
? `## Quick start
|
|
164
|
+
|
|
165
|
+
1. Paste your LLM provider API key into \`.env\` (see \`.env.example\`).
|
|
166
|
+
2. Build and run:
|
|
167
|
+
|
|
168
|
+
\`\`\`bash
|
|
169
|
+
docker compose up --build
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
First run builds the image locally (~3min). Subsequent runs skip straight to start.
|
|
173
|
+
|
|
174
|
+
3. Open http://localhost:${BOT_DEFAULT_PORT}.`
|
|
175
|
+
: `## Quick start
|
|
176
|
+
|
|
177
|
+
1. Paste your LLM provider API key into \`.env\` (see \`.env.example\`).
|
|
178
|
+
2. Run:
|
|
179
|
+
|
|
180
|
+
\`\`\`bash
|
|
181
|
+
docker compose up
|
|
182
|
+
\`\`\`
|
|
183
|
+
|
|
184
|
+
First run pulls the bot image from GHCR (~30s on a typical connection); subsequent runs are instant.
|
|
185
|
+
|
|
186
|
+
3. Open http://localhost:${BOT_DEFAULT_PORT}.`;
|
|
187
|
+
|
|
188
|
+
const composeLine = OFFLINE_BUILD
|
|
189
|
+
? `\`docker-compose.yml\` — builds \`mojulo/bot:local\` from the Dockerfile in this directory.`
|
|
190
|
+
: `\`docker-compose.yml\` — pulls \`${BOT_IMAGE}\` and runs it with your config mounted.`;
|
|
191
|
+
|
|
192
|
+
return `# ${botName}
|
|
193
|
+
|
|
194
|
+
Portable Mojulo bot artifact. One image, one zip, one command.
|
|
195
|
+
|
|
196
|
+
${quickStart}
|
|
197
|
+
|
|
198
|
+
## What's inside
|
|
199
|
+
|
|
200
|
+
- ${composeLine}
|
|
201
|
+
- \`.env.example\` — LLM keys + webhook URLs.
|
|
202
|
+
- \`config/\`
|
|
203
|
+
- \`instructions.txt\` — composed protocol cartridges.
|
|
204
|
+
- \`config.json\` — bot identity + LLM provider + model.
|
|
205
|
+
- \`formFormat.json\` — ghost-form schema (if forms enabled).
|
|
206
|
+
- \`embeddings.json\` — pre-baked vector index (if knowledge or triage enabled).
|
|
207
|
+
|
|
208
|
+
## Enabled protocols
|
|
209
|
+
|
|
210
|
+
${protocols}
|
|
211
|
+
${vectorSection}${formsSection}
|
|
212
|
+
## Deploying elsewhere
|
|
213
|
+
|
|
214
|
+
The artifact is endpoint-agnostic. Any host that can run \`docker compose up\`
|
|
215
|
+
works: Fly.io, Railway, a $5 VPS, or your laptop.
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function writeJson(file, data) {
|
|
220
|
+
return fsp.writeFile(file, JSON.stringify(data, null, 2), 'utf8');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function zipDirectory(sourceDir, outPath) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const output = fs.createWriteStream(outPath);
|
|
226
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
227
|
+
output.on('close', () => resolve({ bytes: archive.pointer() }));
|
|
228
|
+
archive.on('error', reject);
|
|
229
|
+
archive.pipe(output);
|
|
230
|
+
archive.directory(sourceDir, false);
|
|
231
|
+
archive.finalize();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export class DockerDeployer {
|
|
236
|
+
/**
|
|
237
|
+
* Build a portable bot artifact. Pure: takes config in, writes a ZIP out.
|
|
238
|
+
* Caller is responsible for any DB updates around the call.
|
|
239
|
+
*
|
|
240
|
+
* @param {Object} params
|
|
241
|
+
* @param {string} params.deploymentId - Used only for staging dir + zip filename
|
|
242
|
+
* @param {string} params.botName - Slug-friendly bot name
|
|
243
|
+
* @param {Object} params.config - Deployment config
|
|
244
|
+
* @param {string} params.apiKey - Generated admin API key for bot
|
|
245
|
+
* @param {Array} [params.appointmentDestinations]
|
|
246
|
+
* @param {Array} [params.triageDestinations]
|
|
247
|
+
* @param {Array} [params.opticalReadFields]
|
|
248
|
+
* @param {Object} [params.enabledProtocols]
|
|
249
|
+
* @param {string} [params.embeddingStorageKey]
|
|
250
|
+
* @param {string} [params.embeddingModel]
|
|
251
|
+
* @param {number} [params.embeddingChunkCount]
|
|
252
|
+
* @param {boolean} [params.withDocs=false] - Bundle source documents into documents/
|
|
253
|
+
* @param {Array<{originalName: string, storagePath: string}>} [params.documents=[]] - Required when withDocs=true
|
|
254
|
+
*/
|
|
255
|
+
async deploy(params) {
|
|
256
|
+
const {
|
|
257
|
+
deploymentId,
|
|
258
|
+
botName,
|
|
259
|
+
config,
|
|
260
|
+
apiKey,
|
|
261
|
+
appointmentDestinations = [],
|
|
262
|
+
triageDestinations = [],
|
|
263
|
+
opticalReadFields = [],
|
|
264
|
+
enabledProtocols = {},
|
|
265
|
+
embeddingStorageKey = null,
|
|
266
|
+
embeddingModel = null,
|
|
267
|
+
embeddingChunkCount = null,
|
|
268
|
+
withDocs = false,
|
|
269
|
+
documents = [],
|
|
270
|
+
} = params;
|
|
271
|
+
|
|
272
|
+
await ensureDir(ARTIFACTS_DIR);
|
|
273
|
+
const stagingDir = path.join(ARTIFACTS_DIR, `${botName}-${deploymentId}`);
|
|
274
|
+
if (fs.existsSync(stagingDir)) await fsp.rm(stagingDir, { recursive: true, force: true });
|
|
275
|
+
await ensureDir(stagingDir);
|
|
276
|
+
|
|
277
|
+
// 1. Stage the bot source.
|
|
278
|
+
// Prebuilt-image mode (default) writes nothing here — the GHCR image
|
|
279
|
+
// holds the source and `docker-compose.yml` (written in step 7) pulls
|
|
280
|
+
// it at `docker compose up`. Offline mode copies the full template so
|
|
281
|
+
// `docker compose up --build` works air-gapped, which only makes sense
|
|
282
|
+
// in clone-and-run installs (the npm package doesn't ship lite-template).
|
|
283
|
+
if (OFFLINE_BUILD) {
|
|
284
|
+
if (!fs.existsSync(LITE_TEMPLATE_PATH)) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`MOJULO_OFFLINE_BUILD=1 but lite-template not found at ${LITE_TEMPLATE_PATH}. ` +
|
|
287
|
+
`Offline mode requires a repo clone; set LITE_TEMPLATE_PATH or unset MOJULO_OFFLINE_BUILD.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
await copyTemplateFiles(LITE_TEMPLATE_PATH, stagingDir, TEMPLATE_EXCLUDES);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 2. Create config, data dirs
|
|
294
|
+
const configDir = path.join(stagingDir, 'config');
|
|
295
|
+
const dataDir = path.join(stagingDir, 'data');
|
|
296
|
+
await ensureDir(configDir);
|
|
297
|
+
await ensureDir(dataDir);
|
|
298
|
+
|
|
299
|
+
// 3. Compose instructions.txt from enabled protocols
|
|
300
|
+
const objective = config.objective || `Help users as ${botName}`;
|
|
301
|
+
const protocolData = {
|
|
302
|
+
formStructure: config.formStructure,
|
|
303
|
+
appointments: appointmentDestinations,
|
|
304
|
+
triage: triageDestinations,
|
|
305
|
+
opticalRead: { fields: opticalReadFields },
|
|
306
|
+
};
|
|
307
|
+
const instructions =
|
|
308
|
+
config._composedInstructions ||
|
|
309
|
+
(await composeInstructions({ objective, enabledProtocols, protocolData }));
|
|
310
|
+
await fsp.writeFile(path.join(configDir, 'instructions.txt'), instructions, 'utf8');
|
|
311
|
+
|
|
312
|
+
// 4. Write config.json — container reads bot identity + LLM provider here
|
|
313
|
+
const hasEmbeddings = !!embeddingStorageKey;
|
|
314
|
+
const configJson = {
|
|
315
|
+
config: {
|
|
316
|
+
...config.config,
|
|
317
|
+
instructions: './config/instructions.txt',
|
|
318
|
+
rag: {
|
|
319
|
+
...(config.config?.rag || {}),
|
|
320
|
+
...(hasEmbeddings
|
|
321
|
+
? {
|
|
322
|
+
embeddingsPath: './config/embeddings.json',
|
|
323
|
+
embeddingModel,
|
|
324
|
+
embeddingChunkCount,
|
|
325
|
+
}
|
|
326
|
+
: {}),
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
llm: config.llm,
|
|
330
|
+
};
|
|
331
|
+
await writeJson(path.join(configDir, 'config.json'), configJson);
|
|
332
|
+
|
|
333
|
+
// 5. Write per-protocol config files
|
|
334
|
+
if (config.formStructure) {
|
|
335
|
+
await writeJson(path.join(configDir, 'formFormat.json'), config.formStructure);
|
|
336
|
+
}
|
|
337
|
+
if (appointmentDestinations.length > 0) {
|
|
338
|
+
await writeJson(path.join(configDir, 'calendarConfig.json'), {
|
|
339
|
+
destinations: appointmentDestinations,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (triageDestinations.length > 0) {
|
|
343
|
+
// Captured at build time — never re-resolved at runtime. The JSON is
|
|
344
|
+
// the authoritative deploymentId list the LLM picks from; route
|
|
345
|
+
// description text lives in embeddings.json for retrieval reinforcement.
|
|
346
|
+
await writeJson(path.join(configDir, 'triageRoutes.json'), triageDestinations);
|
|
347
|
+
}
|
|
348
|
+
if (opticalReadFields.length > 0) {
|
|
349
|
+
// Read once at bot startup like formFormat.json. The bot's /api/extract
|
|
350
|
+
// endpoint and the upload-card UX both key off this list — keep the
|
|
351
|
+
// shape (idName, label, hint) byte-identical to what the wizard saved
|
|
352
|
+
// so edit-mode round-trips don't drift.
|
|
353
|
+
await writeJson(path.join(configDir, 'opticalReadFields.json'), opticalReadFields);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 6. Vector RAG: copy the pre-baked embeddings blob into the artifact.
|
|
357
|
+
// Build is pure copy — no embed call here. Re-build = re-copy. Bots
|
|
358
|
+
// with neither knowledge nor triage have no embeddings; runtime tolerates
|
|
359
|
+
// the missing file and silently disables RAG.
|
|
360
|
+
if (hasEmbeddings) {
|
|
361
|
+
const embeddingsBuffer = await downloadToBuffer(embeddingStorageKey);
|
|
362
|
+
await fsp.writeFile(path.join(configDir, 'embeddings.json'), embeddingsBuffer);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 6.5 Optional: bundle source documents under documents/. The compose file
|
|
366
|
+
// always mounts ./documents:/app/documents, so anything written here
|
|
367
|
+
// becomes visible to the bot at runtime. Skipped by default — only
|
|
368
|
+
// the "Download with Docs" path opts in.
|
|
369
|
+
if (withDocs && documents.length > 0) {
|
|
370
|
+
const docsDir = path.join(stagingDir, 'documents');
|
|
371
|
+
await ensureDir(docsDir);
|
|
372
|
+
const used = new Map();
|
|
373
|
+
for (const doc of documents) {
|
|
374
|
+
if (!doc?.storagePath) continue;
|
|
375
|
+
let name = sanitizeDocFilename(doc.originalName);
|
|
376
|
+
const seen = used.get(name) || 0;
|
|
377
|
+
if (seen > 0) {
|
|
378
|
+
const dot = name.lastIndexOf('.');
|
|
379
|
+
name = dot > 0
|
|
380
|
+
? `${name.slice(0, dot)} (${seen})${name.slice(dot)}`
|
|
381
|
+
: `${name} (${seen})`;
|
|
382
|
+
}
|
|
383
|
+
used.set(sanitizeDocFilename(doc.originalName), seen + 1);
|
|
384
|
+
const buf = await downloadToBuffer(doc.storagePath);
|
|
385
|
+
await fsp.writeFile(path.join(docsDir, name), buf);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 7. Write docker-compose.yml + .env.example + README.md
|
|
390
|
+
await fsp.writeFile(
|
|
391
|
+
path.join(stagingDir, 'docker-compose.yml'),
|
|
392
|
+
buildDockerCompose(botName),
|
|
393
|
+
'utf8'
|
|
394
|
+
);
|
|
395
|
+
await fsp.writeFile(
|
|
396
|
+
path.join(stagingDir, '.env.example'),
|
|
397
|
+
buildEnvExample(config.llm || {}),
|
|
398
|
+
'utf8'
|
|
399
|
+
);
|
|
400
|
+
await fsp.writeFile(
|
|
401
|
+
path.join(stagingDir, 'README.md'),
|
|
402
|
+
buildReadme(botName, enabledProtocols, hasEmbeddings),
|
|
403
|
+
'utf8'
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// 9. Write the bot's admin API key into the staging .env (users are
|
|
407
|
+
// expected to replace the LLM key themselves). The pre-populated
|
|
408
|
+
// MOJULO_API_KEY lets the bot protect its /api/conversations
|
|
409
|
+
// endpoints immediately.
|
|
410
|
+
await fsp.writeFile(
|
|
411
|
+
path.join(stagingDir, '.env'),
|
|
412
|
+
`MOJULO_API_KEY=${apiKey}\n# Paste your LLM provider key below. See .env.example.\n`,
|
|
413
|
+
'utf8'
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// 10. Zip it up. The with-docs variant lives at a sibling path so it
|
|
417
|
+
// never clobbers the lean cache the deployment row tracks.
|
|
418
|
+
const zipSuffix = withDocs ? '-with-docs' : '';
|
|
419
|
+
const zipPath = path.join(
|
|
420
|
+
ARTIFACTS_DIR,
|
|
421
|
+
`${botName}-${deploymentId}${zipSuffix}.zip`
|
|
422
|
+
);
|
|
423
|
+
await zipDirectory(stagingDir, zipPath);
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
success: true,
|
|
427
|
+
appName: botName,
|
|
428
|
+
artifactPath: zipPath,
|
|
429
|
+
relativeArtifactPath: path.relative(process.cwd(), zipPath),
|
|
430
|
+
url: `http://localhost:${BOT_DEFAULT_PORT}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async destroy(_appId) {
|
|
435
|
+
// Lite: "destroy" just removes the stored artifact; containers the user
|
|
436
|
+
// ran themselves are theirs to stop.
|
|
437
|
+
return { success: true };
|
|
438
|
+
}
|
|
439
|
+
}
|