Skip to content

Commit b1c6579

Browse files
dont require LLMClient to use stagehand (#379)
* dont require LLMClient to use stagehand * make sure default LLMClient still works if api key is available * changeset * set logger if not defined * rm API keys for e2e in CI * update error msg * test error occurs without API key or LLM client
1 parent 7ee5841 commit b1c6579

File tree

5 files changed

+141
-46
lines changed

5 files changed

+141
-46
lines changed

Diff for: .changeset/polite-papayas-occur.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
dont require LLM Client to use non-ai stagehand functions

Diff for: .github/workflows/ci.yml

-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ jobs:
8181
runs-on: ubuntu-latest
8282
timeout-minutes: 50
8383
env:
84-
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
85-
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
8684
HEADLESS: true
8785

8886
steps:

Diff for: evals/deterministic/tests/Errors/apiKeyError.test.ts

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { test, expect } from "@playwright/test";
2+
import { Stagehand } from "../../../../lib";
3+
import StagehandConfig from "../../stagehand.config";
4+
import { z } from "zod";
5+
6+
test.describe("API key/LLMClient error", () => {
7+
test("Should confirm that we get an error if we call extract without LLM API key or LLMClient", async () => {
8+
const stagehand = new Stagehand(StagehandConfig);
9+
await stagehand.init();
10+
await stagehand.page.goto("https://docs.browserbase.com/introduction");
11+
12+
let errorThrown: Error | null = null;
13+
14+
try {
15+
await stagehand.page.extract({
16+
instruction:
17+
"From the introduction page, extract the explanation of what Browserbase is.",
18+
schema: z.object({
19+
stars: z.string().describe("the explanation of what Browserbase is"),
20+
}),
21+
});
22+
} catch (error) {
23+
errorThrown = error as Error;
24+
}
25+
26+
expect(errorThrown).toBeInstanceOf(Error);
27+
expect(errorThrown?.message).toContain(
28+
"No LLM API key or LLM Client configured",
29+
);
30+
31+
await stagehand.close();
32+
});
33+
34+
test("Should confirm that we get an error if we call act without LLM API key or LLMClient", async () => {
35+
const stagehand = new Stagehand(StagehandConfig);
36+
await stagehand.init();
37+
await stagehand.page.goto("https://docs.browserbase.com/introduction");
38+
39+
let errorThrown: Error | null = null;
40+
41+
try {
42+
await stagehand.page.act({
43+
action: "Click on the 'Quickstart' section",
44+
});
45+
} catch (error) {
46+
errorThrown = error as Error;
47+
}
48+
49+
expect(errorThrown).toBeInstanceOf(Error);
50+
expect(errorThrown?.message).toContain(
51+
"No LLM API key or LLM Client configured",
52+
);
53+
54+
await stagehand.close();
55+
});
56+
57+
test("Should confirm that we get an error if we call observe without LLM API key or LLMClient", async () => {
58+
const stagehand = new Stagehand(StagehandConfig);
59+
await stagehand.init();
60+
await stagehand.page.goto("https://docs.browserbase.com/introduction");
61+
62+
let errorThrown: Error | null = null;
63+
64+
try {
65+
await stagehand.page.observe();
66+
} catch (error) {
67+
errorThrown = error as Error;
68+
}
69+
70+
expect(errorThrown).toBeInstanceOf(Error);
71+
expect(errorThrown?.message).toContain(
72+
"No LLM API key or LLM Client configured",
73+
);
74+
75+
await stagehand.close();
76+
});
77+
});

Diff for: lib/StagehandPage.ts

+45-35
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,28 @@ export class StagehandPage {
5656
});
5757
this.stagehand = stagehand;
5858
this.intContext = context;
59-
this.actHandler = new StagehandActHandler({
60-
verbose: this.stagehand.verbose,
61-
llmProvider: this.stagehand.llmProvider,
62-
enableCaching: this.stagehand.enableCaching,
63-
logger: this.stagehand.logger,
64-
stagehandPage: this,
65-
stagehandContext: this.intContext,
66-
llmClient: llmClient,
67-
});
68-
this.extractHandler = new StagehandExtractHandler({
69-
stagehand: this.stagehand,
70-
logger: this.stagehand.logger,
71-
stagehandPage: this,
72-
});
73-
this.observeHandler = new StagehandObserveHandler({
74-
stagehand: this.stagehand,
75-
logger: this.stagehand.logger,
76-
stagehandPage: this,
77-
});
7859
this.llmClient = llmClient;
60+
if (this.llmClient) {
61+
this.actHandler = new StagehandActHandler({
62+
verbose: this.stagehand.verbose,
63+
llmProvider: this.stagehand.llmProvider,
64+
enableCaching: this.stagehand.enableCaching,
65+
logger: this.stagehand.logger,
66+
stagehandPage: this,
67+
stagehandContext: this.intContext,
68+
llmClient: llmClient,
69+
});
70+
this.extractHandler = new StagehandExtractHandler({
71+
stagehand: this.stagehand,
72+
logger: this.stagehand.logger,
73+
stagehandPage: this,
74+
});
75+
this.observeHandler = new StagehandObserveHandler({
76+
stagehand: this.stagehand,
77+
logger: this.stagehand.logger,
78+
stagehandPage: this,
79+
});
80+
}
7981
}
8082

