@formio/uag 1.13.2 → 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 +237 -535
- package/bin/uag-eval.js +99 -0
- package/bin/uag.js +116 -0
- package/lib/UAGFormInterface.d.ts +72 -2
- package/lib/UAGFormInterface.js +226 -57
- package/lib/UAGProjectInterface.d.ts +45 -5
- package/lib/UAGProjectInterface.js +187 -36
- package/lib/actionLogs.d.ts +24 -0
- package/lib/actionLogs.js +149 -0
- package/lib/actions/runTask.d.ts +43 -0
- package/lib/actions/runTask.js +324 -0
- package/lib/agent/anonymous.d.ts +34 -0
- package/lib/agent/anonymous.js +99 -0
- package/lib/agent/chat.d.ts +114 -0
- package/lib/agent/chat.js +475 -0
- package/lib/agent/engines.d.ts +17 -0
- package/lib/agent/engines.js +46 -0
- package/lib/agent/errors.d.ts +25 -0
- package/lib/agent/errors.js +132 -0
- package/lib/agent/index.d.ts +4 -0
- package/lib/agent/index.js +19 -0
- package/lib/agent/remote.d.ts +71 -0
- package/lib/agent/remote.js +252 -0
- package/lib/agent/requestId.d.ts +4 -0
- package/lib/agent/requestId.js +19 -0
- package/lib/agent/router.d.ts +61 -0
- package/lib/agent/router.js +307 -0
- package/lib/agent/stubs.d.ts +37 -0
- package/lib/agent/stubs.js +39 -0
- package/lib/agent/tools.d.ts +31 -0
- package/lib/agent/tools.js +76 -0
- package/lib/config.d.ts +97 -3
- package/lib/config.js +19 -0
- package/lib/database.d.ts +70 -0
- package/lib/database.js +212 -0
- package/lib/documents/convert.d.ts +38 -0
- package/lib/documents/convert.js +151 -0
- package/lib/documents/converters.d.ts +28 -0
- package/lib/documents/converters.js +149 -0
- package/lib/documents/detect.d.ts +31 -0
- package/lib/documents/detect.js +169 -0
- package/lib/documents/index.d.ts +9 -0
- package/lib/documents/index.js +43 -0
- package/lib/documents/mime.d.ts +39 -0
- package/lib/documents/mime.js +100 -0
- package/lib/documents/plugins.d.ts +18 -0
- package/lib/documents/plugins.js +65 -0
- package/lib/documents/readers.d.ts +19 -0
- package/lib/documents/readers.js +64 -0
- package/lib/documents/routes.d.ts +52 -0
- package/lib/documents/routes.js +99 -0
- package/lib/documents/types.d.ts +126 -0
- package/lib/documents/types.js +2 -0
- package/lib/documents/uagJs.d.ts +17 -0
- package/lib/documents/uagJs.js +157 -0
- package/lib/formflow.d.ts +47 -0
- package/lib/formflow.js +28 -0
- package/lib/identity.d.ts +53 -0
- package/lib/identity.js +107 -0
- package/lib/index.d.ts +98 -1
- package/lib/index.js +382 -8
- package/lib/markdown.d.ts +1 -0
- package/lib/markdown.js +166 -0
- package/lib/modules/api.d.ts +25 -0
- package/lib/modules/api.js +98 -0
- package/lib/modules/index.d.ts +4 -0
- package/lib/modules/index.js +24 -0
- package/lib/modules/routes.d.ts +14 -0
- package/lib/modules/routes.js +21 -0
- package/lib/modules/types.d.ts +68 -0
- package/lib/modules/types.js +5 -0
- package/lib/pluginFiles.d.ts +28 -0
- package/lib/pluginFiles.js +101 -0
- package/lib/plugins.d.ts +17 -0
- package/lib/plugins.js +93 -0
- package/lib/richText.d.ts +20 -0
- package/lib/richText.js +99 -0
- package/lib/router.d.ts +9 -0
- package/lib/router.js +34 -25
- package/lib/runner.d.ts +149 -0
- package/lib/runner.js +353 -0
- package/lib/runs.d.ts +27 -0
- package/lib/runs.js +13 -0
- package/lib/template.js +7 -9
- package/lib/templates/uagComponentNotFound.md +5 -5
- package/lib/testing.d.ts +74 -0
- package/lib/testing.js +180 -0
- package/lib/tools/SchemaBuilder.d.ts +3 -3
- package/lib/tools/SchemaBuilder.js +4 -4
- package/lib/tools/agentDecide.d.ts +8 -0
- package/lib/tools/agentDecide.js +57 -0
- package/lib/tools/agentProvide.d.ts +8 -0
- package/lib/tools/agentProvide.js +97 -0
- package/lib/tools/collectData.js +3 -2
- package/lib/tools/confirmSubmission.js +1 -1
- package/lib/tools/fetchExternalData.js +8 -17
- package/lib/tools/findSubmission.js +62 -56
- package/lib/tools/getFieldInfo.js +1 -1
- package/lib/tools/getFormFields.js +1 -1
- package/lib/tools/getForms.js +1 -1
- package/lib/tools/index.d.ts +4 -1
- package/lib/tools/index.js +21 -14
- package/lib/tools/readDocument.d.ts +17 -0
- package/lib/tools/readDocument.js +99 -0
- package/lib/tools/submissionUpdate.js +4 -3
- package/lib/tools/submitForm.js +1 -1
- package/lib/tools/utils.d.ts +40 -0
- package/lib/tools/utils.js +16 -1
- package/lib/version.d.ts +2 -0
- package/lib/version.js +5 -0
- package/package.json +49 -19
- package/testing.d.ts +1 -0
- package/testing.js +2 -0
- package/lib/tools/agentProvideData.d.ts +0 -3
- package/lib/tools/agentProvideData.js +0 -62
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INTERNAL_ERROR = void 0;
|
|
4
|
+
exports.isAgentApiRequest = isAgentApiRequest;
|
|
5
|
+
exports.errorCode = errorCode;
|
|
6
|
+
exports.agentError = agentError;
|
|
7
|
+
exports.requestIdOf = requestIdOf;
|
|
8
|
+
exports.errorBody = errorBody;
|
|
9
|
+
exports.agentErrorShape = agentErrorShape;
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const protocol_1 = require("@formio/uag-sdk/protocol");
|
|
12
|
+
const formflow_1 = require("../formflow");
|
|
13
|
+
/**
|
|
14
|
+
* The Agent API's one error shape. Every error a form's agent operation or
|
|
15
|
+
* `/agent/capabilities` answers is JSON
|
|
16
|
+
*
|
|
17
|
+
* { "requestId": "...", "error": "a message for people", "code": "a_stable_code", ... }
|
|
18
|
+
*
|
|
19
|
+
* with the same id in `X-Request-Id`, whatever refused it: the UAG, the
|
|
20
|
+
* project's authentication, the portal's team access, the body parser.
|
|
21
|
+
* Clients branch on `code`; `error` is for people. A 500 never carries details:
|
|
22
|
+
* those are in the UAG's log under the request id.
|
|
23
|
+
*/
|
|
24
|
+
/** A form's agent operations, and the capabilities. */
|
|
25
|
+
const OPERATION = /\/agent\/(extract|decide|generate|chat)\/?$/i;
|
|
26
|
+
const CAPABILITIES = /^\/agent\/capabilities\/?$/i;
|
|
27
|
+
function isAgentApiRequest(req) {
|
|
28
|
+
return (req.method === 'POST' && OPERATION.test(req.path)) || (req.method === 'GET' && CAPABILITIES.test(req.path));
|
|
29
|
+
}
|
|
30
|
+
/** The code for a status, when the error names none. */
|
|
31
|
+
function errorCode(status) {
|
|
32
|
+
switch (status) {
|
|
33
|
+
case 400: return 'invalid_request';
|
|
34
|
+
case 401: return 'unauthorized';
|
|
35
|
+
case 403: return 'forbidden';
|
|
36
|
+
case 404: return 'not_found';
|
|
37
|
+
case 409: return 'conflict';
|
|
38
|
+
case 413: return 'payload_too_large';
|
|
39
|
+
case 429: return 'rate_limited';
|
|
40
|
+
case 502: return 'engines_failed';
|
|
41
|
+
case 503: return 'unavailable';
|
|
42
|
+
case 504: return 'timeout';
|
|
43
|
+
default: return status >= 500 ? 'internal_error' : 'invalid_request';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.INTERNAL_ERROR = 'The request failed. See the UAG logs for this request id.';
|
|
47
|
+
/**
|
|
48
|
+
* An error as the Agent API answers it: its status, and the body. Errors with
|
|
49
|
+
* a status (FormflowError) keep their message and code; anything else is a 500
|
|
50
|
+
* with no detail. Agent operations and module routes share it.
|
|
51
|
+
*/
|
|
52
|
+
function agentError(requestId, err) {
|
|
53
|
+
const known = err instanceof formflow_1.FormflowError;
|
|
54
|
+
const status = known ? err.status : 500;
|
|
55
|
+
return {
|
|
56
|
+
status,
|
|
57
|
+
body: {
|
|
58
|
+
requestId,
|
|
59
|
+
error: status === 500 ? exports.INTERNAL_ERROR : String(err?.message || exports.INTERNAL_ERROR),
|
|
60
|
+
code: (known && err.code) || errorCode(status),
|
|
61
|
+
...(err?.edition && { edition: err.edition })
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** The request's id, assigned once: the caller's, when it is a short token, or a new one. */
|
|
66
|
+
function requestIdOf(req, res) {
|
|
67
|
+
const request = req;
|
|
68
|
+
if (!request.requestId) {
|
|
69
|
+
request.requestId = (0, protocol_1.safeRequestId)(req.get('x-request-id')) || (0, crypto_1.randomUUID)();
|
|
70
|
+
}
|
|
71
|
+
if (!res.headersSent)
|
|
72
|
+
res.set('X-Request-Id', request.requestId);
|
|
73
|
+
return request.requestId;
|
|
74
|
+
}
|
|
75
|
+
/** The error body for a status and whatever a handler sent: an object, JSON text, plain text or nothing. */
|
|
76
|
+
function errorBody(requestId, status, sent) {
|
|
77
|
+
let value = sent;
|
|
78
|
+
if (Buffer.isBuffer(value))
|
|
79
|
+
value = value.toString('utf8');
|
|
80
|
+
if (typeof value === 'string') {
|
|
81
|
+
try {
|
|
82
|
+
value = JSON.parse(value);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
value = { error: value };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const given = value && typeof value === 'object' ? value : {};
|
|
89
|
+
const text = [given.error, given.detail, given.message].find((part) => typeof part === 'string' && part.trim());
|
|
90
|
+
// A 500's own message may carry internals: only the UAG's fixed sentence goes out.
|
|
91
|
+
const error = status >= 500 && status !== 502 && status !== 503 && status !== 504 ? exports.INTERNAL_ERROR : (text || defaultMessage(status));
|
|
92
|
+
return {
|
|
93
|
+
requestId,
|
|
94
|
+
error,
|
|
95
|
+
code: typeof given.code === 'string' && given.code ? given.code : errorCode(status),
|
|
96
|
+
...(typeof given.edition === 'string' && { edition: given.edition })
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function defaultMessage(status) {
|
|
100
|
+
switch (status) {
|
|
101
|
+
case 401: return 'Unauthorized.';
|
|
102
|
+
case 403: return 'Forbidden.';
|
|
103
|
+
case 404: return 'Not found.';
|
|
104
|
+
case 413: return 'The request body is too large.';
|
|
105
|
+
case 429: return 'Too many requests. Try again shortly.';
|
|
106
|
+
default: return 'The request could not be completed.';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Mounted ahead of every route: for an Agent API request, assigns the request
|
|
111
|
+
* id and rewrites any error response into the one shape, so refusals from
|
|
112
|
+
* middleware the UAG does not own (the project's authentication answers plain
|
|
113
|
+
* text, the portal's team access an empty 401) look like every other.
|
|
114
|
+
*/
|
|
115
|
+
function agentErrorShape() {
|
|
116
|
+
return (req, res, next) => {
|
|
117
|
+
if (!isAgentApiRequest(req))
|
|
118
|
+
return next();
|
|
119
|
+
const requestId = requestIdOf(req, res);
|
|
120
|
+
const send = res.send.bind(res);
|
|
121
|
+
let shaped = false;
|
|
122
|
+
res.send = ((body) => {
|
|
123
|
+
// An event stream writes its own error events; only whole responses are shaped.
|
|
124
|
+
if (shaped || res.statusCode < 400)
|
|
125
|
+
return send(body);
|
|
126
|
+
shaped = true;
|
|
127
|
+
res.type('application/json');
|
|
128
|
+
return send(JSON.stringify(errorBody(requestId, res.statusCode, body)));
|
|
129
|
+
});
|
|
130
|
+
next();
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { StubAgentEngine, StubDecisionEngine, stubGenerateRequests } from './stubs';
|
|
2
|
+
export { BUILT_IN_ENGINES, UAG_ROOT, loadUAGEngines } from './engines';
|
|
3
|
+
export { AgentRouter, FormAgentRouter, agentCredentials, agentNotFound } from './router';
|
|
4
|
+
export { RemoteEngine, MIN_KEY_LENGTH } from './remote';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MIN_KEY_LENGTH = exports.RemoteEngine = exports.agentNotFound = exports.agentCredentials = exports.FormAgentRouter = exports.AgentRouter = exports.loadUAGEngines = exports.UAG_ROOT = exports.BUILT_IN_ENGINES = exports.stubGenerateRequests = exports.StubDecisionEngine = exports.StubAgentEngine = void 0;
|
|
4
|
+
var stubs_1 = require("./stubs");
|
|
5
|
+
Object.defineProperty(exports, "StubAgentEngine", { enumerable: true, get: function () { return stubs_1.StubAgentEngine; } });
|
|
6
|
+
Object.defineProperty(exports, "StubDecisionEngine", { enumerable: true, get: function () { return stubs_1.StubDecisionEngine; } });
|
|
7
|
+
Object.defineProperty(exports, "stubGenerateRequests", { enumerable: true, get: function () { return stubs_1.stubGenerateRequests; } });
|
|
8
|
+
var engines_1 = require("./engines");
|
|
9
|
+
Object.defineProperty(exports, "BUILT_IN_ENGINES", { enumerable: true, get: function () { return engines_1.BUILT_IN_ENGINES; } });
|
|
10
|
+
Object.defineProperty(exports, "UAG_ROOT", { enumerable: true, get: function () { return engines_1.UAG_ROOT; } });
|
|
11
|
+
Object.defineProperty(exports, "loadUAGEngines", { enumerable: true, get: function () { return engines_1.loadUAGEngines; } });
|
|
12
|
+
var router_1 = require("./router");
|
|
13
|
+
Object.defineProperty(exports, "AgentRouter", { enumerable: true, get: function () { return router_1.AgentRouter; } });
|
|
14
|
+
Object.defineProperty(exports, "FormAgentRouter", { enumerable: true, get: function () { return router_1.FormAgentRouter; } });
|
|
15
|
+
Object.defineProperty(exports, "agentCredentials", { enumerable: true, get: function () { return router_1.agentCredentials; } });
|
|
16
|
+
Object.defineProperty(exports, "agentNotFound", { enumerable: true, get: function () { return router_1.agentNotFound; } });
|
|
17
|
+
var remote_1 = require("./remote");
|
|
18
|
+
Object.defineProperty(exports, "RemoteEngine", { enumerable: true, get: function () { return remote_1.RemoteEngine; } });
|
|
19
|
+
Object.defineProperty(exports, "MIN_KEY_LENGTH", { enumerable: true, get: function () { return remote_1.MIN_KEY_LENGTH; } });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { RemoteEngineDescription } from '@formio/uag-sdk/protocol';
|
|
2
|
+
import type { AgentEngine, DecisionEngine } from '../formflow';
|
|
3
|
+
/**
|
|
4
|
+
* The `remote` engine type: an engine hosted by an engine server
|
|
5
|
+
* (`formio/uag-engines`), reached over the remote engine protocol. Any
|
|
6
|
+
* engine package can run there, so model credentials and model traffic stay in
|
|
7
|
+
* that container.
|
|
8
|
+
*
|
|
9
|
+
* UAG_ENGINES=jev,claude:remote
|
|
10
|
+
* UAG_ENGINE_CLAUDE_URL=http://engines:3300
|
|
11
|
+
* UAG_ENGINE_CLAUDE_KEY=<the engine server key, at least 32 characters>
|
|
12
|
+
*
|
|
13
|
+
* Every request carries the engine server key, and never a Form.io credential.
|
|
14
|
+
* Research extracts also carry a scoped token, which the engine server uses for
|
|
15
|
+
* its own MCP connection back to the UAG: it allows only the task's read-only
|
|
16
|
+
* tools, with the caller's identity. Each call also carries the Agent API
|
|
17
|
+
* request id, when it is a short token, for logs on either side.
|
|
18
|
+
*/
|
|
19
|
+
/** The shortest engine server key accepted. */
|
|
20
|
+
export declare const MIN_KEY_LENGTH = 32;
|
|
21
|
+
type Fetch = typeof fetch;
|
|
22
|
+
type Connection = {
|
|
23
|
+
/** The instance name, for errors. */
|
|
24
|
+
name: string;
|
|
25
|
+
baseUrl: string;
|
|
26
|
+
key: string;
|
|
27
|
+
fetch: Fetch;
|
|
28
|
+
/** The most PDF bytes one generate request may send: documents.maxTotalBytes. */
|
|
29
|
+
maxDocumentBytes: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Describe the remote engine, retrying while the engine server is unreachable
|
|
33
|
+
* or failing, with the backoff used to wait for a Form.io project. A refused
|
|
34
|
+
* key, or a server that does not host the engine, stops startup at once.
|
|
35
|
+
*/
|
|
36
|
+
export declare function describeRemote(connection: Connection, engine: string, { retries, retryDelay, timeout }?: {
|
|
37
|
+
retries?: number;
|
|
38
|
+
retryDelay?: number;
|
|
39
|
+
timeout?: number;
|
|
40
|
+
}): Promise<RemoteEngineDescription>;
|
|
41
|
+
/** An engine object for a described remote engine. */
|
|
42
|
+
export declare function remoteEngine(connection: Connection, remoteName: string, described: RemoteEngineDescription): DecisionEngine | AgentEngine;
|
|
43
|
+
export declare const RemoteEngine: Readonly<{
|
|
44
|
+
type: "engine";
|
|
45
|
+
name: string;
|
|
46
|
+
title: string;
|
|
47
|
+
kinds: ("agent" | "decision")[];
|
|
48
|
+
apiVersion: 1;
|
|
49
|
+
description: string;
|
|
50
|
+
settings: {
|
|
51
|
+
url: {
|
|
52
|
+
required: true;
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
key: {
|
|
56
|
+
required: true;
|
|
57
|
+
secret: true;
|
|
58
|
+
description: string;
|
|
59
|
+
};
|
|
60
|
+
engine: {
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
timeout: {
|
|
64
|
+
type: "number";
|
|
65
|
+
default: number;
|
|
66
|
+
description: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
create({ name, settings, options, config }: import("@formio/uag-sdk").EngineCreateContext<any>): Promise<import("@formio/appserver-core/lib/formflow").AgentEngine | import("@formio/appserver-core/lib/formflow").DecisionEngine>;
|
|
70
|
+
}>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RemoteEngine = exports.MIN_KEY_LENGTH = void 0;
|
|
4
|
+
exports.describeRemote = describeRemote;
|
|
5
|
+
exports.remoteEngine = remoteEngine;
|
|
6
|
+
const uag_sdk_1 = require("@formio/uag-sdk");
|
|
7
|
+
const protocol_1 = require("@formio/uag-sdk/protocol");
|
|
8
|
+
const requestId_1 = require("./requestId");
|
|
9
|
+
const debug = require('debug')('formio:uag:remote');
|
|
10
|
+
/**
|
|
11
|
+
* The `remote` engine type: an engine hosted by an engine server
|
|
12
|
+
* (`formio/uag-engines`), reached over the remote engine protocol. Any
|
|
13
|
+
* engine package can run there, so model credentials and model traffic stay in
|
|
14
|
+
* that container.
|
|
15
|
+
*
|
|
16
|
+
* UAG_ENGINES=jev,claude:remote
|
|
17
|
+
* UAG_ENGINE_CLAUDE_URL=http://engines:3300
|
|
18
|
+
* UAG_ENGINE_CLAUDE_KEY=<the engine server key, at least 32 characters>
|
|
19
|
+
*
|
|
20
|
+
* Every request carries the engine server key, and never a Form.io credential.
|
|
21
|
+
* Research extracts also carry a scoped token, which the engine server uses for
|
|
22
|
+
* its own MCP connection back to the UAG: it allows only the task's read-only
|
|
23
|
+
* tools, with the caller's identity. Each call also carries the Agent API
|
|
24
|
+
* request id, when it is a short token, for logs on either side.
|
|
25
|
+
*/
|
|
26
|
+
/** The shortest engine server key accepted. */
|
|
27
|
+
exports.MIN_KEY_LENGTH = 32;
|
|
28
|
+
const DEFAULT_RETRIES = 5;
|
|
29
|
+
const DEFAULT_RETRY_DELAY = 5000;
|
|
30
|
+
const MAX_RETRY_DELAY = 60000;
|
|
31
|
+
class StartupError extends uag_sdk_1.EngineConfigError {
|
|
32
|
+
/** Whether trying again may help: the server was unreachable or failing. */
|
|
33
|
+
constructor(message, retry) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.retry = retry;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function readJson(response) {
|
|
39
|
+
try {
|
|
40
|
+
return await response.json();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Describe the remote engine, retrying while the engine server is unreachable
|
|
48
|
+
* or failing, with the backoff used to wait for a Form.io project. A refused
|
|
49
|
+
* key, or a server that does not host the engine, stops startup at once.
|
|
50
|
+
*/
|
|
51
|
+
async function describeRemote(connection, engine, { retries = DEFAULT_RETRIES, retryDelay = DEFAULT_RETRY_DELAY, timeout = 10000 } = {}) {
|
|
52
|
+
const where = `the engine server at ${connection.baseUrl}`;
|
|
53
|
+
const attempts = Math.max(1, retries);
|
|
54
|
+
for (let attempt = 1;; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
return await describeOnce(connection, engine, where, timeout);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (!(err instanceof StartupError && err.retry) || attempt >= attempts) {
|
|
60
|
+
throw new uag_sdk_1.EngineConfigError(`The "${connection.name}" engine could not start: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
const delay = Math.min(retryDelay * Math.pow(2, attempt - 1), MAX_RETRY_DELAY);
|
|
63
|
+
debug(`${err.message} Retrying in ${delay}ms (attempt ${attempt} of ${attempts}).`);
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function describeOnce(connection, engine, where, timeout) {
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await connection.fetch(`${connection.baseUrl}/v1/engines`, {
|
|
72
|
+
headers: { Accept: 'application/json', Authorization: `Bearer ${connection.key}` },
|
|
73
|
+
signal: AbortSignal.timeout(timeout),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw new StartupError(`${where} is unreachable (${err?.cause?.code || err?.name || 'no response'}).`, true);
|
|
78
|
+
}
|
|
79
|
+
if (response.status === 401 || response.status === 403) {
|
|
80
|
+
throw new StartupError(`${where} refused the engine server key (HTTP ${response.status}).`, false);
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new StartupError(`${where} answered HTTP ${response.status}.`, response.status >= 500);
|
|
84
|
+
}
|
|
85
|
+
let engines;
|
|
86
|
+
try {
|
|
87
|
+
engines = (0, protocol_1.checkDescription)(await readJson(response));
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
throw new StartupError(`${where} did not answer as an engine server: ${err.message}`, false);
|
|
91
|
+
}
|
|
92
|
+
const described = engines.find((candidate) => candidate.name === engine);
|
|
93
|
+
if (!described) {
|
|
94
|
+
throw new StartupError(`${where} does not host an engine named "${engine}"` +
|
|
95
|
+
(engines.length ? ` (it hosts ${engines.map((e) => `"${e.name}"`).join(', ')}).` : ' (it hosts none).'), false);
|
|
96
|
+
}
|
|
97
|
+
return described;
|
|
98
|
+
}
|
|
99
|
+
/** One protocol call. Failures are engine failures, safe to show unless the server says otherwise. */
|
|
100
|
+
async function call(connection, path, body, signal, scopedToken) {
|
|
101
|
+
signal?.throwIfAborted();
|
|
102
|
+
const requestId = (0, protocol_1.safeRequestId)((0, requestId_1.currentRequestId)());
|
|
103
|
+
const headers = {
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
Authorization: `Bearer ${connection.key}`,
|
|
107
|
+
...(scopedToken && { [protocol_1.SCOPED_TOKEN_HEADER]: scopedToken }),
|
|
108
|
+
...(requestId && { [protocol_1.REQUEST_ID_HEADER]: requestId }),
|
|
109
|
+
};
|
|
110
|
+
let response;
|
|
111
|
+
try {
|
|
112
|
+
response = await connection.fetch(`${connection.baseUrl}${path}`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers,
|
|
115
|
+
body: JSON.stringify({ protocol: protocol_1.PROTOCOL_VERSION, ...body }),
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (signal?.aborted)
|
|
121
|
+
throw signal.reason ?? err;
|
|
122
|
+
// Callers see the failure; only the UAG's log names where the engine server is.
|
|
123
|
+
debug(`The engine server at ${connection.baseUrl} could not be reached: ${err?.cause?.code || err?.name || 'no response'}`);
|
|
124
|
+
throw new uag_sdk_1.EngineError('The engine server could not be reached.');
|
|
125
|
+
}
|
|
126
|
+
const json = await readJson(response);
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
if (json?.expose === true && typeof json.error === 'string') {
|
|
129
|
+
throw new uag_sdk_1.EngineError(json.error);
|
|
130
|
+
}
|
|
131
|
+
debug(`The engine server answered HTTP ${response.status}: ${json?.error || '(no message)'}`);
|
|
132
|
+
throw new uag_sdk_1.EngineError(`The engine server answered HTTP ${response.status}.`);
|
|
133
|
+
}
|
|
134
|
+
return json;
|
|
135
|
+
}
|
|
136
|
+
/** Validate a response, reporting a malformed one as an engine failure. */
|
|
137
|
+
function valid(check, body) {
|
|
138
|
+
try {
|
|
139
|
+
return check(body);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
throw new uag_sdk_1.EngineError(`The engine server's answer did not follow the remote engine protocol: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** An engine object for a described remote engine. */
|
|
146
|
+
function remoteEngine(connection, remoteName, described) {
|
|
147
|
+
const path = `/v1/engines/${encodeURIComponent(remoteName)}`;
|
|
148
|
+
if (described.kind === 'decision') {
|
|
149
|
+
return {
|
|
150
|
+
name: connection.name,
|
|
151
|
+
kind: 'decision',
|
|
152
|
+
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
153
|
+
model: described.model,
|
|
154
|
+
maxOptions: described.maxOptions,
|
|
155
|
+
async decide({ state, questions, signal }) {
|
|
156
|
+
const response = valid(protocol_1.checkDecisionResponse, await call(connection, `${path}/decide`, { state, questions }, signal));
|
|
157
|
+
return { ...response, model: response.model || described.model };
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const agent = {
|
|
162
|
+
name: connection.name,
|
|
163
|
+
kind: 'agent',
|
|
164
|
+
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
165
|
+
model: described.model,
|
|
166
|
+
toolUse: described.toolUse === true,
|
|
167
|
+
accepts: described.accepts || ['text'],
|
|
168
|
+
async extract(request) {
|
|
169
|
+
const research = !!request.tools?.length;
|
|
170
|
+
// The engine server researches through its own MCP connection to the
|
|
171
|
+
// UAG, which needs a scoped token; without one (no JWT_SECRET), no call.
|
|
172
|
+
if (research && !request.scopedToken) {
|
|
173
|
+
throw new uag_sdk_1.EngineError('The remote engine cannot research without a scoped token. Set JWT_SECRET on the UAG.');
|
|
174
|
+
}
|
|
175
|
+
const body = {
|
|
176
|
+
instructions: request.instructions,
|
|
177
|
+
state: request.state,
|
|
178
|
+
fields: request.fields,
|
|
179
|
+
schema: request.schema,
|
|
180
|
+
...(research && { tools: request.tools, maxIterations: request.maxIterations || 10 }),
|
|
181
|
+
};
|
|
182
|
+
const response = valid(protocol_1.checkExtractResponse, await call(connection, `${path}/extract`, body, request.signal, research ? request.scopedToken : undefined));
|
|
183
|
+
return { ...response, model: response.model || described.model };
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
if (described.generate) {
|
|
187
|
+
agent.generate = async (request) => {
|
|
188
|
+
// Refused before anything is sent; the documents are never logged.
|
|
189
|
+
if ((0, protocol_1.pdfByteLength)(request.attachments) > connection.maxDocumentBytes) {
|
|
190
|
+
throw new uag_sdk_1.EngineError(`The attached PDFs exceed ${connection.maxDocumentBytes} bytes, the most one request to the engine server may carry.`);
|
|
191
|
+
}
|
|
192
|
+
const body = {
|
|
193
|
+
instructions: request.instructions,
|
|
194
|
+
...(request.system !== undefined && { system: request.system }),
|
|
195
|
+
attachments: (0, protocol_1.encodeAttachments)(request.attachments),
|
|
196
|
+
format: request.format,
|
|
197
|
+
...(request.maxTokens !== undefined && { maxTokens: request.maxTokens }),
|
|
198
|
+
};
|
|
199
|
+
const response = valid(protocol_1.checkGenerateResponse, await call(connection, `${path}/generate`, body, request.signal));
|
|
200
|
+
request.onProgress?.(response.text.length);
|
|
201
|
+
return { ...response, model: response.model || described.model };
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return agent;
|
|
205
|
+
}
|
|
206
|
+
exports.RemoteEngine = (0, uag_sdk_1.defineEngine)({
|
|
207
|
+
type: 'engine',
|
|
208
|
+
name: 'remote',
|
|
209
|
+
title: 'Remote engine (engine server)',
|
|
210
|
+
kinds: ['decision', 'agent'],
|
|
211
|
+
apiVersion: uag_sdk_1.ENGINE_API_VERSION,
|
|
212
|
+
description: 'An engine hosted by an engine server, over the remote engine protocol.',
|
|
213
|
+
settings: {
|
|
214
|
+
url: { required: true, description: 'The engine server, such as http://engines:3300.' },
|
|
215
|
+
key: { required: true, secret: true, description: `The engine server key, at least ${exports.MIN_KEY_LENGTH} characters.` },
|
|
216
|
+
engine: { description: 'The engine\'s name on the engine server. Default: the instance name.' },
|
|
217
|
+
timeout: { type: 'number', default: 10000, description: 'Milliseconds for each attempt to describe the engine at startup.' },
|
|
218
|
+
},
|
|
219
|
+
async create({ name, settings, options, config }) {
|
|
220
|
+
let url;
|
|
221
|
+
try {
|
|
222
|
+
url = new URL(settings.url);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
throw new uag_sdk_1.EngineConfigError(`The "${name}" engine's url setting is not a URL: ${JSON.stringify(settings.url)}.`);
|
|
226
|
+
}
|
|
227
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
228
|
+
throw new uag_sdk_1.EngineConfigError(`The "${name}" engine's url setting must be an http or https URL.`);
|
|
229
|
+
}
|
|
230
|
+
if (url.username || url.password) {
|
|
231
|
+
throw new uag_sdk_1.EngineConfigError(`The "${name}" engine's url setting must not include a user name or password: its key setting authenticates the UAG to the engine server.`);
|
|
232
|
+
}
|
|
233
|
+
if (String(settings.key).length < exports.MIN_KEY_LENGTH) {
|
|
234
|
+
throw new uag_sdk_1.EngineConfigError(`The "${name}" engine's key setting must be at least ${exports.MIN_KEY_LENGTH} characters, the same key as the engine server's UAG_ENGINE_SERVER_KEY.`);
|
|
235
|
+
}
|
|
236
|
+
const connection = {
|
|
237
|
+
name,
|
|
238
|
+
baseUrl: url.href.replace(/\/+$/, ''),
|
|
239
|
+
key: settings.key,
|
|
240
|
+
fetch: options.fetch || fetch,
|
|
241
|
+
maxDocumentBytes: Math.min(Number(config?.documents?.maxTotalBytes) || protocol_1.MAX_DOCUMENT_BYTES, protocol_1.MAX_DOCUMENT_BYTES),
|
|
242
|
+
};
|
|
243
|
+
const remoteName = settings.engine || name;
|
|
244
|
+
const described = await describeRemote(connection, remoteName, {
|
|
245
|
+
retries: config.retries,
|
|
246
|
+
retryDelay: config.retryDelay,
|
|
247
|
+
timeout: settings.timeout,
|
|
248
|
+
});
|
|
249
|
+
debug(`"${name}" is the ${described.kind} engine "${remoteName}" (${described.model}) at ${connection.baseUrl}.`);
|
|
250
|
+
return remoteEngine(connection, remoteName, described);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withRequestId = withRequestId;
|
|
4
|
+
exports.currentRequestId = currentRequestId;
|
|
5
|
+
const async_hooks_1 = require("async_hooks");
|
|
6
|
+
/**
|
|
7
|
+
* The Agent API request an operation runs for, so the `remote` engine type can
|
|
8
|
+
* send its id to the engine server: the engine contract carries no request id,
|
|
9
|
+
* and Formflow calls engines within the operation's async context.
|
|
10
|
+
*/
|
|
11
|
+
const current = new async_hooks_1.AsyncLocalStorage();
|
|
12
|
+
/** Run `fn` as part of the request `requestId`. */
|
|
13
|
+
function withRequestId(requestId, fn) {
|
|
14
|
+
return current.run(requestId, fn);
|
|
15
|
+
}
|
|
16
|
+
/** The id of the request being served, if any. */
|
|
17
|
+
function currentRequestId() {
|
|
18
|
+
return current.getStore();
|
|
19
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NextFunction, Request, RequestHandler, Response, Router } from "express";
|
|
2
|
+
import { SubmissionRequest } from "@formio/appserver";
|
|
3
|
+
import { FormflowService } from "../formflow";
|
|
4
|
+
import type { UAGProjectInterface } from "../UAGProjectInterface";
|
|
5
|
+
/**
|
|
6
|
+
* The Agent API: HTTP adapters onto Agentic Formflow in @formio/appserver-core.
|
|
7
|
+
* Each endpoint reads its request, calls the service, and writes JSON (or, for
|
|
8
|
+
* extract, a Server-Sent Events stream). Errors answer with the status the
|
|
9
|
+
* service gives.
|
|
10
|
+
*
|
|
11
|
+
* The operations belong to a form, `{form}/agent/{operation}`, as its
|
|
12
|
+
* submissions do: the form comes from the URL and the caller from the
|
|
13
|
+
* project's own authentication. Capabilities describe the deployment, at
|
|
14
|
+
* `/agent/capabilities`.
|
|
15
|
+
*/
|
|
16
|
+
type AgentRequest = SubmissionRequest & {
|
|
17
|
+
requestId: string;
|
|
18
|
+
};
|
|
19
|
+
/** The largest request body an agent operation takes by default, in bytes. */
|
|
20
|
+
export declare const AGENT_BODY_LIMIT: number;
|
|
21
|
+
/**
|
|
22
|
+
* The largest request body the project's agent operations take: `agentApi.bodyLimit`,
|
|
23
|
+
* then AGENT_BODY_LIMIT, then 5 MB. A document sent inline (a base64 file a task reads)
|
|
24
|
+
* may need more; the server's own MAX_BODY_SIZE (16 MB) still applies first.
|
|
25
|
+
*/
|
|
26
|
+
export declare function agentBodyLimit(project: UAGProjectInterface): number;
|
|
27
|
+
type Handler = (req: AgentRequest, res: Response, service: FormflowService) => Promise<void>;
|
|
28
|
+
/** The parts every Agent API endpoint shares: the request id, and errors answered as JSON (or as an event mid-stream). */
|
|
29
|
+
export declare function agentEndpoints(project: UAGProjectInterface): {
|
|
30
|
+
json: (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
|
|
31
|
+
requestId: (req: any, res: Response, next: NextFunction) => void;
|
|
32
|
+
handle: (operation: string, fn: Handler) => (req: Request, res: Response) => Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* `GET /agent/capabilities`, behind `authenticate`. Only this path is claimed:
|
|
36
|
+
* any other `/agent...` path (a project form at `agent`, for one) falls through
|
|
37
|
+
* to the routes after this router.
|
|
38
|
+
*/
|
|
39
|
+
export declare function AgentRouter(project: UAGProjectInterface, authenticate: RequestHandler): Router;
|
|
40
|
+
/**
|
|
41
|
+
* A form's agent operation for a path that names no form of the project (the
|
|
42
|
+
* old root operations, a deleted form): answered 404 here, so it is never
|
|
43
|
+
* passed through to Form.io. A form that exists goes on to its own routes,
|
|
44
|
+
* which answer 404 themselves when it is not tagged `uag`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function agentNotFound(project: UAGProjectInterface): RequestHandler;
|
|
47
|
+
/**
|
|
48
|
+
* Who may call a form's agent operations, on top of the project's own
|
|
49
|
+
* authentication (Form.io user tokens, portal remote tokens, server keys):
|
|
50
|
+
* scoped tokens never (they are for /mcp), server keys only from servers, and
|
|
51
|
+
* UAG bearer tokens, verified here. A caller with no credential gets through
|
|
52
|
+
* only where the form allows anonymous use (see anonymousGate).
|
|
53
|
+
*/
|
|
54
|
+
export declare function agentCredentials(project: UAGProjectInterface): RequestHandler;
|
|
55
|
+
/**
|
|
56
|
+
* A form's agent operations, `POST /agent/extract|decide|generate|chat`, mounted on
|
|
57
|
+
* the form's own routes (see UAGFormInterface.router), so `req.form` is the
|
|
58
|
+
* form of the URL.
|
|
59
|
+
*/
|
|
60
|
+
export declare function FormAgentRouter(project: UAGProjectInterface, credentials: RequestHandler): Router;
|
|
61
|
+
export {};
|