@formio/uag-sdk 2.0.0-rc.1 → 2.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -4
- package/conformance.js +29 -0
- package/contract.d.ts +34 -0
- package/define.js +43 -7
- package/index.d.ts +12 -3
- package/loader.d.ts +19 -0
- package/loader.js +59 -10
- package/package.json +1 -1
- package/protocol.d.ts +17 -1
- package/protocol.js +81 -1
- package/settings.js +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @formio/uag-sdk
|
|
2
2
|
|
|
3
|
-
<sub>[Form.io UAG](https://github.com/formio/uag/blob/
|
|
3
|
+
<sub>[Form.io UAG](https://github.com/formio/uag/blob/v2.0.0-rc.8/README.md) › [Engines](https://github.com/formio/uag/blob/v2.0.0-rc.8/README.md#engines-and-routing) › this page</sub>
|
|
4
4
|
|
|
5
5
|
Build engine packages for the Form.io Universal Agent Gateway (UAG). This package contains:
|
|
6
6
|
|
|
@@ -15,13 +15,15 @@ An engine package depends on this package only. It never needs `@formio/uag`.
|
|
|
15
15
|
|
|
16
16
|
## An engine package
|
|
17
17
|
|
|
18
|
-
Name the package `@formio/uag-<type>` (official engines) or `uag-engine-<type>` (anyone's). Export one engine type definition:
|
|
18
|
+
Name the package `@formio/uag-<type>` (official engines) or `uag-engine-<type>` (anyone's). Export one engine type definition. It is a plugin of type `"engine"`: `name` is the type name used in `UAG_ENGINES`, `title` (default: `name`) is shown to people, and `version` defaults to your package's own version:
|
|
19
19
|
|
|
20
20
|
```js
|
|
21
21
|
const { defineEngine, EngineError, ENGINE_API_VERSION, requiredSchema } = require('@formio/uag-sdk');
|
|
22
22
|
|
|
23
23
|
module.exports = defineEngine({
|
|
24
|
-
type: '
|
|
24
|
+
type: 'engine',
|
|
25
|
+
name: 'acme',
|
|
26
|
+
title: 'Acme',
|
|
25
27
|
kinds: ['agent'],
|
|
26
28
|
apiVersion: ENGINE_API_VERSION,
|
|
27
29
|
settings: {
|
|
@@ -50,6 +52,23 @@ module.exports = defineEngine({
|
|
|
50
52
|
|
|
51
53
|
The engine must use the instance `name` it is given, and one of the `kinds` it declares. A decision engine has `decide` and `maxOptions`; an agent engine has `extract`, and sets `toolUse: true` when `extract` accepts read-only research tools. See `contract.d.ts` for every request and response.
|
|
52
54
|
|
|
55
|
+
A definition in the shape of the 2.0 release candidates, with the engine's name in `type` (`{ type: 'acme', kinds, ... }`), is refused with a message showing the shape above.
|
|
56
|
+
|
|
57
|
+
### Generating text over documents
|
|
58
|
+
|
|
59
|
+
An agent engine may also implement `generate(request)`, which document plugins call to write free text over uploaded documents, and declare `accepts`, the document kinds it reads natively (default `['text']`; list `'pdf'` only when your model reads PDFs as documents):
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
accepts: ['text', 'pdf'],
|
|
63
|
+
async generate({ instructions, system, attachments, format, maxTokens, signal, onProgress }) {
|
|
64
|
+
// attachments: { kind: 'pdf', data: Buffer, label, pages? } or { kind: 'text', text, label, textFormat? }
|
|
65
|
+
// Put the documents before the instructions; report progress as the answer streams.
|
|
66
|
+
return { text, model, stopReason: 'end' /* or 'max_tokens' when cut off */, usage };
|
|
67
|
+
},
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`generate` must honour `signal`, never log the documents, and report a truncated answer as `stopReason: 'max_tokens'` rather than failing.
|
|
71
|
+
|
|
53
72
|
`create` receives `{ name, settings, options, config, requireSetting }`:
|
|
54
73
|
|
|
55
74
|
- `settings`: the instance's value for each declared setting that has one.
|
|
@@ -135,8 +154,9 @@ The check creates an instance and calls it. Pass options, such as a fake client,
|
|
|
135
154
|
- the definition and the engine have the contract's shape;
|
|
136
155
|
- the engine uses the instance name and the `model` option;
|
|
137
156
|
- `extract` returns only the fields it was asked about, or `decide` only the questions it was asked;
|
|
157
|
+
- `accepts`, when set, lists only `text` and `pdf`; `generate`, when present, returns text, its model and a valid `stopReason`;
|
|
138
158
|
- research stops at `maxIterations`, and reports every tool call it made;
|
|
139
|
-
- an aborted signal stops the engine, including during research
|
|
159
|
+
- an aborted signal stops the engine, including during research and `generate`.
|
|
140
160
|
|
|
141
161
|
A research round is the calls a model makes together; calls made one after another count as separate rounds.
|
|
142
162
|
|
package/conformance.js
CHANGED
|
@@ -206,6 +206,35 @@ async function checkEngine(definition, { name = 'conformance', options = {}, env
|
|
|
206
206
|
controller.abort();
|
|
207
207
|
await within(timeout, rejects(engine.extract(extractRequest({ signal: controller.signal })), 'extract with an aborted signal'), 'extract with an aborted signal');
|
|
208
208
|
});
|
|
209
|
+
await check('accepts lists only known document kinds', () => {
|
|
210
|
+
if (engine.accepts === undefined) return;
|
|
211
|
+
if (!Array.isArray(engine.accepts) || !engine.accepts.length) fail('accepts must be a non-empty list of "text" and "pdf"');
|
|
212
|
+
const unknown = engine.accepts.filter((kind) => !['text', 'pdf'].includes(kind));
|
|
213
|
+
if (unknown.length) fail(`accepts lists unknown kinds: ${unknown.join(', ')}`);
|
|
214
|
+
});
|
|
215
|
+
if (typeof engine.generate === 'function') {
|
|
216
|
+
await check('generate returns text, its model and a valid stopReason', async () => {
|
|
217
|
+
const response = await within(timeout, engine.generate({
|
|
218
|
+
instructions: 'Summarize the attached document in one sentence.',
|
|
219
|
+
attachments: [{ kind: 'text', text: 'Ada Lovelace lives in London and works on engines.', label: 'Document 1: notes.txt', textFormat: 'plain' }],
|
|
220
|
+
format: 'markdown',
|
|
221
|
+
maxTokens: 200,
|
|
222
|
+
}), 'generate');
|
|
223
|
+
if (!response || typeof response !== 'object') fail('generate did not return an object');
|
|
224
|
+
if (typeof response.text !== 'string') fail('the response has no text');
|
|
225
|
+
if (typeof response.model !== 'string' || !response.model) fail('the response does not name its model');
|
|
226
|
+
if (!['end', 'max_tokens'].includes(response.stopReason)) fail(`stopReason must be "end" or "max_tokens", not ${JSON.stringify(response.stopReason)}`);
|
|
227
|
+
checkUsage(response);
|
|
228
|
+
});
|
|
229
|
+
await check('generate stops when its signal is aborted', async () => {
|
|
230
|
+
const controller = new AbortController();
|
|
231
|
+
controller.abort();
|
|
232
|
+
await within(timeout, rejects(engine.generate({ instructions: 'Summarize.', format: 'text', signal: controller.signal }), 'generate with an aborted signal'), 'generate with an aborted signal');
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else if (engine.generate !== undefined) {
|
|
236
|
+
await check('generate is a function', () => fail('generate must be a function when present'));
|
|
237
|
+
}
|
|
209
238
|
if (engine.toolUse) {
|
|
210
239
|
await check('research reports its tool calls and stops at maxIterations', async () => {
|
|
211
240
|
const { state, callTool } = lookupTool();
|
package/contract.d.ts
CHANGED
|
@@ -185,5 +185,39 @@ export interface AgentEngine {
|
|
|
185
185
|
toolUse?: boolean;
|
|
186
186
|
/** Structured extraction that returns values for the requested fields only. */
|
|
187
187
|
extract(request: ExtractRequest): Promise<ExtractResponse>;
|
|
188
|
+
/**
|
|
189
|
+
* The document representations `generate` reads natively. Default ['text'].
|
|
190
|
+
* An engine lists 'pdf' only when it sends PDFs to its model as documents.
|
|
191
|
+
*/
|
|
192
|
+
accepts?: Array<'text' | 'pdf'>;
|
|
193
|
+
/** Free-text generation over attached documents, for document plugins. Optional. */
|
|
194
|
+
generate?(request: GenerateRequest): Promise<GenerateResponse>;
|
|
188
195
|
}
|
|
189
196
|
export type Engine = DecisionEngine | AgentEngine;
|
|
197
|
+
|
|
198
|
+
/** A document an engine reads: PDF bytes, or text with its format. */
|
|
199
|
+
export type Attachment =
|
|
200
|
+
| { kind: 'pdf'; data: Buffer; label: string; pages?: number }
|
|
201
|
+
| { kind: 'text'; text: string; label: string; textFormat?: 'plain' | 'markdown' | 'csv' };
|
|
202
|
+
|
|
203
|
+
export type GenerateRequest = {
|
|
204
|
+
/** The user prompt. */
|
|
205
|
+
instructions: string;
|
|
206
|
+
system?: string;
|
|
207
|
+
/** Sent before the instructions, in order. */
|
|
208
|
+
attachments?: Attachment[];
|
|
209
|
+
format: 'markdown' | 'text';
|
|
210
|
+
/** Default: the engine's own output limit setting. */
|
|
211
|
+
maxTokens?: number;
|
|
212
|
+
signal?: AbortSignal;
|
|
213
|
+
/** Called as the answer streams, with the characters produced so far. */
|
|
214
|
+
onProgress?(outputChars: number): void;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type GenerateResponse = {
|
|
218
|
+
text: string;
|
|
219
|
+
model: string;
|
|
220
|
+
/** 'max_tokens': the answer was cut off at the output limit. */
|
|
221
|
+
stopReason: 'end' | 'max_tokens';
|
|
222
|
+
usage?: EngineUsage;
|
|
223
|
+
};
|
package/define.js
CHANGED
|
@@ -6,6 +6,14 @@ const ENGINE_API_VERSION = 1;
|
|
|
6
6
|
/** Engine type and instance names: what `UAG_ENGINES` and routing lists use. */
|
|
7
7
|
const ENGINE_NAME = /^[a-z][a-z0-9-]{0,62}$/;
|
|
8
8
|
|
|
9
|
+
/** The plugin type of every engine definition. */
|
|
10
|
+
const ENGINE_PLUGIN_TYPE = 'engine';
|
|
11
|
+
|
|
12
|
+
/** The shape an engine definition takes, shown when one is in another shape. */
|
|
13
|
+
function definitionShape(name = 'acme') {
|
|
14
|
+
return `{ type: 'engine', name: '${name}', title: '${name[0].toUpperCase()}${name.slice(1)}', version: '1.0.0', kinds, apiVersion, settings, create }`;
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
const KINDS = ['decision', 'agent'];
|
|
10
18
|
const SETTING_NAME = /^[a-z][a-zA-Z0-9]*$/;
|
|
11
19
|
const VARIABLE_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
@@ -18,8 +26,23 @@ function definitionProblems(definition) {
|
|
|
18
26
|
return ['it is not an object'];
|
|
19
27
|
}
|
|
20
28
|
const problems = [];
|
|
21
|
-
if (
|
|
22
|
-
|
|
29
|
+
if (definition.type !== ENGINE_PLUGIN_TYPE) {
|
|
30
|
+
// The 2.0 release candidates put the engine's name in "type". The shape was
|
|
31
|
+
// never in a final release, so it is refused rather than converted.
|
|
32
|
+
if (typeof definition.type === 'string' && definition.name === undefined) {
|
|
33
|
+
return [`it is in the shape of the 2.0 release candidates, { type: ${JSON.stringify(definition.type)}, kinds, ... }; ` +
|
|
34
|
+
`an engine definition is now a plugin: ${definitionShape(ENGINE_NAME.test(definition.type) ? definition.type : 'acme')}`];
|
|
35
|
+
}
|
|
36
|
+
problems.push(`"type" must be "${ENGINE_PLUGIN_TYPE}"`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof definition.name !== 'string' || !ENGINE_NAME.test(definition.name)) {
|
|
39
|
+
problems.push('"name" must be a name of lowercase letters, digits and "-", starting with a letter');
|
|
40
|
+
}
|
|
41
|
+
if (definition.title !== undefined && (typeof definition.title !== 'string' || !definition.title)) {
|
|
42
|
+
problems.push('"title" must be a non-empty string');
|
|
43
|
+
}
|
|
44
|
+
if (definition.version !== undefined && (typeof definition.version !== 'string' || !/^\d+\.\d+\.\d+/.test(definition.version))) {
|
|
45
|
+
problems.push('"version" must be a semver string such as "1.0.0"');
|
|
23
46
|
}
|
|
24
47
|
if (!Array.isArray(definition.kinds) || !definition.kinds.length || !definition.kinds.every((kind) => KINDS.includes(kind))) {
|
|
25
48
|
problems.push('"kinds" must list "decision", "agent" or both');
|
|
@@ -83,7 +106,10 @@ function isEngineType(value) {
|
|
|
83
106
|
* mistake in it fails when the package loads rather than when the UAG starts:
|
|
84
107
|
*
|
|
85
108
|
* module.exports = defineEngine({
|
|
86
|
-
* type: '
|
|
109
|
+
* type: 'engine',
|
|
110
|
+
* name: 'acme',
|
|
111
|
+
* title: 'Acme',
|
|
112
|
+
* version: '1.0.0', // optional: an engine package's own version is used
|
|
87
113
|
* kinds: ['agent'],
|
|
88
114
|
* apiVersion: ENGINE_API_VERSION,
|
|
89
115
|
* settings: { apiKey: { env: 'ACME_API_KEY', required: true, secret: true } },
|
|
@@ -93,14 +119,24 @@ function isEngineType(value) {
|
|
|
93
119
|
function defineEngine(definition) {
|
|
94
120
|
const problems = definitionProblems(definition);
|
|
95
121
|
if (problems.length) {
|
|
96
|
-
const
|
|
97
|
-
throw new TypeError(`The engine type${
|
|
122
|
+
const name = definition && typeof definition.name === 'string' ? definition.name : definition && typeof definition.type === 'string' && definition.type !== ENGINE_PLUGIN_TYPE ? definition.type : '';
|
|
123
|
+
throw new TypeError(`The engine type${name ? ` "${name}"` : ''} is not valid: ${problems.join('; ')}.`);
|
|
98
124
|
}
|
|
99
125
|
const settings = {};
|
|
100
126
|
for (const [key, spec] of Object.entries(definition.settings || {})) {
|
|
101
127
|
settings[key] = Object.freeze({ ...spec, ...(spec.env !== undefined && { env: Object.freeze([].concat(spec.env)) }) });
|
|
102
128
|
}
|
|
103
|
-
return Object.freeze({ ...definition, kinds: Object.freeze([...definition.kinds]), settings: Object.freeze(settings) });
|
|
129
|
+
return Object.freeze({ ...definition, title: definition.title || definition.name, kinds: Object.freeze([...definition.kinds]), settings: Object.freeze(settings) });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A valid definition with its defaults: `title` is its name, and `version` the
|
|
134
|
+
* version of the package it was found in, when it does not give its own.
|
|
135
|
+
*/
|
|
136
|
+
function withDefaults(definition, packageVersion) {
|
|
137
|
+
if (definition.title && (definition.version || !packageVersion)) return definition;
|
|
138
|
+
const version = definition.version || packageVersion;
|
|
139
|
+
return { ...definition, title: definition.title || definition.name, ...(version && { version }) };
|
|
104
140
|
}
|
|
105
141
|
|
|
106
|
-
module.exports = { ENGINE_API_VERSION, ENGINE_NAME, KINDS, defineEngine, definitionProblems, isEngineType };
|
|
142
|
+
module.exports = { ENGINE_API_VERSION, ENGINE_NAME, ENGINE_PLUGIN_TYPE, KINDS, defineEngine, definitionProblems, definitionShape, isEngineType, withDefaults };
|
package/index.d.ts
CHANGED
|
@@ -33,10 +33,19 @@ export type EngineCreateContext<Settings = Record<string, any>> = {
|
|
|
33
33
|
requireSetting(key: string): any;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* What an engine package exports: a plugin of type "engine". The 2.0 release
|
|
38
|
+
* candidates' shape, with the engine's name in `type`, is refused.
|
|
39
|
+
*/
|
|
37
40
|
export interface EngineTypeDefinition<Settings = Record<string, any>> {
|
|
41
|
+
/** Always "engine": the plugin type. */
|
|
42
|
+
type: 'engine';
|
|
38
43
|
/** The type name, as in `UAG_ENGINES=<type>` and `<name>:<type>`. */
|
|
39
|
-
|
|
44
|
+
name: string;
|
|
45
|
+
/** A human-readable name. Defaults to `name`. */
|
|
46
|
+
title?: string;
|
|
47
|
+
/** The engine's own version (semver). Defaults to the version in its package's package.json. */
|
|
48
|
+
version?: string;
|
|
40
49
|
kinds: readonly ('decision' | 'agent')[];
|
|
41
50
|
/** The engine contract version the type was built for: `ENGINE_API_VERSION`. */
|
|
42
51
|
apiVersion: number;
|
|
@@ -45,7 +54,7 @@ export interface EngineTypeDefinition<Settings = Record<string, any>> {
|
|
|
45
54
|
create(context: EngineCreateContext<Settings>): Engine | Promise<Engine>;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
/** Validate an engine type definition and return it frozen. Throws a TypeError naming every problem. */
|
|
57
|
+
/** Validate an engine type definition and return it frozen, its title defaulting to its name. Throws a TypeError naming every problem. */
|
|
49
58
|
export declare function defineEngine<T extends EngineTypeDefinition<any>>(definition: T): Readonly<T>;
|
|
50
59
|
|
|
51
60
|
/** Whether a value is a valid engine type definition, from this SDK, another copy of it, or none. */
|
package/loader.d.ts
CHANGED
|
@@ -15,7 +15,14 @@ export type LoadOptions = {
|
|
|
15
15
|
/** An enabled engine and where it came from, ready for `Formflow.engineRegistry`. */
|
|
16
16
|
export type LoadedEngine = {
|
|
17
17
|
engine: Engine;
|
|
18
|
+
/** The engine type's name. */
|
|
18
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;
|
|
19
26
|
package?: string;
|
|
20
27
|
version?: string;
|
|
21
28
|
};
|
|
@@ -46,3 +53,15 @@ export declare function createEngine(definition: EngineTypeDefinition, options:
|
|
|
46
53
|
|
|
47
54
|
/** Resolve, configure and create each instance. Any problem stops loading, with the instance named. */
|
|
48
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { ENGINE_API_VERSION, definitionProblems } = require('./define');
|
|
4
|
+
const { ENGINE_API_VERSION, definitionProblems, withDefaults } = require('./define');
|
|
5
5
|
const { EngineConfigError, redact } = require('./errors');
|
|
6
6
|
const { checkName, checkType, checkUnique, isPackageName } = require('./instances');
|
|
7
7
|
const { missingSetting, resolveSettings, secretValues, settingVariable } = require('./settings');
|
|
@@ -87,19 +87,22 @@ function errorText(err) {
|
|
|
87
87
|
return err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
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) {
|
|
92
95
|
const problems = definitionProblems(definition);
|
|
93
96
|
if (problems.length) {
|
|
94
97
|
throw new EngineConfigError(`${source} is not a UAG engine: it does not export a valid engine type definition (${problems.join('; ')}).`);
|
|
95
98
|
}
|
|
96
99
|
if (definition.apiVersion !== apiVersion) {
|
|
97
100
|
throw new EngineConfigError(
|
|
98
|
-
`${source} (engine type "${definition.
|
|
101
|
+
`${source} (engine type "${definition.name}") was built for engine API version ${definition.apiVersion}; ` +
|
|
99
102
|
`this server supports version ${apiVersion}.`
|
|
100
103
|
);
|
|
101
104
|
}
|
|
102
|
-
return definition;
|
|
105
|
+
return withDefaults(definition, packageVersion);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
/**
|
|
@@ -130,7 +133,7 @@ function resolveType(type, { types = {}, paths = [], apiVersion = ENGINE_API_VER
|
|
|
130
133
|
}
|
|
131
134
|
const definition = exported && exported.__esModule && exported.default ? exported.default : exported;
|
|
132
135
|
return {
|
|
133
|
-
definition: checkDefinition(definition, `The package "${candidate}"`, apiVersion),
|
|
136
|
+
definition: checkDefinition(definition, `The package "${candidate}"`, apiVersion, pkg.version),
|
|
134
137
|
package: candidate,
|
|
135
138
|
...(pkg.version && { version: pkg.version })
|
|
136
139
|
};
|
|
@@ -165,7 +168,7 @@ function checkVariables(resolved) {
|
|
|
165
168
|
* Errors name the instance and its type, and never contain a secret setting.
|
|
166
169
|
*/
|
|
167
170
|
async function createEngine(definition, { name, options = {}, env = process.env, config = {} }) {
|
|
168
|
-
const label = `The "${name}" engine (type "${definition.
|
|
171
|
+
const label = `The "${name}" engine (type "${definition.name}")`;
|
|
169
172
|
const settings = resolveSettings(definition, name, options, env);
|
|
170
173
|
const secrets = secretValues(definition, settings, options);
|
|
171
174
|
let engine;
|
|
@@ -206,7 +209,7 @@ async function createEngine(definition, { name, options = {}, env = process.env,
|
|
|
206
209
|
async function loadEngines(instances, { types = {}, paths = [], env = process.env, config = {}, apiVersion = ENGINE_API_VERSION } = {}) {
|
|
207
210
|
const resolved = instances.map((instance) => {
|
|
208
211
|
const found = resolveType(instance.type, { types, paths, apiVersion });
|
|
209
|
-
const name = instance.name || found.definition.
|
|
212
|
+
const name = instance.name || found.definition.name;
|
|
210
213
|
checkName(name);
|
|
211
214
|
return { ...found, name, options: instance.options || {} };
|
|
212
215
|
});
|
|
@@ -215,9 +218,55 @@ async function loadEngines(instances, { types = {}, paths = [], env = process.en
|
|
|
215
218
|
const loaded = [];
|
|
216
219
|
for (const { definition, name, options, package: pkg, version } of resolved) {
|
|
217
220
|
const engine = await createEngine(definition, { name, options, env, config });
|
|
218
|
-
loaded.push({ engine, type: definition.
|
|
221
|
+
loaded.push({ engine, type: definition.name, title: definition.title, kinds: [...definition.kinds], ...(definition.version && { typeVersion: definition.version }), ...(pkg && { package: pkg }), ...(version && { version }) });
|
|
219
222
|
}
|
|
220
223
|
return loaded;
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
|
|
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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formio/uag-sdk",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.10",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"formio",
|
package/protocol.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DecisionQuestion, DecisionResponse, DecisionState, ExtractResponse, FieldSpec, ToolDefinition } from './contract';
|
|
1
|
+
import type { Attachment, DecisionQuestion, DecisionResponse, DecisionState, ExtractResponse, FieldSpec, GenerateRequest, GenerateResponse, ToolDefinition } from './contract';
|
|
2
2
|
|
|
3
3
|
/** The remote engine protocol version this SDK speaks. */
|
|
4
4
|
export declare const PROTOCOL_VERSION: 1;
|
|
@@ -25,8 +25,20 @@ export type RemoteEngineDescription = {
|
|
|
25
25
|
model: string;
|
|
26
26
|
maxOptions?: number;
|
|
27
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;
|
|
28
32
|
};
|
|
29
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
|
+
|
|
30
42
|
export type RemoteExtractRequest = {
|
|
31
43
|
instructions: string;
|
|
32
44
|
state: DecisionState;
|
|
@@ -42,4 +54,8 @@ export declare function checkExtractRequest(body: unknown): RemoteExtractRequest
|
|
|
42
54
|
export declare function checkDescription(body: unknown): RemoteEngineDescription[];
|
|
43
55
|
export declare function checkDecisionResponse(body: unknown): DecisionResponse;
|
|
44
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;
|
|
45
61
|
export declare function errorBody(message: string, expose?: boolean): { protocol: 1; error: string; expose: boolean };
|
package/protocol.js
CHANGED
|
@@ -26,6 +26,14 @@ const SCOPED_TOKEN_HEADER = 'x-uag-scoped-token';
|
|
|
26
26
|
const REQUEST_ID_HEADER = 'x-request-id';
|
|
27
27
|
const REQUEST_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The most PDF bytes one generate request carries: the provider request cap
|
|
31
|
+
* the UAG's documents.maxTotalBytes stays within (32 MB).
|
|
32
|
+
*/
|
|
33
|
+
const MAX_DOCUMENT_BYTES = 32 * 1024 * 1024;
|
|
34
|
+
const DOCUMENT_KINDS = ['text', 'pdf'];
|
|
35
|
+
const TEXT_FORMATS = ['plain', 'markdown', 'csv'];
|
|
36
|
+
|
|
29
37
|
/** A request id that is safe to send and log: up to 128 letters, digits, `.`, `_`, `:` or `-`. Anything else is undefined. */
|
|
30
38
|
function safeRequestId(value) {
|
|
31
39
|
return typeof value === 'string' && REQUEST_ID.test(value) ? value : undefined;
|
|
@@ -102,16 +110,83 @@ function checkDescription(body) {
|
|
|
102
110
|
if (engine.kind === 'decision') {
|
|
103
111
|
check(Number.isInteger(engine.maxOptions) && engine.maxOptions >= 2, `The decision engine "${engine.name}" needs maxOptions.`);
|
|
104
112
|
}
|
|
113
|
+
if (engine.kind === 'agent' && engine.accepts !== undefined) {
|
|
114
|
+
check(Array.isArray(engine.accepts) && engine.accepts.length && engine.accepts.every((kind) => DOCUMENT_KINDS.includes(kind)),
|
|
115
|
+
`The engine "${engine.name}" must list "text" and "pdf" only in "accepts".`);
|
|
116
|
+
}
|
|
105
117
|
return {
|
|
106
118
|
name: engine.name,
|
|
107
119
|
kind: engine.kind,
|
|
108
120
|
model: engine.model,
|
|
109
121
|
...(engine.kind === 'decision' && { maxOptions: engine.maxOptions }),
|
|
110
|
-
...(engine.kind === 'agent' && {
|
|
122
|
+
...(engine.kind === 'agent' && {
|
|
123
|
+
toolUse: engine.toolUse === true,
|
|
124
|
+
accepts: engine.accepts || ['text'],
|
|
125
|
+
generate: engine.generate === true,
|
|
126
|
+
}),
|
|
111
127
|
};
|
|
112
128
|
});
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
/**
|
|
132
|
+
* A generate request, as an engine server receives it. PDF attachments arrive
|
|
133
|
+
* as base64 and are returned as Buffers; their decoded total may not exceed
|
|
134
|
+
* `maxBytes`.
|
|
135
|
+
*/
|
|
136
|
+
function checkGenerateRequest(body, maxBytes = MAX_DOCUMENT_BYTES) {
|
|
137
|
+
checkVersion(body);
|
|
138
|
+
check(typeof body.instructions === 'string', '"instructions" must be a string.');
|
|
139
|
+
check(body.system === undefined || typeof body.system === 'string', '"system" must be a string.');
|
|
140
|
+
check(['markdown', 'text'].includes(body.format), '"format" must be "markdown" or "text".');
|
|
141
|
+
check(body.maxTokens === undefined || (Number.isInteger(body.maxTokens) && body.maxTokens > 0), '"maxTokens" must be a positive whole number.');
|
|
142
|
+
check(body.attachments === undefined || Array.isArray(body.attachments), '"attachments" must be a list.');
|
|
143
|
+
let pdfBytes = 0;
|
|
144
|
+
const attachments = (body.attachments || []).map((attachment, i) => {
|
|
145
|
+
const where = `Attachment ${i + 1}`;
|
|
146
|
+
check(isObject(attachment) && typeof attachment.label === 'string', `${where} needs a label.`);
|
|
147
|
+
if (attachment.kind === 'pdf') {
|
|
148
|
+
check(typeof attachment.data === 'string', `${where} must carry its PDF as base64 "data".`);
|
|
149
|
+
check(attachment.pages === undefined || (Number.isInteger(attachment.pages) && attachment.pages >= 0), `${where} "pages" must be a whole number.`);
|
|
150
|
+
const data = Buffer.from(attachment.data, 'base64');
|
|
151
|
+
pdfBytes += data.length;
|
|
152
|
+
check(pdfBytes <= maxBytes, `The attached PDFs exceed ${maxBytes} bytes.`, 413);
|
|
153
|
+
return { kind: 'pdf', data, label: attachment.label, ...(attachment.pages !== undefined && { pages: attachment.pages }) };
|
|
154
|
+
}
|
|
155
|
+
check(attachment.kind === 'text' && typeof attachment.text === 'string', `${where} must be a "pdf" or "text" attachment.`);
|
|
156
|
+
check(attachment.textFormat === undefined || TEXT_FORMATS.includes(attachment.textFormat), `${where} "textFormat" must be plain, markdown or csv.`);
|
|
157
|
+
return { kind: 'text', text: attachment.text, label: attachment.label, ...(attachment.textFormat && { textFormat: attachment.textFormat }) };
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
instructions: body.instructions,
|
|
161
|
+
...(body.system !== undefined && { system: body.system }),
|
|
162
|
+
attachments,
|
|
163
|
+
format: body.format,
|
|
164
|
+
...(body.maxTokens !== undefined && { maxTokens: body.maxTokens }),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Attachments for the wire: PDF bytes as base64. Never logged. */
|
|
169
|
+
function encodeAttachments(attachments = []) {
|
|
170
|
+
return attachments.map((attachment) => attachment.kind === 'pdf'
|
|
171
|
+
? { kind: 'pdf', data: Buffer.from(attachment.data).toString('base64'), label: attachment.label, ...(attachment.pages !== undefined && { pages: attachment.pages }) }
|
|
172
|
+
: { kind: 'text', text: attachment.text, label: attachment.label, ...(attachment.textFormat && { textFormat: attachment.textFormat }) });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The decoded size of the PDFs among attachments. */
|
|
176
|
+
function pdfByteLength(attachments = []) {
|
|
177
|
+
return attachments.reduce((total, attachment) => total + (attachment.kind === 'pdf' ? Buffer.byteLength(attachment.data) : 0), 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A generate response, as the UAG receives it. */
|
|
181
|
+
function checkGenerateResponse(body) {
|
|
182
|
+
checkVersion(body, 'response');
|
|
183
|
+
check(typeof body.text === 'string', 'The response has no "text".');
|
|
184
|
+
check(typeof body.model === 'string', 'The response does not name its model.');
|
|
185
|
+
check(['end', 'max_tokens'].includes(body.stopReason), '"stopReason" must be "end" or "max_tokens".');
|
|
186
|
+
const usage = checkUsage(body.usage);
|
|
187
|
+
return { text: body.text, model: body.model, stopReason: body.stopReason, ...(usage && { usage }) };
|
|
188
|
+
}
|
|
189
|
+
|
|
115
190
|
/** A token count: a whole number, or null when not reported. */
|
|
116
191
|
function tokenCount(value, key) {
|
|
117
192
|
if (value === undefined || value === null) return null;
|
|
@@ -185,5 +260,10 @@ module.exports = {
|
|
|
185
260
|
checkDescription,
|
|
186
261
|
checkDecisionResponse,
|
|
187
262
|
checkExtractResponse,
|
|
263
|
+
checkGenerateRequest,
|
|
264
|
+
checkGenerateResponse,
|
|
265
|
+
encodeAttachments,
|
|
266
|
+
pdfByteLength,
|
|
267
|
+
MAX_DOCUMENT_BYTES,
|
|
188
268
|
errorBody,
|
|
189
269
|
};
|
package/settings.js
CHANGED
|
@@ -81,7 +81,7 @@ function resolveSettings(definition, name, options = {}, env = process.env) {
|
|
|
81
81
|
function missingSetting(definition, name, key) {
|
|
82
82
|
const spec = (definition.settings || {})[key] || {};
|
|
83
83
|
return new EngineConfigError(
|
|
84
|
-
`The "${name}" engine (type "${definition.
|
|
84
|
+
`The "${name}" engine (type "${definition.name}") needs its ${key} setting: set ${listNames(settingVariables(name, key, spec))}.`
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
|