@elizaos/plugin-form 2.0.0-alpha.2 → 2.0.0-alpha.4
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/dist/index.js +3293 -0
- package/dist/index.js.map +30 -0
- package/package.json +12 -10
- package/LICENSE +0 -21
package/dist/index.js
ADDED
|
@@ -0,0 +1,3293 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
set: (newValue) => all[name] = () => newValue
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
|
+
|
|
13
|
+
// src/types.ts
|
|
14
|
+
var FORM_CONTROL_DEFAULTS, FORM_DEFINITION_DEFAULTS, FORM_SESSION_COMPONENT = "form_session", FORM_SUBMISSION_COMPONENT = "form_submission", FORM_AUTOFILL_COMPONENT = "form_autofill";
|
|
15
|
+
var init_types = __esm(() => {
|
|
16
|
+
FORM_CONTROL_DEFAULTS = {
|
|
17
|
+
type: "text",
|
|
18
|
+
required: false,
|
|
19
|
+
confirmThreshold: 0.8
|
|
20
|
+
};
|
|
21
|
+
FORM_DEFINITION_DEFAULTS = {
|
|
22
|
+
version: 1,
|
|
23
|
+
status: "active",
|
|
24
|
+
ux: {
|
|
25
|
+
allowUndo: true,
|
|
26
|
+
allowSkip: true,
|
|
27
|
+
maxUndoSteps: 5,
|
|
28
|
+
showExamples: true,
|
|
29
|
+
showExplanations: true,
|
|
30
|
+
allowAutofill: true
|
|
31
|
+
},
|
|
32
|
+
ttl: {
|
|
33
|
+
minDays: 14,
|
|
34
|
+
maxDays: 90,
|
|
35
|
+
effortMultiplier: 0.5
|
|
36
|
+
},
|
|
37
|
+
nudge: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
afterInactiveHours: 48,
|
|
40
|
+
maxNudges: 3
|
|
41
|
+
},
|
|
42
|
+
debug: false
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/builtins.ts
|
|
47
|
+
function registerBuiltinTypes(registerFn) {
|
|
48
|
+
for (const type of BUILTIN_TYPES) {
|
|
49
|
+
registerFn(type);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getBuiltinType(id) {
|
|
53
|
+
return BUILTIN_TYPE_MAP.get(id);
|
|
54
|
+
}
|
|
55
|
+
function isBuiltinType(id) {
|
|
56
|
+
return BUILTIN_TYPE_MAP.has(id);
|
|
57
|
+
}
|
|
58
|
+
var EMAIL_REGEX, ISO_DATE_REGEX, textType, numberType, emailType, booleanType, selectType, dateType, fileType, BUILTIN_TYPES, BUILTIN_TYPE_MAP;
|
|
59
|
+
var init_builtins = __esm(() => {
|
|
60
|
+
EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
61
|
+
ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
62
|
+
textType = {
|
|
63
|
+
id: "text",
|
|
64
|
+
builtin: true,
|
|
65
|
+
validate: (value, control) => {
|
|
66
|
+
if (value === null || value === undefined) {
|
|
67
|
+
return { valid: true };
|
|
68
|
+
}
|
|
69
|
+
const str = String(value);
|
|
70
|
+
if (control.minLength !== undefined && str.length < control.minLength) {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
error: `Must be at least ${control.minLength} characters`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (control.maxLength !== undefined && str.length > control.maxLength) {
|
|
77
|
+
return {
|
|
78
|
+
valid: false,
|
|
79
|
+
error: `Must be at most ${control.maxLength} characters`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (control.pattern) {
|
|
83
|
+
const regex = new RegExp(control.pattern);
|
|
84
|
+
if (!regex.test(str)) {
|
|
85
|
+
return { valid: false, error: "Invalid format" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (control.enum && !control.enum.includes(str)) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: `Must be one of: ${control.enum.join(", ")}`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { valid: true };
|
|
95
|
+
},
|
|
96
|
+
parse: (value) => String(value).trim(),
|
|
97
|
+
format: (value) => String(value ?? ""),
|
|
98
|
+
extractionPrompt: "a text string"
|
|
99
|
+
};
|
|
100
|
+
numberType = {
|
|
101
|
+
id: "number",
|
|
102
|
+
builtin: true,
|
|
103
|
+
validate: (value, control) => {
|
|
104
|
+
if (value === null || value === undefined || value === "") {
|
|
105
|
+
return { valid: true };
|
|
106
|
+
}
|
|
107
|
+
const num = typeof value === "number" ? value : parseFloat(String(value));
|
|
108
|
+
if (Number.isNaN(num)) {
|
|
109
|
+
return { valid: false, error: "Must be a valid number" };
|
|
110
|
+
}
|
|
111
|
+
if (control.min !== undefined && num < control.min) {
|
|
112
|
+
return { valid: false, error: `Must be at least ${control.min}` };
|
|
113
|
+
}
|
|
114
|
+
if (control.max !== undefined && num > control.max) {
|
|
115
|
+
return { valid: false, error: `Must be at most ${control.max}` };
|
|
116
|
+
}
|
|
117
|
+
return { valid: true };
|
|
118
|
+
},
|
|
119
|
+
parse: (value) => {
|
|
120
|
+
const cleaned = value.replace(/[,$\s]/g, "");
|
|
121
|
+
return parseFloat(cleaned);
|
|
122
|
+
},
|
|
123
|
+
format: (value) => {
|
|
124
|
+
if (value === null || value === undefined)
|
|
125
|
+
return "";
|
|
126
|
+
const num = typeof value === "number" ? value : parseFloat(String(value));
|
|
127
|
+
if (Number.isNaN(num))
|
|
128
|
+
return String(value);
|
|
129
|
+
return num.toLocaleString();
|
|
130
|
+
},
|
|
131
|
+
extractionPrompt: "a number (integer or decimal)"
|
|
132
|
+
};
|
|
133
|
+
emailType = {
|
|
134
|
+
id: "email",
|
|
135
|
+
builtin: true,
|
|
136
|
+
validate: (value) => {
|
|
137
|
+
if (value === null || value === undefined || value === "") {
|
|
138
|
+
return { valid: true };
|
|
139
|
+
}
|
|
140
|
+
const str = String(value).trim().toLowerCase();
|
|
141
|
+
if (!EMAIL_REGEX.test(str)) {
|
|
142
|
+
return { valid: false, error: "Invalid email format" };
|
|
143
|
+
}
|
|
144
|
+
return { valid: true };
|
|
145
|
+
},
|
|
146
|
+
parse: (value) => value.trim().toLowerCase(),
|
|
147
|
+
format: (value) => String(value ?? "").toLowerCase(),
|
|
148
|
+
extractionPrompt: "an email address (e.g., user@example.com)"
|
|
149
|
+
};
|
|
150
|
+
booleanType = {
|
|
151
|
+
id: "boolean",
|
|
152
|
+
builtin: true,
|
|
153
|
+
validate: (value) => {
|
|
154
|
+
if (value === null || value === undefined) {
|
|
155
|
+
return { valid: true };
|
|
156
|
+
}
|
|
157
|
+
if (typeof value === "boolean") {
|
|
158
|
+
return { valid: true };
|
|
159
|
+
}
|
|
160
|
+
const str = String(value).toLowerCase();
|
|
161
|
+
const validValues = ["true", "false", "yes", "no", "1", "0", "on", "off"];
|
|
162
|
+
if (!validValues.includes(str)) {
|
|
163
|
+
return { valid: false, error: "Must be yes/no or true/false" };
|
|
164
|
+
}
|
|
165
|
+
return { valid: true };
|
|
166
|
+
},
|
|
167
|
+
parse: (value) => {
|
|
168
|
+
const str = value.toLowerCase();
|
|
169
|
+
return ["true", "yes", "1", "on"].includes(str);
|
|
170
|
+
},
|
|
171
|
+
format: (value) => {
|
|
172
|
+
if (value === true)
|
|
173
|
+
return "Yes";
|
|
174
|
+
if (value === false)
|
|
175
|
+
return "No";
|
|
176
|
+
return String(value ?? "");
|
|
177
|
+
},
|
|
178
|
+
extractionPrompt: "a yes/no or true/false value"
|
|
179
|
+
};
|
|
180
|
+
selectType = {
|
|
181
|
+
id: "select",
|
|
182
|
+
builtin: true,
|
|
183
|
+
validate: (value, control) => {
|
|
184
|
+
if (value === null || value === undefined || value === "") {
|
|
185
|
+
return { valid: true };
|
|
186
|
+
}
|
|
187
|
+
const str = String(value);
|
|
188
|
+
if (control.options) {
|
|
189
|
+
const validValues = control.options.map((o) => o.value);
|
|
190
|
+
if (!validValues.includes(str)) {
|
|
191
|
+
const labels = control.options.map((o) => o.label).join(", ");
|
|
192
|
+
return { valid: false, error: `Must be one of: ${labels}` };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (control.enum && !control.options) {
|
|
196
|
+
if (!control.enum.includes(str)) {
|
|
197
|
+
return {
|
|
198
|
+
valid: false,
|
|
199
|
+
error: `Must be one of: ${control.enum.join(", ")}`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { valid: true };
|
|
204
|
+
},
|
|
205
|
+
parse: (value) => value.trim(),
|
|
206
|
+
format: (value) => String(value ?? ""),
|
|
207
|
+
extractionPrompt: "one of the available options"
|
|
208
|
+
};
|
|
209
|
+
dateType = {
|
|
210
|
+
id: "date",
|
|
211
|
+
builtin: true,
|
|
212
|
+
validate: (value) => {
|
|
213
|
+
if (value === null || value === undefined || value === "") {
|
|
214
|
+
return { valid: true };
|
|
215
|
+
}
|
|
216
|
+
const str = String(value);
|
|
217
|
+
if (!ISO_DATE_REGEX.test(str)) {
|
|
218
|
+
return { valid: false, error: "Must be in YYYY-MM-DD format" };
|
|
219
|
+
}
|
|
220
|
+
const date = new Date(str);
|
|
221
|
+
if (Number.isNaN(date.getTime())) {
|
|
222
|
+
return { valid: false, error: "Invalid date" };
|
|
223
|
+
}
|
|
224
|
+
return { valid: true };
|
|
225
|
+
},
|
|
226
|
+
parse: (value) => {
|
|
227
|
+
const date = new Date(value);
|
|
228
|
+
if (!Number.isNaN(date.getTime())) {
|
|
229
|
+
return date.toISOString().split("T")[0];
|
|
230
|
+
}
|
|
231
|
+
return value.trim();
|
|
232
|
+
},
|
|
233
|
+
format: (value) => {
|
|
234
|
+
if (!value)
|
|
235
|
+
return "";
|
|
236
|
+
const date = new Date(String(value));
|
|
237
|
+
if (Number.isNaN(date.getTime()))
|
|
238
|
+
return String(value);
|
|
239
|
+
return date.toLocaleDateString();
|
|
240
|
+
},
|
|
241
|
+
extractionPrompt: "a date (preferably in YYYY-MM-DD format)"
|
|
242
|
+
};
|
|
243
|
+
fileType = {
|
|
244
|
+
id: "file",
|
|
245
|
+
builtin: true,
|
|
246
|
+
validate: (value, _control) => {
|
|
247
|
+
if (value === null || value === undefined) {
|
|
248
|
+
return { valid: true };
|
|
249
|
+
}
|
|
250
|
+
if (typeof value === "object") {
|
|
251
|
+
return { valid: true };
|
|
252
|
+
}
|
|
253
|
+
return { valid: false, error: "Invalid file data" };
|
|
254
|
+
},
|
|
255
|
+
format: (value) => {
|
|
256
|
+
if (!value)
|
|
257
|
+
return "";
|
|
258
|
+
if (Array.isArray(value)) {
|
|
259
|
+
return `${value.length} file(s)`;
|
|
260
|
+
}
|
|
261
|
+
if (typeof value === "object" && value !== null && "name" in value) {
|
|
262
|
+
return String(value.name);
|
|
263
|
+
}
|
|
264
|
+
return "File attached";
|
|
265
|
+
},
|
|
266
|
+
extractionPrompt: "a file attachment (upload required)"
|
|
267
|
+
};
|
|
268
|
+
BUILTIN_TYPES = [
|
|
269
|
+
textType,
|
|
270
|
+
numberType,
|
|
271
|
+
emailType,
|
|
272
|
+
booleanType,
|
|
273
|
+
selectType,
|
|
274
|
+
dateType,
|
|
275
|
+
fileType
|
|
276
|
+
];
|
|
277
|
+
BUILTIN_TYPE_MAP = new Map(BUILTIN_TYPES.map((t) => [t.id, t]));
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// src/validation.ts
|
|
281
|
+
function registerTypeHandler(type, handler) {
|
|
282
|
+
typeHandlers.set(type, handler);
|
|
283
|
+
}
|
|
284
|
+
function getTypeHandler(type) {
|
|
285
|
+
return typeHandlers.get(type);
|
|
286
|
+
}
|
|
287
|
+
function clearTypeHandlers() {
|
|
288
|
+
typeHandlers.clear();
|
|
289
|
+
}
|
|
290
|
+
function validateField(value, control) {
|
|
291
|
+
if (control.required) {
|
|
292
|
+
if (value === undefined || value === null || value === "") {
|
|
293
|
+
return {
|
|
294
|
+
valid: false,
|
|
295
|
+
error: `${control.label || control.key} is required`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (value === undefined || value === null || value === "") {
|
|
300
|
+
return { valid: true };
|
|
301
|
+
}
|
|
302
|
+
const handler = typeHandlers.get(control.type);
|
|
303
|
+
if (handler?.validate) {
|
|
304
|
+
const result = handler.validate(value, control);
|
|
305
|
+
if (!result.valid) {
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
switch (control.type) {
|
|
310
|
+
case "email":
|
|
311
|
+
return validateEmail(value, control);
|
|
312
|
+
case "number":
|
|
313
|
+
return validateNumber(value, control);
|
|
314
|
+
case "boolean":
|
|
315
|
+
return validateBoolean(value, control);
|
|
316
|
+
case "date":
|
|
317
|
+
return validateDate(value, control);
|
|
318
|
+
case "select":
|
|
319
|
+
return validateSelect(value, control);
|
|
320
|
+
case "file":
|
|
321
|
+
return validateFile(value, control);
|
|
322
|
+
default:
|
|
323
|
+
return validateText(value, control);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function validateText(value, control) {
|
|
327
|
+
const strValue = String(value);
|
|
328
|
+
if (control.pattern) {
|
|
329
|
+
const regex = new RegExp(control.pattern);
|
|
330
|
+
if (!regex.test(strValue)) {
|
|
331
|
+
return {
|
|
332
|
+
valid: false,
|
|
333
|
+
error: `${control.label || control.key} has invalid format`
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (control.minLength !== undefined && strValue.length < control.minLength) {
|
|
338
|
+
return {
|
|
339
|
+
valid: false,
|
|
340
|
+
error: `${control.label || control.key} must be at least ${control.minLength} characters`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (control.maxLength !== undefined && strValue.length > control.maxLength) {
|
|
344
|
+
return {
|
|
345
|
+
valid: false,
|
|
346
|
+
error: `${control.label || control.key} must be at most ${control.maxLength} characters`
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (control.enum && control.enum.length > 0) {
|
|
350
|
+
if (!control.enum.includes(strValue)) {
|
|
351
|
+
return {
|
|
352
|
+
valid: false,
|
|
353
|
+
error: `${control.label || control.key} must be one of: ${control.enum.join(", ")}`
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { valid: true };
|
|
358
|
+
}
|
|
359
|
+
function validateEmail(value, control) {
|
|
360
|
+
const strValue = String(value);
|
|
361
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
362
|
+
if (!emailRegex.test(strValue)) {
|
|
363
|
+
return {
|
|
364
|
+
valid: false,
|
|
365
|
+
error: `${control.label || control.key} must be a valid email address`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return validateText(value, control);
|
|
369
|
+
}
|
|
370
|
+
function validateNumber(value, control) {
|
|
371
|
+
const numValue = typeof value === "number" ? value : parseFloat(String(value).replace(/[,$]/g, ""));
|
|
372
|
+
if (Number.isNaN(numValue)) {
|
|
373
|
+
return {
|
|
374
|
+
valid: false,
|
|
375
|
+
error: `${control.label || control.key} must be a number`
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (control.min !== undefined && numValue < control.min) {
|
|
379
|
+
return {
|
|
380
|
+
valid: false,
|
|
381
|
+
error: `${control.label || control.key} must be at least ${control.min}`
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (control.max !== undefined && numValue > control.max) {
|
|
385
|
+
return {
|
|
386
|
+
valid: false,
|
|
387
|
+
error: `${control.label || control.key} must be at most ${control.max}`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return { valid: true };
|
|
391
|
+
}
|
|
392
|
+
function validateBoolean(value, _control) {
|
|
393
|
+
if (typeof value === "boolean") {
|
|
394
|
+
return { valid: true };
|
|
395
|
+
}
|
|
396
|
+
const strValue = String(value).toLowerCase();
|
|
397
|
+
const truthy = ["true", "yes", "1", "on"];
|
|
398
|
+
const falsy = ["false", "no", "0", "off"];
|
|
399
|
+
if (truthy.includes(strValue) || falsy.includes(strValue)) {
|
|
400
|
+
return { valid: true };
|
|
401
|
+
}
|
|
402
|
+
return { valid: false, error: "Must be true or false" };
|
|
403
|
+
}
|
|
404
|
+
function validateDate(value, control) {
|
|
405
|
+
let dateValue;
|
|
406
|
+
if (value instanceof Date) {
|
|
407
|
+
dateValue = value;
|
|
408
|
+
} else if (typeof value === "string" || typeof value === "number") {
|
|
409
|
+
dateValue = new Date(value);
|
|
410
|
+
} else {
|
|
411
|
+
return {
|
|
412
|
+
valid: false,
|
|
413
|
+
error: `${control.label || control.key} must be a valid date`
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (Number.isNaN(dateValue.getTime())) {
|
|
417
|
+
return {
|
|
418
|
+
valid: false,
|
|
419
|
+
error: `${control.label || control.key} must be a valid date`
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (control.min !== undefined && dateValue.getTime() < control.min) {
|
|
423
|
+
return {
|
|
424
|
+
valid: false,
|
|
425
|
+
error: `${control.label || control.key} is too early`
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (control.max !== undefined && dateValue.getTime() > control.max) {
|
|
429
|
+
return {
|
|
430
|
+
valid: false,
|
|
431
|
+
error: `${control.label || control.key} is too late`
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return { valid: true };
|
|
435
|
+
}
|
|
436
|
+
function validateSelect(value, control) {
|
|
437
|
+
if (!control.options || control.options.length === 0) {
|
|
438
|
+
return { valid: true };
|
|
439
|
+
}
|
|
440
|
+
const strValue = String(value);
|
|
441
|
+
const validValues = control.options.map((opt) => opt.value);
|
|
442
|
+
if (!validValues.includes(strValue)) {
|
|
443
|
+
return {
|
|
444
|
+
valid: false,
|
|
445
|
+
error: `${control.label || control.key} must be one of the available options`
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return { valid: true };
|
|
449
|
+
}
|
|
450
|
+
function validateFile(value, control) {
|
|
451
|
+
if (!control.file) {
|
|
452
|
+
return { valid: true };
|
|
453
|
+
}
|
|
454
|
+
const files = Array.isArray(value) ? value : [value];
|
|
455
|
+
if (control.file.maxFiles && files.length > control.file.maxFiles) {
|
|
456
|
+
return {
|
|
457
|
+
valid: false,
|
|
458
|
+
error: `Maximum ${control.file.maxFiles} files allowed`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
for (const file of files) {
|
|
462
|
+
if (!file || typeof file !== "object")
|
|
463
|
+
continue;
|
|
464
|
+
const fileObj = file;
|
|
465
|
+
if (control.file.maxSize && fileObj.size && fileObj.size > control.file.maxSize) {
|
|
466
|
+
return {
|
|
467
|
+
valid: false,
|
|
468
|
+
error: `File size exceeds maximum of ${formatBytes(control.file.maxSize)}`
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (control.file.accept && fileObj.mimeType) {
|
|
472
|
+
const accepted = control.file.accept.some((pattern) => matchesMimeType(fileObj.mimeType, pattern));
|
|
473
|
+
if (!accepted) {
|
|
474
|
+
return {
|
|
475
|
+
valid: false,
|
|
476
|
+
error: `File type ${fileObj.mimeType} is not accepted`
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return { valid: true };
|
|
482
|
+
}
|
|
483
|
+
function matchesMimeType(mimeType, pattern) {
|
|
484
|
+
if (pattern === "*/*")
|
|
485
|
+
return true;
|
|
486
|
+
if (pattern.endsWith("/*")) {
|
|
487
|
+
const prefix = pattern.slice(0, -1);
|
|
488
|
+
return mimeType.startsWith(prefix);
|
|
489
|
+
}
|
|
490
|
+
return mimeType === pattern;
|
|
491
|
+
}
|
|
492
|
+
function formatBytes(bytes) {
|
|
493
|
+
if (bytes < 1024)
|
|
494
|
+
return `${bytes} B`;
|
|
495
|
+
if (bytes < 1024 * 1024)
|
|
496
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
497
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
498
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
499
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
500
|
+
}
|
|
501
|
+
function parseValue(value, control) {
|
|
502
|
+
const handler = typeHandlers.get(control.type);
|
|
503
|
+
if (handler?.parse) {
|
|
504
|
+
return handler.parse(value);
|
|
505
|
+
}
|
|
506
|
+
switch (control.type) {
|
|
507
|
+
case "number":
|
|
508
|
+
return parseFloat(value.replace(/[,$]/g, ""));
|
|
509
|
+
case "boolean": {
|
|
510
|
+
const lower = value.toLowerCase();
|
|
511
|
+
return ["true", "yes", "1", "on"].includes(lower);
|
|
512
|
+
}
|
|
513
|
+
case "date": {
|
|
514
|
+
const timestamp = Date.parse(value);
|
|
515
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : value;
|
|
516
|
+
}
|
|
517
|
+
default:
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function formatValue(value, control) {
|
|
522
|
+
if (value === undefined || value === null)
|
|
523
|
+
return "";
|
|
524
|
+
const handler = typeHandlers.get(control.type);
|
|
525
|
+
if (handler?.format) {
|
|
526
|
+
return handler.format(value);
|
|
527
|
+
}
|
|
528
|
+
if (control.sensitive) {
|
|
529
|
+
const strVal = String(value);
|
|
530
|
+
if (strVal.length > 8) {
|
|
531
|
+
return `${strVal.slice(0, 4)}...${strVal.slice(-4)}`;
|
|
532
|
+
}
|
|
533
|
+
return "****";
|
|
534
|
+
}
|
|
535
|
+
switch (control.type) {
|
|
536
|
+
case "number":
|
|
537
|
+
return typeof value === "number" ? value.toLocaleString() : String(value);
|
|
538
|
+
case "boolean":
|
|
539
|
+
return value ? "Yes" : "No";
|
|
540
|
+
case "date":
|
|
541
|
+
return value instanceof Date ? value.toLocaleDateString() : String(value);
|
|
542
|
+
case "select":
|
|
543
|
+
if (control.options) {
|
|
544
|
+
const option = control.options.find((opt) => opt.value === String(value));
|
|
545
|
+
if (option)
|
|
546
|
+
return option.label;
|
|
547
|
+
}
|
|
548
|
+
return String(value);
|
|
549
|
+
case "file":
|
|
550
|
+
if (Array.isArray(value)) {
|
|
551
|
+
return value.map((f) => f.name || "file").join(", ");
|
|
552
|
+
}
|
|
553
|
+
return value.name || "file";
|
|
554
|
+
default:
|
|
555
|
+
return String(value);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
var typeHandlers;
|
|
559
|
+
var init_validation = __esm(() => {
|
|
560
|
+
typeHandlers = new Map;
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// src/intent.ts
|
|
564
|
+
function quickIntentDetect(text) {
|
|
565
|
+
const lower = text.toLowerCase().trim();
|
|
566
|
+
if (lower.length < 2) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
if (/\b(resume|continue|pick up where|go back to|get back to)\b/.test(lower)) {
|
|
570
|
+
return "restore";
|
|
571
|
+
}
|
|
572
|
+
if (/\b(submit|done|finish|send it|that'?s all|i'?m done|complete|all set)\b/.test(lower)) {
|
|
573
|
+
return "submit";
|
|
574
|
+
}
|
|
575
|
+
if (/\b(save|stash|later|hold on|pause|save for later|come back|save this)\b/.test(lower)) {
|
|
576
|
+
if (!/\b(save and submit|save and send)\b/.test(lower)) {
|
|
577
|
+
return "stash";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (/\b(cancel|abort|nevermind|never mind|forget it|stop|quit|exit)\b/.test(lower)) {
|
|
581
|
+
return "cancel";
|
|
582
|
+
}
|
|
583
|
+
if (/\b(undo|go back|wait no|change that|oops|that'?s wrong|wrong|not right)\b/.test(lower)) {
|
|
584
|
+
return "undo";
|
|
585
|
+
}
|
|
586
|
+
if (/\b(skip|pass|don'?t know|next one|next|don'?t have|no idea)\b/.test(lower)) {
|
|
587
|
+
if (!/\bskip to\b/.test(lower)) {
|
|
588
|
+
return "skip";
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (/\b(why|what'?s that for|explain|what do you mean|what is|purpose|reason)\b\??$/i.test(lower)) {
|
|
592
|
+
return "explain";
|
|
593
|
+
}
|
|
594
|
+
if (/^why\??$/i.test(lower)) {
|
|
595
|
+
return "explain";
|
|
596
|
+
}
|
|
597
|
+
if (/\b(example|like what|show me|such as|for instance|sample)\b\??$/i.test(lower)) {
|
|
598
|
+
return "example";
|
|
599
|
+
}
|
|
600
|
+
if (/^(example|e\.?g\.?)\??$/i.test(lower)) {
|
|
601
|
+
return "example";
|
|
602
|
+
}
|
|
603
|
+
if (/\b(how far|how many left|progress|status|how much more|where are we)\b/.test(lower)) {
|
|
604
|
+
return "progress";
|
|
605
|
+
}
|
|
606
|
+
if (/\b(same as|last time|use my usual|like before|previous|from before)\b/.test(lower)) {
|
|
607
|
+
return "autofill";
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
function isLifecycleIntent(intent) {
|
|
612
|
+
return ["submit", "stash", "restore", "cancel"].includes(intent);
|
|
613
|
+
}
|
|
614
|
+
function isUXIntent(intent) {
|
|
615
|
+
return ["undo", "skip", "explain", "example", "progress", "autofill"].includes(intent);
|
|
616
|
+
}
|
|
617
|
+
function hasDataToExtract(intent) {
|
|
618
|
+
return intent === "fill_form" || intent === "other";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// node_modules/uuid/dist-node/stringify.js
|
|
622
|
+
function unsafeStringify(arr, offset = 0) {
|
|
623
|
+
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
624
|
+
}
|
|
625
|
+
var byteToHex;
|
|
626
|
+
var init_stringify = __esm(() => {
|
|
627
|
+
byteToHex = [];
|
|
628
|
+
for (let i = 0;i < 256; ++i) {
|
|
629
|
+
byteToHex.push((i + 256).toString(16).slice(1));
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// node_modules/uuid/dist-node/rng.js
|
|
634
|
+
import { randomFillSync } from "node:crypto";
|
|
635
|
+
function rng() {
|
|
636
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
637
|
+
randomFillSync(rnds8Pool);
|
|
638
|
+
poolPtr = 0;
|
|
639
|
+
}
|
|
640
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
641
|
+
}
|
|
642
|
+
var rnds8Pool, poolPtr;
|
|
643
|
+
var init_rng = __esm(() => {
|
|
644
|
+
rnds8Pool = new Uint8Array(256);
|
|
645
|
+
poolPtr = rnds8Pool.length;
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// node_modules/uuid/dist-node/native.js
|
|
649
|
+
import { randomUUID } from "node:crypto";
|
|
650
|
+
var native_default;
|
|
651
|
+
var init_native = __esm(() => {
|
|
652
|
+
native_default = { randomUUID };
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// node_modules/uuid/dist-node/v4.js
|
|
656
|
+
function _v4(options, buf, offset) {
|
|
657
|
+
options = options || {};
|
|
658
|
+
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
659
|
+
if (rnds.length < 16) {
|
|
660
|
+
throw new Error("Random bytes length must be >= 16");
|
|
661
|
+
}
|
|
662
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
663
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
664
|
+
if (buf) {
|
|
665
|
+
offset = offset || 0;
|
|
666
|
+
if (offset < 0 || offset + 16 > buf.length) {
|
|
667
|
+
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
668
|
+
}
|
|
669
|
+
for (let i = 0;i < 16; ++i) {
|
|
670
|
+
buf[offset + i] = rnds[i];
|
|
671
|
+
}
|
|
672
|
+
return buf;
|
|
673
|
+
}
|
|
674
|
+
return unsafeStringify(rnds);
|
|
675
|
+
}
|
|
676
|
+
function v4(options, buf, offset) {
|
|
677
|
+
if (native_default.randomUUID && !buf && !options) {
|
|
678
|
+
return native_default.randomUUID();
|
|
679
|
+
}
|
|
680
|
+
return _v4(options, buf, offset);
|
|
681
|
+
}
|
|
682
|
+
var v4_default;
|
|
683
|
+
var init_v4 = __esm(() => {
|
|
684
|
+
init_native();
|
|
685
|
+
init_rng();
|
|
686
|
+
init_stringify();
|
|
687
|
+
v4_default = v4;
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// node_modules/uuid/dist-node/index.js
|
|
691
|
+
var init_dist_node = __esm(() => {
|
|
692
|
+
init_v4();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// src/storage.ts
|
|
696
|
+
async function getActiveSession(runtime, entityId, roomId) {
|
|
697
|
+
const component = await runtime.getComponent(entityId, `${FORM_SESSION_COMPONENT}:${roomId}`);
|
|
698
|
+
if (!component?.data || !isFormSession(component.data))
|
|
699
|
+
return null;
|
|
700
|
+
const session = component.data;
|
|
701
|
+
if (session.status === "active" || session.status === "ready") {
|
|
702
|
+
return session;
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
async function getAllActiveSessions(runtime, entityId) {
|
|
707
|
+
const components = await runtime.getComponents(entityId);
|
|
708
|
+
const sessions = [];
|
|
709
|
+
for (const component of components) {
|
|
710
|
+
if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
|
|
711
|
+
if (component.data && isFormSession(component.data)) {
|
|
712
|
+
const session = component.data;
|
|
713
|
+
if (session.status === "active" || session.status === "ready") {
|
|
714
|
+
sessions.push(session);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return sessions;
|
|
720
|
+
}
|
|
721
|
+
async function getStashedSessions(runtime, entityId) {
|
|
722
|
+
const components = await runtime.getComponents(entityId);
|
|
723
|
+
const sessions = [];
|
|
724
|
+
for (const component of components) {
|
|
725
|
+
if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
|
|
726
|
+
if (component.data && isFormSession(component.data)) {
|
|
727
|
+
const session = component.data;
|
|
728
|
+
if (session.status === "stashed") {
|
|
729
|
+
sessions.push(session);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return sessions;
|
|
735
|
+
}
|
|
736
|
+
async function getSessionById(runtime, entityId, sessionId) {
|
|
737
|
+
const components = await runtime.getComponents(entityId);
|
|
738
|
+
for (const component of components) {
|
|
739
|
+
if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
|
|
740
|
+
if (component.data && isFormSession(component.data)) {
|
|
741
|
+
const session = component.data;
|
|
742
|
+
if (session.id === sessionId) {
|
|
743
|
+
return session;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
async function saveSession(runtime, session) {
|
|
751
|
+
const componentType = `${FORM_SESSION_COMPONENT}:${session.roomId}`;
|
|
752
|
+
const existing = await runtime.getComponent(session.entityId, componentType);
|
|
753
|
+
const context = await resolveComponentContext(runtime, session.roomId);
|
|
754
|
+
const resolvedWorldId = existing?.worldId ?? context.worldId;
|
|
755
|
+
const component = {
|
|
756
|
+
id: existing?.id || v4_default(),
|
|
757
|
+
entityId: session.entityId,
|
|
758
|
+
agentId: runtime.agentId,
|
|
759
|
+
roomId: session.roomId,
|
|
760
|
+
worldId: resolvedWorldId,
|
|
761
|
+
sourceEntityId: runtime.agentId,
|
|
762
|
+
type: componentType,
|
|
763
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
764
|
+
data: JSON.parse(JSON.stringify(session))
|
|
765
|
+
};
|
|
766
|
+
if (existing) {
|
|
767
|
+
await runtime.updateComponent(component);
|
|
768
|
+
} else {
|
|
769
|
+
await runtime.createComponent(component);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function deleteSession(runtime, session) {
|
|
773
|
+
const componentType = `${FORM_SESSION_COMPONENT}:${session.roomId}`;
|
|
774
|
+
const existing = await runtime.getComponent(session.entityId, componentType);
|
|
775
|
+
if (existing) {
|
|
776
|
+
await runtime.deleteComponent(existing.id);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async function saveSubmission(runtime, submission) {
|
|
780
|
+
const componentType = `${FORM_SUBMISSION_COMPONENT}:${submission.formId}:${submission.id}`;
|
|
781
|
+
const context = await resolveComponentContext(runtime);
|
|
782
|
+
const component = {
|
|
783
|
+
id: v4_default(),
|
|
784
|
+
entityId: submission.entityId,
|
|
785
|
+
agentId: runtime.agentId,
|
|
786
|
+
roomId: context.roomId,
|
|
787
|
+
worldId: context.worldId,
|
|
788
|
+
sourceEntityId: runtime.agentId,
|
|
789
|
+
type: componentType,
|
|
790
|
+
createdAt: submission.submittedAt,
|
|
791
|
+
data: JSON.parse(JSON.stringify(submission))
|
|
792
|
+
};
|
|
793
|
+
await runtime.createComponent(component);
|
|
794
|
+
}
|
|
795
|
+
async function getSubmissions(runtime, entityId, formId) {
|
|
796
|
+
const components = await runtime.getComponents(entityId);
|
|
797
|
+
const submissions = [];
|
|
798
|
+
const prefix = formId ? `${FORM_SUBMISSION_COMPONENT}:${formId}:` : `${FORM_SUBMISSION_COMPONENT}:`;
|
|
799
|
+
for (const component of components) {
|
|
800
|
+
if (component.type.startsWith(prefix)) {
|
|
801
|
+
if (component.data && isFormSubmission(component.data)) {
|
|
802
|
+
submissions.push(component.data);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
submissions.sort((a, b) => b.submittedAt - a.submittedAt);
|
|
807
|
+
return submissions;
|
|
808
|
+
}
|
|
809
|
+
async function getAutofillData(runtime, entityId, formId) {
|
|
810
|
+
const componentType = `${FORM_AUTOFILL_COMPONENT}:${formId}`;
|
|
811
|
+
const component = await runtime.getComponent(entityId, componentType);
|
|
812
|
+
if (!component?.data || !isFormAutofillData(component.data))
|
|
813
|
+
return null;
|
|
814
|
+
return component.data;
|
|
815
|
+
}
|
|
816
|
+
async function saveAutofillData(runtime, entityId, formId, values) {
|
|
817
|
+
const componentType = `${FORM_AUTOFILL_COMPONENT}:${formId}`;
|
|
818
|
+
const existing = await runtime.getComponent(entityId, componentType);
|
|
819
|
+
const context = await resolveComponentContext(runtime);
|
|
820
|
+
const resolvedWorldId = existing?.worldId ?? context.worldId;
|
|
821
|
+
const data = {
|
|
822
|
+
formId,
|
|
823
|
+
values,
|
|
824
|
+
updatedAt: Date.now()
|
|
825
|
+
};
|
|
826
|
+
const component = {
|
|
827
|
+
id: existing?.id || v4_default(),
|
|
828
|
+
entityId,
|
|
829
|
+
agentId: runtime.agentId,
|
|
830
|
+
roomId: context.roomId,
|
|
831
|
+
worldId: resolvedWorldId,
|
|
832
|
+
sourceEntityId: runtime.agentId,
|
|
833
|
+
type: componentType,
|
|
834
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
835
|
+
data: JSON.parse(JSON.stringify(data))
|
|
836
|
+
};
|
|
837
|
+
if (existing) {
|
|
838
|
+
await runtime.updateComponent(component);
|
|
839
|
+
} else {
|
|
840
|
+
await runtime.createComponent(component);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value), resolveComponentContext = async (runtime, roomId) => {
|
|
844
|
+
if (roomId) {
|
|
845
|
+
const room = await runtime.getRoom(roomId);
|
|
846
|
+
return { roomId, worldId: room?.worldId ?? runtime.agentId };
|
|
847
|
+
}
|
|
848
|
+
return { roomId: runtime.agentId, worldId: runtime.agentId };
|
|
849
|
+
}, isFormSession = (data) => {
|
|
850
|
+
if (!isRecord(data))
|
|
851
|
+
return false;
|
|
852
|
+
return typeof data.id === "string" && typeof data.formId === "string" && typeof data.entityId === "string" && typeof data.roomId === "string";
|
|
853
|
+
}, isFormSubmission = (data) => {
|
|
854
|
+
if (!isRecord(data))
|
|
855
|
+
return false;
|
|
856
|
+
return typeof data.id === "string" && typeof data.formId === "string" && typeof data.sessionId === "string" && typeof data.entityId === "string";
|
|
857
|
+
}, isFormAutofillData = (data) => {
|
|
858
|
+
if (!isRecord(data))
|
|
859
|
+
return false;
|
|
860
|
+
return typeof data.formId === "string" && typeof data.updatedAt === "number" && typeof data.values === "object";
|
|
861
|
+
};
|
|
862
|
+
var init_storage = __esm(() => {
|
|
863
|
+
init_dist_node();
|
|
864
|
+
init_types();
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// src/template.ts
|
|
868
|
+
function buildTemplateValues(session) {
|
|
869
|
+
const values = {};
|
|
870
|
+
for (const [key, state] of Object.entries(session.fields)) {
|
|
871
|
+
const value = state.value;
|
|
872
|
+
if (typeof value === "string") {
|
|
873
|
+
values[key] = value;
|
|
874
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
875
|
+
values[key] = String(value);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const context = session.context;
|
|
879
|
+
if (context && typeof context === "object" && !Array.isArray(context)) {
|
|
880
|
+
for (const [key, value] of Object.entries(context)) {
|
|
881
|
+
if (typeof value === "string") {
|
|
882
|
+
values[key] = value;
|
|
883
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
884
|
+
values[key] = String(value);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return values;
|
|
889
|
+
}
|
|
890
|
+
function renderTemplate(template, values) {
|
|
891
|
+
if (!template) {
|
|
892
|
+
return template;
|
|
893
|
+
}
|
|
894
|
+
return template.replace(TEMPLATE_PATTERN, (match, key) => {
|
|
895
|
+
const replacement = values[key];
|
|
896
|
+
return replacement !== undefined ? replacement : match;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
function resolveControlTemplates(control, values) {
|
|
900
|
+
const resolvedOptions = control.options?.map((option) => ({
|
|
901
|
+
...option,
|
|
902
|
+
label: renderTemplate(option.label, values) ?? option.label,
|
|
903
|
+
description: renderTemplate(option.description, values)
|
|
904
|
+
}));
|
|
905
|
+
const resolvedFields = control.fields?.map((field) => resolveControlTemplates(field, values));
|
|
906
|
+
return {
|
|
907
|
+
...control,
|
|
908
|
+
label: renderTemplate(control.label, values) ?? control.label,
|
|
909
|
+
description: renderTemplate(control.description, values),
|
|
910
|
+
askPrompt: renderTemplate(control.askPrompt, values),
|
|
911
|
+
example: renderTemplate(control.example, values),
|
|
912
|
+
extractHints: control.extractHints?.map((hint) => renderTemplate(hint, values) ?? hint),
|
|
913
|
+
options: resolvedOptions,
|
|
914
|
+
fields: resolvedFields ?? control.fields
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
var TEMPLATE_PATTERN;
|
|
918
|
+
var init_template = __esm(() => {
|
|
919
|
+
TEMPLATE_PATTERN = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// src/extraction.ts
|
|
923
|
+
import { ModelType, parseKeyValueXml } from "@elizaos/core";
|
|
924
|
+
async function llmIntentAndExtract(runtime, text, form, controls, templateValues) {
|
|
925
|
+
const resolvedControls = templateValues ? controls.map((control) => resolveControlTemplates(control, templateValues)) : controls;
|
|
926
|
+
const fieldsDescription = resolvedControls.filter((c) => !c.hidden).map((c) => {
|
|
927
|
+
const handler = getTypeHandler(c.type);
|
|
928
|
+
const typeHint = handler?.extractionPrompt || c.type;
|
|
929
|
+
const hints = c.extractHints?.join(", ") || "";
|
|
930
|
+
const options = c.options?.map((o) => o.value).join(", ") || "";
|
|
931
|
+
return `- ${c.key} (${c.label}): ${c.description || typeHint}${hints ? ` [hints: ${hints}]` : ""}${options ? ` [options: ${options}]` : ""}`;
|
|
932
|
+
}).join(`
|
|
933
|
+
`);
|
|
934
|
+
const prompt = `You are extracting structured data from a user's natural language message.
|
|
935
|
+
|
|
936
|
+
FORM: ${form.name}
|
|
937
|
+
${form.description ? `DESCRIPTION: ${form.description}` : ""}
|
|
938
|
+
|
|
939
|
+
FIELDS TO EXTRACT:
|
|
940
|
+
${fieldsDescription}
|
|
941
|
+
|
|
942
|
+
USER MESSAGE:
|
|
943
|
+
"${text}"
|
|
944
|
+
|
|
945
|
+
INSTRUCTIONS:
|
|
946
|
+
1. Determine the user's intent:
|
|
947
|
+
- fill_form: They are providing information for form fields
|
|
948
|
+
- submit: They want to submit/complete the form ("done", "submit", "finish", "that's all")
|
|
949
|
+
- stash: They want to save for later ("save for later", "pause", "hold on")
|
|
950
|
+
- restore: They want to resume a saved form ("resume", "continue", "pick up where")
|
|
951
|
+
- cancel: They want to cancel ("cancel", "abort", "nevermind", "forget it")
|
|
952
|
+
- undo: They want to undo last change ("undo", "go back", "wait no")
|
|
953
|
+
- skip: They want to skip current field ("skip", "pass", "don't know")
|
|
954
|
+
- explain: They want explanation ("why?", "what's that for?")
|
|
955
|
+
- example: They want an example ("example?", "like what?")
|
|
956
|
+
- progress: They want progress update ("how far?", "status")
|
|
957
|
+
- autofill: They want to use saved values ("same as last time")
|
|
958
|
+
- other: None of the above
|
|
959
|
+
|
|
960
|
+
2. For fill_form intent, extract all field values mentioned.
|
|
961
|
+
- For each extracted value, provide a confidence score (0.0-1.0)
|
|
962
|
+
- Note if this appears to be a correction to a previous value
|
|
963
|
+
|
|
964
|
+
Respond in this exact XML format:
|
|
965
|
+
<response>
|
|
966
|
+
<intent>fill_form|submit|stash|restore|cancel|undo|skip|explain|example|progress|autofill|other</intent>
|
|
967
|
+
<extractions>
|
|
968
|
+
<field>
|
|
969
|
+
<key>field_key</key>
|
|
970
|
+
<value>extracted_value</value>
|
|
971
|
+
<confidence>0.0-1.0</confidence>
|
|
972
|
+
<reasoning>why this value was extracted</reasoning>
|
|
973
|
+
<is_correction>true|false</is_correction>
|
|
974
|
+
</field>
|
|
975
|
+
<!-- more fields if applicable -->
|
|
976
|
+
</extractions>
|
|
977
|
+
</response>`;
|
|
978
|
+
try {
|
|
979
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
980
|
+
prompt,
|
|
981
|
+
temperature: 0.1
|
|
982
|
+
});
|
|
983
|
+
const parsed = parseExtractionResponse(response);
|
|
984
|
+
for (const extraction of parsed.extractions) {
|
|
985
|
+
const control = resolvedControls.find((c) => c.key === extraction.field);
|
|
986
|
+
if (control) {
|
|
987
|
+
if (typeof extraction.value === "string") {
|
|
988
|
+
extraction.value = parseValue(extraction.value, control);
|
|
989
|
+
}
|
|
990
|
+
const validation = validateField(extraction.value, control);
|
|
991
|
+
if (!validation.valid) {
|
|
992
|
+
extraction.confidence = Math.min(extraction.confidence, 0.3);
|
|
993
|
+
extraction.reasoning = `${extraction.reasoning || ""} (Validation failed: ${validation.error})`;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (form.debug) {
|
|
998
|
+
runtime.logger.debug("[FormExtraction] LLM extraction result:", JSON.stringify(parsed));
|
|
999
|
+
}
|
|
1000
|
+
return parsed;
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
runtime.logger.error("[FormExtraction] LLM extraction failed:", String(error));
|
|
1003
|
+
return { intent: "other", extractions: [] };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function parseExtractionResponse(response) {
|
|
1007
|
+
const result = {
|
|
1008
|
+
intent: "other",
|
|
1009
|
+
extractions: []
|
|
1010
|
+
};
|
|
1011
|
+
try {
|
|
1012
|
+
const parsed = parseKeyValueXml(response);
|
|
1013
|
+
if (parsed) {
|
|
1014
|
+
const intentStr = parsed.intent?.toLowerCase() ?? "other";
|
|
1015
|
+
result.intent = isValidIntent(intentStr) ? intentStr : "other";
|
|
1016
|
+
if (parsed.extractions) {
|
|
1017
|
+
const fields = Array.isArray(parsed.extractions) ? parsed.extractions : parsed.extractions.field ? Array.isArray(parsed.extractions.field) ? parsed.extractions.field : [parsed.extractions.field] : [];
|
|
1018
|
+
for (const field of fields) {
|
|
1019
|
+
if (field?.key) {
|
|
1020
|
+
const extraction = {
|
|
1021
|
+
field: String(field.key),
|
|
1022
|
+
value: field.value ?? null,
|
|
1023
|
+
confidence: parseFloat(String(field.confidence ?? "")) || 0.5,
|
|
1024
|
+
reasoning: field.reasoning ? String(field.reasoning) : undefined,
|
|
1025
|
+
isCorrection: field.is_correction === "true" || field.is_correction === true
|
|
1026
|
+
};
|
|
1027
|
+
result.extractions.push(extraction);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} catch (_error) {
|
|
1033
|
+
const intentMatch = response.match(/<intent>([^<]+)<\/intent>/);
|
|
1034
|
+
if (intentMatch) {
|
|
1035
|
+
const intentStr = intentMatch[1].toLowerCase().trim();
|
|
1036
|
+
result.intent = isValidIntent(intentStr) ? intentStr : "other";
|
|
1037
|
+
}
|
|
1038
|
+
const fieldMatches = response.matchAll(/<field>\s*<key>([^<]+)<\/key>\s*<value>([^<]*)<\/value>\s*<confidence>([^<]+)<\/confidence>/g);
|
|
1039
|
+
for (const match of fieldMatches) {
|
|
1040
|
+
result.extractions.push({
|
|
1041
|
+
field: match[1].trim(),
|
|
1042
|
+
value: match[2].trim(),
|
|
1043
|
+
confidence: parseFloat(match[3]) || 0.5
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return result;
|
|
1048
|
+
}
|
|
1049
|
+
function isValidIntent(str) {
|
|
1050
|
+
const validIntents = [
|
|
1051
|
+
"fill_form",
|
|
1052
|
+
"submit",
|
|
1053
|
+
"stash",
|
|
1054
|
+
"restore",
|
|
1055
|
+
"cancel",
|
|
1056
|
+
"undo",
|
|
1057
|
+
"skip",
|
|
1058
|
+
"explain",
|
|
1059
|
+
"example",
|
|
1060
|
+
"progress",
|
|
1061
|
+
"autofill",
|
|
1062
|
+
"other"
|
|
1063
|
+
];
|
|
1064
|
+
return validIntents.includes(str);
|
|
1065
|
+
}
|
|
1066
|
+
async function extractSingleField(runtime, text, control, debug, templateValues) {
|
|
1067
|
+
const resolvedControl = templateValues ? resolveControlTemplates(control, templateValues) : control;
|
|
1068
|
+
const handler = getTypeHandler(resolvedControl.type);
|
|
1069
|
+
const typeHint = handler?.extractionPrompt || resolvedControl.type;
|
|
1070
|
+
const prompt = `Extract the ${resolvedControl.label} (${typeHint}) from this message:
|
|
1071
|
+
|
|
1072
|
+
"${text}"
|
|
1073
|
+
|
|
1074
|
+
${resolvedControl.description ? `Context: ${resolvedControl.description}` : ""}
|
|
1075
|
+
${resolvedControl.extractHints?.length ? `Look for: ${resolvedControl.extractHints.join(", ")}` : ""}
|
|
1076
|
+
${resolvedControl.options?.length ? `Valid options: ${resolvedControl.options.map((o) => o.value).join(", ")}` : ""}
|
|
1077
|
+
${resolvedControl.example ? `Example: ${resolvedControl.example}` : ""}
|
|
1078
|
+
|
|
1079
|
+
Respond in XML:
|
|
1080
|
+
<response>
|
|
1081
|
+
<found>true|false</found>
|
|
1082
|
+
<value>extracted_value or empty if not found</value>
|
|
1083
|
+
<confidence>0.0-1.0</confidence>
|
|
1084
|
+
<reasoning>brief explanation</reasoning>
|
|
1085
|
+
</response>`;
|
|
1086
|
+
try {
|
|
1087
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
1088
|
+
prompt,
|
|
1089
|
+
temperature: 0.1
|
|
1090
|
+
});
|
|
1091
|
+
const parsed = parseKeyValueXml(response);
|
|
1092
|
+
const found = parsed?.found === true || parsed?.found === "true";
|
|
1093
|
+
if (found) {
|
|
1094
|
+
let value = parsed.value;
|
|
1095
|
+
if (typeof value === "string") {
|
|
1096
|
+
value = parseValue(value, resolvedControl);
|
|
1097
|
+
}
|
|
1098
|
+
const confidence = typeof parsed?.confidence === "number" ? parsed.confidence : parseFloat(String(parsed?.confidence ?? ""));
|
|
1099
|
+
const result = {
|
|
1100
|
+
field: resolvedControl.key,
|
|
1101
|
+
value: value ?? null,
|
|
1102
|
+
confidence: Number.isFinite(confidence) ? confidence : 0.5,
|
|
1103
|
+
reasoning: parsed.reasoning ? String(parsed.reasoning) : undefined
|
|
1104
|
+
};
|
|
1105
|
+
if (debug) {
|
|
1106
|
+
runtime.logger.debug("[FormExtraction] Single field extraction:", JSON.stringify(result));
|
|
1107
|
+
}
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|
|
1110
|
+
return null;
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
runtime.logger.error("[FormExtraction] Single field extraction failed:", String(error));
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function detectCorrection(runtime, text, currentValues, controls, templateValues) {
|
|
1117
|
+
const resolvedControls = templateValues ? controls.map((control) => resolveControlTemplates(control, templateValues)) : controls;
|
|
1118
|
+
const currentValuesStr = resolvedControls.filter((c) => currentValues[c.key] !== undefined).map((c) => `- ${c.label}: ${currentValues[c.key]}`).join(`
|
|
1119
|
+
`);
|
|
1120
|
+
if (!currentValuesStr) {
|
|
1121
|
+
return [];
|
|
1122
|
+
}
|
|
1123
|
+
const prompt = `Is the user correcting any of these previously provided values?
|
|
1124
|
+
|
|
1125
|
+
Current values:
|
|
1126
|
+
${currentValuesStr}
|
|
1127
|
+
|
|
1128
|
+
User message:
|
|
1129
|
+
"${text}"
|
|
1130
|
+
|
|
1131
|
+
If they are correcting a value, extract the new value. Otherwise respond with no corrections.
|
|
1132
|
+
|
|
1133
|
+
Respond in XML:
|
|
1134
|
+
<response>
|
|
1135
|
+
<has_correction>true|false</has_correction>
|
|
1136
|
+
<corrections>
|
|
1137
|
+
<correction>
|
|
1138
|
+
<field>field_label</field>
|
|
1139
|
+
<old_value>previous value</old_value>
|
|
1140
|
+
<new_value>corrected value</new_value>
|
|
1141
|
+
<confidence>0.0-1.0</confidence>
|
|
1142
|
+
</correction>
|
|
1143
|
+
</corrections>
|
|
1144
|
+
</response>`;
|
|
1145
|
+
try {
|
|
1146
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
1147
|
+
prompt,
|
|
1148
|
+
temperature: 0.1
|
|
1149
|
+
});
|
|
1150
|
+
const parsed = parseKeyValueXml(response);
|
|
1151
|
+
const hasCorrection = parsed?.has_correction === true || parsed?.has_correction === "true";
|
|
1152
|
+
if (parsed && hasCorrection && parsed.corrections) {
|
|
1153
|
+
const corrections = [];
|
|
1154
|
+
const correctionList = Array.isArray(parsed.corrections) ? parsed.corrections : parsed.corrections.correction ? Array.isArray(parsed.corrections.correction) ? parsed.corrections.correction : [parsed.corrections.correction] : [];
|
|
1155
|
+
for (const correction of correctionList) {
|
|
1156
|
+
const fieldName = correction.field ? String(correction.field) : "";
|
|
1157
|
+
const control = resolvedControls.find((c) => c.label.toLowerCase() === fieldName.toLowerCase() || c.key.toLowerCase() === fieldName.toLowerCase());
|
|
1158
|
+
if (control) {
|
|
1159
|
+
let value = correction.new_value;
|
|
1160
|
+
if (typeof value === "string") {
|
|
1161
|
+
value = parseValue(value, control);
|
|
1162
|
+
}
|
|
1163
|
+
const confidence = typeof correction.confidence === "number" ? correction.confidence : parseFloat(String(correction.confidence ?? ""));
|
|
1164
|
+
const extraction = {
|
|
1165
|
+
field: control.key,
|
|
1166
|
+
value: value ?? null,
|
|
1167
|
+
confidence: Number.isFinite(confidence) ? confidence : 0.8,
|
|
1168
|
+
isCorrection: true
|
|
1169
|
+
};
|
|
1170
|
+
corrections.push(extraction);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return corrections;
|
|
1174
|
+
}
|
|
1175
|
+
return [];
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
runtime.logger.error("[FormExtraction] Correction detection failed:", String(error));
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
var init_extraction = __esm(() => {
|
|
1182
|
+
init_template();
|
|
1183
|
+
init_validation();
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
// src/service.ts
|
|
1187
|
+
var exports_service = {};
|
|
1188
|
+
__export(exports_service, {
|
|
1189
|
+
FormService: () => FormService
|
|
1190
|
+
});
|
|
1191
|
+
import {
|
|
1192
|
+
logger,
|
|
1193
|
+
Service
|
|
1194
|
+
} from "@elizaos/core";
|
|
1195
|
+
function prettify3(key) {
|
|
1196
|
+
return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1197
|
+
}
|
|
1198
|
+
var FormService;
|
|
1199
|
+
var init_service = __esm(() => {
|
|
1200
|
+
init_dist_node();
|
|
1201
|
+
init_builtins();
|
|
1202
|
+
init_storage();
|
|
1203
|
+
init_types();
|
|
1204
|
+
init_validation();
|
|
1205
|
+
FormService = class FormService extends Service {
|
|
1206
|
+
static serviceType = "FORM";
|
|
1207
|
+
capabilityDescription = "Manages conversational forms for data collection";
|
|
1208
|
+
forms = new Map;
|
|
1209
|
+
controlTypes = new Map;
|
|
1210
|
+
static async start(runtime) {
|
|
1211
|
+
const service = new FormService(runtime);
|
|
1212
|
+
registerBuiltinTypes((type, options) => service.registerControlType(type, options));
|
|
1213
|
+
logger.info("[FormService] Started with built-in types");
|
|
1214
|
+
return service;
|
|
1215
|
+
}
|
|
1216
|
+
async stop() {
|
|
1217
|
+
logger.info("[FormService] Stopped");
|
|
1218
|
+
}
|
|
1219
|
+
registerForm(definition) {
|
|
1220
|
+
const form = {
|
|
1221
|
+
...definition,
|
|
1222
|
+
version: definition.version ?? FORM_DEFINITION_DEFAULTS.version,
|
|
1223
|
+
status: definition.status ?? FORM_DEFINITION_DEFAULTS.status,
|
|
1224
|
+
ux: { ...FORM_DEFINITION_DEFAULTS.ux, ...definition.ux },
|
|
1225
|
+
ttl: { ...FORM_DEFINITION_DEFAULTS.ttl, ...definition.ttl },
|
|
1226
|
+
nudge: { ...FORM_DEFINITION_DEFAULTS.nudge, ...definition.nudge },
|
|
1227
|
+
debug: definition.debug ?? FORM_DEFINITION_DEFAULTS.debug,
|
|
1228
|
+
controls: definition.controls.map((control) => ({
|
|
1229
|
+
...control,
|
|
1230
|
+
type: control.type || FORM_CONTROL_DEFAULTS.type,
|
|
1231
|
+
required: control.required ?? FORM_CONTROL_DEFAULTS.required,
|
|
1232
|
+
confirmThreshold: control.confirmThreshold ?? FORM_CONTROL_DEFAULTS.confirmThreshold,
|
|
1233
|
+
label: control.label || prettify3(control.key)
|
|
1234
|
+
}))
|
|
1235
|
+
};
|
|
1236
|
+
this.forms.set(form.id, form);
|
|
1237
|
+
logger.debug(`[FormService] Registered form: ${form.id}`);
|
|
1238
|
+
}
|
|
1239
|
+
getForm(formId) {
|
|
1240
|
+
return this.forms.get(formId);
|
|
1241
|
+
}
|
|
1242
|
+
listForms() {
|
|
1243
|
+
return Array.from(this.forms.values());
|
|
1244
|
+
}
|
|
1245
|
+
registerType(type, handler) {
|
|
1246
|
+
registerTypeHandler(type, handler);
|
|
1247
|
+
logger.debug(`[FormService] Registered type handler: ${type}`);
|
|
1248
|
+
}
|
|
1249
|
+
getTypeHandler(type) {
|
|
1250
|
+
return getTypeHandler(type);
|
|
1251
|
+
}
|
|
1252
|
+
registerControlType(type, options) {
|
|
1253
|
+
const existing = this.controlTypes.get(type.id);
|
|
1254
|
+
if (existing) {
|
|
1255
|
+
if (existing.builtin && !options?.allowOverride) {
|
|
1256
|
+
logger.warn(`[FormService] Cannot override builtin type '${type.id}' without allowOverride: true`);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
logger.warn(`[FormService] Overriding control type: ${type.id}`);
|
|
1260
|
+
}
|
|
1261
|
+
this.controlTypes.set(type.id, type);
|
|
1262
|
+
logger.debug(`[FormService] Registered control type: ${type.id}`);
|
|
1263
|
+
}
|
|
1264
|
+
getControlType(typeId) {
|
|
1265
|
+
return this.controlTypes.get(typeId);
|
|
1266
|
+
}
|
|
1267
|
+
listControlTypes() {
|
|
1268
|
+
return Array.from(this.controlTypes.values());
|
|
1269
|
+
}
|
|
1270
|
+
isCompositeType(typeId) {
|
|
1271
|
+
const type = this.controlTypes.get(typeId);
|
|
1272
|
+
return !!type?.getSubControls;
|
|
1273
|
+
}
|
|
1274
|
+
isExternalType(typeId) {
|
|
1275
|
+
const type = this.controlTypes.get(typeId);
|
|
1276
|
+
return !!type?.activate;
|
|
1277
|
+
}
|
|
1278
|
+
getSubControls(control) {
|
|
1279
|
+
const type = this.controlTypes.get(control.type);
|
|
1280
|
+
if (!type?.getSubControls) {
|
|
1281
|
+
return [];
|
|
1282
|
+
}
|
|
1283
|
+
return type.getSubControls(control, this.runtime);
|
|
1284
|
+
}
|
|
1285
|
+
async startSession(formId, entityId, roomId, options) {
|
|
1286
|
+
const form = this.getForm(formId);
|
|
1287
|
+
if (!form) {
|
|
1288
|
+
throw new Error(`Form not found: ${formId}`);
|
|
1289
|
+
}
|
|
1290
|
+
const existing = await getActiveSession(this.runtime, entityId, roomId);
|
|
1291
|
+
if (existing) {
|
|
1292
|
+
throw new Error(`Active session already exists for this user/room: ${existing.id}`);
|
|
1293
|
+
}
|
|
1294
|
+
const now = Date.now();
|
|
1295
|
+
const fields = {};
|
|
1296
|
+
for (const control of form.controls) {
|
|
1297
|
+
if (options?.initialValues?.[control.key] !== undefined) {
|
|
1298
|
+
fields[control.key] = {
|
|
1299
|
+
status: "filled",
|
|
1300
|
+
value: options.initialValues[control.key],
|
|
1301
|
+
source: "manual",
|
|
1302
|
+
updatedAt: now
|
|
1303
|
+
};
|
|
1304
|
+
} else if (control.defaultValue !== undefined) {
|
|
1305
|
+
fields[control.key] = {
|
|
1306
|
+
status: "filled",
|
|
1307
|
+
value: control.defaultValue,
|
|
1308
|
+
source: "default",
|
|
1309
|
+
updatedAt: now
|
|
1310
|
+
};
|
|
1311
|
+
} else {
|
|
1312
|
+
fields[control.key] = { status: "empty" };
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const ttlDays = form.ttl?.minDays ?? 14;
|
|
1316
|
+
const expiresAt = now + ttlDays * 24 * 60 * 60 * 1000;
|
|
1317
|
+
const session = {
|
|
1318
|
+
id: v4_default(),
|
|
1319
|
+
formId,
|
|
1320
|
+
formVersion: form.version,
|
|
1321
|
+
entityId,
|
|
1322
|
+
roomId,
|
|
1323
|
+
status: "active",
|
|
1324
|
+
fields,
|
|
1325
|
+
history: [],
|
|
1326
|
+
context: options?.context,
|
|
1327
|
+
locale: options?.locale,
|
|
1328
|
+
effort: {
|
|
1329
|
+
interactionCount: 0,
|
|
1330
|
+
timeSpentMs: 0,
|
|
1331
|
+
firstInteractionAt: now,
|
|
1332
|
+
lastInteractionAt: now
|
|
1333
|
+
},
|
|
1334
|
+
expiresAt,
|
|
1335
|
+
createdAt: now,
|
|
1336
|
+
updatedAt: now
|
|
1337
|
+
};
|
|
1338
|
+
await saveSession(this.runtime, session);
|
|
1339
|
+
if (form.hooks?.onStart) {
|
|
1340
|
+
await this.executeHook(session, "onStart");
|
|
1341
|
+
}
|
|
1342
|
+
logger.debug(`[FormService] Started session ${session.id} for form ${formId}`);
|
|
1343
|
+
return session;
|
|
1344
|
+
}
|
|
1345
|
+
async getActiveSession(entityId, roomId) {
|
|
1346
|
+
return getActiveSession(this.runtime, entityId, roomId);
|
|
1347
|
+
}
|
|
1348
|
+
async getAllActiveSessions(entityId) {
|
|
1349
|
+
return getAllActiveSessions(this.runtime, entityId);
|
|
1350
|
+
}
|
|
1351
|
+
async getStashedSessions(entityId) {
|
|
1352
|
+
return getStashedSessions(this.runtime, entityId);
|
|
1353
|
+
}
|
|
1354
|
+
async saveSession(session) {
|
|
1355
|
+
session.updatedAt = Date.now();
|
|
1356
|
+
await saveSession(this.runtime, session);
|
|
1357
|
+
}
|
|
1358
|
+
async updateField(sessionId, entityId, field, value, confidence, source, messageId) {
|
|
1359
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1360
|
+
if (!session) {
|
|
1361
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1362
|
+
}
|
|
1363
|
+
const form = this.getForm(session.formId);
|
|
1364
|
+
if (!form) {
|
|
1365
|
+
throw new Error(`Form not found: ${session.formId}`);
|
|
1366
|
+
}
|
|
1367
|
+
const control = form.controls.find((c) => c.key === field);
|
|
1368
|
+
if (!control) {
|
|
1369
|
+
throw new Error(`Field not found: ${field}`);
|
|
1370
|
+
}
|
|
1371
|
+
const oldValue = session.fields[field]?.value;
|
|
1372
|
+
const validation = validateField(value, control);
|
|
1373
|
+
let status;
|
|
1374
|
+
if (!validation.valid) {
|
|
1375
|
+
status = "invalid";
|
|
1376
|
+
} else if (confidence < (control.confirmThreshold ?? 0.8)) {
|
|
1377
|
+
status = "uncertain";
|
|
1378
|
+
} else {
|
|
1379
|
+
status = "filled";
|
|
1380
|
+
}
|
|
1381
|
+
const now = Date.now();
|
|
1382
|
+
if (oldValue !== undefined) {
|
|
1383
|
+
const historyEntry = {
|
|
1384
|
+
field,
|
|
1385
|
+
oldValue,
|
|
1386
|
+
newValue: value,
|
|
1387
|
+
timestamp: now
|
|
1388
|
+
};
|
|
1389
|
+
session.history.push(historyEntry);
|
|
1390
|
+
const maxUndo = form.ux?.maxUndoSteps ?? 5;
|
|
1391
|
+
if (session.history.length > maxUndo) {
|
|
1392
|
+
session.history = session.history.slice(-maxUndo);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
session.fields[field] = {
|
|
1396
|
+
status,
|
|
1397
|
+
value,
|
|
1398
|
+
confidence,
|
|
1399
|
+
source,
|
|
1400
|
+
messageId,
|
|
1401
|
+
updatedAt: now,
|
|
1402
|
+
error: !validation.valid ? validation.error : undefined
|
|
1403
|
+
};
|
|
1404
|
+
session.effort.interactionCount++;
|
|
1405
|
+
session.effort.lastInteractionAt = now;
|
|
1406
|
+
session.effort.timeSpentMs = now - session.effort.firstInteractionAt;
|
|
1407
|
+
session.expiresAt = this.calculateTTL(session);
|
|
1408
|
+
const allRequiredFilled = this.checkAllRequiredFilled(session, form);
|
|
1409
|
+
if (allRequiredFilled && session.status === "active") {
|
|
1410
|
+
session.status = "ready";
|
|
1411
|
+
if (form.hooks?.onReady) {
|
|
1412
|
+
await this.executeHook(session, "onReady");
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
session.updatedAt = now;
|
|
1416
|
+
await saveSession(this.runtime, session);
|
|
1417
|
+
if (form.hooks?.onFieldChange) {
|
|
1418
|
+
const hookPayload = { field, value };
|
|
1419
|
+
if (oldValue !== undefined) {
|
|
1420
|
+
hookPayload.oldValue = oldValue;
|
|
1421
|
+
}
|
|
1422
|
+
await this.executeHook(session, "onFieldChange", hookPayload);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
async undoLastChange(sessionId, entityId) {
|
|
1426
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1427
|
+
if (!session) {
|
|
1428
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1429
|
+
}
|
|
1430
|
+
const form = this.getForm(session.formId);
|
|
1431
|
+
if (!form?.ux?.allowUndo) {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
const lastChange = session.history.pop();
|
|
1435
|
+
if (!lastChange) {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
if (lastChange.oldValue !== undefined) {
|
|
1439
|
+
session.fields[lastChange.field] = {
|
|
1440
|
+
status: "filled",
|
|
1441
|
+
value: lastChange.oldValue,
|
|
1442
|
+
source: "correction",
|
|
1443
|
+
updatedAt: Date.now()
|
|
1444
|
+
};
|
|
1445
|
+
} else {
|
|
1446
|
+
session.fields[lastChange.field] = { status: "empty" };
|
|
1447
|
+
}
|
|
1448
|
+
session.updatedAt = Date.now();
|
|
1449
|
+
await saveSession(this.runtime, session);
|
|
1450
|
+
return { field: lastChange.field, restoredValue: lastChange.oldValue };
|
|
1451
|
+
}
|
|
1452
|
+
async skipField(sessionId, entityId, field) {
|
|
1453
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1454
|
+
if (!session) {
|
|
1455
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1456
|
+
}
|
|
1457
|
+
const form = this.getForm(session.formId);
|
|
1458
|
+
if (!form?.ux?.allowSkip) {
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
const control = form.controls.find((c) => c.key === field);
|
|
1462
|
+
if (!control) {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
if (control.required) {
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
session.fields[field] = {
|
|
1469
|
+
status: "skipped",
|
|
1470
|
+
updatedAt: Date.now()
|
|
1471
|
+
};
|
|
1472
|
+
session.updatedAt = Date.now();
|
|
1473
|
+
await saveSession(this.runtime, session);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
async confirmField(sessionId, entityId, field, accepted) {
|
|
1477
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1478
|
+
if (!session) {
|
|
1479
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1480
|
+
}
|
|
1481
|
+
const fieldState = session.fields[field];
|
|
1482
|
+
if (!fieldState || fieldState.status !== "uncertain") {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const now = Date.now();
|
|
1486
|
+
if (accepted) {
|
|
1487
|
+
fieldState.status = "filled";
|
|
1488
|
+
fieldState.confirmedAt = now;
|
|
1489
|
+
} else {
|
|
1490
|
+
fieldState.status = "empty";
|
|
1491
|
+
fieldState.value = undefined;
|
|
1492
|
+
fieldState.confidence = undefined;
|
|
1493
|
+
}
|
|
1494
|
+
fieldState.updatedAt = now;
|
|
1495
|
+
session.updatedAt = now;
|
|
1496
|
+
await saveSession(this.runtime, session);
|
|
1497
|
+
}
|
|
1498
|
+
async updateSubField(sessionId, entityId, parentField, subField, value, confidence, messageId) {
|
|
1499
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1500
|
+
if (!session) {
|
|
1501
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1502
|
+
}
|
|
1503
|
+
const form = this.getForm(session.formId);
|
|
1504
|
+
if (!form) {
|
|
1505
|
+
throw new Error(`Form not found: ${session.formId}`);
|
|
1506
|
+
}
|
|
1507
|
+
const parentControl = form.controls.find((c) => c.key === parentField);
|
|
1508
|
+
if (!parentControl) {
|
|
1509
|
+
throw new Error(`Parent field not found: ${parentField}`);
|
|
1510
|
+
}
|
|
1511
|
+
const controlType = this.getControlType(parentControl.type);
|
|
1512
|
+
if (!controlType?.getSubControls) {
|
|
1513
|
+
throw new Error(`Control type '${parentControl.type}' is not a composite type`);
|
|
1514
|
+
}
|
|
1515
|
+
const subControls = controlType.getSubControls(parentControl, this.runtime);
|
|
1516
|
+
const subControl = subControls.find((c) => c.key === subField);
|
|
1517
|
+
if (!subControl) {
|
|
1518
|
+
throw new Error(`Subfield not found: ${subField} in ${parentField}`);
|
|
1519
|
+
}
|
|
1520
|
+
const now = Date.now();
|
|
1521
|
+
if (!session.fields[parentField]) {
|
|
1522
|
+
session.fields[parentField] = { status: "empty" };
|
|
1523
|
+
}
|
|
1524
|
+
if (!session.fields[parentField].subFields) {
|
|
1525
|
+
session.fields[parentField].subFields = {};
|
|
1526
|
+
}
|
|
1527
|
+
let subFieldStatus;
|
|
1528
|
+
let error;
|
|
1529
|
+
if (controlType.validate) {
|
|
1530
|
+
const result = controlType.validate(value, subControl);
|
|
1531
|
+
if (!result.valid) {
|
|
1532
|
+
subFieldStatus = "invalid";
|
|
1533
|
+
error = result.error;
|
|
1534
|
+
} else if (confidence < (subControl.confirmThreshold ?? 0.8)) {
|
|
1535
|
+
subFieldStatus = "uncertain";
|
|
1536
|
+
} else {
|
|
1537
|
+
subFieldStatus = "filled";
|
|
1538
|
+
}
|
|
1539
|
+
} else {
|
|
1540
|
+
const validation = validateField(value, subControl);
|
|
1541
|
+
if (!validation.valid) {
|
|
1542
|
+
subFieldStatus = "invalid";
|
|
1543
|
+
error = validation.error;
|
|
1544
|
+
} else if (confidence < (subControl.confirmThreshold ?? 0.8)) {
|
|
1545
|
+
subFieldStatus = "uncertain";
|
|
1546
|
+
} else {
|
|
1547
|
+
subFieldStatus = "filled";
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
session.fields[parentField].subFields[subField] = {
|
|
1551
|
+
status: subFieldStatus,
|
|
1552
|
+
value,
|
|
1553
|
+
confidence,
|
|
1554
|
+
source: "extraction",
|
|
1555
|
+
messageId,
|
|
1556
|
+
updatedAt: now,
|
|
1557
|
+
error
|
|
1558
|
+
};
|
|
1559
|
+
session.effort.interactionCount++;
|
|
1560
|
+
session.effort.lastInteractionAt = now;
|
|
1561
|
+
session.effort.timeSpentMs = now - session.effort.firstInteractionAt;
|
|
1562
|
+
session.updatedAt = now;
|
|
1563
|
+
await saveSession(this.runtime, session);
|
|
1564
|
+
logger.debug(`[FormService] Updated subfield ${parentField}.${subField}`);
|
|
1565
|
+
}
|
|
1566
|
+
areSubFieldsFilled(session, parentField) {
|
|
1567
|
+
const form = this.getForm(session.formId);
|
|
1568
|
+
if (!form)
|
|
1569
|
+
return false;
|
|
1570
|
+
const parentControl = form.controls.find((c) => c.key === parentField);
|
|
1571
|
+
if (!parentControl)
|
|
1572
|
+
return false;
|
|
1573
|
+
const controlType = this.getControlType(parentControl.type);
|
|
1574
|
+
if (!controlType?.getSubControls)
|
|
1575
|
+
return false;
|
|
1576
|
+
const subControls = controlType.getSubControls(parentControl, this.runtime);
|
|
1577
|
+
const subFields = session.fields[parentField]?.subFields || {};
|
|
1578
|
+
for (const subControl of subControls) {
|
|
1579
|
+
if (!subControl.required)
|
|
1580
|
+
continue;
|
|
1581
|
+
const subField = subFields[subControl.key];
|
|
1582
|
+
if (!subField || subField.status !== "filled") {
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return true;
|
|
1587
|
+
}
|
|
1588
|
+
getSubFieldValues(session, parentField) {
|
|
1589
|
+
const subFields = session.fields[parentField]?.subFields || {};
|
|
1590
|
+
const values = {};
|
|
1591
|
+
for (const [key, state] of Object.entries(subFields)) {
|
|
1592
|
+
if (state.value !== undefined) {
|
|
1593
|
+
values[key] = state.value;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return values;
|
|
1597
|
+
}
|
|
1598
|
+
async activateExternalField(sessionId, entityId, field) {
|
|
1599
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1600
|
+
if (!session) {
|
|
1601
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1602
|
+
}
|
|
1603
|
+
const form = this.getForm(session.formId);
|
|
1604
|
+
if (!form) {
|
|
1605
|
+
throw new Error(`Form not found: ${session.formId}`);
|
|
1606
|
+
}
|
|
1607
|
+
const control = form.controls.find((c) => c.key === field);
|
|
1608
|
+
if (!control) {
|
|
1609
|
+
throw new Error(`Field not found: ${field}`);
|
|
1610
|
+
}
|
|
1611
|
+
const controlType = this.getControlType(control.type);
|
|
1612
|
+
if (!controlType?.activate) {
|
|
1613
|
+
throw new Error(`Control type '${control.type}' does not support activation`);
|
|
1614
|
+
}
|
|
1615
|
+
const subValues = this.getSubFieldValues(session, field);
|
|
1616
|
+
const context = {
|
|
1617
|
+
runtime: this.runtime,
|
|
1618
|
+
session,
|
|
1619
|
+
control,
|
|
1620
|
+
subValues
|
|
1621
|
+
};
|
|
1622
|
+
const activation = await controlType.activate(context);
|
|
1623
|
+
const now = Date.now();
|
|
1624
|
+
if (!session.fields[field]) {
|
|
1625
|
+
session.fields[field] = { status: "empty" };
|
|
1626
|
+
}
|
|
1627
|
+
session.fields[field].status = "pending";
|
|
1628
|
+
session.fields[field].externalState = {
|
|
1629
|
+
status: "pending",
|
|
1630
|
+
reference: activation.reference,
|
|
1631
|
+
instructions: activation.instructions,
|
|
1632
|
+
address: activation.address,
|
|
1633
|
+
activatedAt: now
|
|
1634
|
+
};
|
|
1635
|
+
session.updatedAt = now;
|
|
1636
|
+
await saveSession(this.runtime, session);
|
|
1637
|
+
logger.info(`[FormService] Activated external field ${field} with reference ${activation.reference}`);
|
|
1638
|
+
return activation;
|
|
1639
|
+
}
|
|
1640
|
+
async confirmExternalField(sessionId, entityId, field, value, externalData) {
|
|
1641
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1642
|
+
if (!session) {
|
|
1643
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1644
|
+
}
|
|
1645
|
+
const fieldState = session.fields[field];
|
|
1646
|
+
if (!fieldState || fieldState.status !== "pending") {
|
|
1647
|
+
logger.warn(`[FormService] Cannot confirm field ${field}: not in pending state`);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const now = Date.now();
|
|
1651
|
+
fieldState.status = "filled";
|
|
1652
|
+
fieldState.value = value;
|
|
1653
|
+
fieldState.source = "external";
|
|
1654
|
+
fieldState.updatedAt = now;
|
|
1655
|
+
if (fieldState.externalState) {
|
|
1656
|
+
fieldState.externalState.status = "confirmed";
|
|
1657
|
+
fieldState.externalState.confirmedAt = now;
|
|
1658
|
+
fieldState.externalState.externalData = externalData;
|
|
1659
|
+
}
|
|
1660
|
+
const form = this.getForm(session.formId);
|
|
1661
|
+
if (form && this.checkAllRequiredFilled(session, form)) {
|
|
1662
|
+
if (session.status === "active") {
|
|
1663
|
+
session.status = "ready";
|
|
1664
|
+
if (form.hooks?.onReady) {
|
|
1665
|
+
await this.executeHook(session, "onReady");
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
session.updatedAt = now;
|
|
1670
|
+
await saveSession(this.runtime, session);
|
|
1671
|
+
try {
|
|
1672
|
+
await this.runtime.emitEvent("FORM_FIELD_CONFIRMED", {
|
|
1673
|
+
runtime: this.runtime,
|
|
1674
|
+
sessionId,
|
|
1675
|
+
entityId,
|
|
1676
|
+
field,
|
|
1677
|
+
value,
|
|
1678
|
+
externalData
|
|
1679
|
+
});
|
|
1680
|
+
} catch (_error) {
|
|
1681
|
+
logger.debug(`[FormService] No event handler for FORM_FIELD_CONFIRMED`);
|
|
1682
|
+
}
|
|
1683
|
+
logger.info(`[FormService] Confirmed external field ${field}`);
|
|
1684
|
+
}
|
|
1685
|
+
async cancelExternalField(sessionId, entityId, field, reason) {
|
|
1686
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1687
|
+
if (!session) {
|
|
1688
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1689
|
+
}
|
|
1690
|
+
const form = this.getForm(session.formId);
|
|
1691
|
+
const control = form?.controls.find((c) => c.key === field);
|
|
1692
|
+
const controlType = control ? this.getControlType(control.type) : undefined;
|
|
1693
|
+
if (controlType?.deactivate && control) {
|
|
1694
|
+
try {
|
|
1695
|
+
await controlType.deactivate({
|
|
1696
|
+
runtime: this.runtime,
|
|
1697
|
+
session,
|
|
1698
|
+
control,
|
|
1699
|
+
subValues: this.getSubFieldValues(session, field)
|
|
1700
|
+
});
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
logger.error(`[FormService] Deactivate failed for ${field}: ${String(error)}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const fieldState = session.fields[field];
|
|
1706
|
+
if (fieldState) {
|
|
1707
|
+
fieldState.status = "empty";
|
|
1708
|
+
fieldState.error = reason;
|
|
1709
|
+
if (fieldState.externalState) {
|
|
1710
|
+
fieldState.externalState.status = "failed";
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
session.updatedAt = Date.now();
|
|
1714
|
+
await saveSession(this.runtime, session);
|
|
1715
|
+
try {
|
|
1716
|
+
await this.runtime.emitEvent("FORM_FIELD_CANCELLED", {
|
|
1717
|
+
runtime: this.runtime,
|
|
1718
|
+
sessionId,
|
|
1719
|
+
entityId,
|
|
1720
|
+
field,
|
|
1721
|
+
reason
|
|
1722
|
+
});
|
|
1723
|
+
} catch (_error) {
|
|
1724
|
+
logger.debug(`[FormService] No event handler for FORM_FIELD_CANCELLED`);
|
|
1725
|
+
}
|
|
1726
|
+
logger.info(`[FormService] Cancelled external field ${field}: ${reason}`);
|
|
1727
|
+
}
|
|
1728
|
+
async submit(sessionId, entityId) {
|
|
1729
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1730
|
+
if (!session) {
|
|
1731
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1732
|
+
}
|
|
1733
|
+
const form = this.getForm(session.formId);
|
|
1734
|
+
if (!form) {
|
|
1735
|
+
throw new Error(`Form not found: ${session.formId}`);
|
|
1736
|
+
}
|
|
1737
|
+
if (!this.checkAllRequiredFilled(session, form)) {
|
|
1738
|
+
throw new Error("Not all required fields are filled");
|
|
1739
|
+
}
|
|
1740
|
+
const now = Date.now();
|
|
1741
|
+
const values = {};
|
|
1742
|
+
const mappedValues = {};
|
|
1743
|
+
const files = {};
|
|
1744
|
+
for (const control of form.controls) {
|
|
1745
|
+
const fieldState = session.fields[control.key];
|
|
1746
|
+
if (fieldState?.value !== undefined) {
|
|
1747
|
+
values[control.key] = fieldState.value;
|
|
1748
|
+
const dbKey = control.dbbind || control.key;
|
|
1749
|
+
mappedValues[dbKey] = fieldState.value;
|
|
1750
|
+
}
|
|
1751
|
+
if (fieldState?.files) {
|
|
1752
|
+
files[control.key] = fieldState.files;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const submission = {
|
|
1756
|
+
id: v4_default(),
|
|
1757
|
+
formId: session.formId,
|
|
1758
|
+
formVersion: session.formVersion,
|
|
1759
|
+
sessionId: session.id,
|
|
1760
|
+
entityId: session.entityId,
|
|
1761
|
+
values,
|
|
1762
|
+
mappedValues,
|
|
1763
|
+
files: Object.keys(files).length > 0 ? files : undefined,
|
|
1764
|
+
submittedAt: now,
|
|
1765
|
+
meta: session.meta
|
|
1766
|
+
};
|
|
1767
|
+
await saveSubmission(this.runtime, submission);
|
|
1768
|
+
await saveAutofillData(this.runtime, entityId, session.formId, values);
|
|
1769
|
+
session.status = "submitted";
|
|
1770
|
+
session.submittedAt = now;
|
|
1771
|
+
session.updatedAt = now;
|
|
1772
|
+
await saveSession(this.runtime, session);
|
|
1773
|
+
if (form.hooks?.onSubmit) {
|
|
1774
|
+
const submissionPayload = JSON.parse(JSON.stringify(submission));
|
|
1775
|
+
await this.executeHook(session, "onSubmit", {
|
|
1776
|
+
submission: submissionPayload
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
logger.debug(`[FormService] Submitted session ${sessionId}`);
|
|
1780
|
+
return submission;
|
|
1781
|
+
}
|
|
1782
|
+
async stash(sessionId, entityId) {
|
|
1783
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1784
|
+
if (!session) {
|
|
1785
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1786
|
+
}
|
|
1787
|
+
const form = this.getForm(session.formId);
|
|
1788
|
+
session.status = "stashed";
|
|
1789
|
+
session.updatedAt = Date.now();
|
|
1790
|
+
await saveSession(this.runtime, session);
|
|
1791
|
+
if (form?.hooks?.onCancel) {}
|
|
1792
|
+
logger.debug(`[FormService] Stashed session ${sessionId}`);
|
|
1793
|
+
}
|
|
1794
|
+
async restore(sessionId, entityId) {
|
|
1795
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1796
|
+
if (!session) {
|
|
1797
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1798
|
+
}
|
|
1799
|
+
if (session.status !== "stashed") {
|
|
1800
|
+
throw new Error(`Session is not stashed: ${session.status}`);
|
|
1801
|
+
}
|
|
1802
|
+
const existing = await getActiveSession(this.runtime, entityId, session.roomId);
|
|
1803
|
+
if (existing && existing.id !== sessionId) {
|
|
1804
|
+
throw new Error(`Active session already exists in room: ${existing.id}`);
|
|
1805
|
+
}
|
|
1806
|
+
session.status = "active";
|
|
1807
|
+
session.updatedAt = Date.now();
|
|
1808
|
+
session.expiresAt = this.calculateTTL(session);
|
|
1809
|
+
await saveSession(this.runtime, session);
|
|
1810
|
+
logger.debug(`[FormService] Restored session ${sessionId}`);
|
|
1811
|
+
return session;
|
|
1812
|
+
}
|
|
1813
|
+
async cancel(sessionId, entityId, force = false) {
|
|
1814
|
+
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1815
|
+
if (!session) {
|
|
1816
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1817
|
+
}
|
|
1818
|
+
if (!force && this.shouldConfirmCancel(session) && !session.cancelConfirmationAsked) {
|
|
1819
|
+
session.cancelConfirmationAsked = true;
|
|
1820
|
+
session.updatedAt = Date.now();
|
|
1821
|
+
await saveSession(this.runtime, session);
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
const form = this.getForm(session.formId);
|
|
1825
|
+
session.status = "cancelled";
|
|
1826
|
+
session.updatedAt = Date.now();
|
|
1827
|
+
await saveSession(this.runtime, session);
|
|
1828
|
+
if (form?.hooks?.onCancel) {
|
|
1829
|
+
await this.executeHook(session, "onCancel");
|
|
1830
|
+
}
|
|
1831
|
+
logger.debug(`[FormService] Cancelled session ${sessionId}`);
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
async getSubmissions(entityId, formId) {
|
|
1835
|
+
return getSubmissions(this.runtime, entityId, formId);
|
|
1836
|
+
}
|
|
1837
|
+
async getAutofill(entityId, formId) {
|
|
1838
|
+
const data = await getAutofillData(this.runtime, entityId, formId);
|
|
1839
|
+
return data?.values || null;
|
|
1840
|
+
}
|
|
1841
|
+
async applyAutofill(session) {
|
|
1842
|
+
const form = this.getForm(session.formId);
|
|
1843
|
+
if (!form?.ux?.allowAutofill) {
|
|
1844
|
+
return [];
|
|
1845
|
+
}
|
|
1846
|
+
const autofill = await getAutofillData(this.runtime, session.entityId, session.formId);
|
|
1847
|
+
if (!autofill) {
|
|
1848
|
+
return [];
|
|
1849
|
+
}
|
|
1850
|
+
const appliedFields = [];
|
|
1851
|
+
const now = Date.now();
|
|
1852
|
+
for (const control of form.controls) {
|
|
1853
|
+
if (session.fields[control.key]?.status !== "empty") {
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
const value = autofill.values[control.key];
|
|
1857
|
+
if (value !== undefined) {
|
|
1858
|
+
session.fields[control.key] = {
|
|
1859
|
+
status: "filled",
|
|
1860
|
+
value,
|
|
1861
|
+
source: "autofill",
|
|
1862
|
+
updatedAt: now
|
|
1863
|
+
};
|
|
1864
|
+
appliedFields.push(control.key);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (appliedFields.length > 0) {
|
|
1868
|
+
session.updatedAt = now;
|
|
1869
|
+
await saveSession(this.runtime, session);
|
|
1870
|
+
}
|
|
1871
|
+
return appliedFields;
|
|
1872
|
+
}
|
|
1873
|
+
getSessionContext(session) {
|
|
1874
|
+
const form = this.getForm(session.formId);
|
|
1875
|
+
if (!form) {
|
|
1876
|
+
return {
|
|
1877
|
+
hasActiveForm: false,
|
|
1878
|
+
progress: 0,
|
|
1879
|
+
filledFields: [],
|
|
1880
|
+
missingRequired: [],
|
|
1881
|
+
uncertainFields: [],
|
|
1882
|
+
nextField: null,
|
|
1883
|
+
pendingExternalFields: []
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
const filledFields = [];
|
|
1887
|
+
const missingRequired = [];
|
|
1888
|
+
const uncertainFields = [];
|
|
1889
|
+
const pendingExternalFields = [];
|
|
1890
|
+
let nextField = null;
|
|
1891
|
+
let filledCount = 0;
|
|
1892
|
+
let totalRequired = 0;
|
|
1893
|
+
for (const control of form.controls) {
|
|
1894
|
+
if (control.hidden)
|
|
1895
|
+
continue;
|
|
1896
|
+
const fieldState = session.fields[control.key];
|
|
1897
|
+
if (control.required) {
|
|
1898
|
+
totalRequired++;
|
|
1899
|
+
}
|
|
1900
|
+
if (fieldState?.status === "filled") {
|
|
1901
|
+
filledCount++;
|
|
1902
|
+
filledFields.push({
|
|
1903
|
+
key: control.key,
|
|
1904
|
+
label: control.label,
|
|
1905
|
+
displayValue: formatValue(fieldState.value ?? null, control)
|
|
1906
|
+
});
|
|
1907
|
+
} else if (fieldState?.status === "pending") {
|
|
1908
|
+
if (fieldState.externalState) {
|
|
1909
|
+
pendingExternalFields.push({
|
|
1910
|
+
key: control.key,
|
|
1911
|
+
label: control.label,
|
|
1912
|
+
instructions: fieldState.externalState.instructions || "Waiting for confirmation...",
|
|
1913
|
+
reference: fieldState.externalState.reference || "",
|
|
1914
|
+
activatedAt: fieldState.externalState.activatedAt || Date.now(),
|
|
1915
|
+
address: fieldState.externalState.address
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
} else if (fieldState?.status === "uncertain") {
|
|
1919
|
+
uncertainFields.push({
|
|
1920
|
+
key: control.key,
|
|
1921
|
+
label: control.label,
|
|
1922
|
+
value: fieldState.value ?? null,
|
|
1923
|
+
confidence: fieldState.confidence ?? 0
|
|
1924
|
+
});
|
|
1925
|
+
} else if (fieldState?.status === "invalid") {
|
|
1926
|
+
missingRequired.push({
|
|
1927
|
+
key: control.key,
|
|
1928
|
+
label: control.label,
|
|
1929
|
+
description: control.description,
|
|
1930
|
+
askPrompt: control.askPrompt
|
|
1931
|
+
});
|
|
1932
|
+
if (!nextField)
|
|
1933
|
+
nextField = control;
|
|
1934
|
+
} else if (control.required && fieldState?.status !== "skipped") {
|
|
1935
|
+
missingRequired.push({
|
|
1936
|
+
key: control.key,
|
|
1937
|
+
label: control.label,
|
|
1938
|
+
description: control.description,
|
|
1939
|
+
askPrompt: control.askPrompt
|
|
1940
|
+
});
|
|
1941
|
+
if (!nextField)
|
|
1942
|
+
nextField = control;
|
|
1943
|
+
} else if (!nextField && fieldState?.status === "empty") {
|
|
1944
|
+
nextField = control;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const progress = totalRequired > 0 ? Math.round(filledCount / totalRequired * 100) : 100;
|
|
1948
|
+
return {
|
|
1949
|
+
hasActiveForm: true,
|
|
1950
|
+
formId: session.formId,
|
|
1951
|
+
formName: form.name,
|
|
1952
|
+
progress,
|
|
1953
|
+
filledFields,
|
|
1954
|
+
missingRequired,
|
|
1955
|
+
uncertainFields,
|
|
1956
|
+
nextField,
|
|
1957
|
+
status: session.status,
|
|
1958
|
+
pendingCancelConfirmation: session.cancelConfirmationAsked && session.status === "active",
|
|
1959
|
+
pendingExternalFields
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
getValues(session) {
|
|
1963
|
+
const values = {};
|
|
1964
|
+
for (const [key, state] of Object.entries(session.fields)) {
|
|
1965
|
+
if (state.value !== undefined) {
|
|
1966
|
+
values[key] = state.value;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return values;
|
|
1970
|
+
}
|
|
1971
|
+
getMappedValues(session) {
|
|
1972
|
+
const form = this.getForm(session.formId);
|
|
1973
|
+
if (!form)
|
|
1974
|
+
return {};
|
|
1975
|
+
const values = {};
|
|
1976
|
+
for (const control of form.controls) {
|
|
1977
|
+
const state = session.fields[control.key];
|
|
1978
|
+
if (state?.value !== undefined) {
|
|
1979
|
+
const key = control.dbbind || control.key;
|
|
1980
|
+
values[key] = state.value;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return values;
|
|
1984
|
+
}
|
|
1985
|
+
calculateTTL(session) {
|
|
1986
|
+
const form = this.getForm(session.formId);
|
|
1987
|
+
const config = form?.ttl || {};
|
|
1988
|
+
const minDays = config.minDays ?? 14;
|
|
1989
|
+
const maxDays = config.maxDays ?? 90;
|
|
1990
|
+
const multiplier = config.effortMultiplier ?? 0.5;
|
|
1991
|
+
const minutesSpent = session.effort.timeSpentMs / 60000;
|
|
1992
|
+
const effortDays = minutesSpent * multiplier;
|
|
1993
|
+
const ttlDays = Math.min(maxDays, Math.max(minDays, effortDays));
|
|
1994
|
+
return Date.now() + ttlDays * 24 * 60 * 60 * 1000;
|
|
1995
|
+
}
|
|
1996
|
+
shouldConfirmCancel(session) {
|
|
1997
|
+
const minEffortMs = 5 * 60 * 1000;
|
|
1998
|
+
return session.effort.timeSpentMs > minEffortMs;
|
|
1999
|
+
}
|
|
2000
|
+
async executeHook(session, hookName, options) {
|
|
2001
|
+
const form = this.getForm(session.formId);
|
|
2002
|
+
const workerName = form?.hooks?.[hookName];
|
|
2003
|
+
if (!workerName)
|
|
2004
|
+
return;
|
|
2005
|
+
const worker = this.runtime.getTaskWorker(workerName);
|
|
2006
|
+
if (!worker) {
|
|
2007
|
+
logger.warn(`[FormService] Hook worker not found: ${workerName}`);
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
const task = {
|
|
2012
|
+
id: session.id,
|
|
2013
|
+
name: workerName,
|
|
2014
|
+
roomId: session.roomId,
|
|
2015
|
+
entityId: session.entityId
|
|
2016
|
+
};
|
|
2017
|
+
await worker.execute(this.runtime, {
|
|
2018
|
+
session,
|
|
2019
|
+
form,
|
|
2020
|
+
...options
|
|
2021
|
+
}, task);
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
logger.error(`[FormService] Hook execution failed: ${hookName}`, String(error));
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
checkAllRequiredFilled(session, form) {
|
|
2027
|
+
for (const control of form.controls) {
|
|
2028
|
+
if (!control.required)
|
|
2029
|
+
continue;
|
|
2030
|
+
const fieldState = session.fields[control.key];
|
|
2031
|
+
if (!fieldState || fieldState.status === "empty" || fieldState.status === "invalid") {
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return true;
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
// src/actions/restore.ts
|
|
2041
|
+
var exports_restore = {};
|
|
2042
|
+
__export(exports_restore, {
|
|
2043
|
+
formRestoreAction: () => formRestoreAction,
|
|
2044
|
+
default: () => restore_default
|
|
2045
|
+
});
|
|
2046
|
+
import {
|
|
2047
|
+
logger as logger2
|
|
2048
|
+
} from "@elizaos/core";
|
|
2049
|
+
var formRestoreAction, restore_default;
|
|
2050
|
+
var init_restore = __esm(() => {
|
|
2051
|
+
formRestoreAction = {
|
|
2052
|
+
name: "FORM_RESTORE",
|
|
2053
|
+
similes: ["RESUME_FORM", "CONTINUE_FORM"],
|
|
2054
|
+
description: "Restore a previously stashed form session",
|
|
2055
|
+
validate: async (runtime, message, _state) => {
|
|
2056
|
+
try {
|
|
2057
|
+
const text = message.content?.text || "";
|
|
2058
|
+
const intent = quickIntentDetect(text);
|
|
2059
|
+
if (intent !== "restore") {
|
|
2060
|
+
return false;
|
|
2061
|
+
}
|
|
2062
|
+
const formService = runtime.getService("FORM");
|
|
2063
|
+
if (!formService) {
|
|
2064
|
+
return false;
|
|
2065
|
+
}
|
|
2066
|
+
const entityId = message.entityId;
|
|
2067
|
+
if (!entityId)
|
|
2068
|
+
return false;
|
|
2069
|
+
const stashed = await formService.getStashedSessions(entityId);
|
|
2070
|
+
return stashed.length > 0;
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
logger2.error("[FormRestoreAction] Validation error:", String(error));
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
},
|
|
2076
|
+
handler: async (runtime, message, _state, _options, callback) => {
|
|
2077
|
+
try {
|
|
2078
|
+
const formService = runtime.getService("FORM");
|
|
2079
|
+
if (!formService) {
|
|
2080
|
+
await callback?.({
|
|
2081
|
+
text: "Sorry, I couldn't find the form service."
|
|
2082
|
+
});
|
|
2083
|
+
return { success: false };
|
|
2084
|
+
}
|
|
2085
|
+
const entityId = message.entityId;
|
|
2086
|
+
const roomId = message.roomId;
|
|
2087
|
+
if (!entityId || !roomId) {
|
|
2088
|
+
await callback?.({
|
|
2089
|
+
text: "Sorry, I couldn't identify you."
|
|
2090
|
+
});
|
|
2091
|
+
return { success: false };
|
|
2092
|
+
}
|
|
2093
|
+
const existing = await formService.getActiveSession(entityId, roomId);
|
|
2094
|
+
if (existing) {
|
|
2095
|
+
const form2 = formService.getForm(existing.formId);
|
|
2096
|
+
await callback?.({
|
|
2097
|
+
text: `You already have an active form: "${form2?.name || existing.formId}". Would you like to continue with that one, or should I save it and restore your other form?`
|
|
2098
|
+
});
|
|
2099
|
+
return { success: false };
|
|
2100
|
+
}
|
|
2101
|
+
const stashed = await formService.getStashedSessions(entityId);
|
|
2102
|
+
if (stashed.length === 0) {
|
|
2103
|
+
await callback?.({
|
|
2104
|
+
text: "You don't have any saved forms to resume."
|
|
2105
|
+
});
|
|
2106
|
+
return { success: false };
|
|
2107
|
+
}
|
|
2108
|
+
const sessionToRestore = stashed.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
2109
|
+
const session = await formService.restore(sessionToRestore.id, entityId);
|
|
2110
|
+
const form = formService.getForm(session.formId);
|
|
2111
|
+
const context = formService.getSessionContext(session);
|
|
2112
|
+
let responseText = `I've restored your "${form?.name || session.formId}" form. `;
|
|
2113
|
+
responseText += `You're ${context.progress}% complete. `;
|
|
2114
|
+
if (context.filledFields.length > 0) {
|
|
2115
|
+
responseText += `
|
|
2116
|
+
|
|
2117
|
+
Here's what I have so far:
|
|
2118
|
+
`;
|
|
2119
|
+
for (const field of context.filledFields) {
|
|
2120
|
+
responseText += `• ${field.label}: ${field.displayValue}
|
|
2121
|
+
`;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
if (context.nextField) {
|
|
2125
|
+
responseText += `
|
|
2126
|
+
Let's continue with ${context.nextField.label}.`;
|
|
2127
|
+
if (context.nextField.askPrompt) {
|
|
2128
|
+
responseText += ` ${context.nextField.askPrompt}`;
|
|
2129
|
+
}
|
|
2130
|
+
} else if (context.status === "ready") {
|
|
2131
|
+
responseText += `
|
|
2132
|
+
Everything looks complete! Ready to submit?`;
|
|
2133
|
+
}
|
|
2134
|
+
await callback?.({
|
|
2135
|
+
text: responseText
|
|
2136
|
+
});
|
|
2137
|
+
return {
|
|
2138
|
+
success: true,
|
|
2139
|
+
data: {
|
|
2140
|
+
sessionId: session.id,
|
|
2141
|
+
formId: session.formId,
|
|
2142
|
+
progress: context.progress
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
logger2.error("[FormRestoreAction] Handler error:", String(error));
|
|
2147
|
+
await callback?.({
|
|
2148
|
+
text: "Sorry, I couldn't restore your form. Please try again."
|
|
2149
|
+
});
|
|
2150
|
+
return { success: false };
|
|
2151
|
+
}
|
|
2152
|
+
},
|
|
2153
|
+
examples: [
|
|
2154
|
+
[
|
|
2155
|
+
{
|
|
2156
|
+
name: "{{user1}}",
|
|
2157
|
+
content: { text: "Resume my form" }
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
name: "{{agentName}}",
|
|
2161
|
+
content: {
|
|
2162
|
+
text: "I've restored your form. Let's continue where you left off."
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
],
|
|
2166
|
+
[
|
|
2167
|
+
{
|
|
2168
|
+
name: "{{user1}}",
|
|
2169
|
+
content: { text: "Continue with my registration" }
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
name: "{{agentName}}",
|
|
2173
|
+
content: {
|
|
2174
|
+
text: "I've restored your Registration form. You're 60% complete."
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
],
|
|
2178
|
+
[
|
|
2179
|
+
{
|
|
2180
|
+
name: "{{user1}}",
|
|
2181
|
+
content: { text: "Pick up where I left off" }
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
name: "{{agentName}}",
|
|
2185
|
+
content: {
|
|
2186
|
+
text: "I've restored your form. Here's what you have so far..."
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
]
|
|
2190
|
+
]
|
|
2191
|
+
};
|
|
2192
|
+
restore_default = formRestoreAction;
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
// src/evaluators/extractor.ts
|
|
2196
|
+
var exports_extractor = {};
|
|
2197
|
+
__export(exports_extractor, {
|
|
2198
|
+
formEvaluator: () => formEvaluator,
|
|
2199
|
+
default: () => extractor_default
|
|
2200
|
+
});
|
|
2201
|
+
import { logger as logger3 } from "@elizaos/core";
|
|
2202
|
+
async function processExtractions(runtime, formService, session, form, entityId, extractions, messageId) {
|
|
2203
|
+
const updatedParents = new Set;
|
|
2204
|
+
for (const extraction of extractions) {
|
|
2205
|
+
if (extraction.field.includes(".")) {
|
|
2206
|
+
const [parentKey, subKey] = extraction.field.split(".");
|
|
2207
|
+
await formService.updateSubField(session.id, entityId, parentKey, subKey, extraction.value, extraction.confidence, messageId);
|
|
2208
|
+
await emitEvent(runtime, "FORM_SUBFIELD_UPDATED", {
|
|
2209
|
+
sessionId: session.id,
|
|
2210
|
+
parentField: parentKey,
|
|
2211
|
+
subField: subKey,
|
|
2212
|
+
value: extraction.value,
|
|
2213
|
+
confidence: extraction.confidence
|
|
2214
|
+
});
|
|
2215
|
+
updatedParents.add(parentKey);
|
|
2216
|
+
if (form.debug) {
|
|
2217
|
+
logger3.debug(`[FormEvaluator] Updated subfield ${parentKey}.${subKey}`);
|
|
2218
|
+
}
|
|
2219
|
+
} else {
|
|
2220
|
+
await formService.updateField(session.id, entityId, extraction.field, extraction.value, extraction.confidence, extraction.isCorrection ? "correction" : "extraction", messageId);
|
|
2221
|
+
await emitEvent(runtime, "FORM_FIELD_EXTRACTED", {
|
|
2222
|
+
sessionId: session.id,
|
|
2223
|
+
field: extraction.field,
|
|
2224
|
+
value: extraction.value,
|
|
2225
|
+
confidence: extraction.confidence
|
|
2226
|
+
});
|
|
2227
|
+
if (form.debug) {
|
|
2228
|
+
logger3.debug(`[FormEvaluator] Updated field ${extraction.field}`);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
for (const parentKey of updatedParents) {
|
|
2233
|
+
await checkAndActivateExternalField(runtime, formService, session, form, entityId, parentKey);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
async function checkAndActivateExternalField(runtime, formService, session, form, entityId, field) {
|
|
2237
|
+
const freshSession = await formService.getActiveSession(entityId, session.roomId);
|
|
2238
|
+
if (!freshSession)
|
|
2239
|
+
return;
|
|
2240
|
+
if (!formService.isExternalType(form.controls.find((c) => c.key === field)?.type || "")) {
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
if (!formService.areSubFieldsFilled(freshSession, field)) {
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const subValues = formService.getSubFieldValues(freshSession, field);
|
|
2247
|
+
await emitEvent(runtime, "FORM_SUBCONTROLS_FILLED", {
|
|
2248
|
+
sessionId: session.id,
|
|
2249
|
+
field,
|
|
2250
|
+
subValues
|
|
2251
|
+
});
|
|
2252
|
+
logger3.debug(`[FormEvaluator] All subcontrols filled for ${field}, activating...`);
|
|
2253
|
+
try {
|
|
2254
|
+
const activation = await formService.activateExternalField(session.id, entityId, field);
|
|
2255
|
+
const activationPayload = JSON.parse(JSON.stringify(activation));
|
|
2256
|
+
await emitEvent(runtime, "FORM_EXTERNAL_ACTIVATED", {
|
|
2257
|
+
sessionId: session.id,
|
|
2258
|
+
field,
|
|
2259
|
+
activation: activationPayload
|
|
2260
|
+
});
|
|
2261
|
+
logger3.info(`[FormEvaluator] Activated external field ${field}: ${activation.instructions}`);
|
|
2262
|
+
} catch (error) {
|
|
2263
|
+
logger3.error(`[FormEvaluator] Failed to activate external field ${field}:`, String(error));
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
async function emitEvent(runtime, eventType, payload) {
|
|
2267
|
+
try {
|
|
2268
|
+
if (typeof runtime.emitEvent === "function") {
|
|
2269
|
+
const eventPayload = { runtime, ...payload };
|
|
2270
|
+
await runtime.emitEvent(eventType, eventPayload);
|
|
2271
|
+
}
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
logger3.debug(`[FormEvaluator] Event emission (${eventType}):`, String(error));
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async function handleSubmit(formService, session, entityId) {
|
|
2277
|
+
try {
|
|
2278
|
+
await formService.submit(session.id, entityId);
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
logger3.debug("[FormEvaluator] Submit failed:", String(error));
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
async function handleUndo(formService, session, entityId, form) {
|
|
2284
|
+
if (!form.ux?.allowUndo) {
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const result = await formService.undoLastChange(session.id, entityId);
|
|
2288
|
+
if (result) {
|
|
2289
|
+
logger3.debug("[FormEvaluator] Undid field:", result.field);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
async function handleSkip(formService, session, entityId, form) {
|
|
2293
|
+
if (!form.ux?.allowSkip) {
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
if (session.lastAskedField) {
|
|
2297
|
+
const skipped = await formService.skipField(session.id, entityId, session.lastAskedField);
|
|
2298
|
+
if (skipped) {
|
|
2299
|
+
logger3.debug("[FormEvaluator] Skipped field:", session.lastAskedField);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
var formEvaluator, extractor_default;
|
|
2304
|
+
var init_extractor = __esm(() => {
|
|
2305
|
+
init_extraction();
|
|
2306
|
+
init_template();
|
|
2307
|
+
formEvaluator = {
|
|
2308
|
+
name: "form_evaluator",
|
|
2309
|
+
description: "Extracts form fields and handles form intents from user messages",
|
|
2310
|
+
similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
|
|
2311
|
+
examples: [],
|
|
2312
|
+
validate: async (runtime, message, _state) => {
|
|
2313
|
+
try {
|
|
2314
|
+
const formService = runtime.getService("FORM");
|
|
2315
|
+
if (!formService)
|
|
2316
|
+
return false;
|
|
2317
|
+
const entityId = message.entityId;
|
|
2318
|
+
const roomId = message.roomId;
|
|
2319
|
+
if (!entityId || !roomId)
|
|
2320
|
+
return false;
|
|
2321
|
+
const session = await formService.getActiveSession(entityId, roomId);
|
|
2322
|
+
const stashed = await formService.getStashedSessions(entityId);
|
|
2323
|
+
return session !== null || stashed.length > 0;
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
logger3.error("[FormEvaluator] Validation error:", String(error));
|
|
2326
|
+
return false;
|
|
2327
|
+
}
|
|
2328
|
+
},
|
|
2329
|
+
handler: async (runtime, message, _state) => {
|
|
2330
|
+
try {
|
|
2331
|
+
const formService = runtime.getService("FORM");
|
|
2332
|
+
if (!formService)
|
|
2333
|
+
return;
|
|
2334
|
+
const entityId = message.entityId;
|
|
2335
|
+
const roomId = message.roomId;
|
|
2336
|
+
const text = message.content?.text || "";
|
|
2337
|
+
if (!entityId || !roomId)
|
|
2338
|
+
return;
|
|
2339
|
+
if (!text.trim())
|
|
2340
|
+
return;
|
|
2341
|
+
let session = await formService.getActiveSession(entityId, roomId);
|
|
2342
|
+
let intent = quickIntentDetect(text);
|
|
2343
|
+
let extractions = [];
|
|
2344
|
+
if (intent === "restore" && !session) {
|
|
2345
|
+
logger3.debug("[FormEvaluator] Restore intent detected, deferring to action");
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (!session) {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
const form = formService.getForm(session.formId);
|
|
2352
|
+
if (!form) {
|
|
2353
|
+
logger3.warn("[FormEvaluator] Form not found for session:", session.formId);
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
const templateValues = buildTemplateValues(session);
|
|
2357
|
+
if (!intent) {
|
|
2358
|
+
const result = await llmIntentAndExtract(runtime, text, form, form.controls, templateValues);
|
|
2359
|
+
intent = result.intent;
|
|
2360
|
+
extractions = result.extractions;
|
|
2361
|
+
if (form.debug) {
|
|
2362
|
+
logger3.debug("[FormEvaluator] LLM extraction result:", JSON.stringify({ intent, extractions }));
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
switch (intent) {
|
|
2366
|
+
case "submit":
|
|
2367
|
+
await handleSubmit(formService, session, entityId);
|
|
2368
|
+
break;
|
|
2369
|
+
case "stash":
|
|
2370
|
+
await formService.stash(session.id, entityId);
|
|
2371
|
+
break;
|
|
2372
|
+
case "cancel":
|
|
2373
|
+
await formService.cancel(session.id, entityId);
|
|
2374
|
+
break;
|
|
2375
|
+
case "undo":
|
|
2376
|
+
await handleUndo(formService, session, entityId, form);
|
|
2377
|
+
break;
|
|
2378
|
+
case "skip":
|
|
2379
|
+
await handleSkip(formService, session, entityId, form);
|
|
2380
|
+
break;
|
|
2381
|
+
case "autofill":
|
|
2382
|
+
await formService.applyAutofill(session);
|
|
2383
|
+
break;
|
|
2384
|
+
case "explain":
|
|
2385
|
+
case "example":
|
|
2386
|
+
case "progress":
|
|
2387
|
+
logger3.debug(`[FormEvaluator] Info intent: ${intent}`);
|
|
2388
|
+
break;
|
|
2389
|
+
case "restore":
|
|
2390
|
+
logger3.debug("[FormEvaluator] Restore intent - deferring to action");
|
|
2391
|
+
break;
|
|
2392
|
+
default:
|
|
2393
|
+
await processExtractions(runtime, formService, session, form, entityId, extractions, message.id);
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
session = await formService.getActiveSession(entityId, roomId);
|
|
2397
|
+
if (session) {
|
|
2398
|
+
session.lastMessageId = message.id;
|
|
2399
|
+
await formService.saveSession(session);
|
|
2400
|
+
}
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
logger3.error("[FormEvaluator] Handler error:", String(error));
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
extractor_default = formEvaluator;
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
// src/providers/context.ts
|
|
2412
|
+
var exports_context = {};
|
|
2413
|
+
__export(exports_context, {
|
|
2414
|
+
formContextProvider: () => formContextProvider,
|
|
2415
|
+
default: () => context_default
|
|
2416
|
+
});
|
|
2417
|
+
import { logger as logger4 } from "@elizaos/core";
|
|
2418
|
+
var formContextProvider, context_default;
|
|
2419
|
+
var init_context = __esm(() => {
|
|
2420
|
+
init_template();
|
|
2421
|
+
formContextProvider = {
|
|
2422
|
+
name: "FORM_CONTEXT",
|
|
2423
|
+
description: "Provides context about active form sessions",
|
|
2424
|
+
get: async (runtime, message, _state) => {
|
|
2425
|
+
try {
|
|
2426
|
+
const formService = runtime.getService("FORM");
|
|
2427
|
+
if (!formService) {
|
|
2428
|
+
return {
|
|
2429
|
+
data: { hasActiveForm: false },
|
|
2430
|
+
values: { formContext: "" },
|
|
2431
|
+
text: ""
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
const entityId = message.entityId;
|
|
2435
|
+
const roomId = message.roomId;
|
|
2436
|
+
if (!entityId || !roomId) {
|
|
2437
|
+
return {
|
|
2438
|
+
data: { hasActiveForm: false },
|
|
2439
|
+
values: { formContext: "" },
|
|
2440
|
+
text: ""
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
const session = await formService.getActiveSession(entityId, roomId);
|
|
2444
|
+
const stashed = await formService.getStashedSessions(entityId);
|
|
2445
|
+
if (!session && stashed.length === 0) {
|
|
2446
|
+
return {
|
|
2447
|
+
data: { hasActiveForm: false, stashedCount: 0 },
|
|
2448
|
+
values: { formContext: "" },
|
|
2449
|
+
text: ""
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
let contextText = "";
|
|
2453
|
+
let contextState;
|
|
2454
|
+
if (session) {
|
|
2455
|
+
contextState = formService.getSessionContext(session);
|
|
2456
|
+
const form = formService.getForm(session.formId);
|
|
2457
|
+
const templateValues = buildTemplateValues(session);
|
|
2458
|
+
const resolveText = (value) => renderTemplate(value, templateValues);
|
|
2459
|
+
contextState = {
|
|
2460
|
+
...contextState,
|
|
2461
|
+
filledFields: contextState.filledFields.map((field) => ({
|
|
2462
|
+
...field,
|
|
2463
|
+
label: resolveText(field.label) ?? field.label
|
|
2464
|
+
})),
|
|
2465
|
+
missingRequired: contextState.missingRequired.map((field) => ({
|
|
2466
|
+
...field,
|
|
2467
|
+
label: resolveText(field.label) ?? field.label,
|
|
2468
|
+
description: resolveText(field.description),
|
|
2469
|
+
askPrompt: resolveText(field.askPrompt)
|
|
2470
|
+
})),
|
|
2471
|
+
uncertainFields: contextState.uncertainFields.map((field) => ({
|
|
2472
|
+
...field,
|
|
2473
|
+
label: resolveText(field.label) ?? field.label
|
|
2474
|
+
})),
|
|
2475
|
+
nextField: contextState.nextField ? resolveControlTemplates(contextState.nextField, templateValues) : null
|
|
2476
|
+
};
|
|
2477
|
+
contextText = `# Active Form: ${form?.name || session.formId}
|
|
2478
|
+
|
|
2479
|
+
`;
|
|
2480
|
+
contextText += `Progress: ${contextState.progress}%
|
|
2481
|
+
|
|
2482
|
+
`;
|
|
2483
|
+
if (contextState.filledFields.length > 0) {
|
|
2484
|
+
contextText += `## Collected Information
|
|
2485
|
+
`;
|
|
2486
|
+
for (const field of contextState.filledFields) {
|
|
2487
|
+
contextText += `- ${field.label}: ${field.displayValue}
|
|
2488
|
+
`;
|
|
2489
|
+
}
|
|
2490
|
+
contextText += `
|
|
2491
|
+
`;
|
|
2492
|
+
}
|
|
2493
|
+
if (contextState.missingRequired.length > 0) {
|
|
2494
|
+
contextText += `## Still Needed
|
|
2495
|
+
`;
|
|
2496
|
+
for (const field of contextState.missingRequired) {
|
|
2497
|
+
contextText += `- ${field.label}${field.description ? ` (${field.description})` : ""}
|
|
2498
|
+
`;
|
|
2499
|
+
}
|
|
2500
|
+
contextText += `
|
|
2501
|
+
`;
|
|
2502
|
+
}
|
|
2503
|
+
if (contextState.uncertainFields.length > 0) {
|
|
2504
|
+
contextText += `## Needs Confirmation
|
|
2505
|
+
`;
|
|
2506
|
+
for (const field of contextState.uncertainFields) {
|
|
2507
|
+
contextText += `- ${field.label}: "${field.value}" (${Math.round(field.confidence * 100)}% confident)
|
|
2508
|
+
`;
|
|
2509
|
+
}
|
|
2510
|
+
contextText += `
|
|
2511
|
+
`;
|
|
2512
|
+
}
|
|
2513
|
+
if (contextState.pendingExternalFields.length > 0) {
|
|
2514
|
+
contextText += `## Waiting For External Action
|
|
2515
|
+
`;
|
|
2516
|
+
for (const field of contextState.pendingExternalFields) {
|
|
2517
|
+
const ageMs = Date.now() - field.activatedAt;
|
|
2518
|
+
const ageMin = Math.floor(ageMs / 60000);
|
|
2519
|
+
const ageText = ageMin < 1 ? "just now" : `${ageMin}m ago`;
|
|
2520
|
+
contextText += `- ${field.label}: ${field.instructions} (started ${ageText})
|
|
2521
|
+
`;
|
|
2522
|
+
if (field.address) {
|
|
2523
|
+
contextText += ` Address: ${field.address}
|
|
2524
|
+
`;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
contextText += `
|
|
2528
|
+
`;
|
|
2529
|
+
}
|
|
2530
|
+
contextText += `## Agent Guidance
|
|
2531
|
+
`;
|
|
2532
|
+
if (contextState.pendingExternalFields.length > 0) {
|
|
2533
|
+
const pending = contextState.pendingExternalFields[0];
|
|
2534
|
+
contextText += `Waiting for external action. Remind user: "${pending.instructions}"
|
|
2535
|
+
`;
|
|
2536
|
+
} else if (contextState.pendingCancelConfirmation) {
|
|
2537
|
+
contextText += `User is trying to cancel. Confirm: "You've spent time on this. Are you sure you want to cancel?"
|
|
2538
|
+
`;
|
|
2539
|
+
} else if (contextState.uncertainFields.length > 0) {
|
|
2540
|
+
const uncertain = contextState.uncertainFields[0];
|
|
2541
|
+
contextText += `Ask user to confirm: "I understood your ${uncertain.label} as '${uncertain.value}'. Is that correct?"
|
|
2542
|
+
`;
|
|
2543
|
+
} else if (contextState.nextField) {
|
|
2544
|
+
const next = contextState.nextField;
|
|
2545
|
+
const prompt = next.askPrompt || `Ask for their ${next.label}`;
|
|
2546
|
+
contextText += `Next: ${prompt}
|
|
2547
|
+
`;
|
|
2548
|
+
if (next.example) {
|
|
2549
|
+
contextText += `Example: "${next.example}"
|
|
2550
|
+
`;
|
|
2551
|
+
}
|
|
2552
|
+
} else if (contextState.status === "ready") {
|
|
2553
|
+
contextText += `All fields collected! Nudge user to submit: "I have everything I need. Ready to submit?"
|
|
2554
|
+
`;
|
|
2555
|
+
}
|
|
2556
|
+
contextText += `
|
|
2557
|
+
`;
|
|
2558
|
+
contextText += `## User Can Say
|
|
2559
|
+
`;
|
|
2560
|
+
contextText += `- Provide information for any field
|
|
2561
|
+
`;
|
|
2562
|
+
contextText += `- "undo" or "go back" to revert last change
|
|
2563
|
+
`;
|
|
2564
|
+
contextText += `- "skip" to skip optional fields
|
|
2565
|
+
`;
|
|
2566
|
+
contextText += `- "why?" to get explanation about a field
|
|
2567
|
+
`;
|
|
2568
|
+
contextText += `- "how far?" to check progress
|
|
2569
|
+
`;
|
|
2570
|
+
contextText += `- "submit" or "done" when ready
|
|
2571
|
+
`;
|
|
2572
|
+
contextText += `- "save for later" to stash the form
|
|
2573
|
+
`;
|
|
2574
|
+
contextText += `- "cancel" to abandon the form
|
|
2575
|
+
`;
|
|
2576
|
+
} else {
|
|
2577
|
+
contextState = {
|
|
2578
|
+
hasActiveForm: false,
|
|
2579
|
+
progress: 0,
|
|
2580
|
+
filledFields: [],
|
|
2581
|
+
missingRequired: [],
|
|
2582
|
+
uncertainFields: [],
|
|
2583
|
+
nextField: null,
|
|
2584
|
+
stashedCount: stashed.length,
|
|
2585
|
+
pendingExternalFields: []
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
if (stashed.length > 0) {
|
|
2589
|
+
contextText += `
|
|
2590
|
+
## Saved Forms
|
|
2591
|
+
`;
|
|
2592
|
+
contextText += `User has ${stashed.length} saved form(s). They can say "resume" or "continue" to restore one.
|
|
2593
|
+
`;
|
|
2594
|
+
for (const s of stashed) {
|
|
2595
|
+
const form = formService.getForm(s.formId);
|
|
2596
|
+
const ctx = formService.getSessionContext(s);
|
|
2597
|
+
contextText += `- ${form?.name || s.formId} (${ctx.progress}% complete)
|
|
2598
|
+
`;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return {
|
|
2602
|
+
data: JSON.parse(JSON.stringify(contextState)),
|
|
2603
|
+
values: {
|
|
2604
|
+
formContext: contextText,
|
|
2605
|
+
hasActiveForm: String(contextState.hasActiveForm),
|
|
2606
|
+
formProgress: String(contextState.progress),
|
|
2607
|
+
formStatus: contextState.status || "",
|
|
2608
|
+
stashedCount: String(stashed.length)
|
|
2609
|
+
},
|
|
2610
|
+
text: contextText
|
|
2611
|
+
};
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
logger4.error("[FormContextProvider] Error:", String(error));
|
|
2614
|
+
return {
|
|
2615
|
+
data: { hasActiveForm: false, error: true },
|
|
2616
|
+
values: { formContext: "Error loading form context." },
|
|
2617
|
+
text: "Error loading form context."
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
};
|
|
2622
|
+
context_default = formContextProvider;
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
// index.ts
|
|
2626
|
+
init_builtins();
|
|
2627
|
+
init_validation();
|
|
2628
|
+
init_storage();
|
|
2629
|
+
init_extraction();
|
|
2630
|
+
init_types();
|
|
2631
|
+
|
|
2632
|
+
// src/ttl.ts
|
|
2633
|
+
init_types();
|
|
2634
|
+
function calculateTTL(session, form) {
|
|
2635
|
+
const config = form?.ttl || {};
|
|
2636
|
+
const minDays = config.minDays ?? FORM_DEFINITION_DEFAULTS.ttl.minDays;
|
|
2637
|
+
const maxDays = config.maxDays ?? FORM_DEFINITION_DEFAULTS.ttl.maxDays;
|
|
2638
|
+
const multiplier = config.effortMultiplier ?? FORM_DEFINITION_DEFAULTS.ttl.effortMultiplier;
|
|
2639
|
+
const minutesSpent = session.effort.timeSpentMs / 60000;
|
|
2640
|
+
const effortDays = minutesSpent * multiplier;
|
|
2641
|
+
const ttlDays = Math.min(maxDays, Math.max(minDays, effortDays));
|
|
2642
|
+
return Date.now() + ttlDays * 24 * 60 * 60 * 1000;
|
|
2643
|
+
}
|
|
2644
|
+
function shouldNudge(session, form) {
|
|
2645
|
+
const nudgeConfig = form?.nudge;
|
|
2646
|
+
if (nudgeConfig?.enabled === false) {
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
const maxNudges = nudgeConfig?.maxNudges ?? FORM_DEFINITION_DEFAULTS.nudge.maxNudges;
|
|
2650
|
+
if ((session.nudgeCount || 0) >= maxNudges) {
|
|
2651
|
+
return false;
|
|
2652
|
+
}
|
|
2653
|
+
const afterInactiveHours = nudgeConfig?.afterInactiveHours ?? FORM_DEFINITION_DEFAULTS.nudge.afterInactiveHours;
|
|
2654
|
+
const inactiveMs = afterInactiveHours * 60 * 60 * 1000;
|
|
2655
|
+
const timeSinceInteraction = Date.now() - session.effort.lastInteractionAt;
|
|
2656
|
+
if (timeSinceInteraction < inactiveMs) {
|
|
2657
|
+
return false;
|
|
2658
|
+
}
|
|
2659
|
+
if (session.lastNudgeAt) {
|
|
2660
|
+
const timeSinceNudge = Date.now() - session.lastNudgeAt;
|
|
2661
|
+
if (timeSinceNudge < 24 * 60 * 60 * 1000) {
|
|
2662
|
+
return false;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
return true;
|
|
2666
|
+
}
|
|
2667
|
+
function isExpiringSoon(session, withinMs) {
|
|
2668
|
+
return session.expiresAt - Date.now() < withinMs;
|
|
2669
|
+
}
|
|
2670
|
+
function isExpired(session) {
|
|
2671
|
+
return session.expiresAt < Date.now();
|
|
2672
|
+
}
|
|
2673
|
+
function shouldConfirmCancel(session) {
|
|
2674
|
+
const minEffortMs = 5 * 60 * 1000;
|
|
2675
|
+
return session.effort.timeSpentMs > minEffortMs;
|
|
2676
|
+
}
|
|
2677
|
+
function formatTimeRemaining(session) {
|
|
2678
|
+
const remaining = session.expiresAt - Date.now();
|
|
2679
|
+
if (remaining <= 0) {
|
|
2680
|
+
return "expired";
|
|
2681
|
+
}
|
|
2682
|
+
const hours = Math.floor(remaining / (60 * 60 * 1000));
|
|
2683
|
+
const days = Math.floor(hours / 24);
|
|
2684
|
+
if (days > 0) {
|
|
2685
|
+
return `${days} day${days > 1 ? "s" : ""}`;
|
|
2686
|
+
}
|
|
2687
|
+
if (hours > 0) {
|
|
2688
|
+
return `${hours} hour${hours > 1 ? "s" : ""}`;
|
|
2689
|
+
}
|
|
2690
|
+
const minutes = Math.floor(remaining / (60 * 1000));
|
|
2691
|
+
return `${minutes} minute${minutes > 1 ? "s" : ""}`;
|
|
2692
|
+
}
|
|
2693
|
+
function formatEffort(session) {
|
|
2694
|
+
const minutes = Math.floor(session.effort.timeSpentMs / 60000);
|
|
2695
|
+
if (minutes < 1) {
|
|
2696
|
+
return "just started";
|
|
2697
|
+
}
|
|
2698
|
+
if (minutes < 60) {
|
|
2699
|
+
return `${minutes} minute${minutes > 1 ? "s" : ""}`;
|
|
2700
|
+
}
|
|
2701
|
+
const hours = Math.floor(minutes / 60);
|
|
2702
|
+
const remainingMinutes = minutes % 60;
|
|
2703
|
+
if (remainingMinutes === 0) {
|
|
2704
|
+
return `${hours} hour${hours > 1 ? "s" : ""}`;
|
|
2705
|
+
}
|
|
2706
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
2707
|
+
}
|
|
2708
|
+
// src/defaults.ts
|
|
2709
|
+
init_types();
|
|
2710
|
+
function applyControlDefaults(control) {
|
|
2711
|
+
return {
|
|
2712
|
+
key: control.key,
|
|
2713
|
+
label: control.label || prettify(control.key),
|
|
2714
|
+
type: control.type || FORM_CONTROL_DEFAULTS.type,
|
|
2715
|
+
required: control.required ?? FORM_CONTROL_DEFAULTS.required,
|
|
2716
|
+
confirmThreshold: control.confirmThreshold ?? FORM_CONTROL_DEFAULTS.confirmThreshold,
|
|
2717
|
+
...control
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
function applyFormDefaults(form) {
|
|
2721
|
+
return {
|
|
2722
|
+
id: form.id,
|
|
2723
|
+
name: form.name || prettify(form.id),
|
|
2724
|
+
version: form.version ?? FORM_DEFINITION_DEFAULTS.version,
|
|
2725
|
+
status: form.status ?? FORM_DEFINITION_DEFAULTS.status,
|
|
2726
|
+
controls: (form.controls || []).map(applyControlDefaults),
|
|
2727
|
+
ux: {
|
|
2728
|
+
allowUndo: form.ux?.allowUndo ?? FORM_DEFINITION_DEFAULTS.ux.allowUndo,
|
|
2729
|
+
allowSkip: form.ux?.allowSkip ?? FORM_DEFINITION_DEFAULTS.ux.allowSkip,
|
|
2730
|
+
maxUndoSteps: form.ux?.maxUndoSteps ?? FORM_DEFINITION_DEFAULTS.ux.maxUndoSteps,
|
|
2731
|
+
showExamples: form.ux?.showExamples ?? FORM_DEFINITION_DEFAULTS.ux.showExamples,
|
|
2732
|
+
showExplanations: form.ux?.showExplanations ?? FORM_DEFINITION_DEFAULTS.ux.showExplanations,
|
|
2733
|
+
allowAutofill: form.ux?.allowAutofill ?? FORM_DEFINITION_DEFAULTS.ux.allowAutofill
|
|
2734
|
+
},
|
|
2735
|
+
ttl: {
|
|
2736
|
+
minDays: form.ttl?.minDays ?? FORM_DEFINITION_DEFAULTS.ttl.minDays,
|
|
2737
|
+
maxDays: form.ttl?.maxDays ?? FORM_DEFINITION_DEFAULTS.ttl.maxDays,
|
|
2738
|
+
effortMultiplier: form.ttl?.effortMultiplier ?? FORM_DEFINITION_DEFAULTS.ttl.effortMultiplier
|
|
2739
|
+
},
|
|
2740
|
+
nudge: {
|
|
2741
|
+
enabled: form.nudge?.enabled ?? FORM_DEFINITION_DEFAULTS.nudge.enabled,
|
|
2742
|
+
afterInactiveHours: form.nudge?.afterInactiveHours ?? FORM_DEFINITION_DEFAULTS.nudge.afterInactiveHours,
|
|
2743
|
+
maxNudges: form.nudge?.maxNudges ?? FORM_DEFINITION_DEFAULTS.nudge.maxNudges,
|
|
2744
|
+
message: form.nudge?.message
|
|
2745
|
+
},
|
|
2746
|
+
debug: form.debug ?? FORM_DEFINITION_DEFAULTS.debug,
|
|
2747
|
+
...form
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
function prettify(key) {
|
|
2751
|
+
return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2752
|
+
}
|
|
2753
|
+
// src/builder.ts
|
|
2754
|
+
class ControlBuilder {
|
|
2755
|
+
control;
|
|
2756
|
+
constructor(key) {
|
|
2757
|
+
this.control = { key };
|
|
2758
|
+
}
|
|
2759
|
+
static field(key) {
|
|
2760
|
+
return new ControlBuilder(key);
|
|
2761
|
+
}
|
|
2762
|
+
static text(key) {
|
|
2763
|
+
return new ControlBuilder(key).type("text");
|
|
2764
|
+
}
|
|
2765
|
+
static email(key) {
|
|
2766
|
+
return new ControlBuilder(key).type("email");
|
|
2767
|
+
}
|
|
2768
|
+
static number(key) {
|
|
2769
|
+
return new ControlBuilder(key).type("number");
|
|
2770
|
+
}
|
|
2771
|
+
static boolean(key) {
|
|
2772
|
+
return new ControlBuilder(key).type("boolean");
|
|
2773
|
+
}
|
|
2774
|
+
static select(key, options) {
|
|
2775
|
+
return new ControlBuilder(key).type("select").options(options);
|
|
2776
|
+
}
|
|
2777
|
+
static date(key) {
|
|
2778
|
+
return new ControlBuilder(key).type("date");
|
|
2779
|
+
}
|
|
2780
|
+
static file(key) {
|
|
2781
|
+
return new ControlBuilder(key).type("file");
|
|
2782
|
+
}
|
|
2783
|
+
type(type) {
|
|
2784
|
+
this.control.type = type;
|
|
2785
|
+
return this;
|
|
2786
|
+
}
|
|
2787
|
+
required() {
|
|
2788
|
+
this.control.required = true;
|
|
2789
|
+
return this;
|
|
2790
|
+
}
|
|
2791
|
+
optional() {
|
|
2792
|
+
this.control.required = false;
|
|
2793
|
+
return this;
|
|
2794
|
+
}
|
|
2795
|
+
hidden() {
|
|
2796
|
+
this.control.hidden = true;
|
|
2797
|
+
return this;
|
|
2798
|
+
}
|
|
2799
|
+
sensitive() {
|
|
2800
|
+
this.control.sensitive = true;
|
|
2801
|
+
return this;
|
|
2802
|
+
}
|
|
2803
|
+
readonly() {
|
|
2804
|
+
this.control.readonly = true;
|
|
2805
|
+
return this;
|
|
2806
|
+
}
|
|
2807
|
+
multiple() {
|
|
2808
|
+
this.control.multiple = true;
|
|
2809
|
+
return this;
|
|
2810
|
+
}
|
|
2811
|
+
pattern(regex) {
|
|
2812
|
+
this.control.pattern = regex;
|
|
2813
|
+
return this;
|
|
2814
|
+
}
|
|
2815
|
+
min(n) {
|
|
2816
|
+
this.control.min = n;
|
|
2817
|
+
return this;
|
|
2818
|
+
}
|
|
2819
|
+
max(n) {
|
|
2820
|
+
this.control.max = n;
|
|
2821
|
+
return this;
|
|
2822
|
+
}
|
|
2823
|
+
minLength(n) {
|
|
2824
|
+
this.control.minLength = n;
|
|
2825
|
+
return this;
|
|
2826
|
+
}
|
|
2827
|
+
maxLength(n) {
|
|
2828
|
+
this.control.maxLength = n;
|
|
2829
|
+
return this;
|
|
2830
|
+
}
|
|
2831
|
+
enum(values) {
|
|
2832
|
+
this.control.enum = values;
|
|
2833
|
+
return this;
|
|
2834
|
+
}
|
|
2835
|
+
options(opts) {
|
|
2836
|
+
this.control.options = opts;
|
|
2837
|
+
return this;
|
|
2838
|
+
}
|
|
2839
|
+
label(label) {
|
|
2840
|
+
this.control.label = label;
|
|
2841
|
+
return this;
|
|
2842
|
+
}
|
|
2843
|
+
ask(prompt) {
|
|
2844
|
+
this.control.askPrompt = prompt;
|
|
2845
|
+
return this;
|
|
2846
|
+
}
|
|
2847
|
+
description(desc) {
|
|
2848
|
+
this.control.description = desc;
|
|
2849
|
+
return this;
|
|
2850
|
+
}
|
|
2851
|
+
hint(...hints) {
|
|
2852
|
+
this.control.extractHints = hints;
|
|
2853
|
+
return this;
|
|
2854
|
+
}
|
|
2855
|
+
example(value) {
|
|
2856
|
+
this.control.example = value;
|
|
2857
|
+
return this;
|
|
2858
|
+
}
|
|
2859
|
+
confirmThreshold(n) {
|
|
2860
|
+
this.control.confirmThreshold = n;
|
|
2861
|
+
return this;
|
|
2862
|
+
}
|
|
2863
|
+
accept(mimeTypes) {
|
|
2864
|
+
this.control.file = { ...this.control.file, accept: mimeTypes };
|
|
2865
|
+
return this;
|
|
2866
|
+
}
|
|
2867
|
+
maxSize(bytes) {
|
|
2868
|
+
this.control.file = { ...this.control.file, maxSize: bytes };
|
|
2869
|
+
return this;
|
|
2870
|
+
}
|
|
2871
|
+
maxFiles(n) {
|
|
2872
|
+
this.control.file = { ...this.control.file, maxFiles: n };
|
|
2873
|
+
return this;
|
|
2874
|
+
}
|
|
2875
|
+
roles(...roles) {
|
|
2876
|
+
this.control.roles = roles;
|
|
2877
|
+
return this;
|
|
2878
|
+
}
|
|
2879
|
+
default(value) {
|
|
2880
|
+
this.control.defaultValue = value;
|
|
2881
|
+
return this;
|
|
2882
|
+
}
|
|
2883
|
+
dependsOn(field, condition = "exists", value) {
|
|
2884
|
+
this.control.dependsOn = { field, condition, value };
|
|
2885
|
+
return this;
|
|
2886
|
+
}
|
|
2887
|
+
dbbind(columnName) {
|
|
2888
|
+
this.control.dbbind = columnName;
|
|
2889
|
+
return this;
|
|
2890
|
+
}
|
|
2891
|
+
section(name) {
|
|
2892
|
+
this.control.ui = { ...this.control.ui, section: name };
|
|
2893
|
+
return this;
|
|
2894
|
+
}
|
|
2895
|
+
order(n) {
|
|
2896
|
+
this.control.ui = { ...this.control.ui, order: n };
|
|
2897
|
+
return this;
|
|
2898
|
+
}
|
|
2899
|
+
placeholder(text) {
|
|
2900
|
+
this.control.ui = { ...this.control.ui, placeholder: text };
|
|
2901
|
+
return this;
|
|
2902
|
+
}
|
|
2903
|
+
helpText(text) {
|
|
2904
|
+
this.control.ui = { ...this.control.ui, helpText: text };
|
|
2905
|
+
return this;
|
|
2906
|
+
}
|
|
2907
|
+
widget(type) {
|
|
2908
|
+
this.control.ui = { ...this.control.ui, widget: type };
|
|
2909
|
+
return this;
|
|
2910
|
+
}
|
|
2911
|
+
i18n(locale, translations) {
|
|
2912
|
+
this.control.i18n = { ...this.control.i18n, [locale]: translations };
|
|
2913
|
+
return this;
|
|
2914
|
+
}
|
|
2915
|
+
meta(key, value) {
|
|
2916
|
+
this.control.meta = { ...this.control.meta, [key]: value };
|
|
2917
|
+
return this;
|
|
2918
|
+
}
|
|
2919
|
+
build() {
|
|
2920
|
+
const control = {
|
|
2921
|
+
key: this.control.key,
|
|
2922
|
+
label: this.control.label || prettify2(this.control.key),
|
|
2923
|
+
type: this.control.type || "text",
|
|
2924
|
+
...this.control
|
|
2925
|
+
};
|
|
2926
|
+
return control;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
class FormBuilder {
|
|
2931
|
+
form;
|
|
2932
|
+
constructor(id) {
|
|
2933
|
+
this.form = { id, controls: [] };
|
|
2934
|
+
}
|
|
2935
|
+
static create(id) {
|
|
2936
|
+
return new FormBuilder(id);
|
|
2937
|
+
}
|
|
2938
|
+
name(name) {
|
|
2939
|
+
this.form.name = name;
|
|
2940
|
+
return this;
|
|
2941
|
+
}
|
|
2942
|
+
description(desc) {
|
|
2943
|
+
this.form.description = desc;
|
|
2944
|
+
return this;
|
|
2945
|
+
}
|
|
2946
|
+
version(v) {
|
|
2947
|
+
this.form.version = v;
|
|
2948
|
+
return this;
|
|
2949
|
+
}
|
|
2950
|
+
control(builder) {
|
|
2951
|
+
const ctrl = builder instanceof ControlBuilder ? builder.build() : builder;
|
|
2952
|
+
this.form.controls?.push(ctrl);
|
|
2953
|
+
return this;
|
|
2954
|
+
}
|
|
2955
|
+
controls(...builders) {
|
|
2956
|
+
for (const builder of builders) {
|
|
2957
|
+
this.control(builder);
|
|
2958
|
+
}
|
|
2959
|
+
return this;
|
|
2960
|
+
}
|
|
2961
|
+
required(...keys) {
|
|
2962
|
+
for (const key of keys) {
|
|
2963
|
+
this.control(ControlBuilder.field(key).required());
|
|
2964
|
+
}
|
|
2965
|
+
return this;
|
|
2966
|
+
}
|
|
2967
|
+
optional(...keys) {
|
|
2968
|
+
for (const key of keys) {
|
|
2969
|
+
this.control(ControlBuilder.field(key));
|
|
2970
|
+
}
|
|
2971
|
+
return this;
|
|
2972
|
+
}
|
|
2973
|
+
roles(...roles) {
|
|
2974
|
+
this.form.roles = roles;
|
|
2975
|
+
return this;
|
|
2976
|
+
}
|
|
2977
|
+
allowMultiple() {
|
|
2978
|
+
this.form.allowMultiple = true;
|
|
2979
|
+
return this;
|
|
2980
|
+
}
|
|
2981
|
+
noUndo() {
|
|
2982
|
+
this.form.ux = { ...this.form.ux, allowUndo: false };
|
|
2983
|
+
return this;
|
|
2984
|
+
}
|
|
2985
|
+
noSkip() {
|
|
2986
|
+
this.form.ux = { ...this.form.ux, allowSkip: false };
|
|
2987
|
+
return this;
|
|
2988
|
+
}
|
|
2989
|
+
noAutofill() {
|
|
2990
|
+
this.form.ux = { ...this.form.ux, allowAutofill: false };
|
|
2991
|
+
return this;
|
|
2992
|
+
}
|
|
2993
|
+
maxUndoSteps(n) {
|
|
2994
|
+
this.form.ux = { ...this.form.ux, maxUndoSteps: n };
|
|
2995
|
+
return this;
|
|
2996
|
+
}
|
|
2997
|
+
ttl(config) {
|
|
2998
|
+
this.form.ttl = { ...this.form.ttl, ...config };
|
|
2999
|
+
return this;
|
|
3000
|
+
}
|
|
3001
|
+
noNudge() {
|
|
3002
|
+
this.form.nudge = { ...this.form.nudge, enabled: false };
|
|
3003
|
+
return this;
|
|
3004
|
+
}
|
|
3005
|
+
nudgeAfter(hours) {
|
|
3006
|
+
this.form.nudge = { ...this.form.nudge, afterInactiveHours: hours };
|
|
3007
|
+
return this;
|
|
3008
|
+
}
|
|
3009
|
+
nudgeMessage(message) {
|
|
3010
|
+
this.form.nudge = { ...this.form.nudge, message };
|
|
3011
|
+
return this;
|
|
3012
|
+
}
|
|
3013
|
+
onStart(workerName) {
|
|
3014
|
+
this.form.hooks = { ...this.form.hooks, onStart: workerName };
|
|
3015
|
+
return this;
|
|
3016
|
+
}
|
|
3017
|
+
onFieldChange(workerName) {
|
|
3018
|
+
this.form.hooks = { ...this.form.hooks, onFieldChange: workerName };
|
|
3019
|
+
return this;
|
|
3020
|
+
}
|
|
3021
|
+
onReady(workerName) {
|
|
3022
|
+
this.form.hooks = { ...this.form.hooks, onReady: workerName };
|
|
3023
|
+
return this;
|
|
3024
|
+
}
|
|
3025
|
+
onSubmit(workerName) {
|
|
3026
|
+
this.form.hooks = { ...this.form.hooks, onSubmit: workerName };
|
|
3027
|
+
return this;
|
|
3028
|
+
}
|
|
3029
|
+
onCancel(workerName) {
|
|
3030
|
+
this.form.hooks = { ...this.form.hooks, onCancel: workerName };
|
|
3031
|
+
return this;
|
|
3032
|
+
}
|
|
3033
|
+
onExpire(workerName) {
|
|
3034
|
+
this.form.hooks = { ...this.form.hooks, onExpire: workerName };
|
|
3035
|
+
return this;
|
|
3036
|
+
}
|
|
3037
|
+
hooks(hooks) {
|
|
3038
|
+
this.form.hooks = { ...this.form.hooks, ...hooks };
|
|
3039
|
+
return this;
|
|
3040
|
+
}
|
|
3041
|
+
debug() {
|
|
3042
|
+
this.form.debug = true;
|
|
3043
|
+
return this;
|
|
3044
|
+
}
|
|
3045
|
+
i18n(locale, translations) {
|
|
3046
|
+
this.form.i18n = { ...this.form.i18n, [locale]: translations };
|
|
3047
|
+
return this;
|
|
3048
|
+
}
|
|
3049
|
+
meta(key, value) {
|
|
3050
|
+
this.form.meta = { ...this.form.meta, [key]: value };
|
|
3051
|
+
return this;
|
|
3052
|
+
}
|
|
3053
|
+
build() {
|
|
3054
|
+
const form = {
|
|
3055
|
+
id: this.form.id,
|
|
3056
|
+
name: this.form.name || prettify2(this.form.id),
|
|
3057
|
+
controls: this.form.controls || [],
|
|
3058
|
+
...this.form
|
|
3059
|
+
};
|
|
3060
|
+
return form;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
var Form = FormBuilder;
|
|
3064
|
+
var C = ControlBuilder;
|
|
3065
|
+
function prettify2(key) {
|
|
3066
|
+
return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
// index.ts
|
|
3070
|
+
init_service();
|
|
3071
|
+
init_restore();
|
|
3072
|
+
init_extractor();
|
|
3073
|
+
init_context();
|
|
3074
|
+
|
|
3075
|
+
// src/tasks/nudge.ts
|
|
3076
|
+
import { logger as logger5 } from "@elizaos/core";
|
|
3077
|
+
var formNudgeWorker = {
|
|
3078
|
+
name: "form_nudge_check",
|
|
3079
|
+
validate: async (_runtime, _message, _state) => {
|
|
3080
|
+
return true;
|
|
3081
|
+
},
|
|
3082
|
+
execute: async (runtime, _options, _task) => {
|
|
3083
|
+
try {
|
|
3084
|
+
const formService = runtime.getService("FORM");
|
|
3085
|
+
if (!formService) {
|
|
3086
|
+
logger5.debug("[FormNudge] Form service not available");
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
logger5.debug("[FormNudge] Nudge check cycle completed");
|
|
3090
|
+
} catch (error) {
|
|
3091
|
+
logger5.error("[FormNudge] Error during nudge check:", String(error));
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
async function processEntityNudges(runtime, entityId) {
|
|
3096
|
+
const formService = runtime.getService("FORM");
|
|
3097
|
+
if (!formService)
|
|
3098
|
+
return;
|
|
3099
|
+
const activeSessions = await formService.getAllActiveSessions(entityId);
|
|
3100
|
+
const stashedSessions = await formService.getStashedSessions(entityId);
|
|
3101
|
+
const allSessions = [...activeSessions, ...stashedSessions];
|
|
3102
|
+
const now = Date.now();
|
|
3103
|
+
const expirationWarningMs = 24 * 60 * 60 * 1000;
|
|
3104
|
+
for (const session of allSessions) {
|
|
3105
|
+
const form = formService.getForm(session.formId);
|
|
3106
|
+
if (session.expiresAt < now) {
|
|
3107
|
+
session.status = "expired";
|
|
3108
|
+
await formService.saveSession(session);
|
|
3109
|
+
if (form?.hooks?.onExpire) {
|
|
3110
|
+
const worker = runtime.getTaskWorker(form.hooks.onExpire);
|
|
3111
|
+
if (worker) {
|
|
3112
|
+
try {
|
|
3113
|
+
await worker.execute(runtime, { session, form }, {});
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
logger5.error("[FormNudge] onExpire hook failed:", String(error));
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
logger5.debug(`[FormNudge] Session ${session.id} expired`);
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
if (isExpiringSoon(session, expirationWarningMs) && !session.expirationWarned) {
|
|
3123
|
+
await sendExpirationWarning(runtime, session, form);
|
|
3124
|
+
session.expirationWarned = true;
|
|
3125
|
+
await formService.saveSession(session);
|
|
3126
|
+
continue;
|
|
3127
|
+
}
|
|
3128
|
+
if (session.status === "stashed" && shouldNudge(session, form)) {
|
|
3129
|
+
await sendNudge(runtime, session, form);
|
|
3130
|
+
session.nudgeCount = (session.nudgeCount || 0) + 1;
|
|
3131
|
+
session.lastNudgeAt = now;
|
|
3132
|
+
await formService.saveSession(session);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
async function sendNudge(runtime, session, form) {
|
|
3137
|
+
const message = form?.nudge?.message || `You have an unfinished "${form?.name || "form"}". Would you like to continue?`;
|
|
3138
|
+
try {
|
|
3139
|
+
if (typeof runtime.sendMessageToRoom === "function") {
|
|
3140
|
+
await runtime.sendMessageToRoom(session.roomId, {
|
|
3141
|
+
text: message
|
|
3142
|
+
});
|
|
3143
|
+
logger5.debug(`[FormNudge] Sent nudge for session in room ${session.roomId}`);
|
|
3144
|
+
}
|
|
3145
|
+
} catch (error) {
|
|
3146
|
+
logger5.error("[FormNudge] Failed to send nudge:", String(error));
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
async function sendExpirationWarning(runtime, session, form) {
|
|
3150
|
+
const remaining = formatTimeRemaining(session);
|
|
3151
|
+
const message = `Your "${form?.name || "form"}" form will expire in ${remaining}. Say "resume" to keep working on it.`;
|
|
3152
|
+
try {
|
|
3153
|
+
if (typeof runtime.sendMessageToRoom === "function") {
|
|
3154
|
+
await runtime.sendMessageToRoom(session.roomId, {
|
|
3155
|
+
text: message
|
|
3156
|
+
});
|
|
3157
|
+
logger5.debug(`[FormNudge] Sent expiration warning for session in room ${session.roomId}`);
|
|
3158
|
+
}
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
logger5.error("[FormNudge] Failed to send expiration warning:", String(error));
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
// index.ts
|
|
3165
|
+
var formPlugin = {
|
|
3166
|
+
name: "form",
|
|
3167
|
+
description: "Agent-native conversational forms for data collection",
|
|
3168
|
+
services: [
|
|
3169
|
+
{
|
|
3170
|
+
serviceType: "FORM",
|
|
3171
|
+
start: async (runtime) => {
|
|
3172
|
+
const { FormService: FormService2 } = await Promise.resolve().then(() => (init_service(), exports_service));
|
|
3173
|
+
return FormService2.start(runtime);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
],
|
|
3177
|
+
providers: [
|
|
3178
|
+
{
|
|
3179
|
+
name: "FORM_CONTEXT",
|
|
3180
|
+
description: "Provides context about active form sessions",
|
|
3181
|
+
get: async (runtime, message, state) => {
|
|
3182
|
+
const { formContextProvider: formContextProvider2 } = await Promise.resolve().then(() => (init_context(), exports_context));
|
|
3183
|
+
return formContextProvider2.get(runtime, message, state);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
],
|
|
3187
|
+
evaluators: [
|
|
3188
|
+
{
|
|
3189
|
+
name: "form_evaluator",
|
|
3190
|
+
description: "Extracts form fields and handles form intents",
|
|
3191
|
+
similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
|
|
3192
|
+
examples: [],
|
|
3193
|
+
validate: async (runtime, message, state) => {
|
|
3194
|
+
const { formEvaluator: formEvaluator2 } = await Promise.resolve().then(() => (init_extractor(), exports_extractor));
|
|
3195
|
+
return formEvaluator2.validate(runtime, message, state);
|
|
3196
|
+
},
|
|
3197
|
+
handler: async (runtime, message, state) => {
|
|
3198
|
+
const { formEvaluator: formEvaluator2 } = await Promise.resolve().then(() => (init_extractor(), exports_extractor));
|
|
3199
|
+
return formEvaluator2.handler(runtime, message, state);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
],
|
|
3203
|
+
actions: [
|
|
3204
|
+
{
|
|
3205
|
+
name: "FORM_RESTORE",
|
|
3206
|
+
similes: ["RESUME_FORM", "CONTINUE_FORM"],
|
|
3207
|
+
description: "Restore a previously stashed form session",
|
|
3208
|
+
validate: async (runtime, message, state) => {
|
|
3209
|
+
const { formRestoreAction: formRestoreAction2 } = await Promise.resolve().then(() => (init_restore(), exports_restore));
|
|
3210
|
+
return formRestoreAction2.validate(runtime, message, state);
|
|
3211
|
+
},
|
|
3212
|
+
handler: async (runtime, message, state, options, callback) => {
|
|
3213
|
+
const { formRestoreAction: formRestoreAction2 } = await Promise.resolve().then(() => (init_restore(), exports_restore));
|
|
3214
|
+
return formRestoreAction2.handler(runtime, message, state, options, callback);
|
|
3215
|
+
},
|
|
3216
|
+
examples: [
|
|
3217
|
+
[
|
|
3218
|
+
{
|
|
3219
|
+
name: "{{user1}}",
|
|
3220
|
+
content: { text: "Resume my form" }
|
|
3221
|
+
},
|
|
3222
|
+
{
|
|
3223
|
+
name: "{{agentName}}",
|
|
3224
|
+
content: {
|
|
3225
|
+
text: "I've restored your form. Let's continue where you left off."
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
]
|
|
3229
|
+
]
|
|
3230
|
+
}
|
|
3231
|
+
]
|
|
3232
|
+
};
|
|
3233
|
+
var typescript_default = formPlugin;
|
|
3234
|
+
export {
|
|
3235
|
+
validateField,
|
|
3236
|
+
shouldNudge,
|
|
3237
|
+
shouldConfirmCancel,
|
|
3238
|
+
saveSubmission,
|
|
3239
|
+
saveSession,
|
|
3240
|
+
saveAutofillData,
|
|
3241
|
+
registerTypeHandler,
|
|
3242
|
+
registerBuiltinTypes,
|
|
3243
|
+
quickIntentDetect,
|
|
3244
|
+
processEntityNudges,
|
|
3245
|
+
prettify,
|
|
3246
|
+
parseValue,
|
|
3247
|
+
matchesMimeType,
|
|
3248
|
+
llmIntentAndExtract,
|
|
3249
|
+
isUXIntent,
|
|
3250
|
+
isLifecycleIntent,
|
|
3251
|
+
isExpiringSoon,
|
|
3252
|
+
isExpired,
|
|
3253
|
+
isBuiltinType,
|
|
3254
|
+
hasDataToExtract,
|
|
3255
|
+
getTypeHandler,
|
|
3256
|
+
getSubmissions,
|
|
3257
|
+
getStashedSessions,
|
|
3258
|
+
getBuiltinType,
|
|
3259
|
+
getAutofillData,
|
|
3260
|
+
getAllActiveSessions,
|
|
3261
|
+
getActiveSession,
|
|
3262
|
+
formatValue,
|
|
3263
|
+
formatTimeRemaining,
|
|
3264
|
+
formatEffort,
|
|
3265
|
+
formRestoreAction,
|
|
3266
|
+
formPlugin,
|
|
3267
|
+
formNudgeWorker,
|
|
3268
|
+
formEvaluator,
|
|
3269
|
+
formContextProvider,
|
|
3270
|
+
extractSingleField,
|
|
3271
|
+
detectCorrection,
|
|
3272
|
+
deleteSession,
|
|
3273
|
+
typescript_default as default,
|
|
3274
|
+
clearTypeHandlers,
|
|
3275
|
+
calculateTTL,
|
|
3276
|
+
applyFormDefaults,
|
|
3277
|
+
applyControlDefaults,
|
|
3278
|
+
FormService,
|
|
3279
|
+
FormBuilder,
|
|
3280
|
+
Form,
|
|
3281
|
+
FORM_SUBMISSION_COMPONENT,
|
|
3282
|
+
FORM_SESSION_COMPONENT,
|
|
3283
|
+
FORM_DEFINITION_DEFAULTS,
|
|
3284
|
+
FORM_CONTROL_DEFAULTS,
|
|
3285
|
+
FORM_AUTOFILL_COMPONENT,
|
|
3286
|
+
ControlBuilder,
|
|
3287
|
+
C,
|
|
3288
|
+
BUILTIN_TYPE_MAP,
|
|
3289
|
+
BUILTIN_TYPES
|
|
3290
|
+
};
|
|
3291
|
+
|
|
3292
|
+
//# debugId=D9CCEBB86B648A7B64756E2164756E21
|
|
3293
|
+
//# sourceMappingURL=index.js.map
|