@formio/uag 2.0.0-rc.1 → 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 (96) hide show
  1. package/README.md +17 -15
  2. package/bin/uag-eval.js +7 -3
  3. package/bin/uag.js +48 -5
  4. package/lib/UAGFormInterface.d.ts +12 -0
  5. package/lib/UAGFormInterface.js +55 -12
  6. package/lib/UAGProjectInterface.d.ts +9 -1
  7. package/lib/UAGProjectInterface.js +25 -7
  8. package/lib/actionLogs.d.ts +1 -10
  9. package/lib/actionLogs.js +5 -3
  10. package/lib/actions/runTask.js +4 -3
  11. package/lib/agent/anonymous.js +5 -4
  12. package/lib/agent/chat.d.ts +3 -2
  13. package/lib/agent/chat.js +35 -99
  14. package/lib/agent/engines.d.ts +10 -6
  15. package/lib/agent/engines.js +19 -10
  16. package/lib/agent/errors.d.ts +25 -0
  17. package/lib/agent/errors.js +132 -0
  18. package/lib/agent/index.d.ts +1 -1
  19. package/lib/agent/index.js +2 -1
  20. package/lib/agent/remote.d.ts +5 -1
  21. package/lib/agent/remote.js +25 -2
  22. package/lib/agent/router.d.ts +32 -2
  23. package/lib/agent/router.js +37 -25
  24. package/lib/agent/stubs.d.ts +13 -2
  25. package/lib/agent/stubs.js +18 -5
  26. package/lib/agent/tools.d.ts +24 -3
  27. package/lib/agent/tools.js +47 -4
  28. package/lib/config.d.ts +31 -4
  29. package/lib/database.d.ts +3 -8
  30. package/lib/database.js +2 -19
  31. package/lib/documents/convert.d.ts +38 -0
  32. package/lib/documents/convert.js +151 -0
  33. package/lib/documents/converters.d.ts +28 -0
  34. package/lib/documents/converters.js +149 -0
  35. package/lib/documents/detect.d.ts +31 -0
  36. package/lib/documents/detect.js +169 -0
  37. package/lib/documents/index.d.ts +9 -0
  38. package/lib/documents/index.js +43 -0
  39. package/lib/documents/mime.d.ts +39 -0
  40. package/lib/documents/mime.js +100 -0
  41. package/lib/documents/plugins.d.ts +18 -0
  42. package/lib/documents/plugins.js +65 -0
  43. package/lib/documents/readers.d.ts +19 -0
  44. package/lib/documents/readers.js +64 -0
  45. package/lib/documents/routes.d.ts +52 -0
  46. package/lib/documents/routes.js +99 -0
  47. package/lib/documents/types.d.ts +126 -0
  48. package/lib/documents/types.js +2 -0
  49. package/lib/documents/uagJs.d.ts +17 -0
  50. package/lib/documents/uagJs.js +157 -0
  51. package/lib/formflow.d.ts +11 -4
  52. package/lib/formflow.js +5 -5
  53. package/lib/index.d.ts +65 -9
  54. package/lib/index.js +228 -26
  55. package/lib/markdown.d.ts +1 -0
  56. package/lib/markdown.js +166 -0
  57. package/lib/modules/api.d.ts +25 -0
  58. package/lib/modules/api.js +98 -0
  59. package/lib/modules/index.d.ts +4 -0
  60. package/lib/modules/index.js +24 -0
  61. package/lib/modules/routes.d.ts +14 -0
  62. package/lib/modules/routes.js +21 -0
  63. package/lib/modules/types.d.ts +68 -0
  64. package/lib/modules/types.js +5 -0
  65. package/lib/pluginFiles.d.ts +28 -0
  66. package/lib/pluginFiles.js +101 -0
  67. package/lib/plugins.d.ts +17 -0
  68. package/lib/plugins.js +93 -0
  69. package/lib/richText.d.ts +20 -0
  70. package/lib/richText.js +99 -0
  71. package/lib/router.d.ts +9 -0
  72. package/lib/router.js +19 -0
  73. package/lib/runner.d.ts +2 -0
  74. package/lib/runner.js +11 -3
  75. package/lib/runs.d.ts +17 -79
  76. package/lib/runs.js +10 -71
  77. package/lib/testing.d.ts +74 -0
  78. package/lib/testing.js +180 -0
  79. package/lib/tools/SchemaBuilder.d.ts +0 -5
  80. package/lib/tools/SchemaBuilder.js +0 -8
  81. package/lib/tools/agentDecide.js +2 -2
  82. package/lib/tools/agentProvide.d.ts +0 -5
  83. package/lib/tools/agentProvide.js +7 -40
  84. package/lib/tools/collectData.js +2 -1
  85. package/lib/tools/index.d.ts +3 -2
  86. package/lib/tools/index.js +5 -5
  87. package/lib/tools/readDocument.d.ts +17 -0
  88. package/lib/tools/readDocument.js +99 -0
  89. package/lib/tools/submissionUpdate.js +3 -2
  90. package/lib/tools/utils.d.ts +40 -0
  91. package/lib/tools/utils.js +16 -1
  92. package/lib/version.d.ts +2 -0
  93. package/lib/version.js +5 -0
  94. package/package.json +17 -10
  95. package/testing.d.ts +1 -0
  96. package/testing.js +2 -0
