mojulo 0.0.0 → 0.1.1
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 +54 -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 +1527 -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 +68 -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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Intent Evaluator
|
|
3
|
+
*
|
|
4
|
+
* LLM-based evaluation of user intent to determine assistance level.
|
|
5
|
+
* Lightweight - only analyzes the user's message and whether documents are attached.
|
|
6
|
+
* Does NOT read document contents - that's handled downstream.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
10
|
+
|
|
11
|
+
const EVALUATOR_MODEL = 'claude-sonnet-4-20250514';
|
|
12
|
+
const EVALUATOR_MAX_TOKENS = 1024;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Evaluation prompt template
|
|
16
|
+
*/
|
|
17
|
+
const EVALUATOR_PROMPT = `You are an intent evaluator for a bot-building platform. Analyze the user's message to determine:
|
|
18
|
+
|
|
19
|
+
1. **Assistance Level**: Does this user need high assistance (guided flow) or low assistance (direct orchestration)?
|
|
20
|
+
|
|
21
|
+
**High Assistance** indicators:
|
|
22
|
+
- Vague or minimal instructions ("make a bot with this", "use this document")
|
|
23
|
+
- Questions about what to do ("what can you do with this?", "how should I set this up?")
|
|
24
|
+
- No specific bot configuration mentioned
|
|
25
|
+
- First-time user language
|
|
26
|
+
- Documents attached but no clear direction on how to use them
|
|
27
|
+
|
|
28
|
+
**Low Assistance** indicators:
|
|
29
|
+
- Specific bot requirements stated (name, purpose, fields to collect)
|
|
30
|
+
- Clear flow type mentioned (lead capture, appointment booking, support bot)
|
|
31
|
+
- Technical specificity (mentions webhooks, forms, specific integrations)
|
|
32
|
+
- Multiple configuration details provided
|
|
33
|
+
- Confident, directive language
|
|
34
|
+
|
|
35
|
+
2. **Extracted Context**: Pull out any actionable configuration from the user's message:
|
|
36
|
+
- Bot name or naming hints ("called X", "named X")
|
|
37
|
+
- Organization/company name ("for Acme Corp")
|
|
38
|
+
- Bot purpose or objective
|
|
39
|
+
- Flow type (support, lead gen, appointments, FAQ, etc.)
|
|
40
|
+
- Specific fields or data to collect
|
|
41
|
+
- Greeting or tone preferences
|
|
42
|
+
|
|
43
|
+
Respond in this exact format:
|
|
44
|
+
|
|
45
|
+
ASSISTANCE_LEVEL: [high|low]
|
|
46
|
+
|
|
47
|
+
CONTEXT:
|
|
48
|
+
[Free-form text with extracted configuration context. Be specific and actionable. This will be passed to the main bot configuration assistant to use as defaults.]
|
|
49
|
+
|
|
50
|
+
REASONING:
|
|
51
|
+
[Brief explanation of why you classified the assistance level this way]`;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Evaluate user intent and extract context
|
|
55
|
+
*
|
|
56
|
+
* @param {string} userMessage - The user's input message
|
|
57
|
+
* @param {boolean} hasDocuments - Whether documents are attached
|
|
58
|
+
* @returns {Promise<{ assistanceLevel: 'high' | 'low', context: string, reasoning: string }>}
|
|
59
|
+
*/
|
|
60
|
+
export async function evaluateIntent(userMessage, hasDocuments = false) {
|
|
61
|
+
const apiKey = process.env.BUILDER_ANTHROPIC_API_KEY;
|
|
62
|
+
if (!apiKey) {
|
|
63
|
+
console.warn('[Evaluator] No API key, defaulting to high assistance');
|
|
64
|
+
return {
|
|
65
|
+
assistanceLevel: 'high',
|
|
66
|
+
context: '',
|
|
67
|
+
reasoning: 'API key not configured, defaulting to guided flow',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build the evaluation input - just message + document presence metadata
|
|
72
|
+
let evaluationInput = `USER MESSAGE:\n${userMessage}`;
|
|
73
|
+
|
|
74
|
+
if (hasDocuments) {
|
|
75
|
+
evaluationInput += `\n\n[Documents are attached to this message]`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const client = new Anthropic({ apiKey });
|
|
80
|
+
|
|
81
|
+
const response = await client.messages.create({
|
|
82
|
+
model: EVALUATOR_MODEL,
|
|
83
|
+
max_tokens: EVALUATOR_MAX_TOKENS,
|
|
84
|
+
messages: [
|
|
85
|
+
{
|
|
86
|
+
role: 'user',
|
|
87
|
+
content: evaluationInput,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
system: EVALUATOR_PROMPT,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const responseText = response.content
|
|
94
|
+
.filter((block) => block.type === 'text')
|
|
95
|
+
.map((block) => block.text)
|
|
96
|
+
.join('\n');
|
|
97
|
+
|
|
98
|
+
return parseEvaluatorResponse(responseText);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('[Evaluator] LLM call failed:', error.message);
|
|
101
|
+
// Fallback to high assistance on error
|
|
102
|
+
return {
|
|
103
|
+
assistanceLevel: 'high',
|
|
104
|
+
context: '',
|
|
105
|
+
reasoning: `Evaluation failed (${error.message}), defaulting to guided flow`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse the evaluator's response into structured format
|
|
112
|
+
*
|
|
113
|
+
* @param {string} responseText - Raw response from evaluator LLM
|
|
114
|
+
* @returns {{ assistanceLevel: 'high' | 'low', context: string, reasoning: string }}
|
|
115
|
+
*/
|
|
116
|
+
function parseEvaluatorResponse(responseText) {
|
|
117
|
+
// Extract assistance level
|
|
118
|
+
const levelMatch = responseText.match(/ASSISTANCE_LEVEL:\s*(high|low)/i);
|
|
119
|
+
const assistanceLevel = levelMatch ? levelMatch[1].toLowerCase() : 'high';
|
|
120
|
+
|
|
121
|
+
// Extract context section
|
|
122
|
+
const contextMatch = responseText.match(/CONTEXT:\s*([\s\S]*?)(?=REASONING:|$)/i);
|
|
123
|
+
const context = contextMatch ? contextMatch[1].trim() : responseText;
|
|
124
|
+
|
|
125
|
+
// Extract reasoning section
|
|
126
|
+
const reasoningMatch = responseText.match(/REASONING:\s*([\s\S]*?)$/i);
|
|
127
|
+
const reasoning = reasoningMatch ? reasoningMatch[1].trim() : 'No reasoning provided';
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
assistanceLevel,
|
|
131
|
+
context,
|
|
132
|
+
reasoning,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Quick heuristic check to determine if evaluation is needed
|
|
138
|
+
* (Skip evaluation for obvious cases to save latency)
|
|
139
|
+
*
|
|
140
|
+
* @param {string} userMessage - The user's input message
|
|
141
|
+
* @param {boolean} hasDocuments - Whether documents are attached
|
|
142
|
+
* @returns {{ skipEvaluation: boolean, defaultLevel: 'high' | 'low' | null }}
|
|
143
|
+
*/
|
|
144
|
+
export function shouldSkipEvaluation(userMessage, hasDocuments) {
|
|
145
|
+
const wordCount = userMessage.split(/\s+/).filter((w) => w.length > 0).length;
|
|
146
|
+
|
|
147
|
+
// Very short message with documents = definitely high assistance
|
|
148
|
+
if (hasDocuments && wordCount <= 10) {
|
|
149
|
+
return { skipEvaluation: true, defaultLevel: 'high' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Very detailed message (100+ words) = definitely low assistance
|
|
153
|
+
if (wordCount >= 100) {
|
|
154
|
+
return { skipEvaluation: true, defaultLevel: 'low' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Otherwise, run the evaluator
|
|
158
|
+
return { skipEvaluation: false, defaultLevel: null };
|
|
159
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder Save Executor
|
|
3
|
+
*
|
|
4
|
+
* Persists a builder session as a deployment row in SQLite. Does NOT build
|
|
5
|
+
* the ZIP artifact — that's a separate, on-demand step (see
|
|
6
|
+
* `lib/deployers/build.js` and `POST /api/deployments/[id]/build`).
|
|
7
|
+
*
|
|
8
|
+
* The chat UI surfaces a "Build & Download" CTA after this returns.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BuilderSessionRepository } from '../db/repositories/builderSessions.js';
|
|
12
|
+
import {
|
|
13
|
+
DeploymentRepository,
|
|
14
|
+
DEPLOYMENT_STATUS,
|
|
15
|
+
} from '../db/repositories/deployments.js';
|
|
16
|
+
import { DeploymentEventRepository } from '../db/repositories/deploymentEvents.js';
|
|
17
|
+
import { ApiKeyRepository } from '../db/repositories/apiKeys.js';
|
|
18
|
+
import { decryptApiKey, generateApiKey } from '../deployment-auth.js';
|
|
19
|
+
import { composeAndCache, getBuilderSession, validateForDeployment } from './session.js';
|
|
20
|
+
import { buildLLMConfig } from '../config-builder.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Save a modular session as a deployment row.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} sessionId
|
|
26
|
+
* @param {string} userId
|
|
27
|
+
* @param {Object} options
|
|
28
|
+
* @param {string} [options.botSpaceId]
|
|
29
|
+
* @param {string} [options.redeploymentId] - If provided, update existing row
|
|
30
|
+
* @returns {Promise<Object>}
|
|
31
|
+
*/
|
|
32
|
+
export async function saveBuilderConfig(sessionId, userId, options = {}) {
|
|
33
|
+
const { botSpaceId, redeploymentId } = options;
|
|
34
|
+
const isUpdate = !!redeploymentId;
|
|
35
|
+
|
|
36
|
+
const session = await getBuilderSession(sessionId, userId);
|
|
37
|
+
|
|
38
|
+
const validation = await validateForDeployment(sessionId, userId);
|
|
39
|
+
if (!validation.valid) {
|
|
40
|
+
throw new Error(`Session not ready to save: ${validation.errors.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { instructions } = await composeAndCache(sessionId, userId);
|
|
44
|
+
|
|
45
|
+
const apiKeyRecords = await ApiKeyRepository.findByUserId(userId);
|
|
46
|
+
const apiKeyRecord = apiKeyRecords.find(
|
|
47
|
+
(key) => key.provider === session.coreConfig.provider
|
|
48
|
+
);
|
|
49
|
+
if (!apiKeyRecord) {
|
|
50
|
+
throw new Error(`No API key found for provider: ${session.coreConfig.provider}`);
|
|
51
|
+
}
|
|
52
|
+
const decryptedApiKey = decryptApiKey(apiKeyRecord.encryptedKey);
|
|
53
|
+
|
|
54
|
+
const deploymentConfig = buildDeploymentConfig(session, instructions, decryptedApiKey);
|
|
55
|
+
const documentIds = session.protocolData.knowledge?.documents?.map((d) => d.id) || [];
|
|
56
|
+
const enabledProtocols = session.enabledProtocols;
|
|
57
|
+
|
|
58
|
+
// All bots run vector retrieval. Knowledge protocol embeds documents;
|
|
59
|
+
// triage protocol embeds route descriptions; both end up in the same
|
|
60
|
+
// single cosine index. A bot with neither protocol simply has no
|
|
61
|
+
// embeddings — RAG is silently disabled at runtime in that case.
|
|
62
|
+
const ragMode = 'vector';
|
|
63
|
+
const embeddings = session.generatedConfigs?.embeddings || null;
|
|
64
|
+
if (enabledProtocols.knowledge && !embeddings?.storageKey) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'Knowledge protocol is enabled but no embeddings were produced. Run process_documents first.'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (enabledProtocols.triage && !embeddings?.storageKey) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'Triage protocol is enabled but no embeddings were produced. Run generate_triage_config first.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let deployment;
|
|
76
|
+
let deploymentApiKey;
|
|
77
|
+
|
|
78
|
+
if (isUpdate) {
|
|
79
|
+
const existing = await DeploymentRepository.findById(redeploymentId);
|
|
80
|
+
if (!existing) {
|
|
81
|
+
throw new Error(`Deployment ${redeploymentId} not found`);
|
|
82
|
+
}
|
|
83
|
+
if (existing.userId && existing.userId !== userId) {
|
|
84
|
+
throw new Error('Unauthorized to update this deployment');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
deploymentApiKey = existing.apiKey;
|
|
88
|
+
|
|
89
|
+
deployment = await DeploymentRepository.update(redeploymentId, {
|
|
90
|
+
botName: session.coreConfig.botName,
|
|
91
|
+
config: {
|
|
92
|
+
...deploymentConfig,
|
|
93
|
+
_modular: {
|
|
94
|
+
paradigm: 'modular',
|
|
95
|
+
enabledProtocols,
|
|
96
|
+
sessionId,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
documentIds,
|
|
100
|
+
error: null,
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
deploymentApiKey = generateApiKey();
|
|
104
|
+
deployment = await DeploymentRepository.create({
|
|
105
|
+
userId,
|
|
106
|
+
botSpaceId: botSpaceId || null,
|
|
107
|
+
botName: session.coreConfig.botName,
|
|
108
|
+
flowType: 'modular',
|
|
109
|
+
status: DEPLOYMENT_STATUS.SAVED,
|
|
110
|
+
config: {
|
|
111
|
+
...deploymentConfig,
|
|
112
|
+
_modular: {
|
|
113
|
+
paradigm: 'modular',
|
|
114
|
+
enabledProtocols,
|
|
115
|
+
sessionId,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
documentIds,
|
|
119
|
+
apiKey: deploymentApiKey,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Stamp embedding pointers onto the row. ragMode is always 'vector' now;
|
|
124
|
+
// setRagMode is kept as a no-op write so legacy rows keep their schema.
|
|
125
|
+
await DeploymentRepository.setRagMode(deployment.id, ragMode);
|
|
126
|
+
if (embeddings?.storageKey) {
|
|
127
|
+
await DeploymentRepository.setEmbeddings(deployment.id, {
|
|
128
|
+
storageKey: embeddings.storageKey,
|
|
129
|
+
model: embeddings.model,
|
|
130
|
+
chunkCount: embeddings.chunkCount,
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
await DeploymentRepository.clearEmbeddings(deployment.id);
|
|
134
|
+
}
|
|
135
|
+
// Re-fetch so the returned deployment reflects the embedding columns.
|
|
136
|
+
deployment = await DeploymentRepository.findById(deployment.id);
|
|
137
|
+
|
|
138
|
+
await BuilderSessionRepository.linkDeployment(sessionId, userId, deployment.id);
|
|
139
|
+
await DeploymentEventRepository.create({
|
|
140
|
+
deploymentId: deployment.id,
|
|
141
|
+
userId,
|
|
142
|
+
eventType: isUpdate ? 'updated' : 'created',
|
|
143
|
+
status: deployment.status,
|
|
144
|
+
config: deployment.config,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await BuilderSessionRepository.updateStepProgress(
|
|
148
|
+
sessionId,
|
|
149
|
+
userId,
|
|
150
|
+
'deploy',
|
|
151
|
+
'completed'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
deploymentId: deployment.id,
|
|
157
|
+
status: deployment.status,
|
|
158
|
+
botName: session.coreConfig.botName,
|
|
159
|
+
sessionId,
|
|
160
|
+
isUpdate,
|
|
161
|
+
buildUrl: `/api/deployments/${deployment.id}/build`,
|
|
162
|
+
downloadUrl: `/api/deployments/${deployment.id}/download`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build deployment config from session data
|
|
168
|
+
* Produces the nested structure expected by validateDeploymentConfig:
|
|
169
|
+
* { config: {...}, llm: { provider, [provider]: {...} }, objective, ... }
|
|
170
|
+
*/
|
|
171
|
+
function buildDeploymentConfig(session, instructions, apiKey) {
|
|
172
|
+
const { coreConfig, identityConfig, enabledProtocols, protocolData, generatedConfigs } = session;
|
|
173
|
+
|
|
174
|
+
const provider = coreConfig.provider || 'anthropic';
|
|
175
|
+
const model = coreConfig.model || 'claude-sonnet-4-20250514';
|
|
176
|
+
|
|
177
|
+
const configSection = {
|
|
178
|
+
instructions: './config/instructions.txt',
|
|
179
|
+
name: coreConfig.botName,
|
|
180
|
+
chatDisplayName: identityConfig.chatDisplayName || 'Bot',
|
|
181
|
+
placeholder: 'Type your message...',
|
|
182
|
+
firstMessage: identityConfig.firstMessage || `Welcome! I'm ${coreConfig.botName}. How can I help you?`,
|
|
183
|
+
suggestedPrompts: (identityConfig.suggestedPrompts || []).map((prompt) =>
|
|
184
|
+
typeof prompt === 'string' ? { suggestedPrompt: prompt } : prompt
|
|
185
|
+
),
|
|
186
|
+
actionsBar: {
|
|
187
|
+
showBar: false,
|
|
188
|
+
showSourceButton: false,
|
|
189
|
+
showMetadataButton: false,
|
|
190
|
+
showCopyButton: false,
|
|
191
|
+
showSuggestedPrompts: false,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (enabledProtocols.formGathering && protocolData.formGathering) {
|
|
196
|
+
configSection.isForm = true;
|
|
197
|
+
configSection.formStructure = './config/formFormat.json';
|
|
198
|
+
if (protocolData.formGathering.formCompletionWebhook) {
|
|
199
|
+
configSection.formCompletionWebhook = protocolData.formGathering.formCompletionWebhook;
|
|
200
|
+
}
|
|
201
|
+
configSection.afterSubmitChatMessage = protocolData.formGathering.afterSubmitChatMessage;
|
|
202
|
+
if (protocolData.formGathering.formSendHome) {
|
|
203
|
+
configSection.formSendHome = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (enabledProtocols.appointments && protocolData.appointments) {
|
|
208
|
+
configSection.isCalendar = true;
|
|
209
|
+
configSection.calendarConfig = './config/calendarConfig.json';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (enabledProtocols.triage && protocolData.triage) {
|
|
213
|
+
configSection.isTriage = true;
|
|
214
|
+
configSection.triageRoutes = './config/triageRoutes.json';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Optical Read: chat-builder writes opticalRead.fields onto generatedConfigs
|
|
218
|
+
// (see generate_optical_read_config). Wire the artifact-side flag/path here
|
|
219
|
+
// and surface the field list at top-level for build.js to pick up.
|
|
220
|
+
const opticalReadFields = enabledProtocols.opticalRead
|
|
221
|
+
? generatedConfigs?.opticalRead?.fields || []
|
|
222
|
+
: [];
|
|
223
|
+
if (enabledProtocols.opticalRead && opticalReadFields.length > 0) {
|
|
224
|
+
configSection.isOpticalRead = true;
|
|
225
|
+
configSection.opticalReadFields = './config/opticalReadFields.json';
|
|
226
|
+
// Default the upload-first entry point on for chat-builder output. The
|
|
227
|
+
// chat builder doesn't expose a per-bot toggle; users can opt out by
|
|
228
|
+
// editing the deployment in the wizard, which respects the saved value.
|
|
229
|
+
configSection.opticalReadShowUploadOnStart = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
config: configSection,
|
|
234
|
+
llm: buildLLMConfig(provider, apiKey, model, {}),
|
|
235
|
+
objective: identityConfig.objective,
|
|
236
|
+
botSummary: generatedConfigs?.botSummary || '',
|
|
237
|
+
formStructure: enabledProtocols.formGathering
|
|
238
|
+
? protocolData.formGathering?.generatedFormJson
|
|
239
|
+
: undefined,
|
|
240
|
+
formCompletionWebhook: protocolData.formGathering?.formCompletionWebhook || undefined,
|
|
241
|
+
afterSubmitChatMessage: protocolData.formGathering?.afterSubmitChatMessage || undefined,
|
|
242
|
+
formSendHome: protocolData.formGathering?.formSendHome || undefined,
|
|
243
|
+
appointmentDestinations: enabledProtocols.appointments
|
|
244
|
+
? protocolData.appointments?.destinations
|
|
245
|
+
: undefined,
|
|
246
|
+
triageRoutes: enabledProtocols.triage ? protocolData.triage?.routes : undefined,
|
|
247
|
+
opticalReadFields: enabledProtocols.opticalRead ? opticalReadFields : undefined,
|
|
248
|
+
paradigm: 'modular',
|
|
249
|
+
enabledProtocols,
|
|
250
|
+
_composedInstructions: instructions,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder Flow Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Exports all builder functionality for the protocol-aware wizard system.
|
|
5
|
+
* Supports both legacy step-card wizard and inverted flow ("Claude proposes, User disposes").
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Session management (legacy wizard mode)
|
|
9
|
+
export {
|
|
10
|
+
createBuilderSession,
|
|
11
|
+
getBuilderSession,
|
|
12
|
+
toggleProtocol,
|
|
13
|
+
generateStepFlow,
|
|
14
|
+
saveCoreConfig,
|
|
15
|
+
saveIdentityConfig,
|
|
16
|
+
saveProtocolConfig,
|
|
17
|
+
composeAndCache,
|
|
18
|
+
validateForDeployment,
|
|
19
|
+
deleteBuilderSession,
|
|
20
|
+
} from './session.js';
|
|
21
|
+
|
|
22
|
+
// Composer bridge
|
|
23
|
+
export { composeFromSession, previewComposition } from './composer-bridge.js';
|
|
24
|
+
|
|
25
|
+
// Validators
|
|
26
|
+
export {
|
|
27
|
+
PROTOCOL_VALIDATORS,
|
|
28
|
+
validateCoreConfig,
|
|
29
|
+
validateIdentityConfig,
|
|
30
|
+
validateEnabledProtocols,
|
|
31
|
+
validateSessionForDeployment,
|
|
32
|
+
} from './validators.js';
|
|
33
|
+
|
|
34
|
+
// Save executor (formerly the deployment executor — now writes to SQLite only;
|
|
35
|
+
// callers that want a built artifact must hit /api/deployments/[id]/build).
|
|
36
|
+
export { saveBuilderConfig } from './executor.js';
|
|
37
|
+
|
|
38
|
+
// Inverted flow tools
|
|
39
|
+
export { BUILDER_TOOLS, TOOL_LABELS, TOOL_ICONS, getToolByName, validateToolInput } from './tools.js';
|
|
40
|
+
|
|
41
|
+
// Inverted flow tool executors
|
|
42
|
+
export { executeBuilderTool, builderToolHandlers } from './tool-executors.js';
|
|
43
|
+
|
|
44
|
+
// Inverted flow system prompt
|
|
45
|
+
export { buildBuilderSystemPrompt } from './system-prompt.js';
|
|
46
|
+
|
|
47
|
+
// Smart intent evaluation
|
|
48
|
+
export { evaluateIntent, shouldSkipEvaluation } from './evaluator.js';
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager for Builder Flow
|
|
3
|
+
*
|
|
4
|
+
* High-level session management functions that orchestrate
|
|
5
|
+
* repository operations and business logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BuilderSessionRepository } from '../db/repositories/builderSessions.js';
|
|
9
|
+
import { composeFromSession } from './composer-bridge.js';
|
|
10
|
+
import { validateSessionForDeployment, PROTOCOL_VALIDATORS } from './validators.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new builder session
|
|
14
|
+
* @param {string|null} orgId - Organization ID (optional)
|
|
15
|
+
* @param {string} userId - User ID
|
|
16
|
+
* @returns {Promise<Object>} Created session
|
|
17
|
+
*/
|
|
18
|
+
export async function createBuilderSession(orgId, userId) {
|
|
19
|
+
return BuilderSessionRepository.create({ userId });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get session with access check
|
|
24
|
+
* @param {string} sessionId - Session ID
|
|
25
|
+
* @param {string} userId - User ID
|
|
26
|
+
* @returns {Promise<Object>} Session object
|
|
27
|
+
* @throws {Error} If session not found or access denied
|
|
28
|
+
*/
|
|
29
|
+
export async function getBuilderSession(sessionId, userId) {
|
|
30
|
+
const session = await BuilderSessionRepository.findByIdAndUserId(sessionId, userId);
|
|
31
|
+
if (!session) {
|
|
32
|
+
throw new Error('Session not found or access denied');
|
|
33
|
+
}
|
|
34
|
+
return session;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Toggle protocol and regenerate step flow
|
|
39
|
+
* @param {string} sessionId - Session ID
|
|
40
|
+
* @param {string} userId - User ID
|
|
41
|
+
* @param {string} protocol - Protocol name
|
|
42
|
+
* @param {boolean} enabled - Enable or disable
|
|
43
|
+
* @returns {Promise<{ session: Object, steps: Array }>}
|
|
44
|
+
*/
|
|
45
|
+
export async function toggleProtocol(sessionId, userId, protocol, enabled) {
|
|
46
|
+
const session = await getBuilderSession(sessionId, userId);
|
|
47
|
+
|
|
48
|
+
const enabledProtocols = {
|
|
49
|
+
...session.enabledProtocols,
|
|
50
|
+
[protocol]: enabled,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const updatedSession = await BuilderSessionRepository.updateProtocols(
|
|
54
|
+
sessionId,
|
|
55
|
+
userId,
|
|
56
|
+
enabledProtocols
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Return updated session with regenerated step list
|
|
60
|
+
return {
|
|
61
|
+
session: updatedSession,
|
|
62
|
+
steps: generateStepFlow(updatedSession.enabledProtocols),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate step flow based on enabled protocols
|
|
68
|
+
* @param {Object} enabledProtocols - Protocol toggles
|
|
69
|
+
* @returns {Array} Array of step objects
|
|
70
|
+
*/
|
|
71
|
+
export function generateStepFlow(enabledProtocols) {
|
|
72
|
+
const steps = [
|
|
73
|
+
{ id: 'core', number: 1, section: 'Bot Setup', required: true },
|
|
74
|
+
{ id: 'protocols', number: 2, section: 'Capabilities', required: true },
|
|
75
|
+
{ id: 'identity', number: 3, section: 'Identity', required: true },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
let stepNumber = 4;
|
|
79
|
+
|
|
80
|
+
if (enabledProtocols.knowledge) {
|
|
81
|
+
steps.push({
|
|
82
|
+
id: 'knowledge',
|
|
83
|
+
number: stepNumber++,
|
|
84
|
+
section: 'Knowledge Base',
|
|
85
|
+
protocol: 'knowledge',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (enabledProtocols.formGathering) {
|
|
90
|
+
steps.push({
|
|
91
|
+
id: 'formGathering',
|
|
92
|
+
number: stepNumber++,
|
|
93
|
+
section: 'Form Collection',
|
|
94
|
+
protocol: 'formGathering',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (enabledProtocols.appointments) {
|
|
99
|
+
steps.push({
|
|
100
|
+
id: 'appointments',
|
|
101
|
+
number: stepNumber++,
|
|
102
|
+
section: 'Appointments',
|
|
103
|
+
protocol: 'appointments',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
steps.push({
|
|
108
|
+
id: 'deploy',
|
|
109
|
+
number: stepNumber,
|
|
110
|
+
section: 'Deploy',
|
|
111
|
+
required: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return steps;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Save core configuration
|
|
119
|
+
* @param {string} sessionId - Session ID
|
|
120
|
+
* @param {string} userId - User ID
|
|
121
|
+
* @param {Object} config - Core configuration
|
|
122
|
+
* @returns {Promise<Object>} Updated session
|
|
123
|
+
*/
|
|
124
|
+
export async function saveCoreConfig(sessionId, userId, config) {
|
|
125
|
+
await getBuilderSession(sessionId, userId); // Access check
|
|
126
|
+
|
|
127
|
+
const updatedSession = await BuilderSessionRepository.updateCoreConfig(
|
|
128
|
+
sessionId,
|
|
129
|
+
userId,
|
|
130
|
+
config
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Mark step as completed
|
|
134
|
+
await BuilderSessionRepository.updateStepProgress(sessionId, userId, 'core', 'completed');
|
|
135
|
+
|
|
136
|
+
return updatedSession;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Save identity configuration
|
|
141
|
+
* @param {string} sessionId - Session ID
|
|
142
|
+
* @param {string} userId - User ID
|
|
143
|
+
* @param {Object} config - Identity configuration
|
|
144
|
+
* @returns {Promise<Object>} Updated session
|
|
145
|
+
*/
|
|
146
|
+
export async function saveIdentityConfig(sessionId, userId, config) {
|
|
147
|
+
await getBuilderSession(sessionId, userId); // Access check
|
|
148
|
+
|
|
149
|
+
const updatedSession = await BuilderSessionRepository.updateIdentityConfig(
|
|
150
|
+
sessionId,
|
|
151
|
+
userId,
|
|
152
|
+
config
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await BuilderSessionRepository.updateStepProgress(sessionId, userId, 'identity', 'completed');
|
|
156
|
+
|
|
157
|
+
return updatedSession;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Save protocol-specific configuration
|
|
162
|
+
* @param {string} sessionId - Session ID
|
|
163
|
+
* @param {string} userId - User ID
|
|
164
|
+
* @param {string} protocol - Protocol name
|
|
165
|
+
* @param {Object} data - Protocol data
|
|
166
|
+
* @returns {Promise<Object>} Updated session
|
|
167
|
+
*/
|
|
168
|
+
export async function saveProtocolConfig(sessionId, userId, protocol, data) {
|
|
169
|
+
const session = await getBuilderSession(sessionId, userId);
|
|
170
|
+
|
|
171
|
+
// Validate protocol data
|
|
172
|
+
if (PROTOCOL_VALIDATORS[protocol]) {
|
|
173
|
+
const validation = PROTOCOL_VALIDATORS[protocol](data);
|
|
174
|
+
if (!validation.valid) {
|
|
175
|
+
throw new Error(validation.error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const updatedSession = await BuilderSessionRepository.updateProtocolData(
|
|
180
|
+
sessionId,
|
|
181
|
+
userId,
|
|
182
|
+
protocol,
|
|
183
|
+
data
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Mark protocol step as completed
|
|
187
|
+
await BuilderSessionRepository.updateStepProgress(sessionId, userId, protocol, 'completed');
|
|
188
|
+
|
|
189
|
+
return updatedSession;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Compose instructions and cache them
|
|
194
|
+
* @param {string} sessionId - Session ID
|
|
195
|
+
* @param {string} userId - User ID
|
|
196
|
+
* @returns {Promise<{ instructions: string, responseFormat: string, protocolsIncluded: string[], cached: boolean }>}
|
|
197
|
+
*/
|
|
198
|
+
export async function composeAndCache(sessionId, userId) {
|
|
199
|
+
const session = await getBuilderSession(sessionId, userId);
|
|
200
|
+
|
|
201
|
+
// Check if cached instructions are still valid
|
|
202
|
+
if (session.composedInstructions) {
|
|
203
|
+
return {
|
|
204
|
+
instructions: session.composedInstructions,
|
|
205
|
+
responseFormat: session.responseFormat,
|
|
206
|
+
cached: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Compose new instructions
|
|
211
|
+
const { instructions, responseFormat, protocolsIncluded } = await composeFromSession(session);
|
|
212
|
+
|
|
213
|
+
// Cache them
|
|
214
|
+
await BuilderSessionRepository.cacheComposedInstructions(
|
|
215
|
+
sessionId,
|
|
216
|
+
userId,
|
|
217
|
+
instructions,
|
|
218
|
+
responseFormat
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
instructions,
|
|
223
|
+
responseFormat,
|
|
224
|
+
protocolsIncluded,
|
|
225
|
+
cached: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Validate session is ready for deployment
|
|
231
|
+
* @param {string} sessionId - Session ID
|
|
232
|
+
* @param {string} userId - User ID
|
|
233
|
+
* @returns {Promise<{ valid: boolean, errors: string[] }>}
|
|
234
|
+
*/
|
|
235
|
+
export async function validateForDeployment(sessionId, userId) {
|
|
236
|
+
const session = await getBuilderSession(sessionId, userId);
|
|
237
|
+
return validateSessionForDeployment(session);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Delete a session
|
|
242
|
+
* @param {string} sessionId - Session ID
|
|
243
|
+
* @param {string} userId - User ID
|
|
244
|
+
* @returns {Promise<boolean>} True if deleted
|
|
245
|
+
*/
|
|
246
|
+
export async function deleteBuilderSession(sessionId, userId) {
|
|
247
|
+
return BuilderSessionRepository.delete(sessionId, userId);
|
|
248
|
+
}
|