@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.
Files changed (115) hide show
  1. package/README.md +237 -535
  2. package/bin/uag-eval.js +99 -0
  3. package/bin/uag.js +116 -0
  4. package/lib/UAGFormInterface.d.ts +72 -2
  5. package/lib/UAGFormInterface.js +226 -57
  6. package/lib/UAGProjectInterface.d.ts +45 -5
  7. package/lib/UAGProjectInterface.js +187 -36
  8. package/lib/actionLogs.d.ts +24 -0
  9. package/lib/actionLogs.js +149 -0
  10. package/lib/actions/runTask.d.ts +43 -0
  11. package/lib/actions/runTask.js +324 -0
  12. package/lib/agent/anonymous.d.ts +34 -0
  13. package/lib/agent/anonymous.js +99 -0
  14. package/lib/agent/chat.d.ts +114 -0
  15. package/lib/agent/chat.js +475 -0
  16. package/lib/agent/engines.d.ts +17 -0
  17. package/lib/agent/engines.js +46 -0
  18. package/lib/agent/errors.d.ts +25 -0
  19. package/lib/agent/errors.js +132 -0
  20. package/lib/agent/index.d.ts +4 -0
  21. package/lib/agent/index.js +19 -0
  22. package/lib/agent/remote.d.ts +71 -0
  23. package/lib/agent/remote.js +252 -0
  24. package/lib/agent/requestId.d.ts +4 -0
  25. package/lib/agent/requestId.js +19 -0
  26. package/lib/agent/router.d.ts +61 -0
  27. package/lib/agent/router.js +307 -0
  28. package/lib/agent/stubs.d.ts +37 -0
  29. package/lib/agent/stubs.js +39 -0
  30. package/lib/agent/tools.d.ts +31 -0
  31. package/lib/agent/tools.js +76 -0
  32. package/lib/config.d.ts +97 -3
  33. package/lib/config.js +19 -0
  34. package/lib/database.d.ts +70 -0
  35. package/lib/database.js +212 -0
  36. package/lib/documents/convert.d.ts +38 -0
  37. package/lib/documents/convert.js +151 -0
  38. package/lib/documents/converters.d.ts +28 -0
  39. package/lib/documents/converters.js +149 -0
  40. package/lib/documents/detect.d.ts +31 -0
  41. package/lib/documents/detect.js +169 -0
  42. package/lib/documents/index.d.ts +9 -0
  43. package/lib/documents/index.js +43 -0
  44. package/lib/documents/mime.d.ts +39 -0
  45. package/lib/documents/mime.js +100 -0
  46. package/lib/documents/plugins.d.ts +18 -0
  47. package/lib/documents/plugins.js +65 -0
  48. package/lib/documents/readers.d.ts +19 -0
  49. package/lib/documents/readers.js +64 -0
  50. package/lib/documents/routes.d.ts +52 -0
  51. package/lib/documents/routes.js +99 -0
  52. package/lib/documents/types.d.ts +126 -0
  53. package/lib/documents/types.js +2 -0
  54. package/lib/documents/uagJs.d.ts +17 -0
  55. package/lib/documents/uagJs.js +157 -0
  56. package/lib/formflow.d.ts +47 -0
  57. package/lib/formflow.js +28 -0
  58. package/lib/identity.d.ts +53 -0
  59. package/lib/identity.js +107 -0
  60. package/lib/index.d.ts +98 -1
  61. package/lib/index.js +382 -8
  62. package/lib/markdown.d.ts +1 -0
  63. package/lib/markdown.js +166 -0
  64. package/lib/modules/api.d.ts +25 -0
  65. package/lib/modules/api.js +98 -0
  66. package/lib/modules/index.d.ts +4 -0
  67. package/lib/modules/index.js +24 -0
  68. package/lib/modules/routes.d.ts +14 -0
  69. package/lib/modules/routes.js +21 -0
  70. package/lib/modules/types.d.ts +68 -0
  71. package/lib/modules/types.js +5 -0
  72. package/lib/pluginFiles.d.ts +28 -0
  73. package/lib/pluginFiles.js +101 -0
  74. package/lib/plugins.d.ts +17 -0
  75. package/lib/plugins.js +93 -0
  76. package/lib/richText.d.ts +20 -0
  77. package/lib/richText.js +99 -0
  78. package/lib/router.d.ts +9 -0
  79. package/lib/router.js +34 -25
  80. package/lib/runner.d.ts +149 -0
  81. package/lib/runner.js +353 -0
  82. package/lib/runs.d.ts +27 -0
  83. package/lib/runs.js +13 -0
  84. package/lib/template.js +7 -9
  85. package/lib/templates/uagComponentNotFound.md +5 -5
  86. package/lib/testing.d.ts +74 -0
  87. package/lib/testing.js +180 -0
  88. package/lib/tools/SchemaBuilder.d.ts +3 -3
  89. package/lib/tools/SchemaBuilder.js +4 -4
  90. package/lib/tools/agentDecide.d.ts +8 -0
  91. package/lib/tools/agentDecide.js +57 -0
  92. package/lib/tools/agentProvide.d.ts +8 -0
  93. package/lib/tools/agentProvide.js +97 -0
  94. package/lib/tools/collectData.js +3 -2
  95. package/lib/tools/confirmSubmission.js +1 -1
  96. package/lib/tools/fetchExternalData.js +8 -17
  97. package/lib/tools/findSubmission.js +62 -56
  98. package/lib/tools/getFieldInfo.js +1 -1
  99. package/lib/tools/getFormFields.js +1 -1
  100. package/lib/tools/getForms.js +1 -1
  101. package/lib/tools/index.d.ts +4 -1
  102. package/lib/tools/index.js +21 -14
  103. package/lib/tools/readDocument.d.ts +17 -0
  104. package/lib/tools/readDocument.js +99 -0
  105. package/lib/tools/submissionUpdate.js +4 -3
  106. package/lib/tools/submitForm.js +1 -1
  107. package/lib/tools/utils.d.ts +40 -0
  108. package/lib/tools/utils.js +16 -1
  109. package/lib/version.d.ts +2 -0
  110. package/lib/version.js +5 -0
  111. package/package.json +49 -19
  112. package/testing.d.ts +1 -0
  113. package/testing.js +2 -0
  114. package/lib/tools/agentProvideData.d.ts +0 -3
  115. 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 { ToolInfo } from './tools/utils';
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
- agent_provide_data?: ToolInfo;
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?: ToolInfo[];
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
+ }