@elizaos/plugin-form 2.0.3-beta.5 → 2.0.3-beta.7
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/actions/form.d.ts +31 -0
- package/dist/actions/form.d.ts.map +1 -0
- package/dist/actions/form.js +187 -0
- package/dist/actions/form.js.map +1 -0
- package/dist/builder.d.ts +320 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +458 -0
- package/dist/builder.js.map +1 -0
- package/dist/builtins.d.ts +128 -0
- package/dist/builtins.d.ts.map +1 -0
- package/dist/builtins.js +233 -0
- package/dist/builtins.js.map +1 -0
- package/dist/defaults.d.ts +95 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +79 -0
- package/dist/defaults.js.map +1 -0
- package/dist/evaluators/extractor.d.ts +28 -0
- package/dist/evaluators/extractor.d.ts.map +1 -0
- package/dist/evaluators/extractor.js +251 -0
- package/dist/evaluators/extractor.js.map +1 -0
- package/dist/extraction.d.ts +55 -0
- package/dist/extraction.d.ts.map +1 -0
- package/dist/extraction.js +347 -0
- package/dist/extraction.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/context.d.ts +56 -0
- package/dist/providers/context.d.ts.map +1 -0
- package/dist/providers/context.js +204 -0
- package/dist/providers/context.js.map +1 -0
- package/dist/service.d.ts +402 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +1199 -0
- package/dist/service.js.map +1 -0
- package/dist/storage.d.ts +228 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +255 -0
- package/dist/storage.js.map +1 -0
- package/dist/template.d.ts +10 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +60 -0
- package/dist/template.js.map +1 -0
- package/dist/ttl.d.ts +144 -0
- package/dist/ttl.d.ts.map +1 -0
- package/dist/ttl.js +85 -0
- package/dist/ttl.js.map +1 -0
- package/dist/types.d.ts +1213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +39 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +156 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +289 -0
- package/dist/validation.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { EvaluatorPriority, logger } from "@elizaos/core";
|
|
2
|
+
import {
|
|
3
|
+
buildFormExtractorPromptSection,
|
|
4
|
+
buildFormExtractorSchema,
|
|
5
|
+
coerceExtractionsAgainstControls,
|
|
6
|
+
parseFormExtractorOutput
|
|
7
|
+
} from "../extraction.js";
|
|
8
|
+
import { buildTemplateValues } from "../template.js";
|
|
9
|
+
async function emitEvent(runtime, eventType, payload) {
|
|
10
|
+
if (typeof runtime.emitEvent !== "function") return;
|
|
11
|
+
const eventPayload = { runtime, ...payload };
|
|
12
|
+
await runtime.emitEvent(eventType, eventPayload);
|
|
13
|
+
}
|
|
14
|
+
async function checkAndActivateExternalField(runtime, formService, session, form, entityId, field) {
|
|
15
|
+
const freshSession = await formService.getActiveSession(
|
|
16
|
+
entityId,
|
|
17
|
+
session.roomId
|
|
18
|
+
);
|
|
19
|
+
if (!freshSession) return;
|
|
20
|
+
const control = form.controls.find((c) => c.key === field);
|
|
21
|
+
if (!control || !formService.isExternalType(control.type)) return;
|
|
22
|
+
if (!formService.areSubFieldsFilled(freshSession, field)) return;
|
|
23
|
+
const subValues = formService.getSubFieldValues(freshSession, field);
|
|
24
|
+
await emitEvent(runtime, "FORM_SUBCONTROLS_FILLED", {
|
|
25
|
+
sessionId: session.id,
|
|
26
|
+
field,
|
|
27
|
+
subValues
|
|
28
|
+
});
|
|
29
|
+
const activation = await formService.activateExternalField(
|
|
30
|
+
session.id,
|
|
31
|
+
entityId,
|
|
32
|
+
field
|
|
33
|
+
);
|
|
34
|
+
const activationPayload = JSON.parse(JSON.stringify(activation));
|
|
35
|
+
await emitEvent(runtime, "FORM_EXTERNAL_ACTIVATED", {
|
|
36
|
+
sessionId: session.id,
|
|
37
|
+
field,
|
|
38
|
+
activation: activationPayload
|
|
39
|
+
});
|
|
40
|
+
logger.info(
|
|
41
|
+
`[FormEvaluator] Activated external field ${field}: ${activation.instructions}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const formIntentProcessor = {
|
|
45
|
+
name: "formIntent",
|
|
46
|
+
priority: 100,
|
|
47
|
+
async process({ output, prepared, runtime: _runtime, message: _message }) {
|
|
48
|
+
const { formService, session, form, entityId } = prepared;
|
|
49
|
+
switch (output.formIntent) {
|
|
50
|
+
case "submit":
|
|
51
|
+
await formService.submit(session.id, entityId);
|
|
52
|
+
return { success: true, values: { formIntent: "submit" } };
|
|
53
|
+
case "stash":
|
|
54
|
+
await formService.stash(session.id, entityId);
|
|
55
|
+
return { success: true, values: { formIntent: "stash" } };
|
|
56
|
+
case "cancel":
|
|
57
|
+
await formService.cancel(session.id, entityId);
|
|
58
|
+
return { success: true, values: { formIntent: "cancel" } };
|
|
59
|
+
case "undo": {
|
|
60
|
+
if (!form.ux?.allowUndo) return void 0;
|
|
61
|
+
const result = await formService.undoLastChange(session.id, entityId);
|
|
62
|
+
return result ? {
|
|
63
|
+
success: true,
|
|
64
|
+
values: { formIntent: "undo", undid: result.field }
|
|
65
|
+
} : void 0;
|
|
66
|
+
}
|
|
67
|
+
case "skip": {
|
|
68
|
+
if (!form.ux?.allowSkip || !session.lastAskedField) return void 0;
|
|
69
|
+
const skipped = await formService.skipField(
|
|
70
|
+
session.id,
|
|
71
|
+
entityId,
|
|
72
|
+
session.lastAskedField
|
|
73
|
+
);
|
|
74
|
+
return skipped ? {
|
|
75
|
+
success: true,
|
|
76
|
+
values: { formIntent: "skip", skipped: session.lastAskedField }
|
|
77
|
+
} : void 0;
|
|
78
|
+
}
|
|
79
|
+
case "autofill":
|
|
80
|
+
await formService.applyAutofill(session);
|
|
81
|
+
return { success: true, values: { formIntent: "autofill" } };
|
|
82
|
+
case "explain":
|
|
83
|
+
case "example":
|
|
84
|
+
case "progress":
|
|
85
|
+
return { success: true, values: { formIntent: output.formIntent } };
|
|
86
|
+
case "restore":
|
|
87
|
+
return void 0;
|
|
88
|
+
default:
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const formExtractionsProcessor = {
|
|
94
|
+
name: "formExtractions",
|
|
95
|
+
priority: 200,
|
|
96
|
+
async process({ output, prepared, runtime, message }) {
|
|
97
|
+
const { formService, session, form, entityId } = prepared;
|
|
98
|
+
if (output.formIntent !== "fill_form" && output.formIntent !== "other") {
|
|
99
|
+
const refreshed2 = await formService.getActiveSession(
|
|
100
|
+
entityId,
|
|
101
|
+
session.roomId
|
|
102
|
+
);
|
|
103
|
+
if (refreshed2) {
|
|
104
|
+
refreshed2.lastMessageId = message.id;
|
|
105
|
+
await formService.saveSession(refreshed2);
|
|
106
|
+
}
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
const updatedParents = /* @__PURE__ */ new Set();
|
|
110
|
+
const coerced = coerceExtractionsAgainstControls(
|
|
111
|
+
output.formExtractions,
|
|
112
|
+
form.controls,
|
|
113
|
+
prepared.templateValues
|
|
114
|
+
);
|
|
115
|
+
for (const extraction of coerced) {
|
|
116
|
+
if (extraction.field.includes(".")) {
|
|
117
|
+
const [parentKey, subKey] = extraction.field.split(".");
|
|
118
|
+
await formService.updateSubField(
|
|
119
|
+
session.id,
|
|
120
|
+
entityId,
|
|
121
|
+
parentKey,
|
|
122
|
+
subKey,
|
|
123
|
+
extraction.value,
|
|
124
|
+
extraction.confidence,
|
|
125
|
+
message.id
|
|
126
|
+
);
|
|
127
|
+
await emitEvent(runtime, "FORM_SUBFIELD_UPDATED", {
|
|
128
|
+
sessionId: session.id,
|
|
129
|
+
parentField: parentKey,
|
|
130
|
+
subField: subKey,
|
|
131
|
+
value: extraction.value,
|
|
132
|
+
confidence: extraction.confidence
|
|
133
|
+
});
|
|
134
|
+
updatedParents.add(parentKey);
|
|
135
|
+
} else {
|
|
136
|
+
await formService.updateField(
|
|
137
|
+
session.id,
|
|
138
|
+
entityId,
|
|
139
|
+
extraction.field,
|
|
140
|
+
extraction.value,
|
|
141
|
+
extraction.confidence,
|
|
142
|
+
extraction.isCorrection ? "correction" : "extraction",
|
|
143
|
+
message.id
|
|
144
|
+
);
|
|
145
|
+
await emitEvent(runtime, "FORM_FIELD_EXTRACTED", {
|
|
146
|
+
sessionId: session.id,
|
|
147
|
+
field: extraction.field,
|
|
148
|
+
value: extraction.value,
|
|
149
|
+
confidence: extraction.confidence
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const parentKey of updatedParents) {
|
|
154
|
+
await checkAndActivateExternalField(
|
|
155
|
+
runtime,
|
|
156
|
+
formService,
|
|
157
|
+
session,
|
|
158
|
+
form,
|
|
159
|
+
entityId,
|
|
160
|
+
parentKey
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const refreshed = await formService.getActiveSession(
|
|
164
|
+
entityId,
|
|
165
|
+
session.roomId
|
|
166
|
+
);
|
|
167
|
+
if (refreshed) {
|
|
168
|
+
refreshed.lastMessageId = message.id;
|
|
169
|
+
await formService.saveSession(refreshed);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
success: true,
|
|
173
|
+
values: {
|
|
174
|
+
extractionCount: output.formExtractions.length
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const formEvaluator = {
|
|
180
|
+
name: "form_extractor",
|
|
181
|
+
description: "Extracts form values + lifecycle/UX intent from user message.",
|
|
182
|
+
similes: ["FORM_EXTRACTION", "FORM_HANDLER", "form_evaluator"],
|
|
183
|
+
// Run before reflection/memory so downstream evaluators see updated form state.
|
|
184
|
+
priority: EvaluatorPriority.FORM,
|
|
185
|
+
providers: ["RECENT_MESSAGES"],
|
|
186
|
+
schema: buildFormExtractorSchema(),
|
|
187
|
+
async shouldRun({ runtime, message }) {
|
|
188
|
+
const formService = runtime.getService("FORM");
|
|
189
|
+
if (!formService) return false;
|
|
190
|
+
const entityId = message.entityId;
|
|
191
|
+
const roomId = message.roomId;
|
|
192
|
+
if (!entityId || !roomId) return false;
|
|
193
|
+
const text = message.content?.text;
|
|
194
|
+
if (!text?.trim()) return false;
|
|
195
|
+
const session = await formService.getActiveSession(entityId, roomId);
|
|
196
|
+
if (session) return true;
|
|
197
|
+
const stashed = await formService.getStashedSessions(entityId);
|
|
198
|
+
return stashed.length > 0;
|
|
199
|
+
},
|
|
200
|
+
async prepare({ runtime, message }) {
|
|
201
|
+
const formService = runtime.getService("FORM");
|
|
202
|
+
if (!formService) {
|
|
203
|
+
throw new Error("FormService not found in prepare()");
|
|
204
|
+
}
|
|
205
|
+
const entityId = message.entityId;
|
|
206
|
+
const roomId = message.roomId;
|
|
207
|
+
const session = await formService.getActiveSession(entityId, roomId);
|
|
208
|
+
if (!session) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"Form evaluator prepared without an active session; FORM action=restore owns the stashed-only path"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const form = formService.getForm(session.formId);
|
|
214
|
+
if (!form) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Form definition not found for session formId=${session.formId}`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
formService,
|
|
221
|
+
session,
|
|
222
|
+
form,
|
|
223
|
+
templateValues: buildTemplateValues(session),
|
|
224
|
+
entityId
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
prompt({ message, prepared }) {
|
|
228
|
+
const text = message.content?.text ?? "";
|
|
229
|
+
return buildFormExtractorPromptSection({
|
|
230
|
+
text,
|
|
231
|
+
form: prepared.form,
|
|
232
|
+
controls: prepared.form.controls,
|
|
233
|
+
templateValues: prepared.templateValues
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
parse(raw) {
|
|
237
|
+
const parsed = parseFormExtractorOutput(raw);
|
|
238
|
+
if (!parsed) return null;
|
|
239
|
+
return {
|
|
240
|
+
formIntent: parsed.intent,
|
|
241
|
+
formExtractions: parsed.extractions
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
processors: [formIntentProcessor, formExtractionsProcessor]
|
|
245
|
+
};
|
|
246
|
+
var extractor_default = formEvaluator;
|
|
247
|
+
export {
|
|
248
|
+
extractor_default as default,
|
|
249
|
+
formEvaluator
|
|
250
|
+
};
|
|
251
|
+
//# sourceMappingURL=extractor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/evaluators/extractor.ts"],"sourcesContent":["/**\n * @module evaluators/extractor\n * @description Form post-turn evaluator that runs in the\n * EvaluatorService pass. Detects form intents (submit, stash, cancel, undo,\n * skip, autofill, info) and extracts field values from the user message,\n * then mutates session state accordingly.\n *\n * The 'restore' intent is handled by FORM action=restore (a planner Action)\n * so the restored form is in scope before the agent's response is generated.\n */\n\nimport type {\n Evaluator,\n EvaluatorProcessor,\n EventPayload,\n IAgentRuntime,\n JsonValue,\n UUID,\n} from \"@elizaos/core\";\nimport { EvaluatorPriority, logger } from \"@elizaos/core\";\nimport {\n buildFormExtractorPromptSection,\n buildFormExtractorSchema,\n coerceExtractionsAgainstControls,\n parseFormExtractorOutput,\n} from \"../extraction.js\";\nimport type { FormService } from \"../service.js\";\nimport { buildTemplateValues, type TemplateValues } from \"../template.js\";\nimport type {\n ExtractionResult,\n FormDefinition,\n FormIntent,\n FormSession,\n} from \"../types.js\";\n\ninterface FormExtractorOutput {\n formIntent: FormIntent;\n formExtractions: ExtractionResult[];\n}\n\ninterface FormExtractorPrepared {\n formService: FormService;\n session: FormSession;\n form: FormDefinition;\n templateValues: TemplateValues;\n entityId: UUID;\n}\n\nasync function emitEvent(\n runtime: IAgentRuntime,\n eventType: string,\n payload: Record<string, JsonValue>,\n): Promise<void> {\n if (typeof runtime.emitEvent !== \"function\") return;\n const eventPayload: EventPayload = { runtime, ...payload };\n await runtime.emitEvent(eventType, eventPayload);\n}\n\nasync function checkAndActivateExternalField(\n runtime: IAgentRuntime,\n formService: FormService,\n session: FormSession,\n form: FormDefinition,\n entityId: UUID,\n field: string,\n): Promise<void> {\n const freshSession = await formService.getActiveSession(\n entityId,\n session.roomId,\n );\n if (!freshSession) return;\n\n const control = form.controls.find((c) => c.key === field);\n if (!control || !formService.isExternalType(control.type)) return;\n if (!formService.areSubFieldsFilled(freshSession, field)) return;\n\n const subValues = formService.getSubFieldValues(freshSession, field);\n await emitEvent(runtime, \"FORM_SUBCONTROLS_FILLED\", {\n sessionId: session.id,\n field,\n subValues,\n });\n\n const activation = await formService.activateExternalField(\n session.id,\n entityId,\n field,\n );\n const activationPayload = JSON.parse(JSON.stringify(activation)) as JsonValue;\n\n await emitEvent(runtime, \"FORM_EXTERNAL_ACTIVATED\", {\n sessionId: session.id,\n field,\n activation: activationPayload,\n });\n\n logger.info(\n `[FormEvaluator] Activated external field ${field}: ${activation.instructions}`,\n );\n}\n\nconst formIntentProcessor: EvaluatorProcessor<\n FormExtractorOutput,\n FormExtractorPrepared\n> = {\n name: \"formIntent\",\n priority: 100,\n async process({ output, prepared, runtime: _runtime, message: _message }) {\n const { formService, session, form, entityId } = prepared;\n\n switch (output.formIntent) {\n case \"submit\":\n await formService.submit(session.id, entityId);\n return { success: true, values: { formIntent: \"submit\" } };\n\n case \"stash\":\n await formService.stash(session.id, entityId);\n return { success: true, values: { formIntent: \"stash\" } };\n\n case \"cancel\":\n await formService.cancel(session.id, entityId);\n return { success: true, values: { formIntent: \"cancel\" } };\n\n case \"undo\": {\n if (!form.ux?.allowUndo) return undefined;\n const result = await formService.undoLastChange(session.id, entityId);\n return result\n ? {\n success: true,\n values: { formIntent: \"undo\", undid: result.field },\n }\n : undefined;\n }\n\n case \"skip\": {\n if (!form.ux?.allowSkip || !session.lastAskedField) return undefined;\n const skipped = await formService.skipField(\n session.id,\n entityId,\n session.lastAskedField,\n );\n return skipped\n ? {\n success: true,\n values: { formIntent: \"skip\", skipped: session.lastAskedField },\n }\n : undefined;\n }\n\n case \"autofill\":\n await formService.applyAutofill(session);\n return { success: true, values: { formIntent: \"autofill\" } };\n\n case \"explain\":\n case \"example\":\n case \"progress\":\n return { success: true, values: { formIntent: output.formIntent } };\n\n case \"restore\":\n // FORM action=restore owns this path; nothing to do here.\n return undefined;\n\n default:\n return undefined;\n }\n },\n};\n\nconst formExtractionsProcessor: EvaluatorProcessor<\n FormExtractorOutput,\n FormExtractorPrepared\n> = {\n name: \"formExtractions\",\n priority: 200,\n async process({ output, prepared, runtime, message }) {\n const { formService, session, form, entityId } = prepared;\n\n // Lifecycle / UX intents shouldn't double-process extractions; only\n // fill_form and `other` may carry inline data.\n if (output.formIntent !== \"fill_form\" && output.formIntent !== \"other\") {\n // Still update last-message tracking before bailing so deduplication works.\n const refreshed = await formService.getActiveSession(\n entityId,\n session.roomId,\n );\n if (refreshed) {\n refreshed.lastMessageId = message.id;\n await formService.saveSession(refreshed);\n }\n return undefined;\n }\n\n const updatedParents = new Set<string>();\n const coerced = coerceExtractionsAgainstControls(\n output.formExtractions,\n form.controls,\n prepared.templateValues,\n );\n\n for (const extraction of coerced) {\n if (extraction.field.includes(\".\")) {\n const [parentKey, subKey] = extraction.field.split(\".\");\n await formService.updateSubField(\n session.id,\n entityId,\n parentKey,\n subKey,\n extraction.value,\n extraction.confidence,\n message.id,\n );\n await emitEvent(runtime, \"FORM_SUBFIELD_UPDATED\", {\n sessionId: session.id,\n parentField: parentKey,\n subField: subKey,\n value: extraction.value,\n confidence: extraction.confidence,\n });\n updatedParents.add(parentKey);\n } else {\n await formService.updateField(\n session.id,\n entityId,\n extraction.field,\n extraction.value,\n extraction.confidence,\n extraction.isCorrection ? \"correction\" : \"extraction\",\n message.id,\n );\n await emitEvent(runtime, \"FORM_FIELD_EXTRACTED\", {\n sessionId: session.id,\n field: extraction.field,\n value: extraction.value,\n confidence: extraction.confidence,\n });\n }\n }\n\n for (const parentKey of updatedParents) {\n await checkAndActivateExternalField(\n runtime,\n formService,\n session,\n form,\n entityId,\n parentKey,\n );\n }\n\n const refreshed = await formService.getActiveSession(\n entityId,\n session.roomId,\n );\n if (refreshed) {\n refreshed.lastMessageId = message.id;\n await formService.saveSession(refreshed);\n }\n\n return {\n success: true,\n values: {\n extractionCount: output.formExtractions.length,\n },\n };\n },\n};\n\nexport const formEvaluator: Evaluator<\n FormExtractorOutput,\n FormExtractorPrepared\n> = {\n name: \"form_extractor\",\n description: \"Extracts form values + lifecycle/UX intent from user message.\",\n similes: [\"FORM_EXTRACTION\", \"FORM_HANDLER\", \"form_evaluator\"],\n // Run before reflection/memory so downstream evaluators see updated form state.\n priority: EvaluatorPriority.FORM,\n providers: [\"RECENT_MESSAGES\"],\n schema: buildFormExtractorSchema(),\n\n async shouldRun({ runtime, message }) {\n const formService = runtime.getService(\"FORM\") as FormService | null;\n if (!formService) return false;\n\n const entityId = message.entityId as UUID | undefined;\n const roomId = message.roomId as UUID | undefined;\n if (!entityId || !roomId) return false;\n\n const text = message.content?.text;\n if (!text?.trim()) return false;\n\n const session = await formService.getActiveSession(entityId, roomId);\n if (session) return true;\n\n const stashed = await formService.getStashedSessions(entityId);\n return stashed.length > 0;\n },\n\n async prepare({ runtime, message }) {\n const formService = runtime.getService(\"FORM\") as FormService | null;\n if (!formService) {\n throw new Error(\"FormService not found in prepare()\");\n }\n const entityId = message.entityId as UUID;\n const roomId = message.roomId as UUID;\n\n const session = await formService.getActiveSession(entityId, roomId);\n if (!session) {\n // shouldRun gates this — only stashed-only state reaches here, in which\n // case the evaluator section will produce an `other` intent and no\n // extractions will be applied because there's no active session.\n throw new Error(\n \"Form evaluator prepared without an active session; FORM action=restore owns the stashed-only path\",\n );\n }\n\n const form = formService.getForm(session.formId);\n if (!form) {\n throw new Error(\n `Form definition not found for session formId=${session.formId}`,\n );\n }\n\n return {\n formService,\n session,\n form,\n templateValues: buildTemplateValues(session),\n entityId,\n };\n },\n\n prompt({ message, prepared }) {\n const text = message.content?.text ?? \"\";\n return buildFormExtractorPromptSection({\n text,\n form: prepared.form,\n controls: prepared.form.controls,\n templateValues: prepared.templateValues,\n });\n },\n\n parse(raw) {\n const parsed = parseFormExtractorOutput(raw);\n if (!parsed) return null;\n return {\n formIntent: parsed.intent,\n formExtractions: parsed.extractions,\n };\n },\n\n processors: [formIntentProcessor, formExtractionsProcessor],\n};\n\nexport default formEvaluator;\n"],"mappings":"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","names":["refreshed"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module extraction
|
|
3
|
+
* @description LLM-based field extraction from natural language.
|
|
4
|
+
*
|
|
5
|
+
* Exposes prompt/schema/parse helpers consumed by the form Evaluator and
|
|
6
|
+
* runs the LLM call directly only for the targeted single-field and
|
|
7
|
+
* correction-detection helpers.
|
|
8
|
+
*/
|
|
9
|
+
import type { IAgentRuntime, JSONSchema, JsonValue } from "@elizaos/core";
|
|
10
|
+
import type { TemplateValues } from "./template";
|
|
11
|
+
import type { ExtractionResult, FormControl, FormDefinition, IntentResult } from "./types";
|
|
12
|
+
/**
|
|
13
|
+
* Build the JSON Schema fragment for the form-extractor evaluator section.
|
|
14
|
+
*
|
|
15
|
+
* Returned shape:
|
|
16
|
+
* { formIntent: <enum>, formExtractions: [{ field, value, confidence, isCorrection }] }
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildFormExtractorSchema(): JSONSchema;
|
|
19
|
+
/**
|
|
20
|
+
* Build the prompt section for the form-extractor evaluator.
|
|
21
|
+
*
|
|
22
|
+
* The evaluator prompt inlines this string and asks the model to
|
|
23
|
+
* populate `{ formIntent, formExtractions }` for the active form.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildFormExtractorPromptSection(params: {
|
|
26
|
+
text: string;
|
|
27
|
+
form: FormDefinition;
|
|
28
|
+
controls: FormControl[];
|
|
29
|
+
templateValues?: TemplateValues;
|
|
30
|
+
}): string;
|
|
31
|
+
/**
|
|
32
|
+
* Parse the raw `{ formIntent, formExtractions }` object produced by the
|
|
33
|
+
* evaluator pass into a typed `IntentResult`. Type coercion and
|
|
34
|
+
* validation against control rules happen later (in the processor) where
|
|
35
|
+
* the form definition is in scope.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseFormExtractorOutput(raw: unknown): IntentResult | null;
|
|
38
|
+
/**
|
|
39
|
+
* Apply type coercion + validation to a parsed extractions list against the
|
|
40
|
+
* resolved form controls. Lowers confidence to 0.3 when a value fails the
|
|
41
|
+
* control's validator (so downstream confirmation flow kicks in).
|
|
42
|
+
*/
|
|
43
|
+
export declare function coerceExtractionsAgainstControls(extractions: ExtractionResult[], controls: FormControl[], templateValues?: TemplateValues): ExtractionResult[];
|
|
44
|
+
/**
|
|
45
|
+
* Extract a specific field value from a user message with a focused prompt.
|
|
46
|
+
*
|
|
47
|
+
* Used when the agent has just asked for a specific field and expects a
|
|
48
|
+
* direct answer. Independent of the evaluator pass.
|
|
49
|
+
*/
|
|
50
|
+
export declare function extractSingleField(runtime: IAgentRuntime, text: string, control: FormControl, debug?: boolean, templateValues?: TemplateValues): Promise<ExtractionResult | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Detect whether the user is correcting a previously filled value.
|
|
53
|
+
*/
|
|
54
|
+
export declare function detectCorrection(runtime: IAgentRuntime, text: string, currentValues: Record<string, JsonValue>, controls: FormControl[], templateValues?: TemplateValues): Promise<ExtractionResult[]>;
|
|
55
|
+
//# sourceMappingURL=extraction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extraction.d.ts","sourceRoot":"","sources":["../src/extraction.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EAEd,YAAY,EACb,MAAM,SAAS,CAAC;AAoGjB;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,UAAU,CA2BrD;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC,GAAG,MAAM,CAsDT;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CA8C1E;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,gBAAgB,EAAE,EAC/B,QAAQ,EAAE,WAAW,EAAE,EACvB,cAAc,CAAC,EAAE,cAAc,GAC9B,gBAAgB,EAAE,CA2CpB;AAMD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,WAAW,EACpB,KAAK,CAAC,EAAE,OAAO,EACf,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAsElC;AAMD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EACxC,QAAQ,EAAE,WAAW,EAAE,EACvB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiG7B"}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { ModelType } from "@elizaos/core";
|
|
2
|
+
import { resolveControlTemplates } from "./template.js";
|
|
3
|
+
import { getTypeHandler, parseValue, validateField } from "./validation.js";
|
|
4
|
+
const FORM_INTENTS = [
|
|
5
|
+
"fill_form",
|
|
6
|
+
"submit",
|
|
7
|
+
"stash",
|
|
8
|
+
"restore",
|
|
9
|
+
"cancel",
|
|
10
|
+
"undo",
|
|
11
|
+
"skip",
|
|
12
|
+
"explain",
|
|
13
|
+
"example",
|
|
14
|
+
"progress",
|
|
15
|
+
"autofill",
|
|
16
|
+
"other"
|
|
17
|
+
];
|
|
18
|
+
const UNSAFE_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
19
|
+
const INTENT_MEANINGS = {
|
|
20
|
+
fill_form: "user is providing field values",
|
|
21
|
+
submit: "user wants to submit or finish the form",
|
|
22
|
+
stash: "user wants to save or pause the form for later",
|
|
23
|
+
restore: "user wants to resume a saved form",
|
|
24
|
+
cancel: "user wants to cancel or abandon the form",
|
|
25
|
+
undo: "user wants to undo the last change",
|
|
26
|
+
skip: "user wants to skip the current field",
|
|
27
|
+
explain: "user wants an explanation",
|
|
28
|
+
example: "user wants an example value",
|
|
29
|
+
progress: "user wants a progress update",
|
|
30
|
+
autofill: "user wants to use saved values",
|
|
31
|
+
other: "none of the above"
|
|
32
|
+
};
|
|
33
|
+
function parseJsonObjectResponse(response) {
|
|
34
|
+
try {
|
|
35
|
+
const trimmed = response.trim();
|
|
36
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
37
|
+
const candidate = (fenced?.[1] ?? trimmed).trim();
|
|
38
|
+
const firstBrace = candidate.indexOf("{");
|
|
39
|
+
const lastBrace = candidate.lastIndexOf("}");
|
|
40
|
+
if (firstBrace < 0 || lastBrace <= firstBrace) return null;
|
|
41
|
+
const parsed = JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
|
|
42
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function parseBoolean(value) {
|
|
51
|
+
return String(value ?? "").trim().toLowerCase() === "true";
|
|
52
|
+
}
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
function isValidIntent(str) {
|
|
57
|
+
return FORM_INTENTS.includes(str);
|
|
58
|
+
}
|
|
59
|
+
function isSafeExtractionField(field) {
|
|
60
|
+
return field.split(".").every((part) => part.length > 0 && !UNSAFE_OBJECT_KEYS.has(part));
|
|
61
|
+
}
|
|
62
|
+
function buildFormExtractorSchema() {
|
|
63
|
+
return {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
formIntent: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: [...FORM_INTENTS]
|
|
69
|
+
},
|
|
70
|
+
formExtractions: {
|
|
71
|
+
type: "array",
|
|
72
|
+
items: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
field: { type: "string" },
|
|
76
|
+
value: {},
|
|
77
|
+
confidence: { type: "number" },
|
|
78
|
+
isCorrection: { type: "boolean" },
|
|
79
|
+
reasoning: { type: "string" }
|
|
80
|
+
},
|
|
81
|
+
required: ["field", "confidence"],
|
|
82
|
+
additionalProperties: false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
required: ["formIntent", "formExtractions"],
|
|
87
|
+
additionalProperties: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function buildFormExtractorPromptSection(params) {
|
|
91
|
+
const { text, form, controls, templateValues } = params;
|
|
92
|
+
const resolvedControls = templateValues ? controls.map(
|
|
93
|
+
(control) => resolveControlTemplates(control, templateValues)
|
|
94
|
+
) : controls;
|
|
95
|
+
const visibleControls = resolvedControls.filter((c) => !c.hidden);
|
|
96
|
+
const fieldsDescription = visibleControls.map((c) => {
|
|
97
|
+
const handler = getTypeHandler(c.type);
|
|
98
|
+
const typeHint = handler?.extractionPrompt || c.type;
|
|
99
|
+
return {
|
|
100
|
+
key: c.key,
|
|
101
|
+
label: c.label,
|
|
102
|
+
type: typeHint,
|
|
103
|
+
description: c.description || typeHint,
|
|
104
|
+
hints: c.extractHints ?? [],
|
|
105
|
+
options: c.options?.map((o) => o.value) ?? []
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return `Extract active form intent + field values.
|
|
109
|
+
|
|
110
|
+
Context JSON:
|
|
111
|
+
${JSON.stringify(
|
|
112
|
+
{
|
|
113
|
+
form: {
|
|
114
|
+
name: form.name,
|
|
115
|
+
description: form.description
|
|
116
|
+
},
|
|
117
|
+
fields: fieldsDescription,
|
|
118
|
+
user_message: text,
|
|
119
|
+
intent_options: FORM_INTENTS,
|
|
120
|
+
intent_meanings: INTENT_MEANINGS
|
|
121
|
+
},
|
|
122
|
+
null,
|
|
123
|
+
2
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
Return:
|
|
127
|
+
{
|
|
128
|
+
"formIntent": "one of intent_options",
|
|
129
|
+
"formExtractions": [
|
|
130
|
+
{ "field": "<key>", "value": <extracted>, "confidence": 0.0-1.0, "isCorrection": false, "reasoning": "brief" }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Rules:
|
|
135
|
+
- Choose exactly one intent.
|
|
136
|
+
- fill_form: extract every mentioned field value.
|
|
137
|
+
- No fields -> formExtractions=[].
|
|
138
|
+
- confidence 0.0-1.0.`;
|
|
139
|
+
}
|
|
140
|
+
function parseFormExtractorOutput(raw) {
|
|
141
|
+
if (!isRecord(raw)) return null;
|
|
142
|
+
const intentStr = typeof raw.formIntent === "string" ? raw.formIntent.toLowerCase() : "other";
|
|
143
|
+
const intent = isValidIntent(intentStr) ? intentStr : "other";
|
|
144
|
+
const rawExtractions = Array.isArray(raw.formExtractions) ? raw.formExtractions : [];
|
|
145
|
+
const extractions = [];
|
|
146
|
+
const seen = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const entry of rawExtractions) {
|
|
148
|
+
if (!isRecord(entry)) continue;
|
|
149
|
+
const fieldKey = typeof entry.field === "string" ? entry.field : "";
|
|
150
|
+
if (!fieldKey) continue;
|
|
151
|
+
if (!isSafeExtractionField(fieldKey)) continue;
|
|
152
|
+
const dedupeKey = `${fieldKey}\0${String(entry.value ?? "")}`;
|
|
153
|
+
if (seen.has(dedupeKey)) continue;
|
|
154
|
+
seen.add(dedupeKey);
|
|
155
|
+
const value = entry.value ?? null;
|
|
156
|
+
let confidence = typeof entry.confidence === "number" ? entry.confidence : parseFloat(String(entry.confidence ?? ""));
|
|
157
|
+
if (!Number.isFinite(confidence)) confidence = 0.5;
|
|
158
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
159
|
+
const reasoning = typeof entry.reasoning === "string" ? entry.reasoning : void 0;
|
|
160
|
+
extractions.push({
|
|
161
|
+
field: fieldKey,
|
|
162
|
+
value,
|
|
163
|
+
confidence,
|
|
164
|
+
reasoning,
|
|
165
|
+
isCorrection: parseBoolean(entry.isCorrection)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return { intent, extractions };
|
|
169
|
+
}
|
|
170
|
+
function coerceExtractionsAgainstControls(extractions, controls, templateValues) {
|
|
171
|
+
const resolvedControls = templateValues ? controls.map(
|
|
172
|
+
(control) => resolveControlTemplates(control, templateValues)
|
|
173
|
+
) : controls;
|
|
174
|
+
const controlsByKey = new Map(
|
|
175
|
+
resolvedControls.map((control) => [control.key, control])
|
|
176
|
+
);
|
|
177
|
+
return extractions.flatMap((extraction) => {
|
|
178
|
+
if (extraction.field.includes(".")) {
|
|
179
|
+
const [parentKey, subKey, ...extraParts] = extraction.field.split(".");
|
|
180
|
+
if (extraParts.length > 0 || !parentKey || !subKey) return [];
|
|
181
|
+
if (!controlsByKey.has(parentKey)) return [];
|
|
182
|
+
return [extraction];
|
|
183
|
+
}
|
|
184
|
+
const control = controlsByKey.get(extraction.field);
|
|
185
|
+
if (!control) return [];
|
|
186
|
+
let value = extraction.value;
|
|
187
|
+
if (typeof value === "string") {
|
|
188
|
+
value = parseValue(value, control);
|
|
189
|
+
}
|
|
190
|
+
const validation = validateField(value, control);
|
|
191
|
+
if (!validation.valid) {
|
|
192
|
+
const reasoning = `${extraction.reasoning ?? ""} (Validation failed: ${validation.error})`.trim();
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
...extraction,
|
|
196
|
+
value,
|
|
197
|
+
confidence: Math.min(extraction.confidence, 0.3),
|
|
198
|
+
reasoning
|
|
199
|
+
}
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
return [{ ...extraction, value }];
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function extractSingleField(runtime, text, control, debug, templateValues) {
|
|
206
|
+
const resolvedControl = templateValues ? resolveControlTemplates(control, templateValues) : control;
|
|
207
|
+
const handler = getTypeHandler(resolvedControl.type);
|
|
208
|
+
const typeHint = handler?.extractionPrompt || resolvedControl.type;
|
|
209
|
+
const prompt = `Extract a single form field value from the user message.
|
|
210
|
+
|
|
211
|
+
Context JSON:
|
|
212
|
+
${JSON.stringify(
|
|
213
|
+
{
|
|
214
|
+
field: {
|
|
215
|
+
key: resolvedControl.key,
|
|
216
|
+
label: resolvedControl.label,
|
|
217
|
+
type: typeHint,
|
|
218
|
+
description: resolvedControl.description,
|
|
219
|
+
hints: resolvedControl.extractHints ?? [],
|
|
220
|
+
options: resolvedControl.options?.map((o) => o.value) ?? [],
|
|
221
|
+
example: resolvedControl.example
|
|
222
|
+
},
|
|
223
|
+
user_message: text
|
|
224
|
+
},
|
|
225
|
+
null,
|
|
226
|
+
2
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
Return only a valid JSON object with this schema:
|
|
230
|
+
{
|
|
231
|
+
"found": true,
|
|
232
|
+
"value": "extracted value or null if not found",
|
|
233
|
+
"confidence": 0.95,
|
|
234
|
+
"reasoning": "brief explanation"
|
|
235
|
+
}`;
|
|
236
|
+
const runModel = runtime.useModel.bind(runtime);
|
|
237
|
+
const response = await runModel(ModelType.TEXT_SMALL, {
|
|
238
|
+
prompt,
|
|
239
|
+
temperature: 0.1
|
|
240
|
+
});
|
|
241
|
+
const parsed = parseJsonObjectResponse(response);
|
|
242
|
+
const found = parsed?.found === true || parsed?.found === "true";
|
|
243
|
+
if (!found || !parsed) return null;
|
|
244
|
+
let value = parsed.value;
|
|
245
|
+
if (typeof value === "string") {
|
|
246
|
+
value = parseValue(value, resolvedControl);
|
|
247
|
+
}
|
|
248
|
+
const confidence = typeof parsed.confidence === "number" ? parsed.confidence : parseFloat(String(parsed.confidence ?? ""));
|
|
249
|
+
const result = {
|
|
250
|
+
field: resolvedControl.key,
|
|
251
|
+
value: value ?? null,
|
|
252
|
+
confidence: Number.isFinite(confidence) ? confidence : 0.5,
|
|
253
|
+
reasoning: parsed.reasoning ? String(parsed.reasoning) : void 0
|
|
254
|
+
};
|
|
255
|
+
if (debug) {
|
|
256
|
+
runtime.logger.debug(
|
|
257
|
+
"[FormExtraction] Single field extraction:",
|
|
258
|
+
JSON.stringify(result)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
async function detectCorrection(runtime, text, currentValues, controls, templateValues) {
|
|
264
|
+
const resolvedControls = templateValues ? controls.map(
|
|
265
|
+
(control) => resolveControlTemplates(control, templateValues)
|
|
266
|
+
) : controls;
|
|
267
|
+
const currentValueEntries = resolvedControls.filter(
|
|
268
|
+
(c) => currentValues[c.key] !== void 0
|
|
269
|
+
);
|
|
270
|
+
if (currentValueEntries.length === 0) return [];
|
|
271
|
+
const currentValueRows = currentValueEntries.map((c) => ({
|
|
272
|
+
key: c.key,
|
|
273
|
+
label: c.label,
|
|
274
|
+
value: currentValues[c.key]
|
|
275
|
+
}));
|
|
276
|
+
const prompt = `Detect whether the user is correcting a previous form value.
|
|
277
|
+
|
|
278
|
+
Context JSON:
|
|
279
|
+
${JSON.stringify(
|
|
280
|
+
{
|
|
281
|
+
current_values: currentValueRows,
|
|
282
|
+
user_message: text
|
|
283
|
+
},
|
|
284
|
+
null,
|
|
285
|
+
2
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
Return only a valid JSON object with this schema:
|
|
289
|
+
{
|
|
290
|
+
"has_correction": true,
|
|
291
|
+
"corrections": [
|
|
292
|
+
{
|
|
293
|
+
"field": "email",
|
|
294
|
+
"old_value": "old@example.com",
|
|
295
|
+
"new_value": "new@example.com",
|
|
296
|
+
"confidence": 0.9
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Rules:
|
|
302
|
+
- Decide whether the user is correcting a previous value.
|
|
303
|
+
- When correcting, extract the replacement value.
|
|
304
|
+
- Use an empty corrections array when no corrections were found.`;
|
|
305
|
+
const runModel = runtime.useModel.bind(runtime);
|
|
306
|
+
const response = await runModel(ModelType.TEXT_SMALL, {
|
|
307
|
+
prompt,
|
|
308
|
+
temperature: 0.1
|
|
309
|
+
});
|
|
310
|
+
const parsed = parseJsonObjectResponse(response);
|
|
311
|
+
const hasCorrection = parsed?.has_correction === true || parsed?.has_correction === "true";
|
|
312
|
+
if (!parsed || !hasCorrection || !parsed.corrections) return [];
|
|
313
|
+
const corrections = [];
|
|
314
|
+
const correctionList = Array.isArray(parsed.corrections) ? parsed.corrections : [];
|
|
315
|
+
const seen = /* @__PURE__ */ new Set();
|
|
316
|
+
for (const correction of correctionList) {
|
|
317
|
+
const fieldName = correction.field ? String(correction.field) : "";
|
|
318
|
+
const control = resolvedControls.find(
|
|
319
|
+
(c) => c.label.toLowerCase() === fieldName.toLowerCase() || c.key.toLowerCase() === fieldName.toLowerCase()
|
|
320
|
+
);
|
|
321
|
+
if (!control) continue;
|
|
322
|
+
const dedupeKey = `${control.key}\0${String(correction.new_value ?? "")}`;
|
|
323
|
+
if (seen.has(dedupeKey)) continue;
|
|
324
|
+
seen.add(dedupeKey);
|
|
325
|
+
let value = correction.new_value;
|
|
326
|
+
if (typeof value === "string") {
|
|
327
|
+
value = parseValue(value, control);
|
|
328
|
+
}
|
|
329
|
+
const confidence = typeof correction.confidence === "number" ? correction.confidence : parseFloat(String(correction.confidence ?? ""));
|
|
330
|
+
corrections.push({
|
|
331
|
+
field: control.key,
|
|
332
|
+
value: value ?? null,
|
|
333
|
+
confidence: Number.isFinite(confidence) ? confidence : 0.8,
|
|
334
|
+
isCorrection: true
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return corrections;
|
|
338
|
+
}
|
|
339
|
+
export {
|
|
340
|
+
buildFormExtractorPromptSection,
|
|
341
|
+
buildFormExtractorSchema,
|
|
342
|
+
coerceExtractionsAgainstControls,
|
|
343
|
+
detectCorrection,
|
|
344
|
+
extractSingleField,
|
|
345
|
+
parseFormExtractorOutput
|
|
346
|
+
};
|
|
347
|
+
//# sourceMappingURL=extraction.js.map
|