@elizaos/plugin-form 2.0.0-alpha.1 → 2.0.0-alpha.11
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/chunk-4B5QLNVA.js +187 -0
- package/dist/chunk-ARWZY3NX.js +284 -0
- package/dist/chunk-R4VBS2YK.js +1597 -0
- package/dist/chunk-TBCL2ILB.js +172 -0
- package/dist/chunk-WY4WK3HD.js +57 -0
- package/dist/chunk-XHECCAUT.js +544 -0
- package/dist/chunk-YTWANJ3R.js +64 -0
- package/dist/context-MHPFYZZ2.js +9 -0
- package/dist/extractor-UWASKXKD.js +11 -0
- package/dist/index.d.ts +3057 -19
- package/dist/index.js +428 -2826
- package/dist/restore-S7JLME4H.js +9 -0
- package/dist/service-TCCXKV3T.js +7 -0
- package/package.json +33 -23
- package/LICENSE +0 -21
- package/README.md +0 -846
- package/dist/actions/restore.d.ts +0 -62
- package/dist/actions/restore.d.ts.map +0 -1
- package/dist/builder.d.ts +0 -320
- package/dist/builder.d.ts.map +0 -1
- package/dist/builtins.d.ts +0 -128
- package/dist/builtins.d.ts.map +0 -1
- package/dist/defaults.d.ts +0 -95
- package/dist/defaults.d.ts.map +0 -1
- package/dist/evaluators/extractor.d.ts +0 -91
- package/dist/evaluators/extractor.d.ts.map +0 -1
- package/dist/extraction.d.ts +0 -105
- package/dist/extraction.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -30
- package/dist/intent.d.ts +0 -116
- package/dist/intent.d.ts.map +0 -1
- package/dist/providers/context.d.ts +0 -69
- package/dist/providers/context.d.ts.map +0 -1
- package/dist/service.d.ts +0 -417
- package/dist/service.d.ts.map +0 -1
- package/dist/storage.d.ts +0 -228
- package/dist/storage.d.ts.map +0 -1
- package/dist/tasks/nudge.d.ts +0 -89
- package/dist/tasks/nudge.d.ts.map +0 -1
- package/dist/template.d.ts +0 -10
- package/dist/template.d.ts.map +0 -1
- package/dist/ttl.d.ts +0 -144
- package/dist/ttl.d.ts.map +0 -1
- package/dist/types.d.ts +0 -1214
- package/dist/types.d.ts.map +0 -1
- package/dist/validation.d.ts +0 -156
- package/dist/validation.d.ts.map +0 -1
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTypeHandler,
|
|
3
|
+
parseValue,
|
|
4
|
+
validateField
|
|
5
|
+
} from "./chunk-ARWZY3NX.js";
|
|
6
|
+
import {
|
|
7
|
+
quickIntentDetect
|
|
8
|
+
} from "./chunk-YTWANJ3R.js";
|
|
9
|
+
import {
|
|
10
|
+
buildTemplateValues,
|
|
11
|
+
resolveControlTemplates
|
|
12
|
+
} from "./chunk-WY4WK3HD.js";
|
|
13
|
+
|
|
14
|
+
// src/evaluators/extractor.ts
|
|
15
|
+
import { logger } from "@elizaos/core";
|
|
16
|
+
|
|
17
|
+
// src/extraction.ts
|
|
18
|
+
import { ModelType, parseKeyValueXml } from "@elizaos/core";
|
|
19
|
+
async function llmIntentAndExtract(runtime, text, form, controls, templateValues) {
|
|
20
|
+
const resolvedControls = templateValues ? controls.map((control) => resolveControlTemplates(control, templateValues)) : controls;
|
|
21
|
+
const fieldsDescription = resolvedControls.filter((c) => !c.hidden).map((c) => {
|
|
22
|
+
const handler = getTypeHandler(c.type);
|
|
23
|
+
const typeHint = handler?.extractionPrompt || c.type;
|
|
24
|
+
const hints = c.extractHints?.join(", ") || "";
|
|
25
|
+
const options = c.options?.map((o) => o.value).join(", ") || "";
|
|
26
|
+
return `- ${c.key} (${c.label}): ${c.description || typeHint}${hints ? ` [hints: ${hints}]` : ""}${options ? ` [options: ${options}]` : ""}`;
|
|
27
|
+
}).join("\n");
|
|
28
|
+
const prompt = `You are extracting structured data from a user's natural language message.
|
|
29
|
+
|
|
30
|
+
FORM: ${form.name}
|
|
31
|
+
${form.description ? `DESCRIPTION: ${form.description}` : ""}
|
|
32
|
+
|
|
33
|
+
FIELDS TO EXTRACT:
|
|
34
|
+
${fieldsDescription}
|
|
35
|
+
|
|
36
|
+
USER MESSAGE:
|
|
37
|
+
"${text}"
|
|
38
|
+
|
|
39
|
+
INSTRUCTIONS:
|
|
40
|
+
1. Determine the user's intent:
|
|
41
|
+
- fill_form: They are providing information for form fields
|
|
42
|
+
- submit: They want to submit/complete the form ("done", "submit", "finish", "that's all")
|
|
43
|
+
- stash: They want to save for later ("save for later", "pause", "hold on")
|
|
44
|
+
- restore: They want to resume a saved form ("resume", "continue", "pick up where")
|
|
45
|
+
- cancel: They want to cancel ("cancel", "abort", "nevermind", "forget it")
|
|
46
|
+
- undo: They want to undo last change ("undo", "go back", "wait no")
|
|
47
|
+
- skip: They want to skip current field ("skip", "pass", "don't know")
|
|
48
|
+
- explain: They want explanation ("why?", "what's that for?")
|
|
49
|
+
- example: They want an example ("example?", "like what?")
|
|
50
|
+
- progress: They want progress update ("how far?", "status")
|
|
51
|
+
- autofill: They want to use saved values ("same as last time")
|
|
52
|
+
- other: None of the above
|
|
53
|
+
|
|
54
|
+
2. For fill_form intent, extract all field values mentioned.
|
|
55
|
+
- For each extracted value, provide a confidence score (0.0-1.0)
|
|
56
|
+
- Note if this appears to be a correction to a previous value
|
|
57
|
+
|
|
58
|
+
Respond using TOON like this:
|
|
59
|
+
intent: fill_form, submit, stash, restore, cancel, undo, skip, explain, example, progress, autofill, or other
|
|
60
|
+
extractions[N]{key,value,confidence,reasoning,is_correction}:
|
|
61
|
+
field_key,extracted_value,0.9,why this value,false
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
intent: fill_form
|
|
65
|
+
extractions[2]{key,value,confidence,reasoning,is_correction}:
|
|
66
|
+
name,Jane Doe,0.95,User said their name is Jane,false
|
|
67
|
+
email,jane@example.com,0.9,Email mentioned in message,false
|
|
68
|
+
|
|
69
|
+
IMPORTANT: Your response must ONLY contain the TOON document above. No preamble or explanation.`;
|
|
70
|
+
try {
|
|
71
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
72
|
+
prompt,
|
|
73
|
+
temperature: 0.1
|
|
74
|
+
});
|
|
75
|
+
const parsed = parseExtractionResponse(response);
|
|
76
|
+
for (const extraction of parsed.extractions) {
|
|
77
|
+
const control = resolvedControls.find((c) => c.key === extraction.field);
|
|
78
|
+
if (control) {
|
|
79
|
+
if (typeof extraction.value === "string") {
|
|
80
|
+
extraction.value = parseValue(extraction.value, control);
|
|
81
|
+
}
|
|
82
|
+
const validation = validateField(extraction.value, control);
|
|
83
|
+
if (!validation.valid) {
|
|
84
|
+
extraction.confidence = Math.min(extraction.confidence, 0.3);
|
|
85
|
+
extraction.reasoning = `${extraction.reasoning || ""} (Validation failed: ${validation.error})`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (form.debug) {
|
|
90
|
+
runtime.logger.debug("[FormExtraction] LLM extraction result:", JSON.stringify(parsed));
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
runtime.logger.error("[FormExtraction] LLM extraction failed:", String(error));
|
|
95
|
+
return { intent: "other", extractions: [] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function parseExtractionResponse(response) {
|
|
99
|
+
const result = {
|
|
100
|
+
intent: "other",
|
|
101
|
+
extractions: []
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
const parsed = parseKeyValueXml(response);
|
|
105
|
+
if (parsed) {
|
|
106
|
+
const intentStr = parsed.intent?.toLowerCase() ?? "other";
|
|
107
|
+
result.intent = isValidIntent(intentStr) ? intentStr : "other";
|
|
108
|
+
if (parsed.extractions) {
|
|
109
|
+
const fields = Array.isArray(parsed.extractions) ? parsed.extractions : parsed.extractions.field ? Array.isArray(parsed.extractions.field) ? parsed.extractions.field : [parsed.extractions.field] : [];
|
|
110
|
+
for (const field of fields) {
|
|
111
|
+
if (field?.key) {
|
|
112
|
+
const extraction = {
|
|
113
|
+
field: String(field.key),
|
|
114
|
+
value: field.value ?? null,
|
|
115
|
+
confidence: parseFloat(String(field.confidence ?? "")) || 0.5,
|
|
116
|
+
reasoning: field.reasoning ? String(field.reasoning) : void 0,
|
|
117
|
+
isCorrection: field.is_correction === "true" || field.is_correction === true
|
|
118
|
+
};
|
|
119
|
+
result.extractions.push(extraction);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (_error) {
|
|
125
|
+
const intentMatch = response.match(/<intent>([^<]+)<\/intent>/);
|
|
126
|
+
if (intentMatch) {
|
|
127
|
+
const intentStr = intentMatch[1].toLowerCase().trim();
|
|
128
|
+
result.intent = isValidIntent(intentStr) ? intentStr : "other";
|
|
129
|
+
}
|
|
130
|
+
const fieldMatches = response.matchAll(
|
|
131
|
+
/<field>\s*<key>([^<]+)<\/key>\s*<value>([^<]*)<\/value>\s*<confidence>([^<]+)<\/confidence>/g
|
|
132
|
+
);
|
|
133
|
+
for (const match of fieldMatches) {
|
|
134
|
+
result.extractions.push({
|
|
135
|
+
field: match[1].trim(),
|
|
136
|
+
value: match[2].trim(),
|
|
137
|
+
confidence: parseFloat(match[3]) || 0.5
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
function isValidIntent(str) {
|
|
144
|
+
const validIntents = [
|
|
145
|
+
"fill_form",
|
|
146
|
+
"submit",
|
|
147
|
+
"stash",
|
|
148
|
+
"restore",
|
|
149
|
+
"cancel",
|
|
150
|
+
"undo",
|
|
151
|
+
"skip",
|
|
152
|
+
"explain",
|
|
153
|
+
"example",
|
|
154
|
+
"progress",
|
|
155
|
+
"autofill",
|
|
156
|
+
"other"
|
|
157
|
+
];
|
|
158
|
+
return validIntents.includes(str);
|
|
159
|
+
}
|
|
160
|
+
async function extractSingleField(runtime, text, control, debug, templateValues) {
|
|
161
|
+
const resolvedControl = templateValues ? resolveControlTemplates(control, templateValues) : control;
|
|
162
|
+
const handler = getTypeHandler(resolvedControl.type);
|
|
163
|
+
const typeHint = handler?.extractionPrompt || resolvedControl.type;
|
|
164
|
+
const prompt = `Extract the ${resolvedControl.label} (${typeHint}) from this message:
|
|
165
|
+
|
|
166
|
+
"${text}"
|
|
167
|
+
|
|
168
|
+
${resolvedControl.description ? `Context: ${resolvedControl.description}` : ""}
|
|
169
|
+
${resolvedControl.extractHints?.length ? `Look for: ${resolvedControl.extractHints.join(", ")}` : ""}
|
|
170
|
+
${resolvedControl.options?.length ? `Valid options: ${resolvedControl.options.map((o) => o.value).join(", ")}` : ""}
|
|
171
|
+
${resolvedControl.example ? `Example: ${resolvedControl.example}` : ""}
|
|
172
|
+
|
|
173
|
+
Respond using TOON like this:
|
|
174
|
+
found: true or false
|
|
175
|
+
value: extracted value or empty if not found
|
|
176
|
+
confidence: 0.0 to 1.0
|
|
177
|
+
reasoning: brief explanation
|
|
178
|
+
|
|
179
|
+
IMPORTANT: Your response must ONLY contain the TOON document above. No preamble or explanation.`;
|
|
180
|
+
try {
|
|
181
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
182
|
+
prompt,
|
|
183
|
+
temperature: 0.1
|
|
184
|
+
});
|
|
185
|
+
const parsed = parseKeyValueXml(response);
|
|
186
|
+
const found = parsed?.found === true || parsed?.found === "true";
|
|
187
|
+
if (found) {
|
|
188
|
+
let value = parsed.value;
|
|
189
|
+
if (typeof value === "string") {
|
|
190
|
+
value = parseValue(value, resolvedControl);
|
|
191
|
+
}
|
|
192
|
+
const confidence = typeof parsed?.confidence === "number" ? parsed.confidence : parseFloat(String(parsed?.confidence ?? ""));
|
|
193
|
+
const result = {
|
|
194
|
+
field: resolvedControl.key,
|
|
195
|
+
value: value ?? null,
|
|
196
|
+
confidence: Number.isFinite(confidence) ? confidence : 0.5,
|
|
197
|
+
reasoning: parsed.reasoning ? String(parsed.reasoning) : void 0
|
|
198
|
+
};
|
|
199
|
+
if (debug) {
|
|
200
|
+
runtime.logger.debug("[FormExtraction] Single field extraction:", JSON.stringify(result));
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
runtime.logger.error("[FormExtraction] Single field extraction failed:", String(error));
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function detectCorrection(runtime, text, currentValues, controls, templateValues) {
|
|
211
|
+
const resolvedControls = templateValues ? controls.map((control) => resolveControlTemplates(control, templateValues)) : controls;
|
|
212
|
+
const currentValuesStr = resolvedControls.filter((c) => currentValues[c.key] !== void 0).map((c) => `- ${c.label}: ${currentValues[c.key]}`).join("\n");
|
|
213
|
+
if (!currentValuesStr) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
const prompt = `Is the user correcting any of these previously provided values?
|
|
217
|
+
|
|
218
|
+
Current values:
|
|
219
|
+
${currentValuesStr}
|
|
220
|
+
|
|
221
|
+
User message:
|
|
222
|
+
"${text}"
|
|
223
|
+
|
|
224
|
+
If they are correcting a value, extract the new value. Otherwise respond with no corrections.
|
|
225
|
+
|
|
226
|
+
Respond using TOON like this:
|
|
227
|
+
has_correction: true or false
|
|
228
|
+
corrections[N]{field,old_value,new_value,confidence}:
|
|
229
|
+
field_label,previous value,corrected value,0.9
|
|
230
|
+
|
|
231
|
+
If no corrections:
|
|
232
|
+
has_correction: false
|
|
233
|
+
|
|
234
|
+
IMPORTANT: Your response must ONLY contain the TOON document above. No preamble or explanation.`;
|
|
235
|
+
try {
|
|
236
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
237
|
+
prompt,
|
|
238
|
+
temperature: 0.1
|
|
239
|
+
});
|
|
240
|
+
const parsed = parseKeyValueXml(response);
|
|
241
|
+
const hasCorrection = parsed?.has_correction === true || parsed?.has_correction === "true";
|
|
242
|
+
if (parsed && hasCorrection && parsed.corrections) {
|
|
243
|
+
const corrections = [];
|
|
244
|
+
const correctionList = Array.isArray(parsed.corrections) ? parsed.corrections : parsed.corrections.correction ? Array.isArray(parsed.corrections.correction) ? parsed.corrections.correction : [parsed.corrections.correction] : [];
|
|
245
|
+
for (const correction of correctionList) {
|
|
246
|
+
const fieldName = correction.field ? String(correction.field) : "";
|
|
247
|
+
const control = resolvedControls.find(
|
|
248
|
+
(c) => c.label.toLowerCase() === fieldName.toLowerCase() || c.key.toLowerCase() === fieldName.toLowerCase()
|
|
249
|
+
);
|
|
250
|
+
if (control) {
|
|
251
|
+
let value = correction.new_value;
|
|
252
|
+
if (typeof value === "string") {
|
|
253
|
+
value = parseValue(value, control);
|
|
254
|
+
}
|
|
255
|
+
const confidence = typeof correction.confidence === "number" ? correction.confidence : parseFloat(String(correction.confidence ?? ""));
|
|
256
|
+
const extraction = {
|
|
257
|
+
field: control.key,
|
|
258
|
+
value: value ?? null,
|
|
259
|
+
confidence: Number.isFinite(confidence) ? confidence : 0.8,
|
|
260
|
+
isCorrection: true
|
|
261
|
+
};
|
|
262
|
+
corrections.push(extraction);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return corrections;
|
|
266
|
+
}
|
|
267
|
+
return [];
|
|
268
|
+
} catch (error) {
|
|
269
|
+
runtime.logger.error("[FormExtraction] Correction detection failed:", String(error));
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/evaluators/extractor.ts
|
|
275
|
+
var formEvaluator = {
|
|
276
|
+
name: "form_evaluator",
|
|
277
|
+
description: "Extracts form fields and handles form intents from user messages",
|
|
278
|
+
similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
|
|
279
|
+
examples: [],
|
|
280
|
+
// No examples needed for evaluators
|
|
281
|
+
/**
|
|
282
|
+
* Validate: Should this evaluator run?
|
|
283
|
+
*
|
|
284
|
+
* Only runs if there's an active form session OR stashed sessions
|
|
285
|
+
* (to handle restore intent).
|
|
286
|
+
*
|
|
287
|
+
* WHY check stashed:
|
|
288
|
+
* - User might say "resume my form"
|
|
289
|
+
* - Need to detect restore intent even without active session
|
|
290
|
+
* - Note: Actual restore is handled by FORM_RESTORE action
|
|
291
|
+
*
|
|
292
|
+
* @returns true if evaluator should run
|
|
293
|
+
*/
|
|
294
|
+
validate: async (runtime, message, _state) => {
|
|
295
|
+
try {
|
|
296
|
+
const formService = runtime.getService("FORM");
|
|
297
|
+
if (!formService) return false;
|
|
298
|
+
const entityId = message.entityId;
|
|
299
|
+
const roomId = message.roomId;
|
|
300
|
+
if (!entityId || !roomId) return false;
|
|
301
|
+
const session = await formService.getActiveSession(entityId, roomId);
|
|
302
|
+
const stashed = await formService.getStashedSessions(entityId);
|
|
303
|
+
return session !== null || stashed.length > 0;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
logger.error("[FormEvaluator] Validation error:", String(error));
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
/**
|
|
310
|
+
* Handler: Process the message and update form state.
|
|
311
|
+
*
|
|
312
|
+
* This is the main logic loop:
|
|
313
|
+
* 1. Try fast-path intent detection
|
|
314
|
+
* 2. Fall back to LLM if no match
|
|
315
|
+
* 3. Handle the detected intent
|
|
316
|
+
* 4. Process any field extractions
|
|
317
|
+
*
|
|
318
|
+
* @param runtime - Agent runtime for service access
|
|
319
|
+
* @param message - The user message to process
|
|
320
|
+
* @param state - Current agent state (optional)
|
|
321
|
+
*/
|
|
322
|
+
handler: async (runtime, message, _state) => {
|
|
323
|
+
try {
|
|
324
|
+
const formService = runtime.getService("FORM");
|
|
325
|
+
if (!formService) return void 0;
|
|
326
|
+
const entityId = message.entityId;
|
|
327
|
+
const roomId = message.roomId;
|
|
328
|
+
const text = message.content?.text || "";
|
|
329
|
+
if (!entityId || !roomId) return void 0;
|
|
330
|
+
if (!text.trim()) return void 0;
|
|
331
|
+
let session = await formService.getActiveSession(entityId, roomId);
|
|
332
|
+
let intent = quickIntentDetect(text);
|
|
333
|
+
let extractions = [];
|
|
334
|
+
if (intent === "restore" && !session) {
|
|
335
|
+
logger.debug("[FormEvaluator] Restore intent detected, deferring to action");
|
|
336
|
+
return void 0;
|
|
337
|
+
}
|
|
338
|
+
if (!session) {
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
const form = formService.getForm(session.formId);
|
|
342
|
+
if (!form) {
|
|
343
|
+
logger.warn("[FormEvaluator] Form not found for session:", session.formId);
|
|
344
|
+
return void 0;
|
|
345
|
+
}
|
|
346
|
+
const templateValues = buildTemplateValues(session);
|
|
347
|
+
if (!intent) {
|
|
348
|
+
const result = await llmIntentAndExtract(
|
|
349
|
+
runtime,
|
|
350
|
+
text,
|
|
351
|
+
form,
|
|
352
|
+
form.controls,
|
|
353
|
+
templateValues
|
|
354
|
+
);
|
|
355
|
+
intent = result.intent;
|
|
356
|
+
extractions = result.extractions;
|
|
357
|
+
if (form.debug) {
|
|
358
|
+
logger.debug(
|
|
359
|
+
"[FormEvaluator] LLM extraction result:",
|
|
360
|
+
JSON.stringify({ intent, extractions })
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
switch (intent) {
|
|
365
|
+
// --- Lifecycle Intents ---
|
|
366
|
+
case "submit":
|
|
367
|
+
await handleSubmit(formService, session, entityId);
|
|
368
|
+
break;
|
|
369
|
+
case "stash":
|
|
370
|
+
await formService.stash(session.id, entityId);
|
|
371
|
+
break;
|
|
372
|
+
case "cancel":
|
|
373
|
+
await formService.cancel(session.id, entityId);
|
|
374
|
+
break;
|
|
375
|
+
// --- UX Intents ---
|
|
376
|
+
case "undo":
|
|
377
|
+
await handleUndo(formService, session, entityId, form);
|
|
378
|
+
break;
|
|
379
|
+
case "skip":
|
|
380
|
+
await handleSkip(formService, session, entityId, form);
|
|
381
|
+
break;
|
|
382
|
+
case "autofill":
|
|
383
|
+
await formService.applyAutofill(session);
|
|
384
|
+
break;
|
|
385
|
+
// --- Info Intents ---
|
|
386
|
+
// These don't change state - the provider gives context for response
|
|
387
|
+
case "explain":
|
|
388
|
+
case "example":
|
|
389
|
+
case "progress":
|
|
390
|
+
logger.debug(`[FormEvaluator] Info intent: ${intent}`);
|
|
391
|
+
break;
|
|
392
|
+
// --- Special Cases ---
|
|
393
|
+
case "restore":
|
|
394
|
+
logger.debug("[FormEvaluator] Restore intent - deferring to action");
|
|
395
|
+
break;
|
|
396
|
+
default:
|
|
397
|
+
await processExtractions(
|
|
398
|
+
runtime,
|
|
399
|
+
formService,
|
|
400
|
+
session,
|
|
401
|
+
form,
|
|
402
|
+
entityId,
|
|
403
|
+
extractions,
|
|
404
|
+
message.id
|
|
405
|
+
);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
session = await formService.getActiveSession(entityId, roomId);
|
|
409
|
+
if (session) {
|
|
410
|
+
session.lastMessageId = message.id;
|
|
411
|
+
await formService.saveSession(session);
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
logger.error("[FormEvaluator] Handler error:", String(error));
|
|
415
|
+
return void 0;
|
|
416
|
+
}
|
|
417
|
+
return void 0;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
async function processExtractions(runtime, formService, session, form, entityId, extractions, messageId) {
|
|
421
|
+
const updatedParents = /* @__PURE__ */ new Set();
|
|
422
|
+
for (const extraction of extractions) {
|
|
423
|
+
if (extraction.field.includes(".")) {
|
|
424
|
+
const [parentKey, subKey] = extraction.field.split(".");
|
|
425
|
+
await formService.updateSubField(
|
|
426
|
+
session.id,
|
|
427
|
+
entityId,
|
|
428
|
+
parentKey,
|
|
429
|
+
subKey,
|
|
430
|
+
extraction.value,
|
|
431
|
+
extraction.confidence,
|
|
432
|
+
messageId
|
|
433
|
+
);
|
|
434
|
+
await emitEvent(runtime, "FORM_SUBFIELD_UPDATED", {
|
|
435
|
+
sessionId: session.id,
|
|
436
|
+
parentField: parentKey,
|
|
437
|
+
subField: subKey,
|
|
438
|
+
value: extraction.value,
|
|
439
|
+
confidence: extraction.confidence
|
|
440
|
+
});
|
|
441
|
+
updatedParents.add(parentKey);
|
|
442
|
+
if (form.debug) {
|
|
443
|
+
logger.debug(`[FormEvaluator] Updated subfield ${parentKey}.${subKey}`);
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
await formService.updateField(
|
|
447
|
+
session.id,
|
|
448
|
+
entityId,
|
|
449
|
+
extraction.field,
|
|
450
|
+
extraction.value,
|
|
451
|
+
extraction.confidence,
|
|
452
|
+
extraction.isCorrection ? "correction" : "extraction",
|
|
453
|
+
messageId
|
|
454
|
+
);
|
|
455
|
+
await emitEvent(runtime, "FORM_FIELD_EXTRACTED", {
|
|
456
|
+
sessionId: session.id,
|
|
457
|
+
field: extraction.field,
|
|
458
|
+
value: extraction.value,
|
|
459
|
+
confidence: extraction.confidence
|
|
460
|
+
});
|
|
461
|
+
if (form.debug) {
|
|
462
|
+
logger.debug(`[FormEvaluator] Updated field ${extraction.field}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
for (const parentKey of updatedParents) {
|
|
467
|
+
await checkAndActivateExternalField(runtime, formService, session, form, entityId, parentKey);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function checkAndActivateExternalField(runtime, formService, session, form, entityId, field) {
|
|
471
|
+
const freshSession = await formService.getActiveSession(entityId, session.roomId);
|
|
472
|
+
if (!freshSession) return;
|
|
473
|
+
if (!formService.isExternalType(form.controls.find((c) => c.key === field)?.type || "")) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!formService.areSubFieldsFilled(freshSession, field)) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const subValues = formService.getSubFieldValues(freshSession, field);
|
|
480
|
+
await emitEvent(runtime, "FORM_SUBCONTROLS_FILLED", {
|
|
481
|
+
sessionId: session.id,
|
|
482
|
+
field,
|
|
483
|
+
subValues
|
|
484
|
+
});
|
|
485
|
+
logger.debug(`[FormEvaluator] All subcontrols filled for ${field}, activating...`);
|
|
486
|
+
try {
|
|
487
|
+
const activation = await formService.activateExternalField(session.id, entityId, field);
|
|
488
|
+
const activationPayload = JSON.parse(JSON.stringify(activation));
|
|
489
|
+
await emitEvent(runtime, "FORM_EXTERNAL_ACTIVATED", {
|
|
490
|
+
sessionId: session.id,
|
|
491
|
+
field,
|
|
492
|
+
activation: activationPayload
|
|
493
|
+
});
|
|
494
|
+
logger.info(`[FormEvaluator] Activated external field ${field}: ${activation.instructions}`);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
logger.error(`[FormEvaluator] Failed to activate external field ${field}:`, String(error));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function emitEvent(runtime, eventType, payload) {
|
|
500
|
+
try {
|
|
501
|
+
if (typeof runtime.emitEvent === "function") {
|
|
502
|
+
const eventPayload = { runtime, ...payload };
|
|
503
|
+
await runtime.emitEvent(eventType, eventPayload);
|
|
504
|
+
}
|
|
505
|
+
} catch (error) {
|
|
506
|
+
logger.debug(`[FormEvaluator] Event emission (${eventType}):`, String(error));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function handleSubmit(formService, session, entityId) {
|
|
510
|
+
try {
|
|
511
|
+
await formService.submit(session.id, entityId);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
logger.debug("[FormEvaluator] Submit failed:", String(error));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function handleUndo(formService, session, entityId, form) {
|
|
517
|
+
if (!form.ux?.allowUndo) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const result = await formService.undoLastChange(session.id, entityId);
|
|
521
|
+
if (result) {
|
|
522
|
+
logger.debug("[FormEvaluator] Undid field:", result.field);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function handleSkip(formService, session, entityId, form) {
|
|
526
|
+
if (!form.ux?.allowSkip) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (session.lastAskedField) {
|
|
530
|
+
const skipped = await formService.skipField(session.id, entityId, session.lastAskedField);
|
|
531
|
+
if (skipped) {
|
|
532
|
+
logger.debug("[FormEvaluator] Skipped field:", session.lastAskedField);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
var extractor_default = formEvaluator;
|
|
537
|
+
|
|
538
|
+
export {
|
|
539
|
+
llmIntentAndExtract,
|
|
540
|
+
extractSingleField,
|
|
541
|
+
detectCorrection,
|
|
542
|
+
formEvaluator,
|
|
543
|
+
extractor_default
|
|
544
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/intent.ts
|
|
2
|
+
function quickIntentDetect(text) {
|
|
3
|
+
const lower = text.toLowerCase().trim();
|
|
4
|
+
if (lower.length < 2) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (/\b(resume|continue|pick up where|go back to|get back to)\b/.test(lower)) {
|
|
8
|
+
return "restore";
|
|
9
|
+
}
|
|
10
|
+
if (/\b(submit|done|finish|send it|that'?s all|i'?m done|complete|all set)\b/.test(lower)) {
|
|
11
|
+
return "submit";
|
|
12
|
+
}
|
|
13
|
+
if (/\b(save|stash|later|hold on|pause|save for later|come back|save this)\b/.test(lower)) {
|
|
14
|
+
if (!/\b(save and submit|save and send)\b/.test(lower)) {
|
|
15
|
+
return "stash";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (/\b(cancel|abort|nevermind|never mind|forget it|stop|quit|exit)\b/.test(lower)) {
|
|
19
|
+
return "cancel";
|
|
20
|
+
}
|
|
21
|
+
if (/\b(undo|go back|wait no|change that|oops|that'?s wrong|wrong|not right)\b/.test(lower)) {
|
|
22
|
+
return "undo";
|
|
23
|
+
}
|
|
24
|
+
if (/\b(skip|pass|don'?t know|next one|next|don'?t have|no idea)\b/.test(lower)) {
|
|
25
|
+
if (!/\bskip to\b/.test(lower)) {
|
|
26
|
+
return "skip";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (/\b(why|what'?s that for|explain|what do you mean|what is|purpose|reason)\b\??$/i.test(lower)) {
|
|
30
|
+
return "explain";
|
|
31
|
+
}
|
|
32
|
+
if (/^why\??$/i.test(lower)) {
|
|
33
|
+
return "explain";
|
|
34
|
+
}
|
|
35
|
+
if (/\b(example|like what|show me|such as|for instance|sample)\b\??$/i.test(lower)) {
|
|
36
|
+
return "example";
|
|
37
|
+
}
|
|
38
|
+
if (/^(example|e\.?g\.?)\??$/i.test(lower)) {
|
|
39
|
+
return "example";
|
|
40
|
+
}
|
|
41
|
+
if (/\b(how far|how many left|progress|status|how much more|where are we)\b/.test(lower)) {
|
|
42
|
+
return "progress";
|
|
43
|
+
}
|
|
44
|
+
if (/\b(same as|last time|use my usual|like before|previous|from before)\b/.test(lower)) {
|
|
45
|
+
return "autofill";
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function isLifecycleIntent(intent) {
|
|
50
|
+
return ["submit", "stash", "restore", "cancel"].includes(intent);
|
|
51
|
+
}
|
|
52
|
+
function isUXIntent(intent) {
|
|
53
|
+
return ["undo", "skip", "explain", "example", "progress", "autofill"].includes(intent);
|
|
54
|
+
}
|
|
55
|
+
function hasDataToExtract(intent) {
|
|
56
|
+
return intent === "fill_form" || intent === "other";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
quickIntentDetect,
|
|
61
|
+
isLifecycleIntent,
|
|
62
|
+
isUXIntent,
|
|
63
|
+
hasDataToExtract
|
|
64
|
+
};
|