@@ -34,14 +34,15 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AGENT_BODY_LIMIT = void 0;
37
+ exports.agentBodyLimit = agentBodyLimit;
38
+ exports.agentEndpoints = agentEndpoints;
37
39
  exports.AgentRouter = AgentRouter;
38
40
  exports.agentNotFound = agentNotFound;
39
41
  exports.agentCredentials = agentCredentials;
40
42
  exports.FormAgentRouter = FormAgentRouter;
41
- const crypto_1 = require("crypto");
42
43
  const lodash_1 = require("lodash");
43
44
  const express_1 = __importStar(require("express"));
44
- const protocol_1 = require("@formio/uag-sdk/protocol");
45
+ const errors_1 = require("./errors");
45
46
  const formflow_1 = require("../formflow");
46
47
  const requestId_1 = require("./requestId");
47
48
  const anonymous_1 = require("./anonymous");
@@ -67,10 +68,17 @@ function openStream(res) {
67
68
  function contextOf(req) {
68
69
  return { auth: req.auth, requestId: req.requestId, identity: (0, formflow_1.callerIdentity)(req.auth, req) };
69
70
  }
70
- /** The task the request names, if any. */
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
+ */
71
75
  function taskOf(req) {
76
+ const named = (value) => typeof value === 'string' && value.trim() !== '';
72
77
  const task = req.body?.task;
73
- return typeof task === 'string' && task ? task : undefined;
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;
74
82
  }
75
83
  /**
76
84
  * Answer extract, streaming per-engine results when the client asks for a
@@ -101,27 +109,37 @@ async function respondWithStream(req, res, run) {
101
109
  res.json(done);
102
110
  }
103
111
  }
104
- /** The largest request body an agent operation takes, in bytes. */
112
+ /** The largest request body an agent operation takes by default, in bytes. */
105
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
+ }
106
123
  /** The parts every Agent API endpoint shares: the request id, and errors answered as JSON (or as an event mid-stream). */
