@formio/uag 2.0.0-rc.5 → 2.0.0-rc.7
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/bin/uag.js +6 -2
- package/lib/UAGFormInterface.d.ts +12 -0
- package/lib/UAGFormInterface.js +30 -3
- package/lib/UAGProjectInterface.d.ts +9 -1
- package/lib/UAGProjectInterface.js +14 -3
- package/lib/agent/anonymous.js +1 -1
- package/lib/agent/engines.d.ts +10 -0
- package/lib/agent/engines.js +15 -1
- package/lib/agent/errors.d.ts +9 -0
- package/lib/agent/errors.js +20 -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 +17 -11
- 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 -2
- package/lib/documents/convert.d.ts +38 -0
- package/lib/documents/convert.js +151 -0
- package/lib/documents/converters.d.ts +26 -0
- package/lib/documents/converters.js +139 -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 +4 -1
- package/lib/formflow.js +2 -2
- package/lib/index.d.ts +50 -6
- package/lib/index.js +150 -15
- package/lib/markdown.d.ts +1 -0
- package/lib/markdown.js +166 -0
- package/lib/modules/api.d.ts +22 -0
- package/lib/modules/api.js +100 -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 +16 -0
- package/lib/pluginFiles.js +62 -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/testing.d.ts +74 -0
- package/lib/testing.js +165 -0
- package/lib/tools/index.d.ts +2 -1
- package/lib/tools/index.js +5 -2
- 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/package.json +10 -5
- package/testing.d.ts +1 -0
- package/testing.js +2 -0
package/bin/uag.js
CHANGED
|
@@ -37,9 +37,13 @@ function restartWithoutSnapshot() {
|
|
|
37
37
|
/** Start the UAG with a module, listening on PORT (default 3200). */
|
|
38
38
|
async function start(uagModule) {
|
|
39
39
|
const Express = require('express');
|
|
40
|
-
const { UAGServer, trustProxySetting } = require('../lib');
|
|
40
|
+
const { UAGServer, trustProxySetting, usePluginFiles } = require('../lib');
|
|
41
41
|
const server = new UAGServer();
|
|
42
|
-
|
|
42
|
+
// A module may be exported as a promise (an async module).
|
|
43
|
+
const resolved = await uagModule;
|
|
44
|
+
if (resolved) server.use(resolved);
|
|
45
|
+
// Then the plugins file: UAG_PLUGINS, or the image's /app/plugins/index.mjs.
|
|
46
|
+
await usePluginFiles(server);
|
|
43
47
|
const app = Express();
|
|
44
48
|
// Behind a reverse proxy, its forwarded headers give the public URL (TRUST_PROXY).
|
|
45
49
|
const trust = trustProxySetting(process.env.TRUST_PROXY);
|
|
@@ -191,5 +191,17 @@ export declare class UAGFormInterface extends FormInterface implements FormflowF
|
|
|
191
191
|
path: string;
|
|
192
192
|
label: string;
|
|
193
193
|
}[];
|
|
194
|
+
/**
|
|
195
|
+
* A value an agent engine generated, before it is checked and written: HTML
|
|
196
|
+
* for a rich text field is reduced to the markup its editor keeps.
|
|
197
|
+
*/
|
|
198
|
+
formflowNormalizeValue(component: Component, value: any): any;
|
|
199
|
+
/**
|
|
200
|
+
* A value an MCP client writes at a data path (collect_field_data,
|
|
201
|
+
* confirm_form_submission, submit_completed_form, submission_update), as
|
|
202
|
+
* an engine's is: HTML for a rich text field is reduced to the markup its
|
|
203
|
+
* editor keeps. Row indices are ignored to find the component.
|
|
204
|
+
*/
|
|
205
|
+
normalizeWrittenValue(path: string, value: any): any;
|
|
194
206
|
htmlToMarkdown(html: string): string;
|
|
195
207
|
}
|
package/lib/UAGFormInterface.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.UAGFormInterface = void 0;
|
|
4
4
|
const appserver_1 = require("@formio/appserver");
|
|
5
5
|
const formflow_1 = require("./formflow");
|
|
6
|
+
const richText_1 = require("./richText");
|
|
6
7
|
const core_1 = require("@formio/core");
|
|
7
8
|
const lodash_1 = require("lodash");
|
|
8
9
|
const node_html_markdown_1 = require("node-html-markdown");
|
|
@@ -235,8 +236,12 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
235
236
|
case 'signature':
|
|
236
237
|
rule += 'Have the user draw their signature. The value will be a base64-encoded PNG image string of that signature.';
|
|
237
238
|
break;
|
|
238
|
-
case 'textfield':
|
|
239
239
|
case 'textarea':
|
|
240
|
+
rule += (0, richText_1.isRichText)(component)
|
|
241
|
+
? richText_1.RICH_TEXT_RULE
|
|
242
|
+
: 'The value can be any alphanumeric string, containing letters, numbers, white space characters, and common symbols.';
|
|
243
|
+
break;
|
|
244
|
+
case 'textfield':
|
|
240
245
|
case 'hidden':
|
|
241
246
|
rule += 'The value can be any alphanumeric string, containing letters, numbers, white space characters, and common symbols.';
|
|
242
247
|
break;
|
|
@@ -596,7 +601,7 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
596
601
|
else if (value && this.isMultiple(comp) && typeof value === 'string') {
|
|
597
602
|
value = value.split(',').map((v) => v.trim());
|
|
598
603
|
}
|
|
599
|
-
(0, lodash_1.set)(submission.data, path, value);
|
|
604
|
+
(0, lodash_1.set)(submission.data, path, this.normalizeWrittenValue(path, value));
|
|
600
605
|
}
|
|
601
606
|
return submission;
|
|
602
607
|
}
|
|
@@ -621,7 +626,12 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
621
626
|
}
|
|
622
627
|
else if (this.inputComponent(component) && (0, core_1.componentHasValue)(component, value)) {
|
|
623
628
|
const modelType = core_1.Utils.getModelType(component);
|
|
624
|
-
if ((0,
|
|
629
|
+
if ((0, formflow_1.isBinaryComponent)(component)) {
|
|
630
|
+
// Files and signatures are described, never inlined: a base64 upload would fill the context.
|
|
631
|
+
const described = (0, formflow_1.describeBinary)(component, value);
|
|
632
|
+
value = (0, lodash_1.isObjectLike)(described) ? JSON.stringify(described) : described;
|
|
633
|
+
}
|
|
634
|
+
else if ((0, lodash_1.isObjectLike)(value)) {
|
|
625
635
|
value = JSON.stringify(value);
|
|
626
636
|
}
|
|
627
637
|
else if (modelType === 'string' && (0, lodash_1.isString)(value)) {
|
|
@@ -670,6 +680,23 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
670
680
|
formflowLabeledData(data = {}) {
|
|
671
681
|
return this.formatData(data).map(({ path, label }) => ({ path, label }));
|
|
672
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* A value an agent engine generated, before it is checked and written: HTML
|
|
685
|
+
* for a rich text field is reduced to the markup its editor keeps.
|
|
686
|
+
*/
|
|
687
|
+
formflowNormalizeValue(component, value) {
|
|
688
|
+
return (0, richText_1.isRichText)(component) && typeof value === 'string' ? (0, richText_1.sanitizeRichText)(value) : value;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* A value an MCP client writes at a data path (collect_field_data,
|
|
692
|
+
* confirm_form_submission, submit_completed_form, submission_update), as
|
|
693
|
+
* an engine's is: HTML for a rich text field is reduced to the markup its
|
|
694
|
+
* editor keeps. Row indices are ignored to find the component.
|
|
695
|
+
*/
|
|
696
|
+
normalizeWrittenValue(path, value) {
|
|
697
|
+
const component = this.getComponent(String(path).replace(/\[\d+\]/g, ''));
|
|
698
|
+
return component ? this.formflowNormalizeValue(component, value) : value;
|
|
699
|
+
}
|
|
673
700
|
htmlToMarkdown(html) {
|
|
674
701
|
return node_html_markdown_1.NodeHtmlMarkdown.translate(html, {
|
|
675
702
|
bulletMarker: '-',
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ProjectInterface, SubmissionRequest } from "@formio/appserver";
|
|
2
|
-
import { EngineRegistry, FormflowConfig, FormflowForm, FormflowHost, FormflowTool, ScopedTokenClaims } from "./formflow";
|
|
2
|
+
import { EngineRegistry, FormflowConfig, FormflowFileReader, FormflowForm, FormflowHost, FormflowTool, ScopedTokenClaims } from "./formflow";
|
|
3
3
|
import { UAGConfig } from "./config";
|
|
4
4
|
import { Form } from "@formio/core";
|
|
5
5
|
import { NextFunction, Response, Router } from "express";
|
|
6
6
|
import { UAGFormInterface } from "./UAGFormInterface";
|
|
7
7
|
import { ResponseTemplate, UAGTemplate } from "./template";
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import type { PluginRegistry } from "@formio/appserver";
|
|
9
10
|
export declare class UAGProjectInterface extends ProjectInterface implements FormflowHost {
|
|
10
11
|
user: any;
|
|
11
12
|
/** The names of the UAG forms the project currently has. */
|
|
@@ -38,6 +39,13 @@ export declare class UAGProjectInterface extends ProjectInterface implements For
|
|
|
38
39
|
formflowConfig(): FormflowConfig;
|
|
39
40
|
/** A form tagged "uag", or null. */
|
|
40
41
|
formflowForm(name: string): Promise<FormflowForm | null>;
|
|
42
|
+
/** The server's plugin registry, once its plugins are listed. */
|
|
43
|
+
plugins?: PluginRegistry;
|
|
44
|
+
/**
|
|
45
|
+
* Readers for uploaded documents: one built from the installed converters,
|
|
46
|
+
* which reads every type with a route to text.
|
|
47
|
+
*/
|
|
48
|
+
formflowFileReaders(): FormflowFileReader[];
|
|
41
49
|
/** Every MCP tool, as Agentic Formflow tools. */
|
|
42
50
|
formflowTools(): Promise<FormflowTool[]>;
|
|
43
51
|
authorizeRequest(req: SubmissionRequest, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
|
|
@@ -15,6 +15,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
15
15
|
const version_1 = require("./version");
|
|
16
16
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
17
17
|
const router_2 = require("./agent/router");
|
|
18
|
+
const readers_1 = require("./documents/readers");
|
|
18
19
|
const tools_1 = require("./agent/tools");
|
|
19
20
|
const debug = require('debug')('formio:uag:UAGProjectInterface');
|
|
20
21
|
class UAGProjectInterface extends appserver_1.ProjectInterface {
|
|
@@ -52,12 +53,15 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
|
|
|
52
53
|
server.registerTool(tool.name, {
|
|
53
54
|
title: tool.title,
|
|
54
55
|
description: tool.description,
|
|
55
|
-
|
|
56
|
+
// Always a schema, even an empty one: the MCP SDK calls a tool
|
|
57
|
+
// registered without one as `execute(extra)`, and every tool is
|
|
58
|
+
// `execute(args, extra)`, as Agent API research calls it.
|
|
59
|
+
inputSchema: tool.inputSchema || {},
|
|
56
60
|
// Carries readOnly/destructive/idempotent/openWorld through to the
|
|
57
61
|
// client. Tools from config.tools may omit them, so this stays
|
|
58
62
|
// conditional rather than sending `annotations: undefined`.
|
|
59
63
|
...(tool.annotations && { annotations: tool.annotations })
|
|
60
|
-
},
|
|
64
|
+
}, (args, extra) => (0, tools_1.runTool)(tool, this, args, extra));
|
|
61
65
|
}
|
|
62
66
|
// A scoped token that allows no tools must still answer tools/list, with
|
|
63
67
|
// an empty list: clients list tools first,
|
|
@@ -121,9 +125,16 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
|
|
|
121
125
|
const form = await this.getForm(name);
|
|
122
126
|
return form?.uag ? form : null;
|
|
123
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Readers for uploaded documents: one built from the installed converters,
|
|
130
|
+
* which reads every type with a route to text.
|
|
131
|
+
*/
|
|
132
|
+
formflowFileReaders() {
|
|
133
|
+
return this.plugins ? [(0, readers_1.converterFileReader)(this.plugins)] : [];
|
|
134
|
+
}
|
|
124
135
|
/** Every MCP tool, as Agentic Formflow tools. */
|
|
125
136
|
async formflowTools() {
|
|
126
|
-
return (await (0, tools_1.allTools)(this)).map(tools_1.formflowTool);
|
|
137
|
+
return (await (0, tools_1.allTools)(this)).map((tool) => (0, tools_1.formflowTool)(tool, this));
|
|
127
138
|
}
|
|
128
139
|
async authorizeRequest(req, res, next) {
|
|
129
140
|
try {
|
package/lib/agent/anonymous.js
CHANGED
|
@@ -76,7 +76,7 @@ function anonymousGate(project) {
|
|
|
76
76
|
const form = request.form;
|
|
77
77
|
const limits = anonymousLimits(project);
|
|
78
78
|
if (limits.perAddress === 0 || limits.total === 0 || !(0, formflow_1.flagOn)(form?.form?.properties?.uagAnonymous)) {
|
|
79
|
-
return res.status(401).json({ error: 'Sign in to use this form\'s agent operations.', code: 'sign_in_required' });
|
|
79
|
+
return res.status(401).json({ error: 'Sign in to use this form\'s agent operations and routes.', code: 'sign_in_required' });
|
|
80
80
|
}
|
|
81
81
|
const taken = limiterFor(project).take(String(req.ip || req.socket?.remoteAddress || ''), limits);
|
|
82
82
|
if (!taken.allowed) {
|
package/lib/agent/engines.d.ts
CHANGED
|
@@ -4,6 +4,16 @@ import { EngineRegistry } from '../formflow';
|
|
|
4
4
|
export declare const UAG_ROOT: string;
|
|
5
5
|
/** Engine types every UAG has, whatever is installed. */
|
|
6
6
|
export declare const BUILT_IN_ENGINES: Record<string, EngineTypeDefinition>;
|
|
7
|
+
/** What is known of each loaded engine type, by type name: for listing it as a plugin. */
|
|
8
|
+
export type LoadedEngineType = {
|
|
9
|
+
name: string;
|
|
10
|
+
title: string;
|
|
11
|
+
version?: string;
|
|
12
|
+
kinds: ('decision' | 'agent')[];
|
|
13
|
+
instances: string[];
|
|
14
|
+
};
|
|
15
|
+
/** The engine types a registry's instances were created from, in order of first use. */
|
|
16
|
+
export declare function engineTypesOf(registry: EngineRegistry | undefined): LoadedEngineType[];
|
|
7
17
|
/**
|
|
8
18
|
* Enable the engines the configuration names (`engines`), or else
|
|
9
19
|
* `UAG_ENGINES`. Each type is resolved from the configuration's `engineTypes`,
|
package/lib/agent/engines.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.BUILT_IN_ENGINES = exports.UAG_ROOT = void 0;
|
|
7
|
+
exports.engineTypesOf = engineTypesOf;
|
|
7
8
|
exports.loadUAGEngines = loadUAGEngines;
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const uag_sdk_1 = require("@formio/uag-sdk");
|
|
@@ -19,6 +20,11 @@ exports.BUILT_IN_ENGINES = {
|
|
|
19
20
|
'stub-decision': stubs_1.StubDecisionEngine,
|
|
20
21
|
'stub-agent': stubs_1.StubAgentEngine,
|
|
21
22
|
};
|
|
23
|
+
const loadedTypes = new WeakMap();
|
|
24
|
+
/** The engine types a registry's instances were created from, in order of first use. */
|
|
25
|
+
function engineTypesOf(registry) {
|
|
26
|
+
return (registry && loadedTypes.get(registry)) || [];
|
|
27
|
+
}
|
|
22
28
|
/**
|
|
23
29
|
* Enable the engines the configuration names (`engines`), or else
|
|
24
30
|
* `UAG_ENGINES`. Each type is resolved from the configuration's `engineTypes`,
|
|
@@ -33,5 +39,13 @@ async function loadUAGEngines(config = {}, env = process.env) {
|
|
|
33
39
|
config,
|
|
34
40
|
apiVersion: appserver_1.Formflow.ENGINE_API_VERSION,
|
|
35
41
|
});
|
|
36
|
-
|
|
42
|
+
const registry = appserver_1.Formflow.engineRegistry(loaded);
|
|
43
|
+
const types = new Map();
|
|
44
|
+
for (const entry of loaded) {
|
|
45
|
+
const type = types.get(entry.type) || { name: entry.type, title: entry.title || entry.type, version: entry.typeVersion || entry.version, kinds: entry.kinds || [entry.engine.kind], instances: [] };
|
|
46
|
+
type.instances.push(entry.engine.name);
|
|
47
|
+
types.set(entry.type, type);
|
|
48
|
+
}
|
|
49
|
+
loadedTypes.set(registry, [...types.values()]);
|
|
50
|
+
return registry;
|
|
37
51
|
}
|
package/lib/agent/errors.d.ts
CHANGED
|
@@ -3,6 +3,15 @@ export declare function isAgentApiRequest(req: Request): boolean;
|
|
|
3
3
|
/** The code for a status, when the error names none. */
|
|
4
4
|
export declare function errorCode(status: number): string;
|
|
5
5
|
export declare const INTERNAL_ERROR = "The request failed. See the UAG logs for this request id.";
|
|
6
|
+
/**
|
|
7
|
+
* An error as the Agent API answers it: its status, and the body. Errors with
|
|
8
|
+
* a status (FormflowError) keep their message and code; anything else is a 500
|
|
9
|
+
* with no detail. Agent operations and module routes share it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function agentError(requestId: string, err: any): {
|
|
12
|
+
status: number;
|
|
13
|
+
body: Record<string, any>;
|
|
14
|
+
};
|
|
6
15
|
/** The request's id, assigned once: the caller's, when it is a short token, or a new one. */
|
|
7
16
|
export declare function requestIdOf(req: Request, res: Response): string;
|
|
8
17
|
/** The error body for a status and whatever a handler sent: an object, JSON text, plain text or nothing. */
|
package/lib/agent/errors.js
CHANGED
|
@@ -3,11 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.INTERNAL_ERROR = void 0;
|
|
4
4
|
exports.isAgentApiRequest = isAgentApiRequest;
|
|
5
5
|
exports.errorCode = errorCode;
|
|
6
|
+
exports.agentError = agentError;
|
|
6
7
|
exports.requestIdOf = requestIdOf;
|
|
7
8
|
exports.errorBody = errorBody;
|
|
8
9
|
exports.agentErrorShape = agentErrorShape;
|
|
9
10
|
const crypto_1 = require("crypto");
|
|
10
11
|
const protocol_1 = require("@formio/uag-sdk/protocol");
|
|
12
|
+
const formflow_1 = require("../formflow");
|
|
11
13
|
/**
|
|
12
14
|
* The Agent API's one error shape. Every error a form's agent operation or
|
|
13
15
|
* `/agent/capabilities` answers is JSON
|
|
@@ -42,6 +44,24 @@ function errorCode(status) {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
exports.INTERNAL_ERROR = 'The request failed. See the UAG logs for this request id.';
|
|
47
|
+
/**
|
|
48
|
+
* An error as the Agent API answers it: its status, and the body. Errors with
|
|
49
|
+
* a status (FormflowError) keep their message and code; anything else is a 500
|
|
50
|
+
* with no detail. Agent operations and module routes share it.
|
|
51
|
+
*/
|
|
52
|
+
function agentError(requestId, err) {
|
|
53
|
+
const known = err instanceof formflow_1.FormflowError;
|
|
54
|
+
const status = known ? err.status : 500;
|
|
55
|
+
return {
|
|
56
|
+
status,
|
|
57
|
+
body: {
|
|
58
|
+
requestId,
|
|
59
|
+
error: status === 500 ? exports.INTERNAL_ERROR : String(err?.message || exports.INTERNAL_ERROR),
|
|
60
|
+
code: (known && err.code) || errorCode(status),
|
|
61
|
+
...(err?.edition && { edition: err.edition })
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
45
65
|
/** The request's id, assigned once: the caller's, when it is a short token, or a new one. */
|
|
46
66
|
function requestIdOf(req, res) {
|
|
47
67
|
const request = req;
|
package/lib/agent/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { StubAgentEngine, StubDecisionEngine } from './stubs';
|
|
1
|
+
export { StubAgentEngine, StubDecisionEngine, stubGenerateRequests } from './stubs';
|
|
2
2
|
export { BUILT_IN_ENGINES, UAG_ROOT, loadUAGEngines } from './engines';
|
|
3
3
|
export { AgentRouter, FormAgentRouter, agentCredentials, agentNotFound } from './router';
|
|
4
4
|
export { RemoteEngine, MIN_KEY_LENGTH } from './remote';
|
package/lib/agent/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MIN_KEY_LENGTH = exports.RemoteEngine = exports.agentNotFound = exports.agentCredentials = exports.FormAgentRouter = exports.AgentRouter = exports.loadUAGEngines = exports.UAG_ROOT = exports.BUILT_IN_ENGINES = exports.StubDecisionEngine = exports.StubAgentEngine = void 0;
|
|
3
|
+
exports.MIN_KEY_LENGTH = exports.RemoteEngine = exports.agentNotFound = exports.agentCredentials = exports.FormAgentRouter = exports.AgentRouter = exports.loadUAGEngines = exports.UAG_ROOT = exports.BUILT_IN_ENGINES = exports.stubGenerateRequests = exports.StubDecisionEngine = exports.StubAgentEngine = void 0;
|
|
4
4
|
var stubs_1 = require("./stubs");
|
|
5
5
|
Object.defineProperty(exports, "StubAgentEngine", { enumerable: true, get: function () { return stubs_1.StubAgentEngine; } });
|
|
6
6
|
Object.defineProperty(exports, "StubDecisionEngine", { enumerable: true, get: function () { return stubs_1.StubDecisionEngine; } });
|
|
7
|
+
Object.defineProperty(exports, "stubGenerateRequests", { enumerable: true, get: function () { return stubs_1.stubGenerateRequests; } });
|
|
7
8
|
var engines_1 = require("./engines");
|
|
8
9
|
Object.defineProperty(exports, "BUILT_IN_ENGINES", { enumerable: true, get: function () { return engines_1.BUILT_IN_ENGINES; } });
|
|
9
10
|
Object.defineProperty(exports, "UAG_ROOT", { enumerable: true, get: function () { return engines_1.UAG_ROOT; } });
|
package/lib/agent/remote.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ type Connection = {
|
|
|
25
25
|
baseUrl: string;
|
|
26
26
|
key: string;
|
|
27
27
|
fetch: Fetch;
|
|
28
|
+
/** The most PDF bytes one generate request may send: documents.maxTotalBytes. */
|
|
29
|
+
maxDocumentBytes: number;
|
|
28
30
|
};
|
|
29
31
|
/**
|
|
30
32
|
* Describe the remote engine, retrying while the engine server is unreachable
|
|
@@ -39,7 +41,9 @@ export declare function describeRemote(connection: Connection, engine: string, {
|
|
|
39
41
|
/** An engine object for a described remote engine. */
|
|
40
42
|
export declare function remoteEngine(connection: Connection, remoteName: string, described: RemoteEngineDescription): DecisionEngine | AgentEngine;
|
|
41
43
|
export declare const RemoteEngine: Readonly<{
|
|
42
|
-
type:
|
|
44
|
+
type: "engine";
|
|
45
|
+
name: string;
|
|
46
|
+
title: string;
|
|
43
47
|
kinds: ("agent" | "decision")[];
|
|
44
48
|
apiVersion: 1;
|
|
45
49
|
description: string;
|
package/lib/agent/remote.js
CHANGED
|
@@ -158,12 +158,13 @@ function remoteEngine(connection, remoteName, described) {
|
|
|
158
158
|
},
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
const agent = {
|
|
162
162
|
name: connection.name,
|
|
163
163
|
kind: 'agent',
|
|
164
164
|
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
165
165
|
model: described.model,
|
|
166
166
|
toolUse: described.toolUse === true,
|
|
167
|
+
accepts: described.accepts || ['text'],
|
|
167
168
|
async extract(request) {
|
|
168
169
|
const research = !!request.tools?.length;
|
|
169
170
|
// The engine server researches through its own MCP connection to the
|
|
@@ -182,9 +183,30 @@ function remoteEngine(connection, remoteName, described) {
|
|
|
182
183
|
return { ...response, model: response.model || described.model };
|
|
183
184
|
},
|
|
184
185
|
};
|
|
186
|
+
if (described.generate) {
|
|
187
|
+
agent.generate = async (request) => {
|
|
188
|
+
// Refused before anything is sent; the documents are never logged.
|
|
189
|
+
if ((0, protocol_1.pdfByteLength)(request.attachments) > connection.maxDocumentBytes) {
|
|
190
|
+
throw new uag_sdk_1.EngineError(`The attached PDFs exceed ${connection.maxDocumentBytes} bytes, the most one request to the engine server may carry.`);
|
|
191
|
+
}
|
|
192
|
+
const body = {
|
|
193
|
+
instructions: request.instructions,
|
|
194
|
+
...(request.system !== undefined && { system: request.system }),
|
|
195
|
+
attachments: (0, protocol_1.encodeAttachments)(request.attachments),
|
|
196
|
+
format: request.format,
|
|
197
|
+
...(request.maxTokens !== undefined && { maxTokens: request.maxTokens }),
|
|
198
|
+
};
|
|
199
|
+
const response = valid(protocol_1.checkGenerateResponse, await call(connection, `${path}/generate`, body, request.signal));
|
|
200
|
+
request.onProgress?.(response.text.length);
|
|
201
|
+
return { ...response, model: response.model || described.model };
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return agent;
|
|
185
205
|
}
|
|
186
206
|
exports.RemoteEngine = (0, uag_sdk_1.defineEngine)({
|
|
187
|
-
type: '
|
|
207
|
+
type: 'engine',
|
|
208
|
+
name: 'remote',
|
|
209
|
+
title: 'Remote engine (engine server)',
|
|
188
210
|
kinds: ['decision', 'agent'],
|
|
189
211
|
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
190
212
|
description: 'An engine hosted by an engine server, over the remote engine protocol.',
|
|
@@ -216,6 +238,7 @@ exports.RemoteEngine = (0, uag_sdk_1.defineEngine)({
|
|
|
216
238
|
baseUrl: url.href.replace(/\/+$/, ''),
|
|
217
239
|
key: settings.key,
|
|
218
240
|
fetch: options.fetch || fetch,
|
|
241
|
+
maxDocumentBytes: Math.min(Number(config?.documents?.maxTotalBytes) || protocol_1.MAX_DOCUMENT_BYTES, protocol_1.MAX_DOCUMENT_BYTES),
|
|
219
242
|
};
|
|
220
243
|
const remoteName = settings.engine || name;
|
|
221
244
|
const described = await describeRemote(connection, remoteName, {
|
package/lib/agent/router.d.ts
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
|
-
import { RequestHandler, Router } from "express";
|
|
1
|
+
import { NextFunction, Request, RequestHandler, Response, Router } from "express";
|
|
2
|
+
import { SubmissionRequest } from "@formio/appserver";
|
|
3
|
+
import { FormflowService } from "../formflow";
|
|
2
4
|
import type { UAGProjectInterface } from "../UAGProjectInterface";
|
|
3
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The Agent API: HTTP adapters onto Agentic Formflow in @formio/appserver-core.
|
|
7
|
+
* Each endpoint reads its request, calls the service, and writes JSON (or, for
|
|
8
|
+
* extract, a Server-Sent Events stream). Errors answer with the status the
|
|
9
|
+
* service gives.
|
|
10
|
+
*
|
|
11
|
+
* The operations belong to a form, `{form}/agent/{operation}`, as its
|
|
12
|
+
* submissions do: the form comes from the URL and the caller from the
|
|
13
|
+
* project's own authentication. Capabilities describe the deployment, at
|
|
14
|
+
* `/agent/capabilities`.
|
|
15
|
+
*/
|
|
16
|
+
type AgentRequest = SubmissionRequest & {
|
|
17
|
+
requestId: string;
|
|
18
|
+
};
|
|
19
|
+
/** The largest request body an agent operation takes by default, in bytes. */
|
|
4
20
|
export declare const AGENT_BODY_LIMIT: number;
|
|
21
|
+
/**
|
|
22
|
+
* The largest request body the project's agent operations take: `agentApi.bodyLimit`,
|
|
23
|
+
* then AGENT_BODY_LIMIT, then 5 MB. A document sent inline (a base64 file a task reads)
|
|
24
|
+
* may need more; the server's own MAX_BODY_SIZE (16 MB) still applies first.
|
|
25
|
+
*/
|
|
26
|
+
export declare function agentBodyLimit(project: UAGProjectInterface): number;
|
|
27
|
+
type Handler = (req: AgentRequest, res: Response, service: FormflowService) => Promise<void>;
|
|
28
|
+
/** The parts every Agent API endpoint shares: the request id, and errors answered as JSON (or as an event mid-stream). */
|
|
29
|
+
export declare function agentEndpoints(project: UAGProjectInterface): {
|
|
30
|
+
json: (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
|
|
31
|
+
requestId: (req: any, res: Response, next: NextFunction) => void;
|
|
32
|
+
handle: (operation: string, fn: Handler) => (req: Request, res: Response) => Promise<void>;
|
|
33
|
+
};
|
|
5
34
|
/**
|
|
6
35
|
* `GET /agent/capabilities`, behind `authenticate`. Only this path is claimed:
|
|
7
36
|
* any other `/agent...` path (a project form at `agent`, for one) falls through
|
|
@@ -29,3 +58,4 @@ export declare function agentCredentials(project: UAGProjectInterface): RequestH
|
|
|
29
58
|
* form of the URL.
|
|
30
59
|
*/
|
|
31
60
|
export declare function FormAgentRouter(project: UAGProjectInterface, credentials: RequestHandler): Router;
|
|
61
|
+
export {};
|
package/lib/agent/router.js
CHANGED
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.AGENT_BODY_LIMIT = void 0;
|
|
37
|
+
exports.agentBodyLimit = agentBodyLimit;
|
|
38
|
+
exports.agentEndpoints = agentEndpoints;
|
|
37
39
|
exports.AgentRouter = AgentRouter;
|
|
38
40
|
exports.agentNotFound = agentNotFound;
|
|
39
41
|
exports.agentCredentials = agentCredentials;
|
|
@@ -107,19 +109,29 @@ async function respondWithStream(req, res, run) {
|
|
|
107
109
|
res.json(done);
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
|
-
/** The largest request body an agent operation takes, in bytes. */
|
|
112
|
+
/** The largest request body an agent operation takes by default, in bytes. */
|
|
111
113
|
exports.AGENT_BODY_LIMIT = 5 * 1024 * 1024;
|
|
114
|
+
/**
|
|
115
|
+
* The largest request body the project's agent operations take: `agentApi.bodyLimit`,
|
|
116
|
+
* then AGENT_BODY_LIMIT, then 5 MB. A document sent inline (a base64 file a task reads)
|
|
117
|
+
* may need more; the server's own MAX_BODY_SIZE (16 MB) still applies first.
|
|
118
|
+
*/
|
|
119
|
+
function agentBodyLimit(project) {
|
|
120
|
+
const configured = Number(project.config?.agentApi?.bodyLimit ?? process.env.AGENT_BODY_LIMIT);
|
|
121
|
+
return Number.isFinite(configured) && configured > 0 ? configured : exports.AGENT_BODY_LIMIT;
|
|
122
|
+
}
|
|
112
123
|
/** The parts every Agent API endpoint shares: the request id, and errors answered as JSON (or as an event mid-stream). */
|
|
113
124
|
function agentEndpoints(project) {
|
|
114
|
-
const
|
|
125
|
+
const limit = agentBodyLimit(project);
|
|
126
|
+
const parse = express_1.default.json({ limit });
|
|
115
127
|
// The server parses JSON bodies first, with its own larger limit, so the
|
|
116
128
|
// agent operations' limit is checked here on a body already read.
|
|
117
129
|
const json = (req, res, next) => {
|
|
118
130
|
if (req.body === undefined)
|
|
119
131
|
return parse(req, res, next);
|
|
120
132
|
const length = Number(req.get('content-length')) || Buffer.byteLength(JSON.stringify(req.body ?? null));
|
|
121
|
-
if (length >
|
|
122
|
-
return res.status(413).json({ error: `The request body is larger than the agent operations' limit of ${
|
|
133
|
+
if (length > limit) {
|
|
134
|
+
return res.status(413).json({ error: `The request body is larger than the agent operations' limit of ${Math.round(limit / (1024 * 1024) * 10) / 10} MB.`, code: 'payload_too_large' });
|
|
123
135
|
}
|
|
124
136
|
next();
|
|
125
137
|
};
|
|
@@ -139,19 +151,13 @@ function agentEndpoints(project) {
|
|
|
139
151
|
await (0, requestId_1.withRequestId)(agentReq.requestId, () => fn(agentReq, res, service));
|
|
140
152
|
}
|
|
141
153
|
catch (err) {
|
|
142
|
-
const status =
|
|
154
|
+
const { status, body } = (0, errors_1.agentError)(agentReq.requestId, err);
|
|
143
155
|
if (status === 500) {
|
|
144
156
|
debug(`[${agentReq.requestId}] ${operation} failed:`, err);
|
|
145
157
|
}
|
|
146
158
|
if (err instanceof formflow_1.RateLimitError) {
|
|
147
159
|
res.set('Retry-After', String(Math.ceil(err.retryAfterMs / 1000)));
|
|
148
160
|
}
|
|
149
|
-
const body = {
|
|
150
|
-
requestId: agentReq.requestId,
|
|
151
|
-
error: status === 500 ? errors_1.INTERNAL_ERROR : err.message,
|
|
152
|
-
code: (err instanceof formflow_1.FormflowError && err.code) || (0, errors_1.errorCode)(status),
|
|
153
|
-
...(err?.edition && { edition: err.edition })
|
|
154
|
-
};
|
|
155
161
|
if (res.headersSent) {
|
|
156
162
|
// Mid-stream: report the failure as an event and close.
|
|
157
163
|
res.write(`event: error\ndata: ${JSON.stringify(body)}\n\n`);
|
package/lib/agent/stubs.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GenerateRequest } from '@formio/uag-sdk';
|
|
1
2
|
/**
|
|
2
3
|
* Stand-in engines for tests and local development. They answer instantly with
|
|
3
4
|
* predictable values, so the Agent API can be exercised end to end without
|
|
@@ -6,7 +7,9 @@
|
|
|
6
7
|
* any instance name.
|
|
7
8
|
*/
|
|
8
9
|
export declare const StubDecisionEngine: Readonly<{
|
|
9
|
-
type:
|
|
10
|
+
type: "engine";
|
|
11
|
+
name: string;
|
|
12
|
+
title: string;
|
|
10
13
|
kinds: "decision"[];
|
|
11
14
|
apiVersion: 1;
|
|
12
15
|
description: string;
|
|
@@ -14,8 +17,16 @@ export declare const StubDecisionEngine: Readonly<{
|
|
|
14
17
|
calls: number;
|
|
15
18
|
};
|
|
16
19
|
}>;
|
|
20
|
+
/** How many generate requests the stub agent engines keep: a UAG running them for a demo never grows without bound. */
|
|
21
|
+
export declare const STUB_GENERATE_REQUESTS_KEPT = 20;
|
|
22
|
+
/** The last generate requests the stub agent engines received (STUB_GENERATE_REQUESTS_KEPT), oldest first, for tests. */
|
|
23
|
+
export declare const stubGenerateRequests: Array<Omit<GenerateRequest, 'signal' | 'onProgress'> & {
|
|
24
|
+
engine: string;
|
|
25
|
+
}>;
|
|
17
26
|
export declare const StubAgentEngine: Readonly<{
|
|
18
|
-
type:
|
|
27
|
+
type: "engine";
|
|
28
|
+
name: string;
|
|
29
|
+
title: string;
|
|
19
30
|
kinds: "agent"[];
|
|
20
31
|
apiVersion: 1;
|
|
21
32
|
description: string;
|
package/lib/agent/stubs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StubAgentEngine = exports.StubDecisionEngine = void 0;
|
|
3
|
+
exports.StubAgentEngine = exports.stubGenerateRequests = exports.STUB_GENERATE_REQUESTS_KEPT = exports.StubDecisionEngine = void 0;
|
|
4
4
|
const uag_sdk_1 = require("@formio/uag-sdk");
|
|
5
5
|
const formflow_1 = require("../formflow");
|
|
6
6
|
/**
|
|
@@ -11,16 +11,29 @@ const formflow_1 = require("../formflow");
|
|
|
11
11
|
* any instance name.
|
|
12
12
|
*/
|
|
13
13
|
exports.StubDecisionEngine = (0, uag_sdk_1.defineEngine)({
|
|
14
|
-
type: '
|
|
14
|
+
type: 'engine',
|
|
15
|
+
name: 'stub-decision',
|
|
16
|
+
title: 'Stub decision engine',
|
|
15
17
|
kinds: ['decision'],
|
|
16
18
|
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
17
19
|
description: 'Answers every decision with a fixed, predictable value. For tests and local development.',
|
|
18
20
|
create: ({ name }) => (0, formflow_1.stubDecisionEngine)({ name }),
|
|
19
21
|
});
|
|
22
|
+
/** How many generate requests the stub agent engines keep: a UAG running them for a demo never grows without bound. */
|
|
23
|
+
exports.STUB_GENERATE_REQUESTS_KEPT = 20;
|
|
24
|
+
/** The last generate requests the stub agent engines received (STUB_GENERATE_REQUESTS_KEPT), oldest first, for tests. */
|
|
25
|
+
exports.stubGenerateRequests = [];
|
|
20
26
|
exports.StubAgentEngine = (0, uag_sdk_1.defineEngine)({
|
|
21
|
-
type: '
|
|
27
|
+
type: 'engine',
|
|
28
|
+
name: 'stub-agent',
|
|
29
|
+
title: 'Stub agent engine',
|
|
22
30
|
kinds: ['agent'],
|
|
23
31
|
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
24
|
-
description: 'Extracts nothing. For tests and local development.',
|
|
25
|
-
|
|
32
|
+
description: 'Extracts nothing; generates a predictable answer from the text it is given. For tests and local development.',
|
|
33
|
+
// Text only; its generate answer shows what reached it. Observing its requests keeps it a built-in stub.
|
|
34
|
+
create: ({ name }) => (0, formflow_1.stubAgentEngine)({ name, onGenerate: (request) => {
|
|
35
|
+
exports.stubGenerateRequests.push({ ...request, engine: name });
|
|
36
|
+
if (exports.stubGenerateRequests.length > exports.STUB_GENERATE_REQUESTS_KEPT)
|
|
37
|
+
exports.stubGenerateRequests.splice(0, exports.stubGenerateRequests.length - exports.STUB_GENERATE_REQUESTS_KEPT);
|
|
38
|
+
} }),
|
|
26
39
|
});
|
package/lib/agent/tools.d.ts
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import { FormflowTool } from "../formflow";
|
|
2
|
+
import type { AuthRequest } from "@formio/appserver";
|
|
2
3
|
import type { UAGProjectInterface } from "../UAGProjectInterface";
|
|
3
|
-
import { ToolInfo } from "../tools/utils";
|
|
4
|
-
/**
|
|
4
|
+
import { ToolEntry, ToolInfo } from "../tools/utils";
|
|
5
|
+
/** Record the module a tool came from: its `execute` gets the module's API as `extra.module`. */
|
|
6
|
+
export declare function setToolModule(entry: object, api: any): void;
|
|
7
|
+
/**
|
|
8
|
+
* A `config.tools` entry as a tool for the project: a bound tool's static
|
|
9
|
+
* fields, overlaid with what its `bind(project)` returns. A tool without
|
|
10
|
+
* `bind` (the 1.x shape) is used as it is.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveTool(project: UAGProjectInterface, entry: ToolEntry): Promise<ToolInfo | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Every tool the UAG serves, including custom tools from configuration, each
|
|
15
|
+
* entry of which is a tool, optionally bound to the project.
|
|
16
|
+
*/
|
|
5
17
|
export declare function allTools(project: UAGProjectInterface): Promise<ToolInfo[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Run a tool with its arguments and the `ToolExtra` for the project: the same
|
|
20
|
+
* call on `/mcp` and for Agent API research. `context` is whatever else the
|
|
21
|
+
* caller has (on `/mcp`, the MCP SDK's request context, with `authInfo`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function runTool(tool: ToolInfo, project: UAGProjectInterface, args: any, context: {
|
|
24
|
+
authInfo: AuthRequest;
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
}): any;
|
|
6
27
|
/**
|
|
7
28
|
* An MCP tool as an Agentic Formflow tool: its schema as JSON, its arguments
|
|
8
29
|
* validated with its own schema, and read-only only when annotated so.
|
|
9
30
|
*/
|
|
10
|
-
export declare function formflowTool(tool: ToolInfo): FormflowTool;
|
|
31
|
+
export declare function formflowTool(tool: ToolInfo, project: UAGProjectInterface): FormflowTool;
|