@diegoaltoworks/chatter 0.31.0 → 0.33.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.
Files changed (87) hide show
  1. package/dist/flows/__fixtures__/broken/handler.d.ts +3 -0
  2. package/dist/flows/__fixtures__/broken/handler.d.ts.map +1 -0
  3. package/dist/flows/__fixtures__/greeting/handler.d.ts +3 -0
  4. package/dist/flows/__fixtures__/greeting/handler.d.ts.map +1 -0
  5. package/dist/flows/__fixtures__/greeting/prefill.d.ts +3 -0
  6. package/dist/flows/__fixtures__/greeting/prefill.d.ts.map +1 -0
  7. package/dist/flows/engine.d.ts +54 -0
  8. package/dist/flows/engine.d.ts.map +1 -0
  9. package/dist/flows/engine.test.d.ts +9 -0
  10. package/dist/flows/engine.test.d.ts.map +1 -0
  11. package/dist/flows/index.d.ts +25 -0
  12. package/dist/flows/index.d.ts.map +1 -0
  13. package/dist/flows/index.js +481 -0
  14. package/dist/flows/index.mjs +448 -0
  15. package/dist/flows/intent.d.ts +10 -0
  16. package/dist/flows/intent.d.ts.map +1 -0
  17. package/dist/flows/intent.test.d.ts +6 -0
  18. package/dist/flows/intent.test.d.ts.map +1 -0
  19. package/dist/flows/loader.d.ts +11 -0
  20. package/dist/flows/loader.d.ts.map +1 -0
  21. package/dist/flows/loader.test.d.ts +8 -0
  22. package/dist/flows/loader.test.d.ts.map +1 -0
  23. package/dist/flows/params.d.ts +15 -0
  24. package/dist/flows/params.d.ts.map +1 -0
  25. package/dist/flows/params.test.d.ts +2 -0
  26. package/dist/flows/params.test.d.ts.map +1 -0
  27. package/dist/flows/registry.d.ts +22 -0
  28. package/dist/flows/registry.d.ts.map +1 -0
  29. package/dist/flows/registry.test.d.ts +2 -0
  30. package/dist/flows/registry.test.d.ts.map +1 -0
  31. package/dist/flows/session.d.ts +15 -0
  32. package/dist/flows/session.d.ts.map +1 -0
  33. package/dist/flows/session.test.d.ts +7 -0
  34. package/dist/flows/session.test.d.ts.map +1 -0
  35. package/dist/flows/types.d.ts +101 -0
  36. package/dist/flows/types.d.ts.map +1 -0
  37. package/dist/flows/utils.d.ts +4 -0
  38. package/dist/flows/utils.d.ts.map +1 -0
  39. package/dist/flows/utils.test.d.ts +2 -0
  40. package/dist/flows/utils.test.d.ts.map +1 -0
  41. package/dist/images/cache.d.ts +32 -0
  42. package/dist/images/cache.d.ts.map +1 -0
  43. package/dist/images/cache.test.d.ts +2 -0
  44. package/dist/images/cache.test.d.ts.map +1 -0
  45. package/dist/images/cacheStore.d.ts +20 -0
  46. package/dist/images/cacheStore.d.ts.map +1 -0
  47. package/dist/images/cacheStore.test.d.ts +8 -0
  48. package/dist/images/cacheStore.test.d.ts.map +1 -0
  49. package/dist/images/captionComposer.d.ts +33 -0
  50. package/dist/images/captionComposer.d.ts.map +1 -0
  51. package/dist/images/captionComposer.test.d.ts +2 -0
  52. package/dist/images/captionComposer.test.d.ts.map +1 -0
  53. package/dist/images/cloudinaryClient.d.ts +12 -0
  54. package/dist/images/cloudinaryClient.d.ts.map +1 -0
  55. package/dist/images/cloudinaryClient.test.d.ts +2 -0
  56. package/dist/images/cloudinaryClient.test.d.ts.map +1 -0
  57. package/dist/images/cloudinaryConfig.d.ts +29 -0
  58. package/dist/images/cloudinaryConfig.d.ts.map +1 -0
  59. package/dist/images/cloudinaryConfig.test.d.ts +2 -0
  60. package/dist/images/cloudinaryConfig.test.d.ts.map +1 -0
  61. package/dist/images/cloudinarySignature.d.ts +9 -0
  62. package/dist/images/cloudinarySignature.d.ts.map +1 -0
  63. package/dist/images/cloudinarySignature.test.d.ts +2 -0
  64. package/dist/images/cloudinarySignature.test.d.ts.map +1 -0
  65. package/dist/images/config.d.ts +28 -0
  66. package/dist/images/config.d.ts.map +1 -0
  67. package/dist/images/config.test.d.ts +2 -0
  68. package/dist/images/config.test.d.ts.map +1 -0
  69. package/dist/images/generator.d.ts +19 -0
  70. package/dist/images/generator.d.ts.map +1 -0
  71. package/dist/images/generator.test.d.ts +2 -0
  72. package/dist/images/generator.test.d.ts.map +1 -0
  73. package/dist/images/index.d.ts +34 -0
  74. package/dist/images/index.d.ts.map +1 -0
  75. package/dist/images/index.js +412 -0
  76. package/dist/images/index.mjs +357 -0
  77. package/dist/images/openaiClient.d.ts +14 -0
  78. package/dist/images/openaiClient.d.ts.map +1 -0
  79. package/dist/images/openaiClient.test.d.ts +2 -0
  80. package/dist/images/openaiClient.test.d.ts.map +1 -0
  81. package/dist/images/types.d.ts +96 -0
  82. package/dist/images/types.d.ts.map +1 -0
  83. package/dist/images/uploader.d.ts +39 -0
  84. package/dist/images/uploader.d.ts.map +1 -0
  85. package/dist/images/uploader.test.d.ts +2 -0
  86. package/dist/images/uploader.test.d.ts.map +1 -0
  87. package/package.json +14 -2
