@bandito-ai/sdk 0.1.7
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 +162 -0
- package/dist/index.d.mts +145 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +640 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +597 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -0
- package/wasm/bandito_engine.d.ts +46 -0
- package/wasm/bandito_engine.js +283 -0
- package/wasm/bandito_engine_bg.wasm +0 -0
- package/wasm/bandito_engine_bg.wasm.d.ts +19 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BanditoClient: () => BanditoClient,
|
|
34
|
+
close: () => close,
|
|
35
|
+
connect: () => connect,
|
|
36
|
+
grade: () => grade,
|
|
37
|
+
pull: () => pull,
|
|
38
|
+
sync: () => sync,
|
|
39
|
+
update: () => update
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/client.ts
|
|
44
|
+
var import_node_crypto = require("crypto");
|
|
45
|
+
var fs2 = __toESM(require("fs"));
|
|
46
|
+
var path2 = __toESM(require("path"));
|
|
47
|
+
var os2 = __toESM(require("os"));
|
|
48
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
49
|
+
|
|
50
|
+
// src/engine.ts
|
|
51
|
+
var wasmModule = null;
|
|
52
|
+
async function initWasm() {
|
|
53
|
+
if (wasmModule) return;
|
|
54
|
+
wasmModule = await import("../wasm/bandito_engine");
|
|
55
|
+
}
|
|
56
|
+
function createEngine(banditJson) {
|
|
57
|
+
if (!wasmModule) {
|
|
58
|
+
throw new Error("WASM not initialized \u2014 call initWasm() first");
|
|
59
|
+
}
|
|
60
|
+
return new wasmModule.BanditEngine(banditJson);
|
|
61
|
+
}
|
|
62
|
+
function updateEngine(engine, banditJson) {
|
|
63
|
+
engine.updateFromSync(banditJson);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/models.ts
|
|
67
|
+
function createArm(data) {
|
|
68
|
+
const arm = {
|
|
69
|
+
armId: data.arm_id,
|
|
70
|
+
modelName: data.model_name,
|
|
71
|
+
modelProvider: data.model_provider,
|
|
72
|
+
systemPrompt: data.system_prompt,
|
|
73
|
+
isPromptTemplated: data.is_prompt_templated ?? false,
|
|
74
|
+
get model() {
|
|
75
|
+
return this.modelName;
|
|
76
|
+
},
|
|
77
|
+
get prompt() {
|
|
78
|
+
return this.systemPrompt;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return Object.freeze(arm);
|
|
82
|
+
}
|
|
83
|
+
function createPullResult(data) {
|
|
84
|
+
const result = {
|
|
85
|
+
arm: data.arm,
|
|
86
|
+
eventId: data.eventId,
|
|
87
|
+
banditId: data.banditId,
|
|
88
|
+
banditName: data.banditName,
|
|
89
|
+
scores: Object.freeze({ ...data.scores }),
|
|
90
|
+
get model() {
|
|
91
|
+
return this.arm.modelName;
|
|
92
|
+
},
|
|
93
|
+
get prompt() {
|
|
94
|
+
return this.arm.systemPrompt;
|
|
95
|
+
},
|
|
96
|
+
_pullTime: data.pullTime
|
|
97
|
+
};
|
|
98
|
+
return Object.freeze(result);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/config.ts
|
|
102
|
+
var fs = __toESM(require("fs"));
|
|
103
|
+
var path = __toESM(require("path"));
|
|
104
|
+
var os = __toESM(require("os"));
|
|
105
|
+
var import_smol_toml = require("smol-toml");
|
|
106
|
+
var DEFAULT_BASE_URL = "https://bandito-api.onrender.com";
|
|
107
|
+
var CONFIG_DIR = path.join(os.homedir(), ".bandito");
|
|
108
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.toml");
|
|
109
|
+
function loadConfig() {
|
|
110
|
+
const config = {
|
|
111
|
+
apiKey: null,
|
|
112
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
113
|
+
dataStorage: "local"
|
|
114
|
+
};
|
|
115
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
116
|
+
try {
|
|
117
|
+
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
118
|
+
const data = (0, import_smol_toml.parse)(content);
|
|
119
|
+
if (data.api_key) config.apiKey = data.api_key;
|
|
120
|
+
if (data.base_url) config.baseUrl = data.base_url;
|
|
121
|
+
if (data.data_storage) config.dataStorage = data.data_storage;
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const envKey = process.env.BANDITO_API_KEY;
|
|
126
|
+
if (envKey) config.apiKey = envKey;
|
|
127
|
+
const envUrl = process.env.BANDITO_BASE_URL;
|
|
128
|
+
if (envUrl) config.baseUrl = envUrl;
|
|
129
|
+
const envStorage = process.env.BANDITO_DATA_STORAGE;
|
|
130
|
+
if (envStorage) config.dataStorage = envStorage;
|
|
131
|
+
return config;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/http.ts
|
|
135
|
+
var MAX_RETRIES = 3;
|
|
136
|
+
var RETRY_BACKOFF_BASE = 500;
|
|
137
|
+
function sleep(ms) {
|
|
138
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
139
|
+
}
|
|
140
|
+
function isRetryable(status) {
|
|
141
|
+
return status >= 500;
|
|
142
|
+
}
|
|
143
|
+
var BanditoHTTP = class {
|
|
144
|
+
baseUrl;
|
|
145
|
+
apiKey;
|
|
146
|
+
timeout;
|
|
147
|
+
constructor(baseUrl, apiKey, timeout = 1e4) {
|
|
148
|
+
this.baseUrl = `${baseUrl.replace(/\/$/, "")}/api/v1`;
|
|
149
|
+
this.apiKey = apiKey;
|
|
150
|
+
this.timeout = timeout;
|
|
151
|
+
}
|
|
152
|
+
async request(method, path3, body) {
|
|
153
|
+
let lastError = null;
|
|
154
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
157
|
+
try {
|
|
158
|
+
const resp = await fetch(`${this.baseUrl}${path3}`, {
|
|
159
|
+
method,
|
|
160
|
+
headers: {
|
|
161
|
+
"X-API-Key": this.apiKey,
|
|
162
|
+
"Content-Type": "application/json"
|
|
163
|
+
},
|
|
164
|
+
body: body != null ? JSON.stringify(body) : void 0,
|
|
165
|
+
signal: controller.signal
|
|
166
|
+
});
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
if (!resp.ok) {
|
|
169
|
+
const text = await resp.text().catch(() => "");
|
|
170
|
+
if (!isRetryable(resp.status) || attempt === MAX_RETRIES - 1) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`HTTP ${resp.status} on ${method} ${path3}: ${text}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
lastError = new Error(
|
|
176
|
+
`HTTP ${resp.status} on ${method} ${path3}: ${text}`
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
return await resp.json();
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
lastError = err;
|
|
184
|
+
const isNetworkOrTimeout = err.name === "AbortError" || err.name === "TypeError";
|
|
185
|
+
if (!isNetworkOrTimeout && attempt < MAX_RETRIES - 1) {
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
if (attempt === MAX_RETRIES - 1) {
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const delay = RETRY_BACKOFF_BASE * 2 ** attempt;
|
|
193
|
+
await sleep(delay);
|
|
194
|
+
}
|
|
195
|
+
throw lastError;
|
|
196
|
+
}
|
|
197
|
+
/** POST /sync/connect — SDK bootstrap. */
|
|
198
|
+
async connect() {
|
|
199
|
+
return this.request("POST", "/sync/connect");
|
|
200
|
+
}
|
|
201
|
+
/** POST /sync/heartbeat — periodic state refresh. */
|
|
202
|
+
async heartbeat() {
|
|
203
|
+
return this.request("POST", "/sync/heartbeat", {});
|
|
204
|
+
}
|
|
205
|
+
/** POST /events — batch event ingestion. */
|
|
206
|
+
async ingestEvents(events) {
|
|
207
|
+
return this.request("POST", "/events", { events });
|
|
208
|
+
}
|
|
209
|
+
/** PATCH /events/{uuid}/grade — submit human grade. */
|
|
210
|
+
async submitGrade(eventUuid, grade2) {
|
|
211
|
+
return this.request("PATCH", `/events/${eventUuid}/grade`, {
|
|
212
|
+
grade: grade2,
|
|
213
|
+
is_graded: true
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/store.ts
|
|
219
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
220
|
+
var SCHEMA = `
|
|
221
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
222
|
+
local_event_uuid TEXT PRIMARY KEY,
|
|
223
|
+
bandit_id INTEGER NOT NULL,
|
|
224
|
+
arm_id INTEGER NOT NULL,
|
|
225
|
+
payload TEXT NOT NULL,
|
|
226
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
227
|
+
created_at REAL NOT NULL,
|
|
228
|
+
human_reward REAL,
|
|
229
|
+
graded_at REAL
|
|
230
|
+
);
|
|
231
|
+
CREATE INDEX IF NOT EXISTS idx_events_status ON events(status);
|
|
232
|
+
`;
|
|
233
|
+
var MIGRATION_GRADING = [
|
|
234
|
+
"ALTER TABLE events ADD COLUMN human_reward REAL",
|
|
235
|
+
"ALTER TABLE events ADD COLUMN graded_at REAL"
|
|
236
|
+
];
|
|
237
|
+
var EventStore = class {
|
|
238
|
+
db;
|
|
239
|
+
pushStmt;
|
|
240
|
+
pendingStmt;
|
|
241
|
+
constructor(dbPath = ":memory:") {
|
|
242
|
+
this.db = new import_better_sqlite3.default(dbPath);
|
|
243
|
+
this.db.pragma("journal_mode = WAL");
|
|
244
|
+
this.db.pragma("busy_timeout = 5000");
|
|
245
|
+
this.db.pragma("synchronous = NORMAL");
|
|
246
|
+
this.db.exec(SCHEMA);
|
|
247
|
+
this.migrate();
|
|
248
|
+
this.pushStmt = this.db.prepare(
|
|
249
|
+
`INSERT OR IGNORE INTO events
|
|
250
|
+
(local_event_uuid, bandit_id, arm_id, payload, status, created_at)
|
|
251
|
+
VALUES (?, ?, ?, ?, 'pending', ?)`
|
|
252
|
+
);
|
|
253
|
+
this.pendingStmt = this.db.prepare(
|
|
254
|
+
`SELECT payload FROM events WHERE status = 'pending'
|
|
255
|
+
ORDER BY created_at ASC LIMIT ?`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
/** Insert a pending event. */
|
|
259
|
+
push(event) {
|
|
260
|
+
this.pushStmt.run(
|
|
261
|
+
event.local_event_uuid,
|
|
262
|
+
event.bandit_id,
|
|
263
|
+
event.arm_id,
|
|
264
|
+
JSON.stringify(event),
|
|
265
|
+
Date.now() / 1e3
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
/** Return up to `limit` pending events (oldest first). */
|
|
269
|
+
pending(limit = 50) {
|
|
270
|
+
const rows = this.pendingStmt.all(limit);
|
|
271
|
+
return rows.map((row) => JSON.parse(row.payload));
|
|
272
|
+
}
|
|
273
|
+
/** Mark events as successfully flushed to cloud. */
|
|
274
|
+
markFlushed(uuids) {
|
|
275
|
+
if (uuids.length === 0) return;
|
|
276
|
+
const placeholders = uuids.map(() => "?").join(",");
|
|
277
|
+
this.db.prepare(
|
|
278
|
+
`UPDATE events SET status = 'flushed'
|
|
279
|
+
WHERE local_event_uuid IN (${placeholders})`
|
|
280
|
+
).run(...uuids);
|
|
281
|
+
}
|
|
282
|
+
/** Record a human grade locally. */
|
|
283
|
+
markGraded(uuid, reward) {
|
|
284
|
+
this.db.prepare(
|
|
285
|
+
`UPDATE events SET human_reward = ?, graded_at = ?
|
|
286
|
+
WHERE local_event_uuid = ?`
|
|
287
|
+
).run(reward, Date.now() / 1e3, uuid);
|
|
288
|
+
}
|
|
289
|
+
/** Close the database connection. */
|
|
290
|
+
close() {
|
|
291
|
+
this.db.close();
|
|
292
|
+
}
|
|
293
|
+
migrate() {
|
|
294
|
+
for (const stmt of MIGRATION_GRADING) {
|
|
295
|
+
try {
|
|
296
|
+
this.db.exec(stmt);
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// src/worker.ts
|
|
304
|
+
var TEXT_FIELDS = ["query_text", "response"];
|
|
305
|
+
var METADATA_FIELDS = ["model_name", "model_provider"];
|
|
306
|
+
function prepareCloudPayload(events, includeText) {
|
|
307
|
+
return events.map((event) => {
|
|
308
|
+
const copy = { ...event };
|
|
309
|
+
for (const field of METADATA_FIELDS) {
|
|
310
|
+
delete copy[field];
|
|
311
|
+
}
|
|
312
|
+
if (!includeText) {
|
|
313
|
+
for (const field of TEXT_FIELDS) {
|
|
314
|
+
delete copy[field];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return copy;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/client.ts
|
|
322
|
+
var DEFAULT_STORE_PATH = path2.join(os2.homedir(), ".bandito", "events.db");
|
|
323
|
+
var MAX_EVENT_RETRIES = 5;
|
|
324
|
+
var BanditoClient = class {
|
|
325
|
+
apiKey;
|
|
326
|
+
baseUrl;
|
|
327
|
+
storePath;
|
|
328
|
+
dataStorageArg;
|
|
329
|
+
dataStorage;
|
|
330
|
+
http = null;
|
|
331
|
+
store = null;
|
|
332
|
+
engines = /* @__PURE__ */ new Map();
|
|
333
|
+
bandits = /* @__PURE__ */ new Map();
|
|
334
|
+
connected = false;
|
|
335
|
+
flushInterval = null;
|
|
336
|
+
flushInProgress = false;
|
|
337
|
+
deadUuids = /* @__PURE__ */ new Set();
|
|
338
|
+
retryCounts = /* @__PURE__ */ new Map();
|
|
339
|
+
constructor(options = {}) {
|
|
340
|
+
this.apiKey = options.apiKey;
|
|
341
|
+
this.baseUrl = options.baseUrl;
|
|
342
|
+
this.storePath = options.storePath;
|
|
343
|
+
this.dataStorageArg = options.dataStorage;
|
|
344
|
+
this.dataStorage = options.dataStorage ?? "local";
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Bootstrap: authenticate and hydrate in-memory state from cloud.
|
|
348
|
+
*
|
|
349
|
+
* Resolves config from: constructor args → env vars → ~/.bandito/config.toml.
|
|
350
|
+
* Initializes WASM, creates HTTP client, SQLite store, fetches full state.
|
|
351
|
+
*/
|
|
352
|
+
async connect() {
|
|
353
|
+
if (this.connected) {
|
|
354
|
+
await this.close();
|
|
355
|
+
}
|
|
356
|
+
await initWasm();
|
|
357
|
+
const config = loadConfig();
|
|
358
|
+
const apiKey = this.apiKey ?? config.apiKey;
|
|
359
|
+
if (!apiKey) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
"apiKey required \u2014 pass it to constructor, set BANDITO_API_KEY, or run `bandito signup`"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const baseUrl = this.baseUrl ?? config.baseUrl;
|
|
365
|
+
if (!this.dataStorageArg) {
|
|
366
|
+
this.dataStorage = config.dataStorage;
|
|
367
|
+
}
|
|
368
|
+
this.http = new BanditoHTTP(baseUrl, apiKey);
|
|
369
|
+
const storePath = this.storePath ?? DEFAULT_STORE_PATH;
|
|
370
|
+
if (storePath !== ":memory:") {
|
|
371
|
+
const dir = path2.dirname(storePath);
|
|
372
|
+
if (!fs2.existsSync(dir)) {
|
|
373
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
this.store = new EventStore(storePath);
|
|
377
|
+
try {
|
|
378
|
+
const data = await this.http.connect();
|
|
379
|
+
this.applySync(data);
|
|
380
|
+
this.deadUuids.clear();
|
|
381
|
+
this.retryCounts.clear();
|
|
382
|
+
await this.flushPending();
|
|
383
|
+
this.flushInterval = setInterval(() => {
|
|
384
|
+
this.flushPending().catch(() => {
|
|
385
|
+
});
|
|
386
|
+
}, 3e4);
|
|
387
|
+
this.connected = true;
|
|
388
|
+
} catch (err) {
|
|
389
|
+
this.store?.close();
|
|
390
|
+
this.store = null;
|
|
391
|
+
this.http = null;
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Local Thompson Sampling decision. Synchronous, <1ms, no network.
|
|
397
|
+
*/
|
|
398
|
+
pull(banditName, options = {}) {
|
|
399
|
+
this.ensureConnected();
|
|
400
|
+
const cache = this.bandits.get(banditName);
|
|
401
|
+
if (!cache) {
|
|
402
|
+
const available = [...this.bandits.keys()];
|
|
403
|
+
throw new Error(
|
|
404
|
+
`Unknown bandit '${banditName}'. Available: [${available.join(", ")}]`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (cache.arms.length === 0) {
|
|
408
|
+
throw new Error(`Bandit '${banditName}' has no active arms`);
|
|
409
|
+
}
|
|
410
|
+
const engine = this.engines.get(banditName);
|
|
411
|
+
const queryLength = options.query?.length ?? void 0;
|
|
412
|
+
const excludeIds = options.exclude ? Int32Array.from(options.exclude) : void 0;
|
|
413
|
+
const resultJson = engine.pull(queryLength, excludeIds);
|
|
414
|
+
const raw = JSON.parse(resultJson);
|
|
415
|
+
const winnerArm = cache.arms.find((a) => a.armId === raw.arm_id);
|
|
416
|
+
if (!winnerArm) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Engine selected arm ${raw.arm_id} but it's not in active arm cache for "${banditName}". This is likely a bug \u2014 please report it at https://github.com/bandito-ai/bandito/issues`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return createPullResult({
|
|
422
|
+
arm: winnerArm,
|
|
423
|
+
eventId: (0, import_node_crypto.randomUUID)(),
|
|
424
|
+
banditId: cache.banditId,
|
|
425
|
+
banditName,
|
|
426
|
+
scores: raw.scores,
|
|
427
|
+
pullTime: import_node_perf_hooks.performance.now()
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Record an LLM call outcome. Writes to SQLite first (crash-safe),
|
|
432
|
+
* then fires off a non-blocking flush to cloud.
|
|
433
|
+
*/
|
|
434
|
+
update(pullResult, options = {}) {
|
|
435
|
+
this.ensureConnected();
|
|
436
|
+
let reward = options.reward;
|
|
437
|
+
if (options.failed && reward == null) {
|
|
438
|
+
reward = 0;
|
|
439
|
+
}
|
|
440
|
+
let latency = options.latency;
|
|
441
|
+
if (latency == null && pullResult._pullTime > 0) {
|
|
442
|
+
latency = import_node_perf_hooks.performance.now() - pullResult._pullTime;
|
|
443
|
+
}
|
|
444
|
+
const event = {
|
|
445
|
+
local_event_uuid: pullResult.eventId,
|
|
446
|
+
bandit_id: pullResult.banditId,
|
|
447
|
+
arm_id: pullResult.arm.armId,
|
|
448
|
+
model_name: pullResult.arm.modelName,
|
|
449
|
+
model_provider: pullResult.arm.modelProvider
|
|
450
|
+
};
|
|
451
|
+
if (options.queryText != null) {
|
|
452
|
+
event.query_text = options.queryText;
|
|
453
|
+
}
|
|
454
|
+
if (options.response != null) {
|
|
455
|
+
event.response = typeof options.response === "string" ? { response: options.response } : options.response;
|
|
456
|
+
}
|
|
457
|
+
if (reward != null) {
|
|
458
|
+
event.early_reward = reward;
|
|
459
|
+
}
|
|
460
|
+
if (options.cost != null) {
|
|
461
|
+
event.cost = options.cost;
|
|
462
|
+
}
|
|
463
|
+
if (latency != null) {
|
|
464
|
+
event.latency = latency;
|
|
465
|
+
}
|
|
466
|
+
if (options.inputTokens != null) {
|
|
467
|
+
event.input_tokens = options.inputTokens;
|
|
468
|
+
}
|
|
469
|
+
if (options.outputTokens != null) {
|
|
470
|
+
event.output_tokens = options.outputTokens;
|
|
471
|
+
}
|
|
472
|
+
if (options.segment != null) {
|
|
473
|
+
event.segment = options.segment;
|
|
474
|
+
}
|
|
475
|
+
if (options.failed) {
|
|
476
|
+
event.run_error = true;
|
|
477
|
+
}
|
|
478
|
+
this.store.push(event);
|
|
479
|
+
this.flushPending().catch(() => {
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Send a human grade for an existing event. Async (HTTP).
|
|
484
|
+
*/
|
|
485
|
+
async grade(eventId, grade2) {
|
|
486
|
+
this.ensureConnected();
|
|
487
|
+
await this.http.submitGrade(eventId, grade2);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Explicit state refresh from cloud.
|
|
491
|
+
*/
|
|
492
|
+
async sync() {
|
|
493
|
+
this.ensureConnected();
|
|
494
|
+
const data = await this.http.heartbeat();
|
|
495
|
+
const prevBandits = new Map(this.bandits);
|
|
496
|
+
const prevEngines = new Map(this.engines);
|
|
497
|
+
try {
|
|
498
|
+
this.applySync(data);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
this.bandits = prevBandits;
|
|
501
|
+
this.engines = prevEngines;
|
|
502
|
+
console.warn("[bandito] Sync response malformed \u2014 keeping last-known-good state", err);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Shut down: clear interval, flush remaining events, close connections.
|
|
507
|
+
*/
|
|
508
|
+
async close() {
|
|
509
|
+
if (this.flushInterval) {
|
|
510
|
+
clearInterval(this.flushInterval);
|
|
511
|
+
this.flushInterval = null;
|
|
512
|
+
}
|
|
513
|
+
if (this.store && this.http) {
|
|
514
|
+
await this.flushPending();
|
|
515
|
+
}
|
|
516
|
+
this.store?.close();
|
|
517
|
+
this.store = null;
|
|
518
|
+
this.http = null;
|
|
519
|
+
this.engines.clear();
|
|
520
|
+
this.bandits.clear();
|
|
521
|
+
this.connected = false;
|
|
522
|
+
}
|
|
523
|
+
// --- Internal ---
|
|
524
|
+
ensureConnected() {
|
|
525
|
+
if (!this.connected) {
|
|
526
|
+
throw new Error("Not connected \u2014 call connect() first");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
applySync(data) {
|
|
530
|
+
const banditsData = data.bandits ?? [];
|
|
531
|
+
const newBandits = /* @__PURE__ */ new Map();
|
|
532
|
+
const newEngines = /* @__PURE__ */ new Map();
|
|
533
|
+
for (const b of banditsData) {
|
|
534
|
+
const arms = b.arms ?? [];
|
|
535
|
+
if (arms.length === 0) continue;
|
|
536
|
+
const activeArms = arms.filter((a) => a.is_active).map((a) => createArm(a));
|
|
537
|
+
const name = b.name;
|
|
538
|
+
const cache = {
|
|
539
|
+
banditId: Number(b.bandit_id),
|
|
540
|
+
name,
|
|
541
|
+
arms: activeArms,
|
|
542
|
+
armWire: arms,
|
|
543
|
+
optimizationMode: b.optimization_mode ?? "base",
|
|
544
|
+
avgLatencyLastN: b.avg_latency_last_n,
|
|
545
|
+
budget: b.budget,
|
|
546
|
+
totalCost: b.total_cost
|
|
547
|
+
};
|
|
548
|
+
newBandits.set(name, cache);
|
|
549
|
+
const existingEngine = this.engines.get(name);
|
|
550
|
+
const banditJson = JSON.stringify(b);
|
|
551
|
+
if (existingEngine) {
|
|
552
|
+
updateEngine(existingEngine, banditJson);
|
|
553
|
+
newEngines.set(name, existingEngine);
|
|
554
|
+
} else {
|
|
555
|
+
newEngines.set(name, createEngine(banditJson));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
this.bandits = newBandits;
|
|
559
|
+
this.engines = newEngines;
|
|
560
|
+
}
|
|
561
|
+
async flushPending() {
|
|
562
|
+
if (this.flushInProgress || !this.store || !this.http) return;
|
|
563
|
+
this.flushInProgress = true;
|
|
564
|
+
try {
|
|
565
|
+
const pending = this.store.pending();
|
|
566
|
+
if (pending.length === 0) return;
|
|
567
|
+
const alive = this.deadUuids.size > 0 ? pending.filter((e) => !this.deadUuids.has(e.local_event_uuid)) : pending;
|
|
568
|
+
if (alive.length === 0) return;
|
|
569
|
+
const payload = prepareCloudPayload(
|
|
570
|
+
alive,
|
|
571
|
+
this.dataStorage !== "local"
|
|
572
|
+
);
|
|
573
|
+
const result = await this.http.ingestEvents(payload);
|
|
574
|
+
const errors = result.errors ?? [];
|
|
575
|
+
const erroredUuids = new Set(
|
|
576
|
+
errors.map((e) => e.local_event_uuid).filter(Boolean)
|
|
577
|
+
);
|
|
578
|
+
for (const uid of erroredUuids) {
|
|
579
|
+
const count = (this.retryCounts.get(uid) ?? 0) + 1;
|
|
580
|
+
this.retryCounts.set(uid, count);
|
|
581
|
+
if (count >= MAX_EVENT_RETRIES) {
|
|
582
|
+
this.deadUuids.add(uid);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const flushedUuids = alive.map((e) => e.local_event_uuid).filter((uid) => !erroredUuids.has(uid));
|
|
586
|
+
if (flushedUuids.length > 0 && this.store) {
|
|
587
|
+
this.store.markFlushed(flushedUuids);
|
|
588
|
+
}
|
|
589
|
+
} catch (err) {
|
|
590
|
+
console.warn("[bandito] Event flush failed \u2014 will retry", err);
|
|
591
|
+
} finally {
|
|
592
|
+
this.flushInProgress = false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/index.ts
|
|
598
|
+
var _client = null;
|
|
599
|
+
function getClient() {
|
|
600
|
+
if (!_client) {
|
|
601
|
+
throw new Error("Not connected \u2014 call connect() first");
|
|
602
|
+
}
|
|
603
|
+
return _client;
|
|
604
|
+
}
|
|
605
|
+
async function connect(options = {}) {
|
|
606
|
+
if (_client) {
|
|
607
|
+
await _client.close();
|
|
608
|
+
}
|
|
609
|
+
_client = new BanditoClient(options);
|
|
610
|
+
await _client.connect();
|
|
611
|
+
}
|
|
612
|
+
function pull(banditName, options) {
|
|
613
|
+
return getClient().pull(banditName, options);
|
|
614
|
+
}
|
|
615
|
+
function update(pullResult, options) {
|
|
616
|
+
getClient().update(pullResult, options);
|
|
617
|
+
}
|
|
618
|
+
async function grade(eventId, gradeValue) {
|
|
619
|
+
await getClient().grade(eventId, gradeValue);
|
|
620
|
+
}
|
|
621
|
+
async function sync() {
|
|
622
|
+
await getClient().sync();
|
|
623
|
+
}
|
|
624
|
+
async function close() {
|
|
625
|
+
if (_client) {
|
|
626
|
+
await _client.close();
|
|
627
|
+
_client = null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
631
|
+
0 && (module.exports = {
|
|
632
|
+
BanditoClient,
|
|
633
|
+
close,
|
|
634
|
+
connect,
|
|
635
|
+
grade,
|
|
636
|
+
pull,
|
|
637
|
+
sync,
|
|
638
|
+
update
|
|
639
|
+
});
|
|
640
|
+
//# sourceMappingURL=index.js.map
|