@formio/uag 1.13.2 → 2.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -535
- package/bin/uag-eval.js +99 -0
- package/bin/uag.js +116 -0
- package/lib/UAGFormInterface.d.ts +72 -2
- package/lib/UAGFormInterface.js +226 -57
- package/lib/UAGProjectInterface.d.ts +45 -5
- package/lib/UAGProjectInterface.js +187 -36
- package/lib/actionLogs.d.ts +24 -0
- package/lib/actionLogs.js +149 -0
- package/lib/actions/runTask.d.ts +43 -0
- package/lib/actions/runTask.js +324 -0
- package/lib/agent/anonymous.d.ts +34 -0
- package/lib/agent/anonymous.js +99 -0
- package/lib/agent/chat.d.ts +114 -0
- package/lib/agent/chat.js +475 -0
- package/lib/agent/engines.d.ts +17 -0
- package/lib/agent/engines.js +46 -0
- package/lib/agent/errors.d.ts +25 -0
- package/lib/agent/errors.js +132 -0
- package/lib/agent/index.d.ts +4 -0
- package/lib/agent/index.js +19 -0
- package/lib/agent/remote.d.ts +71 -0
- package/lib/agent/remote.js +252 -0
- package/lib/agent/requestId.d.ts +4 -0
- package/lib/agent/requestId.js +19 -0
- package/lib/agent/router.d.ts +61 -0
- package/lib/agent/router.js +307 -0
- package/lib/agent/stubs.d.ts +37 -0
- package/lib/agent/stubs.js +39 -0
- package/lib/agent/tools.d.ts +31 -0
- package/lib/agent/tools.js +76 -0
- package/lib/config.d.ts +97 -3
- package/lib/config.js +19 -0
- package/lib/database.d.ts +70 -0
- package/lib/database.js +212 -0
- package/lib/documents/convert.d.ts +38 -0
- package/lib/documents/convert.js +151 -0
- package/lib/documents/converters.d.ts +28 -0
- package/lib/documents/converters.js +149 -0
- package/lib/documents/detect.d.ts +31 -0
- package/lib/documents/detect.js +169 -0
- package/lib/documents/index.d.ts +9 -0
- package/lib/documents/index.js +43 -0
- package/lib/documents/mime.d.ts +39 -0
- package/lib/documents/mime.js +100 -0
- package/lib/documents/plugins.d.ts +18 -0
- package/lib/documents/plugins.js +65 -0
- package/lib/documents/readers.d.ts +19 -0
- package/lib/documents/readers.js +64 -0
- package/lib/documents/routes.d.ts +52 -0
- package/lib/documents/routes.js +99 -0
- package/lib/documents/types.d.ts +126 -0
- package/lib/documents/types.js +2 -0
- package/lib/documents/uagJs.d.ts +17 -0
- package/lib/documents/uagJs.js +157 -0
- package/lib/formflow.d.ts +47 -0
- package/lib/formflow.js +28 -0
- package/lib/identity.d.ts +53 -0
- package/lib/identity.js +107 -0
- package/lib/index.d.ts +98 -1
- package/lib/index.js +382 -8
- package/lib/markdown.d.ts +1 -0
- package/lib/markdown.js +166 -0
- package/lib/modules/api.d.ts +25 -0
- package/lib/modules/api.js +98 -0
- package/lib/modules/index.d.ts +4 -0
- package/lib/modules/index.js +24 -0
- package/lib/modules/routes.d.ts +14 -0
- package/lib/modules/routes.js +21 -0
- package/lib/modules/types.d.ts +68 -0
- package/lib/modules/types.js +5 -0
- package/lib/pluginFiles.d.ts +28 -0
- package/lib/pluginFiles.js +101 -0
- package/lib/plugins.d.ts +17 -0
- package/lib/plugins.js +93 -0
- package/lib/richText.d.ts +20 -0
- package/lib/richText.js +99 -0
- package/lib/router.d.ts +9 -0
- package/lib/router.js +34 -25
- package/lib/runner.d.ts +149 -0
- package/lib/runner.js +353 -0
- package/lib/runs.d.ts +27 -0
- package/lib/runs.js +13 -0
- package/lib/template.js +7 -9
- package/lib/templates/uagComponentNotFound.md +5 -5
- package/lib/testing.d.ts +74 -0
- package/lib/testing.js +180 -0
- package/lib/tools/SchemaBuilder.d.ts +3 -3
- package/lib/tools/SchemaBuilder.js +4 -4
- package/lib/tools/agentDecide.d.ts +8 -0
- package/lib/tools/agentDecide.js +57 -0
- package/lib/tools/agentProvide.d.ts +8 -0
- package/lib/tools/agentProvide.js +97 -0
- package/lib/tools/collectData.js +3 -2
- package/lib/tools/confirmSubmission.js +1 -1
- package/lib/tools/fetchExternalData.js +8 -17
- package/lib/tools/findSubmission.js +62 -56
- package/lib/tools/getFieldInfo.js +1 -1
- package/lib/tools/getFormFields.js +1 -1
- package/lib/tools/getForms.js +1 -1
- package/lib/tools/index.d.ts +4 -1
- package/lib/tools/index.js +21 -14
- package/lib/tools/readDocument.d.ts +17 -0
- package/lib/tools/readDocument.js +99 -0
- package/lib/tools/submissionUpdate.js +4 -3
- package/lib/tools/submitForm.js +1 -1
- package/lib/tools/utils.d.ts +40 -0
- package/lib/tools/utils.js +16 -1
- package/lib/version.d.ts +2 -0
- package/lib/version.js +5 -0
- package/package.json +49 -19
- package/testing.d.ts +1 -0
- package/testing.js +2 -0
- package/lib/tools/agentProvideData.d.ts +0 -3
- package/lib/tools/agentProvideData.js +0 -62
package/lib/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
|
|
113
|
-
*
|
|
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
|
-
|
|
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
|
|
172
|
-
*
|
|
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
|
-
|
|
175
|
-
this.schema.
|
|
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
|
+
}
|
package/lib/tools/collectData.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
84
|
+
key = `data.${criterion.data_path}__regex`;
|
|
85
|
+
value = `/${withoutSlashes(criterion.search_value)}/i`;
|
|
55
86
|
}
|
|
56
|
-
else if (criterion.operator
|
|
57
|
-
|
|
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
|
|
60
|
-
|
|
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
|
|
63
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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) {
|