@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
package/lib/testing.js ADDED
@@ -0,0 +1,180 @@
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.testForm = testForm;
7
+ exports.testAuth = testAuth;
8
+ exports.createTestModuleApi = createTestModuleApi;
9
+ exports.mountModuleRoutes = mountModuleRoutes;
10
+ /**
11
+ * @formio/uag/testing: test a module without a Form.io project or a running UAG.
12
+ *
13
+ * const { createTestModuleApi, testForm, testAuth, mountModuleRoutes } = require('@formio/uag/testing');
14
+ *
15
+ * const api = await createTestModuleApi(MyModule, { config: { myModule: { max: 2 } } });
16
+ * const app = await mountModuleRoutes(MyModule, { form: testForm(definition), auth: testAuth() });
17
+ * await supertest(app).post('/notes/my-module/check').send({ ... });
18
+ *
19
+ * The API is the UAG's own (errors, stream, forms, converters, engine handles,
20
+ * Markdown, callerKey), with stand-ins only for what needs a project: the
21
+ * caller's access (from `testAuth`), the license (`licensed`), engines
22
+ * (`engines`) and stored files (`testForm` reads base64 values; `files` for
23
+ * others). Settings go through the module's own `settings`, with the server's rules.
24
+ */
25
+ const express_1 = __importDefault(require("express"));
26
+ const crypto_1 = require("crypto");
27
+ const appserver_1 = require("@formio/appserver");
28
+ const formflow_1 = require("./formflow");
29
+ const plugins_1 = require("./documents/plugins");
30
+ const errors_1 = require("./agent/errors");
31
+ const version_1 = require("./version");
32
+ const api_1 = require("./modules/api");
33
+ const routes_1 = require("./modules/routes");
34
+ /** A form for tests, as module routes get it: `definition` is the form's JSON. */
35
+ function testForm(definition, options = {}) {
36
+ const form = {
37
+ id: options.id || String(definition._id || 'test-form'),
38
+ name: String(definition.name || definition.path || 'test'),
39
+ path: String(definition.path || definition.name || 'test'),
40
+ title: String(definition.title || definition.name || ''),
41
+ definition,
42
+ async readFile(value, _auth, readOptions = {}) {
43
+ const url = String(value?.url || '');
44
+ if (url.startsWith('data:')) {
45
+ const bytes = Buffer.from(url.slice(url.indexOf(',') + 1), url.includes(';base64,') ? 'base64' : 'utf8');
46
+ if (readOptions.maxBytes !== undefined && bytes.length > readOptions.maxBytes) {
47
+ throw new Error(`The file "${value?.originalName || value?.name || 'unnamed'}" is larger than the ${readOptions.maxBytes} bytes that may be read.`);
48
+ }
49
+ return bytes;
50
+ }
51
+ const files = options.files || this?.files;
52
+ if (files)
53
+ return files(value, readOptions);
54
+ throw new Error('testForm reads base64 (data: URL) files only: give testForm, createTestModuleApi or mountModuleRoutes a "files" function for others.');
55
+ }
56
+ };
57
+ return form;
58
+ }
59
+ /**
60
+ * A caller for tests: a user with these permissions on every form (create and
61
+ * update by default), as a list (`['read']`) or as `formPermissions()` gives
62
+ * them (`{ read: true }`).
63
+ */
64
+ function testAuth(options = {}) {
65
+ const given = options.permissions || ['create', 'update'];
66
+ const granted = { create: false, read: false, update: false, delete: false };
67
+ if (Array.isArray(given)) {
68
+ for (const method of given)
69
+ granted[method] = true;
70
+ }
71
+ else if (given && typeof given === 'object') {
72
+ for (const method of Object.keys(granted))
73
+ granted[method] = given[method] === true;
74
+ }
75
+ else {
76
+ throw new Error('testAuth: "permissions" is a list of create, read, update and delete, or { read: true, ... }');
77
+ }
78
+ return {
79
+ user: options.user || { _id: 'test-user' },
80
+ admin: !!options.admin,
81
+ formPermissions: () => (options.admin ? { create: true, read: true, update: true, delete: true } : granted)
82
+ };
83
+ }
84
+ /** The module, checked for what a test needs: a name and an apiVersion. */
85
+ function loadedOf(module, settings) {
86
+ if (!module || typeof module.name !== 'string' || module.apiVersion === undefined) {
87
+ throw new Error('The module needs a "name" and an "apiVersion" to be tested as a module.');
88
+ }
89
+ return { definition: module, name: module.name, source: 'test', settings };
90
+ }
91
+ /** The module's settings, as the server resolves them. */
92
+ function settingsOf(module, options) {
93
+ if (options.settings)
94
+ return Object.freeze({ ...options.settings });
95
+ const settings = module.settings ? module.settings(options.config || {}, options.env || {}) : {};
96
+ if (settings && typeof settings.then === 'function')
97
+ throw new Error(`Module ${module.name}: "settings" returned a promise; it must return the settings themselves (no async)`);
98
+ return Object.freeze({ ...(settings || {}) });
99
+ }
100
+ /** The form, reading stored values that are not base64 through `files` unless it has its own. */
101
+ function withFiles(form, files) {
102
+ return files && form && typeof form === 'object' && !form.files ? { ...form, files } : form;
103
+ }
104
+ /** The UAG's module API for a module, in a test. */
105
+ async function createTestModuleApi(module, options = {}) {
106
+ const loaded = loadedOf(module, settingsOf(module, options));
107
+ const registry = new appserver_1.PluginRegistry();
108
+ (0, plugins_1.registerDocuments)(registry);
109
+ const licensed = options.licensed !== false;
110
+ const requireLicense = async () => {
111
+ if (!licensed)
112
+ throw new formflow_1.FormflowError(403, 'Agentic Formflow requires a license (the test API was created with licensed: false).', 'license_required');
113
+ };
114
+ const generate = async (engine, request) => {
115
+ await requireLicense();
116
+ return engine.generate(request);
117
+ };
118
+ const engines = (options.engines || []).map((engine) => ({ kind: 'agent', model: engine.name, accepts: ['text'], ...engine }));
119
+ const base = (0, appserver_1.createBaseModuleApi)(loaded, registry, { name: () => 'uag', version: () => version_1.UAG_VERSION, errorBody: routes_1.uagErrorBody });
120
+ const api = (0, api_1.uagModuleApi)(base, {
121
+ registry: () => registry,
122
+ engines: () => ({ get: (name) => engines.find((engine) => engine.name === name), agent: engines.filter((engine) => engine.kind === 'agent') }),
123
+ generate,
124
+ requireLicense
125
+ });
126
+ const pluginTypes = typeof module.pluginTypes === 'function' ? module.pluginTypes(api) : (module.pluginTypes || []);
127
+ for (const type of pluginTypes)
128
+ registry.addType(type, module.name);
129
+ if (module.plugins)
130
+ registry.add(module.plugins, module.name);
131
+ if (options.plugins?.length)
132
+ registry.add(options.plugins, 'test');
133
+ await registry.initialize({ server: {} });
134
+ if (options.files) {
135
+ // A form without its own "files" function reads other stored values through this one.
136
+ const read = api.files.read;
137
+ api.files.read = (form, value, auth, readOptions) => read(withFiles(form, options.files), value, auth, readOptions);
138
+ }
139
+ return api;
140
+ }
141
+ /**
142
+ * The module's form routes on an Express app, as the UAG mounts them on a
143
+ * form (by its path, and by `form/<id>`), for supertest: with the form, the
144
+ * caller, a request id, the access check and the UAG's error shape. No rate
145
+ * limits. `api` is created with `options` unless given.
146
+ */
147
+ async function mountModuleRoutes(module, options) {
148
+ const api = options.api || await createTestModuleApi(module, options);
149
+ const loaded = loadedOf(module, api.settings);
150
+ const app = (0, express_1.default)();
151
+ app.use(express_1.default.json({ limit: '50mb' }));
152
+ const host = {
153
+ errorBody: routes_1.uagErrorBody,
154
+ routeHandlers: (_project, route) => ({
155
+ before: [(req, res, next) => {
156
+ req.requestId = req.get('x-request-id') || (0, crypto_1.randomUUID)();
157
+ res.set('X-Request-Id', req.requestId);
158
+ next();
159
+ }],
160
+ wrap: (run) => async (req, res) => {
161
+ try {
162
+ await run(req, res);
163
+ }
164
+ catch (err) {
165
+ const { status, body } = (0, errors_1.agentError)(req.requestId, err);
166
+ res.status(status).json(body);
167
+ }
168
+ }
169
+ })
170
+ };
171
+ const router = (0, appserver_1.moduleFormRouter)({}, loaded, api, host);
172
+ const withForm = (req, res, next) => {
173
+ req.form = withFiles(options.form, options.files);
174
+ req.auth = options.auth || testAuth();
175
+ next();
176
+ };
177
+ app.use(`/${options.form.path}`, withForm, router);
178
+ app.use(`/form/${options.form.id}`, withForm, router);
179
+ return app;
180
+ }
@@ -109,10 +109,10 @@ export declare class SchemaBuilder {
109
109
  */
110
110
  parent_path(): this;
111
111
  /**
112
- * The agent persona "type" to use when processing the existing submission data. The persona provides context to the agent on how to interpret and understand the data within the form submission. If not provided, the default persona defined within the form's UAG settings will be used.
113
- * @returns
112
+ * The task to work on: the name of a task the form defines (the value of its
113
+ * components' `uag` property).
114
114
  */
115
- persona(): this;
115
+ task(): this;
116
116
  /**
117
117
  * The data_path of a specific field within the form.
118
118
  */
@@ -168,11 +168,11 @@ class SchemaBuilder {
168
168
  return this;
169
169
  }
170
170
  /**
171
- * The agent persona "type" to use when processing the existing submission data. The persona provides context to the agent on how to interpret and understand the data within the form submission. If not provided, the default persona defined within the form's UAG settings will be used.
172
- * @returns
171
+ * The task to work on: the name of a task the form defines (the value of its
172
+ * components' `uag` property).
173
173
  */
174
- persona() {
175
- this.schema.persona = zod_1.default.string().optional().describe('The agent persona "type" to use when processing the existing submission data. The persona provides context to the agent on how to interpret and understand the data within the form submission. If not provided, the default persona defined within the form\'s UAG settings will be used.');
174
+ task() {
175
+ this.schema.task = zod_1.default.string().optional().describe('The name of a task defined on the form: the value of its components\' `uag` property, such as "triage". Not a description of the work. If not provided, the form\'s first task is used.');
176
176
  return this;
177
177
  }
178
178
  /**
@@ -0,0 +1,8 @@
1
+ import { UAGProjectInterface } from "../UAGProjectInterface";
2
+ import { ToolInfo } from "./utils";
3
+ /**
4
+ * Returns a task's decision questions as structured JSON, so any decision
5
+ * model — an enabled engine, or one the MCP client calls itself — can answer
6
+ * them. No model is called here.
7
+ */
8
+ export declare const agentDecide: (project: UAGProjectInterface) => Promise<ToolInfo>;
@@ -0,0 +1,57 @@
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.agentDecide = void 0;
7
+ const lodash_1 = require("lodash");
8
+ const zod_1 = __importDefault(require("zod"));
9
+ const template_1 = require("../template");
10
+ const formflow_1 = require("../formflow");
11
+ const SchemaBuilder_1 = require("./SchemaBuilder");
12
+ const annotations_1 = require("./annotations");
13
+ /**
14
+ * Returns a task's decision questions as structured JSON, so any decision
15
+ * model — an enabled engine, or one the MCP client calls itself — can answer
16
+ * them. No model is called here.
17
+ */
18
+ const agentDecide = async (project) => {
19
+ return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.agent_decide, {
20
+ name: 'agent_decide',
21
+ title: 'Agent Decide',
22
+ description: 'Return the decision questions for a task\'s closed-set fields (radio, select, checkbox, select boxes, survey and small resource selects) as structured JSON: the state to decide on, every question, and how each answer maps back to a field value. Answer the questions with a decision model, then write the values with `submission_update`. Provide either `submission_id` or `form_data`.',
23
+ annotations: (0, annotations_1.reads)('Get decision questions'),
24
+ inputSchema: {
25
+ ...(new SchemaBuilder_1.SchemaBuilder(project)).form_name().submission_id().task().schema,
26
+ form_data: zod_1.default.record(zod_1.default.string(), zod_1.default.any()).optional().describe('The current form data as a submission data object (nested by data path). Used when `submission_id` is not given.')
27
+ },
28
+ execute: async (args, extra) => {
29
+ const { form_name, submission_id, form_data } = args;
30
+ const form = await project.getForm(form_name);
31
+ if (!form) {
32
+ return project.mcpResponse(template_1.ResponseTemplate.formNotFound, { formName: form_name }, true);
33
+ }
34
+ let data = form_data || {};
35
+ if (submission_id) {
36
+ const submission = await form.loadSubmission(submission_id, extra.authInfo);
37
+ if (!submission) {
38
+ return project.mcpResponse(template_1.ResponseTemplate.submissionNotFound, { form: form.form, submissionId: submission_id }, true);
39
+ }
40
+ data = submission.data || {};
41
+ }
42
+ try {
43
+ const task = args.task || undefined;
44
+ const out = await (0, formflow_1.formflow)(project).questions(form, { task, data, submissionId: submission_id }, extra.authInfo);
45
+ return project.mcpJSONResponse({ form: form_name, ...out });
46
+ }
47
+ catch (err) {
48
+ // A request that cannot be planned, or Agentic Formflow without its license.
49
+ if (err instanceof formflow_1.FormflowError) {
50
+ return project.mcpJSONResponse({ error: err.message, ...(err.code && { code: err.code }) }, true);
51
+ }
52
+ throw err;
53
+ }
54
+ }
55
+ });
56
+ };
57
+ exports.agentDecide = agentDecide;
@@ -0,0 +1,8 @@
1
+ import { UAGProjectInterface } from "../UAGProjectInterface";
2
+ import { ToolInfo } from "./utils";
3
+ /**
4
+ * `agent_provide`: a task's criteria, the submission's values and the task's
5
+ * fields, as instructions for an agent to fill those fields. It reads only; the
6
+ * agent writes its values with `submission_update`.
7
+ */
8
+ export declare const agentProvide: (project: UAGProjectInterface) => Promise<ToolInfo>;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentProvide = void 0;
4
+ const template_1 = require("../template");
5
+ const lodash_1 = require("lodash");
6
+ const SchemaBuilder_1 = require("./SchemaBuilder");
7
+ const annotations_1 = require("./annotations");
8
+ const formflow_1 = require("../formflow");
9
+ const DESCRIPTION = 'Provides a mechanism for agents to process existing submission data, understand it, and then to provide additional data values provided a criteria and form context.';
10
+ /**
11
+ * Agentic workflow over MCP is Agentic Formflow: it needs a license or a project
12
+ * on Form.io's hosted service, as the Agent API does. The stub engines do not
13
+ * count, since this tool calls no engine. Answers the refusal, or null.
14
+ */
15
+ async function refusal(project) {
16
+ const state = await (0, formflow_1.formflow)(project).state();
17
+ if (state.allowed && (state.reason === 'licensed' || state.reason === 'hosted'))
18
+ return null;
19
+ return {
20
+ error: 'Agentic workflows require a license (LICENSE_KEY) that includes the UAG (an online license with UAG servers, or an offline license ' +
21
+ 'with the "uag" option), or a project on Form.io\'s hosted service. ' +
22
+ 'MCP tools and conversation forms do not need either.',
23
+ code: 'license_required',
24
+ };
25
+ }
26
+ /**
27
+ * `agent_provide`: a task's criteria, the submission's values and the task's
28
+ * fields, as instructions for an agent to fill those fields. It reads only; the
29
+ * agent writes its values with `submission_update`.
30
+ */
31
+ const agentProvide = async (project) => {
32
+ // Merged into a new object: the overrides are shared config, and the tools are
33
+ // built again for every request.
34
+ return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.agent_provide, provideTool(project));
35
+ };
36
+ exports.agentProvide = agentProvide;
37
+ function provideTool(project) {
38
+ return {
39
+ name: 'agent_provide',
40
+ title: 'Agent Provide',
41
+ description: DESCRIPTION,
42
+ annotations: (0, annotations_1.reads)('Process submission data'),
43
+ inputSchema: (new SchemaBuilder_1.SchemaBuilder(project))
44
+ .form_name()
45
+ .submission_id()
46
+ .task().schema,
47
+ execute: async (args, extra) => {
48
+ const { form_name, submission_id } = args;
49
+ const refused = await refusal(project);
50
+ if (refused)
51
+ return project.mcpJSONResponse(refused, true);
52
+ const requested = args.task || undefined;
53
+ const form = await project.getForm(form_name);
54
+ if (!form) {
55
+ return project.mcpResponse(template_1.ResponseTemplate.formNotFound, { formName: form_name }, true);
56
+ }
57
+ const task = requested || Object.keys(form.uagFields)[0] || 'default';
58
+ const names = { task };
59
+ const uag = form.uagFields[task];
60
+ if (!uag) {
61
+ return project.mcpResponse(template_1.ResponseTemplate.uagComponentNotFound, {
62
+ ...names,
63
+ error: 'no_uag'
64
+ }, true);
65
+ }
66
+ const submission = await form.loadSubmission(submission_id, extra.authInfo);
67
+ if (!submission) {
68
+ return project.mcpResponse(template_1.ResponseTemplate.submissionNotFound, {
69
+ form: form.form,
70
+ submissionId: submission_id
71
+ }, true);
72
+ }
73
+ if (!uag.criteria) {
74
+ return project.mcpResponse(template_1.ResponseTemplate.uagComponentNotFound, {
75
+ ...names,
76
+ error: 'no_criteria'
77
+ }, true);
78
+ }
79
+ const agentFields = Object.values(uag.components);
80
+ if (agentFields.length === 0) {
81
+ return project.mcpResponse(template_1.ResponseTemplate.uagComponentNotFound, {
82
+ ...names,
83
+ error: 'no_fields'
84
+ }, true);
85
+ }
86
+ // Collect more required fields.
87
+ return project.mcpResponse(template_1.ResponseTemplate.agentProcessData, {
88
+ ...names,
89
+ criteria: uag.criteria,
90
+ values: project.uagTemplate?.renderTemplate(template_1.ResponseTemplate.fieldValues, {
91
+ data: form.formatData(submission.data)
92
+ }),
93
+ fields: project.uagTemplate?.renderTemplate(template_1.ResponseTemplate.fieldList, { fields: agentFields })
94
+ });
95
+ }
96
+ };
97
+ }
@@ -6,7 +6,7 @@ const lodash_1 = require("lodash");
6
6
  const SchemaBuilder_1 = require("./SchemaBuilder");
