@formio/uag-jev 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 +74 -2
- package/index.js +154 -0
- package/package.json +42 -4
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2025 Form.io
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
5
|
+
this software and associated documentation files (the "Software"), to deal in the
|
|
6
|
+
Software without restriction, including without limitation the rights to use,
|
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
8
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
15
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
16
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
17
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
19
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @formio/uag-jev
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
|
|
5
|
+
The TypeSafe **Jev** decision engine for the [UAG Agent API](https://github.com/formio/uag/blob/v2.0.0-rc.8/README.md#agent-api). It answers a task's closed-set fields (radio, select, checkbox, select boxes, survey and small resource selects) in one request, typically in 70–500 ms, with a confidence for each answer.
|
|
6
|
+
|
|
7
|
+
It is included in the `formio/uag` Docker image. Enable it by naming it, and set its key:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
UAG_ENGINES=jev
|
|
11
|
+
TYPESAFE_API_KEY=...
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Decisions are Agentic Formflow, so on a self-hosted project the UAG also needs a `LICENSE_KEY` that includes the UAG (see [License](https://github.com/formio/uag/blob/v2.0.0-rc.8/README.md#license)); a project on Form.io's hosted service needs none.
|
|
15
|
+
|
|
16
|
+
## Settings
|
|
17
|
+
|
|
18
|
+
| Setting | Variables | Default |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `apiKey` | `UAG_ENGINE_<NAME>_API_KEY`, `TYPESAFE_API_KEY` | required (secret) |
|
|
21
|
+
| `model` | `UAG_ENGINE_<NAME>_MODEL`, `TYPESAFE_MODEL` | `jev-1.13.0` |
|
|
22
|
+
| `baseURL` | `UAG_ENGINE_<NAME>_BASE_URL`, `TYPESAFE_BASE_URL` | the SDK's default |
|
|
23
|
+
|
|
24
|
+
`<NAME>` is the instance name, upper-cased with `-` as `_`. An instance's own variable wins over the shared one.
|
|
25
|
+
|
|
26
|
+
The SDK's request timeout follows the instance's routing timeout (`routing.timeouts.<name>`, else `agentApi.timeout`, else 30 seconds; always 30 seconds on an engine server), so its retries of rate-limit and overload responses never outlast it. After the timeout, the UAG falls back to the next engine.
|
|
27
|
+
|
|
28
|
+
## Instances
|
|
29
|
+
|
|
30
|
+
Run a second Jev model next to the first, for example to compare a new version on live traffic before switching:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
UAG_ENGINES=jev,next:jev
|
|
34
|
+
TYPESAFE_API_KEY=...
|
|
35
|
+
UAG_ENGINE_NEXT_MODEL=jev-1.14.0
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Routing lists name the instances: `routing.decision: ["next", "jev"]`.
|
|
39
|
+
|
|
40
|
+
## Pin the model
|
|
41
|
+
|
|
42
|
+
Behavior differs between Jev versions. Keep `model` at a dated version, and run `uag-eval` on your own labeled examples before changing it or any threshold. `uag-eval --engine jev` works from a checkout of the UAG repository or inside the `formio/uag` image:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
docker run --rm --entrypoint node -v "$PWD:/data" -e TYPESAFE_API_KEY -e LICENSE_KEY formio/uag:rc \
|
|
46
|
+
--no-node-snapshot bin/uag-eval.js --form /data/claim.json --examples /data/claim-examples.json \
|
|
47
|
+
--engine jev --project <your project URL>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The license must allow that project URL.
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
The UAG builds the questions and interprets the answers; this package only maps them to and from `@typesafe-ai/sdk`. Answers outside the options asked are dropped, and scores are kept within their levels. Jev's [confidence](https://docs.typesafe.ai/confidence) is already normalized against an even split, which is what the UAG's confidence threshold expects, so it is passed on unchanged.
|
|
55
|
+
|
|
56
|
+
**Errors:** a refused API key (401, 403), a rate limit (429) and a TypeSafe server failure (5xx) reach API callers as that, with no response body; any other failure is reported only as "the engine failed". Retries never wait longer than the request timeout for a `Retry-After`.
|
|
57
|
+
|
|
58
|
+
**Data residency:** Jev is a hosted service. Each request sends TypeSafe:
|
|
59
|
+
- the task's criteria and today's date;
|
|
60
|
+
- the values at its `uagContext` paths;
|
|
61
|
+
- its input fields, or, for a task with no input, the whole submission except the fields being decided.
|
|
62
|
+
|
|
63
|
+
Deployments that cannot send data there should not enable this engine; decision fields then fall back to an agent engine.
|
|
64
|
+
|
|
65
|
+
**Logging:** leave `TYPESAFE_LOG_LEVEL` unset (or `warn`) in production. At `debug`, the TypeSafe SDK writes each request and response body, which hold submission data, to the console.
|
|
66
|
+
|
|
67
|
+
## Using it in your own server
|
|
68
|
+
|
|
69
|
+
Name it in configuration instead of `UAG_ENGINES`, with options that take precedence over the environment:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const server = new UAGServer({ engines: [{ name: 'jev', type: 'jev', options: { model: 'jev-1.13.0' } }] });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
License: MIT.
|
package/index.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeSafe Jev decision engine for the UAG Agent API.
|
|
3
|
+
*
|
|
4
|
+
* UAG_ENGINES=jev TYPESAFE_API_KEY=...
|
|
5
|
+
* UAG_ENGINES=jev,next:jev UAG_ENGINE_NEXT_MODEL=jev-1.14.0
|
|
6
|
+
*
|
|
7
|
+
* The UAG compiles decision fields into Choice, Score and Noul questions and
|
|
8
|
+
* sends them all in one call; this engine only maps them to and from the SDK.
|
|
9
|
+
* The routing timeout aborts the request through its signal; without one (as
|
|
10
|
+
* in `uag-eval`), SDK retries wait at most the timeout for a `Retry-After`.
|
|
11
|
+
*
|
|
12
|
+
* Jev's `confidence` is already normalized as the contract asks: for a Choice,
|
|
13
|
+
* (p_max - 1/n) / (1 - 1/n); for a Score, how close the probability sits to the
|
|
14
|
+
* likeliest level, floored at 0 (https://docs.typesafe.ai/confidence).
|
|
15
|
+
*/
|
|
16
|
+
const { defineEngine, EngineError, ENGINE_API_VERSION } = require('@formio/uag-sdk');
|
|
17
|
+
|
|
18
|
+
/** Pin a dated version, never an alias, and re-run `uag-eval` before changing it. */
|
|
19
|
+
const DEFAULT_MODEL = 'jev-1.13.0';
|
|
20
|
+
/** Jev accepts up to 255 options per Choice question. */
|
|
21
|
+
const MAX_OPTIONS = 255;
|
|
22
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
23
|
+
|
|
24
|
+
/** Map one UAG decision question to the SDK's question shape. */
|
|
25
|
+
function toSdkQuestion(question) {
|
|
26
|
+
switch (question.type) {
|
|
27
|
+
case 'choice':
|
|
28
|
+
return { type: 'choice', instructions: question.instructions, criteria: question.options };
|
|
29
|
+
case 'score':
|
|
30
|
+
return { type: 'score', instructions: question.instructions, criteria: question.levels };
|
|
31
|
+
case 'noul':
|
|
32
|
+
return {
|
|
33
|
+
type: 'noul',
|
|
34
|
+
instructions: question.instructions,
|
|
35
|
+
...(question.criteria && { criteria: question.criteria })
|
|
36
|
+
};
|
|
37
|
+
default:
|
|
38
|
+
throw new EngineError(`Unsupported question type "${question.type}".`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Map one SDK answer to the UAG's answer shape, for the question asked: a
|
|
44
|
+
* choice that is not one of its options is dropped, and a score is kept within
|
|
45
|
+
* its levels.
|
|
46
|
+
*/
|
|
47
|
+
function fromSdkAnswer(answer, question) {
|
|
48
|
+
if (!answer || answer.type !== question.type) return undefined;
|
|
49
|
+
switch (answer.type) {
|
|
50
|
+
case 'choice':
|
|
51
|
+
if (typeof answer.choice !== 'string' || !Object.prototype.hasOwnProperty.call(question.options, answer.choice)) return undefined;
|
|
52
|
+
return { type: 'choice', choice: answer.choice, probabilities: { ...answer.probabilities }, confidence: answer.confidence };
|
|
53
|
+
case 'score':
|
|
54
|
+
if (!Number.isFinite(answer.score)) return undefined;
|
|
55
|
+
return {
|
|
56
|
+
type: 'score',
|
|
57
|
+
score: Math.min(Math.max(answer.score, 0), question.levels.length - 1),
|
|
58
|
+
probabilities: { ...answer.probabilities },
|
|
59
|
+
confidence: answer.confidence
|
|
60
|
+
};
|
|
61
|
+
case 'noul':
|
|
62
|
+
return { type: 'noul', probability: answer.noul };
|
|
63
|
+
default:
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Provider failures an API caller can act on, as an `EngineError` with a fixed
|
|
70
|
+
* message: never the response body, which may echo the request.
|
|
71
|
+
*/
|
|
72
|
+
function providerError(err) {
|
|
73
|
+
const status = err && typeof err.status === 'number' ? err.status : undefined;
|
|
74
|
+
if (status === 401 || status === 403) return new EngineError(`TypeSafe refused the decision engine's API key (HTTP ${status}).`);
|
|
75
|
+
if (status === 429) return new EngineError('TypeSafe is rate-limiting the decision engine (HTTP 429). Try again later.');
|
|
76
|
+
if (status >= 500 && status <= 599) return new EngineError(`TypeSafe failed to answer (HTTP ${status}).`);
|
|
77
|
+
return err;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = defineEngine({
|
|
81
|
+
type: 'engine',
|
|
82
|
+
name: 'jev',
|
|
83
|
+
title: 'JEV',
|
|
84
|
+
kinds: ['decision'],
|
|
85
|
+
apiVersion: ENGINE_API_VERSION,
|
|
86
|
+
description: 'TypeSafe Jev: confidence-scored Choice, Score and yes/no decisions.',
|
|
87
|
+
settings: {
|
|
88
|
+
apiKey: { env: 'TYPESAFE_API_KEY', required: true, secret: true },
|
|
89
|
+
model: { env: 'TYPESAFE_MODEL', default: DEFAULT_MODEL, description: 'A dated model version.' },
|
|
90
|
+
baseURL: { env: 'TYPESAFE_BASE_URL', description: 'The API base URL, when not the SDK\'s default.' },
|
|
91
|
+
},
|
|
92
|
+
create({ name, settings, options, config }) {
|
|
93
|
+
const { model } = settings;
|
|
94
|
+
// Keep the SDK's own per-request timeout inside this instance's routing timeout.
|
|
95
|
+
const timeout = (config.routing && config.routing.timeouts && config.routing.timeouts[name])
|
|
96
|
+
|| (config.agentApi && config.agentApi.timeout)
|
|
97
|
+
|| DEFAULT_TIMEOUT;
|
|
98
|
+
|
|
99
|
+
let client = options.client;
|
|
100
|
+
if (!client) {
|
|
101
|
+
const { TypeSafeClient } = require('@typesafe-ai/sdk');
|
|
102
|
+
client = new TypeSafeClient({
|
|
103
|
+
apiKey: settings.apiKey,
|
|
104
|
+
// The SDK refuses to start when it sees `window.document` and
|
|
105
|
+
// `navigator`. Inside the UAG both exist: Node 21+ defines
|
|
106
|
+
// `navigator`, and Form.io's libraries install `window`/`document`
|
|
107
|
+
// shims. Allow it only when actually running on Node, so a real
|
|
108
|
+
// browser is still refused.
|
|
109
|
+
dangerouslyAllowBrowser: typeof process !== 'undefined' && !!(process.versions && process.versions.node),
|
|
110
|
+
defaultModel: model,
|
|
111
|
+
...(settings.baseURL && { baseURL: settings.baseURL }),
|
|
112
|
+
timeout,
|
|
113
|
+
// Never wait longer than the timeout for a Retry-After.
|
|
114
|
+
retry: { maxRetries: 2, backoffMaxMs: Math.min(2000, Math.floor(timeout / 4)), maxRetryAfterMs: Math.min(timeout, 60000) }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
name,
|
|
120
|
+
kind: 'decision',
|
|
121
|
+
apiVersion: ENGINE_API_VERSION,
|
|
122
|
+
model,
|
|
123
|
+
maxOptions: MAX_OPTIONS,
|
|
124
|
+
async decide(request) {
|
|
125
|
+
if (request.signal) request.signal.throwIfAborted();
|
|
126
|
+
const questions = {};
|
|
127
|
+
for (const [id, question] of Object.entries(request.questions)) {
|
|
128
|
+
questions[id] = toSdkQuestion(question);
|
|
129
|
+
}
|
|
130
|
+
let result;
|
|
131
|
+
try {
|
|
132
|
+
result = await client.systemOne(
|
|
133
|
+
{ state: request.state, questions, model },
|
|
134
|
+
{ signal: request.signal, timeout }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
throw providerError(err);
|
|
139
|
+
}
|
|
140
|
+
const answers = {};
|
|
141
|
+
for (const [id, answer] of Object.entries(result.answers || {})) {
|
|
142
|
+
// Only the questions asked: anything else is dropped here, not passed on.
|
|
143
|
+
const mapped = Object.prototype.hasOwnProperty.call(request.questions, id) && fromSdkAnswer(answer, request.questions[id]);
|
|
144
|
+
if (mapped) answers[id] = mapped;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
model: result.model || model,
|
|
148
|
+
answers,
|
|
149
|
+
...(result.usage && { usage: { inputTokens: result.usage.input_tokens ?? null, outputTokens: result.usage.output_tokens ?? null } })
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formio/uag-jev",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0-rc.10",
|
|
4
|
+
"description": "TypeSafe Jev decision engine for the Form.io Universal Agent Gateway (UAG): fast, confidence-scored Choice, Score and yes/no decisions for the Agent API.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"formio",
|
|
7
|
+
"uag",
|
|
8
|
+
"universal-agent-gateway",
|
|
9
|
+
"ai",
|
|
10
|
+
"agents",
|
|
11
|
+
"engine",
|
|
12
|
+
"typesafe",
|
|
13
|
+
"jev",
|
|
14
|
+
"decisions"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"main": "index.js",
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "mocha"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/formio/uag.git",
|
|
31
|
+
"directory": "engines/jev"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@formio/uag-sdk": "^2.0.0-rc.10",
|
|
38
|
+
"@typesafe-ai/sdk": "0.7.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"chai": "^6.3.0",
|
|
42
|
+
"mocha": "^12.0.3"
|
|
43
|
+
}
|
|
44
|
+
}
|