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,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Builder Utility
|
|
3
|
+
* Transforms simple form data into the config.json structure expected by dragbot-factory
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
LLM_PROVIDERS,
|
|
8
|
+
buildBedrockModelId,
|
|
9
|
+
stripBedrockModelPrefix,
|
|
10
|
+
getAllowedProtocolsForModel,
|
|
11
|
+
} from './llm-providers.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract terms and conditions text from formStructure's consentToTC field
|
|
15
|
+
* @param {Object} formStructure - The form structure object with sections
|
|
16
|
+
* @returns {string} The T&C text or empty string
|
|
17
|
+
*/
|
|
18
|
+
function extractTermsFromFormStructure(formStructure) {
|
|
19
|
+
if (!formStructure?.sections) return '';
|
|
20
|
+
|
|
21
|
+
for (const section of formStructure.sections) {
|
|
22
|
+
for (const field of section.fields || []) {
|
|
23
|
+
if (field.id === 'consentToTC' && field.termsText) {
|
|
24
|
+
return field.termsText;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inject consentToTC field into form structure if terms and conditions exist
|
|
33
|
+
* @param {Object} formStructure - The form structure object with sections
|
|
34
|
+
* @param {string} termsAndConditions - The T&C text
|
|
35
|
+
* @returns {Object} Modified form structure with consentToTC field appended
|
|
36
|
+
*/
|
|
37
|
+
function injectConsentToTCField(formStructure, termsAndConditions) {
|
|
38
|
+
if (!formStructure || !termsAndConditions?.trim()) {
|
|
39
|
+
return formStructure;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Deep clone to avoid mutating original
|
|
43
|
+
const modified = JSON.parse(JSON.stringify(formStructure));
|
|
44
|
+
|
|
45
|
+
// Create the consentToTC field
|
|
46
|
+
const consentField = {
|
|
47
|
+
id: 'consentToTC',
|
|
48
|
+
label: 'I agree to the Terms and Conditions',
|
|
49
|
+
type: 'checkbox',
|
|
50
|
+
required: true,
|
|
51
|
+
termsText: termsAndConditions.trim()
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Append to the last section's fields
|
|
55
|
+
if (modified.sections && modified.sections.length > 0) {
|
|
56
|
+
const lastSection = modified.sections[modified.sections.length - 1];
|
|
57
|
+
lastSection.fields.push(consentField);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return modified;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build LLM configuration section
|
|
65
|
+
*
|
|
66
|
+
* When apiKey is empty (saved-key-by-reference flow), the selected provider's
|
|
67
|
+
* credential fields are emitted blank — the server fills them in by decrypting
|
|
68
|
+
* the api_keys row before persisting the deployment. See resolve-api-key.js.
|
|
69
|
+
*/
|
|
70
|
+
export function buildLLMConfig(provider, apiKey, model, additionalSettings = {}) {
|
|
71
|
+
// Build config with all providers (empty configs for non-selected providers)
|
|
72
|
+
const llmConfig = { provider };
|
|
73
|
+
const usingSavedKey = !apiKey;
|
|
74
|
+
|
|
75
|
+
// Add config for each provider
|
|
76
|
+
Object.keys(LLM_PROVIDERS).forEach(key => {
|
|
77
|
+
const keyConfig = LLM_PROVIDERS[key];
|
|
78
|
+
|
|
79
|
+
if (key === 'ollama') {
|
|
80
|
+
// Ollama runs against a self-hosted endpoint with no credentials. The
|
|
81
|
+
// wizard collects host + model; everything else uses defaults baked
|
|
82
|
+
// into the runtime adapter. additionalSettings.ollamaHost falls back
|
|
83
|
+
// to the provider's defaultHost so a user who never touches the host
|
|
84
|
+
// field still ends up with a working artifact.
|
|
85
|
+
if (key === provider) {
|
|
86
|
+
llmConfig[key] = {
|
|
87
|
+
host: additionalSettings.ollamaHost || keyConfig.defaultHost,
|
|
88
|
+
model,
|
|
89
|
+
format: 'json',
|
|
90
|
+
timeout: 300000,
|
|
91
|
+
};
|
|
92
|
+
} else {
|
|
93
|
+
llmConfig[key] = {
|
|
94
|
+
host: keyConfig.defaultHost,
|
|
95
|
+
model: keyConfig.defaultModel,
|
|
96
|
+
format: 'json',
|
|
97
|
+
timeout: 300000,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
} else if (key === 'bedrock') {
|
|
101
|
+
// Bedrock uses different config structure (credentials stored as JSON in apiKey)
|
|
102
|
+
if (key === provider && !usingSavedKey) {
|
|
103
|
+
// Parse credentials from apiKey JSON
|
|
104
|
+
const credentials = JSON.parse(apiKey);
|
|
105
|
+
// Ensure region is always set (fallback to us-east-1)
|
|
106
|
+
const region = credentials.region || 'us-east-1';
|
|
107
|
+
// Apply geographic prefix to model ID for cross-region inference
|
|
108
|
+
const prefixedModel = buildBedrockModelId(model, region);
|
|
109
|
+
llmConfig[key] = {
|
|
110
|
+
region,
|
|
111
|
+
useIamRole: credentials.useIamRole,
|
|
112
|
+
accessKeyId: credentials.accessKeyId,
|
|
113
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
114
|
+
model: prefixedModel,
|
|
115
|
+
};
|
|
116
|
+
} else if (key === provider && usingSavedKey) {
|
|
117
|
+
// Saved-key path: emit user's model unprefixed; server will decrypt
|
|
118
|
+
// the saved credentials, set the region, and apply the geo prefix.
|
|
119
|
+
llmConfig[key] = {
|
|
120
|
+
region: 'us-east-1',
|
|
121
|
+
useIamRole: false,
|
|
122
|
+
accessKeyId: null,
|
|
123
|
+
secretAccessKey: null,
|
|
124
|
+
model,
|
|
125
|
+
};
|
|
126
|
+
} else {
|
|
127
|
+
// Empty Bedrock config
|
|
128
|
+
llmConfig[key] = {
|
|
129
|
+
region: 'us-east-1',
|
|
130
|
+
useIamRole: true,
|
|
131
|
+
accessKeyId: null,
|
|
132
|
+
secretAccessKey: null,
|
|
133
|
+
model: keyConfig.defaultModel,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// Standard providers with baseURL/endpoint
|
|
138
|
+
if (key === provider) {
|
|
139
|
+
const baseConfig = {
|
|
140
|
+
apiKey,
|
|
141
|
+
model,
|
|
142
|
+
baseURL: keyConfig.baseURL,
|
|
143
|
+
endpoint: keyConfig.endpoint,
|
|
144
|
+
timeout: 300000
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Add provider-specific settings
|
|
148
|
+
if (key === 'openai') {
|
|
149
|
+
baseConfig.organization = additionalSettings.organization || '';
|
|
150
|
+
}
|
|
151
|
+
if (key === 'anthropic') {
|
|
152
|
+
baseConfig.maxTokens = additionalSettings.maxTokens || 4096;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
llmConfig[key] = baseConfig;
|
|
156
|
+
} else {
|
|
157
|
+
// Empty config for non-selected provider
|
|
158
|
+
const emptyConfig = {
|
|
159
|
+
apiKey: '',
|
|
160
|
+
model: keyConfig.defaultModel,
|
|
161
|
+
baseURL: keyConfig.baseURL,
|
|
162
|
+
endpoint: keyConfig.endpoint,
|
|
163
|
+
timeout: 300000
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Add provider-specific fields for empty configs
|
|
167
|
+
if (key === 'openai') {
|
|
168
|
+
emptyConfig.organization = '';
|
|
169
|
+
}
|
|
170
|
+
if (key === 'anthropic') {
|
|
171
|
+
emptyConfig.maxTokens = 4096;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
llmConfig[key] = emptyConfig;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return llmConfig;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build full deployment config from form data
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} formData - Simple form state
|
|
186
|
+
* @param {string} formData.botName - Bot name
|
|
187
|
+
* @param {string} formData.objective - Bot objective (MANDATORY)
|
|
188
|
+
* @param {string} formData.firstMessage - Welcome message
|
|
189
|
+
* @param {string} formData.provider - LLM provider (openai|anthropic|bedrock)
|
|
190
|
+
* @param {string} formData.apiKey - API key for selected provider
|
|
191
|
+
* @param {string} formData.model - Model name
|
|
192
|
+
* @param {Array} formData.suggestedPrompts - Optional suggested prompts
|
|
193
|
+
* @param {Object} formData.uiSettings - Optional UI customization
|
|
194
|
+
* @param {Array} formData.triageDestinations - Triage destinations (for triage flow)
|
|
195
|
+
* @param {string} flowType - The wizard flow type ('standard', 'conversational', 'triage', 'modular')
|
|
196
|
+
* @param {Object} options - Additional options
|
|
197
|
+
* @param {Object} options.enabledProtocols - For modular flow: { knowledge, formGathering, appointments }
|
|
198
|
+
* @returns {Object} Full deployment config
|
|
199
|
+
*/
|
|
200
|
+
export function buildDeploymentConfig(formData, flowType = 'conversational', options = {}) {
|
|
201
|
+
const { enabledProtocols, apiKeyId } = options;
|
|
202
|
+
|
|
203
|
+
const isModularTriage = flowType === 'modular' && enabledProtocols?.triage;
|
|
204
|
+
const isAppointments = flowType === 'modular' && enabledProtocols?.appointments;
|
|
205
|
+
const isOpticalRead = flowType === 'modular' && enabledProtocols?.opticalRead;
|
|
206
|
+
const isSkipRag = !!formData.skipRag;
|
|
207
|
+
|
|
208
|
+
// Ollama is credential-less — no apiKey, no saved-key reference. Validate
|
|
209
|
+
// host + model only; the deployer bakes the host into config.json and the
|
|
210
|
+
// bot's adapter calls it directly. Default host falls back to the provider
|
|
211
|
+
// entry's defaultHost in buildLLMConfig if the field is empty.
|
|
212
|
+
const isOllama = formData.provider === 'ollama';
|
|
213
|
+
const required = (apiKeyId || isOllama)
|
|
214
|
+
? ['botName', 'objective', 'provider', 'model']
|
|
215
|
+
: ['botName', 'objective', 'provider', 'apiKey', 'model'];
|
|
216
|
+
const missing = required.filter(field => !formData[field]);
|
|
217
|
+
|
|
218
|
+
if (missing.length > 0) {
|
|
219
|
+
throw new Error(`Missing required fields: ${missing.join(', ')}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Final-line-of-defense protocol gate. The wizard prunes enabledProtocols
|
|
223
|
+
// when the user changes provider/model, and the chat builder's
|
|
224
|
+
// recommend_protocols clamps its suggestions; this catches any path that
|
|
225
|
+
// bypasses both (direct API caller, replay of a stale session, etc.).
|
|
226
|
+
// Restricted Ollama models (qwen3, mistral-nemo) can only ship the
|
|
227
|
+
// knowledge protocol — refuse to compile a config that says otherwise.
|
|
228
|
+
if (flowType === 'modular' && enabledProtocols) {
|
|
229
|
+
const allowedForModel = getAllowedProtocolsForModel(formData.provider, formData.model);
|
|
230
|
+
if (allowedForModel) {
|
|
231
|
+
const disallowed = Object.entries(enabledProtocols)
|
|
232
|
+
.filter(([id, on]) => on && !allowedForModel.has(id))
|
|
233
|
+
.map(([id]) => id);
|
|
234
|
+
if (disallowed.length > 0) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`${formData.model} only supports: ${Array.from(allowedForModel).join(', ')}. ` +
|
|
237
|
+
`Disable these protocols or switch to a more capable model: ${disallowed.join(', ')}.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const configSection = {
|
|
244
|
+
instructions: './config/instructions.txt',
|
|
245
|
+
name: formData.botName,
|
|
246
|
+
chatDisplayName: formData.uiSettings?.chatDisplayName || 'Bot',
|
|
247
|
+
placeholder: formData.uiSettings?.placeholder || 'Type your message...',
|
|
248
|
+
firstMessage: formData.firstMessage || `Welcome! I'm ${formData.botName}. How can I help you?`,
|
|
249
|
+
suggestedPrompts: formData.suggestedPrompts || [],
|
|
250
|
+
actionsBar: {
|
|
251
|
+
showBar: false,
|
|
252
|
+
showSourceButton: false,
|
|
253
|
+
showMetadataButton: false,
|
|
254
|
+
showCopyButton: false,
|
|
255
|
+
showSuggestedPrompts: false
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Add form-related fields if form structure exists
|
|
260
|
+
if (formData.formStructure?.generatedJson) {
|
|
261
|
+
// Validate required form fields - webhook is optional if formSendHome is enabled
|
|
262
|
+
const hasWebhook = formData.formCompletionWebhook && formData.formCompletionWebhook.trim();
|
|
263
|
+
const hasFormSendHome = formData.formSendHome;
|
|
264
|
+
if (!hasWebhook && !hasFormSendHome) {
|
|
265
|
+
throw new Error('Either formCompletionWebhook or formSendHome is required when form collection is enabled');
|
|
266
|
+
}
|
|
267
|
+
if (!formData.afterSubmitChatMessage || !formData.afterSubmitChatMessage.trim()) {
|
|
268
|
+
throw new Error('afterSubmitChatMessage is required when form collection is enabled');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
configSection.isForm = true;
|
|
272
|
+
configSection.formStructure = './config/formFormat.json';
|
|
273
|
+
if (hasWebhook) {
|
|
274
|
+
configSection.formCompletionWebhook = formData.formCompletionWebhook;
|
|
275
|
+
}
|
|
276
|
+
configSection.afterSubmitChatMessage = formData.afterSubmitChatMessage;
|
|
277
|
+
// formSendHome flag - URL will be injected by deployer
|
|
278
|
+
if (hasFormSendHome) {
|
|
279
|
+
configSection.formSendHome = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Add calendar flag for appointments flow
|
|
284
|
+
if (isAppointments) {
|
|
285
|
+
configSection.isCalendar = true;
|
|
286
|
+
configSection.calendarConfig = './config/calendarConfig.json';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Add triage flag for modular triage flow
|
|
290
|
+
if (isModularTriage) {
|
|
291
|
+
configSection.isTriage = true;
|
|
292
|
+
configSection.triageRoutes = './config/triageRoutes.json';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Add optical read flag for modular optical-read flow. The bot runtime
|
|
296
|
+
// reads opticalReadFields.json at boot like formFormat.json — it gates the
|
|
297
|
+
// /api/extract endpoint and primes the upload-card UX.
|
|
298
|
+
if (isOpticalRead) {
|
|
299
|
+
configSection.isOpticalRead = true;
|
|
300
|
+
configSection.opticalReadFields = './config/opticalReadFields.json';
|
|
301
|
+
if (formData.opticalReadShowUploadOnStart) {
|
|
302
|
+
// Frontend reads this to render an upload card in the suggested-prompts
|
|
303
|
+
// strip on first load; field list ships separately (see opticalReadFields).
|
|
304
|
+
configSection.opticalReadShowUploadOnStart = true;
|
|
305
|
+
}
|
|
306
|
+
if (formData.opticalReadAfterSubmitMessage && formData.opticalReadAfterSubmitMessage.trim()) {
|
|
307
|
+
// Optional follow-up message rendered in the chat after the user
|
|
308
|
+
// submits the extracted-fields panel. Independent from form-gathering's
|
|
309
|
+
// afterSubmitChatMessage so each protocol can have its own copy.
|
|
310
|
+
configSection.opticalReadAfterSubmitMessage = formData.opticalReadAfterSubmitMessage.trim();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
// Config section (UI settings)
|
|
316
|
+
config: configSection,
|
|
317
|
+
|
|
318
|
+
// LLM section
|
|
319
|
+
llm: buildLLMConfig(
|
|
320
|
+
formData.provider,
|
|
321
|
+
formData.apiKey,
|
|
322
|
+
formData.model,
|
|
323
|
+
{
|
|
324
|
+
...(formData.llmSettings || {}),
|
|
325
|
+
ollamaHost: formData.ollamaHost,
|
|
326
|
+
}
|
|
327
|
+
),
|
|
328
|
+
|
|
329
|
+
// Extra fields for deployer (not part of config.json)
|
|
330
|
+
objective: formData.objective,
|
|
331
|
+
botSummary: formData.botSummary || undefined,
|
|
332
|
+
skipRag: isSkipRag || undefined,
|
|
333
|
+
formStructure: injectConsentToTCField(
|
|
334
|
+
formData.formStructure?.generatedJson,
|
|
335
|
+
formData.termsAndConditions
|
|
336
|
+
) || undefined,
|
|
337
|
+
formCompletionWebhook: formData.formCompletionWebhook || undefined,
|
|
338
|
+
afterSubmitChatMessage: formData.afterSubmitChatMessage || undefined,
|
|
339
|
+
formSendHome: formData.formSendHome || undefined, // Deployer will inject URL
|
|
340
|
+
|
|
341
|
+
// Triage-specific: store routes for modular flow edit mode
|
|
342
|
+
triageRoutes: isModularTriage ? formData.triageRoutes : undefined,
|
|
343
|
+
|
|
344
|
+
// Appointments-specific: store destinations for edit mode
|
|
345
|
+
appointmentDestinations: isAppointments ? formData.appointmentDestinations : undefined,
|
|
346
|
+
|
|
347
|
+
// Optical Read: store fields for the deployer + edit-mode round trip.
|
|
348
|
+
// The deployer also gets these via the top-level opticalReadFields body
|
|
349
|
+
// field for parity with appointmentDestinations / triageDestinations.
|
|
350
|
+
opticalReadFields: isOpticalRead ? formData.opticalReadFields : undefined
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Reverse buildDeploymentConfig - converts stored config back to form state
|
|
356
|
+
* Used for cloning/editing existing deployments
|
|
357
|
+
*
|
|
358
|
+
* @param {Object} config - Deployment config from database
|
|
359
|
+
* @returns {Object} Form data suitable for ConfigForm
|
|
360
|
+
*/
|
|
361
|
+
export function parseDeploymentConfig(config) {
|
|
362
|
+
const provider = config.llm.provider;
|
|
363
|
+
const providerConfig = config.llm[provider];
|
|
364
|
+
|
|
365
|
+
// For Bedrock, apiKey is JSON credentials; for others it's the key itself.
|
|
366
|
+
// Ollama has no credentials — just a host URL that round-trips into the
|
|
367
|
+
// wizard's ollamaHost field.
|
|
368
|
+
let apiKey = '';
|
|
369
|
+
let ollamaHost = '';
|
|
370
|
+
let model = providerConfig.model;
|
|
371
|
+
if (provider === 'bedrock') {
|
|
372
|
+
apiKey = JSON.stringify({
|
|
373
|
+
region: providerConfig.region,
|
|
374
|
+
useIamRole: providerConfig.useIamRole,
|
|
375
|
+
accessKeyId: providerConfig.accessKeyId,
|
|
376
|
+
secretAccessKey: providerConfig.secretAccessKey,
|
|
377
|
+
});
|
|
378
|
+
// Strip geographic prefix from model ID so it matches dropdown options
|
|
379
|
+
model = stripBedrockModelPrefix(model);
|
|
380
|
+
} else if (provider === 'ollama') {
|
|
381
|
+
ollamaHost = providerConfig.host || '';
|
|
382
|
+
} else {
|
|
383
|
+
apiKey = providerConfig.apiKey || '';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
// Bot Identity
|
|
388
|
+
botName: config.config.name,
|
|
389
|
+
objective: config.objective || '',
|
|
390
|
+
firstMessage: config.config.firstMessage,
|
|
391
|
+
|
|
392
|
+
// LLM Configuration
|
|
393
|
+
provider,
|
|
394
|
+
model,
|
|
395
|
+
apiKey,
|
|
396
|
+
ollamaHost,
|
|
397
|
+
|
|
398
|
+
// Form Structure (if it exists)
|
|
399
|
+
formStructure: config.formStructure ? {
|
|
400
|
+
naturalLanguageInput: '', // Not stored, will be empty
|
|
401
|
+
generatedJson: config.formStructure
|
|
402
|
+
} : undefined,
|
|
403
|
+
|
|
404
|
+
// Form Collection Settings
|
|
405
|
+
formCompletionWebhook: config.formCompletionWebhook || '',
|
|
406
|
+
afterSubmitChatMessage: config.afterSubmitChatMessage || '',
|
|
407
|
+
formSendHome: config.formSendHome || false,
|
|
408
|
+
termsAndConditions: extractTermsFromFormStructure(config.formStructure),
|
|
409
|
+
|
|
410
|
+
skipRag: config.skipRag || false,
|
|
411
|
+
|
|
412
|
+
// UI Settings
|
|
413
|
+
uiSettings: {
|
|
414
|
+
chatDisplayName: config.config.chatDisplayName || 'Bot',
|
|
415
|
+
placeholder: config.config.placeholder || 'Type your message...',
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
// Suggested Prompts
|
|
419
|
+
suggestedPrompts: config.config.suggestedPrompts || [],
|
|
420
|
+
|
|
421
|
+
// Triage Destinations (for triage flow edit mode)
|
|
422
|
+
triageDestinations: config.triageDestinations || [],
|
|
423
|
+
|
|
424
|
+
// Appointment Destinations (for appointments flow edit mode)
|
|
425
|
+
appointmentDestinations: config.appointmentDestinations || []
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Parse modular deployment config - reconstructs ModularWizardContext state
|
|
431
|
+
* Used for editing modular bots
|
|
432
|
+
*
|
|
433
|
+
* @param {Object} config - Deployment config from database (with _modular metadata)
|
|
434
|
+
* @param {Object} [options]
|
|
435
|
+
* @param {boolean} [options.hasStoredApiKey] - From the GET endpoint; the
|
|
436
|
+
* config has been credential-redacted, so the wizard relies on this flag
|
|
437
|
+
* to know a key is on file and gate the credential requirement.
|
|
438
|
+
* @returns {Object} State suitable for ModularWizardProvider initialData
|
|
439
|
+
*/
|
|
440
|
+
export function parseModularDeploymentConfig(config, options = {}) {
|
|
441
|
+
// Extract modular metadata (persisted by /api/deploy)
|
|
442
|
+
const modularMeta = config._modular || {};
|
|
443
|
+
const enabledProtocols = modularMeta.enabledProtocols || {
|
|
444
|
+
// Infer protocols from config if metadata not available (legacy fallback)
|
|
445
|
+
knowledge: !config.skipRag,
|
|
446
|
+
formGathering: !!config.formStructure || !!config.config?.isForm,
|
|
447
|
+
appointments: (config.appointmentDestinations?.length > 0) || !!config.config?.isCalendar,
|
|
448
|
+
triage: (config.triageRoutes?.length > 0) || !!config.config?.isTriage,
|
|
449
|
+
opticalRead: (config.opticalReadFields?.length > 0) || !!config.config?.isOpticalRead,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Compute core fields (handles Bedrock vs standard providers; Ollama
|
|
453
|
+
// carries a host instead of a key)
|
|
454
|
+
const coreProvider = config.llm?.provider || 'anthropic';
|
|
455
|
+
const coreProviderConfig = config.llm?.[coreProvider] || {};
|
|
456
|
+
let coreApiKey = '';
|
|
457
|
+
let coreOllamaHost = '';
|
|
458
|
+
let coreModel = coreProviderConfig.model || '';
|
|
459
|
+
if (coreProvider === 'bedrock') {
|
|
460
|
+
coreApiKey = JSON.stringify({
|
|
461
|
+
region: coreProviderConfig.region,
|
|
462
|
+
useIamRole: coreProviderConfig.useIamRole,
|
|
463
|
+
accessKeyId: coreProviderConfig.accessKeyId,
|
|
464
|
+
secretAccessKey: coreProviderConfig.secretAccessKey,
|
|
465
|
+
});
|
|
466
|
+
// Strip geographic prefix from model ID so it matches dropdown options
|
|
467
|
+
coreModel = stripBedrockModelPrefix(coreModel);
|
|
468
|
+
} else if (coreProvider === 'ollama') {
|
|
469
|
+
coreOllamaHost = coreProviderConfig.host || '';
|
|
470
|
+
} else {
|
|
471
|
+
coreApiKey = coreProviderConfig.apiKey || '';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
// Protocol toggles
|
|
476
|
+
enabledProtocols,
|
|
477
|
+
|
|
478
|
+
// Core fields
|
|
479
|
+
core: {
|
|
480
|
+
provider: coreProvider,
|
|
481
|
+
model: coreModel,
|
|
482
|
+
apiKey: coreApiKey,
|
|
483
|
+
ollamaHost: coreOllamaHost,
|
|
484
|
+
apiKeyId: null,
|
|
485
|
+
hasStoredApiKey: !!options.hasStoredApiKey,
|
|
486
|
+
botName: config.config?.name || '',
|
|
487
|
+
objective: config.objective || '',
|
|
488
|
+
botSummary: config.botSummary || '',
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
// Identity fields
|
|
492
|
+
identity: {
|
|
493
|
+
firstMessage: config.config?.firstMessage || '',
|
|
494
|
+
chatDisplayName: config.config?.chatDisplayName || 'Bot',
|
|
495
|
+
placeholder: config.config?.placeholder || 'Type your message...',
|
|
496
|
+
suggestedPrompts: config.config?.suggestedPrompts || [],
|
|
497
|
+
},
|
|
498
|
+
|
|
499
|
+
// Protocol-specific data
|
|
500
|
+
protocolData: {
|
|
501
|
+
knowledge: {
|
|
502
|
+
skipRag: config.skipRag || false,
|
|
503
|
+
documents: [], // Documents need to be fetched separately by ID
|
|
504
|
+
embeddings: config._modular?.embeddings || config.embeddings || null,
|
|
505
|
+
},
|
|
506
|
+
formGathering: {
|
|
507
|
+
formLocale: 'en-US',
|
|
508
|
+
formStructureInput: '', // Not stored
|
|
509
|
+
generatedFormJson: config.formStructure ? JSON.stringify(config.formStructure, null, 2) : null,
|
|
510
|
+
formCompletionWebhook: config.formCompletionWebhook || config.config?.formCompletionWebhook || '',
|
|
511
|
+
afterSubmitChatMessage: config.afterSubmitChatMessage || config.config?.afterSubmitChatMessage || '',
|
|
512
|
+
formSendHome: config.formSendHome || config.config?.formSendHome || false,
|
|
513
|
+
termsAndConditions: extractTermsFromFormStructure(config.formStructure),
|
|
514
|
+
},
|
|
515
|
+
appointments: {
|
|
516
|
+
destinations: config.appointmentDestinations || [],
|
|
517
|
+
},
|
|
518
|
+
triage: {
|
|
519
|
+
routes: config.triageRoutes || [],
|
|
520
|
+
},
|
|
521
|
+
opticalRead: {
|
|
522
|
+
fields: config.opticalReadFields || [],
|
|
523
|
+
showUploadOnStart: !!config.config?.opticalReadShowUploadOnStart,
|
|
524
|
+
afterSubmitMessage: config.config?.opticalReadAfterSubmitMessage || '',
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
// Deployment config reference
|
|
529
|
+
deploymentConfig: null,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Detect if a deployment config is from the modular paradigm
|
|
535
|
+
* @param {Object} config - Deployment config from database
|
|
536
|
+
* @returns {boolean} True if this is a modular deployment
|
|
537
|
+
*/
|
|
538
|
+
export function isModularDeployment(config) {
|
|
539
|
+
return config?._modular?.paradigm === 'modular';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Validate form structure JSON
|
|
544
|
+
*/
|
|
545
|
+
function validateFormStructure(formStructure) {
|
|
546
|
+
if (!formStructure || typeof formStructure !== 'object') {
|
|
547
|
+
return 'Form structure must be a valid object';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!Array.isArray(formStructure.sections) || formStructure.sections.length === 0) {
|
|
551
|
+
return 'Form structure must have at least one section';
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Validate each section
|
|
555
|
+
for (let i = 0; i < formStructure.sections.length; i++) {
|
|
556
|
+
const section = formStructure.sections[i];
|
|
557
|
+
|
|
558
|
+
if (!section.id) {
|
|
559
|
+
return `Section ${i + 1} is missing an id`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!section.label) {
|
|
563
|
+
return `Section ${i + 1} is missing a label`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!Array.isArray(section.fields) || section.fields.length === 0) {
|
|
567
|
+
return `Section "${section.label}" must have at least one field`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Validate each field
|
|
571
|
+
for (let j = 0; j < section.fields.length; j++) {
|
|
572
|
+
const field = section.fields[j];
|
|
573
|
+
|
|
574
|
+
if (!field.id) {
|
|
575
|
+
return `Field ${j + 1} in section "${section.label}" is missing an id`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (!field.label) {
|
|
579
|
+
return `Field "${field.id}" in section "${section.label}" is missing a label`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!field.type) {
|
|
583
|
+
return `Field "${field.id}" in section "${section.label}" is missing a type`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Set default value for required if not provided
|
|
587
|
+
if (field.required === undefined) {
|
|
588
|
+
field.required = false;
|
|
589
|
+
} else if (typeof field.required !== 'boolean') {
|
|
590
|
+
return `Field "${field.id}" in section "${section.label}" must have a required boolean`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Validate dropdown has options
|
|
594
|
+
if (field.type === 'dropdown' && (!Array.isArray(field.options) || field.options.length === 0)) {
|
|
595
|
+
return `Field "${field.id}" in section "${section.label}" is a dropdown but has no options`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return null; // Valid
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Validate deployment config
|
|
605
|
+
*/
|
|
606
|
+
export function validateDeploymentConfig(config) {
|
|
607
|
+
const errors = [];
|
|
608
|
+
|
|
609
|
+
// Check config section
|
|
610
|
+
if (!config.config?.name) errors.push('Bot name is required');
|
|
611
|
+
if (!config.config?.firstMessage) errors.push('First message is required');
|
|
612
|
+
|
|
613
|
+
// Check LLM section
|
|
614
|
+
if (!config.llm?.provider) errors.push('LLM provider is required');
|
|
615
|
+
if (!config.llm[config.llm.provider]?.model) errors.push('Model is required');
|
|
616
|
+
|
|
617
|
+
// Validate credentials based on provider
|
|
618
|
+
const provider = config.llm?.provider;
|
|
619
|
+
const providerConfig = config.llm?.[provider];
|
|
620
|
+
if (provider === 'bedrock') {
|
|
621
|
+
// Bedrock stores credentials as separate fields (accessKeyId, secretAccessKey, useIamRole)
|
|
622
|
+
if (!providerConfig?.useIamRole && (!providerConfig?.accessKeyId || !providerConfig?.secretAccessKey)) {
|
|
623
|
+
errors.push('AWS Access Key ID and Secret Access Key are required (or enable IAM Role)');
|
|
624
|
+
}
|
|
625
|
+
} else if (provider === 'ollama') {
|
|
626
|
+
// Ollama is credential-less; host is the only required transport field.
|
|
627
|
+
// buildLLMConfig falls back to defaultHost when the wizard leaves it
|
|
628
|
+
// blank, so this should only fail if someone hand-constructs a config.
|
|
629
|
+
if (!providerConfig?.host) errors.push('Ollama host URL is required');
|
|
630
|
+
} else {
|
|
631
|
+
// Standard API key validation for other providers
|
|
632
|
+
if (!providerConfig?.apiKey) errors.push('API key is required');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Check mandatory custom fields
|
|
636
|
+
if (!config.objective) errors.push('Objective is required');
|
|
637
|
+
|
|
638
|
+
// Validate form structure (optional, but if provided must be valid)
|
|
639
|
+
if (config.formStructure) {
|
|
640
|
+
const formError = validateFormStructure(config.formStructure);
|
|
641
|
+
if (formError) {
|
|
642
|
+
errors.push(`Form structure validation failed: ${formError}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
valid: errors.length === 0,
|
|
648
|
+
errors
|
|
649
|
+
};
|
|
650
|
+
}
|
package/lib/db/ids.js
ADDED