@lmnr-ai/lmnr 0.7.5-alpha.2 → 0.7.6
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/dist/cli.d.mts +1 -7
- package/dist/cli.d.ts +1 -7
- package/dist/cli.js +1442 -91
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1442 -80
- package/dist/cli.mjs.map +1 -1
- package/dist/{evaluations-DS4TjeW7.d.mts → evaluations-DtyOVeCu.d.mts} +99 -5
- package/dist/{evaluations-DS4TjeW7.d.ts → evaluations-DtyOVeCu.d.ts} +99 -5
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1417 -722
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1422 -720
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -6,10 +6,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __export = (target, all) => {
|
|
10
|
-
for (var name in all)
|
|
11
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
-
};
|
|
13
9
|
var __copyProps = (to, from, except, desc) => {
|
|
14
10
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
11
|
for (let key of __getOwnPropNames(from))
|
|
@@ -26,21 +22,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
22
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
23
|
mod
|
|
28
24
|
));
|
|
29
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
25
|
|
|
31
|
-
// src/cli.ts
|
|
32
|
-
var cli_exports = {};
|
|
33
|
-
__export(cli_exports, {
|
|
34
|
-
loadModule: () => loadModule
|
|
35
|
-
});
|
|
36
|
-
module.exports = __toCommonJS(cli_exports);
|
|
26
|
+
// src/cli/index.ts
|
|
37
27
|
var import_commander = require("commander");
|
|
38
|
-
var esbuild = __toESM(require("esbuild"));
|
|
39
|
-
var fs = __toESM(require("fs"));
|
|
40
|
-
var glob = __toESM(require("glob"));
|
|
41
28
|
|
|
42
29
|
// package.json
|
|
43
|
-
var version = "0.7.5
|
|
30
|
+
var version = "0.7.5";
|
|
44
31
|
|
|
45
32
|
// src/utils.ts
|
|
46
33
|
var import_api = require("@opentelemetry/api");
|
|
@@ -85,6 +72,48 @@ function initializeLogger(options) {
|
|
|
85
72
|
}));
|
|
86
73
|
}
|
|
87
74
|
var logger = initializeLogger();
|
|
75
|
+
var isStringUUID = (id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id);
|
|
76
|
+
var newUUID = () => {
|
|
77
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
78
|
+
return crypto.randomUUID();
|
|
79
|
+
} else {
|
|
80
|
+
return (0, import_uuid.v4)();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var otelSpanIdToUUID = (spanId) => {
|
|
84
|
+
let id = spanId.toLowerCase();
|
|
85
|
+
if (id.startsWith("0x")) {
|
|
86
|
+
id = id.slice(2);
|
|
87
|
+
}
|
|
88
|
+
if (id.length !== 16) {
|
|
89
|
+
logger.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
|
|
90
|
+
}
|
|
91
|
+
if (!/^[0-9a-f]+$/.test(id)) {
|
|
92
|
+
logger.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
|
|
93
|
+
return newUUID();
|
|
94
|
+
}
|
|
95
|
+
return id.padStart(32, "0").replace(
|
|
96
|
+
/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/,
|
|
97
|
+
"$1-$2-$3-$4-$5"
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
var otelTraceIdToUUID = (traceId) => {
|
|
101
|
+
let id = traceId.toLowerCase();
|
|
102
|
+
if (id.startsWith("0x")) {
|
|
103
|
+
id = id.slice(2);
|
|
104
|
+
}
|
|
105
|
+
if (id.length !== 32) {
|
|
106
|
+
logger.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
|
|
107
|
+
}
|
|
108
|
+
if (!/^[0-9a-f]+$/.test(id)) {
|
|
109
|
+
logger.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
|
|
110
|
+
return newUUID();
|
|
111
|
+
}
|
|
112
|
+
return id.replace(
|
|
113
|
+
/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/,
|
|
114
|
+
"$1-$2-$3-$4-$5"
|
|
115
|
+
);
|
|
116
|
+
};
|
|
88
117
|
var getDirname = () => {
|
|
89
118
|
if (typeof __dirname !== "undefined") {
|
|
90
119
|
return __dirname;
|
|
@@ -94,16 +123,1283 @@ var getDirname = () => {
|
|
|
94
123
|
}
|
|
95
124
|
return process.cwd();
|
|
96
125
|
};
|
|
126
|
+
var slicePayload = (value, length) => {
|
|
127
|
+
if (value === null || value === void 0) {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
const str = JSON.stringify(value);
|
|
131
|
+
if (str.length <= length) {
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
return str.slice(0, length) + "...";
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/client/index.ts
|
|
138
|
+
var import_dotenv = require("dotenv");
|
|
139
|
+
|
|
140
|
+
// src/client/resources/agent.ts
|
|
141
|
+
var import_api3 = require("@opentelemetry/api");
|
|
97
142
|
|
|
98
|
-
// src/
|
|
143
|
+
// src/opentelemetry-lib/tracing/context.ts
|
|
144
|
+
var import_api2 = require("@opentelemetry/api");
|
|
145
|
+
var import_async_hooks = require("async_hooks");
|
|
146
|
+
var LaminarContextManager = class {
|
|
147
|
+
static getContext() {
|
|
148
|
+
const contexts = this._asyncLocalStorage.getStore() || [];
|
|
149
|
+
for (let i = contexts.length - 1; i >= 0; i--) {
|
|
150
|
+
const context = contexts[i];
|
|
151
|
+
const span = import_api2.trace.getSpan(context);
|
|
152
|
+
if (!span) {
|
|
153
|
+
return context;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const isActive = this._activeSpans.has(span.spanContext().spanId);
|
|
157
|
+
if (isActive) {
|
|
158
|
+
return context;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
return context;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return import_api2.ROOT_CONTEXT;
|
|
165
|
+
}
|
|
166
|
+
static pushContext(context) {
|
|
167
|
+
const contexts = this._asyncLocalStorage.getStore() || [];
|
|
168
|
+
const newContexts = [...contexts, context];
|
|
169
|
+
this._asyncLocalStorage.enterWith(newContexts);
|
|
170
|
+
}
|
|
171
|
+
static popContext() {
|
|
172
|
+
const contexts = this._asyncLocalStorage.getStore() || [];
|
|
173
|
+
if (contexts.length > 0) {
|
|
174
|
+
const newContexts = contexts.slice(0, -1).filter((context) => {
|
|
175
|
+
const span = import_api2.trace.getSpan(context);
|
|
176
|
+
if (!span) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
return this._activeSpans.has(span.spanContext().spanId);
|
|
181
|
+
} catch {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
this._asyncLocalStorage.enterWith(newContexts);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
static clearContexts() {
|
|
189
|
+
this._asyncLocalStorage.enterWith([]);
|
|
190
|
+
}
|
|
191
|
+
static getContextStack() {
|
|
192
|
+
return this._asyncLocalStorage.getStore() || [];
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Run a function with an isolated context stack.
|
|
196
|
+
* This ensures that parallel executions don't interfere with each other.
|
|
197
|
+
*/
|
|
198
|
+
static runWithIsolatedContext(initialStack, fn) {
|
|
199
|
+
return this._asyncLocalStorage.run(initialStack, fn);
|
|
200
|
+
}
|
|
201
|
+
static addActiveSpan(spanId) {
|
|
202
|
+
this._activeSpans.add(spanId);
|
|
203
|
+
}
|
|
204
|
+
static removeActiveSpan(spanId) {
|
|
205
|
+
this._activeSpans.delete(spanId);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
LaminarContextManager._asyncLocalStorage = new import_async_hooks.AsyncLocalStorage();
|
|
209
|
+
// Static registry for cross-async span management
|
|
210
|
+
// We're keeping track of spans have started (and running) here for the cases when span
|
|
211
|
+
// is started in one async context and ended in another
|
|
212
|
+
// We use this registry to ignore the context of spans that were already ended in
|
|
213
|
+
// another async context.
|
|
214
|
+
// LaminarSpan adds and removes itself to and from this registry in start()
|
|
215
|
+
// and end() methods respectively.
|
|
216
|
+
LaminarContextManager._activeSpans = /* @__PURE__ */ new Set();
|
|
217
|
+
|
|
218
|
+
// src/client/resources/index.ts
|
|
219
|
+
var BaseResource = class {
|
|
220
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
221
|
+
this.baseHttpUrl = baseHttpUrl;
|
|
222
|
+
this.projectApiKey = projectApiKey;
|
|
223
|
+
}
|
|
224
|
+
headers() {
|
|
225
|
+
return {
|
|
226
|
+
Authorization: `Bearer ${this.projectApiKey}`,
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
Accept: "application/json"
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async handleError(response) {
|
|
232
|
+
const errorMsg = await response.text();
|
|
233
|
+
throw new Error(`${response.status} ${errorMsg}`);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/client/resources/agent.ts
|
|
99
238
|
var logger2 = initializeLogger();
|
|
239
|
+
var AgentResource = class extends BaseResource {
|
|
240
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
241
|
+
super(baseHttpUrl, projectApiKey);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Run Laminar index agent
|
|
245
|
+
*
|
|
246
|
+
* @param { RunAgentOptions } options - The options for running the agent
|
|
247
|
+
* @param { string } options.prompt - The prompt for the agent
|
|
248
|
+
* @param { string } [options.parentSpanContext] - The parent span context for tracing
|
|
249
|
+
* @param { ModelProvider } [options.modelProvider] - LLM provider to use
|
|
250
|
+
* @param { string } [options.model] - The model name as specified in the provider API
|
|
251
|
+
* @param { boolean } [options.stream] - Whether to stream the response. Defaults to false.
|
|
252
|
+
* @param { boolean } [options.enableThinking] - Whether to enable thinking in the underlying
|
|
253
|
+
* LLM. Defaults to true.
|
|
254
|
+
* @param { number } [options.timeout] - The timeout in seconds for the agent.
|
|
255
|
+
* Note: This is a soft timeout. The agent will finish a step even after the timeout has
|
|
256
|
+
* been reached.
|
|
257
|
+
* @param { string } [options.cdpUrl] - The URL of an existing Chrome DevTools Protocol
|
|
258
|
+
* (CDP) browser instance.
|
|
259
|
+
* @param { number } [options.maxSteps] - The maximum number of steps the agent can take.
|
|
260
|
+
* Defaults to 100.
|
|
261
|
+
* @param { number } [options.thinkingTokenBudget] - The maximum number of tokens the underlying
|
|
262
|
+
* LLM can spend on thinking per step, if supported by the LLM provider.
|
|
263
|
+
* @param { string } [options.startUrl] - The URL to start the agent on.
|
|
264
|
+
* Make sure it's a valid URL - refer to https://playwright.dev/docs/api/class-page#page-goto
|
|
265
|
+
* If not specified, the agent will infer this from the prompt.
|
|
266
|
+
* @param { string } [options.userAgent] - The user agent to set in the browser.
|
|
267
|
+
* If not specified, Laminar will use the default user agent.
|
|
268
|
+
* @param { boolean } [options.returnScreenshots] - Whether to return screenshots with
|
|
269
|
+
* each step. Defaults to false.
|
|
270
|
+
* @param { boolean } [options.returnAgentState] - Whether to return the agent state.
|
|
271
|
+
* Agent state can be used to resume the agent in subsequent runs.
|
|
272
|
+
* CAUTION: Agent state is a very large object. Defaults to false.
|
|
273
|
+
* @param { boolean } [options.returnStorageState] - Whether to return the storage state.
|
|
274
|
+
* Storage state includes browser cookies, auth, etc.
|
|
275
|
+
* CAUTION: Storage state is a relatively large object. Defaults to false.
|
|
276
|
+
* @param { boolean } [options.disableGiveControl] - Whether to NOT direct the agent
|
|
277
|
+
* to give control back to the user for tasks such as logging in. Defaults to false.
|
|
278
|
+
* @returns { Promise<AgentOutput | ReadableStream<RunAgentResponseChunk>> }
|
|
279
|
+
* The agent output or a stream of response chunks
|
|
280
|
+
*/
|
|
281
|
+
async run({
|
|
282
|
+
prompt,
|
|
283
|
+
parentSpanContext,
|
|
284
|
+
modelProvider,
|
|
285
|
+
model,
|
|
286
|
+
stream,
|
|
287
|
+
enableThinking,
|
|
288
|
+
timeout,
|
|
289
|
+
cdpUrl,
|
|
290
|
+
agentState,
|
|
291
|
+
storageState,
|
|
292
|
+
maxSteps,
|
|
293
|
+
thinkingTokenBudget,
|
|
294
|
+
startUrl,
|
|
295
|
+
userAgent,
|
|
296
|
+
returnScreenshots,
|
|
297
|
+
returnAgentState,
|
|
298
|
+
returnStorageState,
|
|
299
|
+
disableGiveControl
|
|
300
|
+
}) {
|
|
301
|
+
let requestParentSpanContext = parentSpanContext;
|
|
302
|
+
if (!requestParentSpanContext) {
|
|
303
|
+
const currentSpan = import_api3.trace.getSpan(LaminarContextManager.getContext()) ?? import_api3.trace.getActiveSpan();
|
|
304
|
+
if (currentSpan && currentSpan.isRecording()) {
|
|
305
|
+
const traceId = otelTraceIdToUUID(currentSpan.spanContext().traceId);
|
|
306
|
+
const spanId = otelSpanIdToUUID(currentSpan.spanContext().spanId);
|
|
307
|
+
requestParentSpanContext = JSON.stringify({
|
|
308
|
+
trace_id: traceId,
|
|
309
|
+
span_id: spanId,
|
|
310
|
+
is_remote: currentSpan.spanContext().isRemote
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const request = {
|
|
315
|
+
prompt,
|
|
316
|
+
parentSpanContext: requestParentSpanContext,
|
|
317
|
+
modelProvider,
|
|
318
|
+
model,
|
|
319
|
+
stream: true,
|
|
320
|
+
enableThinking: enableThinking ?? true,
|
|
321
|
+
timeout,
|
|
322
|
+
cdpUrl,
|
|
323
|
+
agentState,
|
|
324
|
+
storageState,
|
|
325
|
+
maxSteps,
|
|
326
|
+
thinkingTokenBudget,
|
|
327
|
+
startUrl,
|
|
328
|
+
userAgent,
|
|
329
|
+
returnScreenshots: returnScreenshots ?? false,
|
|
330
|
+
returnAgentState: returnAgentState ?? false,
|
|
331
|
+
returnStorageState: returnStorageState ?? false,
|
|
332
|
+
disableGiveControl: disableGiveControl ?? false
|
|
333
|
+
};
|
|
334
|
+
if (stream) {
|
|
335
|
+
return this.runStreaming(request);
|
|
336
|
+
} else {
|
|
337
|
+
return await this.runNonStreaming(request);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Run agent in streaming mode
|
|
342
|
+
*/
|
|
343
|
+
async runStreaming(request) {
|
|
344
|
+
const response = await fetch(this.baseHttpUrl + "/v1/agent/run", {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: this.headers(),
|
|
347
|
+
body: JSON.stringify(request)
|
|
348
|
+
});
|
|
349
|
+
if (!response.ok) {
|
|
350
|
+
await this.handleError(response);
|
|
351
|
+
}
|
|
352
|
+
if (!response.body) {
|
|
353
|
+
throw new Error("Response body is null");
|
|
354
|
+
}
|
|
355
|
+
return response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new TransformStream({
|
|
356
|
+
start() {
|
|
357
|
+
this.buffer = "";
|
|
358
|
+
},
|
|
359
|
+
transform(chunk, controller) {
|
|
360
|
+
this.buffer += chunk;
|
|
361
|
+
const lines = this.buffer.split("\n");
|
|
362
|
+
this.buffer = lines.pop() || "";
|
|
363
|
+
for (const line of lines) {
|
|
364
|
+
if (line.startsWith("[DONE]")) {
|
|
365
|
+
controller.terminate();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (!line.startsWith("data: ")) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const jsonStr = line.substring(6);
|
|
372
|
+
if (jsonStr) {
|
|
373
|
+
try {
|
|
374
|
+
const parsed = JSON.parse(jsonStr);
|
|
375
|
+
controller.enqueue(parsed);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
logger2.error(`Error parsing JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
flush(controller) {
|
|
383
|
+
if (this.buffer) {
|
|
384
|
+
if (this.buffer.startsWith("data: ")) {
|
|
385
|
+
const jsonStr = this.buffer.substring(6);
|
|
386
|
+
if (jsonStr) {
|
|
387
|
+
try {
|
|
388
|
+
const parsed = JSON.parse(jsonStr);
|
|
389
|
+
controller.enqueue(parsed);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
logger2.error(`Error parsing JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Run agent in non-streaming mode
|
|
401
|
+
*/
|
|
402
|
+
async runNonStreaming(request) {
|
|
403
|
+
const stream = await this.runStreaming(request);
|
|
404
|
+
const reader = stream.getReader();
|
|
405
|
+
let finalChunk = null;
|
|
406
|
+
let errorChunk = null;
|
|
407
|
+
try {
|
|
408
|
+
while (true) {
|
|
409
|
+
const { done, value } = await reader.read();
|
|
410
|
+
if (done) break;
|
|
411
|
+
if (value.chunkType === "finalOutput") {
|
|
412
|
+
finalChunk = value;
|
|
413
|
+
break;
|
|
414
|
+
} else if (value.chunkType === "error") {
|
|
415
|
+
errorChunk = value;
|
|
416
|
+
break;
|
|
417
|
+
} else if (value.chunkType === "timeout") {
|
|
418
|
+
errorChunk = {
|
|
419
|
+
chunkType: "error",
|
|
420
|
+
messageId: value.messageId,
|
|
421
|
+
error: "Timeout"
|
|
422
|
+
};
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} finally {
|
|
427
|
+
reader.releaseLock();
|
|
428
|
+
}
|
|
429
|
+
if (errorChunk) {
|
|
430
|
+
throw new Error(errorChunk.error);
|
|
431
|
+
}
|
|
432
|
+
return finalChunk?.content || { result: { isDone: true } };
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/version.ts
|
|
437
|
+
var getLangVersion = () => {
|
|
438
|
+
if (process?.versions?.node) {
|
|
439
|
+
return `node@${process.versions.node}`;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// src/client/resources/browser-events.ts
|
|
444
|
+
var BrowserEventsResource = class extends BaseResource {
|
|
445
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
446
|
+
super(baseHttpUrl, projectApiKey);
|
|
447
|
+
}
|
|
448
|
+
async send({
|
|
449
|
+
sessionId,
|
|
450
|
+
traceId,
|
|
451
|
+
events
|
|
452
|
+
}) {
|
|
453
|
+
const payload = {
|
|
454
|
+
sessionId,
|
|
455
|
+
traceId,
|
|
456
|
+
events,
|
|
457
|
+
source: getLangVersion() ?? "javascript",
|
|
458
|
+
sdkVersion: version
|
|
459
|
+
};
|
|
460
|
+
const jsonString = JSON.stringify(payload);
|
|
461
|
+
const blob = new Blob([jsonString], { type: "application/json" });
|
|
462
|
+
const compressedStream = blob.stream().pipeThrough(new CompressionStream("gzip"));
|
|
463
|
+
const compressedResponse = new Response(compressedStream);
|
|
464
|
+
const compressedData = await compressedResponse.arrayBuffer();
|
|
465
|
+
const response = await fetch(this.baseHttpUrl + "/v1/browser-sessions/events", {
|
|
466
|
+
method: "POST",
|
|
467
|
+
headers: {
|
|
468
|
+
...this.headers(),
|
|
469
|
+
"Content-Encoding": "gzip"
|
|
470
|
+
},
|
|
471
|
+
body: compressedData
|
|
472
|
+
});
|
|
473
|
+
if (!response.ok) {
|
|
474
|
+
await this.handleError(response);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// src/client/resources/datasets.ts
|
|
480
|
+
var logger3 = initializeLogger();
|
|
481
|
+
var DEFAULT_DATASET_PULL_LIMIT = 100;
|
|
482
|
+
var DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
|
|
483
|
+
var DatasetsResource = class extends BaseResource {
|
|
484
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
485
|
+
super(baseHttpUrl, projectApiKey);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* List all datasets.
|
|
489
|
+
*
|
|
490
|
+
* @returns {Promise<Dataset[]>} Array of datasets
|
|
491
|
+
*/
|
|
492
|
+
async listDatasets() {
|
|
493
|
+
const response = await fetch(this.baseHttpUrl + "/v1/datasets", {
|
|
494
|
+
method: "GET",
|
|
495
|
+
headers: this.headers()
|
|
496
|
+
});
|
|
497
|
+
if (!response.ok) {
|
|
498
|
+
await this.handleError(response);
|
|
499
|
+
}
|
|
500
|
+
return response.json();
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get a dataset by name.
|
|
504
|
+
*
|
|
505
|
+
* @param {string} name - Name of the dataset
|
|
506
|
+
* @returns {Promise<Dataset[]>} Array of datasets with matching name
|
|
507
|
+
*/
|
|
508
|
+
async getDatasetByName(name) {
|
|
509
|
+
const params = new URLSearchParams({ name });
|
|
510
|
+
const response = await fetch(
|
|
511
|
+
this.baseHttpUrl + `/v1/datasets?${params.toString()}`,
|
|
512
|
+
{
|
|
513
|
+
method: "GET",
|
|
514
|
+
headers: this.headers()
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
if (!response.ok) {
|
|
518
|
+
await this.handleError(response);
|
|
519
|
+
}
|
|
520
|
+
return response.json();
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Push datapoints to a dataset.
|
|
524
|
+
*
|
|
525
|
+
* @param {Object} options - Push options
|
|
526
|
+
* @param {Datapoint<D, T>[]} options.points - Datapoints to push
|
|
527
|
+
* @param {string} [options.name] - Name of the dataset (either name or id must be provided)
|
|
528
|
+
* @param {StringUUID} [options.id] - ID of the dataset (either name or id must be provided)
|
|
529
|
+
* @param {number} [options.batchSize] - Batch size for pushing (default: 100)
|
|
530
|
+
* @param {boolean} [options.createDataset] - Whether to create the dataset if it doesn't exist
|
|
531
|
+
* @returns {Promise<PushDatapointsResponse | undefined>}
|
|
532
|
+
*/
|
|
533
|
+
async push({
|
|
534
|
+
points,
|
|
535
|
+
name,
|
|
536
|
+
id,
|
|
537
|
+
batchSize = DEFAULT_DATASET_PUSH_BATCH_SIZE,
|
|
538
|
+
createDataset = false
|
|
539
|
+
}) {
|
|
540
|
+
if (!name && !id) {
|
|
541
|
+
throw new Error("Either name or id must be provided");
|
|
542
|
+
}
|
|
543
|
+
if (name && id) {
|
|
544
|
+
throw new Error("Only one of name or id must be provided");
|
|
545
|
+
}
|
|
546
|
+
if (createDataset && !name) {
|
|
547
|
+
throw new Error("Name must be provided when creating a new dataset");
|
|
548
|
+
}
|
|
549
|
+
const identifier = name ? { name } : { datasetId: id };
|
|
550
|
+
const totalBatches = Math.ceil(points.length / batchSize);
|
|
551
|
+
let response;
|
|
552
|
+
for (let i = 0; i < points.length; i += batchSize) {
|
|
553
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
554
|
+
logger3.debug(`Pushing batch ${batchNum} of ${totalBatches}`);
|
|
555
|
+
const batch = points.slice(i, i + batchSize);
|
|
556
|
+
const fetchResponse = await fetch(
|
|
557
|
+
this.baseHttpUrl + "/v1/datasets/datapoints",
|
|
558
|
+
{
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: this.headers(),
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
...identifier,
|
|
563
|
+
datapoints: batch.map((point) => ({
|
|
564
|
+
data: point.data,
|
|
565
|
+
target: point.target ?? {},
|
|
566
|
+
metadata: point.metadata ?? {}
|
|
567
|
+
})),
|
|
568
|
+
createDataset
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
if (fetchResponse.status !== 200 && fetchResponse.status !== 201) {
|
|
573
|
+
await this.handleError(fetchResponse);
|
|
574
|
+
}
|
|
575
|
+
response = await fetchResponse.json();
|
|
576
|
+
}
|
|
577
|
+
return response;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Pull datapoints from a dataset.
|
|
581
|
+
*
|
|
582
|
+
* @param {Object} options - Pull options
|
|
583
|
+
* @param {string} [options.name] - Name of the dataset (either name or id must be provided)
|
|
584
|
+
* @param {StringUUID} [options.id] - ID of the dataset (either name or id must be provided)
|
|
585
|
+
* @param {number} [options.limit] - Maximum number of datapoints to return (default: 100)
|
|
586
|
+
* @param {number} [options.offset] - Offset for pagination (default: 0)
|
|
587
|
+
* @returns {Promise<GetDatapointsResponse<D, T>>}
|
|
588
|
+
*/
|
|
589
|
+
async pull({
|
|
590
|
+
name,
|
|
591
|
+
id,
|
|
592
|
+
limit = DEFAULT_DATASET_PULL_LIMIT,
|
|
593
|
+
offset = 0
|
|
594
|
+
}) {
|
|
595
|
+
if (!name && !id) {
|
|
596
|
+
throw new Error("Either name or id must be provided");
|
|
597
|
+
}
|
|
598
|
+
if (name && id) {
|
|
599
|
+
throw new Error("Only one of name or id must be provided");
|
|
600
|
+
}
|
|
601
|
+
const paramsObj = {
|
|
602
|
+
offset: offset.toString(),
|
|
603
|
+
limit: limit.toString()
|
|
604
|
+
};
|
|
605
|
+
if (name) {
|
|
606
|
+
paramsObj.name = name;
|
|
607
|
+
} else {
|
|
608
|
+
paramsObj.datasetId = id;
|
|
609
|
+
}
|
|
610
|
+
const params = new URLSearchParams(paramsObj);
|
|
611
|
+
const response = await fetch(
|
|
612
|
+
this.baseHttpUrl + `/v1/datasets/datapoints?${params.toString()}`,
|
|
613
|
+
{
|
|
614
|
+
method: "GET",
|
|
615
|
+
headers: this.headers()
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
if (!response.ok) {
|
|
619
|
+
await this.handleError(response);
|
|
620
|
+
}
|
|
621
|
+
return response.json();
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// src/client/resources/evals.ts
|
|
626
|
+
var logger4 = initializeLogger();
|
|
627
|
+
var INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH = 16e6;
|
|
628
|
+
var EvalsResource = class extends BaseResource {
|
|
629
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
630
|
+
super(baseHttpUrl, projectApiKey);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Initialize an evaluation.
|
|
634
|
+
*
|
|
635
|
+
* @param {string} name - Name of the evaluation
|
|
636
|
+
* @param {string} groupName - Group name of the evaluation
|
|
637
|
+
* @param {Record<string, any>} metadata - Optional metadata
|
|
638
|
+
* @returns {Promise<InitEvaluationResponse>} Response from the evaluation initialization
|
|
639
|
+
*/
|
|
640
|
+
async init(name, groupName, metadata) {
|
|
641
|
+
const response = await fetch(this.baseHttpUrl + "/v1/evals", {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: this.headers(),
|
|
644
|
+
body: JSON.stringify({
|
|
645
|
+
name: name ?? null,
|
|
646
|
+
groupName: groupName ?? null,
|
|
647
|
+
metadata: metadata ?? null
|
|
648
|
+
})
|
|
649
|
+
});
|
|
650
|
+
if (!response.ok) {
|
|
651
|
+
await this.handleError(response);
|
|
652
|
+
}
|
|
653
|
+
return response.json();
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Create a new evaluation and return its ID.
|
|
657
|
+
*
|
|
658
|
+
* @param {string} [name] - Optional name of the evaluation
|
|
659
|
+
* @param {string} [groupName] - An identifier to group evaluations
|
|
660
|
+
* @param {Record<string, any>} [metadata] - Optional metadata
|
|
661
|
+
* @returns {Promise<StringUUID>} The evaluation ID
|
|
662
|
+
*/
|
|
663
|
+
async create(args) {
|
|
664
|
+
const evaluation = await this.init(args?.name, args?.groupName, args?.metadata);
|
|
665
|
+
return evaluation.id;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Create a new evaluation and return its ID.
|
|
669
|
+
* @deprecated use `create` instead.
|
|
670
|
+
*/
|
|
671
|
+
async createEvaluation(name, groupName, metadata) {
|
|
672
|
+
const evaluation = await this.init(name, groupName, metadata);
|
|
673
|
+
return evaluation.id;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Create a datapoint for an evaluation.
|
|
677
|
+
*
|
|
678
|
+
* @param {Object} options - Create datapoint options
|
|
679
|
+
* @param {string} options.evalId - The evaluation ID
|
|
680
|
+
* @param {D} options.data - The input data for the executor
|
|
681
|
+
* @param {T} [options.target] - The target/expected output for evaluators
|
|
682
|
+
* @param {Record<string, any>} [options.metadata] - Optional metadata
|
|
683
|
+
* @param {number} [options.index] - Optional index of the datapoint
|
|
684
|
+
* @param {string} [options.traceId] - Optional trace ID
|
|
685
|
+
* @returns {Promise<StringUUID>} The datapoint ID
|
|
686
|
+
*/
|
|
687
|
+
async createDatapoint({
|
|
688
|
+
evalId,
|
|
689
|
+
data,
|
|
690
|
+
target,
|
|
691
|
+
metadata,
|
|
692
|
+
index,
|
|
693
|
+
traceId
|
|
694
|
+
}) {
|
|
695
|
+
const datapointId = newUUID();
|
|
696
|
+
const partialDatapoint = {
|
|
697
|
+
id: datapointId,
|
|
698
|
+
data,
|
|
699
|
+
target,
|
|
700
|
+
index: index ?? 0,
|
|
701
|
+
traceId: traceId ?? newUUID(),
|
|
702
|
+
executorSpanId: newUUID(),
|
|
703
|
+
metadata
|
|
704
|
+
};
|
|
705
|
+
await this.saveDatapoints({
|
|
706
|
+
evalId,
|
|
707
|
+
datapoints: [partialDatapoint]
|
|
708
|
+
});
|
|
709
|
+
return datapointId;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Update a datapoint with evaluation results.
|
|
713
|
+
*
|
|
714
|
+
* @param {Object} options - Update datapoint options
|
|
715
|
+
* @param {string} options.evalId - The evaluation ID
|
|
716
|
+
* @param {string} options.datapointId - The datapoint ID
|
|
717
|
+
* @param {Record<string, number>} options.scores - The scores
|
|
718
|
+
* @param {O} [options.executorOutput] - The executor output
|
|
719
|
+
* @returns {Promise<void>}
|
|
720
|
+
*/
|
|
721
|
+
async updateDatapoint({
|
|
722
|
+
evalId,
|
|
723
|
+
datapointId,
|
|
724
|
+
scores,
|
|
725
|
+
executorOutput
|
|
726
|
+
}) {
|
|
727
|
+
const response = await fetch(
|
|
728
|
+
this.baseHttpUrl + `/v1/evals/${evalId}/datapoints/${datapointId}`,
|
|
729
|
+
{
|
|
730
|
+
method: "POST",
|
|
731
|
+
headers: this.headers(),
|
|
732
|
+
body: JSON.stringify({
|
|
733
|
+
executorOutput,
|
|
734
|
+
scores
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
if (!response.ok) {
|
|
739
|
+
await this.handleError(response);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Save evaluation datapoints.
|
|
744
|
+
*
|
|
745
|
+
* @param {Object} options - Save datapoints options
|
|
746
|
+
* @param {string} options.evalId - ID of the evaluation
|
|
747
|
+
* @param {EvaluationDatapoint<D, T, O>[]} options.datapoints - Datapoint to add
|
|
748
|
+
* @param {string} [options.groupName] - Group name of the evaluation
|
|
749
|
+
* @returns {Promise<void>} Response from the datapoint addition
|
|
750
|
+
*/
|
|
751
|
+
async saveDatapoints({
|
|
752
|
+
evalId,
|
|
753
|
+
datapoints,
|
|
754
|
+
groupName
|
|
755
|
+
}) {
|
|
756
|
+
const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: this.headers(),
|
|
759
|
+
body: JSON.stringify({
|
|
760
|
+
points: datapoints.map((d) => ({
|
|
761
|
+
...d,
|
|
762
|
+
data: slicePayload(d.data, INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH),
|
|
763
|
+
target: slicePayload(d.target, INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH),
|
|
764
|
+
executorOutput: slicePayload(
|
|
765
|
+
d.executorOutput,
|
|
766
|
+
INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH
|
|
767
|
+
)
|
|
768
|
+
})),
|
|
769
|
+
groupName: groupName ?? null
|
|
770
|
+
})
|
|
771
|
+
});
|
|
772
|
+
if (response.status === 413) {
|
|
773
|
+
return await this.retrySaveDatapoints({
|
|
774
|
+
evalId,
|
|
775
|
+
datapoints,
|
|
776
|
+
groupName
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
if (!response.ok) {
|
|
780
|
+
await this.handleError(response);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Get evaluation datapoints.
|
|
785
|
+
*
|
|
786
|
+
* @deprecated Use `client.datasets.pull()` instead.
|
|
787
|
+
* @param {Object} options - Get datapoints options
|
|
788
|
+
* @param {string} options.datasetName - Name of the dataset
|
|
789
|
+
* @param {number} options.offset - Offset at which to start the query
|
|
790
|
+
* @param {number} options.limit - Maximum number of datapoints to return
|
|
791
|
+
* @returns {Promise<GetDatapointsResponse>} Response from the datapoint retrieval
|
|
792
|
+
*/
|
|
793
|
+
async getDatapoints({
|
|
794
|
+
datasetName,
|
|
795
|
+
offset,
|
|
796
|
+
limit
|
|
797
|
+
}) {
|
|
798
|
+
logger4.warn(
|
|
799
|
+
"evals.getDatapoints() is deprecated. Use client.datasets.pull() instead."
|
|
800
|
+
);
|
|
801
|
+
const params = new URLSearchParams({
|
|
802
|
+
name: datasetName,
|
|
803
|
+
offset: offset.toString(),
|
|
804
|
+
limit: limit.toString()
|
|
805
|
+
});
|
|
806
|
+
const response = await fetch(
|
|
807
|
+
this.baseHttpUrl + `/v1/datasets/datapoints?${params.toString()}`,
|
|
808
|
+
{
|
|
809
|
+
method: "GET",
|
|
810
|
+
headers: this.headers()
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
if (!response.ok) {
|
|
814
|
+
await this.handleError(response);
|
|
815
|
+
}
|
|
816
|
+
return await response.json();
|
|
817
|
+
}
|
|
818
|
+
async retrySaveDatapoints({
|
|
819
|
+
evalId,
|
|
820
|
+
datapoints,
|
|
821
|
+
groupName,
|
|
822
|
+
maxRetries = 25,
|
|
823
|
+
initialLength = INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH
|
|
824
|
+
}) {
|
|
825
|
+
let length = initialLength;
|
|
826
|
+
let lastResponse = null;
|
|
827
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
828
|
+
logger4.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
|
|
829
|
+
const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
|
|
830
|
+
method: "POST",
|
|
831
|
+
headers: this.headers(),
|
|
832
|
+
body: JSON.stringify({
|
|
833
|
+
points: datapoints.map((d) => ({
|
|
834
|
+
...d,
|
|
835
|
+
data: slicePayload(d.data, length),
|
|
836
|
+
target: slicePayload(d.target, length),
|
|
837
|
+
executorOutput: slicePayload(d.executorOutput, length)
|
|
838
|
+
})),
|
|
839
|
+
groupName: groupName ?? null
|
|
840
|
+
})
|
|
841
|
+
});
|
|
842
|
+
lastResponse = response;
|
|
843
|
+
length = Math.floor(length / 2);
|
|
844
|
+
if (response.status !== 413) {
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (lastResponse && !lastResponse.ok) {
|
|
849
|
+
await this.handleError(lastResponse);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// src/client/resources/evaluators.ts
|
|
855
|
+
var EvaluatorsResource = class extends BaseResource {
|
|
856
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
857
|
+
super(baseHttpUrl, projectApiKey);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Create a score for a span or trace
|
|
861
|
+
*
|
|
862
|
+
* @param {ScoreOptions} options - Score creation options
|
|
863
|
+
* @param {string} options.name - Name of the score
|
|
864
|
+
* @param {string} [options.traceId] - The trace ID to score (will be attached to top-level span)
|
|
865
|
+
* @param {string} [options.spanId] - The span ID to score
|
|
866
|
+
* @param {Record<string, any>} [options.metadata] - Additional metadata
|
|
867
|
+
* @param {number} options.score - The score value (float)
|
|
868
|
+
* @returns {Promise<void>}
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* // Score by trace ID (will attach to root span)
|
|
872
|
+
* await evaluators.score({
|
|
873
|
+
* name: "quality",
|
|
874
|
+
* traceId: "trace-id-here",
|
|
875
|
+
* score: 0.95,
|
|
876
|
+
* metadata: { model: "gpt-4" }
|
|
877
|
+
* });
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* // Score by span ID
|
|
881
|
+
* await evaluators.score({
|
|
882
|
+
* name: "relevance",
|
|
883
|
+
* spanId: "span-id-here",
|
|
884
|
+
* score: 0.87
|
|
885
|
+
* });
|
|
886
|
+
*/
|
|
887
|
+
async score(options) {
|
|
888
|
+
const { name, metadata, score } = options;
|
|
889
|
+
let payload;
|
|
890
|
+
if ("traceId" in options && options.traceId) {
|
|
891
|
+
const formattedTraceId = isStringUUID(options.traceId) ? options.traceId : otelTraceIdToUUID(options.traceId);
|
|
892
|
+
payload = {
|
|
893
|
+
name,
|
|
894
|
+
metadata,
|
|
895
|
+
score,
|
|
896
|
+
source: "Code" /* Code */,
|
|
897
|
+
traceId: formattedTraceId
|
|
898
|
+
};
|
|
899
|
+
} else if ("spanId" in options && options.spanId) {
|
|
900
|
+
const formattedSpanId = isStringUUID(options.spanId) ? options.spanId : otelSpanIdToUUID(options.spanId);
|
|
901
|
+
payload = {
|
|
902
|
+
name,
|
|
903
|
+
metadata,
|
|
904
|
+
score,
|
|
905
|
+
source: "Code" /* Code */,
|
|
906
|
+
spanId: formattedSpanId
|
|
907
|
+
};
|
|
908
|
+
} else {
|
|
909
|
+
throw new Error("Either 'traceId' or 'spanId' must be provided.");
|
|
910
|
+
}
|
|
911
|
+
const response = await fetch(this.baseHttpUrl + "/v1/evaluators/score", {
|
|
912
|
+
method: "POST",
|
|
913
|
+
headers: this.headers(),
|
|
914
|
+
body: JSON.stringify(payload)
|
|
915
|
+
});
|
|
916
|
+
if (!response.ok) {
|
|
917
|
+
await this.handleError(response);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/client/resources/tags.ts
|
|
923
|
+
var TagsResource = class extends BaseResource {
|
|
924
|
+
/** Resource for tagging traces. */
|
|
925
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
926
|
+
super(baseHttpUrl, projectApiKey);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Tag a trace with a list of tags. Note that the trace must be ended before
|
|
930
|
+
* tagging it. You may want to call `await Laminar.flush()` after the trace
|
|
931
|
+
* that you want to tag.
|
|
932
|
+
*
|
|
933
|
+
* @param {string | StringUUID} trace_id - The trace id to tag.
|
|
934
|
+
* @param {string[] | string} tags - The tag or list of tags to add to the trace.
|
|
935
|
+
* @returns {Promise<any>} The response from the server.
|
|
936
|
+
* @example
|
|
937
|
+
* ```javascript
|
|
938
|
+
* import { Laminar, observe, LaminarClient } from "@lmnr-ai/lmnr";
|
|
939
|
+
* Laminar.initialize();
|
|
940
|
+
* const client = new LaminarClient();
|
|
941
|
+
* let traceId: StringUUID | null = null;
|
|
942
|
+
* // Make sure this is called outside of traced context.
|
|
943
|
+
* await observe(
|
|
944
|
+
* {
|
|
945
|
+
* name: "my-trace",
|
|
946
|
+
* },
|
|
947
|
+
* async () => {
|
|
948
|
+
* traceId = await Laminar.getTraceId();
|
|
949
|
+
* await foo();
|
|
950
|
+
* },
|
|
951
|
+
* );
|
|
952
|
+
*
|
|
953
|
+
* // or make sure the trace is ended by this point.
|
|
954
|
+
* await Laminar.flush();
|
|
955
|
+
* if (traceId) {
|
|
956
|
+
* await client.tags.tag(traceId, ["tag1", "tag2"]);
|
|
957
|
+
* }
|
|
958
|
+
* ```
|
|
959
|
+
*/
|
|
960
|
+
async tag(trace_id, tags) {
|
|
961
|
+
const traceTags = Array.isArray(tags) ? tags : [tags];
|
|
962
|
+
const formattedTraceId = isStringUUID(trace_id) ? trace_id : otelTraceIdToUUID(trace_id);
|
|
963
|
+
const url = this.baseHttpUrl + "/v1/tag";
|
|
964
|
+
const payload = {
|
|
965
|
+
"traceId": formattedTraceId,
|
|
966
|
+
"names": traceTags
|
|
967
|
+
};
|
|
968
|
+
const response = await fetch(
|
|
969
|
+
url,
|
|
970
|
+
{
|
|
971
|
+
method: "POST",
|
|
972
|
+
headers: this.headers(),
|
|
973
|
+
body: JSON.stringify(payload)
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
if (!response.ok) {
|
|
977
|
+
await this.handleError(response);
|
|
978
|
+
}
|
|
979
|
+
return response.json();
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// src/client/index.ts
|
|
984
|
+
var LaminarClient = class {
|
|
985
|
+
constructor({
|
|
986
|
+
baseUrl,
|
|
987
|
+
projectApiKey,
|
|
988
|
+
port
|
|
989
|
+
} = {}) {
|
|
990
|
+
(0, import_dotenv.config)({
|
|
991
|
+
quiet: true
|
|
992
|
+
});
|
|
993
|
+
this.projectApiKey = projectApiKey ?? process.env.LMNR_PROJECT_API_KEY;
|
|
994
|
+
const httpPort = port ?? (baseUrl?.match(/:\d{1,5}$/g) ? parseInt(baseUrl.match(/:\d{1,5}$/g)[0].slice(1)) : 443);
|
|
995
|
+
const baseUrlNoPort = (baseUrl ?? process.env.LMNR_BASE_URL)?.replace(/\/$/, "").replace(/:\d{1,5}$/g, "");
|
|
996
|
+
this.baseUrl = `${baseUrlNoPort ?? "https://api.lmnr.ai"}:${httpPort}`;
|
|
997
|
+
this._agent = new AgentResource(this.baseUrl, this.projectApiKey);
|
|
998
|
+
this._browserEvents = new BrowserEventsResource(this.baseUrl, this.projectApiKey);
|
|
999
|
+
this._datasets = new DatasetsResource(this.baseUrl, this.projectApiKey);
|
|
1000
|
+
this._evals = new EvalsResource(this.baseUrl, this.projectApiKey);
|
|
1001
|
+
this._evaluators = new EvaluatorsResource(this.baseUrl, this.projectApiKey);
|
|
1002
|
+
this._tags = new TagsResource(this.baseUrl, this.projectApiKey);
|
|
1003
|
+
}
|
|
1004
|
+
get agent() {
|
|
1005
|
+
return this._agent;
|
|
1006
|
+
}
|
|
1007
|
+
get browserEvents() {
|
|
1008
|
+
return this._browserEvents;
|
|
1009
|
+
}
|
|
1010
|
+
get datasets() {
|
|
1011
|
+
return this._datasets;
|
|
1012
|
+
}
|
|
1013
|
+
get evals() {
|
|
1014
|
+
return this._evals;
|
|
1015
|
+
}
|
|
1016
|
+
get evaluators() {
|
|
1017
|
+
return this._evaluators;
|
|
1018
|
+
}
|
|
1019
|
+
get tags() {
|
|
1020
|
+
return this._tags;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
// src/cli/file-utils.ts
|
|
1025
|
+
var import_csv_parser = __toESM(require("csv-parser"));
|
|
1026
|
+
var import_export_to_csv = require("export-to-csv");
|
|
1027
|
+
var import_fs = require("fs");
|
|
1028
|
+
var fs = __toESM(require("fs/promises"));
|
|
1029
|
+
var path2 = __toESM(require("path"));
|
|
1030
|
+
var logger5 = initializeLogger();
|
|
1031
|
+
var isSupportedFile = (file) => {
|
|
1032
|
+
const ext = path2.extname(file).toLowerCase();
|
|
1033
|
+
return [".json", ".csv", ".jsonl"].includes(ext);
|
|
1034
|
+
};
|
|
1035
|
+
var collectFiles = async (paths, recursive = false) => {
|
|
1036
|
+
const collectedFiles = [];
|
|
1037
|
+
for (const filepath of paths) {
|
|
1038
|
+
try {
|
|
1039
|
+
const stats = await fs.stat(filepath);
|
|
1040
|
+
if (stats.isFile()) {
|
|
1041
|
+
if (isSupportedFile(filepath)) {
|
|
1042
|
+
collectedFiles.push(filepath);
|
|
1043
|
+
} else {
|
|
1044
|
+
logger5.warn(`Skipping unsupported file type: ${filepath}`);
|
|
1045
|
+
}
|
|
1046
|
+
} else if (stats.isDirectory()) {
|
|
1047
|
+
const entries = await fs.readdir(filepath);
|
|
1048
|
+
for (const entry of entries) {
|
|
1049
|
+
const fullPath = path2.join(filepath, entry);
|
|
1050
|
+
const entryStats = await fs.stat(fullPath);
|
|
1051
|
+
if (entryStats.isFile() && isSupportedFile(fullPath)) {
|
|
1052
|
+
collectedFiles.push(fullPath);
|
|
1053
|
+
} else if (recursive && entryStats.isDirectory()) {
|
|
1054
|
+
const subFiles = await collectFiles([fullPath], true);
|
|
1055
|
+
collectedFiles.push(...subFiles);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
logger5.warn(
|
|
1061
|
+
`Path does not exist or is not accessible: ${filepath}. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return collectedFiles;
|
|
1066
|
+
};
|
|
1067
|
+
var readJsonFile = async (filepath) => {
|
|
1068
|
+
const content = await fs.readFile(filepath, "utf-8");
|
|
1069
|
+
const parsed = JSON.parse(content);
|
|
1070
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
1071
|
+
};
|
|
1072
|
+
var tryParseJson = (content) => {
|
|
1073
|
+
if (typeof content !== "string") {
|
|
1074
|
+
return content;
|
|
1075
|
+
}
|
|
1076
|
+
const trimmed = content.trim();
|
|
1077
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
1078
|
+
return content;
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
return JSON.parse(content);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
logger5.debug(
|
|
1084
|
+
`Error parsing JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
1085
|
+
);
|
|
1086
|
+
return content;
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
var parseCsvRow = (row) => {
|
|
1090
|
+
const parsed = {};
|
|
1091
|
+
for (const [key, value] of Object.entries(row)) {
|
|
1092
|
+
parsed[key] = tryParseJson(value);
|
|
1093
|
+
}
|
|
1094
|
+
return parsed;
|
|
1095
|
+
};
|
|
1096
|
+
var readCsvFile = async (filepath) => new Promise((resolve, reject) => {
|
|
1097
|
+
const results = [];
|
|
1098
|
+
(0, import_fs.createReadStream)(filepath).pipe((0, import_csv_parser.default)()).on("data", (data) => results.push(parseCsvRow(data))).on("end", () => resolve(results)).on("error", reject);
|
|
1099
|
+
});
|
|
1100
|
+
async function readJsonlFile(filepath) {
|
|
1101
|
+
const content = await fs.readFile(filepath, "utf-8");
|
|
1102
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
1103
|
+
return lines.map((line) => JSON.parse(line));
|
|
1104
|
+
}
|
|
1105
|
+
async function readFile2(filepath) {
|
|
1106
|
+
const ext = path2.extname(filepath).toLowerCase();
|
|
1107
|
+
if (ext === ".json") {
|
|
1108
|
+
return readJsonFile(filepath);
|
|
1109
|
+
} else if (ext === ".csv") {
|
|
1110
|
+
return readCsvFile(filepath);
|
|
1111
|
+
} else if (ext === ".jsonl") {
|
|
1112
|
+
return readJsonlFile(filepath);
|
|
1113
|
+
} else {
|
|
1114
|
+
throw new Error(`Unsupported file type: ${ext}`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
var loadFromPaths = async (paths, recursive = false) => {
|
|
1118
|
+
const files = await collectFiles(paths, recursive);
|
|
1119
|
+
if (files.length === 0) {
|
|
1120
|
+
logger5.warn("No supported files found in the specified paths");
|
|
1121
|
+
return [];
|
|
1122
|
+
}
|
|
1123
|
+
logger5.info(`Found ${files.length} file(s) to read`);
|
|
1124
|
+
const result = [];
|
|
1125
|
+
for (const file of files) {
|
|
1126
|
+
try {
|
|
1127
|
+
const data = await readFile2(file);
|
|
1128
|
+
result.push(...data);
|
|
1129
|
+
logger5.info(`Read ${data.length} record(s) from ${file}`);
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
logger5.error(
|
|
1132
|
+
`Error reading file ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
1133
|
+
);
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return result;
|
|
1138
|
+
};
|
|
1139
|
+
var writeJsonFile = async (filepath, data) => {
|
|
1140
|
+
const content = JSON.stringify(data, null, 2);
|
|
1141
|
+
await fs.writeFile(filepath, content, "utf-8");
|
|
1142
|
+
};
|
|
1143
|
+
var writeCsvFile = async (filepath, data) => {
|
|
1144
|
+
if (data.length === 0) {
|
|
1145
|
+
throw new Error("No data to write to CSV");
|
|
1146
|
+
}
|
|
1147
|
+
const formattedData = data.map((item) => Object.fromEntries(
|
|
1148
|
+
Object.entries(item).map(([key, value]) => [key, stringifyForCsv(value)])
|
|
1149
|
+
));
|
|
1150
|
+
const csvConfig = (0, import_export_to_csv.mkConfig)({ useKeysAsHeaders: true });
|
|
1151
|
+
const csvOutput = (0, import_export_to_csv.generateCsv)(csvConfig)(formattedData);
|
|
1152
|
+
const csvString = (0, import_export_to_csv.asString)(csvOutput);
|
|
1153
|
+
await fs.writeFile(filepath, csvString, "utf-8");
|
|
1154
|
+
};
|
|
1155
|
+
var writeJsonlFile = async (filepath, data) => {
|
|
1156
|
+
const lines = data.map((item) => JSON.stringify(item)).join("\n");
|
|
1157
|
+
await fs.writeFile(filepath, lines + "\n", "utf-8");
|
|
1158
|
+
};
|
|
1159
|
+
var writeToFile = async (filepath, data, format) => {
|
|
1160
|
+
const dir = path2.dirname(filepath);
|
|
1161
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1162
|
+
const ext = format ?? path2.extname(filepath).slice(1);
|
|
1163
|
+
if (format && format !== path2.extname(filepath).slice(1)) {
|
|
1164
|
+
logger5.warn(
|
|
1165
|
+
`Output format ${format} does not match file extension ${path2.extname(filepath).slice(1)}`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
if (ext === "json") {
|
|
1169
|
+
await writeJsonFile(filepath, data);
|
|
1170
|
+
} else if (ext === "csv") {
|
|
1171
|
+
await writeCsvFile(filepath, data);
|
|
1172
|
+
} else if (ext === "jsonl") {
|
|
1173
|
+
await writeJsonlFile(filepath, data);
|
|
1174
|
+
} else {
|
|
1175
|
+
throw new Error(`Unsupported output format: ${ext}`);
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
var stringifyForCsv = (value) => {
|
|
1179
|
+
if (value === null || value === void 0) {
|
|
1180
|
+
return "";
|
|
1181
|
+
}
|
|
1182
|
+
if (typeof value === "string") {
|
|
1183
|
+
return value;
|
|
1184
|
+
}
|
|
1185
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1186
|
+
return String(value);
|
|
1187
|
+
}
|
|
1188
|
+
return JSON.stringify(value);
|
|
1189
|
+
};
|
|
1190
|
+
var printToConsole = (data, format = "json") => {
|
|
1191
|
+
if (format === "json") {
|
|
1192
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1193
|
+
} else if (format === "csv") {
|
|
1194
|
+
if (data.length === 0) {
|
|
1195
|
+
logger5.error("No data to print");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const formattedData = data.map((item) => Object.fromEntries(
|
|
1199
|
+
Object.entries(item).map(([key, value]) => [key, stringifyForCsv(value)])
|
|
1200
|
+
));
|
|
1201
|
+
const csvConfig = (0, import_export_to_csv.mkConfig)({ useKeysAsHeaders: true });
|
|
1202
|
+
const csvOutput = (0, import_export_to_csv.generateCsv)(csvConfig)(formattedData);
|
|
1203
|
+
const csvString = (0, import_export_to_csv.asString)(csvOutput);
|
|
1204
|
+
console.log(csvString);
|
|
1205
|
+
} else if (format === "jsonl") {
|
|
1206
|
+
data.forEach((item) => console.log(JSON.stringify(item)));
|
|
1207
|
+
} else {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`Unsupported output format: ${String(format)}. (supported formats: json, csv, jsonl)`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
// src/cli/datasets.ts
|
|
1215
|
+
var logger6 = initializeLogger();
|
|
1216
|
+
var DEFAULT_DATASET_PULL_BATCH_SIZE = 100;
|
|
1217
|
+
var DEFAULT_DATASET_PUSH_BATCH_SIZE2 = 100;
|
|
1218
|
+
var pullAllData = async (client, identifier, batchSize = DEFAULT_DATASET_PULL_BATCH_SIZE, offset = 0, limit) => {
|
|
1219
|
+
let hasMore = true;
|
|
1220
|
+
let currentOffset = offset;
|
|
1221
|
+
const stopAt = limit !== void 0 ? offset + limit : void 0;
|
|
1222
|
+
const result = [];
|
|
1223
|
+
while (hasMore && (stopAt === void 0 || currentOffset < stopAt)) {
|
|
1224
|
+
const data = await client.datasets.pull({
|
|
1225
|
+
...identifier,
|
|
1226
|
+
offset: currentOffset,
|
|
1227
|
+
limit: batchSize
|
|
1228
|
+
});
|
|
1229
|
+
result.push(...data.items);
|
|
1230
|
+
if (data.items.length === 0 || data.items.length < batchSize) {
|
|
1231
|
+
hasMore = false;
|
|
1232
|
+
} else if (stopAt !== void 0 && currentOffset + batchSize >= stopAt) {
|
|
1233
|
+
hasMore = false;
|
|
1234
|
+
} else if (data.totalCount !== void 0 && currentOffset + batchSize >= data.totalCount) {
|
|
1235
|
+
hasMore = false;
|
|
1236
|
+
}
|
|
1237
|
+
currentOffset += batchSize;
|
|
1238
|
+
}
|
|
1239
|
+
if (limit !== void 0) {
|
|
1240
|
+
return result.slice(0, limit);
|
|
1241
|
+
}
|
|
1242
|
+
return result;
|
|
1243
|
+
};
|
|
1244
|
+
var handleDatasetsList = async (options) => {
|
|
1245
|
+
const client = new LaminarClient({
|
|
1246
|
+
projectApiKey: options.projectApiKey,
|
|
1247
|
+
baseUrl: options.baseUrl,
|
|
1248
|
+
port: options.port
|
|
1249
|
+
});
|
|
1250
|
+
try {
|
|
1251
|
+
const datasets = await client.datasets.listDatasets();
|
|
1252
|
+
if (datasets.length === 0) {
|
|
1253
|
+
console.log("No datasets found.");
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const idWidth = 36;
|
|
1257
|
+
const createdAtWidth = 19;
|
|
1258
|
+
console.log(`
|
|
1259
|
+
${"ID".padEnd(idWidth)} ${"Created At".padEnd(createdAtWidth)} Name`);
|
|
1260
|
+
console.log(`${"-".repeat(idWidth)} ${"-".repeat(createdAtWidth)} ${"-".repeat(20)}`);
|
|
1261
|
+
for (const dataset of datasets) {
|
|
1262
|
+
const createdAt = new Date(dataset.createdAt);
|
|
1263
|
+
const createdAtStr = createdAt.toISOString().replace("T", " ").substring(0, 19);
|
|
1264
|
+
console.log(
|
|
1265
|
+
`${dataset.id.padEnd(idWidth)} ${createdAtStr.padEnd(createdAtWidth)} ${dataset.name}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
console.log(`
|
|
1269
|
+
Total: ${datasets.length} dataset(s)
|
|
1270
|
+
`);
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
logger6.error(
|
|
1273
|
+
`Failed to list datasets: ${error instanceof Error ? error.message : String(error)}`
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
var handleDatasetsPush = async (paths, options) => {
|
|
1278
|
+
if (!options.name && !options.id) {
|
|
1279
|
+
logger6.error("Either name or id must be provided");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (options.name && options.id) {
|
|
1283
|
+
logger6.error("Only one of name or id must be provided");
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const client = new LaminarClient({
|
|
1287
|
+
projectApiKey: options.projectApiKey,
|
|
1288
|
+
baseUrl: options.baseUrl,
|
|
1289
|
+
port: options.port
|
|
1290
|
+
});
|
|
1291
|
+
const data = await loadFromPaths(paths, options.recursive);
|
|
1292
|
+
if (data.length === 0) {
|
|
1293
|
+
logger6.warn("No data to push. Skipping");
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const identifier = options.name ? { name: options.name } : { id: options.id };
|
|
1297
|
+
try {
|
|
1298
|
+
await client.datasets.push({
|
|
1299
|
+
points: data,
|
|
1300
|
+
...identifier,
|
|
1301
|
+
batchSize: options.batchSize ?? DEFAULT_DATASET_PUSH_BATCH_SIZE2
|
|
1302
|
+
});
|
|
1303
|
+
logger6.info(`Pushed ${data.length} data points to dataset ${options.name || options.id}`);
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
logger6.error(
|
|
1306
|
+
`Failed to push dataset: ${error instanceof Error ? error.message : String(error)}`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
var handleDatasetsPull = async (outputPath, options) => {
|
|
1311
|
+
if (!options.name && !options.id) {
|
|
1312
|
+
logger6.error("Either name or id must be provided");
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (options.name && options.id) {
|
|
1316
|
+
logger6.error("Only one of name or id must be provided");
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const client = new LaminarClient({
|
|
1320
|
+
projectApiKey: options.projectApiKey,
|
|
1321
|
+
baseUrl: options.baseUrl,
|
|
1322
|
+
port: options.port
|
|
1323
|
+
});
|
|
1324
|
+
const identifier = options.name ? { name: options.name } : { id: options.id };
|
|
1325
|
+
try {
|
|
1326
|
+
const result = await pullAllData(
|
|
1327
|
+
client,
|
|
1328
|
+
identifier,
|
|
1329
|
+
options.batchSize ?? DEFAULT_DATASET_PULL_BATCH_SIZE,
|
|
1330
|
+
options.offset ?? 0,
|
|
1331
|
+
options.limit
|
|
1332
|
+
);
|
|
1333
|
+
if (outputPath) {
|
|
1334
|
+
await writeToFile(outputPath, result, options.outputFormat);
|
|
1335
|
+
logger6.info(`Successfully pulled ${result.length} data points to ${outputPath}`);
|
|
1336
|
+
} else {
|
|
1337
|
+
printToConsole(result, options.outputFormat ?? "json");
|
|
1338
|
+
}
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
logger6.error(
|
|
1341
|
+
`Failed to pull dataset: ${error instanceof Error ? error.message : String(error)}`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
var handleDatasetsCreate = async (name, paths, options) => {
|
|
1346
|
+
const client = new LaminarClient({
|
|
1347
|
+
projectApiKey: options.projectApiKey,
|
|
1348
|
+
baseUrl: options.baseUrl,
|
|
1349
|
+
port: options.port
|
|
1350
|
+
});
|
|
1351
|
+
const data = await loadFromPaths(paths, options.recursive);
|
|
1352
|
+
if (data.length === 0) {
|
|
1353
|
+
logger6.warn("No data to push. Skipping");
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
logger6.info(`Pushing ${data.length} data points to dataset '${name}'...`);
|
|
1357
|
+
try {
|
|
1358
|
+
await client.datasets.push({
|
|
1359
|
+
points: data,
|
|
1360
|
+
name,
|
|
1361
|
+
batchSize: options.batchSize ?? DEFAULT_DATASET_PUSH_BATCH_SIZE2,
|
|
1362
|
+
createDataset: true
|
|
1363
|
+
});
|
|
1364
|
+
logger6.info(`Successfully pushed ${data.length} data points to dataset '${name}'`);
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
logger6.error(
|
|
1367
|
+
`Failed to create dataset: ${error instanceof Error ? error.message : String(error)}`
|
|
1368
|
+
);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
logger6.info(`Pulling data from dataset '${name}'...`);
|
|
1372
|
+
try {
|
|
1373
|
+
const result = await pullAllData(
|
|
1374
|
+
client,
|
|
1375
|
+
{ name },
|
|
1376
|
+
options.batchSize ?? DEFAULT_DATASET_PULL_BATCH_SIZE,
|
|
1377
|
+
0,
|
|
1378
|
+
void 0
|
|
1379
|
+
);
|
|
1380
|
+
await writeToFile(options.outputFile, result, options.outputFormat);
|
|
1381
|
+
logger6.info(
|
|
1382
|
+
`Successfully created dataset '${name}' and saved ${result.length} datapoints to ${options.outputFile}`
|
|
1383
|
+
);
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
logger6.error(
|
|
1386
|
+
`Failed to pull dataset after creation: ${error instanceof Error ? error.message : String(error)}`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
// src/cli/evals.ts
|
|
1392
|
+
var esbuild = __toESM(require("esbuild"));
|
|
1393
|
+
var fs2 = __toESM(require("fs"));
|
|
1394
|
+
var glob = __toESM(require("glob"));
|
|
1395
|
+
var logger7 = initializeLogger();
|
|
100
1396
|
var createSkipDynamicImportsPlugin = (skipModules) => ({
|
|
101
1397
|
name: "skip-dynamic-imports",
|
|
102
1398
|
setup(build2) {
|
|
103
1399
|
if (!skipModules || skipModules.length === 0) return;
|
|
104
1400
|
build2.onResolve({ filter: /.*/ }, (args) => {
|
|
105
1401
|
if (args.kind === "dynamic-import" && skipModules.includes(args.path)) {
|
|
106
|
-
|
|
1402
|
+
logger7.warn(`Skipping dynamic import: ${args.path}`);
|
|
107
1403
|
return {
|
|
108
1404
|
path: args.path,
|
|
109
1405
|
namespace: "lmnr-skip-dynamic-import"
|
|
@@ -138,6 +1434,85 @@ function loadModule({
|
|
|
138
1434
|
);
|
|
139
1435
|
return globalThis._evaluations;
|
|
140
1436
|
}
|
|
1437
|
+
async function runEvaluation(files, options) {
|
|
1438
|
+
let evalFiles;
|
|
1439
|
+
if (files && files.length > 0) {
|
|
1440
|
+
evalFiles = files.flatMap((file) => glob.sync(file));
|
|
1441
|
+
} else {
|
|
1442
|
+
evalFiles = glob.sync("evals/**/*.eval.{ts,js}");
|
|
1443
|
+
}
|
|
1444
|
+
evalFiles.sort();
|
|
1445
|
+
if (evalFiles.length === 0) {
|
|
1446
|
+
logger7.error("No evaluation files found. Please provide a file or ensure there are eval files that are named like `*.eval.{ts,js}` in the `evals` directory or its subdirectories.");
|
|
1447
|
+
process.exit(1);
|
|
1448
|
+
}
|
|
1449
|
+
if (files.length === 0) {
|
|
1450
|
+
logger7.info(`Located ${evalFiles.length} evaluation files in evals/`);
|
|
1451
|
+
} else {
|
|
1452
|
+
logger7.info(`Running ${evalFiles.length} evaluation files.`);
|
|
1453
|
+
}
|
|
1454
|
+
const scores = [];
|
|
1455
|
+
for (const file of evalFiles) {
|
|
1456
|
+
logger7.info(`Loading ${file}...`);
|
|
1457
|
+
const buildOptions = {
|
|
1458
|
+
bundle: true,
|
|
1459
|
+
platform: "node",
|
|
1460
|
+
entryPoints: [file],
|
|
1461
|
+
outfile: `tmp_out_${file}.js`,
|
|
1462
|
+
write: false,
|
|
1463
|
+
// will be loaded in memory as a temp file
|
|
1464
|
+
external: [
|
|
1465
|
+
"node_modules/*",
|
|
1466
|
+
"playwright",
|
|
1467
|
+
"puppeteer",
|
|
1468
|
+
"puppeteer-core",
|
|
1469
|
+
"playwright-core",
|
|
1470
|
+
"fsevents",
|
|
1471
|
+
...options.externalPackages ? options.externalPackages : []
|
|
1472
|
+
],
|
|
1473
|
+
plugins: [
|
|
1474
|
+
createSkipDynamicImportsPlugin(options.dynamicImportsToSkip || [])
|
|
1475
|
+
],
|
|
1476
|
+
treeShaking: true
|
|
1477
|
+
};
|
|
1478
|
+
const result = await esbuild.build(buildOptions);
|
|
1479
|
+
if (!result.outputFiles) {
|
|
1480
|
+
logger7.error("Error when building: No output files found it is likely that all eval files are not valid TypeScript or JavaScript files.");
|
|
1481
|
+
if (options.failOnError) {
|
|
1482
|
+
process.exit(1);
|
|
1483
|
+
}
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const outputFileText = result.outputFiles[0].text;
|
|
1487
|
+
const evaluations = loadModule({
|
|
1488
|
+
filename: file,
|
|
1489
|
+
moduleText: outputFileText
|
|
1490
|
+
});
|
|
1491
|
+
logger7.info(`Loaded ${evaluations.length} evaluations from ${file}`);
|
|
1492
|
+
for (const evaluation of evaluations) {
|
|
1493
|
+
if (!evaluation?.run) {
|
|
1494
|
+
logger7.error(`Evaluation ${file} does not properly call evaluate()`);
|
|
1495
|
+
if (options.failOnError) {
|
|
1496
|
+
process.exit(1);
|
|
1497
|
+
}
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const evalResult = await evaluation.run();
|
|
1501
|
+
scores.push({
|
|
1502
|
+
file,
|
|
1503
|
+
scores: evalResult?.averageScores ?? {},
|
|
1504
|
+
url: evalResult?.url ?? "",
|
|
1505
|
+
evaluationId: evalResult?.evaluationId ?? ""
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (options.outputFile) {
|
|
1510
|
+
fs2.writeFileSync(options.outputFile, JSON.stringify(scores, null, 2));
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/cli/index.ts
|
|
1515
|
+
var logger8 = initializeLogger();
|
|
141
1516
|
async function cli() {
|
|
142
1517
|
const program = new import_commander.Command();
|
|
143
1518
|
program.name("lmnr").description("CLI for Laminar. Use `lmnr <subcommand> --help` for more information.").version(version, "-v, --version", "display version number");
|
|
@@ -157,75 +1532,55 @@ async function cli() {
|
|
|
157
1532
|
"--dynamic-imports-to-skip <modules...>",
|
|
158
1533
|
"[ADVANCED] List of module names to skip when encountered as dynamic imports. These dynamic imports will resolve to an empty module to prevent build failures. This is meant to skip the imports that are not used in the evaluation itself."
|
|
159
1534
|
).action(async (files, options) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const evaluations = loadModule({
|
|
210
|
-
filename: file,
|
|
211
|
-
moduleText: outputFileText
|
|
212
|
-
});
|
|
213
|
-
logger2.info(`Loaded ${evaluations.length} evaluations from ${file}`);
|
|
214
|
-
for (const evaluation of evaluations) {
|
|
215
|
-
if (!evaluation?.run) {
|
|
216
|
-
logger2.error(`Evaluation ${file} does not properly call evaluate()`);
|
|
217
|
-
if (options.failOnError) {
|
|
218
|
-
process.exit(1);
|
|
219
|
-
}
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
const evalScores = await evaluation.run();
|
|
223
|
-
scores.push({ file, scores: evalScores });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (options.outputFile) {
|
|
227
|
-
fs.writeFileSync(options.outputFile, JSON.stringify(scores, null, 2));
|
|
228
|
-
}
|
|
1535
|
+
await runEvaluation(files, options);
|
|
1536
|
+
});
|
|
1537
|
+
const datasetsCmd = program.command("datasets").description("Manage datasets").option(
|
|
1538
|
+
"--project-api-key <key>",
|
|
1539
|
+
"Project API key. If not provided, reads from LMNR_PROJECT_API_KEY env variable"
|
|
1540
|
+
).option(
|
|
1541
|
+
"--base-url <url>",
|
|
1542
|
+
"Base URL for the Laminar API. Defaults to https://api.lmnr.ai or LMNR_BASE_URL env variable"
|
|
1543
|
+
).option(
|
|
1544
|
+
"--port <port>",
|
|
1545
|
+
"Port for the Laminar API. Defaults to 443",
|
|
1546
|
+
(val) => parseInt(val, 10)
|
|
1547
|
+
);
|
|
1548
|
+
datasetsCmd.command("list").description("List all datasets").action(async (options, cmd) => {
|
|
1549
|
+
const parentOpts = cmd.parent?.opts() || {};
|
|
1550
|
+
await handleDatasetsList({ ...parentOpts, ...options });
|
|
1551
|
+
});
|
|
1552
|
+
datasetsCmd.command("push").description("Push datapoints to an existing dataset").argument("<paths...>", "Paths to files or directories containing data to push").option("-n, --name <name>", "Name of the dataset (either name or id must be provided)").option("--id <id>", "ID of the dataset (either name or id must be provided)").option("-r, --recursive", "Recursively read files in directories", false).option(
|
|
1553
|
+
"--batch-size <size>",
|
|
1554
|
+
"Batch size for pushing data",
|
|
1555
|
+
(val) => parseInt(val, 10),
|
|
1556
|
+
100
|
|
1557
|
+
).action(async (paths, options, cmd) => {
|
|
1558
|
+
const parentOpts = cmd.parent?.opts() || {};
|
|
1559
|
+
await handleDatasetsPush(paths, { ...parentOpts, ...options });
|
|
1560
|
+
});
|
|
1561
|
+
datasetsCmd.command("pull").description("Pull data from a dataset").argument("[output-path]", "Path to save the data. If not provided, prints to console").option("-n, --name <name>", "Name of the dataset (either name or id must be provided)").option("--id <id>", "ID of the dataset (either name or id must be provided)").option(
|
|
1562
|
+
"--output-format <format>",
|
|
1563
|
+
"Output format (json, csv, jsonl). Inferred from file extension if not provided"
|
|
1564
|
+
).option(
|
|
1565
|
+
"--batch-size <size>",
|
|
1566
|
+
"Batch size for pulling data",
|
|
1567
|
+
(val) => parseInt(val, 10),
|
|
1568
|
+
100
|
|
1569
|
+
).option("--limit <limit>", "Limit number of datapoints to pull", (val) => parseInt(val, 10)).option("--offset <offset>", "Offset for pagination", (val) => parseInt(val, 10), 0).action(async (outputPath, options, cmd) => {
|
|
1570
|
+
const parentOpts = cmd.parent?.opts() || {};
|
|
1571
|
+
await handleDatasetsPull(outputPath, { ...parentOpts, ...options });
|
|
1572
|
+
});
|
|
1573
|
+
datasetsCmd.command("create").description("Create a dataset from input files").argument("<name>", "Name of the dataset to create").argument("<paths...>", "Paths to files or directories containing data to push").requiredOption("-o, --output-file <file>", "Path to save the pulled data").option(
|
|
1574
|
+
"--output-format <format>",
|
|
1575
|
+
"Output format (json, csv, jsonl). Inferred from file extension if not provided"
|
|
1576
|
+
).option("-r, --recursive", "Recursively read files in directories", false).option(
|
|
1577
|
+
"--batch-size <size>",
|
|
1578
|
+
"Batch size for pushing/pulling data",
|
|
1579
|
+
(val) => parseInt(val, 10),
|
|
1580
|
+
100
|
|
1581
|
+
).action(async (name, paths, options, cmd) => {
|
|
1582
|
+
const parentOpts = cmd.parent?.opts() || {};
|
|
1583
|
+
await handleDatasetsCreate(name, paths, { ...parentOpts, ...options });
|
|
229
1584
|
});
|
|
230
1585
|
if (!process.argv.slice(2).length) {
|
|
231
1586
|
program.help();
|
|
@@ -234,11 +1589,7 @@ async function cli() {
|
|
|
234
1589
|
await program.parseAsync();
|
|
235
1590
|
}
|
|
236
1591
|
cli().catch((err) => {
|
|
237
|
-
|
|
1592
|
+
logger8.error(err instanceof Error ? err.message : err);
|
|
238
1593
|
throw err;
|
|
239
1594
|
});
|
|
240
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
241
|
-
0 && (module.exports = {
|
|
242
|
-
loadModule
|
|
243
|
-
});
|
|
244
1595
|
//# sourceMappingURL=cli.js.map
|