107
124
  function agentEndpoints(project) {
108
- const parse = express_1.default.json({ limit: exports.AGENT_BODY_LIMIT });
125
+ const limit = agentBodyLimit(project);
126
+ const parse = express_1.default.json({ limit });
109
127
  // The server parses JSON bodies first, with its own larger limit, so the
110
128
  // agent operations' limit is checked here on a body already read.
111
129
  const json = (req, res, next) => {
112
130
  if (req.body === undefined)
113
131
  return parse(req, res, next);
114
132
  const length = Number(req.get('content-length')) || Buffer.byteLength(JSON.stringify(req.body ?? null));
115
- if (length > exports.AGENT_BODY_LIMIT) {
116
- return res.status(413).json({ error: `The request body is larger than the agent operations' limit of ${exports.AGENT_BODY_LIMIT / (1024 * 1024)} MB.` });
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' });
117
135
  }
118
136
  next();
119
137
  };
120
- // Request id, after authentication: refused requests carry none. One that
121
- // is not a short token is replaced, so it is never echoed, logged or sent on.
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.
122
141
  const requestId = (req, res, next) => {
123
- req.requestId = (0, protocol_1.safeRequestId)(req.get('x-request-id')) || (0, crypto_1.randomUUID)();
124
- res.set('X-Request-Id', req.requestId);
142
+ (0, errors_1.requestIdOf)(req, res);
125
143
  next();
126
144
  };
127
145
  const handle = (operation, fn) => async (req, res) => {
@@ -133,19 +151,13 @@ function agentEndpoints(project) {
133
151
  await (0, requestId_1.withRequestId)(agentReq.requestId, () => fn(agentReq, res, service));
134
152
  }
135
153
  catch (err) {
136
- const status = err instanceof formflow_1.FormflowError ? err.status : 500;
154
+ const { status, body } = (0, errors_1.agentError)(agentReq.requestId, err);
137
155
  if (status === 500) {
138
156
  debug(`[${agentReq.requestId}] ${operation} failed:`, err);
139
157
  }
140
158
  if (err instanceof formflow_1.RateLimitError) {
141
159
  res.set('Retry-After', String(Math.ceil(err.retryAfterMs / 1000)));
142
160
  }
143
- const body = {
144
- requestId: agentReq.requestId,
145
- error: status === 500 ? 'The request failed. See the UAG logs for this request id.' : err.message,
146
- ...(err instanceof formflow_1.FormflowError && err.code && { code: err.code }),
147
- ...(err?.edition && { edition: err.edition })
148
- };
149
161
  if (res.headersSent) {
150
162
  // Mid-stream: report the failure as an event and close.
151
163
  res.write(`event: error\ndata: ${JSON.stringify(body)}\n\n`);
@@ -207,11 +219,11 @@ function agentNotFound(project) {
207
219
  : !!formPath && forms.some((form) => String(form.path || '').toLowerCase() === formPath.toLowerCase());
208
220
  if (known)
209
221
  return next();
210
- res.status(404).json({ error: `The form "${byId ? byId[1] : formPath}" does not exist or is not tagged "uag".` });
222
+ res.status(404).json({ error: `The form "${byId ? byId[1] : formPath}" does not exist or is not tagged "uag".`, code: 'form_not_found' });
211
223
  };
212
224
  }
213
- function refuse(res, detail) {
214
- return res.status(401).json({ detail });
225
+ function refuse(res, error) {
226
+ return res.status(401).json({ error, code: 'unauthorized' });
215
227
  }
216
228
  /**
217
229
  * Who may call a form's agent operations, on top of the project's own
@@ -283,10 +295,10 @@ function FormAgentRouter(project, credentials) {
283
295
  route('chat', 'chat', async (req, res, service) => {
284
296
  const form = await project.formflowForm(formOf(req));
285
297
  if (!form) {
286
- throw new formflow_1.FormflowError(404, `The form "${formOf(req)}" does not exist or is not tagged "uag".`);
298
+ throw new formflow_1.FormflowError(404, `The form "${formOf(req)}" does not exist or is not tagged "uag".`, 'form_not_found');
287
299
  }
288
- if (String(form.form?.properties?.uagChat) === 'false') {
289
- throw new formflow_1.FormflowError(403, `Chat is turned off for the form "${form.formflowName}".`);
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');
290
302
  }
291
303
  service.requireAccess(req.auth, form, ['create']);
292
304
  res.json(await (0, chat_1.chatTurn)(project, form, req.body, { auth: req.auth, requestId: req.requestId }));
@@ -1,3 +1,4 @@
1
+ import type { GenerateRequest } from '@formio/uag-sdk';
1
2
  /**
2
3
  * Stand-in engines for tests and local development. They answer instantly with
3
4
  * predictable values, so the Agent API can be exercised end to end without
@@ -6,7 +7,9 @@
6
7
  * any instance name.
7
8
  */
8
9
  export declare const StubDecisionEngine: Readonly<{
9
- type: string;
10
+ type: "engine";
11
+ name: string;
12
+ title: string;
10
13
  kinds: "decision"[];
11
14
  apiVersion: 1;
12
15
  description: string;
@@ -14,8 +17,16 @@ export declare const StubDecisionEngine: Readonly<{
14
17
  calls: number;
15
18
  };
16
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
+ }>;
17
26
  export declare const StubAgentEngine: Readonly<{
18
- type: string;
27
+ type: "engine";
28
+ name: string;
29
+ title: string;
19
30
  kinds: "agent"[];
20
31
  apiVersion: 1;
21
32
  description: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StubAgentEngine = exports.StubDecisionEngine = void 0;
3
+ exports.StubAgentEngine = exports.stubGenerateRequests = exports.STUB_GENERATE_REQUESTS_KEPT = exports.StubDecisionEngine = void 0;
4
4
  const uag_sdk_1 = require("@formio/uag-sdk");
5
5
  const formflow_1 = require("../formflow");
6
6
  /**
@@ -11,16 +11,29 @@ const formflow_1 = require("../formflow");
11
11
  * any instance name.
12
12
  */
13
13
  exports.StubDecisionEngine = (0, uag_sdk_1.defineEngine)({
14
- type: 'stub-decision',
14
+ type: 'engine',
15
+ name: 'stub-decision',
16
+ title: 'Stub decision engine',
15
17
  kinds: ['decision'],
16
18
  apiVersion: uag_sdk_1.ENGINE_API_VERSION,
17
19
  description: 'Answers every decision with a fixed, predictable value. For tests and local development.',
18
20
  create: ({ name }) => (0, formflow_1.stubDecisionEngine)({ name }),
19
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 = [];
20
26
  exports.StubAgentEngine = (0, uag_sdk_1.defineEngine)({
21
- type: 'stub-agent',
27
+ type: 'engine',
28
+ name: 'stub-agent',
29
+ title: 'Stub agent engine',
22
30
  kinds: ['agent'],
23
31
  apiVersion: uag_sdk_1.ENGINE_API_VERSION,
24
- description: 'Extracts nothing. For tests and local development.',
25
- create: ({ name }) => (0, formflow_1.stubAgentEngine)({ name }),
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
+ } }),
26
39
  });
@@ -1,10 +1,31 @@
1
1
  import { FormflowTool } from "../formflow";
2
+ import type { AuthRequest } from "@formio/appserver";
2
3
  import type { UAGProjectInterface } from "../UAGProjectInterface";
3
- import { ToolInfo } from "../tools/utils";
4
- /** Every tool the UAG serves, including custom tools from configuration. */
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
+ */
5
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;
6
27
  /**
7
28
  * An MCP tool as an Agentic Formflow tool: its schema as JSON, its arguments
8
29
  * validated with its own schema, and read-only only when annotated so.
9
30
  */
10
- export declare function formflowTool(tool: ToolInfo): FormflowTool;
31
+ export declare function formflowTool(tool: ToolInfo, project: UAGProjectInterface): FormflowTool;
@@ -3,19 +3,62 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setToolModule = setToolModule;
7
+ exports.resolveTool = resolveTool;
6
8
  exports.allTools = allTools;
9
+ exports.runTool = runTool;
7
10
  exports.formflowTool = formflowTool;
8
11
  const zod_1 = __importDefault(require("zod"));
9
12
  const tools_1 = require("../tools");
10
- /** Every tool the UAG serves, including custom tools from configuration. */
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
+ */
11
39
  async function allTools(project) {
12
- return [...await (0, tools_1.getTools)(project), ...(project.config?.tools || [])].filter((tool) => !!tool?.name);
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);
13
56
  }
14
57
  /**
15
58
  * An MCP tool as an Agentic Formflow tool: its schema as JSON, its arguments
16
59
  * validated with its own schema, and read-only only when annotated so.
17
60
  */
18
- function formflowTool(tool) {
61
+ function formflowTool(tool, project) {
19
62
  const schema = zod_1.default.object(tool.inputSchema || {});
20
63
  const inputSchema = zod_1.default.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' });
21
64
  delete inputSchema.$schema;
@@ -28,6 +71,6 @@ function formflowTool(tool) {
28
71
  const parsed = schema.safeParse(args || {});
29
72
  return parsed.success ? { ok: true, value: parsed.data } : { ok: false, error: parsed.error.message };
30
73
  },
31
- execute: async (args, auth) => tool.execute(args, { authInfo: auth })
74
+ execute: async (args, auth) => runTool(tool, project, args, { authInfo: auth })
32
75
  };
33
76
  }
package/lib/config.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ServerConfig } from '@formio/appserver';
2
2
  import type { FormflowAgentApiConfig, FormflowRateLimit, FormflowRoutingConfig } from './formflow';
3
3
  import type { EngineInstanceSpec, EngineTypeDefinition } from '@formio/uag-sdk';
4
- import { ToolInfo } from './tools/utils';
4
+ import type { ToolEntry, ToolInfo } from './tools/utils';
5
5
  import type { UAGDatabaseOptions } from './database';
6
6
  export type UAGForm = {
7
7
  name: string;
@@ -47,10 +47,32 @@ export type UAGToolOverride = {
47
47
  submission_update?: ToolInfo;
48
48
  find_submissions?: ToolInfo;
49
49
  agent_provide?: ToolInfo;
50
- /** @deprecated The earlier name of `agent_provide`; also applied to it when `agent_provide` is not set. */
51
- agent_provide_data?: ToolInfo;
52
50
  agent_decide?: ToolInfo;
53
51
  fetch_external_data?: ToolInfo;
52
+ read_document?: ToolInfo;
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>;
54
76
  };
55
77
  /**
56
78
  * One engine instance: `"<type>"` or `"<name>:<type>"`, or `{ name, type, options }`, where the
@@ -87,6 +109,8 @@ export interface UAGChatConfig {
87
109
  export type UAGAgentApiConfig = FormflowAgentApiConfig & {
88
110
  anonymous?: UAGAnonymousConfig;
89
111
  chat?: UAGChatConfig;
112
+ /** The largest request body an agent operation takes, in bytes (AGENT_BODY_LIMIT). Default 5 MB. */
113
+ bodyLimit?: number;
90
114
  };
91
115
  /**
92
116
  * Express's `trust proxy` from TRUST_PROXY: `true` or `false`, a number of
@@ -100,8 +124,11 @@ export interface UAGConfig extends ServerConfig {
100
124
  baseUrl?: string;
101
125
  loginForm?: string;
102
126
  responseTemplates?: UAGTemplateConfig;
127
+ /** Limits and choices of the AI Document module. */
128
+ documents?: UAGDocumentsConfig;
103
129
  toolOverrides?: UAGToolOverride;
104
- tools?: ToolInfo[];
130
+ /** Custom MCP tools: each a tool, optionally bound to the project with `bind(project)` (see ToolInfo.bind). */
131
+ tools?: ToolEntry[];
105
132
  /**
106
133
  * The engine instances to enable, as in `UAG_ENGINES` (`"jev"`, `"fast:claude"`) or as
107
134
  * `{ name, type, options }`. When unset, `UAG_ENGINES` is used; when neither names any, no
package/lib/database.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Collection, Db, MongoClient, MongoClientOptions } from 'mongodb';
2
- import type { AuthCodeStore } from '@formio/appserver-core';
2
+ import type { AuthCodeStore } from '@formio/appserver';
3
3
  /**
4
4
  * The UAG's own database (MONGO, with the database named by UAG_DATABASE). It
5
- * holds only the UAG's state: authorization codes for remote MCP clients, the
6
- * start of a license server outage, and task-run records. Never submissions.
5
+ * holds only the UAG's state: authorization codes for remote MCP clients, and
6
+ * task-run records. Never submissions.
7
7
  *
8
8
  * Every collection the UAG uses is named `uag-*`, and nothing else in the
9
9
  * database is read or written, so the database may be shared. Before writing
@@ -66,10 +66,5 @@ export declare class UAGDatabase {
66
66
  private privileged;
67
67
  /** Authorization codes for remote MCP clients (MCP_OAUTH), in `uag-authcodes`. */
68
68
  authCodeStore(): AuthCodeStore;
69
- /** When the license server became unreachable, in `uag-meta`, so a restart neither begins nor extends the grace period. */
70
- licenseGrace(): {
71
- read(): Promise<number | undefined>;
72
- write(since?: number): Promise<void>;
73
- };
74
69
  close(): Promise<void>;
75
70
  }
package/lib/database.js CHANGED
@@ -6,8 +6,8 @@ const mongodb_1 = require("mongodb");
6
6
  const debug = require('debug')('formio:uag:database');
7
7
  /**
8
8
  * The UAG's own database (MONGO, with the database named by UAG_DATABASE). It
9
- * holds only the UAG's state: authorization codes for remote MCP clients, the
10
- * start of a license server outage, and task-run records. Never submissions.
9
+ * holds only the UAG's state: authorization codes for remote MCP clients, and
10
+ * task-run records. Never submissions.
11
11
  *
12
12
  * Every collection the UAG uses is named `uag-*`, and nothing else in the
13
13
  * database is read or written, so the database may be shared. Before writing
@@ -23,7 +23,6 @@ exports.UAG_COLLECTIONS = Object.freeze({
23
23
  const PREFIX = 'uag-';
24
24
  const APP = 'formio-uag';
25
25
  const SCHEMA_ID = 'schema';
26
- const LICENSE_ID = 'license';
27
26
  const RETENTION_INDEX = 'retention';
28
27
  const DEFAULT_RETENTION_DAYS = 90;
29
28
  // MongoDB's "Unauthorized" error.
@@ -206,22 +205,6 @@ class UAGDatabase {
206
205
  }
207
206
  };
208
207
  }
209
- /** When the license server became unreachable, in `uag-meta`, so a restart neither begins nor extends the grace period. */
210
- licenseGrace() {
211
- const meta = this.meta;
212
- const database = this;
213
- return {
214
- async read() {
215
- const found = await meta.findOne({ _id: LICENSE_ID });
216
- return typeof found?.outageSince === 'number' ? found.outageSince : undefined;
217
- },
218
- async write(since) {
219
- // It may write before the license is known, so it never leaves uag-* collections without the marker.
220
- await database.bootstrap();
221
- await meta.updateOne({ _id: LICENSE_ID }, since === undefined ? { $unset: { outageSince: '' } } : { $set: { outageSince: since } }, { upsert: true });
222
- }
223
- };
224
- }
225
208
  async close() {
226
209
  await this.client.close();
227
210
  }
@@ -0,0 +1,38 @@
1
+ import type { PluginContext } from '@formio/appserver';
2
+ import type { Converter, ConverterOutput, Representation } from './types';
3
+ export declare const CONVERTER_DEFAULTS: {
4
+ maxBytes: number;
5
+ timeoutMs: number;
6
+ concurrency: number;
7
+ };
8
+ /** A conversion that did not complete. The message names the converter and the file, never its content. */
9
+ export declare class ConversionError extends Error {
10
+ constructor(message: string);
11
+ }
12
+ export type ConvertOptions = {
13
+ to: Representation;
14
+ settings?: Record<string, any>;
15
+ signal?: AbortSignal;
16
+ log: PluginContext['log'];
17
+ };
18
+ /**
19
+ * Run one conversion: under the converter's size limit, in a temporary
20
+ * directory made for the call and removed after it, aborted at its timeout,
21
+ * and queued behind its other conversions beyond its concurrency.
22
+ */
23
+ export declare function runConverter(converter: Converter, input: {
24
+ buffer: Buffer;
25
+ type: string;
26
+ name: string;
27
+ }, options: ConvertOptions): Promise<ConverterOutput>;
28
+ /**
29
+ * Run a route: each step's output feeds the next (an intermediate step always
30
+ * makes a PDF), with each converter's operator settings.
31
+ */
32
+ export declare function runRoute(steps: Converter[], input: {
33
+ buffer: Buffer;
34
+ type: string;
35
+ name: string;
36
+ }, to: Representation, options: Omit<ConvertOptions, 'to' | 'settings'> & {
37
+ settingsOf?: (converter: Converter) => Record<string, any> | undefined;
38
+ }): Promise<ConverterOutput>;
@@ -0,0 +1,151 @@
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.ConversionError = exports.CONVERTER_DEFAULTS = void 0;
7
+ exports.runConverter = runConverter;
8
+ exports.runRoute = runRoute;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ exports.CONVERTER_DEFAULTS = { maxBytes: 25 * 1024 * 1024, timeoutMs: 120000, concurrency: 2 };
13
+ /** A conversion that did not complete. The message names the converter and the file, never its content. */
14
+ class ConversionError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = 'ConversionError';
18
+ }
19
+ }
20
+ exports.ConversionError = ConversionError;
21
+ /**
22
+ * A queue per converter, so at most `concurrency` of its conversions run at
23
+ * once. A slot is held until its conversion has actually stopped: a converter
24
+ * that ignores its signal still counts after its caller has given up on it.
25
+ */
26
+ class Queue {
27
+ constructor(limit) {
28
+ this.limit = limit;
29
+ this.running = 0;
30
+ this.waiting = [];
31
+ }
32
+ /** Wait for a slot; the returned function frees it (once). */
33
+ async acquire(signal) {
34
+ signal?.throwIfAborted();
35
+ if (this.running < this.limit) {
36
+ this.running++;
37
+ }
38
+ else {
39
+ // A freed slot passes straight to the next waiter, so no new caller can take it in between.
40
+ await new Promise((resolve, reject) => {
41
+ const go = () => { signal?.removeEventListener('abort', cancel); resolve(); };
42
+ const cancel = () => { this.waiting = this.waiting.filter((entry) => entry !== go); reject(signal.reason ?? new Error('aborted')); };
43
+ this.waiting.push(go);
44
+ signal?.addEventListener('abort', cancel, { once: true });
45
+ });
46
+ }
47
+ let released = false;
48
+ return () => {
49
+ if (released)
50
+ return;
51
+ released = true;
52
+ const next = this.waiting.shift();
53
+ if (next)
54
+ next();
55
+ else
56
+ this.running--;
57
+ };
58
+ }
59
+ }
60
+ const queues = new WeakMap();
61
+ /**
62
+ * Run one conversion: under the converter's size limit, in a temporary
63
+ * directory made for the call and removed after it, aborted at its timeout,
64
+ * and queued behind its other conversions beyond its concurrency.
65
+ */
66
+ async function runConverter(converter, input, options) {
67
+ const limits = { ...exports.CONVERTER_DEFAULTS, ...(converter.limits || {}) };
68
+ if (input.buffer.length > limits.maxBytes) {
69
+ throw new ConversionError(`"${input.name}" is larger than the ${converter.name} converter takes (${limits.maxBytes} bytes).`);
70
+ }
71
+ let queue = queues.get(converter);
72
+ if (!queue) {
73
+ queue = new Queue(Math.max(1, limits.concurrency));
74
+ queues.set(converter, queue);
75
+ }
76
+ const release = await queue.acquire(options.signal);
77
+ let tmpdir;
78
+ try {
79
+ options.signal?.throwIfAborted();
80
+ tmpdir = await fs_1.default.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'uag-convert-'));
81
+ }
82
+ catch (err) {
83
+ release();
84
+ throw err;
85
+ }
86
+ const controller = new AbortController();
87
+ const abort = () => controller.abort(options.signal?.reason);
88
+ options.signal?.addEventListener('abort', abort, { once: true });
89
+ if (options.signal?.aborted)
90
+ abort();
91
+ const work = Promise.resolve().then(() => converter.convert(input, { to: options.to, settings: options.settings || {}, signal: controller.signal, tmpdir, log: options.log }));
92
+ // The slot and the temporary directory are kept until convert() settles, even after a timeout or an abort.
93
+ const stopped = work.then(() => undefined, () => undefined).then(async () => {
94
+ await fs_1.default.promises.rm(tmpdir, { recursive: true, force: true }).catch(() => undefined);
95
+ release();
96
+ });
97
+ let timer;
98
+ let finished = false;
99
+ try {
100
+ const timeout = new Promise((_, reject) => {
101
+ timer = setTimeout(() => {
102
+ const error = new ConversionError(`Converting "${input.name}" with ${converter.name} took longer than ${limits.timeoutMs} ms.`);
103
+ // Abort first: a converter running a process stops it on its signal.
104
+ controller.abort(error);
105
+ reject(error);
106
+ }, limits.timeoutMs);
107
+ });
108
+ const aborted = new Promise((_, reject) => {
109
+ if (controller.signal.aborted)
110
+ reject(controller.signal.reason);
111
+ controller.signal.addEventListener('abort', () => reject(controller.signal.reason), { once: true });
112
+ });
113
+ const output = await Promise.race([work.finally(() => { finished = true; }), timeout, aborted]);
114
+ return checkOutput(converter, input.name, options.to, output);
115
+ }
116
+ finally {
117
+ clearTimeout(timer);
118
+ options.signal?.removeEventListener('abort', abort);
119
+ // A conversion that completed has its directory removed before its result is used.
120
+ if (finished)
121
+ await stopped;
122
+ }
123
+ }
124
+ function checkOutput(converter, name, to, output) {
125
+ if (to === 'pdf') {
126
+ if (!output || !Buffer.isBuffer(output.pdf))
127
+ throw new ConversionError(`The ${converter.name} converter did not produce a PDF for "${name}".`);
128
+ return output;
129
+ }
130
+ if (!output || typeof output.text !== 'string' || !['plain', 'markdown', 'csv'].includes(output.textFormat)) {
131
+ throw new ConversionError(`The ${converter.name} converter did not produce text for "${name}".`);
132
+ }
133
+ return output;
134
+ }
135
+ /**
136
+ * Run a route: each step's output feeds the next (an intermediate step always
137
+ * makes a PDF), with each converter's operator settings.
138
+ */
139
+ async function runRoute(steps, input, to, options) {
140
+ let current = input;
141
+ let output;
142
+ for (let i = 0; i < steps.length; i++) {
143
+ const target = i === steps.length - 1 ? to : 'pdf';
144
+ output = await runConverter(steps[i], current, { ...options, to: target, settings: options.settingsOf?.(steps[i]) });
145
+ if (target === 'pdf')
146
+ current = { buffer: output.pdf, type: 'application/pdf', name: input.name };
147
+ }
148
+ if (!output)
149
+ throw new ConversionError(`No converter step for "${input.name}".`);
150
+ return output;
151
+ }