@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.
- package/README.md +17 -15
- package/bin/uag-eval.js +7 -3
- package/bin/uag.js +48 -5
- package/lib/UAGFormInterface.d.ts +12 -0
- package/lib/UAGFormInterface.js +55 -12
- package/lib/UAGProjectInterface.d.ts +9 -1
- package/lib/UAGProjectInterface.js +25 -7
- package/lib/actionLogs.d.ts +1 -10
- package/lib/actionLogs.js +5 -3
- package/lib/actions/runTask.js +4 -3
- package/lib/agent/anonymous.js +5 -4
- package/lib/agent/chat.d.ts +3 -2
- package/lib/agent/chat.js +35 -99
- package/lib/agent/engines.d.ts +10 -6
- package/lib/agent/engines.js +19 -10
- package/lib/agent/errors.d.ts +25 -0
- package/lib/agent/errors.js +132 -0
- package/lib/agent/index.d.ts +1 -1
- package/lib/agent/index.js +2 -1
- package/lib/agent/remote.d.ts +5 -1
- package/lib/agent/remote.js +25 -2
- package/lib/agent/router.d.ts +32 -2
- package/lib/agent/router.js +37 -25
- package/lib/agent/stubs.d.ts +13 -2
- package/lib/agent/stubs.js +18 -5
- package/lib/agent/tools.d.ts +24 -3
- package/lib/agent/tools.js +47 -4
- package/lib/config.d.ts +31 -4
- package/lib/database.d.ts +3 -8
- package/lib/database.js +2 -19
- 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 +11 -4
- package/lib/formflow.js +5 -5
- package/lib/index.d.ts +65 -9
- package/lib/index.js +228 -26
- 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 +19 -0
- package/lib/runner.d.ts +2 -0
- package/lib/runner.js +11 -3
- package/lib/runs.d.ts +17 -79
- package/lib/runs.js +10 -71
- package/lib/testing.d.ts +74 -0
- package/lib/testing.js +180 -0
- package/lib/tools/SchemaBuilder.d.ts +0 -5
- package/lib/tools/SchemaBuilder.js +0 -8
- package/lib/tools/agentDecide.js +2 -2
- package/lib/tools/agentProvide.d.ts +0 -5
- package/lib/tools/agentProvide.js +7 -40
- package/lib/tools/collectData.js +2 -1
- package/lib/tools/index.d.ts +3 -2
- package/lib/tools/index.js +5 -5
- package/lib/tools/readDocument.d.ts +17 -0
- package/lib/tools/readDocument.js +99 -0
- package/lib/tools/submissionUpdate.js +3 -2
- 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 +17 -10
- package/testing.d.ts +1 -0
- package/testing.js +2 -0
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
|
+
}
|
|
@@ -113,11 +113,6 @@ export declare class SchemaBuilder {
|
|
|
113
113
|
* components' `uag` property).
|
|
114
114
|
*/
|
|
115
115
|
task(): this;
|
|
116
|
-
/**
|
|
117
|
-
* The legacy name for `task`. Kept in the schema so clients that still send
|
|
118
|
-
* it are not silently given the default task.
|
|
119
|
-
*/
|
|
120
|
-
persona(): this;
|
|
121
116
|
/**
|
|
122
117
|
* The data_path of a specific field within the form.
|
|
123
118
|
*/
|
|
@@ -175,14 +175,6 @@ class SchemaBuilder {
|
|
|
175
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
|
-
/**
|
|
179
|
-
* The legacy name for `task`. Kept in the schema so clients that still send
|
|
180
|
-
* it are not silently given the default task.
|
|
181
|
-
*/
|
|
182
|
-
persona() {
|
|
183
|
-
this.schema.persona = zod_1.default.string().optional().describe('Deprecated: the former name for `task`. Send `task` instead.');
|
|
184
|
-
return this;
|
|
185
|
-
}
|
|
186
178
|
/**
|
|
187
179
|
* The data_path of a specific field within the form.
|
|
188
180
|
*/
|
package/lib/tools/agentDecide.js
CHANGED
|
@@ -22,7 +22,7 @@ const agentDecide = async (project) => {
|
|
|
22
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
23
|
annotations: (0, annotations_1.reads)('Get decision questions'),
|
|
24
24
|
inputSchema: {
|
|
25
|
-
...(new SchemaBuilder_1.SchemaBuilder(project)).form_name().submission_id().task().
|
|
25
|
+
...(new SchemaBuilder_1.SchemaBuilder(project)).form_name().submission_id().task().schema,
|
|
26
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
27
|
},
|
|
28
28
|
execute: async (args, extra) => {
|
|
@@ -40,7 +40,7 @@ const agentDecide = async (project) => {
|
|
|
40
40
|
data = submission.data || {};
|
|
41
41
|
}
|
|
42
42
|
try {
|
|
43
|
-
const task =
|
|
43
|
+
const task = args.task || undefined;
|
|
44
44
|
const out = await (0, formflow_1.formflow)(project).questions(form, { task, data, submissionId: submission_id }, extra.authInfo);
|
|
45
45
|
return project.mcpJSONResponse({ form: form_name, ...out });
|
|
46
46
|
}
|
|
@@ -6,8 +6,3 @@ import { ToolInfo } from "./utils";
|
|
|
6
6
|
* agent writes its values with `submission_update`.
|
|
7
7
|
*/
|
|
8
8
|
export declare const agentProvide: (project: UAGProjectInterface) => Promise<ToolInfo>;
|
|
9
|
-
/**
|
|
10
|
-
* `agent_provide_data`: the earlier name of `agent_provide`, kept so MCP clients
|
|
11
|
-
* and integrations that call it keep working. Removed in a future major release.
|
|
12
|
-
*/
|
|
13
|
-
export declare const agentProvideData: (project: UAGProjectInterface) => Promise<ToolInfo>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.agentProvide = void 0;
|
|
4
4
|
const template_1 = require("../template");
|
|
5
5
|
const lodash_1 = require("lodash");
|
|
6
6
|
const SchemaBuilder_1 = require("./SchemaBuilder");
|
|
@@ -29,66 +29,33 @@ async function refusal(project) {
|
|
|
29
29
|
* agent writes its values with `submission_update`.
|
|
30
30
|
*/
|
|
31
31
|
const agentProvide = async (project) => {
|
|
32
|
-
// Overrides written for the tool's earlier name still apply, except a rename,
|
|
33
|
-
// which belongs to the deprecated tool and would clash with it.
|
|
34
|
-
const overrides = project.config?.toolOverrides;
|
|
35
|
-
const own = overrides?.agent_provide || (overrides?.agent_provide_data ? (0, lodash_1.omit)(overrides.agent_provide_data, 'name') : {});
|
|
36
32
|
// Merged into a new object: the overrides are shared config, and the tools are
|
|
37
33
|
// built again for every request.
|
|
38
|
-
return (0, lodash_1.defaultsDeep)({},
|
|
34
|
+
return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.agent_provide, provideTool(project));
|
|
39
35
|
};
|
|
40
36
|
exports.agentProvide = agentProvide;
|
|
41
|
-
|
|
42
|
-
* `agent_provide_data`: the earlier name of `agent_provide`, kept so MCP clients
|
|
43
|
-
* and integrations that call it keep working. Removed in a future major release.
|
|
44
|
-
*/
|
|
45
|
-
const agentProvideData = async (project) => {
|
|
46
|
-
const tool = provideTool(project, 'agent_provide_data');
|
|
47
|
-
const execute = tool.execute;
|
|
48
|
-
return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.agent_provide_data, {
|
|
49
|
-
...tool,
|
|
50
|
-
title: 'Agent Provide Data (deprecated)',
|
|
51
|
-
description: `Deprecated: use \`agent_provide\`, which takes the same arguments. ${DESCRIPTION}`,
|
|
52
|
-
execute: (args, extra) => {
|
|
53
|
-
(0, formflow_1.warnLegacy)('agent_provide_data', 'the agent_provide_data MCP tool; use agent_provide.');
|
|
54
|
-
return execute(args, extra);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
};
|
|
58
|
-
exports.agentProvideData = agentProvideData;
|
|
59
|
-
function provideTool(project, name) {
|
|
37
|
+
function provideTool(project) {
|
|
60
38
|
return {
|
|
61
|
-
name,
|
|
39
|
+
name: 'agent_provide',
|
|
62
40
|
title: 'Agent Provide',
|
|
63
41
|
description: DESCRIPTION,
|
|
64
42
|
annotations: (0, annotations_1.reads)('Process submission data'),
|
|
65
43
|
inputSchema: (new SchemaBuilder_1.SchemaBuilder(project))
|
|
66
44
|
.form_name()
|
|
67
45
|
.submission_id()
|
|
68
|
-
.task()
|
|
69
|
-
.persona().schema,
|
|
46
|
+
.task().schema,
|
|
70
47
|
execute: async (args, extra) => {
|
|
71
48
|
const { form_name, submission_id } = args;
|
|
72
49
|
const refused = await refusal(project);
|
|
73
50
|
if (refused)
|
|
74
51
|
return project.mcpJSONResponse(refused, true);
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
requested = (0, formflow_1.resolveTaskName)(args, name);
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
if (err instanceof formflow_1.FormflowError)
|
|
81
|
-
return project.mcpJSONResponse({ error: err.message }, true);
|
|
82
|
-
throw err;
|
|
83
|
-
}
|
|
52
|
+
const requested = args.task || undefined;
|
|
84
53
|
const form = await project.getForm(form_name);
|
|
85
54
|
if (!form) {
|
|
86
55
|
return project.mcpResponse(template_1.ResponseTemplate.formNotFound, { formName: form_name }, true);
|
|
87
56
|
}
|
|
88
57
|
const task = requested || Object.keys(form.uagFields)[0] || 'default';
|
|
89
|
-
|
|
90
|
-
// customized a template that still renders `persona`.
|
|
91
|
-
const names = { task, persona: task };
|
|
58
|
+
const names = { task };
|
|
92
59
|
const uag = form.uagFields[task];
|
|
93
60
|
if (!uag) {
|
|
94
61
|
return project.mcpResponse(template_1.ResponseTemplate.uagComponentNotFound, {
|
package/lib/tools/collectData.js
CHANGED
|
@@ -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
|
}
|
package/lib/tools/index.d.ts
CHANGED
|
@@ -8,10 +8,11 @@ import { findSubmission } from "./findSubmission";
|
|
|
8
8
|
import { submissionUpdate } from "./submissionUpdate";
|
|
9
9
|
import { ToolInfo } from "./utils";
|
|
10
10
|
import { UAGProjectInterface } from "../UAGProjectInterface";
|
|
11
|
-
import { agentProvide
|
|
11
|
+
import { agentProvide } from "./agentProvide";
|
|
12
12
|
import { agentDecide } from "./agentDecide";
|
|
13
13
|
import { fetchExternalData } from "./fetchExternalData";
|
|
14
|
+
import { readDocument } from "./readDocument";
|
|
14
15
|
export * from './utils';
|
|
15
16
|
export * from './annotations';
|
|
16
|
-
export { getForms, getFormFields, getFieldInfo, collectData, confirmSubmission, submitCompletedForm, findSubmission, submissionUpdate, fetchExternalData, agentProvide,
|
|
17
|
+
export { getForms, getFormFields, getFieldInfo, collectData, confirmSubmission, submitCompletedForm, findSubmission, submissionUpdate, fetchExternalData, agentProvide, agentDecide, readDocument };
|
|
17
18
|
export declare const getTools: (project: UAGProjectInterface) => Promise<ToolInfo[]>;
|
package/lib/tools/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.getTools = exports.
|
|
17
|
+
exports.getTools = exports.readDocument = exports.agentDecide = exports.agentProvide = exports.fetchExternalData = exports.submissionUpdate = exports.findSubmission = exports.submitCompletedForm = exports.confirmSubmission = exports.collectData = exports.getFieldInfo = exports.getFormFields = exports.getForms = void 0;
|
|
18
18
|
const getForms_1 = require("./getForms");
|
|
19
19
|
Object.defineProperty(exports, "getForms", { enumerable: true, get: function () { return getForms_1.getForms; } });
|
|
20
20
|
const getFormFields_1 = require("./getFormFields");
|
|
@@ -33,11 +33,12 @@ const submissionUpdate_1 = require("./submissionUpdate");
|
|
|
33
33
|
Object.defineProperty(exports, "submissionUpdate", { enumerable: true, get: function () { return submissionUpdate_1.submissionUpdate; } });
|
|
34
34
|
const agentProvide_1 = require("./agentProvide");
|
|
35
35
|
Object.defineProperty(exports, "agentProvide", { enumerable: true, get: function () { return agentProvide_1.agentProvide; } });
|
|
36
|
-
Object.defineProperty(exports, "agentProvideData", { enumerable: true, get: function () { return agentProvide_1.agentProvideData; } });
|
|
37
36
|
const agentDecide_1 = require("./agentDecide");
|
|
38
37
|
Object.defineProperty(exports, "agentDecide", { enumerable: true, get: function () { return agentDecide_1.agentDecide; } });
|
|
39
38
|
const fetchExternalData_1 = require("./fetchExternalData");
|
|
40
39
|
Object.defineProperty(exports, "fetchExternalData", { enumerable: true, get: function () { return fetchExternalData_1.fetchExternalData; } });
|
|
40
|
+
const readDocument_1 = require("./readDocument");
|
|
41
|
+
Object.defineProperty(exports, "readDocument", { enumerable: true, get: function () { return readDocument_1.readDocument; } });
|
|
41
42
|
__exportStar(require("./utils"), exports);
|
|
42
43
|
__exportStar(require("./annotations"), exports);
|
|
43
44
|
const getTools = async (project) => {
|
|
@@ -52,9 +53,8 @@ const getTools = async (project) => {
|
|
|
52
53
|
(0, submissionUpdate_1.submissionUpdate)(project),
|
|
53
54
|
(0, agentProvide_1.agentProvide)(project),
|
|
54
55
|
(0, agentDecide_1.agentDecide)(project),
|
|
55
|
-
|
|
56
|
-
(0,
|
|
57
|
-
(0, fetchExternalData_1.fetchExternalData)(project)
|
|
56
|
+
(0, fetchExternalData_1.fetchExternalData)(project),
|
|
57
|
+
(0, readDocument_1.readDocument)(project)
|
|
58
58
|
]);
|
|
59
59
|
};
|
|
60
60
|
exports.getTools = getTools;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { UAGProjectInterface } from '../UAGProjectInterface';
|
|
2
|
+
import { ToolInfo } from './utils';
|
|
3
|
+
/**
|
|
4
|
+
* read_document's limits: the largest file it reads (`documents.maxFileBytes`,
|
|
5
|
+
* else DOCUMENTS_MAX_FILE_BYTES, 25 MB), and the most characters of text it
|
|
6
|
+
* returns (`agentApi.files.maxChars`, as task inputs are cut, 200000).
|
|
7
|
+
*/
|
|
8
|
+
export declare function readDocumentLimits(config: any, env?: NodeJS.ProcessEnv): {
|
|
9
|
+
maxBytes: number;
|
|
10
|
+
maxChars: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* read_document: the text of an uploaded document in a submission (a file in a
|
|
14
|
+
* file component, or in an AI Document's files), read through the installed
|
|
15
|
+
* converters with the caller's identity. Read-only.
|
|
16
|
+
*/
|
|
17
|
+
export declare const readDocument: (project: UAGProjectInterface) => Promise<ToolInfo>;
|
|
@@ -0,0 +1,99 @@
|
|
|
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.readDocument = void 0;
|
|
7
|
+
exports.readDocumentLimits = readDocumentLimits;
|
|
8
|
+
const zod_1 = __importDefault(require("zod"));
|
|
9
|
+
const lodash_1 = require("lodash");
|
|
10
|
+
const core_1 = require("@formio/core");
|
|
11
|
+
const template_1 = require("../template");
|
|
12
|
+
const annotations_1 = require("./annotations");
|
|
13
|
+
const readers_1 = require("../documents/readers");
|
|
14
|
+
const DEFAULT_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
15
|
+
const DEFAULT_MAX_CHARS = 200000;
|
|
16
|
+
const positive = (value) => (Number(value) > 0 ? Number(value) : undefined);
|
|
17
|
+
/**
|
|
18
|
+
* read_document's limits: the largest file it reads (`documents.maxFileBytes`,
|
|
19
|
+
* else DOCUMENTS_MAX_FILE_BYTES, 25 MB), and the most characters of text it
|
|
20
|
+
* returns (`agentApi.files.maxChars`, as task inputs are cut, 200000).
|
|
21
|
+
*/
|
|
22
|
+
function readDocumentLimits(config, env = process.env) {
|
|
23
|
+
return {
|
|
24
|
+
maxBytes: positive(config?.documents?.maxFileBytes) || positive(env.DOCUMENTS_MAX_FILE_BYTES) || DEFAULT_MAX_FILE_BYTES,
|
|
25
|
+
maxChars: positive(config?.agentApi?.files?.maxChars) || DEFAULT_MAX_CHARS
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The component at a data path, row indices removed (`rows[2].contract` is `rows.contract`). */
|
|
29
|
+
function componentAt(components, dataPath) {
|
|
30
|
+
const path = dataPath.replace(/\[\d+\]/g, '');
|
|
31
|
+
let found;
|
|
32
|
+
// The data path: with every component included, the second argument also names layout components (panels, columns).
|
|
33
|
+
core_1.Utils.eachComponent(components, (component, componentPath, _components, _parent, paths) => {
|
|
34
|
+
if (!found && (paths?.path ?? componentPath) === path)
|
|
35
|
+
found = component;
|
|
36
|
+
}, true);
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* read_document: the text of an uploaded document in a submission (a file in a
|
|
41
|
+
* file component, or in an AI Document's files), read through the installed
|
|
42
|
+
* converters with the caller's identity. Read-only.
|
|
43
|
+
*/
|
|
44
|
+
const readDocument = async (project) => {
|
|
45
|
+
return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.read_document, {
|
|
46
|
+
name: 'read_document',
|
|
47
|
+
title: 'Read Uploaded Document',
|
|
48
|
+
description: 'Read the text of a document uploaded to a submission: a file in a file field, or in an AI Document field. ' +
|
|
49
|
+
'Give the form, the submission id (from `find_submissions`) and the data path of the field (such as "contract" or "rows[0].attachment"; ' +
|
|
50
|
+
'add an index, such as "contract[1]", for another file than the first). Returns the file name, its type, its page count when known, and its text; ' +
|
|
51
|
+
'a long text is cut, and `truncated` is true.',
|
|
52
|
+
annotations: (0, annotations_1.reads)('Read an uploaded document'),
|
|
53
|
+
inputSchema: {
|
|
54
|
+
form_name: zod_1.default.string().describe('The name of the form, as `get_forms` lists it.'),
|
|
55
|
+
submission_id: zod_1.default.string().describe('The full ID of the submission, as `find_submissions` gives it.'),
|
|
56
|
+
data_path: zod_1.default.string().describe('The data path of the file field or AI Document field.')
|
|
57
|
+
},
|
|
58
|
+
execute: async ({ form_name, submission_id, data_path }, extra) => {
|
|
59
|
+
const form = await project.getForm(form_name);
|
|
60
|
+
if (!form) {
|
|
61
|
+
return project.mcpResponse(template_1.ResponseTemplate.formNotFound, { formName: form_name }, true);
|
|
62
|
+
}
|
|
63
|
+
const submission = await form.loadSubmission(submission_id, extra.authInfo);
|
|
64
|
+
if (!submission) {
|
|
65
|
+
return project.mcpResponse(template_1.ResponseTemplate.submissionNotFound, { submissionId: submission_id, form: form.form }, true);
|
|
66
|
+
}
|
|
67
|
+
// An index on the last segment picks a file of a multiple field: contract[1].
|
|
68
|
+
const index = /\[(\d+)\]$/.exec(data_path);
|
|
69
|
+
const fieldPath = index ? data_path.slice(0, index.index) : data_path;
|
|
70
|
+
const value = (0, lodash_1.get)(submission.data, fieldPath);
|
|
71
|
+
const isDocument = value && typeof value === 'object' && !Array.isArray(value) && Array.isArray(value.files);
|
|
72
|
+
const files = isDocument ? value.files : Array.isArray(value) ? value : (value && typeof value === 'object' ? [value] : []);
|
|
73
|
+
const file = files[index ? Number(index[1]) : 0];
|
|
74
|
+
const component = componentAt(form.form?.components || [], isDocument ? `${fieldPath}.files` : fieldPath);
|
|
75
|
+
if (!file || !component || component.type !== 'file') {
|
|
76
|
+
return project.mcpJSONResponse({ error: `No uploaded file at "${data_path}" in that submission. Give the data path of a file field or an AI Document field.` }, true);
|
|
77
|
+
}
|
|
78
|
+
if (!project.plugins) {
|
|
79
|
+
return project.mcpJSONResponse({ error: 'Documents cannot be read until the server has started.' }, true);
|
|
80
|
+
}
|
|
81
|
+
const limits = readDocumentLimits(project.config);
|
|
82
|
+
try {
|
|
83
|
+
const buffer = await form.readFile(file, extra.authInfo, { maxBytes: limits.maxBytes, component });
|
|
84
|
+
if (!buffer) {
|
|
85
|
+
return project.mcpJSONResponse({ error: `The file is stored as ${file.storage || 'unknown'}, which cannot be read here.` }, true);
|
|
86
|
+
}
|
|
87
|
+
const name = String(file.originalName || file.name || 'file');
|
|
88
|
+
const read = await (0, readers_1.readAsText)(project.plugins, buffer, { name, type: typeof file.type === 'string' ? file.type : undefined }, { signal: extra?.signal });
|
|
89
|
+
const truncated = read.text.length > limits.maxChars;
|
|
90
|
+
return project.mcpJSONResponse({ file_name: name, type: read.type, pages: read.pages ?? null, text: truncated ? read.text.slice(0, limits.maxChars) : read.text, truncated });
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// The message names the file and the reason, never its content.
|
|
94
|
+
return project.mcpJSONResponse({ error: err?.message || 'The file could not be read.' }, true);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
exports.readDocument = readDocument;
|
|
@@ -35,10 +35,11 @@ const submissionUpdate = async (project) => {
|
|
|
35
35
|
const updateSummary = [];
|
|
36
36
|
for (const update of updates) {
|
|
37
37
|
const previousValue = (0, lodash_1.get)(updatedData, update.data_path);
|
|
38
|
-
|
|
38
|
+
const newValue = form.normalizeWrittenValue(update.data_path, update.new_value);
|
|
39
|
+
(0, lodash_1.set)(updatedData, update.data_path, newValue);
|
|
39
40
|
updateSummary.push({
|
|
40
41
|
data_path: update.data_path,
|
|
41
|
-
new_value:
|
|
42
|
+
new_value: newValue,
|
|
42
43
|
previous_value: previousValue || ''
|
|
43
44
|
});
|
|
44
45
|
}
|
package/lib/tools/utils.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ZodRawShape } from "zod";
|
|
2
2
|
import { ToolAnnotations } from "./annotations";
|
|
3
|
+
import type { AuthRequest } from "@formio/appserver";
|
|
4
|
+
import type { UAGProjectInterface } from "../UAGProjectInterface";
|
|
3
5
|
/**
|
|
4
6
|
* Escape a literal value so it can be carried in a `__regex` query parameter.
|
|
5
7
|
*
|
|
@@ -20,14 +22,52 @@ import { ToolAnnotations } from "./annotations";
|
|
|
20
22
|
* @returns The value as a regular expression pattern that matches it literally.
|
|
21
23
|
*/
|
|
22
24
|
export declare const escapeSearchPattern: (value: string) => string;
|
|
25
|
+
/**
|
|
26
|
+
* What a tool's `execute` receives besides its arguments, on every path that
|
|
27
|
+
* runs it: MCP clients on `/mcp`, and Agent API research in process.
|
|
28
|
+
*/
|
|
29
|
+
export type ToolExtra = {
|
|
30
|
+
/** The caller's identity: who the work runs as. Pass it to form methods so Form.io applies its permissions. */
|
|
31
|
+
authInfo: AuthRequest;
|
|
32
|
+
/** The project serving the request. */
|
|
33
|
+
project: UAGProjectInterface;
|
|
34
|
+
/** For a tool from a module with a name and an apiVersion: the module's API (its settings, engines, ...). */
|
|
35
|
+
module?: import('../modules').UAGModuleApi;
|
|
36
|
+
/** On `/mcp`, the rest of the MCP SDK's request context (signal, request id, ...). */
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
};
|
|
23
39
|
export type ToolInfo = {
|
|
24
40
|
name?: string;
|
|
25
41
|
title?: string;
|
|
26
42
|
description?: string;
|
|
27
43
|
inputSchema?: ZodRawShape | undefined;
|
|
28
44
|
annotations?: ToolAnnotations;
|
|
45
|
+
/**
|
|
46
|
+
* Runs the tool: always called with its arguments (an empty object when it
|
|
47
|
+
* takes none), then a `ToolExtra`. Typed loosely so existing tools and their
|
|
48
|
+
* callers keep compiling; annotate `extra` as `ToolExtra` in your own tools.
|
|
49
|
+
*/
|
|
29
50
|
execute?: any;
|
|
51
|
+
/**
|
|
52
|
+
* Builds the project-specific part of the tool (`inputSchema`, `annotations`,
|
|
53
|
+
* `execute`, ...) for a project, as the built-in tools are built. The static
|
|
54
|
+
* `name`, `title` and `description` are used unless `bind` returns its own,
|
|
55
|
+
* so the tool's name is known without a project. Called whenever the tool
|
|
56
|
+
* list is built (every `/mcp` request and every research plan): keep it
|
|
57
|
+
* cheap, and load libraries and other slow state once, outside it.
|
|
58
|
+
*/
|
|
59
|
+
bind?: (project: UAGProjectInterface) => Partial<ToolInfo> | Promise<Partial<ToolInfo>>;
|
|
30
60
|
};
|
|
61
|
+
/** An entry of `config.tools`: a tool, optionally bound to the project with `bind`. */
|
|
62
|
+
export type ToolEntry = ToolInfo;
|
|
63
|
+
/** The shape a `config.tools` entry takes, shown when one is a bare function. */
|
|
64
|
+
export declare const BOUND_TOOL_SHAPE = "{ name: 'my_tool', title: 'My Tool', description: '...', bind(project) { return { inputSchema, annotations, execute }; } }";
|
|
65
|
+
/**
|
|
66
|
+
* Refuse `config.tools` entries that are bare functions (the unreleased factory
|
|
67
|
+
* form): a tool is an object, whose `bind(project)` builds its project-specific
|
|
68
|
+
* part. Throws naming the entry and showing the shape.
|
|
69
|
+
*/
|
|
70
|
+
export declare function assertToolEntries(entries: unknown[] | undefined): void;
|
|
31
71
|
export type ParentInfo = {
|
|
32
72
|
type: string;
|
|
33
73
|
label?: string;
|
package/lib/tools/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.escapeSearchPattern = void 0;
|
|
3
|
+
exports.BOUND_TOOL_SHAPE = exports.escapeSearchPattern = void 0;
|
|
4
|
+
exports.assertToolEntries = assertToolEntries;
|
|
4
5
|
const lodash_1 = require("lodash");
|
|
5
6
|
/**
|
|
6
7
|
* Escape a literal value so it can be carried in a `__regex` query parameter.
|
|
@@ -23,3 +24,17 @@ const lodash_1 = require("lodash");
|
|
|
23
24
|
*/
|
|
24
25
|
const escapeSearchPattern = (value) => (0, lodash_1.escapeRegExp)(value).replace(/\//g, '\\x2f');
|
|
25
26
|
exports.escapeSearchPattern = escapeSearchPattern;
|
|
27
|
+
/** The shape a `config.tools` entry takes, shown when one is a bare function. */
|
|
28
|
+
exports.BOUND_TOOL_SHAPE = "{ name: 'my_tool', title: 'My Tool', description: '...', bind(project) { return { inputSchema, annotations, execute }; } }";
|
|
29
|
+
/**
|
|
30
|
+
* Refuse `config.tools` entries that are bare functions (the unreleased factory
|
|
31
|
+
* form): a tool is an object, whose `bind(project)` builds its project-specific
|
|
32
|
+
* part. Throws naming the entry and showing the shape.
|
|
33
|
+
*/
|
|
34
|
+
function assertToolEntries(entries) {
|
|
35
|
+
(entries || []).forEach((entry, index) => {
|
|
36
|
+
if (typeof entry === 'function') {
|
|
37
|
+
throw new Error(`config.tools[${index}] is a function. A custom tool is an object; build its project-specific part with bind(project): ${exports.BOUND_TOOL_SHAPE}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
package/lib/version.d.ts
ADDED
package/lib/version.js
ADDED