diff --git a/.changeset/polite-papayas-occur.md b/.changeset/polite-papayas-occur.md new file mode 100644 index 00000000..ab101e4a --- /dev/null +++ b/.changeset/polite-papayas-occur.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": patch +--- + +dont require LLM Client to use non-ai stagehand functions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d041ac6a..3642d4ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,8 +81,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 50 env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} HEADLESS: true steps: diff --git a/evals/deterministic/tests/Errors/apiKeyError.test.ts b/evals/deterministic/tests/Errors/apiKeyError.test.ts new file mode 100644 index 00000000..c181fe27 --- /dev/null +++ b/evals/deterministic/tests/Errors/apiKeyError.test.ts @@ -0,0 +1,77 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../../lib"; +import StagehandConfig from "../../stagehand.config"; +import { z } from "zod"; + +test.describe("API key/LLMClient error", () => { + test("Should confirm that we get an error if we call extract without LLM API key or LLMClient", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + await stagehand.page.goto("https://docs.browserbase.com/introduction"); + + let errorThrown: Error | null = null; + + try { + await stagehand.page.extract({ + instruction: + "From the introduction page, extract the explanation of what Browserbase is.", + schema: z.object({ + stars: z.string().describe("the explanation of what Browserbase is"), + }), + }); + } catch (error) { + errorThrown = error as Error; + } + + expect(errorThrown).toBeInstanceOf(Error); + expect(errorThrown?.message).toContain( + "No LLM API key or LLM Client configured", + ); + + await stagehand.close(); + }); + + test("Should confirm that we get an error if we call act without LLM API key or LLMClient", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + await stagehand.page.goto("https://docs.browserbase.com/introduction"); + + let errorThrown: Error | null = null; + + try { + await stagehand.page.act({ + action: "Click on the 'Quickstart' section", + }); + } catch (error) { + errorThrown = error as Error; + } + + expect(errorThrown).toBeInstanceOf(Error); + expect(errorThrown?.message).toContain( + "No LLM API key or LLM Client configured", + ); + + await stagehand.close(); + }); + + test("Should confirm that we get an error if we call observe without LLM API key or LLMClient", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + await stagehand.page.goto("https://docs.browserbase.com/introduction"); + + let errorThrown: Error | null = null; + + try { + await stagehand.page.observe(); + } catch (error) { + errorThrown = error as Error; + } + + expect(errorThrown).toBeInstanceOf(Error); + expect(errorThrown?.message).toContain( + "No LLM API key or LLM Client configured", + ); + + await stagehand.close(); + }); +}); diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index fbbf85b0..d7bcafe5 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -56,26 +56,28 @@ export class StagehandPage { }); this.stagehand = stagehand; this.intContext = context; - this.actHandler = new StagehandActHandler({ - verbose: this.stagehand.verbose, - llmProvider: this.stagehand.llmProvider, - enableCaching: this.stagehand.enableCaching, - logger: this.stagehand.logger, - stagehandPage: this, - stagehandContext: this.intContext, - llmClient: llmClient, - }); - this.extractHandler = new StagehandExtractHandler({ - stagehand: this.stagehand, - logger: this.stagehand.logger, - stagehandPage: this, - }); - this.observeHandler = new StagehandObserveHandler({ - stagehand: this.stagehand, - logger: this.stagehand.logger, - stagehandPage: this, - }); this.llmClient = llmClient; + if (this.llmClient) { + this.actHandler = new StagehandActHandler({ + verbose: this.stagehand.verbose, + llmProvider: this.stagehand.llmProvider, + enableCaching: this.stagehand.enableCaching, + logger: this.stagehand.logger, + stagehandPage: this, + stagehandContext: this.intContext, + llmClient: llmClient, + }); + this.extractHandler = new StagehandExtractHandler({ + stagehand: this.stagehand, + logger: this.stagehand.logger, + stagehandPage: this, + }); + this.observeHandler = new StagehandObserveHandler({ + stagehand: this.stagehand, + logger: this.stagehand.logger, + stagehandPage: this, + }); + } } async init(): Promise { @@ -98,22 +100,30 @@ export class StagehandPage { return result; }; - if (prop === "act") { - return async (options: ActOptions) => { - return this.act(options); - }; - } - - if (prop === "extract") { - return async (options: ExtractOptions) => { - return this.extract(options); - }; - } - - if (prop === "observe") { - return async (options: ObserveOptions) => { - return this.observe(options); - }; + if (this.llmClient) { + if (prop === "act") { + return async (options: ActOptions) => { + return this.act(options); + }; + } + if (prop === "extract") { + return async (options: ExtractOptions) => { + return this.extract(options); + }; + } + if (prop === "observe") { + return async (options: ObserveOptions) => { + return this.observe(options); + }; + } + } else { + if (prop === "act" || prop === "extract" || prop === "observe") { + return () => { + throw new Error( + "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.", + ); + }; + } } if (prop === "on") { diff --git a/lib/index.ts b/lib/index.ts index c898074c..0d958f67 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -359,17 +359,22 @@ export class Stagehand { this.projectId = projectId ?? process.env.BROWSERBASE_PROJECT_ID; this.verbose = verbose ?? 0; this.debugDom = debugDom ?? false; - this.llmClient = - llmClient || - this.llmProvider.getClient( - modelName ?? DEFAULT_MODEL_NAME, - modelClientOptions, - ); - - if (!this.llmClient.logger) { + if (llmClient) { + this.llmClient = llmClient; + } else { + try { + // try to set a default LLM client + this.llmClient = this.llmProvider.getClient( + modelName ?? DEFAULT_MODEL_NAME, + modelClientOptions, + ); + } catch { + this.llmClient = undefined; + } + } + if (this.llmClient && !this.llmClient.logger) { this.llmClient.logger = this.logger; } - this.domSettleTimeoutMs = domSettleTimeoutMs ?? 30_000; this.headless = headless ?? false; this.browserbaseSessionCreateParams = browserbaseSessionCreateParams;