Skip to content

Commit 33f2b3f

Browse files
authored
stagehand api offloading (#486)
* add api class & types * simplify stagehand method calls * revert example * changeset * use new syntax in evals * new extract eval * allow no observe input * add api param to constructor * offload to api * refresh page after goto and act * convert zod schema * use env api url * add systemprompt to init * update api responses * throw error when using api w/ local * update stream header * add end call * fix end & stream * rm changeset * revert stagehand config * Update .env.example * changeset
1 parent 2c855cf commit 33f2b3f

File tree

8 files changed

+417
-34
lines changed

8 files changed

+417
-34
lines changed

Diff for: .changeset/afraid-pears-own.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": minor
3+
---
4+
5+
[Unreleased] Parameterized offloading Stagehand method calls to the Stagehand API. In the future, this will allow for better observability and debugging experience.

Diff for: .env.example

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ ENABLE_CACHING=false
77
EVAL_MODELS="gpt-4o,claude-3-5-sonnet-latest"
88
EXPERIMENTAL_EVAL_MODELS="gpt-4o,claude-3-5-sonnet-latest,o1-mini,o1-preview"
99
EVAL_CATEGORIES="observe,act,combination,extract,experimental"
10+
STAGEHAND_API_URL="http://localhost:80"

Diff for: lib/StagehandPage.ts

+94-16
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
1+
import { Browserbase } from "@browserbasehq/sdk";
12
import type {
2-
Page as PlaywrightPage,
3-
BrowserContext as PlaywrightContext,
43
CDPSession,
4+
BrowserContext as PlaywrightContext,
5+
Page as PlaywrightPage,
56
} from "@playwright/test";
6-
import { LLMClient } from "./llm/LLMClient";
7-
import { ActOptions, ActResult, GotoOptions, Stagehand } from "./index";
8-
import { StagehandActHandler } from "./handlers/actHandler";
9-
import { StagehandContext } from "./StagehandContext";
7+
import { chromium } from "@playwright/test";
8+
import { z } from "zod";
109
import { Page, defaultExtractSchema } from "../types/page";
1110
import {
1211
ExtractOptions,
1312
ExtractResult,
1413
ObserveOptions,
1514
ObserveResult,
1615
} from "../types/stagehand";
17-
import { z } from "zod";
16+
import { StagehandAPI } from "./api";
17+
import { StagehandActHandler } from "./handlers/actHandler";
1818
import { StagehandExtractHandler } from "./handlers/extractHandler";
1919
import { StagehandObserveHandler } from "./handlers/observeHandler";
20+
import { ActOptions, ActResult, GotoOptions, Stagehand } from "./index";
21+
import { LLMClient } from "./llm/LLMClient";
22+
import { StagehandContext } from "./StagehandContext";
2023
import { clearOverlays } from "./utils";
2124

25+
const BROWSERBASE_REGION_DOMAIN = {
26+
"us-west-2": "wss://connect.usw2.browserbase.com",
27+
"us-east-1": "wss://connect.use1.browserbase.com",
28+
"eu-central-1": "wss://connect.euc1.browserbase.com",
29+
"ap-southeast-1": "wss://connect.apse1.browserbase.com",
30+
};
31+
2232
export class StagehandPage {
2333
private stagehand: Stagehand;
2434
private intPage: Page;
@@ -28,13 +38,16 @@ export class StagehandPage {
2838
private observeHandler: StagehandObserveHandler;
2939
private llmClient: LLMClient;
3040
private cdpClient: CDPSession | null = null;
41+
private api: StagehandAPI;
42+
private userProvidedInstructions?: string;
3143

3244
constructor(
3345
page: PlaywrightPage,
3446
stagehand: Stagehand,
3547
context: StagehandContext,
3648
llmClient: LLMClient,
3749
userProvidedInstructions?: string,
50+
api?: StagehandAPI,
3851
) {
3952
this.intPage = Object.assign(page, {
4053
act: () => {
@@ -61,6 +74,8 @@ export class StagehandPage {
6174
this.stagehand = stagehand;
6275
this.intContext = context;
6376
this.llmClient = llmClient;
77+
this.api = api;
78+
this.userProvidedInstructions = userProvidedInstructions;
6479
if (this.llmClient) {
6580
this.actHandler = new StagehandActHandler({
6681
verbose: this.stagehand.verbose,
@@ -88,23 +103,72 @@ export class StagehandPage {
88103
}
89104
}
90105

106+
private async _refreshPageFromAPI() {
107+
if (!this.api) return;
108+
109+
const sessionId = this.stagehand.browserbaseSessionID;
110+
if (!sessionId) {
111+
throw new Error("No Browserbase session ID found");
112+
}
113+
114+
const browserbase = new Browserbase({
115+
apiKey: process.env.BROWSERBASE_API_KEY,
116+
});
117+
118+
const sessionStatus = await browserbase.sessions.retrieve(sessionId);
119+
const browserbaseDomain =
120+
BROWSERBASE_REGION_DOMAIN[sessionStatus.region] ||
121+
"wss://connect.browserbase.com";
122+
const connectUrl = `${browserbaseDomain}?apiKey=${process.env.BROWSERBASE_API_KEY}&sessionId=${sessionId}`;
123+
124+
const browser = await chromium.connectOverCDP(connectUrl);
125+
const context = browser.contexts()[0];
126+
const newPage = context.pages()[0];
127+
128+
const newStagehandPage = await new StagehandPage(
129+
newPage,
130+
this.stagehand,
131+
this.intContext,
132+
this.llmClient,
133+
this.userProvidedInstructions,
134+
this.api,
135+
).init();
136+
137+
this.intPage = newStagehandPage.page;
138+
139+
if (this.stagehand.debugDom) {
140+
await this.intPage.evaluate(
141+
(debugDom) => (window.showChunks = debugDom),
142+
this.stagehand.debugDom,
143+
);
144+
}
145+
await this.intPage.waitForLoadState("domcontentloaded");
146+
await this._waitForSettledDom();
147+
}
148+
91149
async init(): Promise<StagehandPage> {
92150
const page = this.intPage;
93151
const stagehand = this.stagehand;
94152
this.intPage = new Proxy(page, {
95153
get: (target, prop) => {
96-
// Override the goto method to add debugDom and waitForSettledDom
97154
if (prop === "goto")
98155
return async (url: string, options: GotoOptions) => {
99-
const result = await page.goto(url, options);
100-
if (stagehand.debugDom) {
101-
await page.evaluate(
102-
(debugDom) => (window.showChunks = debugDom),
103-
stagehand.debugDom,
104-
);
156+
const result = this.api
157+
? await this.api.goto(url, options)
158+
: await page.goto(url, options);
159+
160+
if (this.api) {
161+
await this._refreshPageFromAPI();
162+
} else {
163+
if (stagehand.debugDom) {
164+
await page.evaluate(
165+
(debugDom) => (window.showChunks = debugDom),
166+
stagehand.debugDom,
167+
);
168+
}
169+
await this.intPage.waitForLoadState("domcontentloaded");
170+
await this._waitForSettledDom();
105171
}
106-
await this.intPage.waitForLoadState("domcontentloaded");
107-
await this._waitForSettledDom();
108172
return result;
109173
};
110174

@@ -343,6 +407,12 @@ export class StagehandPage {
343407
});
344408
}
345409

410+
if (this.api) {
411+
const result = await this.api.act(actionOrOptions);
412+
await this._refreshPageFromAPI();
413+
return result;
414+
}
415+
346416
const requestId = Math.random().toString(36).substring(2);
347417
const llmClient: LLMClient = modelName
348418
? this.stagehand.llmProvider.getClient(modelName, modelClientOptions)
@@ -431,6 +501,10 @@ export class StagehandPage {
431501
useTextExtract,
432502
} = options;
433503

504+
if (this.api) {
505+
return this.api.extract<T>(options);
506+
}
507+
434508
const requestId = Math.random().toString(36).substring(2);
435509
const llmClient = modelName
436510
? this.stagehand.llmProvider.getClient(modelName, modelClientOptions)
@@ -540,6 +614,10 @@ export class StagehandPage {
540614
});
541615
}
542616

617+
if (this.api) {
618+
return this.api.observe(options);
619+
}
620+
543621
const requestId = Math.random().toString(36).substring(2);
544622
const llmClient = modelName
545623
? this.stagehand.llmProvider.getClient(modelName, modelClientOptions)

0 commit comments

Comments
 (0)