@formio/uag 1.13.2 → 2.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -535
- package/bin/uag-eval.js +99 -0
- package/bin/uag.js +116 -0
- package/lib/UAGFormInterface.d.ts +72 -2
- package/lib/UAGFormInterface.js +226 -57
- package/lib/UAGProjectInterface.d.ts +45 -5
- package/lib/UAGProjectInterface.js +187 -36
- package/lib/actionLogs.d.ts +24 -0
- package/lib/actionLogs.js +149 -0
- package/lib/actions/runTask.d.ts +43 -0
- package/lib/actions/runTask.js +324 -0
- package/lib/agent/anonymous.d.ts +34 -0
- package/lib/agent/anonymous.js +99 -0
- package/lib/agent/chat.d.ts +114 -0
- package/lib/agent/chat.js +475 -0
- package/lib/agent/engines.d.ts +17 -0
- package/lib/agent/engines.js +46 -0
- package/lib/agent/errors.d.ts +25 -0
- package/lib/agent/errors.js +132 -0
- package/lib/agent/index.d.ts +4 -0
- package/lib/agent/index.js +19 -0
- package/lib/agent/remote.d.ts +71 -0
- package/lib/agent/remote.js +252 -0
- package/lib/agent/requestId.d.ts +4 -0
- package/lib/agent/requestId.js +19 -0
- package/lib/agent/router.d.ts +61 -0
- package/lib/agent/router.js +307 -0
- package/lib/agent/stubs.d.ts +37 -0
- package/lib/agent/stubs.js +39 -0
- package/lib/agent/tools.d.ts +31 -0
- package/lib/agent/tools.js +76 -0
- package/lib/config.d.ts +97 -3
- package/lib/config.js +19 -0
- package/lib/database.d.ts +70 -0
- package/lib/database.js +212 -0
- package/lib/documents/convert.d.ts +38 -0
- package/lib/documents/convert.js +151 -0
- package/lib/documents/converters.d.ts +28 -0
- package/lib/documents/converters.js +149 -0
- package/lib/documents/detect.d.ts +31 -0
- package/lib/documents/detect.js +169 -0
- package/lib/documents/index.d.ts +9 -0
- package/lib/documents/index.js +43 -0
- package/lib/documents/mime.d.ts +39 -0
- package/lib/documents/mime.js +100 -0
- package/lib/documents/plugins.d.ts +18 -0
- package/lib/documents/plugins.js +65 -0
- package/lib/documents/readers.d.ts +19 -0
- package/lib/documents/readers.js +64 -0
- package/lib/documents/routes.d.ts +52 -0
- package/lib/documents/routes.js +99 -0
- package/lib/documents/types.d.ts +126 -0
- package/lib/documents/types.js +2 -0
- package/lib/documents/uagJs.d.ts +17 -0
- package/lib/documents/uagJs.js +157 -0
- package/lib/formflow.d.ts +47 -0
- package/lib/formflow.js +28 -0
- package/lib/identity.d.ts +53 -0
- package/lib/identity.js +107 -0
- package/lib/index.d.ts +98 -1
- package/lib/index.js +382 -8
- package/lib/markdown.d.ts +1 -0
- package/lib/markdown.js +166 -0
- package/lib/modules/api.d.ts +25 -0
- package/lib/modules/api.js +98 -0
- package/lib/modules/index.d.ts +4 -0
- package/lib/modules/index.js +24 -0
- package/lib/modules/routes.d.ts +14 -0
- package/lib/modules/routes.js +21 -0
- package/lib/modules/types.d.ts +68 -0
- package/lib/modules/types.js +5 -0
- package/lib/pluginFiles.d.ts +28 -0
- package/lib/pluginFiles.js +101 -0
- package/lib/plugins.d.ts +17 -0
- package/lib/plugins.js +93 -0
- package/lib/richText.d.ts +20 -0
- package/lib/richText.js +99 -0
- package/lib/router.d.ts +9 -0
- package/lib/router.js +34 -25
- package/lib/runner.d.ts +149 -0
- package/lib/runner.js +353 -0
- package/lib/runs.d.ts +27 -0
- package/lib/runs.js +13 -0
- package/lib/template.js +7 -9
- package/lib/templates/uagComponentNotFound.md +5 -5
- package/lib/testing.d.ts +74 -0
- package/lib/testing.js +180 -0
- package/lib/tools/SchemaBuilder.d.ts +3 -3
- package/lib/tools/SchemaBuilder.js +4 -4
- package/lib/tools/agentDecide.d.ts +8 -0
- package/lib/tools/agentDecide.js +57 -0
- package/lib/tools/agentProvide.d.ts +8 -0
- package/lib/tools/agentProvide.js +97 -0
- package/lib/tools/collectData.js +3 -2
- package/lib/tools/confirmSubmission.js +1 -1
- package/lib/tools/fetchExternalData.js +8 -17
- package/lib/tools/findSubmission.js +62 -56
- package/lib/tools/getFieldInfo.js +1 -1
- package/lib/tools/getFormFields.js +1 -1
- package/lib/tools/getForms.js +1 -1
- package/lib/tools/index.d.ts +4 -1
- package/lib/tools/index.js +21 -14
- package/lib/tools/readDocument.d.ts +17 -0
- package/lib/tools/readDocument.js +99 -0
- package/lib/tools/submissionUpdate.js +4 -3
- package/lib/tools/submitForm.js +1 -1
- package/lib/tools/utils.d.ts +40 -0
- package/lib/tools/utils.js +16 -1
- package/lib/version.d.ts +2 -0
- package/lib/version.js +5 -0
- package/package.json +49 -19
- package/testing.d.ts +1 -0
- package/testing.js +2 -0
- package/lib/tools/agentProvideData.d.ts +0 -3
- package/lib/tools/agentProvideData.js +0 -62
package/lib/runner.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskRunner = exports.SWEEP_MS = exports.MAX_CHAIN = exports.RUN_TASK_CHAIN = exports.RUN_TASK_AS_USER = exports.RUN_TASK_IDENTITY = exports.RUN_TASK_REQUEST_ID = exports.RUN_TASK_CALLER = exports.RUN_TASK_ACTING_FOR = exports.RUN_TASK_SAVE = void 0;
|
|
4
|
+
exports.taskRunner = taskRunner;
|
|
5
|
+
const formflow_1 = require("./formflow");
|
|
6
|
+
const identity_1 = require("./identity");
|
|
7
|
+
const runs_1 = require("./runs");
|
|
8
|
+
const requestId_1 = require("./agent/requestId");
|
|
9
|
+
const debug = require('debug')('formio:uag:runner');
|
|
10
|
+
/**
|
|
11
|
+
* The request field that marks the runner's own save, so the form's Run task
|
|
12
|
+
* actions that run before the save skip it: its data is the agent's output.
|
|
13
|
+
*/
|
|
14
|
+
exports.RUN_TASK_SAVE = 'uagRunTaskSave';
|
|
15
|
+
/**
|
|
16
|
+
* The request fields that carry, through the runner's own save, who the run
|
|
17
|
+
* acts for, so a Run task action that the save starts acts for the same user.
|
|
18
|
+
*/
|
|
19
|
+
exports.RUN_TASK_ACTING_FOR = 'uagActingFor';
|
|
20
|
+
exports.RUN_TASK_CALLER = 'uagCaller';
|
|
21
|
+
exports.RUN_TASK_REQUEST_ID = 'uagRequestId';
|
|
22
|
+
exports.RUN_TASK_IDENTITY = 'uagIdentity';
|
|
23
|
+
exports.RUN_TASK_AS_USER = 'uagAsUser';
|
|
24
|
+
exports.RUN_TASK_CHAIN = 'uagChain';
|
|
25
|
+
/**
|
|
26
|
+
* Runs a runner's save may start in a row: a task whose save starts a task
|
|
27
|
+
* that changes something each time would otherwise never stop.
|
|
28
|
+
*/
|
|
29
|
+
exports.MAX_CHAIN = 5;
|
|
30
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
31
|
+
/** How often a replica looks for runs that are pending or lost their replica. */
|
|
32
|
+
exports.SWEEP_MS = 60 * 1000;
|
|
33
|
+
/**
|
|
34
|
+
* Runs Run task actions' background runs: load the stored submission, run the
|
|
35
|
+
* task, and save only what changed, as an update through the UAG's own
|
|
36
|
+
* pipeline (so the form's update actions, including other Run task actions,
|
|
37
|
+
* run on it). If the submission changes while the task runs, the run starts
|
|
38
|
+
* again once on the new version, then gives up as a conflict.
|
|
39
|
+
*
|
|
40
|
+
* Without the UAG's database, runs are kept in memory, a few at a time, and
|
|
41
|
+
* are lost if the process stops. With it, every run is recorded; a run that
|
|
42
|
+
* acts with an agent role is recorded as pending before the caller's response
|
|
43
|
+
* and taken from the database by one replica at a time, under a lease it
|
|
44
|
+
* renews, so it survives a restart. A sweep at startup and every minute takes
|
|
45
|
+
* runs that are pending or whose replica stopped.
|
|
46
|
+
*/
|
|
47
|
+
class TaskRunner {
|
|
48
|
+
constructor(project, concurrency = DEFAULT_CONCURRENCY) {
|
|
49
|
+
this.project = project;
|
|
50
|
+
this.concurrency = concurrency;
|
|
51
|
+
this.queue = [];
|
|
52
|
+
this.active = 0;
|
|
53
|
+
this.waiting = [];
|
|
54
|
+
this.sweeping = false;
|
|
55
|
+
}
|
|
56
|
+
/** Whether runs acting with an agent role are recorded and survive a restart. */
|
|
57
|
+
get durable() {
|
|
58
|
+
return !!this.store;
|
|
59
|
+
}
|
|
60
|
+
/** Record runs in the UAG's database, and take durable runs from it. */
|
|
61
|
+
useStore(store) {
|
|
62
|
+
this.store = store;
|
|
63
|
+
this.stop();
|
|
64
|
+
this.sweeper = setInterval(() => this.sweep(), exports.SWEEP_MS);
|
|
65
|
+
this.sweeper.unref?.();
|
|
66
|
+
this.sweep();
|
|
67
|
+
}
|
|
68
|
+
/** Stop sweeping (the runs in progress finish). */
|
|
69
|
+
stop() {
|
|
70
|
+
if (this.sweeper)
|
|
71
|
+
clearInterval(this.sweeper);
|
|
72
|
+
this.sweeper = undefined;
|
|
73
|
+
}
|
|
74
|
+
/** The project's run records, when there is a database. */
|
|
75
|
+
get records() {
|
|
76
|
+
return this.store;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Start a run. A durable run is recorded as pending before this resolves, so
|
|
80
|
+
* the caller's response is sent only once it can survive a restart.
|
|
81
|
+
*/
|
|
82
|
+
async enqueue(job) {
|
|
83
|
+
if (this.store) {
|
|
84
|
+
const durable = !!job.agentRole;
|
|
85
|
+
try {
|
|
86
|
+
const record = await this.store.add(this.newRecord(job, durable));
|
|
87
|
+
if (durable) {
|
|
88
|
+
this.sweep();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.queue.push({ job, record });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.error(`[formio-uag] Run task "${job.task}" could not be recorded (${err?.message || err}); it runs in memory.`);
|
|
95
|
+
this.queue.push({ job });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.queue.push({ job });
|
|
100
|
+
}
|
|
101
|
+
this.drain();
|
|
102
|
+
}
|
|
103
|
+
/** Record a run made before the save, once the save gave the submission its id. */
|
|
104
|
+
async recordBefore(job, outcome) {
|
|
105
|
+
if (!this.store)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
const record = await this.store.add({ ...this.newRecord(job, false, 'before'), status: 'running' });
|
|
109
|
+
await this.store.finish(record, resultOf(outcome));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
debug(`Could not record a run before the save: ${err?.message || err}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
newRecord(job, durable, handler = 'after') {
|
|
116
|
+
return {
|
|
117
|
+
project: String(this.project.project?._id || ''),
|
|
118
|
+
form: job.formId || '',
|
|
119
|
+
formName: job.form,
|
|
120
|
+
submission: job.submission,
|
|
121
|
+
action: job.action || 'Agent: Run task',
|
|
122
|
+
task: job.task,
|
|
123
|
+
answer: job.answer,
|
|
124
|
+
handler,
|
|
125
|
+
method: job.method || 'create',
|
|
126
|
+
durable,
|
|
127
|
+
actingFor: job.actingFor,
|
|
128
|
+
...(job.agentRole && { agentRole: job.agentRole }),
|
|
129
|
+
...(job.requestId && { requestId: job.requestId }),
|
|
130
|
+
...(job.chain && { chain: job.chain }),
|
|
131
|
+
status: (durable ? 'pending' : 'running')
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Resolves once every queued and running job has finished (for tests and shutdown). */
|
|
135
|
+
idle() {
|
|
136
|
+
if (this.isIdle())
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
return new Promise((resolve) => this.waiting.push(resolve));
|
|
139
|
+
}
|
|
140
|
+
isIdle() {
|
|
141
|
+
return !this.active && !this.queue.length && !this.sweeping;
|
|
142
|
+
}
|
|
143
|
+
settled() {
|
|
144
|
+
if (this.isIdle())
|
|
145
|
+
this.waiting.splice(0).forEach((resolve) => resolve());
|
|
146
|
+
}
|
|
147
|
+
drain() {
|
|
148
|
+
while (this.active < this.concurrency && this.queue.length) {
|
|
149
|
+
const { job, record } = this.queue.shift();
|
|
150
|
+
this.track(job, record ? (outcome) => this.store.finish(record, resultOf(outcome)) : undefined);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Take durable runs from the database while there is room. */
|
|
154
|
+
async sweep() {
|
|
155
|
+
if (!this.store || this.sweeping)
|
|
156
|
+
return;
|
|
157
|
+
this.sweeping = true;
|
|
158
|
+
try {
|
|
159
|
+
while (this.active < this.concurrency) {
|
|
160
|
+
const record = await this.store.claim();
|
|
161
|
+
if (!record)
|
|
162
|
+
break;
|
|
163
|
+
this.runRecord(record);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
debug(`Could not take runs from the database: ${err?.message || err}`);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
this.sweeping = false;
|
|
171
|
+
this.settled();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
runRecord(record) {
|
|
175
|
+
const store = this.store;
|
|
176
|
+
const renewal = setInterval(() => store.renew(record._id).catch(() => { }), runs_1.RENEW_MS);
|
|
177
|
+
renewal.unref?.();
|
|
178
|
+
this.track(jobOf(record), (outcome) => store.finish(record._id, resultOf(outcome)), () => {
|
|
179
|
+
clearInterval(renewal);
|
|
180
|
+
this.sweep();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** Run a job, report it, and record its end. */
|
|
184
|
+
track(job, finish, done) {
|
|
185
|
+
this.active++;
|
|
186
|
+
// Engines (the remote type) send the id of the request that started the run.
|
|
187
|
+
(0, requestId_1.withRequestId)(job.requestId, () => this.execute(job))
|
|
188
|
+
.catch((err) => ({ status: 'failed', written: {}, cleared: [], engines: [], saved: false, error: err?.message || String(err) }))
|
|
189
|
+
.then(async (outcome) => {
|
|
190
|
+
this.report(job, outcome);
|
|
191
|
+
await finish?.(outcome).catch((err) => debug(`Could not record the end of a run: ${err?.message || err}`));
|
|
192
|
+
})
|
|
193
|
+
.finally(() => {
|
|
194
|
+
this.active--;
|
|
195
|
+
done?.();
|
|
196
|
+
this.drain();
|
|
197
|
+
this.settled();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
report(job, outcome) {
|
|
201
|
+
if (outcome.status === 'failed') {
|
|
202
|
+
console.error(`[formio-uag] Run task "${job.task}" failed for submission ${job.submission} of "${job.form}": ${outcome.error}`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
debug(`Run task "${job.task}" on ${job.form}/${job.submission}: ${outcome.saved ? `saved ${Object.keys(outcome.written).length} field(s)` : 'nothing changed'}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Who the job acts as at Form.io (`act`: a new identity for each request,
|
|
210
|
+
* since signed tokens are short-lived), and who its engines research as.
|
|
211
|
+
* Never the project's key: the agent role's signed token, or the caller's own.
|
|
212
|
+
*/
|
|
213
|
+
async identities(job) {
|
|
214
|
+
if (job.agentRole) {
|
|
215
|
+
const claim = { action: job.action, task: job.task };
|
|
216
|
+
const research = await (0, identity_1.userAuth)(this.project, job.actingFor, claim);
|
|
217
|
+
return {
|
|
218
|
+
act: () => (0, identity_1.agentAuth)(this.project, job.actingFor, job.agentRole, claim),
|
|
219
|
+
research,
|
|
220
|
+
identity: (0, formflow_1.identityOf)(research, `Bearer ${research.token}`)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (job.asUser) {
|
|
224
|
+
const claim = { action: job.action, task: job.task };
|
|
225
|
+
const research = await (0, identity_1.userAuth)(this.project, job.actingFor, claim);
|
|
226
|
+
return {
|
|
227
|
+
act: () => (0, identity_1.userAuth)(this.project, job.actingFor, claim),
|
|
228
|
+
research,
|
|
229
|
+
identity: (0, formflow_1.identityOf)(research, `Bearer ${research.token}`)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (!job.caller)
|
|
233
|
+
throw new formflow_1.FormflowError(500, 'A run without an agent role needs the caller\'s identity.');
|
|
234
|
+
const caller = job.caller;
|
|
235
|
+
return { act: async () => caller.clone(), research: caller, identity: job.identity };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Why Form.io refused the agent's signed token, when it did: asks Form.io who
|
|
239
|
+
* the token is, so a JWT_SECRET that is not Form.io's is told apart from
|
|
240
|
+
* other failures.
|
|
241
|
+
*/
|
|
242
|
+
async refusal(job) {
|
|
243
|
+
if (!job.agentRole || !this.project.endpoint)
|
|
244
|
+
return null;
|
|
245
|
+
try {
|
|
246
|
+
const auth = await (0, identity_1.agentAuth)(this.project, job.actingFor, job.agentRole, { action: job.action, task: job.task });
|
|
247
|
+
const response = await fetch(`${this.project.endpoint}/current`, { headers: { 'x-jwt-token': auth.token || '', Accept: 'application/json' } });
|
|
248
|
+
return (0, identity_1.refusedSignedToken)(response.status);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Run one job to completion. */
|
|
255
|
+
async execute(job) {
|
|
256
|
+
try {
|
|
257
|
+
return await this.run(job);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
throw (await this.refusal(job)) || err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async run(job) {
|
|
264
|
+
const form = await this.project.formflowForm(job.form);
|
|
265
|
+
if (!form)
|
|
266
|
+
throw new formflow_1.FormflowError(404, `The form "${job.form}" was not found.`);
|
|
267
|
+
const { act, research, identity } = await this.identities(job);
|
|
268
|
+
const service = (0, formflow_1.formflow)(this.project);
|
|
269
|
+
const load = async () => {
|
|
270
|
+
const stored = await form.loadSubmission(job.submission, await act());
|
|
271
|
+
// Form.io answers a submission the caller may not read as missing, so say both.
|
|
272
|
+
if (!stored)
|
|
273
|
+
throw new formflow_1.FormflowError(404, `Submission "${job.submission}" was not found, or the user the run acts as may not read it (give submitters Read Own access, or the action an agent role).`);
|
|
274
|
+
return stored;
|
|
275
|
+
};
|
|
276
|
+
let stored = await load();
|
|
277
|
+
for (let attempt = 1;; attempt++) {
|
|
278
|
+
const run = await service.runTask(form, job.task, stored, research, { answer: job.answer, stored: true, identity, requestId: job.requestId });
|
|
279
|
+
const engines = run.results.map((result) => ({ kind: result.kind, engine: result.engine ?? null, model: result.model ?? null }));
|
|
280
|
+
const failed = run.failed.map((group) => group.error || 'no engine could answer').join('; ');
|
|
281
|
+
const changed = Object.keys(run.written).length > 0 || run.cleared.length > 0;
|
|
282
|
+
const refused = run.refused || {};
|
|
283
|
+
if (Object.keys(refused).length) {
|
|
284
|
+
console.warn(`[formio-uag] Run task "${job.task}" left out answers the form refused for submission ${job.submission} of "${job.form}": ` +
|
|
285
|
+
Object.entries(refused).map(([path, message]) => `${path} (${message})`).join('; '));
|
|
286
|
+
}
|
|
287
|
+
const left = Object.keys(refused).length ? { refused } : {};
|
|
288
|
+
if (!changed) {
|
|
289
|
+
return { status: failed ? 'failed' : 'done', written: {}, cleared: [], engines, saved: false, ...left, ...(failed && { error: failed }) };
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
// Only the task's fields changed; `modified` refuses the save if a person saved in the meantime.
|
|
293
|
+
await form.submit({ _id: stored._id, modified: stored.modified, data: run.submission.data, metadata: run.submission.metadata }, await act(), {
|
|
294
|
+
[exports.RUN_TASK_SAVE]: true,
|
|
295
|
+
[exports.RUN_TASK_ACTING_FOR]: job.actingFor,
|
|
296
|
+
...(job.requestId && { [exports.RUN_TASK_REQUEST_ID]: job.requestId }),
|
|
297
|
+
// The runs this save starts act as the user, never with this run's agent role.
|
|
298
|
+
...(job.caller ? { [exports.RUN_TASK_CALLER]: job.caller, [exports.RUN_TASK_IDENTITY]: job.identity } : { [exports.RUN_TASK_AS_USER]: true }),
|
|
299
|
+
[exports.RUN_TASK_CHAIN]: (job.chain || 0) + 1
|
|
300
|
+
});
|
|
301
|
+
return { status: failed ? 'failed' : 'done', written: run.written, cleared: run.cleared, engines, saved: true, ...left, ...(failed && { error: failed }) };
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
if (err?.name !== 'ConflictError' || attempt >= 2) {
|
|
305
|
+
if (err?.name === 'ConflictError') {
|
|
306
|
+
throw new formflow_1.FormflowError(409, 'The submission changed while its task was running, and again on the second try. Nothing was saved.');
|
|
307
|
+
}
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
stored = await load();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
exports.TaskRunner = TaskRunner;
|
|
316
|
+
/** What a record keeps of a run's outcome: paths, never values. */
|
|
317
|
+
function resultOf(outcome) {
|
|
318
|
+
return {
|
|
319
|
+
status: outcome.status,
|
|
320
|
+
engines: outcome.engines,
|
|
321
|
+
written: Object.keys(outcome.written),
|
|
322
|
+
cleared: outcome.cleared,
|
|
323
|
+
...(outcome.refused && { refused: Object.keys(outcome.refused) }),
|
|
324
|
+
saved: outcome.saved,
|
|
325
|
+
...(outcome.error && { error: outcome.error })
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/** The job a durable record describes. */
|
|
329
|
+
function jobOf(record) {
|
|
330
|
+
return {
|
|
331
|
+
form: record.formName,
|
|
332
|
+
formId: record.form,
|
|
333
|
+
submission: record.submission,
|
|
334
|
+
task: record.task,
|
|
335
|
+
answer: record.answer,
|
|
336
|
+
actingFor: record.actingFor,
|
|
337
|
+
agentRole: record.agentRole,
|
|
338
|
+
action: record.action,
|
|
339
|
+
method: record.method,
|
|
340
|
+
...(record.requestId && { requestId: record.requestId }),
|
|
341
|
+
...(record.chain && { chain: record.chain })
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/** The project's runner, created on first use. */
|
|
345
|
+
const runners = new WeakMap();
|
|
346
|
+
function taskRunner(project) {
|
|
347
|
+
let runner = runners.get(project);
|
|
348
|
+
if (!runner) {
|
|
349
|
+
runner = new TaskRunner(project);
|
|
350
|
+
runners.set(project, runner);
|
|
351
|
+
}
|
|
352
|
+
return runner;
|
|
353
|
+
}
|
package/lib/runs.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb';
|
|
2
|
+
import type { ActingFor } from './identity';
|
|
3
|
+
import { LEASE_MS, MAX_ATTEMPTS, RENEW_MS } from './formflow';
|
|
4
|
+
import type { CoreNewTaskRun, CoreTaskRunRecord, CoreTaskRunStore, TaskRunStatus } from './formflow';
|
|
5
|
+
/**
|
|
6
|
+
* Run task records in the UAG's database (`uag-taskruns`): one per run, for
|
|
7
|
+
* audit (Action Logs), and, for runs that act with an agent role, the queue
|
|
8
|
+
* that lets a run survive a restart and be taken by one replica at a time.
|
|
9
|
+
* A record never holds a token.
|
|
10
|
+
*
|
|
11
|
+
* The store is Agentic Formflow's (`useDatabase`); the UAG gives it the
|
|
12
|
+
* collection and its ids.
|
|
13
|
+
*/
|
|
14
|
+
export type { TaskRunStatus };
|
|
15
|
+
export type TaskRunRecord = CoreTaskRunRecord<ObjectId> & {
|
|
16
|
+
actingFor: ActingFor;
|
|
17
|
+
};
|
|
18
|
+
export type NewTaskRun = CoreNewTaskRun<ObjectId> & {
|
|
19
|
+
actingFor: ActingFor;
|
|
20
|
+
};
|
|
21
|
+
export type TaskRunStore = CoreTaskRunStore<ObjectId>;
|
|
22
|
+
export { LEASE_MS, MAX_ATTEMPTS, RENEW_MS };
|
|
23
|
+
/** The ids of the UAG's database: MongoDB ObjectIds. */
|
|
24
|
+
export declare const OBJECT_IDS: {
|
|
25
|
+
create: () => ObjectId;
|
|
26
|
+
parse: (id: string) => ObjectId | null;
|
|
27
|
+
};
|
package/lib/runs.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OBJECT_IDS = exports.RENEW_MS = exports.MAX_ATTEMPTS = exports.LEASE_MS = void 0;
|
|
4
|
+
const mongodb_1 = require("mongodb");
|
|
5
|
+
const formflow_1 = require("./formflow");
|
|
6
|
+
Object.defineProperty(exports, "LEASE_MS", { enumerable: true, get: function () { return formflow_1.LEASE_MS; } });
|
|
7
|
+
Object.defineProperty(exports, "MAX_ATTEMPTS", { enumerable: true, get: function () { return formflow_1.MAX_ATTEMPTS; } });
|
|
8
|
+
Object.defineProperty(exports, "RENEW_MS", { enumerable: true, get: function () { return formflow_1.RENEW_MS; } });
|
|
9
|
+
/** The ids of the UAG's database: MongoDB ObjectIds. */
|
|
10
|
+
exports.OBJECT_IDS = {
|
|
11
|
+
create: () => new mongodb_1.ObjectId(),
|
|
12
|
+
parse: (id) => (mongodb_1.ObjectId.isValid(id) ? new mongodb_1.ObjectId(id) : null)
|
|
13
|
+
};
|
package/lib/template.js
CHANGED
|
@@ -89,18 +89,16 @@ class UAGTemplate {
|
|
|
89
89
|
* @returns The compiled template function
|
|
90
90
|
*/
|
|
91
91
|
getTemplate(templateName) {
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const cached = this.templateCache.get(templateName);
|
|
93
|
+
if (cached) {
|
|
94
|
+
return cached;
|
|
94
95
|
}
|
|
95
|
-
let templateContent =
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
const templatePath = path.join(path.join(__dirname, 'templates') || '', `${templateName}.md`);
|
|
96
|
+
let templateContent = this.config[templateName];
|
|
97
|
+
if (!templateContent) {
|
|
98
|
+
const templatePath = path.join(__dirname, 'templates', `${templateName}.md`);
|
|
101
99
|
if (!fs.existsSync(templatePath))
|
|
102
100
|
throw new Error(`Template '${templateName}' not found at ${templatePath}`);
|
|
103
|
-
templateContent =
|
|
101
|
+
templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
104
102
|
}
|
|
105
103
|
const compiled = (0, lodash_1.template)(templateContent);
|
|
106
104
|
this.templateCache.set(templateName, compiled);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<% if (error === 'no_uag') { %>
|
|
2
|
-
There are no `uag` fields present within the provided form. In order to use the `
|
|
3
|
-
- **Criteria**: A content component with properties `uag="<%=
|
|
4
|
-
- **Agent Fields**: Any fields you wish the agent to populate with its own data, you must flag those fields with the property `uag="<%=
|
|
2
|
+
There are no `uag` fields present within the provided form. In order to use the `agent_provide` tool, you must first flag certain fields as the **Criteria** and **Agent Fields**.
|
|
3
|
+
- **Criteria**: A content component with properties `uag="<%= task %>"` and `uagField="criteria"`. Within the content of this component, provide detailed instructions (criteria) for the agent to follow when analyzing the data.
|
|
4
|
+
- **Agent Fields**: Any fields you wish the agent to populate with its own data, you must flag those fields with the property `uag="<%= task %>"`. These fields will then be handed to the AI Agent as required fields for them to populate according to the provided **Criteria**.
|
|
5
5
|
<% } %>
|
|
6
6
|
<% if (error === 'no_criteria') { %>
|
|
7
|
-
No `criteria` field was found within this form with the properties `uag="<%=
|
|
7
|
+
No `criteria` field was found within this form with the properties `uag="<%= task %>"` and `uagField="criteria"`. To add `criteria`, add a Content component to the form and then give it the following properties: `uag="<%= task %>"` and `uagField="criteria"`. Within the content of this component, provide detailed instructions (the criteria) for the agent to follow when analyzing the data.
|
|
8
8
|
<% } %>
|
|
9
9
|
<% if (error === 'no_fields') { %>
|
|
10
|
-
There are no `uag` fields within this form under the provided `
|
|
10
|
+
There are no `uag` fields within this form under the provided `task=<%= task %>` that are available for the AI Agent to provide values for. To flag a field as fillable by the AI Agent, you must add the property and value `uag="<%= task %>"`
|
|
11
11
|
<% } %>
|
package/lib/testing.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @formio/uag/testing: test a module without a Form.io project or a running UAG.
|
|
3
|
+
*
|
|
4
|
+
* const { createTestModuleApi, testForm, testAuth, mountModuleRoutes } = require('@formio/uag/testing');
|
|
5
|
+
*
|
|
6
|
+
* const api = await createTestModuleApi(MyModule, { config: { myModule: { max: 2 } } });
|
|
7
|
+
* const app = await mountModuleRoutes(MyModule, { form: testForm(definition), auth: testAuth() });
|
|
8
|
+
* await supertest(app).post('/notes/my-module/check').send({ ... });
|
|
9
|
+
*
|
|
10
|
+
* The API is the UAG's own (errors, stream, forms, converters, engine handles,
|
|
11
|
+
* Markdown, callerKey), with stand-ins only for what needs a project: the
|
|
12
|
+
* caller's access (from `testAuth`), the license (`licensed`), engines
|
|
13
|
+
* (`engines`) and stored files (`testForm` reads base64 values; `files` for
|
|
14
|
+
* others). Settings go through the module's own `settings`, with the server's rules.
|
|
15
|
+
*/
|
|
16
|
+
import express from 'express';
|
|
17
|
+
import type { AuthRequest, ModuleForm, ServerModule } from '@formio/appserver';
|
|
18
|
+
import type { UAGModuleApi } from './modules/types';
|
|
19
|
+
/** An engine for tests: an agent engine that generates. */
|
|
20
|
+
export type TestEngine = {
|
|
21
|
+
name: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
accepts?: Array<'text' | 'pdf'>;
|
|
24
|
+
generate(request: any): any;
|
|
25
|
+
kind?: 'agent';
|
|
26
|
+
};
|
|
27
|
+
export type TestModuleOptions = {
|
|
28
|
+
/** The settings as given, instead of running the module's `settings`. */
|
|
29
|
+
settings?: Record<string, any>;
|
|
30
|
+
/** The configuration and environment the module's `settings` gets ({} by default). */
|
|
31
|
+
config?: Record<string, any>;
|
|
32
|
+
env?: Record<string, string | undefined>;
|
|
33
|
+
/** Plugins to register beside the module's own (document plugins, converters). */
|
|
34
|
+
plugins?: any[];
|
|
35
|
+
/** The engines `api.engines.select()` chooses from. */
|
|
36
|
+
engines?: TestEngine[];
|
|
37
|
+
/** Whether Agentic Formflow is licensed (true): false makes `requireLicense` and engine handles' `generate` refuse with 403. */
|
|
38
|
+
licensed?: boolean;
|
|
39
|
+
/** Reads a stored file value that is not base64 (the test form reads those). */
|
|
40
|
+
files?: (value: any, options: {
|
|
41
|
+
component?: any;
|
|
42
|
+
maxBytes?: number;
|
|
43
|
+
}) => Buffer | null | Promise<Buffer | null>;
|
|
44
|
+
};
|
|
45
|
+
/** A form for tests, as module routes get it: `definition` is the form's JSON. */
|
|
46
|
+
export declare function testForm(definition: Record<string, any>, options?: {
|
|
47
|
+
id?: string;
|
|
48
|
+
files?: TestModuleOptions['files'];
|
|
49
|
+
}): ModuleForm & {
|
|
50
|
+
readFile(value: any, auth: any, options?: any): Promise<Buffer | null>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A caller for tests: a user with these permissions on every form (create and
|
|
54
|
+
* update by default), as a list (`['read']`) or as `formPermissions()` gives
|
|
55
|
+
* them (`{ read: true }`).
|
|
56
|
+
*/
|
|
57
|
+
export declare function testAuth(options?: {
|
|
58
|
+
user?: Record<string, any>;
|
|
59
|
+
permissions?: Array<'create' | 'read' | 'update' | 'delete'> | Partial<Record<'create' | 'read' | 'update' | 'delete', boolean>>;
|
|
60
|
+
admin?: boolean;
|
|
61
|
+
}): AuthRequest;
|
|
62
|
+
/** The UAG's module API for a module, in a test. */
|
|
63
|
+
export declare function createTestModuleApi<Settings extends Record<string, any> = Record<string, any>>(module: ServerModule<any>, options?: TestModuleOptions): Promise<UAGModuleApi<Settings>>;
|
|
64
|
+
/**
|
|
65
|
+
* The module's form routes on an Express app, as the UAG mounts them on a
|
|
66
|
+
* form (by its path, and by `form/<id>`), for supertest: with the form, the
|
|
67
|
+
* caller, a request id, the access check and the UAG's error shape. No rate
|
|
68
|
+
* limits. `api` is created with `options` unless given.
|
|
69
|
+
*/
|
|
70
|
+
export declare function mountModuleRoutes(module: ServerModule<any>, options: TestModuleOptions & {
|
|
71
|
+
form: ReturnType<typeof testForm>;
|
|
72
|
+
auth?: AuthRequest;
|
|
73
|
+
api?: UAGModuleApi;
|
|
74
|
+
}): Promise<express.Express>;
|