7
7
  const annotations_1 = require("./annotations");
8
8
  const collectData = async (project) => {
9
- return (0, lodash_1.defaultsDeep)(project.config?.toolOverrides?.collect_field_data || {}, {
9
+ return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.collect_field_data, {
10
10
  name: 'collect_field_data',
11
11
  title: 'Collect Field Data',
12
12
  description: 'Collect data for a form, or a specific field with nested components. Identify the component by its path, validate the provided value, and update the form_data object. After updating, determine the next required field to collect or indicate if all required fields are complete. PREREQUISITE: This tool should only be called after the `get_form_fields` tool AND subsequentially the `get_field_info` tool have been called to understand the structure and validation rules of the fields.',
@@ -34,7 +34,8 @@ const collectData = async (project) => {
34
34
  const fields = await form.getFields(submission, extra.authInfo, parent?.data_path);
35
35
  if (fields.errors.length > 0) {
36
36
  if (as_json) {
37
- return project.mcpJSONResponse(fields.errors);
37
+ // structuredContent must be an object: an MCP client refuses a bare array.
38
+ return project.mcpJSONResponse({ errors: fields.errors }, true);
38
39
  }
39
40
  return project.mcpResponse(template_1.ResponseTemplate.fieldValidationErrors, { invalidFields: fields.errors });
40
41
  }
@@ -6,7 +6,7 @@ const lodash_1 = require("lodash");
6
6
  const SchemaBuilder_1 = require("./SchemaBuilder");
7
7
  const annotations_1 = require("./annotations");
8
8
  const confirmSubmission = async (project) => {
9
- return (0, lodash_1.defaultsDeep)(project.config?.toolOverrides?.confirm_form_submission || {}, {
9
+ return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.confirm_form_submission, {
10
10
  name: 'confirm_form_submission',
11
11
  title: 'Confirm Form Submission',
12
12
  description: 'Show a summary of the collected form data if the user wishes to see a summary of the collected data',
@@ -41,9 +41,10 @@ function extractValue(item, component) {
41
41
  }
42
42
  /**
43
43
  * Check if a component is a select-type component with external data (url or resource).
44
+ * A radio can load its options from a URL too.
44
45
  */
45
46
  function isExternalSelect(component) {
46
- if (component.type !== 'select' && component.type !== 'selectboxes')
47
+ if (component.type !== 'select' && component.type !== 'selectboxes' && component.type !== 'radio')
47
48
  return false;
48
49
  const dataSrc = component.dataSrc;
49
50
  return dataSrc === 'url' || dataSrc === 'resource';
@@ -71,7 +72,7 @@ function interpolateHeaders(headers, context) {
71
72
  return result;
72
73
  }
73
74
  const fetchExternalData = async (project) => {
74
- return (0, lodash_1.defaultsDeep)(project.config?.toolOverrides?.fetch_external_data || {}, {
75
+ return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.fetch_external_data, {
75
76
  name: 'fetch_external_data',
76
77
  title: 'Fetch External Data',
77
78
  description: 'Fetch external data for a component that loads data from an external URL, a Form.io Resource, or a DataSource. Use this tool when `get_form_fields` indicates that a field requires calling `fetch_external_data` to retrieve its data. For select components, this returns the available options. For datasource components, this returns the fetched data. You can optionally provide a `search_value` to filter results server-side. If the component\'s URL or headers contain interpolation tokens (e.g. `{{ data.someField }}`), you MUST provide `form_data` with the current collected data so the tokens can be resolved.',
@@ -123,24 +124,14 @@ const fetchExternalData = async (project) => {
123
124
  if (!isExternalSelect(component)) {
124
125
  return project.mcpResponse(template_1.ResponseTemplate.fetchExternalDataError, {
125
126
  dataPath: data_path,
126
- error: `Component at "${data_path}" is of type "${component.type}" and does not load external data. This tool only supports select/selectboxes components with dataSrc "url" or "resource", and datasource components.`
127
+ error: `Component at "${data_path}" is of type "${component.type}" and does not load external data. This tool only supports select, selectboxes and radio components with dataSrc "url" or "resource", and datasource components.`
127
128
  }, true);
128
129
  }
130
+ // isExternalSelect() has checked that dataSrc is "url" or "resource".
129
131
  const selectComponent = component;
130
- const dataSrc = selectComponent.dataSrc;
131
- if (dataSrc !== 'url' && dataSrc !== 'resource') {
132
- return project.mcpResponse(template_1.ResponseTemplate.fetchExternalDataError, {
133
- dataPath: data_path,
134
- error: `Component at "${data_path}" has dataSrc "${dataSrc}". Only "url" and "resource" are supported by this tool.`
135
- }, true);
136
- }
137
- let items = [];
138
- if (dataSrc === 'url') {
139
- items = await fetchUrlData(selectComponent, search_value, extra.authInfo, evalContext);
140
- }
141
- else if (dataSrc === 'resource') {
142
- items = await fetchResourceData(selectComponent, search_value, extra.authInfo, project, evalContext);
143
- }
132
+ const items = selectComponent.dataSrc === 'url'
133
+ ? await fetchUrlData(selectComponent, search_value, extra.authInfo, evalContext)
134
+ : await fetchResourceData(selectComponent, search_value, extra.authInfo, project, evalContext);
144
135
  const options = items.map(item => ({
145
136
  label: extractLabel(item, selectComponent, evalContext),
146
137
  value: extractValue(item, selectComponent)
@@ -7,8 +7,36 @@ const lodash_1 = require("lodash");
7
7
  const SchemaBuilder_1 = require("./SchemaBuilder");
8
8
  const annotations_1 = require("./annotations");
9
9
  const error = require('debug')('formio:uag:findSubmission:error');
10
+ /** The query parameter suffix for each operator that compares the value as given. */
11
+ const COMPARISON_SUFFIXES = new Map([
12
+ ['equals', ''],
13
+ ['greater_than', '__gt'],
14
+ ['greater_than_equal', '__gte'],
15
+ ['less_than', '__lt'],
16
+ ['less_than_equal', '__lte'],
17
+ ['not_equals', '__ne'],
18
+ ]);
19
+ /** The query parameter suffix for each operator that takes comma-separated values. */
20
+ const LIST_SUFFIXES = new Map([
21
+ ['in', '__in'],
22
+ ['nin', '__nin'],
23
+ ]);
24
+ /** The `__regex` pattern for each operator that matches the value literally. */
25
+ const LITERAL_PATTERNS = new Map([
26
+ ['contains', (value) => `/${(0, utils_1.escapeSearchPattern)(value)}/i`],
27
+ ['starts_with', (value) => `/^${(0, utils_1.escapeSearchPattern)(value)}/i`],
28
+ ['ends_with', (value) => `/${(0, utils_1.escapeSearchPattern)(value)}$/i`],
29
+ ]);
30
+ /**
31
+ * A regex operator's pattern with every "/" in it, bare or escaped, written as
32
+ * the `\x2f` escape, which matches the same character. See escapeSearchPattern()
33
+ * for why the query parameter cannot carry a slash.
34
+ */
35
+ const withoutSlashes = (pattern) => pattern.replace(/\\.|\//g, (match) => (match === '/' || match === '\\/') ? '\\x2f' : match);
36
+ /** The last 4 characters of a submission's id, which users disambiguate with. */
37
+ const partialIdOf = (submission) => submission._id ? submission._id.toString().slice(-4) : 'N/A';
10
38
  const findSubmission = async (project) => {
11
- return (0, lodash_1.defaultsDeep)(project.config?.toolOverrides?.find_submissions || {}, {
39
+ return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.find_submissions, {
12
40
  name: 'find_submissions',
13
41
  title: 'Find submissions within a form',
14
42
  description: 'Find existing form submissions based on field values (search query). Use this to search for people, records, or data by name, email, phone, position, company, or other field values. Examples: find a contact by name, find someone who works at a company, find records with specific criteria. To retrieve the total count of records within a form, use this tool without providing a `search_query`.',
@@ -36,6 +64,8 @@ const findSubmission = async (project) => {
36
64
  error: 'Each search criterion must include both data_path and search_value'
37
65
  }, true);
38
66
  }
67
+ let key;
68
+ let value;
39
69
  if (criterion.operator === 'regex') {
40
70
  // The value is a pattern by request here, so it is passed through
41
71
  // unescaped. It is still compiled first: the server drops a filter
@@ -51,42 +81,20 @@ const findSubmission = async (project) => {
51
81
  error: `"${criterion.search_value}" is not a valid regular expression for "${criterion.data_path}". Use the "contains" operator to search for it literally.`
52
82
  }, true);
53
83
  }
54
- query[`data.${criterion.data_path}__regex`] = `/${criterion.search_value}/i`;
84
+ key = `data.${criterion.data_path}__regex`;
85
+ value = `/${withoutSlashes(criterion.search_value)}/i`;
55
86
  }
56
- else if (criterion.operator === 'equals') {
57
- query[`data.${criterion.data_path}`] = criterion.search_value;
87
+ else if (LITERAL_PATTERNS.has(criterion.operator)) {
88
+ key = `data.${criterion.data_path}__regex`;
89
+ value = LITERAL_PATTERNS.get(criterion.operator)(criterion.search_value);
58
90
  }
59
- else if (criterion.operator === 'contains') {
60
- query[`data.${criterion.data_path}__regex`] = `/${(0, utils_1.escapeSearchPattern)(criterion.search_value)}/i`;
91
+ else if (COMPARISON_SUFFIXES.has(criterion.operator)) {
92
+ key = `data.${criterion.data_path}${COMPARISON_SUFFIXES.get(criterion.operator)}`;
93
+ value = criterion.search_value;
61
94
  }
62
- else if (criterion.operator === 'starts_with') {
63
- query[`data.${criterion.data_path}__regex`] = `/^${(0, utils_1.escapeSearchPattern)(criterion.search_value)}/i`;
64
- }
65
- else if (criterion.operator === 'ends_with') {
66
- query[`data.${criterion.data_path}__regex`] = `/${(0, utils_1.escapeSearchPattern)(criterion.search_value)}$/i`;
67
- }
68
- else if (criterion.operator === 'greater_than') {
69
- query[`data.${criterion.data_path}__gt`] = criterion.search_value;
70
- }
71
- else if (criterion.operator === 'greater_than_equal') {
72
- query[`data.${criterion.data_path}__gte`] = criterion.search_value;
73
- }
74
- else if (criterion.operator === 'less_than') {
75
- query[`data.${criterion.data_path}__lt`] = criterion.search_value;
76
- }
77
- else if (criterion.operator === 'less_than_equal') {
78
- query[`data.${criterion.data_path}__lte`] = criterion.search_value;
79
- }
80
- else if (criterion.operator === 'not_equals') {
81
- query[`data.${criterion.data_path}__ne`] = criterion.search_value;
82
- }
83
- else if (criterion.operator === 'in') {
84
- const values = criterion.search_value.split(',').map((v) => v.trim());
85
- query[`data.${criterion.data_path}__in`] = values;
86
- }
87
- else if (criterion.operator === 'nin') {
88
- const values = criterion.search_value.split(',').map((v) => v.trim());
89
- query[`data.${criterion.data_path}__nin`] = values;
95
+ else if (LIST_SUFFIXES.has(criterion.operator)) {
96
+ key = `data.${criterion.data_path}${LIST_SUFFIXES.get(criterion.operator)}`;
97
+ value = criterion.search_value.split(',').map((v) => v.trim());
90
98
  }
91
99
  else {
92
100
  return project.mcpResponse(template_1.ResponseTemplate.submissionSearchError, {
@@ -95,6 +103,16 @@ const findSubmission = async (project) => {
95
103
  error: `Unsupported operator "${criterion.operator}"`
96
104
  }, true);
97
105
  }
106
+ // Criteria are ANDed, but each query parameter holds one value: a
107
+ // second one would replace the first and widen the search.
108
+ if (key in query) {
109
+ return project.mcpResponse(template_1.ResponseTemplate.submissionSearchError, {
110
+ form: form.form,
111
+ searchQuery: search_query,
112
+ error: `"${criterion.data_path}" has two criteria that cannot be combined (contains, starts_with, ends_with and regex count as one kind). Use a single "regex" criterion that matches both, or "in"/"nin" for several values.`
113
+ }, true);
114
+ }
115
+ query[key] = value;
98
116
  }
99
117
  }
100
118
  try {
@@ -129,7 +147,7 @@ const findSubmission = async (project) => {
129
147
  formName: form_name
130
148
  });
131
149
  }
132
- if (submission_id_partial && submissions.length > 1) {
150
+ if (submission_id_partial) {
133
151
  const matchingSubmissions = submissions.filter(sub => sub._id && sub._id.toString().toLowerCase().includes(submission_id_partial.toLowerCase()));
134
152
  if (matchingSubmissions.length === 0) {
135
153
  return project.mcpResponse(template_1.ResponseTemplate.submissionPartialIdNotFound, {
@@ -138,7 +156,7 @@ const findSubmission = async (project) => {
138
156
  partialId: submission_id_partial,
139
157
  availableSubmissions: submissions.map(sub => ({
140
158
  ...form.formatSubmission(sub),
141
- partialId: sub._id ? sub._id.toString().slice(-4) : 'N/A'
159
+ partialId: partialIdOf(sub)
142
160
  }))
143
161
  }, true);
144
162
  }
@@ -150,35 +168,23 @@ const findSubmission = async (project) => {
150
168
  matchingSubmissions: matchingSubmissions.map(sub => ({
151
169
  ...form.formatSubmission(sub),
152
170
  fullId: sub._id,
153
- partialId: sub._id ? sub._id.toString().slice(-4) : 'N/A'
171
+ partialId: partialIdOf(sub)
154
172
  }))
155
173
  }, true);
156
174
  }
157
175
  submissions = matchingSubmissions;
158
176
  }
159
- function getFieldValues(submission) {
160
- if (!fields_requested || fields_requested.length === 0) {
161
- return [];
162
- }
163
- const data = [];
164
- for (const fieldPath of fields_requested) {
165
- data.push({ path: fieldPath, value: (0, lodash_1.get)(submission.data, fieldPath, null) });
166
- }
167
- return data;
168
- }
169
177
  return project.mcpResponse(template_1.ResponseTemplate.submissionsFound, {
170
178
  form: form.form,
171
179
  searchQuery: JSON.stringify(search_query),
172
180
  submissionCount,
173
- submissions: submissions.map(sub => {
174
- return {
175
- _id: sub._id,
176
- created: sub.created,
177
- modified: sub.modified,
178
- data: getFieldValues(sub),
179
- partialId: sub._id ? sub._id.toString().slice(-4) : 'N/A'
180
- };
181
- })
181
+ submissions: submissions.map(sub => ({
182
+ _id: sub._id,
183
+ created: sub.created,
184
+ modified: sub.modified,
185
+ data: (fields_requested || []).map((path) => ({ path, value: (0, lodash_1.get)(sub.data, path, null) })),
186
+ partialId: partialIdOf(sub)
187
+ }))
182
188
  });
183
189
  }
184
190
  catch (err) {