@@ -0,0 +1,448 @@
1
+ // src/flows/intent.ts
2
+ async function detectIntent(client, model, message, flows, conversationContext) {
3
+ const contextStr = conversationContext && conversationContext.length > 0 ? `
4
+
5
+ Recent conversation:
6
+ ${conversationContext.join("\n")}` : "";
7
+ const flowDescriptions = Array.from(flows.entries()).map(([id, flow], index) => {
8
+ const keywords = flow.definition.triggerKeywords.join(", ");
9
+ return `${index + 1}. ${id} -- ${flow.definition.description}
10
+ - Keywords: ${keywords}`;
11
+ }).join("\n\n");
12
+ const systemPrompt = `You are an intent detector.
13
+
14
+ Detect ONE category. Use these EXACT ids (they must match flow ids):
15
+
16
+ ${flowDescriptions}
17
+
18
+ ${flows.size + 1}. chatbot -- All other questions (general fallback)
19
+ ${contextStr}
20
+
21
+ Return JSON with:
22
+ - "intent": one of the ids above (exact string)
23
+ - "confidence": 0.0-1.0 (how certain you are)
24
+ - "reasoning": brief explanation
25
+
26
+ Rules:
27
+ - If uncertain, always return "chatbot" with low confidence`;
28
+ try {
29
+ const response = await client.chat.completions.create({
30
+ model,
31
+ temperature: 0.1,
32
+ response_format: { type: "json_object" },
33
+ messages: [
34
+ { role: "system", content: systemPrompt },
35
+ { role: "user", content: message }
36
+ ]
37
+ });
38
+ const content = response.choices[0]?.message?.content || "{}";
39
+ const raw = JSON.parse(content);
40
+ const validFlowIds = new Set(flows.keys());
41
+ const intent = validFlowIds.has(raw.intent) ? raw.intent : "chatbot";
42
+ const confidence = typeof raw.confidence === "number" ? raw.confidence : 0;
43
+ return { intent, confidence, reasoning: raw.reasoning };
44
+ } catch (error) {
45
+ console.warn(
46
+ `[flows] intent detection failed: ${error instanceof Error ? error.message : String(error)}`
47
+ );
48
+ return { intent: "chatbot", confidence: 0, reasoning: "exception, defaulting to chatbot" };
49
+ }
50
+ }
51
+
52
+ // src/flows/loader.ts
53
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
54
+ import { join, resolve } from "node:path";
55
+ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set(["lib", "tests", "registry"]);
56
+ async function loadFlowsFromDirectory(flowsDir) {
57
+ const flows = /* @__PURE__ */ new Map();
58
+ if (!existsSync(flowsDir)) {
59
+ console.warn(`[flows] directory does not exist: ${flowsDir}`);
60
+ return flows;
61
+ }
62
+ const flowDirs = readdirSync(flowsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).filter((dirent) => !SKIPPED_DIR_NAMES.has(dirent.name)).map((dirent) => dirent.name);
63
+ for (const flowName of flowDirs) {
64
+ try {
65
+ flows.set(flowName, await loadFlow(flowsDir, flowName));
66
+ } catch (error) {
67
+ console.warn(
68
+ `[flows] failed to load "${flowName}": ${error instanceof Error ? error.message : String(error)}`
69
+ );
70
+ }
71
+ }
72
+ return flows;
73
+ }
74
+ async function loadFlow(flowsDir, flowName) {
75
+ const flowPath = resolve(flowsDir, flowName);
76
+ const definitionPath = join(flowPath, "flow.json");
77
+ const instructionsPath = join(flowPath, "instructions.md");
78
+ const handlerPath = join(flowPath, "handler.ts");
79
+ if (!existsSync(definitionPath)) {
80
+ throw new Error(`flow.json not found for ${flowName}`);
81
+ }
82
+ if (!existsSync(instructionsPath)) {
83
+ throw new Error(`instructions.md not found for ${flowName}`);
84
+ }
85
+ if (!existsSync(handlerPath)) {
86
+ throw new Error(`handler.ts not found for ${flowName}`);
87
+ }
88
+ const definition = JSON.parse(readFileSync(definitionPath, "utf-8"));
89
+ if (definition.id !== flowName) {
90
+ throw new Error(`Flow id mismatch: ${definition.id} !== ${flowName}`);
91
+ }
92
+ if (!definition.triggerKeywords || definition.triggerKeywords.length === 0) {
93
+ throw new Error(`Flow ${flowName} has no trigger keywords`);
94
+ }
95
+ if (!definition.schema || Object.keys(definition.schema.properties ?? {}).length === 0) {
96
+ throw new Error(`Flow ${flowName} has no schema`);
97
+ }
98
+ const handlerModule = await import(handlerPath);
99
+ const handler = handlerModule.execute;
100
+ if (typeof handler !== "function") {
101
+ throw new Error(`Flow ${flowName} handler.ts must export an 'execute' function`);
102
+ }
103
+ const prefillPath = join(flowPath, "prefill.ts");
104
+ let prefill;
105
+ if (existsSync(prefillPath)) {
106
+ const prefillModule = await import(prefillPath);
107
+ if (typeof prefillModule.prefillFromContext === "function") {
108
+ prefill = prefillModule.prefillFromContext;
109
+ } else {
110
+ console.warn(
111
+ `[flows] "${flowName}" prefill.ts does not export a prefillFromContext function`
112
+ );
113
+ }
114
+ }
115
+ return { definition, handler, instructionsPath, prefill };
116
+ }
117
+
118
+ // src/flows/params.ts
119
+ import { readFileSync as readFileSync2 } from "node:fs";
120
+ function isMissing(value) {
121
+ return value === void 0 || value === null;
122
+ }
123
+ async function extractParameters(client, model, flow, message, existingParams, now = Date.now) {
124
+ const instructions = readFileSync2(flow.instructionsPath, "utf-8");
125
+ const schema = flow.definition.schema;
126
+ const properties = schema.properties || {};
127
+ const required = schema.required || [];
128
+ const schemaDescription = Object.entries(properties).map(
129
+ ([key, field]) => `- ${key} (${field.type}${required.includes(key) ? ", required" : ""}): ${field.description || ""}`
130
+ ).join("\n");
131
+ const existingParamsStr = Object.keys(existingParams).length > 0 ? `
132
+
133
+ Already collected:
134
+ ${JSON.stringify(existingParams, null, 2)}` : "";
135
+ const missingFields = required.filter((key) => isMissing(existingParams[key]));
136
+ const currentlyAsking = missingFields.length > 0 ? missingFields[0] : null;
137
+ const nowDate = new Date(now());
138
+ const todayStr = nowDate.toISOString().split("T")[0];
139
+ const currentYear = nowDate.getFullYear();
140
+ const currentMonth = nowDate.getMonth() + 1;
141
+ const nextYear = currentYear + 1;
142
+ const tomorrow = new Date(nowDate);
143
+ tomorrow.setDate(tomorrow.getDate() + 1);
144
+ const tomorrowStr = tomorrow.toISOString().split("T")[0];
145
+ const dateContext = `
146
+
147
+ ## CRITICAL DATE CONTEXT
148
+ TODAY IS: ${todayStr} (year ${currentYear}, month ${currentMonth})
149
+
150
+ **DATE EXTRACTION RULES:**
151
+ - "tomorrow" = ${tomorrowStr}
152
+ - Months 1-${currentMonth} = year ${nextYear}
153
+ - Months ${currentMonth + 1}-12 = year ${currentYear}
154
+ - NEVER output a date before ${todayStr}`;
155
+ const currentFieldHint = currentlyAsking ? `
156
+
157
+ ## IMPORTANT CONTEXT
158
+ We just asked the user for: "${currentlyAsking}"
159
+ If the user's response is a simple value, interpret it as the value for the "${currentlyAsking}" parameter.` : "";
160
+ const systemPrompt = `${instructions}
161
+ ${dateContext}
162
+
163
+ ## Schema
164
+ ${schemaDescription}
165
+ ${existingParamsStr}
166
+ ${currentFieldHint}
167
+
168
+ ## Task
169
+ Extract any parameters from the user's message that match the schema above.
170
+ Return JSON with:
171
+ - "extractedParams": object with any NEW parameters found
172
+
173
+ Do not compute "allParamsFilled" - the caller recomputes it from the merged params.`;
174
+ const response = await client.chat.completions.create({
175
+ model,
176
+ temperature: 0.2,
177
+ response_format: { type: "json_object" },
178
+ messages: [
179
+ { role: "system", content: systemPrompt },
180
+ { role: "user", content: message }
181
+ ]
182
+ });
183
+ const content = response.choices[0]?.message?.content || "{}";
184
+ const result = JSON.parse(content);
185
+ const extractedParams = Object.fromEntries(
186
+ Object.entries(result.extractedParams || {}).filter(([, value]) => !isMissing(value))
187
+ );
188
+ const mergedParams = { ...existingParams, ...extractedParams };
189
+ const allParamsFilled = required.every((key) => !isMissing(mergedParams[key]));
190
+ let nextPrompt;
191
+ if (!allParamsFilled) {
192
+ const missingKey = required.find((key) => isMissing(mergedParams[key]));
193
+ if (missingKey) {
194
+ nextPrompt = `What is the ${missingKey.replace(/([A-Z])/g, " $1").toLowerCase()}?`;
195
+ }
196
+ }
197
+ return { extractedParams, allParamsFilled, nextPrompt };
198
+ }
199
+
200
+ // src/flows/registry.ts
201
+ import { readFileSync as readFileSync3 } from "node:fs";
202
+ function createFlowRegistry(flows, criticalKeywords = {}) {
203
+ const keywordEntries = Object.entries(criticalKeywords);
204
+ return {
205
+ getFlow(id) {
206
+ return flows.get(id);
207
+ },
208
+ getAllFlows() {
209
+ return Array.from(flows.values());
210
+ },
211
+ getInstructions(id) {
212
+ const flow = flows.get(id);
213
+ if (!flow) {
214
+ throw new Error(`Flow ${id} not found`);
215
+ }
216
+ return readFileSync3(flow.instructionsPath, "utf-8");
217
+ },
218
+ matchByKeyword(message) {
219
+ const lowerMessage = message.toLowerCase();
220
+ for (const [keyword, flowId] of keywordEntries) {
221
+ if (lowerMessage.includes(keyword.toLowerCase())) {
222
+ const flow = flows.get(flowId);
223
+ if (flow) return flow;
224
+ }
225
+ }
226
+ return void 0;
227
+ }
228
+ };
229
+ }
230
+
231
+ // src/flows/utils.ts
232
+ var DEFAULT_CANCEL_KEYWORDS = ["cancel", "nevermind", "never mind", "stop", "forget it", "quit"];
233
+ function escapeRegExp(value) {
234
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
235
+ }
236
+ function containsWholePhrase(message, phrase) {
237
+ return new RegExp(`(^|\\W)${escapeRegExp(phrase)}($|\\W)`, "i").test(message);
238
+ }
239
+ function shouldExitFlow(message, cancelKeywords = DEFAULT_CANCEL_KEYWORDS) {
240
+ return cancelKeywords.some((keyword) => containsWholePhrase(message, keyword));
241
+ }
242
+
243
+ // src/flows/engine.ts
244
+ var DEFAULT_MIN_CONFIDENCE = 0.7;
245
+ var DEFAULT_CANCEL_MESSAGE = "Okay, cancelled.";
246
+ var DEFAULT_ERROR_MESSAGE = "Sorry, something went wrong. Please try again.";
247
+ function createFlowEngine(options) {
248
+ const {
249
+ client,
250
+ model,
251
+ flowsDir,
252
+ sessionStore,
253
+ criticalKeywords,
254
+ minConfidence = DEFAULT_MIN_CONFIDENCE,
255
+ cancelKeywords,
256
+ cancelMessage = DEFAULT_CANCEL_MESSAGE,
257
+ errorMessage = DEFAULT_ERROR_MESSAGE,
258
+ sessionTtlMs,
259
+ now = Date.now
260
+ } = options;
261
+ let registry = createFlowRegistry(/* @__PURE__ */ new Map());
262
+ async function matchFlow(message, conversationContext) {
263
+ const byKeyword = registry.matchByKeyword(message);
264
+ if (byKeyword) return byKeyword;
265
+ const allFlows = registry.getAllFlows();
266
+ if (allFlows.length === 0) return void 0;
267
+ const flowMap = new Map(allFlows.map((flow) => [flow.definition.id, flow]));
268
+ const detection = await detectIntent(client, model, message, flowMap, conversationContext);
269
+ if (detection.confidence >= minConfidence) {
270
+ return registry.getFlow(detection.intent);
271
+ }
272
+ return void 0;
273
+ }
274
+ async function runToCompletionOrPrompt(flow, sessionKey, message, existingParams, attempts, startedAt, channel) {
275
+ const extraction = await extractParameters(client, model, flow, message, existingParams, now);
276
+ const mergedParams = { ...existingParams, ...extraction.extractedParams };
277
+ if (extraction.allParamsFilled) {
278
+ const result = await flow.handler(mergedParams, { sessionKey, channel });
279
+ await sessionStore.clear(sessionKey);
280
+ return {
281
+ isFlowActive: false,
282
+ message: result.message,
283
+ flowCompleted: true,
284
+ flowSuccess: result.success,
285
+ result: result.result
286
+ };
287
+ }
288
+ await sessionStore.set(sessionKey, {
289
+ flowId: flow.definition.id,
290
+ params: mergedParams,
291
+ attempts: attempts + 1,
292
+ startedAt
293
+ });
294
+ return {
295
+ isFlowActive: true,
296
+ message: extraction.nextPrompt || "Could you provide more details?",
297
+ flowCompleted: false
298
+ };
299
+ }
300
+ return {
301
+ async loadFlows() {
302
+ const flows = await loadFlowsFromDirectory(flowsDir);
303
+ registry = createFlowRegistry(flows, criticalKeywords);
304
+ },
305
+ getFlow(id) {
306
+ return registry.getFlow(id);
307
+ },
308
+ getAllFlows() {
309
+ return registry.getAllFlows();
310
+ },
311
+ async process(sessionKey, message, processOptions) {
312
+ const channel = processOptions?.channel;
313
+ let activeSession = await sessionStore.get(sessionKey);
314
+ if (activeSession && sessionTtlMs !== void 0 && now() - activeSession.startedAt > sessionTtlMs) {
315
+ await sessionStore.clear(sessionKey);
316
+ activeSession = null;
317
+ }
318
+ if (activeSession && shouldExitFlow(message, cancelKeywords)) {
319
+ await sessionStore.clear(sessionKey);
320
+ return {
321
+ isFlowActive: false,
322
+ message: cancelMessage,
323
+ flowCompleted: false,
324
+ cancelled: true
325
+ };
326
+ }
327
+ if (activeSession) {
328
+ const flow = registry.getFlow(activeSession.flowId);
329
+ if (!flow) {
330
+ await sessionStore.clear(sessionKey);
331
+ return { isFlowActive: false, message: errorMessage, flowCompleted: false, error: true };
332
+ }
333
+ try {
334
+ return await runToCompletionOrPrompt(
335
+ flow,
336
+ sessionKey,
337
+ message,
338
+ activeSession.params,
339
+ activeSession.attempts,
340
+ activeSession.startedAt,
341
+ channel
342
+ );
343
+ } catch {
344
+ return { isFlowActive: true, message: errorMessage, flowCompleted: false, error: true };
345
+ }
346
+ }
347
+ const matched = await matchFlow(message, processOptions?.conversationContext);
348
+ if (!matched) {
349
+ return { isFlowActive: false, message: "", flowCompleted: false };
350
+ }
351
+ try {
352
+ const globalParams = matched.prefill ? matched.prefill(sessionKey, processOptions?.prefillContext ?? {}) : {};
353
+ return await runToCompletionOrPrompt(
354
+ matched,
355
+ sessionKey,
356
+ message,
357
+ globalParams,
358
+ 0,
359
+ now(),
360
+ channel
361
+ );
362
+ } catch {
363
+ return { isFlowActive: false, message: errorMessage, flowCompleted: false, error: true };
364
+ }
365
+ }
366
+ };
367
+ }
368
+
369
+ // src/flows/session.ts
370
+ var schemaReady = /* @__PURE__ */ new WeakMap();
371
+ function ensureSessionSchema(client, tableName) {
372
+ let perTable = schemaReady.get(client);
373
+ if (!perTable) {
374
+ perTable = /* @__PURE__ */ new Map();
375
+ schemaReady.set(client, perTable);
376
+ }
377
+ const tables = perTable;
378
+ let ready = tables.get(tableName);
379
+ if (!ready) {
380
+ ready = client.execute(
381
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
382
+ session_key TEXT PRIMARY KEY,
383
+ flow_id TEXT NOT NULL,
384
+ params TEXT NOT NULL,
385
+ attempts INTEGER NOT NULL,
386
+ started_at INTEGER NOT NULL
387
+ )`
388
+ ).catch((error) => {
389
+ tables.delete(tableName);
390
+ throw error;
391
+ });
392
+ tables.set(tableName, ready);
393
+ }
394
+ return ready;
395
+ }
396
+ var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
397
+ function createTursoFlowSessionStore(client, tableName = "flow_sessions") {
398
+ if (!VALID_TABLE_NAME.test(tableName)) {
399
+ throw new Error(`Invalid flow session store table name: ${tableName}`);
400
+ }
401
+ return {
402
+ async get(sessionKey) {
403
+ await ensureSessionSchema(client, tableName);
404
+ const result = await client.execute({
405
+ sql: `SELECT flow_id, params, attempts, started_at FROM ${tableName} WHERE session_key = ?`,
406
+ args: [sessionKey]
407
+ });
408
+ const row = result.rows[0];
409
+ if (!row) return null;
410
+ return {
411
+ flowId: String(row.flow_id),
412
+ params: JSON.parse(String(row.params)),
413
+ attempts: Number(row.attempts),
414
+ startedAt: Number(row.started_at)
415
+ };
416
+ },
417
+ async set(sessionKey, state) {
418
+ await ensureSessionSchema(client, tableName);
419
+ await client.execute({
420
+ sql: `INSERT INTO ${tableName} (session_key, flow_id, params, attempts, started_at) VALUES (?, ?, ?, ?, ?)
421
+ ON CONFLICT(session_key) DO UPDATE SET flow_id = excluded.flow_id, params = excluded.params, attempts = excluded.attempts, started_at = excluded.started_at`,
422
+ args: [
423
+ sessionKey,
424
+ state.flowId,
425
+ JSON.stringify(state.params),
426
+ state.attempts,
427
+ state.startedAt
428
+ ]
429
+ });
430
+ },
431
+ async clear(sessionKey) {
432
+ await ensureSessionSchema(client, tableName);
433
+ await client.execute({
434
+ sql: `DELETE FROM ${tableName} WHERE session_key = ?`,
435
+ args: [sessionKey]
436
+ });
437
+ }
438
+ };
439
+ }
440
+ export {
441
+ createFlowEngine,
442
+ createFlowRegistry,
443
+ createTursoFlowSessionStore,
444
+ detectIntent,
445
+ extractParameters,
446
+ loadFlowsFromDirectory,
447
+ shouldExitFlow
448
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * LLM-powered intent detection — classifies a message into a flow id, or the
3
+ * `chatbot` fallback, using the caller's OpenAI client (never a raw fetch, so
4
+ * the same client tests already fake for chat completions covers this too).
5
+ */
6
+ import type OpenAI from "openai";
7
+ import type { IntentDetection, LoadedFlow } from "./types";
8
+ /** Detects which loaded flow (if any) a message expresses intent for. */
9
+ export declare function detectIntent(client: OpenAI, model: string, message: string, flows: Map<string, LoadedFlow>, conversationContext?: string[]): Promise<IntentDetection>;
10
+ //# sourceMappingURL=intent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent.d.ts","sourceRoot":"","sources":["../../src/flows/intent.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE3D,yEAAyE;AACzE,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAC9B,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAC7B,OAAO,CAAC,eAAe,CAAC,CAyD1B"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Intent detection tests — a faked OpenAI client (see src/routes/openai.test.ts
3
+ * for the same pattern), never a real API call.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=intent.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent.test.d.ts","sourceRoot":"","sources":["../../src/flows/intent.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Directory-based flow loader.
3
+ *
4
+ * Discovers flow directories on disk and loads each one's contract. Kept
5
+ * byte-for-byte compatible with the reference implementation's on-disk
6
+ * shape, so existing flow directories load unchanged.
7
+ */
8
+ import type { LoadedFlow } from "./types";
9
+ /** Loads every flow directory under `flowsDir`. A flow that fails to load is skipped, not fatal to the rest. */
10
+ export declare function loadFlowsFromDirectory(flowsDir: string): Promise<Map<string, LoadedFlow>>;
11
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/flows/loader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAA4C,UAAU,EAAE,MAAM,SAAS,CAAC;AAIpF,gHAAgH;AAChH,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAwB/F"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Loader tests — verifies flow directories load per the reference
3
+ * implementation's on-disk contract (flow.json/handler.ts/instructions.md,
4
+ * optional prefill.ts), and that a malformed directory is skipped rather
5
+ * than failing the whole load.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=loader.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.test.d.ts","sourceRoot":"","sources":["../../src/flows/loader.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Schema-driven parameter extraction — pulls structured params out of a
3
+ * message using the flow's `instructions.md` and JSON schema, across as many
4
+ * turns as it takes to fill every required field.
5
+ */
6
+ import type OpenAI from "openai";
7
+ import type { FlowExtractionResult, LoadedFlow } from "./types";
8
+ /**
9
+ * Extracts (and merges) params from `message` against `flow`'s schema.
10
+ *
11
+ * `now` is injectable so date-relative prompt context ("tomorrow") is
12
+ * deterministic in tests; defaults to the real clock.
13
+ */
14
+ export declare function extractParameters(client: OpenAI, model: string, flow: LoadedFlow, message: string, existingParams: Record<string, unknown>, now?: () => number): Promise<FlowExtractionResult>;
15
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../../src/flows/params.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAOhE;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,GAAG,GAAE,MAAM,MAAiB,GAC3B,OAAO,CAAC,oBAAoB,CAAC,CA6F/B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=params.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.test.d.ts","sourceRoot":"","sources":["../../src/flows/params.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Flow registry — lookup and the pure (non-LLM) half of matching.
3
+ *
4
+ * Framework-free: holds loaded flows and answers keyword lookups
5
+ * synchronously. LLM-based intent matching lives in `./intent` and is
6
+ * orchestrated by `./engine`, which is what decides whether the keyword step
7
+ * or the LLM step wins.
8
+ */
9
+ import type { LoadedFlow } from "./types";
10
+ export interface FlowRegistry {
11
+ getFlow(id: string): LoadedFlow | undefined;
12
+ getAllFlows(): LoadedFlow[];
13
+ getInstructions(id: string): string;
14
+ /**
15
+ * Immediate, deterministic trigger: the first configured keyword found in
16
+ * `message` (case-insensitive substring match) wins over LLM intent
17
+ * detection. Empty/unset `criticalKeywords` disables this step entirely.
18
+ */
19
+ matchByKeyword(message: string): LoadedFlow | undefined;
20
+ }
21
+ export declare function createFlowRegistry(flows: Map<string, LoadedFlow>, criticalKeywords?: Record<string, string>): FlowRegistry;
22
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/flows/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC5C,WAAW,IAAI,UAAU,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC;;;;OAIG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;CACzD;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAC9B,gBAAgB,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAC5C,YAAY,CA4Bd"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../../src/flows/registry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Turso/libsql-backed {@link FlowSessionStore}.
3
+ *
4
+ * Session state lives in the database rather than process memory because
5
+ * deployments run multiple instances — the instance that handles turn 2 of a
6
+ * multi-turn flow is not guaranteed to be the one that handled turn 1.
7
+ *
8
+ * `@libsql/client` is imported for its type alone, so this module adds no
9
+ * runtime dependency to anyone importing `./flows` for the pure engine.
10
+ */
11
+ import type { Client } from "@libsql/client";
12
+ import type { FlowSessionStore } from "./types";
13
+ /** Creates a session store keyed by an arbitrary session id (phone number, WhatsApp JID, web session id) on `tableName`. */
14
+ export declare function createTursoFlowSessionStore(client: Client, tableName?: string): FlowSessionStore;
15
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/flows/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAyClE,4HAA4H;AAC5H,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,MAAM,EACd,SAAS,SAAkB,GAC1B,gBAAgB,CA6ClB"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Turso flow session store tests — run against a real in-memory libsql
3
+ * client, since what's worth proving here is the SQL: upsert-on-conflict
4
+ * behaves as an upsert, and table creation is idempotent.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=session.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.test.d.ts","sourceRoot":"","sources":["../../src/flows/session.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}