@itsoltech/basal-client 0.2.0 → 0.4.0
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 +72 -4
- package/dist/cjs/batches.cjs +191 -0
- package/dist/cjs/batches.d.cts +166 -0
- package/dist/cjs/errors.cjs +31 -0
- package/dist/cjs/errors.d.cts +15 -0
- package/dist/cjs/index.cjs +119 -55
- package/dist/cjs/index.d.cts +32 -18
- package/dist/cjs/questions.cjs +7 -2
- package/dist/cjs/questions.d.cts +2 -0
- package/dist/cjs/tasks.cjs +175 -0
- package/dist/cjs/tasks.d.cts +108 -0
- package/dist/esm/batches.d.ts +166 -0
- package/dist/esm/batches.js +182 -0
- package/dist/esm/errors.d.ts +15 -0
- package/dist/esm/errors.js +23 -0
- package/dist/esm/index.d.ts +32 -18
- package/dist/esm/index.js +107 -40
- package/dist/esm/questions.d.ts +2 -0
- package/dist/esm/questions.js +7 -2
- package/dist/esm/tasks.d.ts +108 -0
- package/dist/esm/tasks.js +164 -0
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,39 +1,20 @@
|
|
|
1
1
|
import { buildRequest } from "./questions.js";
|
|
2
|
+
import { BasalError, BasalHTTPError, BasalProtocolError, BasalResponseTooLargeError, BasalTransportError } from "./errors.js";
|
|
3
|
+
import { createBody, idempotencyHeader, listPath, resourcePath, uploadForm, } from "./batches.js";
|
|
2
4
|
export * from "./questions.js";
|
|
3
5
|
export * from "./media.js";
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export class BasalHTTPError extends BasalError {
|
|
8
|
-
statusCode;
|
|
9
|
-
body;
|
|
10
|
-
retryAfter;
|
|
11
|
-
constructor(statusCode, body, retryAfter) {
|
|
12
|
-
super(`Basal HTTP ${statusCode}`);
|
|
13
|
-
this.statusCode = statusCode;
|
|
14
|
-
this.body = body;
|
|
15
|
-
this.retryAfter = retryAfter;
|
|
16
|
-
this.name = "BasalHTTPError";
|
|
17
|
-
// Node's default inspection and JSON.stringify must not print reflected state or credentials.
|
|
18
|
-
Object.defineProperty(this, "body", { enumerable: false });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
export class BasalTransportError extends BasalError {
|
|
22
|
-
}
|
|
23
|
-
export class BasalProtocolError extends BasalError {
|
|
24
|
-
}
|
|
25
|
-
export class BasalResponseTooLargeError extends BasalError {
|
|
26
|
-
}
|
|
6
|
+
export * from "./tasks.js";
|
|
7
|
+
export * from "./errors.js";
|
|
8
|
+
export { batchInput, parseBatchResults } from "./batches.js";
|
|
27
9
|
async function readBody(response, limit, signal) {
|
|
28
10
|
// Count bytes from the actual stream, including decompressed responses; never trust Content-Length alone.
|
|
29
11
|
const reader = response.body?.getReader();
|
|
30
12
|
if (!reader)
|
|
31
|
-
return
|
|
13
|
+
return new Uint8Array();
|
|
32
14
|
const cancel = () => { void reader.cancel().catch(() => { }); };
|
|
33
15
|
signal.addEventListener("abort", cancel, { once: true });
|
|
34
16
|
if (signal.aborted)
|
|
35
17
|
cancel();
|
|
36
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
37
18
|
const parts = [];
|
|
38
19
|
let bytes = 0;
|
|
39
20
|
let complete = false;
|
|
@@ -48,18 +29,36 @@ async function readBody(response, limit, signal) {
|
|
|
48
29
|
if (bytes > limit) {
|
|
49
30
|
throw new BasalResponseTooLargeError(`Basal response exceeds ${limit} bytes`);
|
|
50
31
|
}
|
|
51
|
-
parts.push(
|
|
32
|
+
parts.push(value);
|
|
52
33
|
}
|
|
53
|
-
parts.push(decoder.decode());
|
|
54
|
-
return parts.join("");
|
|
55
34
|
}
|
|
56
35
|
finally {
|
|
57
36
|
signal.removeEventListener("abort", cancel);
|
|
58
|
-
// A
|
|
37
|
+
// A read failure must also stop the underlying HTTP body, before releasing ownership.
|
|
59
38
|
if (!complete)
|
|
60
39
|
cancel();
|
|
61
40
|
reader.releaseLock();
|
|
62
41
|
}
|
|
42
|
+
if (parts.length === 1)
|
|
43
|
+
return parts[0];
|
|
44
|
+
const out = new Uint8Array(bytes);
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
out.set(part, offset);
|
|
48
|
+
offset += part.byteLength;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function positiveTimeout(value, name) {
|
|
53
|
+
if (!Number.isFinite(value) || value <= 0 || value > 2_147_483_647) {
|
|
54
|
+
throw new TypeError(`${name} must be positive and at most 2147483647`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function byteLimit(value, name) {
|
|
59
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
60
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
61
|
+
return value;
|
|
63
62
|
}
|
|
64
63
|
/** Reusable fetch client. Server root URL, optional proxy prefix, without /v1.
|
|
65
64
|
* No automatic retries. AbortSignal cancels a request; timeout includes body reading.
|
|
@@ -95,15 +94,24 @@ export class BasalClient {
|
|
|
95
94
|
this.headers.Authorization = `Bearer ${options.apiKey}`;
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
request(method, path, body, signal) {
|
|
98
|
+
return this.call({ method, path, json: body, signal });
|
|
99
|
+
}
|
|
100
|
+
async call(call) {
|
|
101
|
+
// Serialization and option errors are local input errors, not failures of the HTTP transport.
|
|
102
|
+
const encoded = call.json === undefined ? undefined : JSON.stringify(call.json);
|
|
103
|
+
const timeoutMs = call.timeoutMs === undefined ? this.timeoutMs : positiveTimeout(call.timeoutMs, "timeoutMs");
|
|
104
|
+
const limit = call.maxBytes === undefined ? this.maxResponseBytes : byteLimit(call.maxBytes, "maxBytes");
|
|
105
|
+
const headers = { ...this.headers, ...call.headers };
|
|
106
|
+
if (encoded !== undefined)
|
|
107
|
+
headers["Content-Type"] = "application/json";
|
|
108
|
+
const { signal } = call;
|
|
101
109
|
const controller = new AbortController();
|
|
102
110
|
const abort = () => controller.abort(signal?.reason);
|
|
103
111
|
signal?.addEventListener("abort", abort, { once: true });
|
|
104
112
|
if (signal?.aborted)
|
|
105
113
|
abort();
|
|
106
|
-
const timer = setTimeout(() => controller.abort(new Error("Basal request timed out")),
|
|
114
|
+
const timer = setTimeout(() => controller.abort(new Error("Basal request timed out")), timeoutMs);
|
|
107
115
|
let onAbort = () => { };
|
|
108
116
|
const aborted = new Promise((_, reject) => {
|
|
109
117
|
onAbort = () => reject(new BasalTransportError("Basal HTTP request aborted", { cause: controller.signal.reason }));
|
|
@@ -113,24 +121,39 @@ export class BasalClient {
|
|
|
113
121
|
});
|
|
114
122
|
const work = async () => {
|
|
115
123
|
let response;
|
|
116
|
-
let
|
|
124
|
+
let content;
|
|
125
|
+
let raw = "";
|
|
117
126
|
try {
|
|
118
|
-
|
|
119
|
-
method, headers:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
const init = {
|
|
128
|
+
method: call.method, headers, signal: controller.signal, redirect: "error", credentials: "omit", cache: "no-store",
|
|
129
|
+
};
|
|
130
|
+
if (encoded !== undefined)
|
|
131
|
+
init.body = encoded;
|
|
132
|
+
else if (call.body instanceof Blob)
|
|
133
|
+
init.body = call.body;
|
|
134
|
+
else if (call.body !== undefined) {
|
|
135
|
+
// Streamed request bodies require half-duplex fetch (Node.js, Chromium over HTTP/2).
|
|
136
|
+
init.body = call.body(controller.signal);
|
|
137
|
+
init.duplex = "half";
|
|
138
|
+
}
|
|
139
|
+
response = await this.fetch(this.baseUrl + call.path, init);
|
|
123
140
|
if (response.redirected || ([301, 302, 303, 307, 308].includes(response.status) && response.headers.has("Location"))) {
|
|
124
141
|
await response.body?.cancel().catch(() => { });
|
|
125
142
|
throw new BasalTransportError("Basal transport returned a redirect");
|
|
126
143
|
}
|
|
127
|
-
|
|
144
|
+
// A per-download cap limits successful artifact bytes. Error envelopes retain the client's normal cap
|
|
145
|
+
// so a small artifact cap does not hide an actionable 404/429 and its Retry-After.
|
|
146
|
+
content = await readBody(response, response.ok ? limit : this.maxResponseBytes, controller.signal);
|
|
147
|
+
if (!call.bytes || !response.ok)
|
|
148
|
+
raw = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
128
149
|
}
|
|
129
150
|
catch (cause) {
|
|
130
151
|
if (cause instanceof BasalError)
|
|
131
152
|
throw cause;
|
|
132
153
|
throw new BasalTransportError("Basal HTTP transport failed", { cause });
|
|
133
154
|
}
|
|
155
|
+
if (call.bytes && response.ok)
|
|
156
|
+
return content;
|
|
134
157
|
let data;
|
|
135
158
|
try {
|
|
136
159
|
data = JSON.parse(raw);
|
|
@@ -156,6 +179,9 @@ export class BasalClient {
|
|
|
156
179
|
controller.signal.removeEventListener("abort", onAbort);
|
|
157
180
|
clearTimeout(timer);
|
|
158
181
|
signal?.removeEventListener("abort", abort);
|
|
182
|
+
// An answer can arrive before a streamed upload is fully sent (e.g. 413); stop its source.
|
|
183
|
+
if (typeof call.body === "function")
|
|
184
|
+
controller.abort(new Error("Basal request finished"));
|
|
159
185
|
}
|
|
160
186
|
}
|
|
161
187
|
async decide(state, questionsOrSignal, options = {}) {
|
|
@@ -190,4 +216,45 @@ export class BasalClient {
|
|
|
190
216
|
return typed;
|
|
191
217
|
}
|
|
192
218
|
models(signal) { return this.request("GET", "v1/models", undefined, signal); }
|
|
219
|
+
/** Upload a JSONL input file (`purpose=batch`) from caller-owned bytes, a Blob or a stream; never fetches URLs. */
|
|
220
|
+
async uploadFile(request, options = {}) {
|
|
221
|
+
const form = uploadForm(request);
|
|
222
|
+
return await this.call({
|
|
223
|
+
method: "POST", path: "v1/files", body: form.body, headers: { "Content-Type": form.contentType },
|
|
224
|
+
signal: options.signal, timeoutMs: options.timeoutMs,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async listFiles(options = {}) {
|
|
228
|
+
return await this.call({ method: "GET", path: listPath("v1/files", options), signal: options.signal });
|
|
229
|
+
}
|
|
230
|
+
async getFile(fileId, options = {}) {
|
|
231
|
+
return await this.call({ method: "GET", path: resourcePath("v1/files", fileId), signal: options.signal });
|
|
232
|
+
}
|
|
233
|
+
async deleteFile(fileId, options = {}) {
|
|
234
|
+
return await this.call({ method: "DELETE", path: resourcePath("v1/files", fileId), signal: options.signal });
|
|
235
|
+
}
|
|
236
|
+
/** Raw file bytes (JSONL), buffered up to `maxBytes` (default `maxResponseBytes`); never JSON-parsed. */
|
|
237
|
+
async downloadFile(fileId, options = {}) {
|
|
238
|
+
return await this.call({
|
|
239
|
+
method: "GET", path: resourcePath("v1/files", fileId, "/content"), bytes: true,
|
|
240
|
+
headers: { Accept: "application/jsonl, application/json" },
|
|
241
|
+
signal: options.signal, timeoutMs: options.timeoutMs, maxBytes: options.maxBytes,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async createBatch(request, options = {}) {
|
|
245
|
+
const headers = idempotencyHeader(options.idempotencyKey);
|
|
246
|
+
return await this.call({ method: "POST", path: "v1/batches", json: createBody(request), headers, signal: options.signal });
|
|
247
|
+
}
|
|
248
|
+
async listBatches(options = {}) {
|
|
249
|
+
const { after, limit } = options;
|
|
250
|
+
const query = { ...(after === undefined ? {} : { after }), ...(limit === undefined ? {} : { limit }) };
|
|
251
|
+
return await this.call({ method: "GET", path: listPath("v1/batches", query), signal: options.signal });
|
|
252
|
+
}
|
|
253
|
+
async getBatch(batchId, options = {}) {
|
|
254
|
+
return await this.call({ method: "GET", path: resourcePath("v1/batches", batchId), signal: options.signal });
|
|
255
|
+
}
|
|
256
|
+
/** Request cancellation; the batch moves to `cancelling` and later `cancelled`. Does not wait. */
|
|
257
|
+
async cancelBatch(batchId, options = {}) {
|
|
258
|
+
return await this.call({ method: "POST", path: resourcePath("v1/batches", batchId, "/cancel"), signal: options.signal });
|
|
259
|
+
}
|
|
193
260
|
}
|
package/dist/esm/questions.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface ToolOptions extends MultiOptions {
|
|
|
33
33
|
rank?: number;
|
|
34
34
|
multi?: boolean;
|
|
35
35
|
max_error?: number;
|
|
36
|
+
/** Tool-description truncation cap; omitted keeps the server default (300), zero disables truncation. */
|
|
37
|
+
description_chars?: number;
|
|
36
38
|
}
|
|
37
39
|
export interface ActOptions<K extends string = string, A extends string = string> extends EvidenceOptions {
|
|
38
40
|
criteria?: Record<K, string>;
|
package/dist/esm/questions.js
CHANGED
|
@@ -30,13 +30,13 @@ function evidence(options) {
|
|
|
30
30
|
export class QuestionSpec {
|
|
31
31
|
wire;
|
|
32
32
|
constructor(kind, instructions, fields) {
|
|
33
|
-
this.wire = structuredClone({ ...fields, type: kind, instructions: text(instructions, 'instructions') });
|
|
33
|
+
this.wire = structuredClone({ ...fields, type: kind, ...(['classify', 'verify', 'compress', 'extract'].includes(kind) ? (instructions ? { instructions } : {}) : { instructions: text(instructions, 'instructions') }) });
|
|
34
34
|
}
|
|
35
35
|
toJSON() { return structuredClone(this.wire); }
|
|
36
36
|
/** Validate and retrieve an answer with this question's result type. */
|
|
37
37
|
read(response, id) {
|
|
38
38
|
const answer = response.answers?.[id];
|
|
39
|
-
if (!answer || answer.type !== this.wire.type || !answer.probabilities || typeof answer.probabilities !== 'object' || Array.isArray(answer.probabilities)) {
|
|
39
|
+
if (!answer || !('probabilities' in answer) || answer.type !== this.wire.type || !answer.probabilities || typeof answer.probabilities !== 'object' || Array.isArray(answer.probabilities)) {
|
|
40
40
|
throw new TypeError('Basal returned a missing or mismatched answer');
|
|
41
41
|
}
|
|
42
42
|
const fields = {
|
|
@@ -201,6 +201,11 @@ export class ToolQuestion extends QuestionSpec {
|
|
|
201
201
|
if (options.multi !== undefined && typeof options.multi !== 'boolean')
|
|
202
202
|
throw new TypeError('invalid multi');
|
|
203
203
|
const fields = { tools, ...(options.extra === undefined ? {} : { extra: options.extra }) };
|
|
204
|
+
if (options.description_chars !== undefined) {
|
|
205
|
+
if (!Number.isSafeInteger(options.description_chars) || options.description_chars < 0)
|
|
206
|
+
throw new TypeError('description_chars must be a non-negative safe integer');
|
|
207
|
+
fields.description_chars = options.description_chars;
|
|
208
|
+
}
|
|
204
209
|
if (options.rank !== undefined) {
|
|
205
210
|
if (!Number.isSafeInteger(options.rank) || options.rank < 1)
|
|
206
211
|
throw new TypeError('rank must be a positive integer');
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { QuestionSpec } from './questions.js';
|
|
2
|
+
import type { DecisionResponse, JsonValue, Question, MultiObjective } from './index.js';
|
|
3
|
+
/** Task automation is unknown (null) without a threshold certified for that task/error target. */
|
|
4
|
+
export type Automation = Record<string, boolean | null>;
|
|
5
|
+
export interface ClassifyAnswer<K extends string = string> {
|
|
6
|
+
type: 'classify';
|
|
7
|
+
experimental: true;
|
|
8
|
+
scores: Record<K, number>;
|
|
9
|
+
label?: K;
|
|
10
|
+
top_label?: K;
|
|
11
|
+
confidence?: number;
|
|
12
|
+
automated?: Automation;
|
|
13
|
+
labels?: K[];
|
|
14
|
+
set_confidence?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ClaimResult {
|
|
17
|
+
verdict: 'supported' | 'contradicted' | 'not_stated' | 'not_supported';
|
|
18
|
+
probabilities: Record<string, number>;
|
|
19
|
+
confidence: number;
|
|
20
|
+
automated: Automation;
|
|
21
|
+
}
|
|
22
|
+
export interface VerifyAnswer {
|
|
23
|
+
type: 'verify';
|
|
24
|
+
experimental: true;
|
|
25
|
+
claims: Record<string, ClaimResult>;
|
|
26
|
+
all_supported: boolean;
|
|
27
|
+
not_supported: string[];
|
|
28
|
+
}
|
|
29
|
+
/** Inclusive start/exclusive end count Unicode code points, not JavaScript UTF-16 units. */
|
|
30
|
+
export interface CompressionUnit {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
probability: number;
|
|
34
|
+
}
|
|
35
|
+
export interface CompressAnswer {
|
|
36
|
+
type: 'compress';
|
|
37
|
+
experimental: true;
|
|
38
|
+
compressed: string;
|
|
39
|
+
kept: number[];
|
|
40
|
+
units: CompressionUnit[];
|
|
41
|
+
unit: 'sentence' | 'line' | 'paragraph';
|
|
42
|
+
original_chars: number;
|
|
43
|
+
compressed_chars: number;
|
|
44
|
+
ratio: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ExtractField {
|
|
47
|
+
value: JsonValue;
|
|
48
|
+
confidence: number;
|
|
49
|
+
source: 'choice' | 'span';
|
|
50
|
+
calibration: 'validated' | 'per decision';
|
|
51
|
+
probabilities?: Record<string, number>;
|
|
52
|
+
present_probability?: number;
|
|
53
|
+
text?: string;
|
|
54
|
+
start?: number;
|
|
55
|
+
end?: number;
|
|
56
|
+
line_probability?: number;
|
|
57
|
+
span_probability?: number;
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface ExtractAnswer {
|
|
61
|
+
type: 'extract';
|
|
62
|
+
experimental: true;
|
|
63
|
+
data: Record<string, JsonValue>;
|
|
64
|
+
fields: Record<string, ExtractField>;
|
|
65
|
+
}
|
|
66
|
+
export type TaskAnswer = ClassifyAnswer | VerifyAnswer | CompressAnswer | ExtractAnswer;
|
|
67
|
+
export interface ClassifyOptions {
|
|
68
|
+
multi_label?: boolean;
|
|
69
|
+
none?: boolean;
|
|
70
|
+
max_error?: number | readonly number[];
|
|
71
|
+
option_keys?: 'show' | 'hide';
|
|
72
|
+
threshold?: number;
|
|
73
|
+
min?: number;
|
|
74
|
+
max?: number;
|
|
75
|
+
objective?: MultiObjective;
|
|
76
|
+
}
|
|
77
|
+
export interface VerifyOptions {
|
|
78
|
+
binary?: boolean;
|
|
79
|
+
max_error?: number | readonly number[];
|
|
80
|
+
}
|
|
81
|
+
export interface CompressOptions {
|
|
82
|
+
unit?: 'sentence' | 'line' | 'paragraph';
|
|
83
|
+
keep_ratio?: number;
|
|
84
|
+
threshold?: number;
|
|
85
|
+
min_keep?: number;
|
|
86
|
+
}
|
|
87
|
+
declare abstract class TaskQuestion<A extends TaskAnswer> extends QuestionSpec<A> {
|
|
88
|
+
protected constructor(kind: A['type'], text: string, fields: Question);
|
|
89
|
+
read(response: DecisionResponse, id: string): A;
|
|
90
|
+
}
|
|
91
|
+
export declare class ClassifyQuestion<K extends string = string> extends TaskQuestion<ClassifyAnswer<K>> {
|
|
92
|
+
constructor(text: string, labels: Record<K, JsonValue> | readonly JsonValue[], options?: ClassifyOptions);
|
|
93
|
+
}
|
|
94
|
+
export declare class VerifyQuestion extends TaskQuestion<VerifyAnswer> {
|
|
95
|
+
constructor(text: string, claims: Record<string, JsonValue> | readonly JsonValue[], options?: VerifyOptions);
|
|
96
|
+
}
|
|
97
|
+
export declare class CompressQuestion extends TaskQuestion<CompressAnswer> {
|
|
98
|
+
constructor(text?: string, options?: CompressOptions);
|
|
99
|
+
}
|
|
100
|
+
export declare class ExtractQuestion extends TaskQuestion<ExtractAnswer> {
|
|
101
|
+
constructor(text: string, schema: Record<string, JsonValue>);
|
|
102
|
+
}
|
|
103
|
+
export declare function classify<const K extends string>(text: string, labels: Record<K, JsonValue>, options?: ClassifyOptions): ClassifyQuestion<K>;
|
|
104
|
+
export declare function classify(text: string, labels: readonly JsonValue[], options?: ClassifyOptions): ClassifyQuestion;
|
|
105
|
+
export declare function verify(text: string, claims: Record<string, JsonValue> | readonly JsonValue[], options?: VerifyOptions): VerifyQuestion;
|
|
106
|
+
export declare function compress(text?: string, options?: CompressOptions): CompressQuestion;
|
|
107
|
+
export declare function extract(text: string, schema: Record<string, JsonValue>): ExtractQuestion;
|
|
108
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { QuestionSpec } from './questions.js';
|
|
2
|
+
function object(v) {
|
|
3
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
4
|
+
throw new TypeError('expected task object');
|
|
5
|
+
return v;
|
|
6
|
+
}
|
|
7
|
+
function probability(v) { if (typeof v !== 'number' || !Number.isFinite(v) || v < 0 || v > 1)
|
|
8
|
+
throw new TypeError('invalid probability'); }
|
|
9
|
+
function probabilities(v) { for (const n of Object.values(object(v)))
|
|
10
|
+
probability(n); }
|
|
11
|
+
function automation(v) { if (Object.values(object(v)).some(n => n !== null && typeof n !== 'boolean'))
|
|
12
|
+
throw new TypeError('invalid automation'); }
|
|
13
|
+
function strings(v) { if (!Array.isArray(v) || v.some(k => typeof k !== 'string'))
|
|
14
|
+
throw new TypeError('expected string list'); }
|
|
15
|
+
function offset(v) { if (!Number.isSafeInteger(v) || v < 0)
|
|
16
|
+
throw new TypeError('invalid Unicode code point offset/count'); }
|
|
17
|
+
function span(v) { offset(v.start); offset(v.end); if (v.end < v.start)
|
|
18
|
+
throw new TypeError('reversed source span'); }
|
|
19
|
+
function risks(v) {
|
|
20
|
+
if (v === undefined)
|
|
21
|
+
return;
|
|
22
|
+
const a = Array.isArray(v) ? v : [v];
|
|
23
|
+
if (a.length > 64 || a.some(n => typeof n !== 'number' || !Number.isFinite(n) || n <= 0 || n >= 1))
|
|
24
|
+
throw new TypeError('invalid max_error');
|
|
25
|
+
}
|
|
26
|
+
function instructions(v) { if (typeof v !== 'string')
|
|
27
|
+
throw new TypeError('instructions must be a string'); return v; }
|
|
28
|
+
class TaskQuestion extends QuestionSpec {
|
|
29
|
+
constructor(kind, text, fields) { super(kind, instructions(text), fields); }
|
|
30
|
+
read(response, id) {
|
|
31
|
+
const d = object(response.answers?.[id]);
|
|
32
|
+
if (d.type !== this.toJSON().type || d.experimental !== true)
|
|
33
|
+
throw new TypeError('Basal returned a missing or mismatched task');
|
|
34
|
+
if (d.type === 'classify') {
|
|
35
|
+
probabilities(d.scores);
|
|
36
|
+
const scores = object(d.scores);
|
|
37
|
+
if (this.toJSON().multi_label) {
|
|
38
|
+
strings(d.labels);
|
|
39
|
+
if (d.labels.some(k => !Object.hasOwn(scores, k)))
|
|
40
|
+
throw new TypeError('unknown label');
|
|
41
|
+
probability(d.set_confidence);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
if (typeof d.label !== 'string' || !Object.hasOwn(scores, d.label) || d.top_label !== d.label)
|
|
45
|
+
throw new TypeError('invalid label');
|
|
46
|
+
probability(d.confidence);
|
|
47
|
+
automation(d.automated);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (d.type === 'verify') {
|
|
51
|
+
const claims = object(d.claims);
|
|
52
|
+
for (const raw of Object.values(claims)) {
|
|
53
|
+
const c = object(raw);
|
|
54
|
+
const p = object(c.probabilities);
|
|
55
|
+
probabilities(p);
|
|
56
|
+
if (typeof c.verdict !== 'string' || !['supported', 'contradicted', 'not_stated', 'not_supported'].includes(c.verdict) || !Object.hasOwn(p, c.verdict))
|
|
57
|
+
throw new TypeError('invalid verdict');
|
|
58
|
+
probability(c.confidence);
|
|
59
|
+
automation(c.automated);
|
|
60
|
+
}
|
|
61
|
+
strings(d.not_supported);
|
|
62
|
+
if (typeof d.all_supported !== 'boolean' || d.not_supported.some(k => !Object.hasOwn(claims, k)))
|
|
63
|
+
throw new TypeError('invalid verification summary');
|
|
64
|
+
}
|
|
65
|
+
else if (d.type === 'compress') {
|
|
66
|
+
if (typeof d.compressed !== 'string' || !['sentence', 'line', 'paragraph'].includes(d.unit) || !Array.isArray(d.units) || !Array.isArray(d.kept))
|
|
67
|
+
throw new TypeError('invalid compression');
|
|
68
|
+
for (const u of d.units) {
|
|
69
|
+
const v = object(u);
|
|
70
|
+
span(v);
|
|
71
|
+
probability(v.probability);
|
|
72
|
+
}
|
|
73
|
+
for (const k of d.kept) {
|
|
74
|
+
offset(k);
|
|
75
|
+
if (k >= d.units.length)
|
|
76
|
+
throw new TypeError('invalid kept unit');
|
|
77
|
+
}
|
|
78
|
+
offset(d.original_chars);
|
|
79
|
+
offset(d.compressed_chars);
|
|
80
|
+
if (typeof d.ratio !== 'number' || !Number.isFinite(d.ratio) || d.ratio < 0)
|
|
81
|
+
throw new TypeError('invalid ratio');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
object(d.data);
|
|
85
|
+
for (const raw of Object.values(object(d.fields))) {
|
|
86
|
+
const f = object(raw);
|
|
87
|
+
if (!Object.hasOwn(f, 'value') || !['choice', 'span'].includes(f.source) || !['validated', 'per decision'].includes(f.calibration))
|
|
88
|
+
throw new TypeError('invalid extraction field');
|
|
89
|
+
probability(f.confidence);
|
|
90
|
+
if (f.probabilities !== undefined)
|
|
91
|
+
probabilities(f.probabilities);
|
|
92
|
+
for (const k of ['present_probability', 'line_probability', 'span_probability'])
|
|
93
|
+
if (f[k] !== undefined)
|
|
94
|
+
probability(f[k]);
|
|
95
|
+
if (f.text !== undefined) {
|
|
96
|
+
if (typeof f.text !== 'string')
|
|
97
|
+
throw new TypeError('invalid source text');
|
|
98
|
+
span(f);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return structuredClone(d);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export class ClassifyQuestion extends TaskQuestion {
|
|
106
|
+
constructor(text, labels, options = {}) {
|
|
107
|
+
if (!labels || typeof labels !== 'object' || Object.keys(labels).length < 1 || Object.keys(labels).length > 256)
|
|
108
|
+
throw new TypeError('classify requires 1..256 labels');
|
|
109
|
+
risks(options.max_error);
|
|
110
|
+
for (const k of ['multi_label', 'none'])
|
|
111
|
+
if (options[k] !== undefined && typeof options[k] !== 'boolean')
|
|
112
|
+
throw new TypeError(`invalid ${k}`);
|
|
113
|
+
if (options.option_keys !== undefined && !['show', 'hide'].includes(options.option_keys))
|
|
114
|
+
throw new TypeError('invalid option_keys');
|
|
115
|
+
if (options.threshold !== undefined) {
|
|
116
|
+
probability(options.threshold);
|
|
117
|
+
if (options.objective !== undefined && options.objective !== 'threshold')
|
|
118
|
+
throw new TypeError('threshold requires threshold objective');
|
|
119
|
+
}
|
|
120
|
+
for (const k of ['min', 'max'])
|
|
121
|
+
if (options[k] !== undefined)
|
|
122
|
+
offset(options[k]);
|
|
123
|
+
if (options.min !== undefined && options.max !== undefined && options.min > options.max)
|
|
124
|
+
throw new TypeError('min exceeds max');
|
|
125
|
+
if (options.objective !== undefined && !['threshold', 'exact', 'iou', 'f1'].includes(options.objective))
|
|
126
|
+
throw new TypeError('invalid objective');
|
|
127
|
+
if (!options.multi_label && ['threshold', 'min', 'max', 'objective'].some(k => Object.hasOwn(options, k)))
|
|
128
|
+
throw new TypeError('selection options require multi_label');
|
|
129
|
+
super('classify', text, { labels: structuredClone(labels), ...options });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export class VerifyQuestion extends TaskQuestion {
|
|
133
|
+
constructor(text, claims, options = {}) {
|
|
134
|
+
if (!claims || typeof claims !== 'object' || !Object.keys(claims).length)
|
|
135
|
+
throw new TypeError('verify requires claims');
|
|
136
|
+
risks(options.max_error);
|
|
137
|
+
if (options.binary !== undefined && typeof options.binary !== 'boolean')
|
|
138
|
+
throw new TypeError('invalid binary');
|
|
139
|
+
super('verify', text, { claims: structuredClone(claims), ...options });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export class CompressQuestion extends TaskQuestion {
|
|
143
|
+
constructor(text = '', options = {}) {
|
|
144
|
+
if (options.unit !== undefined && !['sentence', 'line', 'paragraph'].includes(options.unit))
|
|
145
|
+
throw new TypeError('invalid unit');
|
|
146
|
+
if (options.keep_ratio !== undefined) {
|
|
147
|
+
probability(options.keep_ratio);
|
|
148
|
+
if (options.keep_ratio === 0)
|
|
149
|
+
throw new TypeError('keep_ratio must be positive');
|
|
150
|
+
}
|
|
151
|
+
if (options.threshold !== undefined)
|
|
152
|
+
probability(options.threshold);
|
|
153
|
+
if (options.min_keep !== undefined)
|
|
154
|
+
offset(options.min_keep);
|
|
155
|
+
super('compress', text, options);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export class ExtractQuestion extends TaskQuestion {
|
|
159
|
+
constructor(text, schema) { object(schema); super('extract', text, { schema }); }
|
|
160
|
+
}
|
|
161
|
+
export function classify(text, labels, options) { return new ClassifyQuestion(text, labels, options); }
|
|
162
|
+
export function verify(text, claims, options) { return new VerifyQuestion(text, claims, options); }
|
|
163
|
+
export function compress(text = '', options) { return new CompressQuestion(text, options); }
|
|
164
|
+
export function extract(text, schema) { return new ExtractQuestion(text, schema); }
|