8183
async init(): Promise<StagehandPage> {
@@ -98,22 +100,30 @@ export class StagehandPage {
98100
return result;
99101
};
100102

101-
if (prop === "act") {
102-
return async (options: ActOptions) => {
103-
return this.act(options);
104-
};
105-
}
106-
107-
if (prop === "extract") {
108-
return async (options: ExtractOptions<z.AnyZodObject>) => {
109-
return this.extract(options);
110-
};
111-
}
112-
113-
if (prop === "observe") {
114-
return async (options: ObserveOptions) => {
115-
return this.observe(options);
116-
};
103+
if (this.llmClient) {
104+
if (prop === "act") {
105+
return async (options: ActOptions) => {
106+
return this.act(options);
107+
};
108+
}
109+
if (prop === "extract") {
110+
return async (options: ExtractOptions<z.AnyZodObject>) => {
111+
return this.extract(options);
112+
};
113+
}
114+
if (prop === "observe") {
115+
return async (options: ObserveOptions) => {
116+
return this.observe(options);
117+
};
118+
}
119+
} else {
120+
if (prop === "act" || prop === "extract" || prop === "observe") {
121+
return () => {
122+
throw new Error(
123+
"No LLM API key or LLM Client configured. An LLM API key or a custom LLM Client is required to use act, extract, or observe.",
124+
);
125+
};
126+
}
117127
}
118128

119129
if (prop === "on") {

Diff for: lib/index.ts

+14-9
Original file line numberDiff line numberDiff line change
@@ -359,17 +359,22 @@ export class Stagehand {
359359
this.projectId = projectId ?? process.env.BROWSERBASE_PROJECT_ID;
360360
this.verbose = verbose ?? 0;
361361
this.debugDom = debugDom ?? false;
362-
this.llmClient =
363-
llmClient ||
364-
this.llmProvider.getClient(
365-
modelName ?? DEFAULT_MODEL_NAME,
366-
modelClientOptions,
367-
);
368-
369-
if (!this.llmClient.logger) {
362+
if (llmClient) {
363+
this.llmClient = llmClient;
364+
} else {
365+
try {
366+
// try to set a default LLM client
367+
this.llmClient = this.llmProvider.getClient(
368+
modelName ?? DEFAULT_MODEL_NAME,
369+
modelClientOptions,
370+
);
371+
} catch {
372+
this.llmClient = undefined;
373+
}
374+
}
375+
if (this.llmClient && !this.llmClient.logger) {
370376
this.llmClient.logger = this.logger;
371377
}
372-
373378
this.domSettleTimeoutMs = domSettleTimeoutMs ?? 30_000;
374379
this.headless = headless ?? false;
375380
this.browserbaseSessionCreateParams = browserbaseSessionCreateParams;

0 commit comments

Comments
 (0)