@formio/uag 1.13.2 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +235 -535
  2. package/bin/uag-eval.js +95 -0
  3. package/bin/uag.js +73 -0
  4. package/lib/UAGFormInterface.d.ts +60 -2
  5. package/lib/UAGFormInterface.js +172 -46
  6. package/lib/UAGProjectInterface.d.ts +37 -5
  7. package/lib/UAGProjectInterface.js +166 -33
  8. package/lib/actionLogs.d.ts +33 -0
  9. package/lib/actionLogs.js +147 -0
  10. package/lib/actions/runTask.d.ts +43 -0
  11. package/lib/actions/runTask.js +323 -0
  12. package/lib/agent/anonymous.d.ts +34 -0
  13. package/lib/agent/anonymous.js +98 -0
  14. package/lib/agent/chat.d.ts +113 -0
  15. package/lib/agent/chat.js +539 -0
  16. package/lib/agent/engines.d.ts +13 -0
  17. package/lib/agent/engines.js +37 -0
  18. package/lib/agent/index.d.ts +4 -0
  19. package/lib/agent/index.js +18 -0
  20. package/lib/agent/remote.d.ts +67 -0
  21. package/lib/agent/remote.js +229 -0
  22. package/lib/agent/requestId.d.ts +4 -0
  23. package/lib/agent/requestId.js +19 -0
  24. package/lib/agent/router.d.ts +31 -0
  25. package/lib/agent/router.js +295 -0
  26. package/lib/agent/stubs.d.ts +26 -0
  27. package/lib/agent/stubs.js +26 -0
  28. package/lib/agent/tools.d.ts +10 -0
  29. package/lib/agent/tools.js +33 -0
  30. package/lib/config.d.ts +67 -0
  31. package/lib/config.js +19 -0
  32. package/lib/database.d.ts +75 -0
  33. package/lib/database.js +229 -0
  34. package/lib/formflow.d.ts +40 -0
  35. package/lib/formflow.js +28 -0
  36. package/lib/identity.d.ts +53 -0
  37. package/lib/identity.js +107 -0
  38. package/lib/index.d.ts +41 -0
  39. package/lib/index.js +179 -7
  40. package/lib/router.js +15 -25
  41. package/lib/runner.d.ts +147 -0
  42. package/lib/runner.js +345 -0
  43. package/lib/runs.d.ts +89 -0
  44. package/lib/runs.js +74 -0
  45. package/lib/template.js +7 -9
  46. package/lib/templates/uagComponentNotFound.md +5 -5
  47. package/lib/tools/SchemaBuilder.d.ts +7 -2
  48. package/lib/tools/SchemaBuilder.js +11 -3
  49. package/lib/tools/agentDecide.d.ts +8 -0
  50. package/lib/tools/agentDecide.js +57 -0
  51. package/lib/tools/agentProvide.d.ts +13 -0
  52. package/lib/tools/agentProvide.js +130 -0
  53. package/lib/tools/collectData.js +1 -1
  54. package/lib/tools/confirmSubmission.js +1 -1
  55. package/lib/tools/fetchExternalData.js +8 -17
  56. package/lib/tools/findSubmission.js +62 -56
  57. package/lib/tools/getFieldInfo.js +1 -1
  58. package/lib/tools/getFormFields.js +1 -1
  59. package/lib/tools/getForms.js +1 -1
  60. package/lib/tools/index.d.ts +3 -1
  61. package/lib/tools/index.js +21 -14
  62. package/lib/tools/submissionUpdate.js +1 -1
  63. package/lib/tools/submitForm.js +1 -1
  64. package/package.json +42 -18
  65. package/lib/tools/agentProvideData.d.ts +0 -3
  66. package/lib/tools/agentProvideData.js +0 -62
@@ -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
- persona: string;
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
  }
@@ -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
- constructor() {
10
- super(...arguments);
11
- this.uag = null;
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 persona = component.properties?.uag;
45
+ const task = component.properties?.uag;
17
46
  if (!this.uagFields) {
18
47
  this.uagFields = {};
19
48
  }
20
- let uagFields = this.uagFields[persona];
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 (component?.html) {
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[persona] = 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
- if (component.type === 'select' || component.type === 'selectboxes') {
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.reduce((acc, v) => {
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
- if (parent.tree ||
118
- parent.type === 'datagrid' ||
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
- return parentInfo;
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
- rule += '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.).';
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
- rule += '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`).';
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
- rule += '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`).';
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
- if (component.tree ||
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
- if (path.startsWith(within) && (path !== within)) {
378
- // Make sure to set the largest index for the nested component.
379
- const indexMatch = path.match(/\[(\d+)\]/);
380
- fieldInfo.rowIndex = indexMatch?.length ? parseInt(indexMatch[indexMatch.length - 1], 10) : 0;
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
- fieldInfo[criteria].rules[component.type] = this.getComponentValueRule(component, path);
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, -3);
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
- formNames: string[];
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
- uagRouter(): Router;
19
- authorizeRequest(req: SubmissionRequest, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
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';