@itsoltech/basal-client 0.1.0 → 0.2.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 CHANGED
@@ -57,8 +57,8 @@ in `cause`. `BasalHTTPError.body` remains directly accessible but is non-enumera
57
57
  inspection and JSON.stringify do not log it. There are no automatic retries or redirects. Types describe the native contract; successful
58
58
  responses are checked to be JSON objects without full schema validation. Unknown JSON fields are preserved.
59
59
 
60
- Supports `choice`, `noul`, `score`, `multi`, `act`, `facts` and `evidence`. Native choice/score accepts 2–10
61
- options. Criterion insertion order matters; JavaScript enumerates integer-like object keys numerically,
60
+ Supports `choice`, `noul`, `score`, `multi`, `act` and `tool`, with model-dependent `facts` and `evidence` capabilities. Native choice/score accepts 2–255
61
+ options for format-2 models (older model limits are checked by the server). Criterion insertion order matters; JavaScript enumerates integer-like object keys numerically,
62
62
  so use textual keys (e.g. `low`, `high`) or an ordered score array. Evidence offsets count Unicode code
63
63
  points; use the returned text, or `Array.from(state).slice(start, end).join("")` for textual states.
64
64
 
@@ -76,3 +76,41 @@ retains the native JSON without those defaults. The one-argument raw `decide(req
76
76
 
77
77
 
78
78
  [Full documentation and publication setup](https://github.com/itsoltech/basal-rs/blob/main/docs/SDK.md).
79
+
80
+
81
+ ## Basal 2.0 and Omni
82
+
83
+ Typed choice/score and act outcomes allow 2–255 options. The server remains
84
+ responsible for limits of older models and for calibration/model capabilities.
85
+ `tool` accepts OpenAI, MCP and Anthropic definition objects, optional non-tool
86
+ choices (`answer_directly`, `ask_user`, `none`), ranking, multi selection and
87
+ certified deferral. The SDK returns the decision; it never executes tools.
88
+
89
+ Multi selection supports `threshold`, `exact`, `iou` and `f1`, count limits,
90
+ `exclusive`, `fallback` and `fallback_threshold`. `threshold` is optional in
91
+ results because objective-based responses instead contain `objective` and
92
+ `expected_score`; constraint diagnostics remain available. Single tool selection
93
+ can be null on certified deferral, while multi mode returns a tool list.
94
+
95
+ Typed request options include `lang` (`pl`/`en`), ordered inline `media` and
96
+ `detail` (`default`/`high`). Media helpers accept encoded bytes, base64 or a data
97
+ URL; they never fetch URLs. Read files explicitly in your application. Decoding,
98
+ model support and resource limits are checked by the server. Omni requires a
99
+ CUDA server started with media enabled. `calibration: "text"` means text
100
+ calibration applied to media, not validated calibration for images/audio.
101
+
102
+ Usage exposes `longest_prompt_tokens`, `second_order_branches` and media counts,
103
+ soft tokens and detail. Discovery exposes `prompt_format`, `orders`, enabled
104
+ media and calibration temperatures. Model names remain strings: use exact names
105
+ from model discovery, not a hardcoded model enumeration. Keep old models and
106
+ raw JSON access available. These SDKs cover server-supported format-2 decisions;
107
+ upstream 2.1 task types are not implemented by this server.
108
+
109
+ ```typescript
110
+ import { BasalClient, image, tool, multi } from "@itsoltech/basal-client";
111
+ const response = await new BasalClient().decide("Inspect the supplied material.", {
112
+ route: tool("Choose a tool.", [{name:"inspect", description:"Inspect material", inputSchema:{type:"object"}}], {extra:["none"], rank:2}),
113
+ labels: multi("Select visible objects.", {animal:"An animal", vehicle:"A vehicle"}, {objective:"f1"}),
114
+ }, {model:"basal-omni-1.0-mini", lang:"en", media:[image(new Uint8Array(await file.arrayBuffer()))]});
115
+ console.log(response.answers.route, response.answers.labels.expected_score);
116
+ ```
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.BasalClient = exports.BasalResponseTooLargeError = exports.BasalProtocolError = exports.BasalTransportError = exports.BasalHTTPError = exports.BasalError = void 0;
18
18
  const questions_cjs_1 = require("./questions.cjs");
19
19
  __exportStar(require("./questions.cjs"), exports);
20
+ __exportStar(require("./media.cjs"), exports);
20
21
  class BasalError extends Error {
21
22
  }
22
23
  exports.BasalError = BasalError;
@@ -1,11 +1,29 @@
1
1
  import { type QuestionSet, type TypedDecisionResponse, type DecisionOptions } from "./questions.cjs";
2
2
  export * from "./questions.cjs";
3
+ export * from "./media.cjs";
3
4
  /** Native Basal wire types. Criterion order is significant; do not sort keys. */
4
5
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
5
6
  [key: string]: JsonValue;
6
7
  };
8
+ export type Calibration = "validated" | "unvalidated" | "text";
9
+ export type MultiObjective = "threshold" | "exact" | "iou" | "f1";
10
+ export interface MediaInput {
11
+ type: "image" | "audio";
12
+ data: string;
13
+ }
14
+ export interface MediaUsage {
15
+ images: number;
16
+ audio: number;
17
+ media_tokens: number;
18
+ detail: "default" | "high";
19
+ }
20
+ export interface ToolRanking {
21
+ tool: string;
22
+ probability: number;
23
+ }
24
+ export type ToolDefinition = Record<string, JsonValue>;
7
25
  export interface Question {
8
- type?: "choice" | "noul" | "score" | "multi" | "act";
26
+ type?: "choice" | "noul" | "score" | "multi" | "act" | "tool";
9
27
  instructions?: JsonValue;
10
28
  criteria?: Record<string, JsonValue> | JsonValue[];
11
29
  levels?: Record<string, JsonValue> | JsonValue[];
@@ -18,12 +36,23 @@ export interface Question {
18
36
  costs?: Record<string, number> | Record<string, Record<string, number>>;
19
37
  defer_action?: string;
20
38
  max_error?: number;
39
+ objective?: MultiObjective;
40
+ exclusive?: readonly string[];
41
+ fallback?: string;
42
+ fallback_threshold?: number;
43
+ tools?: readonly ToolDefinition[];
44
+ extra?: readonly ("answer_directly" | "ask_user" | "none")[];
45
+ rank?: number;
46
+ multi?: boolean;
21
47
  }
22
48
  export interface DecisionRequest {
23
49
  state: JsonValue;
24
50
  questions: Record<string, Question>;
25
51
  model?: string;
26
52
  facts?: "off" | "auto";
53
+ lang?: "pl" | "en";
54
+ media?: readonly MediaInput[];
55
+ detail?: "default" | "high";
27
56
  }
28
57
  export interface EvidenceSpan {
29
58
  text: string;
@@ -34,6 +63,7 @@ export interface EvidenceSpan {
34
63
  interface BaseAnswer<K extends string = string> {
35
64
  probabilities: Record<K, number>;
36
65
  evidence?: EvidenceSpan[];
66
+ calibration?: Calibration;
37
67
  }
38
68
  export interface ChoiceAnswer<K extends string = string> extends BaseAnswer<K> {
39
69
  type: "choice";
@@ -51,29 +81,57 @@ export interface ScoreAnswer extends BaseAnswer {
51
81
  legend: Record<string, string>;
52
82
  confidence: number;
53
83
  }
54
- export interface MultiAnswer<K extends string = string> extends BaseAnswer<K> {
84
+ export interface SelectionResult<K extends string = string> {
85
+ threshold?: number;
86
+ objective?: MultiObjective;
87
+ expected_score?: number | null;
88
+ constraints_applied?: JsonValue;
89
+ set_confidence: number;
90
+ }
91
+ export interface MultiAnswer<K extends string = string> extends BaseAnswer<K>, SelectionResult<K> {
55
92
  type: "multi";
56
93
  selected: K[];
57
- threshold: number;
58
- set_confidence: number;
59
94
  }
95
+ export interface SingleToolAnswer extends BaseAnswer {
96
+ type: "tool";
97
+ multi?: false;
98
+ tool: string | null;
99
+ call: boolean;
100
+ confidence: number;
101
+ ranking?: ToolRanking[];
102
+ deferred?: boolean;
103
+ refused?: string;
104
+ max_error?: string;
105
+ }
106
+ export interface MultiToolAnswer extends BaseAnswer, SelectionResult {
107
+ type: "tool";
108
+ multi: true;
109
+ tools: string[];
110
+ call: boolean;
111
+ ranking?: ToolRanking[];
112
+ }
113
+ export type ToolAnswer = SingleToolAnswer | MultiToolAnswer;
60
114
  export interface ActAnswer<K extends string = string, A extends string = string> extends BaseAnswer<K> {
61
115
  type: "act";
62
116
  action: A;
63
117
  answer: K;
64
118
  expected_costs: Record<A, number>;
65
119
  confidence: number;
66
- calibration: "validated" | "unvalidated";
120
+ calibration: Calibration;
67
121
  refused?: string;
68
122
  max_error?: string;
123
+ deferred?: boolean;
69
124
  }
70
- export type Answer = ChoiceAnswer | NoulAnswer | ScoreAnswer | MultiAnswer | ActAnswer;
125
+ export type Answer = ChoiceAnswer | NoulAnswer | ScoreAnswer | MultiAnswer | ActAnswer | ToolAnswer;
71
126
  export interface Usage {
72
127
  input_tokens: number;
73
128
  output_tokens: number;
74
129
  questions?: number;
75
130
  branches?: number;
76
131
  latency_ms?: number;
132
+ longest_prompt_tokens?: number;
133
+ second_order_branches?: number;
134
+ media?: MediaUsage;
77
135
  }
78
136
  export interface DecisionResponse {
79
137
  model: string;
@@ -86,6 +144,10 @@ export interface ModelMetadata {
86
144
  release_date?: string;
87
145
  mode?: string;
88
146
  early_exit?: JsonValue[];
147
+ prompt_format?: number;
148
+ orders?: JsonValue;
149
+ temperature_per_prim?: Record<string, number>;
150
+ media?: ("image" | "audio")[];
89
151
  }
90
152
  export interface ModelsResponse {
91
153
  models: ModelMetadata[];
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.image = image;
4
+ exports.audio = audio;
5
+ function media(type, data) {
6
+ if (data instanceof Uint8Array) {
7
+ // Bounded chunks avoid the argument limit of spreading a whole image/audio file.
8
+ const chunks = [];
9
+ for (let offset = 0; offset < data.length; offset += 8192)
10
+ chunks.push(String.fromCharCode(...data.subarray(offset, offset + 8192)));
11
+ data = btoa(chunks.join(''));
12
+ }
13
+ if (typeof data !== 'string' || !data.trim() || /^https?:\/\//i.test(data))
14
+ throw new TypeError('media data must be bytes, base64 or a data URL');
15
+ return { type, data };
16
+ }
17
+ /** Inline image; never fetches a URL or reads a file. */
18
+ function image(data) { return media('image', data); }
19
+ /** Inline audio; never fetches a URL or reads a file. */
20
+ function audio(data) { return media('audio', data); }
@@ -0,0 +1,5 @@
1
+ import type { MediaInput } from './index.cjs';
2
+ /** Inline image; never fetches a URL or reads a file. */
3
+ export declare function image(data: Uint8Array | string): MediaInput;
4
+ /** Inline audio; never fetches a URL or reads a file. */
5
+ export declare function audio(data: Uint8Array | string): MediaInput;
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ActQuestion = exports.MultiQuestion = exports.ScoreQuestion = exports.NoulQuestion = exports.ChoiceQuestion = exports.QuestionSpec = void 0;
3
+ exports.ActQuestion = exports.ToolQuestion = exports.MultiQuestion = exports.ScoreQuestion = exports.NoulQuestion = exports.ChoiceQuestion = exports.QuestionSpec = void 0;
4
4
  exports.choice = choice;
5
5
  exports.noul = noul;
6
6
  exports.score = score;
7
7
  exports.multi = multi;
8
8
  exports.act = act;
9
+ exports.tool = tool;
9
10
  exports.buildRequest = buildRequest;
10
11
  function text(value, name) {
11
12
  if (typeof value !== 'string' || !value.trim())
@@ -51,7 +52,8 @@ class QuestionSpec {
51
52
  const fields = {
52
53
  choice: { choice: 'string', confidence: 'number' }, noul: { noul: 'number', confidence: 'number' },
53
54
  score: { score: 'number', legend: 'object', confidence: 'number' },
54
- multi: { selected: 'object', threshold: 'number', set_confidence: 'number' },
55
+ multi: { selected: 'object', set_confidence: 'number' },
56
+ tool: answer.type === 'tool' && answer.multi ? { tools: 'object', call: 'boolean', set_confidence: 'number' } : { call: 'boolean', confidence: 'number' },
55
57
  act: { action: 'string', answer: 'string', expected_costs: 'object', confidence: 'number', calibration: 'string' },
56
58
  };
57
59
  const data = answer;
@@ -84,14 +86,45 @@ class QuestionSpec {
84
86
  if (answer.type === 'score' && (Array.isArray(answer.legend) || Object.values(answer.legend).some(v => typeof v !== 'string')))
85
87
  throw new TypeError('invalid legend');
86
88
  if (answer.type === 'act') {
87
- if (!['validated', 'unvalidated'].includes(answer.calibration) || !Object.hasOwn(this.wire.costs ?? {}, answer.action) || !expectedKeys.includes(answer.answer))
89
+ if (!['validated', 'unvalidated', 'text'].includes(answer.calibration) || !Object.hasOwn(this.wire.costs ?? {}, answer.action) || !expectedKeys.includes(answer.answer))
88
90
  throw new TypeError('invalid action');
89
91
  if (Array.isArray(answer.expected_costs) || Object.keys(this.wire.costs ?? {}).some(key => !Object.hasOwn(answer.expected_costs, key)))
90
92
  throw new TypeError('invalid expected costs');
91
93
  for (const value of Object.values(answer.expected_costs))
92
94
  number(value, 'expected cost');
93
95
  }
94
- return { ...answer, ...(answer.type === 'multi' ? {} : { evidence: answer.evidence ?? [] }) };
96
+ if (answer.type === 'multi' || (answer.type === 'tool' && answer.multi)) {
97
+ validateSelection(answer, this.wire.objective ?? 'threshold');
98
+ }
99
+ if (answer.type === 'tool') {
100
+ if ((answer.multi ?? false) !== (this.wire.multi ?? false))
101
+ throw new TypeError('mismatched tool mode');
102
+ if (answer.multi) {
103
+ if (!Array.isArray(answer.tools) || answer.tools.some(k => typeof k !== 'string' || !Object.hasOwn(answer.probabilities, k)))
104
+ throw new TypeError('invalid selected tools');
105
+ }
106
+ else {
107
+ if (answer.tool === null ? answer.deferred !== true : typeof answer.tool !== 'string' || !Object.hasOwn(answer.probabilities, answer.tool))
108
+ throw new TypeError('invalid selected tool');
109
+ if (answer.deferred !== undefined && typeof answer.deferred !== 'boolean')
110
+ throw new TypeError('invalid deferred');
111
+ for (const key of ['refused', 'max_error'])
112
+ if (answer[key] !== undefined && typeof answer[key] !== 'string')
113
+ throw new TypeError('invalid tool metadata');
114
+ }
115
+ if (answer.ranking !== undefined) {
116
+ if (!Array.isArray(answer.ranking))
117
+ throw new TypeError('invalid ranking');
118
+ for (const entry of answer.ranking) {
119
+ if (!entry || typeof entry.tool !== 'string' || !Object.hasOwn(answer.probabilities, entry.tool))
120
+ throw new TypeError('invalid ranking entry');
121
+ number(entry.probability, 'ranking probability', true);
122
+ }
123
+ }
124
+ }
125
+ if (answer.type === 'act' && answer.deferred !== undefined && typeof answer.deferred !== 'boolean')
126
+ throw new TypeError('invalid deferred');
127
+ return { ...answer, ...(answer.type === 'multi' || answer.type === 'tool' ? {} : { evidence: answer.evidence ?? [] }) };
95
128
  }
96
129
  }
97
130
  exports.QuestionSpec = QuestionSpec;
@@ -99,7 +132,7 @@ class ChoiceQuestion extends QuestionSpec {
99
132
  constructor(instructions, values, options = {}) {
100
133
  if (options.option_keys !== undefined && !['show', 'hide'].includes(options.option_keys))
101
134
  throw new TypeError('invalid option_keys');
102
- super('choice', instructions, { criteria: criteria(values, 2, 10), ...evidence(options), ...(options.option_keys === 'hide' ? { option_keys: 'hide' } : {}) });
135
+ super('choice', instructions, { criteria: criteria(values, 2, 255), ...evidence(options), ...(options.option_keys === 'hide' ? { option_keys: 'hide' } : {}) });
103
136
  }
104
137
  }
105
138
  exports.ChoiceQuestion = ChoiceQuestion;
@@ -109,36 +142,101 @@ class NoulQuestion extends QuestionSpec {
109
142
  exports.NoulQuestion = NoulQuestion;
110
143
  class ScoreQuestion extends QuestionSpec {
111
144
  constructor(instructions, levels, options = {}) {
112
- if (!Array.isArray(levels) || levels.length < 2 || levels.length > 10)
113
- throw new TypeError('score requires 2–10 ordered levels');
145
+ if (!Array.isArray(levels) || levels.length < 2 || levels.length > 255)
146
+ throw new TypeError('score requires 2–255 ordered levels');
114
147
  super('score', instructions, { criteria: levels.map(v => text(v, 'level')), ...evidence(options) });
115
148
  }
116
149
  }
117
150
  exports.ScoreQuestion = ScoreQuestion;
151
+ function selectionOptions(options, keys) {
152
+ const fields = {};
153
+ const objective = options.objective ?? 'threshold';
154
+ if (!['threshold', 'exact', 'iou', 'f1'].includes(objective))
155
+ throw new TypeError('invalid objective');
156
+ if (objective !== 'threshold')
157
+ fields.objective = objective;
158
+ if (options.threshold !== undefined) {
159
+ if (objective !== 'threshold')
160
+ throw new TypeError('threshold applies only to objective threshold');
161
+ fields.threshold = number(options.threshold, 'threshold', true);
162
+ }
163
+ for (const key of ['min', 'max']) {
164
+ const v = options[key];
165
+ if (v !== undefined) {
166
+ if (!Number.isSafeInteger(v) || v < 0 || (keys && v > keys.length))
167
+ throw new TypeError(`invalid ${key}`);
168
+ fields[key] = v;
169
+ }
170
+ }
171
+ if (options.min !== undefined && options.max !== undefined && options.min > options.max)
172
+ throw new TypeError('min exceeds max');
173
+ if (options.exclusive !== undefined) {
174
+ if (!Array.isArray(options.exclusive))
175
+ throw new TypeError('exclusive must be an array');
176
+ fields.exclusive = options.exclusive.map(k => { text(k, 'exclusive key'); if (keys && !keys.includes(k))
177
+ throw new TypeError('unknown exclusive key'); return k; });
178
+ }
179
+ if (options.fallback !== undefined) {
180
+ fields.fallback = text(options.fallback, 'fallback');
181
+ if (keys && !keys.includes(options.fallback))
182
+ throw new TypeError('unknown fallback');
183
+ }
184
+ if (options.fallback_threshold !== undefined) {
185
+ if (options.fallback === undefined)
186
+ throw new TypeError('fallback_threshold requires fallback');
187
+ fields.fallback_threshold = number(options.fallback_threshold, 'fallback_threshold', true);
188
+ }
189
+ return fields;
190
+ }
191
+ function validateSelection(answer, objective) {
192
+ number(answer.set_confidence, 'set_confidence', true);
193
+ if (objective === 'threshold')
194
+ number(answer.threshold, 'threshold', true);
195
+ else {
196
+ if (answer.objective !== objective)
197
+ throw new TypeError('mismatched objective');
198
+ if (answer.expected_score !== null && answer.expected_score !== undefined)
199
+ number(answer.expected_score, 'expected_score', true);
200
+ }
201
+ }
118
202
  class MultiQuestion extends QuestionSpec {
119
203
  constructor(instructions, values, options = {}) {
120
204
  if ('evidence' in options)
121
205
  throw new TypeError('multi does not support evidence');
122
- const fields = { criteria: criteria(values, 1) };
123
- if (options.threshold !== undefined)
124
- fields.threshold = number(options.threshold, 'threshold', true);
125
- for (const key of ['min', 'max']) {
126
- const value = options[key];
127
- if (value !== undefined) {
128
- if (!Number.isInteger(value) || value < 0 || value > Object.keys(values).length)
129
- throw new TypeError(`invalid ${key}`);
130
- fields[key] = value;
131
- }
132
- }
133
- if (options.min !== undefined && options.max !== undefined && options.min > options.max)
134
- throw new TypeError('min exceeds max');
135
- super('multi', instructions, fields);
206
+ super('multi', instructions, { criteria: criteria(values, 1), ...selectionOptions(options, Object.keys(values)) });
136
207
  }
137
208
  }
138
209
  exports.MultiQuestion = MultiQuestion;
210
+ class ToolQuestion extends QuestionSpec {
211
+ constructor(instructions, tools, options = {}) {
212
+ if (!Array.isArray(tools) || tools.some(t => !t || typeof t !== 'object' || Array.isArray(t)))
213
+ throw new TypeError('tools must contain definition objects');
214
+ if (options.extra !== undefined && (!Array.isArray(options.extra) || options.extra.some(k => !['answer_directly', 'ask_user', 'none'].includes(k))))
215
+ throw new TypeError('invalid extra tool option');
216
+ if (options.multi !== undefined && typeof options.multi !== 'boolean')
217
+ throw new TypeError('invalid multi');
218
+ const fields = { tools, ...(options.extra === undefined ? {} : { extra: options.extra }) };
219
+ if (options.rank !== undefined) {
220
+ if (!Number.isSafeInteger(options.rank) || options.rank < 1)
221
+ throw new TypeError('rank must be a positive integer');
222
+ fields.rank = options.rank;
223
+ }
224
+ if (options.max_error !== undefined)
225
+ fields.max_error = number(options.max_error, 'max_error', true);
226
+ const selection = selectionOptions(options);
227
+ if (!options.multi && Object.keys(selection).length)
228
+ throw new TypeError('selection options require multi');
229
+ if (options.multi) {
230
+ fields.multi = true;
231
+ Object.assign(fields, selection);
232
+ }
233
+ super('tool', instructions, fields);
234
+ }
235
+ }
236
+ exports.ToolQuestion = ToolQuestion;
139
237
  class ActQuestion extends QuestionSpec {
140
238
  constructor(instructions, costs, options = {}) {
141
- const optionsMap = options.criteria === undefined ? undefined : criteria(options.criteria, 2, 10);
239
+ const optionsMap = options.criteria === undefined ? undefined : criteria(options.criteria, 2, 255);
142
240
  const outcomes = Object.keys(optionsMap ?? { 'true': 'Yes', 'false': 'No' });
143
241
  if (!costs || typeof costs !== 'object' || Array.isArray(costs) || !Object.keys(costs).length)
144
242
  throw new TypeError('act requires a cost matrix');
@@ -162,6 +260,7 @@ function noul(instructions, options) { return new NoulQuestion(instructions, opt
162
260
  function score(instructions, levels, options) { return new ScoreQuestion(instructions, levels, options); }
163
261
  function multi(instructions, values, options) { return new MultiQuestion(instructions, values, options); }
164
262
  function act(instructions, costs, options) { return new ActQuestion(instructions, costs, options); }
263
+ function tool(instructions, tools, options) { return new ToolQuestion(instructions, tools, options); }
165
264
  function buildRequest(state, questions, options) {
166
265
  if (state === null || state === undefined)
167
266
  throw new TypeError('state must not be null/undefined');
@@ -175,5 +274,11 @@ function buildRequest(state, questions, options) {
175
274
  text(options.model, 'model');
176
275
  if (options.facts !== undefined && !['off', 'auto'].includes(options.facts))
177
276
  throw new TypeError('invalid facts');
178
- return { state, questions: Object.fromEntries(entries), ...(options.model === undefined ? {} : { model: options.model }), ...(options.facts === undefined ? {} : { facts: options.facts }) };
277
+ if (options.lang !== undefined && !['pl', 'en'].includes(options.lang))
278
+ throw new TypeError('invalid lang');
279
+ if (options.detail !== undefined && !['default', 'high'].includes(options.detail))
280
+ throw new TypeError('invalid detail');
281
+ if (options.media !== undefined && (!Array.isArray(options.media) || options.media.length < 1 || options.media.length > 8 || options.media.some(m => !m || !['image', 'audio'].includes(m.type) || typeof m.data !== 'string' || !m.data.trim())))
282
+ throw new TypeError('invalid media');
283
+ return { state, questions: Object.fromEntries(entries), ...(options.lang === undefined ? {} : { lang: options.lang }), ...(options.detail === undefined ? {} : { detail: options.detail }), ...(options.media === undefined ? {} : { media: structuredClone(options.media) }), ...(options.model === undefined ? {} : { model: options.model }), ...(options.facts === undefined ? {} : { facts: options.facts }) };
179
284
  }
@@ -1,4 +1,4 @@
1
- import type { ActAnswer, Answer, ChoiceAnswer, DecisionResponse, EvidenceSpan, JsonValue, MultiAnswer, NoulAnswer, Question, ScoreAnswer } from './index.cjs';
1
+ import type { ActAnswer, Answer, ChoiceAnswer, DecisionResponse, EvidenceSpan, JsonValue, MultiAnswer, NoulAnswer, Question, ScoreAnswer, ToolAnswer, ToolDefinition, MultiObjective, DecisionRequest } from './index.cjs';
2
2
  /** Required result metadata on the typed path; missing spans become an empty array. */
3
3
  export type TypedChoiceAnswer<K extends string = string> = ChoiceAnswer<K> & {
4
4
  evidence: EvidenceSpan[];
@@ -19,19 +19,27 @@ export interface EvidenceOptions {
19
19
  export interface ChoiceOptions extends EvidenceOptions {
20
20
  option_keys?: 'show' | 'hide';
21
21
  }
22
- export interface MultiOptions {
22
+ export interface MultiOptions<K extends string = string> {
23
23
  threshold?: number;
24
24
  min?: number;
25
25
  max?: number;
26
+ objective?: MultiObjective;
27
+ exclusive?: readonly K[];
28
+ fallback?: K;
29
+ fallback_threshold?: number;
30
+ }
31
+ export interface ToolOptions extends MultiOptions {
32
+ extra?: readonly ("answer_directly" | "ask_user" | "none")[];
33
+ rank?: number;
34
+ multi?: boolean;
35
+ max_error?: number;
26
36
  }
27
37
  export interface ActOptions<K extends string = string, A extends string = string> extends EvidenceOptions {
28
38
  criteria?: Record<K, string>;
29
39
  defer_action?: A;
30
40
  max_error?: number;
31
41
  }
32
- export interface DecisionOptions {
33
- model?: string;
34
- facts?: 'off' | 'auto';
42
+ export interface DecisionOptions extends Pick<DecisionRequest, 'model' | 'facts' | 'lang' | 'media' | 'detail'> {
35
43
  signal?: AbortSignal;
36
44
  }
37
45
  /** Immutable snapshot of a typed question. Use factories or the concrete constructors. */
@@ -52,7 +60,10 @@ export declare class ScoreQuestion extends QuestionSpec<TypedScoreAnswer> {
52
60
  constructor(instructions: string, levels: readonly string[], options?: EvidenceOptions);
53
61
  }
54
62
  export declare class MultiQuestion<K extends string = string> extends QuestionSpec<TypedMultiAnswer<K>> {
55
- constructor(instructions: string, values: Record<K, string>, options?: MultiOptions);
63
+ constructor(instructions: string, values: Record<K, string>, options?: MultiOptions<K>);
64
+ }
65
+ export declare class ToolQuestion extends QuestionSpec<ToolAnswer> {
66
+ constructor(instructions: string, tools: readonly ToolDefinition[], options?: ToolOptions);
56
67
  }
57
68
  export declare class ActQuestion<K extends string = 'true' | 'false', A extends string = string> extends QuestionSpec<TypedActAnswer<K, A>> {
58
69
  constructor(instructions: string, costs: Record<A, Record<NoInfer<K>, number>>, options?: ActOptions<K, NoInfer<A>>);
@@ -60,8 +71,9 @@ export declare class ActQuestion<K extends string = 'true' | 'false', A extends
60
71
  export declare function choice<const K extends string>(instructions: string, values: Record<K, string>, options?: ChoiceOptions): ChoiceQuestion<K>;
61
72
  export declare function noul(instructions: string, options?: EvidenceOptions): NoulQuestion;
62
73
  export declare function score(instructions: string, levels: readonly string[], options?: EvidenceOptions): ScoreQuestion;
63
- export declare function multi<const K extends string>(instructions: string, values: Record<K, string>, options?: MultiOptions): MultiQuestion<K>;
74
+ export declare function multi<const K extends string>(instructions: string, values: Record<K, string>, options?: MultiOptions<K>): MultiQuestion<K>;
64
75
  export declare function act<const K extends string = 'true' | 'false', const A extends string = string>(instructions: string, costs: Record<A, Record<NoInfer<K>, number>>, options?: ActOptions<K, NoInfer<A>>): ActQuestion<K, A>;
76
+ export declare function tool(instructions: string, tools: readonly ToolDefinition[], options?: ToolOptions): ToolQuestion;
65
77
  export type QuestionSet = Record<string, QuestionSpec>;
66
78
  export type TypedDecisionResponse<Q extends QuestionSet> = Omit<DecisionResponse, 'answers'> & {
67
79
  answers: {
@@ -70,9 +82,4 @@ export type TypedDecisionResponse<Q extends QuestionSet> = Omit<DecisionResponse
70
82
  answer<K extends keyof Q & (string | number)>(key: K): Q[K] extends QuestionSpec<infer A> ? A : never;
71
83
  raw: DecisionResponse;
72
84
  };
73
- export declare function buildRequest(state: JsonValue, questions: QuestionSet, options: DecisionOptions): {
74
- state: JsonValue;
75
- questions: Record<string, Question>;
76
- model?: string;
77
- facts?: 'off' | 'auto';
78
- };
85
+ export declare function buildRequest(state: JsonValue, questions: QuestionSet, options: DecisionOptions): DecisionRequest;
@@ -1,11 +1,29 @@
1
1
  import { type QuestionSet, type TypedDecisionResponse, type DecisionOptions } from "./questions.js";
2
2
  export * from "./questions.js";
3
+ export * from "./media.js";
3
4
  /** Native Basal wire types. Criterion order is significant; do not sort keys. */
4
5
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
5
6
  [key: string]: JsonValue;
6
7
  };
8
+ export type Calibration = "validated" | "unvalidated" | "text";
9
+ export type MultiObjective = "threshold" | "exact" | "iou" | "f1";
10
+ export interface MediaInput {
11
+ type: "image" | "audio";
12
+ data: string;
13
+ }
14
+ export interface MediaUsage {
15
+ images: number;
16
+ audio: number;
17
+ media_tokens: number;
18
+ detail: "default" | "high";
19
+ }
20
+ export interface ToolRanking {
21
+ tool: string;
22
+ probability: number;
23
+ }
24
+ export type ToolDefinition = Record<string, JsonValue>;
7
25
  export interface Question {
8
- type?: "choice" | "noul" | "score" | "multi" | "act";
26
+ type?: "choice" | "noul" | "score" | "multi" | "act" | "tool";
9
27
  instructions?: JsonValue;
10
28
  criteria?: Record<string, JsonValue> | JsonValue[];
11
29
  levels?: Record<string, JsonValue> | JsonValue[];
@@ -18,12 +36,23 @@ export interface Question {
18
36
  costs?: Record<string, number> | Record<string, Record<string, number>>;
19
37
  defer_action?: string;
20
38
  max_error?: number;
39
+ objective?: MultiObjective;
40
+ exclusive?: readonly string[];
41
+ fallback?: string;
42
+ fallback_threshold?: number;
43
+ tools?: readonly ToolDefinition[];
44
+ extra?: readonly ("answer_directly" | "ask_user" | "none")[];
45
+ rank?: number;
46
+ multi?: boolean;
21
47
  }
22
48
  export interface DecisionRequest {
23
49
  state: JsonValue;
24
50
  questions: Record<string, Question>;
25
51
  model?: string;
26
52
  facts?: "off" | "auto";
53
+ lang?: "pl" | "en";
54
+ media?: readonly MediaInput[];
55
+ detail?: "default" | "high";
27
56
  }
28
57
  export interface EvidenceSpan {
29
58
  text: string;
@@ -34,6 +63,7 @@ export interface EvidenceSpan {
34
63
  interface BaseAnswer<K extends string = string> {
35
64
  probabilities: Record<K, number>;
36
65
  evidence?: EvidenceSpan[];
66
+ calibration?: Calibration;
37
67
  }
38
68
  export interface ChoiceAnswer<K extends string = string> extends BaseAnswer<K> {
39
69
  type: "choice";
@@ -51,29 +81,57 @@ export interface ScoreAnswer extends BaseAnswer {
51
81
  legend: Record<string, string>;
52
82
  confidence: number;
53
83
  }
54
- export interface MultiAnswer<K extends string = string> extends BaseAnswer<K> {
84
+ export interface SelectionResult<K extends string = string> {
85
+ threshold?: number;
86
+ objective?: MultiObjective;
87
+ expected_score?: number | null;
88
+ constraints_applied?: JsonValue;
89
+ set_confidence: number;
90
+ }
91
+ export interface MultiAnswer<K extends string = string> extends BaseAnswer<K>, SelectionResult<K> {
55
92
  type: "multi";
56
93
  selected: K[];
57
- threshold: number;
58
- set_confidence: number;
59
94
  }
95
+ export interface SingleToolAnswer extends BaseAnswer {
96
+ type: "tool";
97
+ multi?: false;
98
+ tool: string | null;
99
+ call: boolean;
100
+ confidence: number;
101
+ ranking?: ToolRanking[];
102
+ deferred?: boolean;
103
+ refused?: string;
104
+ max_error?: string;
105
+ }
106
+ export interface MultiToolAnswer extends BaseAnswer, SelectionResult {
107
+ type: "tool";
108
+ multi: true;
109
+ tools: string[];
110
+ call: boolean;
111
+ ranking?: ToolRanking[];
112
+ }
113
+ export type ToolAnswer = SingleToolAnswer | MultiToolAnswer;
60
114
  export interface ActAnswer<K extends string = string, A extends string = string> extends BaseAnswer<K> {
61
115
  type: "act";
62
116
  action: A;
63
117
  answer: K;
64
118
  expected_costs: Record<A, number>;
65
119
  confidence: number;
66
- calibration: "validated" | "unvalidated";
120
+ calibration: Calibration;
67
121
  refused?: string;
68
122
  max_error?: string;
123
+ deferred?: boolean;
69
124
  }
70
- export type Answer = ChoiceAnswer | NoulAnswer | ScoreAnswer | MultiAnswer | ActAnswer;
125
+ export type Answer = ChoiceAnswer | NoulAnswer | ScoreAnswer | MultiAnswer | ActAnswer | ToolAnswer;
71
126
  export interface Usage {
72
127
  input_tokens: number;
73
128
  output_tokens: number;
74
129
  questions?: number;
75
130
  branches?: number;
76
131
  latency_ms?: number;
132
+ longest_prompt_tokens?: number;
133
+ second_order_branches?: number;
134
+ media?: MediaUsage;
77
135
  }
78
136
  export interface DecisionResponse {
79
137
  model: string;
@@ -86,6 +144,10 @@ export interface ModelMetadata {
86
144
  release_date?: string;
87
145
  mode?: string;
88
146
  early_exit?: JsonValue[];
147
+ prompt_format?: number;
148
+ orders?: JsonValue;
149
+ temperature_per_prim?: Record<string, number>;
150
+ media?: ("image" | "audio")[];
89
151
  }
90
152
  export interface ModelsResponse {
91
153
  models: ModelMetadata[];
package/dist/esm/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { buildRequest } from "./questions.js";
2
2
  export * from "./questions.js";
3
+ export * from "./media.js";
3
4
  export class BasalError extends Error {
4
5
  }
5
6
  /** Non-2xx response. Server body and Retry-After are available without logging them. */
@@ -0,0 +1,5 @@
1
+ import type { MediaInput } from './index.js';
2
+ /** Inline image; never fetches a URL or reads a file. */
3
+ export declare function image(data: Uint8Array | string): MediaInput;
4
+ /** Inline audio; never fetches a URL or reads a file. */
5
+ export declare function audio(data: Uint8Array | string): MediaInput;
@@ -0,0 +1,16 @@
1
+ function media(type, data) {
2
+ if (data instanceof Uint8Array) {
3
+ // Bounded chunks avoid the argument limit of spreading a whole image/audio file.
4
+ const chunks = [];
5
+ for (let offset = 0; offset < data.length; offset += 8192)
6
+ chunks.push(String.fromCharCode(...data.subarray(offset, offset + 8192)));
7
+ data = btoa(chunks.join(''));
8
+ }
9
+ if (typeof data !== 'string' || !data.trim() || /^https?:\/\//i.test(data))
10
+ throw new TypeError('media data must be bytes, base64 or a data URL');
11
+ return { type, data };
12
+ }
13
+ /** Inline image; never fetches a URL or reads a file. */
14
+ export function image(data) { return media('image', data); }
15
+ /** Inline audio; never fetches a URL or reads a file. */
16
+ export function audio(data) { return media('audio', data); }
@@ -1,4 +1,4 @@
1
- import type { ActAnswer, Answer, ChoiceAnswer, DecisionResponse, EvidenceSpan, JsonValue, MultiAnswer, NoulAnswer, Question, ScoreAnswer } from './index.js';
1
+ import type { ActAnswer, Answer, ChoiceAnswer, DecisionResponse, EvidenceSpan, JsonValue, MultiAnswer, NoulAnswer, Question, ScoreAnswer, ToolAnswer, ToolDefinition, MultiObjective, DecisionRequest } from './index.js';
2
2
  /** Required result metadata on the typed path; missing spans become an empty array. */
3
3
  export type TypedChoiceAnswer<K extends string = string> = ChoiceAnswer<K> & {
4
4
  evidence: EvidenceSpan[];
@@ -19,19 +19,27 @@ export interface EvidenceOptions {
19
19
  export interface ChoiceOptions extends EvidenceOptions {
20
20
  option_keys?: 'show' | 'hide';
21
21
  }
22
- export interface MultiOptions {
22
+ export interface MultiOptions<K extends string = string> {
23
23
  threshold?: number;
24
24
  min?: number;
25
25
  max?: number;
26
+ objective?: MultiObjective;
27
+ exclusive?: readonly K[];
28
+ fallback?: K;
29
+ fallback_threshold?: number;
30
+ }
31
+ export interface ToolOptions extends MultiOptions {
32
+ extra?: readonly ("answer_directly" | "ask_user" | "none")[];
33
+ rank?: number;
34
+ multi?: boolean;
35
+ max_error?: number;
26
36
  }
27
37
  export interface ActOptions<K extends string = string, A extends string = string> extends EvidenceOptions {
28
38
  criteria?: Record<K, string>;
29
39
  defer_action?: A;
30
40
  max_error?: number;
31
41
  }
32
- export interface DecisionOptions {
33
- model?: string;
34
- facts?: 'off' | 'auto';
42
+ export interface DecisionOptions extends Pick<DecisionRequest, 'model' | 'facts' | 'lang' | 'media' | 'detail'> {
35
43
  signal?: AbortSignal;
36
44
  }
37
45
  /** Immutable snapshot of a typed question. Use factories or the concrete constructors. */
@@ -52,7 +60,10 @@ export declare class ScoreQuestion extends QuestionSpec<TypedScoreAnswer> {
52
60
  constructor(instructions: string, levels: readonly string[], options?: EvidenceOptions);
53
61
  }
54
62
  export declare class MultiQuestion<K extends string = string> extends QuestionSpec<TypedMultiAnswer<K>> {
55
- constructor(instructions: string, values: Record<K, string>, options?: MultiOptions);
63
+ constructor(instructions: string, values: Record<K, string>, options?: MultiOptions<K>);
64
+ }
65
+ export declare class ToolQuestion extends QuestionSpec<ToolAnswer> {
66
+ constructor(instructions: string, tools: readonly ToolDefinition[], options?: ToolOptions);
56
67
  }
57
68
  export declare class ActQuestion<K extends string = 'true' | 'false', A extends string = string> extends QuestionSpec<TypedActAnswer<K, A>> {
58
69
  constructor(instructions: string, costs: Record<A, Record<NoInfer<K>, number>>, options?: ActOptions<K, NoInfer<A>>);
@@ -60,8 +71,9 @@ export declare class ActQuestion<K extends string = 'true' | 'false', A extends
60
71
  export declare function choice<const K extends string>(instructions: string, values: Record<K, string>, options?: ChoiceOptions): ChoiceQuestion<K>;
61
72
  export declare function noul(instructions: string, options?: EvidenceOptions): NoulQuestion;
62
73
  export declare function score(instructions: string, levels: readonly string[], options?: EvidenceOptions): ScoreQuestion;
63
- export declare function multi<const K extends string>(instructions: string, values: Record<K, string>, options?: MultiOptions): MultiQuestion<K>;
74
+ export declare function multi<const K extends string>(instructions: string, values: Record<K, string>, options?: MultiOptions<K>): MultiQuestion<K>;
64
75
  export declare function act<const K extends string = 'true' | 'false', const A extends string = string>(instructions: string, costs: Record<A, Record<NoInfer<K>, number>>, options?: ActOptions<K, NoInfer<A>>): ActQuestion<K, A>;
76
+ export declare function tool(instructions: string, tools: readonly ToolDefinition[], options?: ToolOptions): ToolQuestion;
65
77
  export type QuestionSet = Record<string, QuestionSpec>;
66
78
  export type TypedDecisionResponse<Q extends QuestionSet> = Omit<DecisionResponse, 'answers'> & {
67
79
  answers: {
@@ -70,9 +82,4 @@ export type TypedDecisionResponse<Q extends QuestionSet> = Omit<DecisionResponse
70
82
  answer<K extends keyof Q & (string | number)>(key: K): Q[K] extends QuestionSpec<infer A> ? A : never;
71
83
  raw: DecisionResponse;
72
84
  };
73
- export declare function buildRequest(state: JsonValue, questions: QuestionSet, options: DecisionOptions): {
74
- state: JsonValue;
75
- questions: Record<string, Question>;
76
- model?: string;
77
- facts?: 'off' | 'auto';
78
- };
85
+ export declare function buildRequest(state: JsonValue, questions: QuestionSet, options: DecisionOptions): DecisionRequest;
@@ -42,7 +42,8 @@ export class QuestionSpec {
42
42
  const fields = {
43
43
  choice: { choice: 'string', confidence: 'number' }, noul: { noul: 'number', confidence: 'number' },
44
44
  score: { score: 'number', legend: 'object', confidence: 'number' },
45
- multi: { selected: 'object', threshold: 'number', set_confidence: 'number' },
45
+ multi: { selected: 'object', set_confidence: 'number' },
46
+ tool: answer.type === 'tool' && answer.multi ? { tools: 'object', call: 'boolean', set_confidence: 'number' } : { call: 'boolean', confidence: 'number' },
46
47
  act: { action: 'string', answer: 'string', expected_costs: 'object', confidence: 'number', calibration: 'string' },
47
48
  };
48
49
  const data = answer;
@@ -75,21 +76,52 @@ export class QuestionSpec {
75
76
  if (answer.type === 'score' && (Array.isArray(answer.legend) || Object.values(answer.legend).some(v => typeof v !== 'string')))
76
77
  throw new TypeError('invalid legend');
77
78
  if (answer.type === 'act') {
78
- if (!['validated', 'unvalidated'].includes(answer.calibration) || !Object.hasOwn(this.wire.costs ?? {}, answer.action) || !expectedKeys.includes(answer.answer))
79
+ if (!['validated', 'unvalidated', 'text'].includes(answer.calibration) || !Object.hasOwn(this.wire.costs ?? {}, answer.action) || !expectedKeys.includes(answer.answer))
79
80
  throw new TypeError('invalid action');
80
81
  if (Array.isArray(answer.expected_costs) || Object.keys(this.wire.costs ?? {}).some(key => !Object.hasOwn(answer.expected_costs, key)))
81
82
  throw new TypeError('invalid expected costs');
82
83
  for (const value of Object.values(answer.expected_costs))
83
84
  number(value, 'expected cost');
84
85
  }
85
- return { ...answer, ...(answer.type === 'multi' ? {} : { evidence: answer.evidence ?? [] }) };
86
+ if (answer.type === 'multi' || (answer.type === 'tool' && answer.multi)) {
87
+ validateSelection(answer, this.wire.objective ?? 'threshold');
88
+ }
89
+ if (answer.type === 'tool') {
90
+ if ((answer.multi ?? false) !== (this.wire.multi ?? false))
91
+ throw new TypeError('mismatched tool mode');
92
+ if (answer.multi) {
93
+ if (!Array.isArray(answer.tools) || answer.tools.some(k => typeof k !== 'string' || !Object.hasOwn(answer.probabilities, k)))
94
+ throw new TypeError('invalid selected tools');
95
+ }
96
+ else {
97
+ if (answer.tool === null ? answer.deferred !== true : typeof answer.tool !== 'string' || !Object.hasOwn(answer.probabilities, answer.tool))
98
+ throw new TypeError('invalid selected tool');
99
+ if (answer.deferred !== undefined && typeof answer.deferred !== 'boolean')
100
+ throw new TypeError('invalid deferred');
101
+ for (const key of ['refused', 'max_error'])
102
+ if (answer[key] !== undefined && typeof answer[key] !== 'string')
103
+ throw new TypeError('invalid tool metadata');
104
+ }
105
+ if (answer.ranking !== undefined) {
106
+ if (!Array.isArray(answer.ranking))
107
+ throw new TypeError('invalid ranking');
108
+ for (const entry of answer.ranking) {
109
+ if (!entry || typeof entry.tool !== 'string' || !Object.hasOwn(answer.probabilities, entry.tool))
110
+ throw new TypeError('invalid ranking entry');
111
+ number(entry.probability, 'ranking probability', true);
112
+ }
113
+ }
114
+ }
115
+ if (answer.type === 'act' && answer.deferred !== undefined && typeof answer.deferred !== 'boolean')
116
+ throw new TypeError('invalid deferred');
117
+ return { ...answer, ...(answer.type === 'multi' || answer.type === 'tool' ? {} : { evidence: answer.evidence ?? [] }) };
86
118
  }
87
119
  }
88
120
  export class ChoiceQuestion extends QuestionSpec {
89
121
  constructor(instructions, values, options = {}) {
90
122
  if (options.option_keys !== undefined && !['show', 'hide'].includes(options.option_keys))
91
123
  throw new TypeError('invalid option_keys');
92
- super('choice', instructions, { criteria: criteria(values, 2, 10), ...evidence(options), ...(options.option_keys === 'hide' ? { option_keys: 'hide' } : {}) });
124
+ super('choice', instructions, { criteria: criteria(values, 2, 255), ...evidence(options), ...(options.option_keys === 'hide' ? { option_keys: 'hide' } : {}) });
93
125
  }
94
126
  }
95
127
  export class NoulQuestion extends QuestionSpec {
@@ -97,34 +129,98 @@ export class NoulQuestion extends QuestionSpec {
97
129
  }
98
130
  export class ScoreQuestion extends QuestionSpec {
99
131
  constructor(instructions, levels, options = {}) {
100
- if (!Array.isArray(levels) || levels.length < 2 || levels.length > 10)
101
- throw new TypeError('score requires 2–10 ordered levels');
132
+ if (!Array.isArray(levels) || levels.length < 2 || levels.length > 255)
133
+ throw new TypeError('score requires 2–255 ordered levels');
102
134
  super('score', instructions, { criteria: levels.map(v => text(v, 'level')), ...evidence(options) });
103
135
  }
104
136
  }
137
+ function selectionOptions(options, keys) {
138
+ const fields = {};
139
+ const objective = options.objective ?? 'threshold';
140
+ if (!['threshold', 'exact', 'iou', 'f1'].includes(objective))
141
+ throw new TypeError('invalid objective');
142
+ if (objective !== 'threshold')
143
+ fields.objective = objective;
144
+ if (options.threshold !== undefined) {
145
+ if (objective !== 'threshold')
146
+ throw new TypeError('threshold applies only to objective threshold');
147
+ fields.threshold = number(options.threshold, 'threshold', true);
148
+ }
149
+ for (const key of ['min', 'max']) {
150
+ const v = options[key];
151
+ if (v !== undefined) {
152
+ if (!Number.isSafeInteger(v) || v < 0 || (keys && v > keys.length))
153
+ throw new TypeError(`invalid ${key}`);
154
+ fields[key] = v;
155
+ }
156
+ }
157
+ if (options.min !== undefined && options.max !== undefined && options.min > options.max)
158
+ throw new TypeError('min exceeds max');
159
+ if (options.exclusive !== undefined) {
160
+ if (!Array.isArray(options.exclusive))
161
+ throw new TypeError('exclusive must be an array');
162
+ fields.exclusive = options.exclusive.map(k => { text(k, 'exclusive key'); if (keys && !keys.includes(k))
163
+ throw new TypeError('unknown exclusive key'); return k; });
164
+ }
165
+ if (options.fallback !== undefined) {
166
+ fields.fallback = text(options.fallback, 'fallback');
167
+ if (keys && !keys.includes(options.fallback))
168
+ throw new TypeError('unknown fallback');
169
+ }
170
+ if (options.fallback_threshold !== undefined) {
171
+ if (options.fallback === undefined)
172
+ throw new TypeError('fallback_threshold requires fallback');
173
+ fields.fallback_threshold = number(options.fallback_threshold, 'fallback_threshold', true);
174
+ }
175
+ return fields;
176
+ }
177
+ function validateSelection(answer, objective) {
178
+ number(answer.set_confidence, 'set_confidence', true);
179
+ if (objective === 'threshold')
180
+ number(answer.threshold, 'threshold', true);
181
+ else {
182
+ if (answer.objective !== objective)
183
+ throw new TypeError('mismatched objective');
184
+ if (answer.expected_score !== null && answer.expected_score !== undefined)
185
+ number(answer.expected_score, 'expected_score', true);
186
+ }
187
+ }
105
188
  export class MultiQuestion extends QuestionSpec {
106
189
  constructor(instructions, values, options = {}) {
107
190
  if ('evidence' in options)
108
191
  throw new TypeError('multi does not support evidence');
109
- const fields = { criteria: criteria(values, 1) };
110
- if (options.threshold !== undefined)
111
- fields.threshold = number(options.threshold, 'threshold', true);
112
- for (const key of ['min', 'max']) {
113
- const value = options[key];
114
- if (value !== undefined) {
115
- if (!Number.isInteger(value) || value < 0 || value > Object.keys(values).length)
116
- throw new TypeError(`invalid ${key}`);
117
- fields[key] = value;
118
- }
192
+ super('multi', instructions, { criteria: criteria(values, 1), ...selectionOptions(options, Object.keys(values)) });
193
+ }
194
+ }
195
+ export class ToolQuestion extends QuestionSpec {
196
+ constructor(instructions, tools, options = {}) {
197
+ if (!Array.isArray(tools) || tools.some(t => !t || typeof t !== 'object' || Array.isArray(t)))
198
+ throw new TypeError('tools must contain definition objects');
199
+ if (options.extra !== undefined && (!Array.isArray(options.extra) || options.extra.some(k => !['answer_directly', 'ask_user', 'none'].includes(k))))
200
+ throw new TypeError('invalid extra tool option');
201
+ if (options.multi !== undefined && typeof options.multi !== 'boolean')
202
+ throw new TypeError('invalid multi');
203
+ const fields = { tools, ...(options.extra === undefined ? {} : { extra: options.extra }) };
204
+ if (options.rank !== undefined) {
205
+ if (!Number.isSafeInteger(options.rank) || options.rank < 1)
206
+ throw new TypeError('rank must be a positive integer');
207
+ fields.rank = options.rank;
208
+ }
209
+ if (options.max_error !== undefined)
210
+ fields.max_error = number(options.max_error, 'max_error', true);
211
+ const selection = selectionOptions(options);
212
+ if (!options.multi && Object.keys(selection).length)
213
+ throw new TypeError('selection options require multi');
214
+ if (options.multi) {
215
+ fields.multi = true;
216
+ Object.assign(fields, selection);
119
217
  }
120
- if (options.min !== undefined && options.max !== undefined && options.min > options.max)
121
- throw new TypeError('min exceeds max');
122
- super('multi', instructions, fields);
218
+ super('tool', instructions, fields);
123
219
  }
124
220
  }
125
221
  export class ActQuestion extends QuestionSpec {
126
222
  constructor(instructions, costs, options = {}) {
127
- const optionsMap = options.criteria === undefined ? undefined : criteria(options.criteria, 2, 10);
223
+ const optionsMap = options.criteria === undefined ? undefined : criteria(options.criteria, 2, 255);
128
224
  const outcomes = Object.keys(optionsMap ?? { 'true': 'Yes', 'false': 'No' });
129
225
  if (!costs || typeof costs !== 'object' || Array.isArray(costs) || !Object.keys(costs).length)
130
226
  throw new TypeError('act requires a cost matrix');
@@ -147,6 +243,7 @@ export function noul(instructions, options) { return new NoulQuestion(instructio
147
243
  export function score(instructions, levels, options) { return new ScoreQuestion(instructions, levels, options); }
148
244
  export function multi(instructions, values, options) { return new MultiQuestion(instructions, values, options); }
149
245
  export function act(instructions, costs, options) { return new ActQuestion(instructions, costs, options); }
246
+ export function tool(instructions, tools, options) { return new ToolQuestion(instructions, tools, options); }
150
247
  export function buildRequest(state, questions, options) {
151
248
  if (state === null || state === undefined)
152
249
  throw new TypeError('state must not be null/undefined');
@@ -160,5 +257,11 @@ export function buildRequest(state, questions, options) {
160
257
  text(options.model, 'model');
161
258
  if (options.facts !== undefined && !['off', 'auto'].includes(options.facts))
162
259
  throw new TypeError('invalid facts');
163
- return { state, questions: Object.fromEntries(entries), ...(options.model === undefined ? {} : { model: options.model }), ...(options.facts === undefined ? {} : { facts: options.facts }) };
260
+ if (options.lang !== undefined && !['pl', 'en'].includes(options.lang))
261
+ throw new TypeError('invalid lang');
262
+ if (options.detail !== undefined && !['default', 'high'].includes(options.detail))
263
+ throw new TypeError('invalid detail');
264
+ if (options.media !== undefined && (!Array.isArray(options.media) || options.media.length < 1 || options.media.length > 8 || options.media.some(m => !m || !['image', 'audio'].includes(m.type) || typeof m.data !== 'string' || !m.data.trim())))
265
+ throw new TypeError('invalid media');
266
+ return { state, questions: Object.fromEntries(entries), ...(options.lang === undefined ? {} : { lang: options.lang }), ...(options.detail === undefined ? {} : { detail: options.detail }), ...(options.media === undefined ? {} : { media: structuredClone(options.media) }), ...(options.model === undefined ? {} : { model: options.model }), ...(options.facts === undefined ? {} : { facts: options.facts }) };
164
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itsoltech/basal-client",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript and JavaScript client for the native Basal HTTP API",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",