@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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AGENT_BODY_LIMIT = void 0;
|
|
37
|
+
exports.agentBodyLimit = agentBodyLimit;
|
|
38
|
+
exports.agentEndpoints = agentEndpoints;
|
|
39
|
+
exports.AgentRouter = AgentRouter;
|
|
40
|
+
exports.agentNotFound = agentNotFound;
|
|
41
|
+
exports.agentCredentials = agentCredentials;
|
|
42
|
+
exports.FormAgentRouter = FormAgentRouter;
|
|
43
|
+
const lodash_1 = require("lodash");
|
|
44
|
+
const express_1 = __importStar(require("express"));
|
|
45
|
+
const errors_1 = require("./errors");
|
|
46
|
+
const formflow_1 = require("../formflow");
|
|
47
|
+
const requestId_1 = require("./requestId");
|
|
48
|
+
const anonymous_1 = require("./anonymous");
|
|
49
|
+
const chat_1 = require("./chat");
|
|
50
|
+
const debug = require('debug')('formio:uag:agent');
|
|
51
|
+
function wantsStream(req) {
|
|
52
|
+
return String(req.get('accept') || '').includes('text/event-stream');
|
|
53
|
+
}
|
|
54
|
+
/** Server-Sent Events over the POST response. */
|
|
55
|
+
function openStream(res) {
|
|
56
|
+
res.status(200).set({
|
|
57
|
+
'Content-Type': 'text/event-stream',
|
|
58
|
+
'Cache-Control': 'no-cache',
|
|
59
|
+
'Connection': 'keep-alive',
|
|
60
|
+
'X-Accel-Buffering': 'no'
|
|
61
|
+
});
|
|
62
|
+
res.flushHeaders?.();
|
|
63
|
+
return (event, data) => {
|
|
64
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Who is asking, and who the operation runs as. */
|
|
68
|
+
function contextOf(req) {
|
|
69
|
+
return { auth: req.auth, requestId: req.requestId, identity: (0, formflow_1.callerIdentity)(req.auth, req) };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The task the request names, if any. A request that still names a 1.x
|
|
73
|
+
* `persona` is refused: ignoring it would run every task, an engine call each.
|
|
74
|
+
*/
|
|
75
|
+
function taskOf(req) {
|
|
76
|
+
const named = (value) => typeof value === 'string' && value.trim() !== '';
|
|
77
|
+
const task = req.body?.task;
|
|
78
|
+
if (named(req.body?.persona) && !named(task)) {
|
|
79
|
+
throw new formflow_1.FormflowError(400, 'UAG 2.0 renamed "persona" to "task". Send "task" instead.');
|
|
80
|
+
}
|
|
81
|
+
return named(task) ? task : undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Answer extract, streaming per-engine results when the client asks for a
|
|
85
|
+
* stream: decision results arrive before slower LLM results.
|
|
86
|
+
*/
|
|
87
|
+
async function respondWithStream(req, res, run) {
|
|
88
|
+
const stream = wantsStream(req);
|
|
89
|
+
let send = null;
|
|
90
|
+
const done = await run({
|
|
91
|
+
...contextOf(req),
|
|
92
|
+
onStart: () => {
|
|
93
|
+
if (stream)
|
|
94
|
+
send = openStream(res);
|
|
95
|
+
},
|
|
96
|
+
onResult: (result) => {
|
|
97
|
+
if (!send)
|
|
98
|
+
return;
|
|
99
|
+
send('result', (0, formflow_1.resultEvent)(result));
|
|
100
|
+
if (result.error)
|
|
101
|
+
send('error', { kind: result.kind, task: result.task, error: result.error, attempts: result.attempts });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
if (send) {
|
|
105
|
+
send('done', done);
|
|
106
|
+
res.end();
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
res.json(done);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** The largest request body an agent operation takes by default, in bytes. */
|
|
113
|
+
exports.AGENT_BODY_LIMIT = 5 * 1024 * 1024;
|
|
114
|
+
/**
|
|
115
|
+
* The largest request body the project's agent operations take: `agentApi.bodyLimit`,
|
|
116
|
+
* then AGENT_BODY_LIMIT, then 5 MB. A document sent inline (a base64 file a task reads)
|
|
117
|
+
* may need more; the server's own MAX_BODY_SIZE (16 MB) still applies first.
|
|
118
|
+
*/
|
|
119
|
+
function agentBodyLimit(project) {
|
|
120
|
+
const configured = Number(project.config?.agentApi?.bodyLimit ?? process.env.AGENT_BODY_LIMIT);
|
|
121
|
+
return Number.isFinite(configured) && configured > 0 ? configured : exports.AGENT_BODY_LIMIT;
|
|
122
|
+
}
|
|
123
|
+
/** The parts every Agent API endpoint shares: the request id, and errors answered as JSON (or as an event mid-stream). */
|
|
124
|
+
function agentEndpoints(project) {
|
|
125
|
+
const limit = agentBodyLimit(project);
|
|
126
|
+
const parse = express_1.default.json({ limit });
|
|
127
|
+
// The server parses JSON bodies first, with its own larger limit, so the
|
|
128
|
+
// agent operations' limit is checked here on a body already read.
|
|
129
|
+
const json = (req, res, next) => {
|
|
130
|
+
if (req.body === undefined)
|
|
131
|
+
return parse(req, res, next);
|
|
132
|
+
const length = Number(req.get('content-length')) || Buffer.byteLength(JSON.stringify(req.body ?? null));
|
|
133
|
+
if (length > limit) {
|
|
134
|
+
return res.status(413).json({ error: `The request body is larger than the agent operations' limit of ${Math.round(limit / (1024 * 1024) * 10) / 10} MB.`, code: 'payload_too_large' });
|
|
135
|
+
}
|
|
136
|
+
next();
|
|
137
|
+
};
|
|
138
|
+
// The request id, usually assigned already by agentErrorShape, ahead of every
|
|
139
|
+
// refusal. One that is not a short token is replaced, so it is never echoed,
|
|
140
|
+
// logged or sent on.
|
|
141
|
+
const requestId = (req, res, next) => {
|
|
142
|
+
(0, errors_1.requestIdOf)(req, res);
|
|
143
|
+
next();
|
|
144
|
+
};
|
|
145
|
+
const handle = (operation, fn) => async (req, res) => {
|
|
146
|
+
const agentReq = req;
|
|
147
|
+
const service = (0, formflow_1.formflow)(project);
|
|
148
|
+
try {
|
|
149
|
+
service.takeCaller(operation, agentReq.auth);
|
|
150
|
+
// Remote engines send the request id on to the engine server.
|
|
151
|
+
await (0, requestId_1.withRequestId)(agentReq.requestId, () => fn(agentReq, res, service));
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const { status, body } = (0, errors_1.agentError)(agentReq.requestId, err);
|
|
155
|
+
if (status === 500) {
|
|
156
|
+
debug(`[${agentReq.requestId}] ${operation} failed:`, err);
|
|
157
|
+
}
|
|
158
|
+
if (err instanceof formflow_1.RateLimitError) {
|
|
159
|
+
res.set('Retry-After', String(Math.ceil(err.retryAfterMs / 1000)));
|
|
160
|
+
}
|
|
161
|
+
if (res.headersSent) {
|
|
162
|
+
// Mid-stream: report the failure as an event and close.
|
|
163
|
+
res.write(`event: error\ndata: ${JSON.stringify(body)}\n\n`);
|
|
164
|
+
res.end();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
res.status(status).json(body);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
return { json, requestId, handle };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* `GET /agent/capabilities`, behind `authenticate`. Only this path is claimed:
|
|
175
|
+
* any other `/agent...` path (a project form at `agent`, for one) falls through
|
|
176
|
+
* to the routes after this router.
|
|
177
|
+
*/
|
|
178
|
+
function AgentRouter(project, authenticate) {
|
|
179
|
+
const router = (0, express_1.Router)();
|
|
180
|
+
const { json, requestId, handle } = agentEndpoints(project);
|
|
181
|
+
router.get('/agent/capabilities', authenticate, json, requestId, handle('capabilities', async (req, res, service) => {
|
|
182
|
+
const capabilities = await service.capabilities();
|
|
183
|
+
// Chat is part of conversation forms: it needs an agent engine, never a license.
|
|
184
|
+
const engines = (0, chat_1.chatEngines)(project).map((engine) => engine.name);
|
|
185
|
+
capabilities.operations.chat = { available: engines.length > 0, engines };
|
|
186
|
+
res.json({ requestId: req.requestId, ...capabilities });
|
|
187
|
+
}));
|
|
188
|
+
return router;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A form's agent operation for a path that names no form of the project (the
|
|
192
|
+
* old root operations, a deleted form): answered 404 here, so it is never
|
|
193
|
+
* passed through to Form.io. A form that exists goes on to its own routes,
|
|
194
|
+
* which answer 404 themselves when it is not tagged `uag`.
|
|
195
|
+
*/
|
|
196
|
+
function agentNotFound(project) {
|
|
197
|
+
// Express matches routes ignoring case, so this does too.
|
|
198
|
+
const operation = /^(.*)\/agent\/(extract|decide|generate|chat)\/?$/i;
|
|
199
|
+
return async (req, res, next) => {
|
|
200
|
+
const match = req.method === 'POST' ? operation.exec(req.path) : null;
|
|
201
|
+
if (!match)
|
|
202
|
+
return next();
|
|
203
|
+
// The form's path under the project's mount: the root on Open Source,
|
|
204
|
+
// otherwise `/{name}` or `/project/{id}`, and nothing else.
|
|
205
|
+
const mounts = [/^\/project\/[0-9a-fA-F]{24}(?=\/|$)/i];
|
|
206
|
+
if (project.name)
|
|
207
|
+
mounts.push(new RegExp(`^/${(0, lodash_1.escapeRegExp)(project.name)}(?=/|$)`, 'i'));
|
|
208
|
+
let formPath = match[1];
|
|
209
|
+
const mount = mounts.find((pattern) => pattern.test(formPath));
|
|
210
|
+
if (mount)
|
|
211
|
+
formPath = formPath.replace(mount, '');
|
|
212
|
+
else if (!project.oss)
|
|
213
|
+
formPath = '';
|
|
214
|
+
formPath = formPath.replace(/^\/+/, '');
|
|
215
|
+
const byId = /^form\/([0-9a-fA-F]{24})$/i.exec(formPath);
|
|
216
|
+
const forms = Object.values(project.forms || {});
|
|
217
|
+
const known = byId
|
|
218
|
+
? forms.some((form) => String(form.form?._id || '') === byId[1]) || !!(await project.getKnownFormById?.(byId[1]))
|
|
219
|
+
: !!formPath && forms.some((form) => String(form.path || '').toLowerCase() === formPath.toLowerCase());
|
|
220
|
+
if (known)
|
|
221
|
+
return next();
|
|
222
|
+
res.status(404).json({ error: `The form "${byId ? byId[1] : formPath}" does not exist or is not tagged "uag".`, code: 'form_not_found' });
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function refuse(res, error) {
|
|
226
|
+
return res.status(401).json({ error, code: 'unauthorized' });
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Who may call a form's agent operations, on top of the project's own
|
|
230
|
+
* authentication (Form.io user tokens, portal remote tokens, server keys):
|
|
231
|
+
* scoped tokens never (they are for /mcp), server keys only from servers, and
|
|
232
|
+
* UAG bearer tokens, verified here. A caller with no credential gets through
|
|
233
|
+
* only where the form allows anonymous use (see anonymousGate).
|
|
234
|
+
*/
|
|
235
|
+
function agentCredentials(project) {
|
|
236
|
+
const anonymous = (0, anonymous_1.anonymousGate)(project);
|
|
237
|
+
return (req, res, next) => {
|
|
238
|
+
const request = req;
|
|
239
|
+
const authorization = req.get('authorization');
|
|
240
|
+
const bearer = (0, formflow_1.bearerOf)(authorization);
|
|
241
|
+
// Scoped tokens are for a task's calls back into /mcp. They must never
|
|
242
|
+
// start new work: an admin scoped token would otherwise grant full
|
|
243
|
+
// admin access to the agent operations for its lifetime.
|
|
244
|
+
if (bearer && (0, formflow_1.isScopedToken)(bearer)) {
|
|
245
|
+
return refuse(res, 'Scoped tokens can only be used on /mcp.');
|
|
246
|
+
}
|
|
247
|
+
if (req.get('x-token') !== undefined || req.get('x-admin-key') !== undefined) {
|
|
248
|
+
// Browsers send Origin on cross-origin requests, and Sec-Fetch-Site on all
|
|
249
|
+
// of them. (Node's fetch sends Sec-Fetch-Mode but not Sec-Fetch-Site.)
|
|
250
|
+
if (req.get('origin') || req.get('sec-fetch-site')) {
|
|
251
|
+
return refuse(res, 'Admin credentials cannot be used from a browser. Use a bearer token.');
|
|
252
|
+
}
|
|
253
|
+
return request.auth?.admin ? next() : refuse(res, 'Invalid admin credential.');
|
|
254
|
+
}
|
|
255
|
+
if (request.auth?.admin || request.auth?.user) {
|
|
256
|
+
return next();
|
|
257
|
+
}
|
|
258
|
+
if (bearer) {
|
|
259
|
+
const form = request.form;
|
|
260
|
+
return project.authorizeRequest(request, res, () => {
|
|
261
|
+
if (request.auth)
|
|
262
|
+
request.auth.form = form || null;
|
|
263
|
+
next();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return anonymous(req, res, next);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A form's agent operations, `POST /agent/extract|decide|generate|chat`, mounted on
|
|
271
|
+
* the form's own routes (see UAGFormInterface.router), so `req.form` is the
|
|
272
|
+
* form of the URL.
|
|
273
|
+
*/
|
|
274
|
+
function FormAgentRouter(project, credentials) {
|
|
275
|
+
const router = (0, express_1.Router)();
|
|
276
|
+
const { json, requestId, handle } = agentEndpoints(project);
|
|
277
|
+
const route = (path, operation, fn) => {
|
|
278
|
+
router.post(`/agent/${path}`, credentials, json, requestId, handle(operation, fn));
|
|
279
|
+
};
|
|
280
|
+
const formOf = (req) => req.form?.name;
|
|
281
|
+
route('extract', 'extract', async (req, res, service) => {
|
|
282
|
+
const { text, data, overwrite } = req.body || {};
|
|
283
|
+
const task = taskOf(req);
|
|
284
|
+
await respondWithStream(req, res, (context) => service.extract({ form: formOf(req), task, text, data, overwrite }, context));
|
|
285
|
+
});
|
|
286
|
+
route('decide', 'decide', async (req, res, service) => {
|
|
287
|
+
const { data, submissionId, overwrite } = req.body || {};
|
|
288
|
+
res.json(await service.decide({ form: formOf(req), task: taskOf(req), data, submissionId, overwrite }, contextOf(req)));
|
|
289
|
+
});
|
|
290
|
+
route('generate', 'generate', async (req, res, service) => {
|
|
291
|
+
const { data, overwrite } = req.body || {};
|
|
292
|
+
res.json(await service.generate({ form: formOf(req), task: taskOf(req), data, overwrite }, contextOf(req)));
|
|
293
|
+
});
|
|
294
|
+
// A turn of the form's chat. Not Agentic Formflow: no license check, as for the MCP tools.
|
|
295
|
+
route('chat', 'chat', async (req, res, service) => {
|
|
296
|
+
const form = await project.formflowForm(formOf(req));
|
|
297
|
+
if (!form) {
|
|
298
|
+
throw new formflow_1.FormflowError(404, `The form "${formOf(req)}" does not exist or is not tagged "uag".`, 'form_not_found');
|
|
299
|
+
}
|
|
300
|
+
if ((0, formflow_1.flagOff)(form.form?.properties?.uagChat)) {
|
|
301
|
+
throw new formflow_1.FormflowError(403, `Chat is turned off for the form "${form.formflowName}".`, 'chat_disabled');
|
|
302
|
+
}
|
|
303
|
+
service.requireAccess(req.auth, form, ['create']);
|
|
304
|
+
res.json(await (0, chat_1.chatTurn)(project, form, req.body, { auth: req.auth, requestId: req.requestId }));
|
|
305
|
+
});
|
|
306
|
+
return router;
|
|
307
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { GenerateRequest } from '@formio/uag-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Stand-in engines for tests and local development. They answer instantly with
|
|
4
|
+
* predictable values, so the Agent API can be exercised end to end without
|
|
5
|
+
* model credentials: `UAG_ENGINES=stub-decision,stub-agent`. Core creates
|
|
6
|
+
* them, so they keep the standing that lets them run without a license under
|
|
7
|
+
* any instance name.
|
|
8
|
+
*/
|
|
9
|
+
export declare const StubDecisionEngine: Readonly<{
|
|
10
|
+
type: "engine";
|
|
11
|
+
name: string;
|
|
12
|
+
title: string;
|
|
13
|
+
kinds: "decision"[];
|
|
14
|
+
apiVersion: 1;
|
|
15
|
+
description: string;
|
|
16
|
+
create: ({ name }: import("@formio/uag-sdk").EngineCreateContext<any>) => import("@formio/appserver-core/lib/formflow").DecisionEngine & {
|
|
17
|
+
calls: number;
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
20
|
+
/** How many generate requests the stub agent engines keep: a UAG running them for a demo never grows without bound. */
|
|
21
|
+
export declare const STUB_GENERATE_REQUESTS_KEPT = 20;
|
|
22
|
+
/** The last generate requests the stub agent engines received (STUB_GENERATE_REQUESTS_KEPT), oldest first, for tests. */
|
|
23
|
+
export declare const stubGenerateRequests: Array<Omit<GenerateRequest, 'signal' | 'onProgress'> & {
|
|
24
|
+
engine: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare const StubAgentEngine: Readonly<{
|
|
27
|
+
type: "engine";
|
|
28
|
+
name: string;
|
|
29
|
+
title: string;
|
|
30
|
+
kinds: "agent"[];
|
|
31
|
+
apiVersion: 1;
|
|
32
|
+
description: string;
|
|
33
|
+
create: ({ name }: import("@formio/uag-sdk").EngineCreateContext<any>) => import("@formio/appserver-core/lib/formflow").AgentEngine & {
|
|
34
|
+
calls: number;
|
|
35
|
+
lastExtract?: import("@formio/appserver-core/lib/formflow").ExtractRequest;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StubAgentEngine = exports.stubGenerateRequests = exports.STUB_GENERATE_REQUESTS_KEPT = exports.StubDecisionEngine = void 0;
|
|
4
|
+
const uag_sdk_1 = require("@formio/uag-sdk");
|
|
5
|
+
const formflow_1 = require("../formflow");
|
|
6
|
+
/**
|
|
7
|
+
* Stand-in engines for tests and local development. They answer instantly with
|
|
8
|
+
* predictable values, so the Agent API can be exercised end to end without
|
|
9
|
+
* model credentials: `UAG_ENGINES=stub-decision,stub-agent`. Core creates
|
|
10
|
+
* them, so they keep the standing that lets them run without a license under
|
|
11
|
+
* any instance name.
|
|
12
|
+
*/
|
|
13
|
+
exports.StubDecisionEngine = (0, uag_sdk_1.defineEngine)({
|
|
14
|
+
type: 'engine',
|
|
15
|
+
name: 'stub-decision',
|
|
16
|
+
title: 'Stub decision engine',
|
|
17
|
+
kinds: ['decision'],
|
|
18
|
+
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
19
|
+
description: 'Answers every decision with a fixed, predictable value. For tests and local development.',
|
|
20
|
+
create: ({ name }) => (0, formflow_1.stubDecisionEngine)({ name }),
|
|
21
|
+
});
|
|
22
|
+
/** How many generate requests the stub agent engines keep: a UAG running them for a demo never grows without bound. */
|
|
23
|
+
exports.STUB_GENERATE_REQUESTS_KEPT = 20;
|
|
24
|
+
/** The last generate requests the stub agent engines received (STUB_GENERATE_REQUESTS_KEPT), oldest first, for tests. */
|
|
25
|
+
exports.stubGenerateRequests = [];
|
|
26
|
+
exports.StubAgentEngine = (0, uag_sdk_1.defineEngine)({
|
|
27
|
+
type: 'engine',
|
|
28
|
+
name: 'stub-agent',
|
|
29
|
+
title: 'Stub agent engine',
|
|
30
|
+
kinds: ['agent'],
|
|
31
|
+
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
32
|
+
description: 'Extracts nothing; generates a predictable answer from the text it is given. For tests and local development.',
|
|
33
|
+
// Text only; its generate answer shows what reached it. Observing its requests keeps it a built-in stub.
|
|
34
|
+
create: ({ name }) => (0, formflow_1.stubAgentEngine)({ name, onGenerate: (request) => {
|
|
35
|
+
exports.stubGenerateRequests.push({ ...request, engine: name });
|
|
36
|
+
if (exports.stubGenerateRequests.length > exports.STUB_GENERATE_REQUESTS_KEPT)
|
|
37
|
+
exports.stubGenerateRequests.splice(0, exports.stubGenerateRequests.length - exports.STUB_GENERATE_REQUESTS_KEPT);
|
|
38
|
+
} }),
|
|
39
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FormflowTool } from "../formflow";
|
|
2
|
+
import type { AuthRequest } from "@formio/appserver";
|
|
3
|
+
import type { UAGProjectInterface } from "../UAGProjectInterface";
|
|
4
|
+
import { ToolEntry, ToolInfo } from "../tools/utils";
|
|
5
|
+
/** Record the module a tool came from: its `execute` gets the module's API as `extra.module`. */
|
|
6
|
+
export declare function setToolModule(entry: object, api: any): void;
|
|
7
|
+
/**
|
|
8
|
+
* A `config.tools` entry as a tool for the project: a bound tool's static
|
|
9
|
+
* fields, overlaid with what its `bind(project)` returns. A tool without
|
|
10
|
+
* `bind` (the 1.x shape) is used as it is.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveTool(project: UAGProjectInterface, entry: ToolEntry): Promise<ToolInfo | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Every tool the UAG serves, including custom tools from configuration, each
|
|
15
|
+
* entry of which is a tool, optionally bound to the project.
|
|
16
|
+
*/
|
|
17
|
+
export declare function allTools(project: UAGProjectInterface): Promise<ToolInfo[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Run a tool with its arguments and the `ToolExtra` for the project: the same
|
|
20
|
+
* call on `/mcp` and for Agent API research. `context` is whatever else the
|
|
21
|
+
* caller has (on `/mcp`, the MCP SDK's request context, with `authInfo`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function runTool(tool: ToolInfo, project: UAGProjectInterface, args: any, context: {
|
|
24
|
+
authInfo: AuthRequest;
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
}): any;
|
|
27
|
+
/**
|
|
28
|
+
* An MCP tool as an Agentic Formflow tool: its schema as JSON, its arguments
|
|
29
|
+
* validated with its own schema, and read-only only when annotated so.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formflowTool(tool: ToolInfo, project: UAGProjectInterface): FormflowTool;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setToolModule = setToolModule;
|
|
7
|
+
exports.resolveTool = resolveTool;
|
|
8
|
+
exports.allTools = allTools;
|
|
9
|
+
exports.runTool = runTool;
|
|
10
|
+
exports.formflowTool = formflowTool;
|
|
11
|
+
const zod_1 = __importDefault(require("zod"));
|
|
12
|
+
const tools_1 = require("../tools");
|
|
13
|
+
const utils_1 = require("../tools/utils");
|
|
14
|
+
/** The module API of the module each `config.tools` entry came from, for its `extra.module`. */
|
|
15
|
+
const toolModules = new WeakMap();
|
|
16
|
+
/** Record the module a tool came from: its `execute` gets the module's API as `extra.module`. */
|
|
17
|
+
function setToolModule(entry, api) {
|
|
18
|
+
if (entry && typeof entry === 'object')
|
|
19
|
+
toolModules.set(entry, api);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A `config.tools` entry as a tool for the project: a bound tool's static
|
|
23
|
+
* fields, overlaid with what its `bind(project)` returns. A tool without
|
|
24
|
+
* `bind` (the 1.x shape) is used as it is.
|
|
25
|
+
*/
|
|
26
|
+
async function resolveTool(project, entry) {
|
|
27
|
+
if (!entry || typeof entry.bind !== 'function')
|
|
28
|
+
return entry;
|
|
29
|
+
const { bind, ...fixed } = entry;
|
|
30
|
+
const tool = { ...fixed, ...((await bind(project)) || {}) };
|
|
31
|
+
if (toolModules.has(entry))
|
|
32
|
+
toolModules.set(tool, toolModules.get(entry));
|
|
33
|
+
return tool;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every tool the UAG serves, including custom tools from configuration, each
|
|
37
|
+
* entry of which is a tool, optionally bound to the project.
|
|
38
|
+
*/
|
|
39
|
+
async function allTools(project) {
|
|
40
|
+
(0, utils_1.assertToolEntries)(project.config?.tools);
|
|
41
|
+
const custom = await Promise.all((project.config?.tools || []).map((entry) => resolveTool(project, entry)));
|
|
42
|
+
return [...await (0, tools_1.getTools)(project), ...custom].filter((tool) => !!tool?.name);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run a tool with its arguments and the `ToolExtra` for the project: the same
|
|
46
|
+
* call on `/mcp` and for Agent API research. `context` is whatever else the
|
|
47
|
+
* caller has (on `/mcp`, the MCP SDK's request context, with `authInfo`).
|
|
48
|
+
*/
|
|
49
|
+
function runTool(tool, project, args, context) {
|
|
50
|
+
if (!tool.execute) {
|
|
51
|
+
return { content: [{ type: 'text', text: `The tool "${tool.name}" has no implementation.` }], isError: true };
|
|
52
|
+
}
|
|
53
|
+
const module = toolModules.get(tool);
|
|
54
|
+
const extra = { ...context, authInfo: context.authInfo, project, ...(module && { module }) };
|
|
55
|
+
return tool.execute(args ?? {}, extra);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* An MCP tool as an Agentic Formflow tool: its schema as JSON, its arguments
|
|
59
|
+
* validated with its own schema, and read-only only when annotated so.
|
|
60
|
+
*/
|
|
61
|
+
function formflowTool(tool, project) {
|
|
62
|
+
const schema = zod_1.default.object(tool.inputSchema || {});
|
|
63
|
+
const inputSchema = zod_1.default.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' });
|
|
64
|
+
delete inputSchema.$schema;
|
|
65
|
+
return {
|
|
66
|
+
name: tool.name,
|
|
67
|
+
description: tool.description || tool.title || tool.name,
|
|
68
|
+
readOnly: tool.annotations?.readOnlyHint === true,
|
|
69
|
+
inputSchema,
|
|
70
|
+
parse: (args) => {
|
|
71
|
+
const parsed = schema.safeParse(args || {});
|
|
72
|
+
return parsed.success ? { ok: true, value: parsed.data } : { ok: false, error: parsed.error.message };
|
|
73
|
+
},
|
|
74
|
+
execute: async (args, auth) => runTool(tool, project, args, { authInfo: auth })
|
|
75
|
+
};
|
|
76
|
+
}
|
package/lib/config.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ServerConfig } from '@formio/appserver';
|
|
2
|
-
import {
|
|
2
|
+
import type { FormflowAgentApiConfig, FormflowRateLimit, FormflowRoutingConfig } from './formflow';
|
|
3
|
+
import type { EngineInstanceSpec, EngineTypeDefinition } from '@formio/uag-sdk';
|
|
4
|
+
import type { ToolEntry, ToolInfo } from './tools/utils';
|
|
5
|
+
import type { UAGDatabaseOptions } from './database';
|
|
3
6
|
export type UAGForm = {
|
|
4
7
|
name: string;
|
|
5
8
|
title: string;
|
|
@@ -43,13 +46,104 @@ export type UAGToolOverride = {
|
|
|
43
46
|
submit_completed_form?: ToolInfo;
|
|
44
47
|
submission_update?: ToolInfo;
|
|
45
48
|
find_submissions?: ToolInfo;
|
|
46
|
-
|
|
49
|
+
agent_provide?: ToolInfo;
|
|
50
|
+
agent_decide?: ToolInfo;
|
|
47
51
|
fetch_external_data?: ToolInfo;
|
|
52
|
+
read_document?: ToolInfo;
|
|
48
53
|
};
|
|
54
|
+
/** `config.documents`: limits and choices of the AI Document module (@formio/uag-extras/modules/ai-document). */
|
|
55
|
+
export type UAGDocumentsConfig = {
|
|
56
|
+
/** Largest uploaded file read, in bytes (25 MB). DOCUMENTS_MAX_FILE_BYTES. */
|
|
57
|
+
maxFileBytes?: number;
|
|
58
|
+
/** Most files one component sends (10). */
|
|
59
|
+
maxFiles?: number;
|
|
60
|
+
/** Most bytes of one request's files (32 MB, the provider request cap). */
|
|
61
|
+
maxTotalBytes?: number;
|
|
62
|
+
/** Most pages once converted (500). DOCUMENTS_MAX_PAGES. */
|
|
63
|
+
maxPages?: number;
|
|
64
|
+
/** Most characters of text once converted (2,000,000). */
|
|
65
|
+
maxTextChars?: number;
|
|
66
|
+
/** One processing request, in ms (600000). DOCUMENTS_TIMEOUT_MS. */
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
/** Comment line interval on a streamed response, in ms (15000). */
|
|
69
|
+
heartbeatMs?: number;
|
|
70
|
+
/** Concurrent processing requests per identity (3). */
|
|
71
|
+
perUser?: number;
|
|
72
|
+
/** The engine instance documents use. Default: the first capable agent engine. DOCUMENTS_ENGINE. */
|
|
73
|
+
engine?: string;
|
|
74
|
+
/** Converter name by MIME type, when several can do a step. */
|
|
75
|
+
prefer?: Record<string, string>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* One engine instance: `"<type>"` or `"<name>:<type>"`, or `{ name, type, options }`, where the
|
|
79
|
+
* type is a type name, a package name, or an engine type definition.
|
|
80
|
+
*/
|
|
81
|
+
export type UAGEngineEntry = string | EngineInstanceSpec;
|
|
82
|
+
/** Engine preference lists for each kind of work. See FormflowRoutingConfig. */
|
|
83
|
+
export type UAGRoutingConfig = FormflowRoutingConfig;
|
|
84
|
+
/** A request allowance. See FormflowRateLimit. */
|
|
85
|
+
export type UAGRateLimit = FormflowRateLimit;
|
|
86
|
+
/** Agent API defaults: thresholds, timeouts, tools and rate limits. See FormflowAgentApiConfig. */
|
|
87
|
+
/** Limits on anonymous use of a form's agent operations, per minute in each UAG process. 0 turns anonymous use off. */
|
|
88
|
+
export interface UAGAnonymousConfig {
|
|
89
|
+
/** Requests from one client address (AGENT_ANONYMOUS_RATE_LIMIT, default 10). */
|
|
90
|
+
rateLimit?: number;
|
|
91
|
+
/** Requests from all anonymous callers together (AGENT_ANONYMOUS_TOTAL_RATE_LIMIT, default 100). */
|
|
92
|
+
totalRateLimit?: number;
|
|
93
|
+
}
|
|
94
|
+
/** A form's chat (`{form}/agent/chat`): its engines and the bounds of a turn. */
|
|
95
|
+
export interface UAGChatConfig {
|
|
96
|
+
/** Engine names, or `agent` for every agent engine, in the order to try them. Default: `['agent']`. */
|
|
97
|
+
engines?: string[];
|
|
98
|
+
/** Milliseconds to wait for one engine before trying the next. Default: the engine's routing timeout. */
|
|
99
|
+
timeout?: number;
|
|
100
|
+
/** Rounds of `check_values` calls an engine may make before it answers. Default: 3. */
|
|
101
|
+
maxIterations?: number;
|
|
102
|
+
/** The most messages a turn may carry. Default: 40. */
|
|
103
|
+
maxMessages?: number;
|
|
104
|
+
/** The most characters in one message. Default: 2000. */
|
|
105
|
+
maxMessageLength?: number;
|
|
106
|
+
/** The most characters of conversation sent to the engine; older messages are left out. Default: 24000. */
|
|
107
|
+
maxTranscriptChars?: number;
|
|
108
|
+
}
|
|
109
|
+
export type UAGAgentApiConfig = FormflowAgentApiConfig & {
|
|
110
|
+
anonymous?: UAGAnonymousConfig;
|
|
111
|
+
chat?: UAGChatConfig;
|
|
112
|
+
/** The largest request body an agent operation takes, in bytes (AGENT_BODY_LIMIT). Default 5 MB. */
|
|
113
|
+
bodyLimit?: number;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Express's `trust proxy` from TRUST_PROXY: `true` or `false`, a number of
|
|
117
|
+
* proxy hops, or addresses and ranges (`loopback, 10.0.0.0/8`). Unset when
|
|
118
|
+
* TRUST_PROXY is empty. With it, the UAG reads its public URL, and callers'
|
|
119
|
+
* addresses, from the proxy's forwarded headers.
|
|
120
|
+
*/
|
|
121
|
+
export declare function trustProxySetting(value: string | undefined): boolean | number | string | undefined;
|
|
49
122
|
export interface UAGConfig extends ServerConfig {
|
|
123
|
+
/** The UAG's public URL (BASE_URL). Optional: derived from each request when unset. */
|
|
50
124
|
baseUrl?: string;
|
|
51
125
|
loginForm?: string;
|
|
52
126
|
responseTemplates?: UAGTemplateConfig;
|
|
127
|
+
/** Limits and choices of the AI Document module. */
|
|
128
|
+
documents?: UAGDocumentsConfig;
|
|
53
129
|
toolOverrides?: UAGToolOverride;
|
|
54
|
-
tools
|
|
130
|
+
/** Custom MCP tools: each a tool, optionally bound to the project with `bind(project)` (see ToolInfo.bind). */
|
|
131
|
+
tools?: ToolEntry[];
|
|
132
|
+
/**
|
|
133
|
+
* The engine instances to enable, as in `UAG_ENGINES` (`"jev"`, `"fast:claude"`) or as
|
|
134
|
+
* `{ name, type, options }`. When unset, `UAG_ENGINES` is used; when neither names any, no
|
|
135
|
+
* engine is enabled.
|
|
136
|
+
*/
|
|
137
|
+
engines?: UAGEngineEntry[];
|
|
138
|
+
/** Engine types this application provides, by type name, resolved before built-in types and packages. */
|
|
139
|
+
engineTypes?: Record<string, EngineTypeDefinition>;
|
|
140
|
+
routing?: UAGRoutingConfig;
|
|
141
|
+
agentApi?: UAGAgentApiConfig;
|
|
142
|
+
/**
|
|
143
|
+
* The UAG's own database (`MONGO`, `UAG_DATABASE`, `MONGO_CONFIG`,
|
|
144
|
+
* `TASK_RUN_RETENTION_DAYS`), for remote MCP sign-in and durable task runs.
|
|
145
|
+
* It needs a license that includes the UAG, unless the project and the
|
|
146
|
+
* database are both on development hosts.
|
|
147
|
+
*/
|
|
148
|
+
database?: UAGDatabaseOptions;
|
|
55
149
|
}
|
package/lib/config.js
CHANGED
|
@@ -1,2 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trustProxySetting = trustProxySetting;
|
|
4
|
+
/**
|
|
5
|
+
* Express's `trust proxy` from TRUST_PROXY: `true` or `false`, a number of
|
|
6
|
+
* proxy hops, or addresses and ranges (`loopback, 10.0.0.0/8`). Unset when
|
|
7
|
+
* TRUST_PROXY is empty. With it, the UAG reads its public URL, and callers'
|
|
8
|
+
* addresses, from the proxy's forwarded headers.
|
|
9
|
+
*/
|
|
10
|
+
function trustProxySetting(value) {
|
|
11
|
+
const text = String(value ?? '').trim();
|
|
12
|
+
if (!text)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (text === 'true')
|
|
15
|
+
return true;
|
|
16
|
+
if (text === 'false')
|
|
17
|
+
return false;
|
|
18
|
+
if (/^\d+$/.test(text))
|
|
19
|
+
return parseInt(text, 10);
|
|
20
|
+
return text;
|
|
21
|
+
}
|