@formio/uag 1.13.1 → 2.0.0-rc.1
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 +235 -535
- package/bin/uag-eval.js +95 -0
- package/bin/uag.js +73 -0
- package/lib/UAGFormInterface.d.ts +60 -2
- package/lib/UAGFormInterface.js +172 -46
- package/lib/UAGProjectInterface.d.ts +37 -5
- package/lib/UAGProjectInterface.js +166 -29
- package/lib/actionLogs.d.ts +33 -0
- package/lib/actionLogs.js +147 -0
- package/lib/actions/runTask.d.ts +43 -0
- package/lib/actions/runTask.js +323 -0
- package/lib/agent/anonymous.d.ts +34 -0
- package/lib/agent/anonymous.js +98 -0
- package/lib/agent/chat.d.ts +113 -0
- package/lib/agent/chat.js +539 -0
- package/lib/agent/engines.d.ts +13 -0
- package/lib/agent/engines.js +37 -0
- package/lib/agent/index.d.ts +4 -0
- package/lib/agent/index.js +18 -0
- package/lib/agent/remote.d.ts +67 -0
- package/lib/agent/remote.js +229 -0
- package/lib/agent/requestId.d.ts +4 -0
- package/lib/agent/requestId.js +19 -0
- package/lib/agent/router.d.ts +31 -0
- package/lib/agent/router.js +295 -0
- package/lib/agent/stubs.d.ts +26 -0
- package/lib/agent/stubs.js +26 -0
- package/lib/agent/tools.d.ts +10 -0
- package/lib/agent/tools.js +33 -0
- package/lib/config.d.ts +67 -0
- package/lib/config.js +19 -0
- package/lib/database.d.ts +75 -0
- package/lib/database.js +229 -0
- package/lib/formflow.d.ts +40 -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 +41 -0
- package/lib/index.js +179 -7
- package/lib/router.js +15 -25
- package/lib/runner.d.ts +147 -0
- package/lib/runner.js +345 -0
- package/lib/runs.d.ts +89 -0
- package/lib/runs.js +74 -0
- package/lib/template.js +7 -9
- package/lib/templates/uagComponentNotFound.md +5 -5
- package/lib/tools/SchemaBuilder.d.ts +7 -2
- package/lib/tools/SchemaBuilder.js +11 -3
- package/lib/tools/agentDecide.d.ts +8 -0
- package/lib/tools/agentDecide.js +57 -0
- package/lib/tools/agentProvide.d.ts +13 -0
- package/lib/tools/agentProvide.js +130 -0
- package/lib/tools/annotations.d.ts +34 -0
- package/lib/tools/annotations.js +56 -0
- package/lib/tools/collectData.js +3 -1
- package/lib/tools/confirmSubmission.js +3 -1
- package/lib/tools/fetchExternalData.js +10 -17
- package/lib/tools/findSubmission.js +64 -56
- package/lib/tools/getFieldInfo.js +3 -1
- package/lib/tools/getFormFields.js +3 -1
- package/lib/tools/getForms.js +3 -1
- package/lib/tools/index.d.ts +4 -1
- package/lib/tools/index.js +22 -14
- package/lib/tools/submissionUpdate.js +3 -1
- package/lib/tools/submitForm.js +3 -1
- package/lib/tools/utils.d.ts +2 -0
- package/package.json +41 -18
- package/lib/tools/agentProvideData.d.ts +0 -3
- package/lib/tools/agentProvideData.js +0 -60
package/bin/uag-eval.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* uag-eval: measure decision accuracy against automation on labeled examples.
|
|
4
|
+
*
|
|
5
|
+
* uag-eval --form claim.json --examples claim-examples.json [--task triage]
|
|
6
|
+
* [--engine jev] [--json report.json]
|
|
7
|
+
*
|
|
8
|
+
* --form A Form.io form definition (JSON export).
|
|
9
|
+
* --task The task to evaluate (default: the form's first). `--persona` is the old name.
|
|
10
|
+
* --examples A JSON array of { "id", "data": { ... }, "expected": { "<path>": <value or null> } }.
|
|
11
|
+
* null means "the input does not say", which expects the field to stay empty.
|
|
12
|
+
* --engine The decision engine to evaluate: an instance named in UAG_ENGINES, or an
|
|
13
|
+
* entry in its syntax (`jev`, `next:jev`). Default: the first in UAG_ENGINES.
|
|
14
|
+
* --json Also write the full report as JSON.
|
|
15
|
+
* --project The project URL the evaluation runs for (default: none). A real engine
|
|
16
|
+
* needs a LICENSE_KEY that includes the UAG for this URL; the stub
|
|
17
|
+
* engines need neither.
|
|
18
|
+
*
|
|
19
|
+
* The engine is loaded as the UAG loads it, so the same UAG_ENGINES and
|
|
20
|
+
* UAG_ENGINE_* variables give the same instance, with the same model. Its type
|
|
21
|
+
* may be built in, or an installed engine package. Resource-backed selects need
|
|
22
|
+
* a live project and are skipped.
|
|
23
|
+
*/
|
|
24
|
+
require('dotenv/config');
|
|
25
|
+
const { readFileSync, writeFileSync } = require('fs');
|
|
26
|
+
const { parseInstances } = require('@formio/uag-sdk');
|
|
27
|
+
const { ProjectInterface, UAGProjectInterface, UAGFormInterface, loadUAGEngines } = require('../lib');
|
|
28
|
+
const { FormflowService, formatReport, resolveTaskName } = require('../lib/formflow');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The instance to evaluate: the one UAG_ENGINES names `name`, so a deployed
|
|
32
|
+
* instance is evaluated with its own settings, or else `name` as an entry.
|
|
33
|
+
*/
|
|
34
|
+
function engineEntry(name, env) {
|
|
35
|
+
const deployed = parseInstances(env.UAG_ENGINES);
|
|
36
|
+
if (!name) return deployed[0];
|
|
37
|
+
return deployed.find((instance) => instance.name === name) || parseInstances(name)[0];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function args(argv) {
|
|
41
|
+
const out = {};
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
if (argv[i].startsWith('--')) out[argv[i].slice(2)] = argv[i + 1];
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const opts = args(process.argv.slice(2));
|
|
50
|
+
if (!opts.form || !opts.examples) {
|
|
51
|
+
console.error('Usage: uag-eval --form <form.json> --examples <examples.json> [--task <name>] [--engine <name>] [--json <out.json>] [--project <url>]');
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
const task = resolveTaskName(opts, 'uag-eval');
|
|
55
|
+
const formDef = JSON.parse(readFileSync(opts.form, 'utf8'));
|
|
56
|
+
const examples = JSON.parse(readFileSync(opts.examples, 'utf8'));
|
|
57
|
+
const entry = engineEntry(opts.engine, process.env);
|
|
58
|
+
if (!entry) {
|
|
59
|
+
console.error('Name the decision engine with --engine or UAG_ENGINES.');
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const registry = await loadUAGEngines({ engines: [entry] }, process.env);
|
|
64
|
+
const engine = registry.decision[0];
|
|
65
|
+
if (!engine) {
|
|
66
|
+
console.error(`"${registry.all[0].name}" is not a decision engine.`);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The license the evaluation may use, read as a running UAG reads it.
|
|
71
|
+
await ProjectInterface.connect({ config: { license: String(process.env.UAG_LICENSE || ''), licenseType: 'uagServer', licenseFeature: 'uag' } });
|
|
72
|
+
// Build the form offline. Outside a running server the project would create
|
|
73
|
+
// the base form class, so the UAG form interface is constructed directly.
|
|
74
|
+
const project = new UAGProjectInterface(opts.project || 'http://offline.invalid');
|
|
75
|
+
const name = formDef.name || formDef.path || 'form';
|
|
76
|
+
formDef.tags = [...new Set([...(formDef.tags || []), 'uag'])];
|
|
77
|
+
// The constructor sets the form, which derives its UAG registration and tasks.
|
|
78
|
+
const form = new UAGFormInterface(project, formDef);
|
|
79
|
+
project.forms[name] = form;
|
|
80
|
+
|
|
81
|
+
const report = await new FormflowService(project).evaluate(form, task, examples, engine, {}, (done, total) => {
|
|
82
|
+
process.stderr.write(`\r${done}/${total} example(s)`);
|
|
83
|
+
});
|
|
84
|
+
process.stderr.write('\n');
|
|
85
|
+
console.log(formatReport(report));
|
|
86
|
+
if (opts.json) {
|
|
87
|
+
writeFileSync(opts.json, JSON.stringify(report, null, 2));
|
|
88
|
+
console.log(`Report written to ${opts.json}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main().catch((err) => {
|
|
93
|
+
console.error(err.message || err);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
package/bin/uag.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* uag: start the Form.io UAG.
|
|
4
|
+
*
|
|
5
|
+
* uag the UAG with no module
|
|
6
|
+
* UAG_MODULE=./my-module uag
|
|
7
|
+
*
|
|
8
|
+
* Configuration comes from the environment (PROJECT, PROJECT_KEY or ADMIN_KEY,
|
|
9
|
+
* JWT_SECRET, UAG_ENGINES, MONGO, ...) and an optional module: a file or
|
|
10
|
+
* folder exporting `{ config, actions, ... }`, named by UAG_MODULE.
|
|
11
|
+
*
|
|
12
|
+
* Engines are found among the installed packages, so a custom image holds only
|
|
13
|
+
* the engines it installs:
|
|
14
|
+
*
|
|
15
|
+
* npm install @formio/uag @formio/uag-claude
|
|
16
|
+
* UAG_ENGINES=claude CLAUDE_API_KEY=... npx uag
|
|
17
|
+
*/
|
|
18
|
+
// Before anything else, so a module that reads the environment sees .env too.
|
|
19
|
+
require('dotenv/config');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
/** isolated-vm, which runs form logic, needs Node's startup snapshot off. */
|
|
23
|
+
function snapshotDisabled() {
|
|
24
|
+
return process.execArgv.includes('--no-node-snapshot') || /(^|\s)--no-node-snapshot(\s|$)/.test(process.env.NODE_OPTIONS || '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Run this file again with the flag, passing signals on, and exit as it does. */
|
|
28
|
+
function restartWithoutSnapshot() {
|
|
29
|
+
const { spawn } = require('child_process');
|
|
30
|
+
const child = spawn(process.execPath, ['--no-node-snapshot', ...process.execArgv, __filename, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
31
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
32
|
+
process.on(signal, () => child.kill(signal));
|
|
33
|
+
}
|
|
34
|
+
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Start the UAG with a module, listening on PORT (default 3200). */
|
|
38
|
+
async function start(uagModule) {
|
|
39
|
+
const Express = require('express');
|
|
40
|
+
const { UAGServer, trustProxySetting } = require('../lib');
|
|
41
|
+
const server = new UAGServer();
|
|
42
|
+
if (uagModule) server.use(uagModule);
|
|
43
|
+
const app = Express();
|
|
44
|
+
// Behind a reverse proxy, its forwarded headers give the public URL (TRUST_PROXY).
|
|
45
|
+
const trust = trustProxySetting(process.env.TRUST_PROXY);
|
|
46
|
+
if (trust !== undefined) app.set('trust proxy', trust);
|
|
47
|
+
// Loads the engines the module's `engines` setting, or else UAG_ENGINES,
|
|
48
|
+
// names; a misconfigured engine stops startup with its name in the error.
|
|
49
|
+
app.use(await server.router());
|
|
50
|
+
const port = process.env.PORT || 3200;
|
|
51
|
+
await new Promise((resolve) => app.listen(port, resolve));
|
|
52
|
+
console.log(`Form.io UAG server running on port ${port}`);
|
|
53
|
+
console.log(`Visit http://localhost:${port} to access the application`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fail(error) {
|
|
57
|
+
console.error('Failed to start server:', error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (require.main === module) {
|
|
62
|
+
if (!snapshotDisabled()) {
|
|
63
|
+
restartWithoutSnapshot();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
Promise.resolve()
|
|
67
|
+
.then(() => (process.env.UAG_MODULE ? require(path.resolve(process.env.UAG_MODULE)) : undefined))
|
|
68
|
+
.then(start)
|
|
69
|
+
.catch(fail);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { start, fail };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AppServerForm, AuthRequest, FormInterface, InterpolatedError } from "@formio/appserver";
|
|
2
|
+
import type { FormflowForm, FormflowTask } from "./formflow";
|
|
2
3
|
import { Component, Submission, DataObject, ComponentPaths } from "@formio/core";
|
|
3
4
|
import { UAGForm } from "./config";
|
|
4
5
|
import { ParentInfo } from "./tools";
|
|
6
|
+
import { Router } from "express";
|
|
5
7
|
export type UAGComponentInfo = {
|
|
6
8
|
path: string;
|
|
7
9
|
label: string;
|
|
@@ -49,14 +51,48 @@ export type FormFieldInfo = {
|
|
|
49
51
|
components: UAGComponentInfo[];
|
|
50
52
|
};
|
|
51
53
|
};
|
|
54
|
+
/** A field a chat may collect, with its current value. */
|
|
55
|
+
export type ChatField = UAGComponentInfo & {
|
|
56
|
+
required: boolean;
|
|
57
|
+
empty: boolean;
|
|
58
|
+
value: any;
|
|
59
|
+
};
|
|
60
|
+
/** A component a chat cannot collect yet. */
|
|
61
|
+
export type ChatUnsupportedField = {
|
|
62
|
+
path: string;
|
|
63
|
+
label: string;
|
|
64
|
+
required: boolean;
|
|
65
|
+
empty: boolean;
|
|
66
|
+
};
|
|
67
|
+
export type ChatFields = {
|
|
68
|
+
fields: ChatField[];
|
|
69
|
+
unsupported: ChatUnsupportedField[];
|
|
70
|
+
errors: FormFieldError[];
|
|
71
|
+
};
|
|
52
72
|
export type UAGFields = {
|
|
53
|
-
|
|
73
|
+
task: string;
|
|
54
74
|
criteria: string;
|
|
55
75
|
components: Record<string, UAGComponentInfo>;
|
|
76
|
+
/** Data paths of the components flagged `uagField: "input"`: the source text for extraction and decisions. */
|
|
77
|
+
inputs?: string[];
|
|
78
|
+
/** Task-level settings, read from the custom properties of the task's criteria component (e.g. `uagEngine`, `uagTools`). */
|
|
79
|
+
settings?: Record<string, string>;
|
|
56
80
|
};
|
|
57
|
-
export declare class UAGFormInterface extends FormInterface {
|
|
81
|
+
export declare class UAGFormInterface extends FormInterface implements FormflowForm {
|
|
58
82
|
uag: UAGForm | null;
|
|
59
83
|
uagFields: Record<string, UAGFields>;
|
|
84
|
+
/**
|
|
85
|
+
* The form's routes, with its agent operations (`agent/extract`, `agent/decide`,
|
|
86
|
+
* `agent/generate`, `agent/chat`) beside its submissions: the project's authentication has
|
|
87
|
+
* already run, and the form is the one of the URL.
|
|
88
|
+
*/
|
|
89
|
+
router(): Promise<Router>;
|
|
90
|
+
/**
|
|
91
|
+
* Sets the form, deriving its UAG registration and agent fields from it. The form is set again
|
|
92
|
+
* whenever it loads, and whenever a TTL refresh or a deploy brings a new version, so both are
|
|
93
|
+
* rebuilt from the form each time rather than added to what an earlier version left.
|
|
94
|
+
*/
|
|
95
|
+
setForm(form: AppServerForm): AppServerForm;
|
|
60
96
|
setComponent(form: AppServerForm, component: Component, path: string, components: Component[] | undefined, parent: Component | undefined, paths: ComponentPaths | undefined): void;
|
|
61
97
|
getComponentFormat(component: Component): string;
|
|
62
98
|
getComponentInfo(component: Component, path: string): UAGComponentInfo;
|
|
@@ -111,6 +147,19 @@ export declare class UAGFormInterface extends FormInterface {
|
|
|
111
147
|
* @returns { Promise<FormFieldInfo> } - The extracted form field information.
|
|
112
148
|
*/
|
|
113
149
|
getFields(submission: Submission, authInfo: AuthRequest, within?: string | string[]): Promise<FormFieldInfo>;
|
|
150
|
+
/** Whether the chat cannot collect this component yet, so the person finishes it in the form. */
|
|
151
|
+
isChatUnsupported(component: Component): boolean;
|
|
152
|
+
/** Data paths of the fields a task fills: its decision and value fields, not its inputs. */
|
|
153
|
+
taskOwnedPaths(): Set<string>;
|
|
154
|
+
/**
|
|
155
|
+
* The fields a chat may collect on this data: visible input fields that a
|
|
156
|
+
* person may enter, filled or not, with the rules the MCP tools give
|
|
157
|
+
* agents. Fields a task fills (and `uagAuthoritative` ones), hidden,
|
|
158
|
+
* disabled and calculated fields are left out; components the chat
|
|
159
|
+
* cannot collect yet are listed in `unsupported`, and their children are
|
|
160
|
+
* not visited.
|
|
161
|
+
*/
|
|
162
|
+
chatFields(submission: Submission, authInfo: AuthRequest): Promise<ChatFields>;
|
|
114
163
|
/**
|
|
115
164
|
* Return the component at the specified data path.
|
|
116
165
|
* @param path { string } - The data path of the component to retrieve.
|
|
@@ -133,5 +182,14 @@ export declare class UAGFormInterface extends FormInterface {
|
|
|
133
182
|
convertToSubmission(data?: Record<string, any>): Submission;
|
|
134
183
|
formatData(data?: DataObject): UAGData;
|
|
135
184
|
formatSubmission(submission: Submission): UAGSubmission;
|
|
185
|
+
/** The form's name in Agentic Formflow responses and messages. */
|
|
186
|
+
get formflowName(): string;
|
|
187
|
+
/** The form's tasks, for Agentic Formflow. */
|
|
188
|
+
formflowTasks(): Record<string, FormflowTask>;
|
|
189
|
+
/** The submission's values in form order with their labels, for Agentic Formflow. */
|
|
190
|
+
formflowLabeledData(data?: DataObject): {
|
|
191
|
+
path: string;
|
|
192
|
+
label: string;
|
|
193
|
+
}[];
|
|
136
194
|
htmlToMarkdown(html: string): string;
|
|
137
195
|
}
|
package/lib/UAGFormInterface.js
CHANGED
|
@@ -5,27 +5,66 @@ const appserver_1 = require("@formio/appserver");
|
|
|
5
5
|
const core_1 = require("@formio/core");
|
|
6
6
|
const lodash_1 = require("lodash");
|
|
7
7
|
const node_html_markdown_1 = require("node-html-markdown");
|
|
8
|
+
const router_1 = require("./agent/router");
|
|
9
|
+
/** Component types a chat cannot collect yet: tables, nested forms, uploads, drawings and the like. */
|
|
10
|
+
const CHAT_UNSUPPORTED = new Set([
|
|
11
|
+
'datagrid', 'editgrid', 'datatable', 'dynamicWizard', 'tagpad', 'form', 'datamap', 'address',
|
|
12
|
+
'file', 'signature', 'sketchpad', 'datasource', 'password', 'captcha'
|
|
13
|
+
]);
|
|
8
14
|
class UAGFormInterface extends appserver_1.FormInterface {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The form's routes, with its agent operations (`agent/extract`, `agent/decide`,
|
|
17
|
+
* `agent/generate`, `agent/chat`) beside its submissions: the project's authentication has
|
|
18
|
+
* already run, and the form is the one of the URL.
|
|
19
|
+
*/
|
|
20
|
+
async router() {
|
|
21
|
+
const router = await super.router();
|
|
22
|
+
const project = this.project;
|
|
23
|
+
router.use((0, router_1.FormAgentRouter)(project, (0, router_1.agentCredentials)(project)));
|
|
24
|
+
return router;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sets the form, deriving its UAG registration and agent fields from it. The form is set again
|
|
28
|
+
* whenever it loads, and whenever a TTL refresh or a deploy brings a new version, so both are
|
|
29
|
+
* rebuilt from the form each time rather than added to what an earlier version left.
|
|
30
|
+
*/
|
|
31
|
+
setForm(form) {
|
|
12
32
|
this.uagFields = {};
|
|
33
|
+
const result = super.setForm(form);
|
|
34
|
+
const machineName = result.machineName;
|
|
35
|
+
this.uag = result.tags?.includes('uag') ? {
|
|
36
|
+
machineName,
|
|
37
|
+
name: result.name || result.path || machineName,
|
|
38
|
+
title: result.title || result.name || machineName,
|
|
39
|
+
description: result.properties?.description || `A form to submit new ${result.title} records.`
|
|
40
|
+
} : null;
|
|
41
|
+
return result;
|
|
13
42
|
}
|
|
14
43
|
setComponent(form, component, path, components, parent, paths) {
|
|
15
44
|
if (form.tags?.includes('uag') && component.properties?.uag) {
|
|
16
|
-
const
|
|
45
|
+
const task = component.properties?.uag;
|
|
17
46
|
if (!this.uagFields) {
|
|
18
47
|
this.uagFields = {};
|
|
19
48
|
}
|
|
20
|
-
let uagFields = this.uagFields[
|
|
49
|
+
let uagFields = this.uagFields[task];
|
|
21
50
|
if (!uagFields) {
|
|
22
|
-
uagFields = { components: {} };
|
|
51
|
+
uagFields = { task, components: {} };
|
|
23
52
|
}
|
|
24
53
|
if (component.properties?.uagField) {
|
|
25
54
|
const property = component.properties?.uagField;
|
|
26
|
-
if (
|
|
55
|
+
if (property === 'input') {
|
|
56
|
+
// An input can be a single field or a nested component; for a
|
|
57
|
+
// nested one, every child value under this path is input.
|
|
58
|
+
const inputPath = paths?.dataPath || component.key;
|
|
59
|
+
uagFields.inputs = [...(uagFields.inputs || []), inputPath];
|
|
60
|
+
}
|
|
61
|
+
else if (component?.html) {
|
|
27
62
|
uagFields[property] = this.htmlToMarkdown(component.html);
|
|
28
63
|
}
|
|
64
|
+
if (property === 'criteria') {
|
|
65
|
+
const { uag, uagField, ...settings } = component.properties || {};
|
|
66
|
+
uagFields.settings = { ...(uagFields.settings || {}), ...settings };
|
|
67
|
+
}
|
|
29
68
|
}
|
|
30
69
|
else {
|
|
31
70
|
if (component.components) {
|
|
@@ -42,7 +81,7 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
42
81
|
uagFields.components[compInfo.path] = compInfo;
|
|
43
82
|
}
|
|
44
83
|
}
|
|
45
|
-
this.uagFields[
|
|
84
|
+
this.uagFields[task] = uagFields;
|
|
46
85
|
}
|
|
47
86
|
return super.setComponent(form, component, path, components, parent, paths);
|
|
48
87
|
}
|
|
@@ -72,7 +111,8 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
72
111
|
if (component.placeholder) {
|
|
73
112
|
fieldInfo.prompt = component.placeholder;
|
|
74
113
|
}
|
|
75
|
-
|
|
114
|
+
// A radio's options are what its rule tells the agent to choose from.
|
|
115
|
+
if (component.type === 'select' || component.type === 'selectboxes' || component.type === 'radio') {
|
|
76
116
|
if (component.dataSrc === 'url') {
|
|
77
117
|
fieldInfo.options = [{ label: '** USE fetch_external_data TOOL **', value: `Call the \`fetch_external_data\` tool with \`field_path\`="${path}" to retrieve available options from the external URL.` }];
|
|
78
118
|
}
|
|
@@ -88,10 +128,7 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
88
128
|
fieldInfo.options = [{ label: '', value: 'No options available' }];
|
|
89
129
|
}
|
|
90
130
|
else {
|
|
91
|
-
fieldInfo.options = values.
|
|
92
|
-
acc.push({ label: v.label, value: v.value });
|
|
93
|
-
return acc;
|
|
94
|
-
}, []);
|
|
131
|
+
fieldInfo.options = values.map((v) => ({ label: v.label, value: v.value }));
|
|
95
132
|
}
|
|
96
133
|
}
|
|
97
134
|
}
|
|
@@ -114,19 +151,18 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
114
151
|
label: parent.label || parent.key,
|
|
115
152
|
data_path: parent_path
|
|
116
153
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
parent.type === 'editgrid') {
|
|
120
|
-
parentInfo.isTable = true;
|
|
121
|
-
return parentInfo;
|
|
122
|
-
}
|
|
154
|
+
// The type comes first: the builder saves containers with `tree: true`
|
|
155
|
+
// too, and a container holds one object, not rows.
|
|
123
156
|
if (parent.type === 'form') {
|
|
124
157
|
parentInfo.isForm = true;
|
|
125
|
-
return parentInfo;
|
|
126
158
|
}
|
|
127
|
-
if (parent.type === 'container') {
|
|
159
|
+
else if (parent.type === 'container') {
|
|
128
160
|
parentInfo.isContainer = true;
|
|
129
|
-
|
|
161
|
+
}
|
|
162
|
+
else if (parent.tree ||
|
|
163
|
+
parent.type === 'datagrid' ||
|
|
164
|
+
parent.type === 'editgrid') {
|
|
165
|
+
parentInfo.isTable = true;
|
|
130
166
|
}
|
|
131
167
|
return parentInfo;
|
|
132
168
|
}
|
|
@@ -163,20 +199,17 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
163
199
|
return `To determine what fields are within this component, use the \`get_form_fields\` tool with \`parent_path\`="<data_path>" (e.g. \`parent_path\`="${parent.data_path}"). To collect data for this component, use the \`collect_field_data\` tool with the \`parent_path\` set for the parent's data_path.`;
|
|
164
200
|
}
|
|
165
201
|
getComponentValueRule(component, data_path) {
|
|
166
|
-
let rule = '';
|
|
167
202
|
const parent = this.getParentInfoFromComponent(component, data_path);
|
|
168
203
|
if (parent?.isTable) {
|
|
169
|
-
|
|
170
|
-
return rule;
|
|
204
|
+
return 'The value is a table of rows (array of objects), where each row is a new instance of data values for the child components. ' + this.getParentToolDescription(parent) + ' All `data_path`(s) for the components within this table should contain the current row index (e.g. `dataGrid[0].a`, `dataGrid[0].b`, `dataGrid[1].b`, etc.).';
|
|
171
205
|
}
|
|
172
206
|
if (parent?.isForm) {
|
|
173
|
-
|
|
174
|
-
return rule;
|
|
207
|
+
return 'The value is a nested form submission in the format `{data: {...}}` where `{...}` is the values for the child components. ' + this.getParentToolDescription(parent) + ' All `data_path`(s) for the components within this nested form should be prefixed with `data` (e.g. `nestedForm.data.exampleField`).';
|
|
175
208
|
}
|
|
176
209
|
if (parent?.isContainer) {
|
|
177
|
-
|
|
178
|
-
return rule;
|
|
210
|
+
return 'The value is an object/map of nested component values in the format `{...}`. ' + this.getParentToolDescription(parent) + ' All `data_path`(s) for the components within this container should be prefixed with the container\'s `data_path` (e.g. `container.exampleField`).';
|
|
179
211
|
}
|
|
212
|
+
let rule = '';
|
|
180
213
|
switch (component.type) {
|
|
181
214
|
case 'tags':
|
|
182
215
|
rule += 'The value can be any alphanumeric string, containing letters, numbers, but no white space characters or symbols. Multiple tags should be comma-separated.';
|
|
@@ -245,8 +278,6 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
245
278
|
rule += `\n - **${value.label}** (${value.value})`;
|
|
246
279
|
});
|
|
247
280
|
break;
|
|
248
|
-
default:
|
|
249
|
-
rule += '';
|
|
250
281
|
}
|
|
251
282
|
return rule;
|
|
252
283
|
}
|
|
@@ -259,13 +290,10 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
259
290
|
* @returns { boolean } - True if the component is a nested data component, false otherwise.
|
|
260
291
|
*/
|
|
261
292
|
isNestedComponent(component) {
|
|
262
|
-
|
|
293
|
+
return !!component.tree ||
|
|
263
294
|
component.type === 'datagrid' ||
|
|
264
295
|
component.type === 'editgrid' ||
|
|
265
|
-
component.type === 'form'
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
return false;
|
|
296
|
+
component.type === 'form';
|
|
269
297
|
}
|
|
270
298
|
/**
|
|
271
299
|
* Determine if the component is a non-input component that should be skipped when collecting fields.
|
|
@@ -374,19 +402,19 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
374
402
|
return;
|
|
375
403
|
}
|
|
376
404
|
if (typeof within === 'string') {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
405
|
+
// Only paths below `within`: "customerNotes" is not within "customer".
|
|
406
|
+
const rest = path.slice(within.length);
|
|
407
|
+
if (path.startsWith(within) && (rest.startsWith('.') || rest.startsWith('['))) {
|
|
408
|
+
// The row of `within` itself, not of a table it sits in
|
|
409
|
+
// ("outer[0].inner[3].x" is row 3 of "outer[0].inner").
|
|
410
|
+
const indexMatch = rest.match(/^\[(\d+)\]/);
|
|
411
|
+
fieldInfo.rowIndex = indexMatch ? parseInt(indexMatch[1], 10) : 0;
|
|
381
412
|
}
|
|
382
413
|
else {
|
|
414
|
+
// If within is a string, then it is a single path to limit components within (non-inclusive).
|
|
383
415
|
return;
|
|
384
416
|
}
|
|
385
417
|
}
|
|
386
|
-
// If within is a string, then it is a single path to limit components within (non-inclusive).
|
|
387
|
-
if ((typeof within === 'string') && (!path.startsWith(within) || (path === within))) {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
418
|
}
|
|
391
419
|
// If this is a component within the nested path, then skip it...
|
|
392
420
|
else if (nestedPath && path.startsWith(nestedPath) && (path !== nestedPath)) {
|
|
@@ -412,7 +440,13 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
412
440
|
}
|
|
413
441
|
// Add the component info to the appropriate list.
|
|
414
442
|
const criteria = component.validate?.required ? 'required' : 'optional';
|
|
415
|
-
|
|
443
|
+
// Rules are listed by type, but some depend on the component (its
|
|
444
|
+
// options source, multiple values, a survey's questions, a nested
|
|
445
|
+
// component's path): one that differs gets an entry of its own.
|
|
446
|
+
const rules = fieldInfo[criteria].rules;
|
|
447
|
+
const rule = this.getComponentValueRule(component, path);
|
|
448
|
+
const known = rules[component.type];
|
|
449
|
+
rules[(known === undefined || known === rule) ? component.type : `${component.type} (${path})`] = rule;
|
|
416
450
|
fieldInfo[criteria].components.push(this.getComponentInfo(component, path));
|
|
417
451
|
}
|
|
418
452
|
}
|
|
@@ -422,6 +456,86 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
422
456
|
}
|
|
423
457
|
return fieldInfo;
|
|
424
458
|
}
|
|
459
|
+
/** Whether the chat cannot collect this component yet, so the person finishes it in the form. */
|
|
460
|
+
isChatUnsupported(component) {
|
|
461
|
+
if (CHAT_UNSUPPORTED.has(component.type))
|
|
462
|
+
return true;
|
|
463
|
+
const dataSrc = component.dataSrc;
|
|
464
|
+
if (component.type === 'select' && (dataSrc === 'url' || dataSrc === 'resource'))
|
|
465
|
+
return true;
|
|
466
|
+
if (component.tree && component.type !== 'container')
|
|
467
|
+
return true;
|
|
468
|
+
return ['nestedArray', 'nestedDataArray', 'dataObject', 'map'].includes(core_1.Utils.getModelType(component));
|
|
469
|
+
}
|
|
470
|
+
/** Data paths of the fields a task fills: its decision and value fields, not its inputs. */
|
|
471
|
+
taskOwnedPaths() {
|
|
472
|
+
const owned = new Set();
|
|
473
|
+
for (const task of Object.values(this.uagFields || {})) {
|
|
474
|
+
Object.keys(task.components || {}).forEach((path) => owned.add(path));
|
|
475
|
+
}
|
|
476
|
+
return owned;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The fields a chat may collect on this data: visible input fields that a
|
|
480
|
+
* person may enter, filled or not, with the rules the MCP tools give
|
|
481
|
+
* agents. Fields a task fills (and `uagAuthoritative` ones), hidden,
|
|
482
|
+
* disabled and calculated fields are left out; components the chat
|
|
483
|
+
* cannot collect yet are listed in `unsupported`, and their children are
|
|
484
|
+
* not visited.
|
|
485
|
+
*/
|
|
486
|
+
async chatFields(submission, authInfo) {
|
|
487
|
+
const result = { fields: [], unsupported: [], errors: [] };
|
|
488
|
+
const owned = this.taskOwnedPaths();
|
|
489
|
+
const skipped = [];
|
|
490
|
+
const within = (path, parent) => path.startsWith(`${parent}.`) || path.startsWith(`${parent}[`);
|
|
491
|
+
const context = await this.process(submission, authInfo, null, null, [
|
|
492
|
+
{
|
|
493
|
+
name: 'chatFields',
|
|
494
|
+
shouldProcess: () => true,
|
|
495
|
+
process: async (context) => {
|
|
496
|
+
const { component, path } = context;
|
|
497
|
+
if (this.isChatUnsupported(component) && !skipped.some((parent) => within(path, parent))) {
|
|
498
|
+
skipped.push(path);
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
postProcess: async (context) => {
|
|
502
|
+
const { component, path, value } = context;
|
|
503
|
+
if (skipped.some((parent) => within(path, parent)))
|
|
504
|
+
return;
|
|
505
|
+
if (skipped[skipped.length - 1] === path)
|
|
506
|
+
skipped.pop();
|
|
507
|
+
if (!this.inputComponent(component) || component.type === 'container')
|
|
508
|
+
return;
|
|
509
|
+
// A field inside a hidden panel or container is only marked `intentionallyHidden`.
|
|
510
|
+
if (component.scope?.conditionallyHidden || component.scope?.intentionallyHidden || component.hidden || component.type === 'hidden')
|
|
511
|
+
return;
|
|
512
|
+
const required = !!component.validate?.required;
|
|
513
|
+
const empty = !(0, core_1.componentHasValue)(component, value);
|
|
514
|
+
if (this.isChatUnsupported(component)) {
|
|
515
|
+
result.unsupported.push({ path, label: component.label || component.key || path, required, empty });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (component.disabled || component.persistent === false || component.persistent === 'client-only')
|
|
519
|
+
return;
|
|
520
|
+
if (component.calculateValue && !component.allowCalculateOverride)
|
|
521
|
+
return;
|
|
522
|
+
if (owned.has(path) || component.properties?.uagAuthoritative === 'true')
|
|
523
|
+
return;
|
|
524
|
+
result.fields.push({
|
|
525
|
+
...this.getComponentInfo(component, path),
|
|
526
|
+
rule: this.getComponentValueRule(component, path),
|
|
527
|
+
required,
|
|
528
|
+
empty,
|
|
529
|
+
value
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
]);
|
|
534
|
+
if (context.scope?.errors?.length) {
|
|
535
|
+
result.errors = this.convertToFormFieldErrors((0, core_1.interpolateErrors)(context.scope.errors));
|
|
536
|
+
}
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
425
539
|
/**
|
|
426
540
|
* Return the component at the specified data path.
|
|
427
541
|
* @param path { string } - The data path of the component to retrieve.
|
|
@@ -512,7 +626,7 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
512
626
|
}
|
|
513
627
|
}, false, false, undefined, undefined, undefined, (component, data) => {
|
|
514
628
|
if (this.isNestedComponent(component)) {
|
|
515
|
-
prefix = prefix.slice(0, -
|
|
629
|
+
prefix = prefix.slice(0, -2);
|
|
516
630
|
}
|
|
517
631
|
if (component.key === 'uag' || withinUAG) {
|
|
518
632
|
withinUAG = false;
|
|
@@ -528,6 +642,18 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
528
642
|
modified: submission.modified
|
|
529
643
|
};
|
|
530
644
|
}
|
|
645
|
+
/** The form's name in Agentic Formflow responses and messages. */
|
|
646
|
+
get formflowName() {
|
|
647
|
+
return this.uag?.name || this.form?.name || '';
|
|
648
|
+
}
|
|
649
|
+
/** The form's tasks, for Agentic Formflow. */
|
|
650
|
+
formflowTasks() {
|
|
651
|
+
return this.uagFields || {};
|
|
652
|
+
}
|
|
653
|
+
/** The submission's values in form order with their labels, for Agentic Formflow. */
|
|
654
|
+
formflowLabeledData(data = {}) {
|
|
655
|
+
return this.formatData(data).map(({ path, label }) => ({ path, label }));
|
|
656
|
+
}
|
|
531
657
|
htmlToMarkdown(html) {
|
|
532
658
|
return node_html_markdown_1.NodeHtmlMarkdown.translate(html, {
|
|
533
659
|
bulletMarker: '-',
|
|
@@ -1,22 +1,54 @@
|
|
|
1
1
|
import { ProjectInterface, SubmissionRequest } from "@formio/appserver";
|
|
2
|
+
import { EngineRegistry, FormflowConfig, FormflowForm, FormflowHost, FormflowTool, ScopedTokenClaims } from "./formflow";
|
|
2
3
|
import { UAGConfig } from "./config";
|
|
3
4
|
import { Form } from "@formio/core";
|
|
4
5
|
import { NextFunction, Response, Router } from "express";
|
|
5
6
|
import { UAGFormInterface } from "./UAGFormInterface";
|
|
6
7
|
import { ResponseTemplate, UAGTemplate } from "./template";
|
|
7
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
-
export declare class UAGProjectInterface extends ProjectInterface {
|
|
9
|
+
export declare class UAGProjectInterface extends ProjectInterface implements FormflowHost {
|
|
9
10
|
user: any;
|
|
10
|
-
|
|
11
|
+
/** The names of the UAG forms the project currently has. */
|
|
12
|
+
get formNames(): string[];
|
|
11
13
|
uagTemplate: UAGTemplate | null;
|
|
14
|
+
/** Engines enabled at startup; empty until UAGServer.router() resolves them. */
|
|
15
|
+
engines: EngineRegistry;
|
|
12
16
|
mcpServer: McpServer;
|
|
13
17
|
get config(): UAGConfig;
|
|
14
18
|
constructor(endpoint?: string);
|
|
15
19
|
initialize(): Promise<void>;
|
|
16
|
-
buildMcpServer(): Promise<McpServer>;
|
|
20
|
+
buildMcpServer(scope?: Pick<ScopedTokenClaims, 'tools'>): Promise<McpServer>;
|
|
17
21
|
addForm(form: Form, machineName: string): UAGFormInterface | undefined;
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The UAG's routes: `/mcp`, and unless excluded, `/agent/capabilities`.
|
|
24
|
+
* UAGServer mounts capabilities separately, ahead of the project router. A
|
|
25
|
+
* form's agent operations are on the form's own routes (UAGFormInterface).
|
|
26
|
+
*/
|
|
27
|
+
uagRouter(includeAgentApi?: boolean): Router;
|
|
28
|
+
/** `/agent/capabilities`, behind its own authentication. */
|
|
29
|
+
agentRouter(): Router;
|
|
30
|
+
/** Whether runs after the save are recorded and survive a restart (the UAG's database). */
|
|
31
|
+
durableRuns(): boolean;
|
|
32
|
+
/** The engines enabled for Agentic Formflow. */
|
|
33
|
+
formflowEngines(): EngineRegistry;
|
|
34
|
+
/**
|
|
35
|
+
* Agentic Formflow's routing, Agent API and auth settings. The engine
|
|
36
|
+
* settings stay here: the UAG loads the engines, not core.
|
|
37
|
+
*/
|
|
38
|
+
formflowConfig(): FormflowConfig;
|
|
39
|
+
/** A form tagged "uag", or null. */
|
|
40
|
+
formflowForm(name: string): Promise<FormflowForm | null>;
|
|
41
|
+
/** Every MCP tool, as Agentic Formflow tools. */
|
|
42
|
+
formflowTools(): Promise<FormflowTool[]>;
|
|
43
|
+
authorizeRequest(req: SubmissionRequest, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
|
|
44
|
+
/**
|
|
45
|
+
* The user a Form.io token stands for, as the project's x-jwt-token
|
|
46
|
+
* authentication decides it: a token tied to a Form.io session is asked of
|
|
47
|
+
* Form.io; one signed with this UAG's secret is read from its claims, bound
|
|
48
|
+
* to this project; any other is asked of Form.io. Never an administrator:
|
|
49
|
+
* an administrator's research carries no user token.
|
|
50
|
+
*/
|
|
51
|
+
private formioTokenAuth;
|
|
20
52
|
mcpJSONResponse(data?: object, isError?: boolean): {
|
|
21
53
|
content: {
|
|
22
54
|
type: 'text';
|