Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
"@opentelemetry/api": "^1.9.0"
},
"devDependencies": {
"@types/mustache": "^4.2.6"
"@types/mustache": "^4.2.6",
"@types/progress": "^2.0.7"
},
"optionalDependencies": {
"progress": "^2.0.3"
}
}
40 changes: 36 additions & 4 deletions packages/client/src/experiment/ExperimentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export class ExperimentManager {
metadata,
maxConcurrency: batchSize = 50,
runEvaluators,
progress: progressOption,
} = config;

const runName = this.createExperimentRunName({
Expand All @@ -200,6 +201,35 @@ export class ExperimentManager {
);
}

let progressBar: { tick: (n?: number) => void } | null = null;
const showProgress =
progressOption ??
(typeof process !== "undefined" && process.stderr?.isTTY === true);
const canShowBar =
showProgress &&
typeof process !== "undefined" &&
data.length > 0 &&
(process.stderr?.isTTY === true || progressOption === true);
if (canShowBar) {
if (canShowBar) {
try {
const Progress = (await import("progress")).default as new (
format: string,
options: { total: number; stream: NodeJS.WritableStream },
) => { tick: (n?: number) => void };
progressBar = new Progress(
"Experiment [:bar] :current/:total :percent :eta",
{
total: data.length,
stream: process.stderr,
},
);
} catch (error) {
// Progress module not available, continue without progress bar
}
}
}

const itemResults: ExperimentItemResult<Input, ExpectedOutput, Metadata>[] =
[];

Expand All @@ -208,8 +238,8 @@ export class ExperimentManager {

const promises: Promise<
ExperimentItemResult<Input, ExpectedOutput, Metadata>
>[] = batch.map(async (item) => {
return this.runItem({
>[] = batch.map((item) =>
this.runItem({
item,
evaluators,
task,
Expand All @@ -218,8 +248,10 @@ export class ExperimentManager {
experimentDescription: description,
experimentMetadata: metadata,
datasetVersion: config.datasetVersion,
});
});
}).finally(() => {
progressBar?.tick(1);
}),
);

const settledResults = await Promise.allSettled(promises);
const results = settledResults.reduce(
Expand Down
8 changes: 8 additions & 0 deletions packages/client/src/experiment/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,14 @@ export type ExperimentParams<
* If not provided, returns the latest version.
*/
datasetVersion?: string;

/**
* Whether to show a terminal progress bar (tqdm-style) when running in Node with a TTY.
*
* Default: true when stderr is a TTY, false otherwise (e.g. browser, CI, piped output).
* Set to false to disable the bar.
*/
progress?: boolean;
};

export type ExperimentItemResult<
Expand Down
21 changes: 21 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions tests/e2e/experiments.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,42 @@ describe("Langfuse Datasets E2E", () => {
expect(result.datasetRunId).toBeUndefined();
});

describe("Progress bar", () => {
it("should run experiment with progress: false (no bar)", async () => {
const result = await langfuse.experiment.run({
name: "Progress disabled",
description: "Experiment with progress bar disabled",
data: dataset.slice(0, 2),
task,
evaluators: [createEvaluatorFromAutoevals(Factuality)],
progress: false,
});

await testEnv.spanProcessor.forceFlush();
await waitForServerIngestion(1000);

expect(result.itemResults).toHaveLength(2);
expect(result.runName).toContain("Progress disabled");
});

it("should run experiment with progress: true (bar when TTY)", async () => {
const result = await langfuse.experiment.run({
name: "Progress enabled",
description: "Experiment with progress bar enabled",
data: dataset.slice(0, 2),
task,
evaluators: [createEvaluatorFromAutoevals(Factuality)],
progress: true,
});

await testEnv.spanProcessor.forceFlush();
await waitForServerIngestion(1000);

expect(result.itemResults).toHaveLength(2);
expect(result.runName).toContain("Progress enabled");
});
});

// Error Handling Tests
describe("Error Handling", () => {
it("should handle evaluator failures gracefully", async () => {
Expand Down