@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.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-alpha.2";
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/cli.ts
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
- logger2.warn(`Skipping dynamic import: ${args.path}`);
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
- let evalFiles;
161
- if (files && files.length > 0) {
162
- evalFiles = files.flatMap((file) => glob.sync(file));
163
- } else {
164
- evalFiles = glob.sync("evals/**/*.eval.{ts,js}");
165
- }
166
- evalFiles.sort();
167
- if (evalFiles.length === 0) {
168
- logger2.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.");
169
- process.exit(1);
170
- }
171
- if (files.length === 0) {
172
- logger2.info(`Located ${evalFiles.length} evaluation files in evals/`);
173
- } else {
174
- logger2.info(`Running ${evalFiles.length} evaluation files.`);
175
- }
176
- const scores = [];
177
- for (const file of evalFiles) {
178
- logger2.info(`Loading ${file}...`);
179
- const buildOptions = {
180
- bundle: true,
181
- platform: "node",
182
- entryPoints: [file],
183
- outfile: `tmp_out_${file}.js`,
184
- write: false,
185
- // will be loaded in memory as a temp file
186
- external: [
187
- "node_modules/*",
188
- "playwright",
189
- "puppeteer",
190
- "puppeteer-core",
191
- "playwright-core",
192
- "fsevents",
193
- ...options.externalPackages ? options.externalPackages : []
194
- ],
195
- plugins: [
196
- createSkipDynamicImportsPlugin(options.dynamicImportsToSkip || [])
197
- ],
198
- treeShaking: true
199
- };
200
- const result = await esbuild.build(buildOptions);
201
- if (!result.outputFiles) {
202
- logger2.error("Error when building: No output files found it is likely that all eval files are not valid TypeScript or JavaScript files.");
203
- if (options.failOnError) {
204
- process.exit(1);
205
- }
206
- continue;
207
- }
208
- const outputFileText = result.outputFiles[0].text;
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
- logger2.error(err instanceof Error ? err.message : err);
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