@formio/uag-sdk 0.0.0-stage → 2.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +19 -0
- package/README.md +162 -2
- package/conformance.d.ts +31 -0
- package/conformance.js +291 -0
- package/contract.d.ts +223 -0
- package/define.js +142 -0
- package/errors.js +43 -0
- package/helpers.js +145 -0
- package/index.d.ts +111 -0
- package/index.js +30 -0
- package/instances.js +117 -0
- package/loader.d.ts +67 -0
- package/loader.js +272 -0
- package/package.json +57 -4
- package/protocol.d.ts +61 -0
- package/protocol.js +269 -0
- package/settings.js +99 -0
package/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* @formio/uag-sdk: what engine packages for the Form.io UAG are built
|
|
4
|
+
* with. The engine contract's types are in contract.d.ts; the loader is
|
|
5
|
+
* `@formio/uag-sdk/loader` and the conformance check
|
|
6
|
+
* `@formio/uag-sdk/conformance`.
|
|
7
|
+
*/
|
|
8
|
+
const { ENGINE_API_VERSION, ENGINE_NAME, defineEngine, isEngineType } = require('./define');
|
|
9
|
+
const { EngineError, EngineConfigError, redact } = require('./errors');
|
|
10
|
+
const { parseInstances, engineInstances } = require('./instances');
|
|
11
|
+
const { settingVariable, resolveSettings } = require('./settings');
|
|
12
|
+
const { requiredSchema, decodeValues, parseJsonObject, toolResultText } = require('./helpers');
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
ENGINE_API_VERSION,
|
|
16
|
+
ENGINE_NAME,
|
|
17
|
+
defineEngine,
|
|
18
|
+
isEngineType,
|
|
19
|
+
EngineError,
|
|
20
|
+
EngineConfigError,
|
|
21
|
+
redact,
|
|
22
|
+
parseInstances,
|
|
23
|
+
engineInstances,
|
|
24
|
+
settingVariable,
|
|
25
|
+
resolveSettings,
|
|
26
|
+
requiredSchema,
|
|
27
|
+
decodeValues,
|
|
28
|
+
parseJsonObject,
|
|
29
|
+
toolResultText,
|
|
30
|
+
};
|
package/instances.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { ENGINE_NAME } = require('./define');
|
|
3
|
+
const { EngineConfigError } = require('./errors');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* npm package names, scoped or not. Every segment starts with a letter or digit,
|
|
7
|
+
* so "." and ".." (and so any relative path) can never match.
|
|
8
|
+
*/
|
|
9
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
10
|
+
const MAX_PACKAGE_NAME = 214;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether a type is given as a package name rather than a type name: scoped
|
|
14
|
+
* (`@acme/uag-engine-x`), or unscoped with the conventional prefix (`uag-engine-x`).
|
|
15
|
+
*/
|
|
16
|
+
function isPackageName(type) {
|
|
17
|
+
return type.startsWith('@') || type.includes('/') || type.startsWith('uag-engine-');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Refuse a type that is neither a type name nor a package name, before any lookup. */
|
|
21
|
+
function checkType(type) {
|
|
22
|
+
const valid = isPackageName(type)
|
|
23
|
+
? PACKAGE_NAME.test(type) && type.length <= MAX_PACKAGE_NAME
|
|
24
|
+
: ENGINE_NAME.test(type);
|
|
25
|
+
if (!valid) {
|
|
26
|
+
throw new EngineConfigError(
|
|
27
|
+
`The engine type name ${JSON.stringify(type)} is not valid. Use a type name such as "claude", ` +
|
|
28
|
+
'or a package name such as "@acme/uag-engine-x" or "uag-engine-x".'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function checkName(name) {
|
|
34
|
+
if (!ENGINE_NAME.test(name)) {
|
|
35
|
+
throw new EngineConfigError(
|
|
36
|
+
`The engine name ${JSON.stringify(name)} is not valid: use lowercase letters, digits and "-", ` +
|
|
37
|
+
'starting with a letter, up to 63 characters.'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Refuse two instances with the same name. Instances without a name yet are skipped. */
|
|
43
|
+
function checkUnique(instances) {
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
for (const { name } of instances) {
|
|
46
|
+
if (!name) continue;
|
|
47
|
+
if (seen.has(name)) {
|
|
48
|
+
throw new EngineConfigError(`Two engines are named "${name}". Engine instance names must be unique.`);
|
|
49
|
+
}
|
|
50
|
+
seen.add(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One `UAG_ENGINES` entry: `<type>` or `<name>:<type>`. */
|
|
55
|
+
function parseEntry(entry) {
|
|
56
|
+
const colon = entry.indexOf(':');
|
|
57
|
+
const name = colon === -1 ? undefined : entry.slice(0, colon).trim();
|
|
58
|
+
const type = colon === -1 ? entry : entry.slice(colon + 1).trim();
|
|
59
|
+
if (name !== undefined) checkName(name);
|
|
60
|
+
checkType(type);
|
|
61
|
+
// A bare package name takes its type's name, which is known once it loads.
|
|
62
|
+
return { name: name ?? (isPackageName(type) ? undefined : type), type, options: {} };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One configuration entry: a `UAG_ENGINES` string, or `{ name, type, options }`. */
|
|
66
|
+
function parseConfigured(entry) {
|
|
67
|
+
if (typeof entry === 'string') {
|
|
68
|
+
return parseEntry(entry.trim());
|
|
69
|
+
}
|
|
70
|
+
if (!entry || typeof entry !== 'object') {
|
|
71
|
+
throw new EngineConfigError(`An engine entry must be a string or { name, type, options }, not ${JSON.stringify(entry)}.`);
|
|
72
|
+
}
|
|
73
|
+
const { name, type, options = {} } = entry;
|
|
74
|
+
if (name !== undefined) checkName(name);
|
|
75
|
+
if (typeof type === 'string') {
|
|
76
|
+
checkType(type);
|
|
77
|
+
}
|
|
78
|
+
else if (!type || typeof type !== 'object') {
|
|
79
|
+
throw new EngineConfigError(`The engine${name ? ` "${name}"` : ''} needs a type: a type name, a package name, or an engine type definition.`);
|
|
80
|
+
}
|
|
81
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
82
|
+
throw new EngineConfigError(`The options of the engine${name ? ` "${name}"` : ''} must be an object.`);
|
|
83
|
+
}
|
|
84
|
+
const named = name ?? (typeof type === 'string' ? (isPackageName(type) ? undefined : type) : undefined);
|
|
85
|
+
return { name: named, type, options };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The engine instances in a `UAG_ENGINES` string or a configuration list. Each
|
|
90
|
+
* is `{ name, type, options }`; `name` is undefined until the type is known when
|
|
91
|
+
* the entry names only a package or a definition. Nothing named means none.
|
|
92
|
+
*/
|
|
93
|
+
function parseInstances(source) {
|
|
94
|
+
if (source === undefined || source === null) return [];
|
|
95
|
+
let entries;
|
|
96
|
+
if (typeof source === 'string') {
|
|
97
|
+
entries = source.split(',').map((entry) => entry.trim()).filter(Boolean).map(parseEntry);
|
|
98
|
+
}
|
|
99
|
+
else if (Array.isArray(source)) {
|
|
100
|
+
entries = source.filter((entry) => entry !== '').map(parseConfigured);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw new EngineConfigError('The engine list must be a comma-separated string or an array.');
|
|
104
|
+
}
|
|
105
|
+
checkUnique(entries);
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The instances to enable: from configuration (`engines`) when it lists them,
|
|
111
|
+
* otherwise from `UAG_ENGINES`. Configuration takes precedence.
|
|
112
|
+
*/
|
|
113
|
+
function engineInstances(config = {}, env = process.env) {
|
|
114
|
+
return parseInstances(Array.isArray(config && config.engines) ? config.engines : env.UAG_ENGINES);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { PACKAGE_NAME, isPackageName, checkName, checkType, checkUnique, parseInstances, engineInstances };
|
package/loader.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Engine } from './contract';
|
|
2
|
+
import type { EngineInstanceSpec, EngineTypeDefinition } from './index';
|
|
3
|
+
|
|
4
|
+
export type LoadOptions = {
|
|
5
|
+
/** Types resolved before any package: the built-in ones, and ones the embedding application passes. */
|
|
6
|
+
types?: Record<string, EngineTypeDefinition>;
|
|
7
|
+
/** The directories whose `node_modules` packages are looked up in, from `enginePaths`; nowhere else. */
|
|
8
|
+
paths?: string[];
|
|
9
|
+
env?: Record<string, string | undefined>;
|
|
10
|
+
config?: Record<string, any>;
|
|
11
|
+
/** The engine contract version the host supports. Default: this SDK's. */
|
|
12
|
+
apiVersion?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** An enabled engine and where it came from, ready for `Formflow.engineRegistry`. */
|
|
16
|
+
export type LoadedEngine = {
|
|
17
|
+
engine: Engine;
|
|
18
|
+
/** The engine type's name. */
|
|
19
|
+
type: string;
|
|
20
|
+
/** The engine type's title. */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** The kinds the engine type declares. */
|
|
23
|
+
kinds?: ('decision' | 'agent')[];
|
|
24
|
+
/** The engine type's own version, from its definition or its package. */
|
|
25
|
+
typeVersion?: string;
|
|
26
|
+
package?: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The directories whose `node_modules` packages are looked up in: the host's own
|
|
32
|
+
* installation and each directory above it, then each absolute directory in
|
|
33
|
+
* `UAG_ENGINE_PATH` (without the directories above it).
|
|
34
|
+
*/
|
|
35
|
+
export declare function enginePaths(root: string | undefined, env?: Record<string, string | undefined>): string[];
|
|
36
|
+
|
|
37
|
+
/** The packages a type name may come from, in order. */
|
|
38
|
+
export declare function packageCandidates(type: string): string[];
|
|
39
|
+
|
|
40
|
+
export declare function resolveType(type: string | EngineTypeDefinition, options?: LoadOptions): {
|
|
41
|
+
definition: EngineTypeDefinition;
|
|
42
|
+
package?: string;
|
|
43
|
+
version?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Create one engine for an instance, resolving its settings. */
|
|
47
|
+
export declare function createEngine(definition: EngineTypeDefinition, options: {
|
|
48
|
+
name: string;
|
|
49
|
+
options?: Record<string, any>;
|
|
50
|
+
env?: Record<string, string | undefined>;
|
|
51
|
+
config?: Record<string, any>;
|
|
52
|
+
}): Promise<Engine>;
|
|
53
|
+
|
|
54
|
+
/** Resolve, configure and create each instance. Any problem stops loading, with the instance named. */
|
|
55
|
+
export declare function loadEngines(instances: EngineInstanceSpec[], options?: LoadOptions): Promise<LoadedEngine[]>;
|
|
56
|
+
|
|
57
|
+
/** One instance planned but not created: what creating it takes, and how a failure to start reads. */
|
|
58
|
+
export type PlannedEngine = Omit<LoadedEngine, 'engine'> & {
|
|
59
|
+
name: string;
|
|
60
|
+
kinds: ('decision' | 'agent')[];
|
|
61
|
+
definition: EngineTypeDefinition;
|
|
62
|
+
args: { name: string; settings: Record<string, any>; options: Record<string, any>; config: Record<string, any>; requireSetting(key: string): any };
|
|
63
|
+
failure(err: unknown): string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Resolve and configure each instance without creating it, for a host that creates engines elsewhere. */
|
|
67
|
+
export declare function planEngines(instances: EngineInstanceSpec[], options?: LoadOptions): PlannedEngine[];
|
package/loader.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { ENGINE_API_VERSION, definitionProblems, withDefaults } = require('./define');
|
|
5
|
+
const { EngineConfigError, redact } = require('./errors');
|
|
6
|
+
const { checkName, checkType, checkUnique, isPackageName } = require('./instances');
|
|
7
|
+
const { missingSetting, resolveSettings, secretValues, settingVariable } = require('./settings');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Finding, configuring and creating engine instances. The UAG, `uag-eval` and
|
|
11
|
+
* the engine server all load engines here, so one configuration means the same
|
|
12
|
+
* engines in each.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The directories whose `node_modules` engine packages are looked up in: the
|
|
17
|
+
* host's own installation and each directory above it (where Node finds the
|
|
18
|
+
* host's own dependencies, so engines installed beside the host are found),
|
|
19
|
+
* then each absolute directory in `UAG_ENGINE_PATH`, without the directories
|
|
20
|
+
* above it. Never the working directory, `NODE_PATH` or Node's global folders.
|
|
21
|
+
*/
|
|
22
|
+
function enginePaths(root, env = process.env) {
|
|
23
|
+
const extra = String(env.UAG_ENGINE_PATH || '').split(path.delimiter).map((dir) => dir.trim()).filter(Boolean);
|
|
24
|
+
for (const dir of extra) {
|
|
25
|
+
if (!path.isAbsolute(dir)) {
|
|
26
|
+
throw new EngineConfigError(`UAG_ENGINE_PATH must list absolute directories; "${dir}" is relative.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const host = [];
|
|
30
|
+
for (let dir = root && path.resolve(root); dir; ) {
|
|
31
|
+
// As Node does, never look for node_modules/node_modules.
|
|
32
|
+
if (path.basename(dir) !== 'node_modules') host.push(dir);
|
|
33
|
+
const parent = path.dirname(dir);
|
|
34
|
+
dir = parent === dir ? undefined : parent;
|
|
35
|
+
}
|
|
36
|
+
return [...host, ...extra];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The packages a type name may come from, in order. */
|
|
40
|
+
function packageCandidates(type) {
|
|
41
|
+
return isPackageName(type) ? [type] : [`@formio/uag-${type}`, `uag-engine-${type}`];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether `file` is `dir` or inside it. */
|
|
45
|
+
function isInside(file, dir) {
|
|
46
|
+
const relative = path.relative(dir, file);
|
|
47
|
+
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find `name` in `<dir>/node_modules` of the first of `paths` that has it, and
|
|
52
|
+
* nowhere else: not in the directories above, `NODE_PATH` or Node's global
|
|
53
|
+
* folders, which `require.resolve` would also search. Returns its entry file
|
|
54
|
+
* and package.json, or undefined when no path has it.
|
|
55
|
+
*/
|
|
56
|
+
function findPackage(name, paths) {
|
|
57
|
+
for (const dir of paths) {
|
|
58
|
+
const packageDir = path.join(dir, 'node_modules', ...name.split('/'));
|
|
59
|
+
const manifest = path.join(packageDir, 'package.json');
|
|
60
|
+
if (!fs.existsSync(manifest)) continue;
|
|
61
|
+
let json;
|
|
62
|
+
try {
|
|
63
|
+
json = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
throw new EngineConfigError(`The package "${name}" has an unreadable package.json (${manifest}): ${err && err.message}`);
|
|
67
|
+
}
|
|
68
|
+
let entry;
|
|
69
|
+
try {
|
|
70
|
+
entry = require.resolve(name, { paths: [dir] });
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw new EngineConfigError(`The package "${name}" (${packageDir}) has no entry point that require() can load: ${err && (err.code || err.message)}.`);
|
|
74
|
+
}
|
|
75
|
+
// A broken package must not fall through to a copy found elsewhere.
|
|
76
|
+
// Real paths, so a linked workspace package is still its own.
|
|
77
|
+
if (!isInside(fs.realpathSync(entry), fs.realpathSync(packageDir))) {
|
|
78
|
+
throw new EngineConfigError(`The package "${name}" in ${packageDir} resolved to ${entry}, outside it; it was not loaded.`);
|
|
79
|
+
}
|
|
80
|
+
return { entry, json };
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The message of anything thrown, including a string. */
|
|
86
|
+
function errorText(err) {
|
|
87
|
+
return err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check a definition, from this SDK, another copy of it, or none, and return it
|
|
92
|
+
* with its defaults (`title`, and `version` from the package it came from).
|
|
93
|
+
*/
|
|
94
|
+
function checkDefinition(definition, source, apiVersion, packageVersion) {
|
|
95
|
+
const problems = definitionProblems(definition);
|
|
96
|
+
if (problems.length) {
|
|
97
|
+
throw new EngineConfigError(`${source} is not a UAG engine: it does not export a valid engine type definition (${problems.join('; ')}).`);
|
|
98
|
+
}
|
|
99
|
+
if (definition.apiVersion !== apiVersion) {
|
|
100
|
+
throw new EngineConfigError(
|
|
101
|
+
`${source} (engine type "${definition.name}") was built for engine API version ${definition.apiVersion}; ` +
|
|
102
|
+
`this server supports version ${apiVersion}.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return withDefaults(definition, packageVersion);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The definition for an instance's type: a definition given directly, a type in
|
|
110
|
+
* `types` (built-in, or passed by the embedding application), then the packages
|
|
111
|
+
* `@formio/uag-<type>` and `uag-engine-<type>`, or the package named.
|
|
112
|
+
* Returns the package's name and version when it came from one.
|
|
113
|
+
*/
|
|
114
|
+
function resolveType(type, { types = {}, paths = [], apiVersion = ENGINE_API_VERSION } = {}) {
|
|
115
|
+
if (type && typeof type === 'object') {
|
|
116
|
+
return { definition: checkDefinition(type, 'The engine type given in configuration', apiVersion) };
|
|
117
|
+
}
|
|
118
|
+
checkType(type);
|
|
119
|
+
if (!isPackageName(type) && Object.prototype.hasOwnProperty.call(types, type)) {
|
|
120
|
+
return { definition: checkDefinition(types[type], `The built-in engine type "${type}"`, apiVersion) };
|
|
121
|
+
}
|
|
122
|
+
const candidates = packageCandidates(type);
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
const found = findPackage(candidate, paths);
|
|
125
|
+
if (!found) continue;
|
|
126
|
+
const { entry, json: pkg } = found;
|
|
127
|
+
let exported;
|
|
128
|
+
try {
|
|
129
|
+
exported = require(entry);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new EngineConfigError(`The package "${candidate}" failed to load: ${errorText(err)}`);
|
|
133
|
+
}
|
|
134
|
+
const definition = exported && exported.__esModule && exported.default ? exported.default : exported;
|
|
135
|
+
return {
|
|
136
|
+
definition: checkDefinition(definition, `The package "${candidate}"`, apiVersion, pkg.version),
|
|
137
|
+
package: candidate,
|
|
138
|
+
...(pkg.version && { version: pkg.version })
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const known = Object.keys(types);
|
|
142
|
+
throw new EngineConfigError(
|
|
143
|
+
`No engine type "${type}" is installed. Looked for ${candidates.map((name) => `"${name}"`).join(' and ')}. ` +
|
|
144
|
+
`Install one (npm install ${candidates[0]}), or add a directory that contains it to UAG_ENGINE_PATH.` +
|
|
145
|
+
(known.length ? ` Types available without installing: ${known.join(', ')}.` : '')
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Refuse two instance settings that would read the same `UAG_ENGINE_*` variable. */
|
|
150
|
+
function checkVariables(resolved) {
|
|
151
|
+
const owners = new Map();
|
|
152
|
+
for (const { name, definition } of resolved) {
|
|
153
|
+
for (const key of Object.keys(definition.settings || {})) {
|
|
154
|
+
const variable = settingVariable(name, key);
|
|
155
|
+
const owner = owners.get(variable);
|
|
156
|
+
if (owner && owner.name !== name) {
|
|
157
|
+
throw new EngineConfigError(
|
|
158
|
+
`The engines "${owner.name}" and "${name}" would both read ${variable} (for ${owner.key} and ${key}). Rename one of them.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
owners.set(variable, { name, key });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create one engine from its definition, for an instance name and options.
|
|
168
|
+
* Errors name the instance and its type, and never contain a secret setting.
|
|
169
|
+
*/
|
|
170
|
+
async function createEngine(definition, { name, options = {}, env = process.env, config = {} }) {
|
|
171
|
+
const label = `The "${name}" engine (type "${definition.name}")`;
|
|
172
|
+
const settings = resolveSettings(definition, name, options, env);
|
|
173
|
+
const secrets = secretValues(definition, settings, options);
|
|
174
|
+
let engine;
|
|
175
|
+
try {
|
|
176
|
+
engine = await definition.create({
|
|
177
|
+
name,
|
|
178
|
+
settings,
|
|
179
|
+
options,
|
|
180
|
+
config,
|
|
181
|
+
requireSetting: (key) => {
|
|
182
|
+
if (settings[key] === undefined) throw missingSetting(definition, name, key);
|
|
183
|
+
return settings[key];
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
const message = redact(errorText(err), secrets);
|
|
189
|
+
throw new EngineConfigError(err instanceof EngineConfigError || (err && err.name === 'EngineConfigError') ? message : `${label} failed to start: ${message}`);
|
|
190
|
+
}
|
|
191
|
+
if (!engine || typeof engine !== 'object') {
|
|
192
|
+
throw new EngineConfigError(`${label} did not create an engine.`);
|
|
193
|
+
}
|
|
194
|
+
if (engine.name !== name) {
|
|
195
|
+
throw new EngineConfigError(`${label} created an engine named ${JSON.stringify(engine.name)}; it must use the instance name.`);
|
|
196
|
+
}
|
|
197
|
+
if (!definition.kinds.includes(engine.kind)) {
|
|
198
|
+
throw new EngineConfigError(`${label} created a ${JSON.stringify(engine.kind)} engine; its type declares ${definition.kinds.join(' and ')}.`);
|
|
199
|
+
}
|
|
200
|
+
return engine;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Load the given instances (from `parseInstances` or `engineInstances`): resolve
|
|
205
|
+
* each type, resolve its settings, and create the engine. Returns
|
|
206
|
+
* `{ engine, type, package, version }` for each, in order, ready for
|
|
207
|
+
* `Formflow.engineRegistry`. Any problem stops loading with the instance named.
|
|
208
|
+
*/
|
|
209
|
+
async function loadEngines(instances, { types = {}, paths = [], env = process.env, config = {}, apiVersion = ENGINE_API_VERSION } = {}) {
|
|
210
|
+
const resolved = instances.map((instance) => {
|
|
211
|
+
const found = resolveType(instance.type, { types, paths, apiVersion });
|
|
212
|
+
const name = instance.name || found.definition.name;
|
|
213
|
+
checkName(name);
|
|
214
|
+
return { ...found, name, options: instance.options || {} };
|
|
215
|
+
});
|
|
216
|
+
checkUnique(resolved);
|
|
217
|
+
checkVariables(resolved);
|
|
218
|
+
const loaded = [];
|
|
219
|
+
for (const { definition, name, options, package: pkg, version } of resolved) {
|
|
220
|
+
const engine = await createEngine(definition, { name, options, env, config });
|
|
221
|
+
loaded.push({ engine, type: definition.name, title: definition.title, kinds: [...definition.kinds], ...(definition.version && { typeVersion: definition.version }), ...(pkg && { package: pkg }), ...(version && { version }) });
|
|
222
|
+
}
|
|
223
|
+
return loaded;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Plan the given instances without creating them: resolve each type and its
|
|
228
|
+
* settings, and describe how each is created and how a failure to start reads
|
|
229
|
+
* (with secrets left out). For a host that has engines created elsewhere, such
|
|
230
|
+
* as Agentic Formflow's `startEngines`, which creates and wraps them.
|
|
231
|
+
*/
|
|
232
|
+
function planEngines(instances, { types = {}, paths = [], env = process.env, config = {}, apiVersion = ENGINE_API_VERSION } = {}) {
|
|
233
|
+
const resolved = instances.map((instance) => {
|
|
234
|
+
const found = resolveType(instance.type, { types, paths, apiVersion });
|
|
235
|
+
const name = instance.name || found.definition.name;
|
|
236
|
+
checkName(name);
|
|
237
|
+
return { ...found, name, options: instance.options || {} };
|
|
238
|
+
});
|
|
239
|
+
checkUnique(resolved);
|
|
240
|
+
checkVariables(resolved);
|
|
241
|
+
return resolved.map(({ definition, name, options, package: pkg, version }) => {
|
|
242
|
+
const label = `The "${name}" engine (type "${definition.name}")`;
|
|
243
|
+
const settings = resolveSettings(definition, name, options, env);
|
|
244
|
+
const secrets = secretValues(definition, settings, options);
|
|
245
|
+
return {
|
|
246
|
+
name,
|
|
247
|
+
type: definition.name,
|
|
248
|
+
title: definition.title,
|
|
249
|
+
kinds: [...definition.kinds],
|
|
250
|
+
...(definition.version && { typeVersion: definition.version }),
|
|
251
|
+
...(pkg && { package: pkg }),
|
|
252
|
+
...(version && { version }),
|
|
253
|
+
definition,
|
|
254
|
+
args: {
|
|
255
|
+
name,
|
|
256
|
+
settings,
|
|
257
|
+
options,
|
|
258
|
+
config,
|
|
259
|
+
requireSetting: (key) => {
|
|
260
|
+
if (settings[key] === undefined) throw missingSetting(definition, name, key);
|
|
261
|
+
return settings[key];
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
failure: (err) => {
|
|
265
|
+
const message = redact(errorText(err), secrets);
|
|
266
|
+
return err instanceof EngineConfigError || (err && err.name === 'EngineConfigError') ? message : `${label} failed to start: ${message}`;
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = { enginePaths, packageCandidates, resolveType, createEngine, loadEngines, planEngines };
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formio/uag-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0-rc.10",
|
|
4
|
+
"description": "Build engine packages for the Form.io Universal Agent Gateway (UAG): the engine contract, defineEngine, settings and instance resolution, the engine loader, and a conformance check.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"formio",
|
|
7
|
+
"uag",
|
|
8
|
+
"universal-agent-gateway",
|
|
9
|
+
"ai",
|
|
10
|
+
"agents",
|
|
11
|
+
"engine",
|
|
12
|
+
"sdk"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"main": "index.js",
|
|
16
|
+
"types": "index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./index.d.ts",
|
|
20
|
+
"default": "./index.js"
|
|
21
|
+
},
|
|
22
|
+
"./loader": {
|
|
23
|
+
"types": "./loader.d.ts",
|
|
24
|
+
"default": "./loader.js"
|
|
25
|
+
},
|
|
26
|
+
"./conformance": {
|
|
27
|
+
"types": "./conformance.d.ts",
|
|
28
|
+
"default": "./conformance.js"
|
|
29
|
+
},
|
|
30
|
+
"./protocol": {
|
|
31
|
+
"types": "./protocol.d.ts",
|
|
32
|
+
"default": "./protocol.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"*.js",
|
|
38
|
+
"*.d.ts",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "mocha"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22.0.0"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/formio/uag.git",
|
|
50
|
+
"directory": "packages/engine-sdk"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"chai": "^6.3.0",
|
|
57
|
+
"mocha": "^12.0.3"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/protocol.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Attachment, DecisionQuestion, DecisionResponse, DecisionState, ExtractResponse, FieldSpec, GenerateRequest, GenerateResponse, ToolDefinition } from './contract';
|
|
2
|
+
|
|
3
|
+
/** The remote engine protocol version this SDK speaks. */
|
|
4
|
+
export declare const PROTOCOL_VERSION: 1;
|
|
5
|
+
|
|
6
|
+
/** The header that carries a scoped token, on research extracts only. */
|
|
7
|
+
export declare const SCOPED_TOKEN_HEADER: 'x-uag-scoped-token';
|
|
8
|
+
|
|
9
|
+
/** The optional header that carries the Agent API request id, on decide and extract. */
|
|
10
|
+
export declare const REQUEST_ID_HEADER: 'x-request-id';
|
|
11
|
+
|
|
12
|
+
/** A request id that is safe to send and log (up to 128 of `[A-Za-z0-9._:-]`), or undefined. */
|
|
13
|
+
export declare function safeRequestId(value: unknown): string | undefined;
|
|
14
|
+
|
|
15
|
+
/** A message in another protocol version, or of the wrong shape. */
|
|
16
|
+
export declare class ProtocolError extends Error {
|
|
17
|
+
status: number;
|
|
18
|
+
constructor(message: string, status?: number);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One engine an engine server hosts, as `GET /v1/engines` describes it. */
|
|
22
|
+
export type RemoteEngineDescription = {
|
|
23
|
+
name: string;
|
|
24
|
+
kind: 'decision' | 'agent';
|
|
25
|
+
model: string;
|
|
26
|
+
maxOptions?: number;
|
|
27
|
+
toolUse?: boolean;
|
|
28
|
+
/** Agent engines: the document kinds generate reads. Default ['text']. */
|
|
29
|
+
accepts?: Array<'text' | 'pdf'>;
|
|
30
|
+
/** Agent engines: whether the engine generates free text. */
|
|
31
|
+
generate?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** The most PDF bytes one generate request carries (32 MB). */
|
|
35
|
+
export declare const MAX_DOCUMENT_BYTES: number;
|
|
36
|
+
|
|
37
|
+
/** A generate request on the wire: PDFs as base64. */
|
|
38
|
+
export type WireAttachment =
|
|
39
|
+
| { kind: 'pdf'; data: string; label: string; pages?: number }
|
|
40
|
+
| { kind: 'text'; text: string; label: string; textFormat?: 'plain' | 'markdown' | 'csv' };
|
|
41
|
+
|
|
42
|
+
export type RemoteExtractRequest = {
|
|
43
|
+
instructions: string;
|
|
44
|
+
state: DecisionState;
|
|
45
|
+
fields: FieldSpec[];
|
|
46
|
+
schema: Record<string, any>;
|
|
47
|
+
tools?: ToolDefinition[];
|
|
48
|
+
maxIterations?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export declare function checkVersion(body: unknown, side?: 'request' | 'response'): void;
|
|
52
|
+
export declare function checkDecideRequest(body: unknown): { state: DecisionState; questions: Record<string, DecisionQuestion> };
|
|
53
|
+
export declare function checkExtractRequest(body: unknown): RemoteExtractRequest;
|
|
54
|
+
export declare function checkDescription(body: unknown): RemoteEngineDescription[];
|
|
55
|
+
export declare function checkDecisionResponse(body: unknown): DecisionResponse;
|
|
56
|
+
export declare function checkExtractResponse(body: unknown): ExtractResponse;
|
|
57
|
+
export declare function checkGenerateRequest(body: unknown, maxBytes?: number): Omit<GenerateRequest, 'signal' | 'onProgress'>;
|
|
58
|
+
export declare function checkGenerateResponse(body: unknown): GenerateResponse;
|
|
59
|
+
export declare function encodeAttachments(attachments?: Attachment[]): WireAttachment[];
|
|
60
|
+
export declare function pdfByteLength(attachments?: Attachment[]): number;
|
|
61
|
+
export declare function errorBody(message: string, expose?: boolean): { protocol: 1; error: string; expose: boolean };
|