@browserbasehq/stagehand 1.4.0 → 1.5.0
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/lib/cache/ActionCache.ts +158 -0
- package/lib/cache/BaseCache.ts +553 -0
- package/lib/cache/LLMCache.ts +48 -0
- package/lib/cache.ts +99 -0
- package/lib/dom/build/index.js +626 -0
- package/lib/dom/build/scriptContent.ts +1 -0
- package/lib/dom/debug.ts +147 -0
- package/lib/dom/genDomScripts.ts +29 -0
- package/lib/dom/global.d.ts +25 -0
- package/lib/dom/index.ts +3 -0
- package/lib/dom/process.ts +441 -0
- package/lib/dom/utils.ts +17 -0
- package/lib/dom/xpathUtils.ts +246 -0
- package/lib/handlers/actHandler.ts +1421 -0
- package/lib/handlers/extractHandler.ts +179 -0
- package/lib/handlers/observeHandler.ts +170 -0
- package/lib/index.ts +900 -0
- package/lib/inference.ts +324 -0
- package/lib/llm/AnthropicClient.ts +314 -0
- package/lib/llm/LLMClient.ts +66 -0
- package/lib/llm/LLMProvider.ts +81 -0
- package/lib/llm/OpenAIClient.ts +206 -0
- package/lib/prompt.ts +341 -0
- package/lib/utils.ts +16 -0
- package/lib/vision.ts +299 -0
- package/package.json +3 -3
package/lib/index.ts
ADDED
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import { Browserbase } from "@browserbasehq/sdk";
|
|
2
|
+
import { type BrowserContext, chromium, type Page } from "@playwright/test";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { BrowserResult } from "../types/browser";
|
|
9
|
+
import { LogLine } from "../types/log";
|
|
10
|
+
import {
|
|
11
|
+
ActOptions,
|
|
12
|
+
ActResult,
|
|
13
|
+
ConstructorParams,
|
|
14
|
+
ExtractOptions,
|
|
15
|
+
ExtractResult,
|
|
16
|
+
InitFromPageOptions,
|
|
17
|
+
InitFromPageResult,
|
|
18
|
+
InitOptions,
|
|
19
|
+
InitResult,
|
|
20
|
+
ObserveOptions,
|
|
21
|
+
ObserveResult,
|
|
22
|
+
} from "../types/stagehand";
|
|
23
|
+
import { scriptContent } from "./dom/build/scriptContent";
|
|
24
|
+
import { StagehandActHandler } from "./handlers/actHandler";
|
|
25
|
+
import { StagehandExtractHandler } from "./handlers/extractHandler";
|
|
26
|
+
import { StagehandObserveHandler } from "./handlers/observeHandler";
|
|
27
|
+
import { LLMClient } from "./llm/LLMClient";
|
|
28
|
+
import { LLMProvider } from "./llm/LLMProvider";
|
|
29
|
+
import { logLineToString } from "./utils";
|
|
30
|
+
|
|
31
|
+
require("dotenv").config({ path: ".env" });
|
|
32
|
+
|
|
33
|
+
const DEFAULT_MODEL_NAME = "gpt-4o";
|
|
34
|
+
|
|
35
|
+
async function getBrowser(
|
|
36
|
+
apiKey: string | undefined,
|
|
37
|
+
projectId: string | undefined,
|
|
38
|
+
env: "LOCAL" | "BROWSERBASE" = "LOCAL",
|
|
39
|
+
headless: boolean = false,
|
|
40
|
+
logger: (message: LogLine) => void,
|
|
41
|
+
browserbaseSessionCreateParams?: Browserbase.Sessions.SessionCreateParams,
|
|
42
|
+
browserbaseResumeSessionID?: string,
|
|
43
|
+
): Promise<BrowserResult> {
|
|
44
|
+
if (env === "BROWSERBASE") {
|
|
45
|
+
if (!apiKey) {
|
|
46
|
+
logger({
|
|
47
|
+
category: "init",
|
|
48
|
+
message:
|
|
49
|
+
"BROWSERBASE_API_KEY is required to use BROWSERBASE env. Defaulting to LOCAL.",
|
|
50
|
+
level: 0,
|
|
51
|
+
});
|
|
52
|
+
env = "LOCAL";
|
|
53
|
+
}
|
|
54
|
+
if (!projectId) {
|
|
55
|
+
logger({
|
|
56
|
+
category: "init",
|
|
57
|
+
message:
|
|
58
|
+
"BROWSERBASE_PROJECT_ID is required for some Browserbase features that may not work without it.",
|
|
59
|
+
level: 1,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (env === "BROWSERBASE") {
|
|
65
|
+
if (!apiKey) {
|
|
66
|
+
throw new Error("BROWSERBASE_API_KEY is required.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let debugUrl: string | undefined = undefined;
|
|
70
|
+
let sessionUrl: string | undefined = undefined;
|
|
71
|
+
let sessionId: string;
|
|
72
|
+
let connectUrl: string;
|
|
73
|
+
|
|
74
|
+
const browserbase = new Browserbase({
|
|
75
|
+
apiKey,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (browserbaseResumeSessionID) {
|
|
79
|
+
// Validate the session status
|
|
80
|
+
try {
|
|
81
|
+
const sessionStatus = await browserbase.sessions.retrieve(
|
|
82
|
+
browserbaseResumeSessionID,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (sessionStatus.status !== "RUNNING") {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Session ${browserbaseResumeSessionID} is not running (status: ${sessionStatus.status})`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
sessionId = browserbaseResumeSessionID;
|
|
92
|
+
connectUrl = `wss://connect.browserbase.com?apiKey=${apiKey}&sessionId=${sessionId}`;
|
|
93
|
+
|
|
94
|
+
logger({
|
|
95
|
+
category: "init",
|
|
96
|
+
message: "resuming existing browserbase session...",
|
|
97
|
+
level: 1,
|
|
98
|
+
auxiliary: {
|
|
99
|
+
sessionId: {
|
|
100
|
+
value: sessionId,
|
|
101
|
+
type: "string",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
logger({
|
|
107
|
+
category: "init",
|
|
108
|
+
message: "failed to resume session",
|
|
109
|
+
level: 1,
|
|
110
|
+
auxiliary: {
|
|
111
|
+
error: {
|
|
112
|
+
value: error.message,
|
|
113
|
+
type: "string",
|
|
114
|
+
},
|
|
115
|
+
trace: {
|
|
116
|
+
value: error.stack,
|
|
117
|
+
type: "string",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Create new session (existing code)
|
|
125
|
+
logger({
|
|
126
|
+
category: "init",
|
|
127
|
+
message: "creating new browserbase session...",
|
|
128
|
+
level: 0,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!projectId) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"BROWSERBASE_PROJECT_ID is required for new Browserbase sessions.",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const session = await browserbase.sessions.create({
|
|
138
|
+
projectId,
|
|
139
|
+
...browserbaseSessionCreateParams,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
sessionId = session.id;
|
|
143
|
+
connectUrl = session.connectUrl;
|
|
144
|
+
logger({
|
|
145
|
+
category: "init",
|
|
146
|
+
message: "created new browserbase session",
|
|
147
|
+
level: 1,
|
|
148
|
+
auxiliary: {
|
|
149
|
+
sessionId: {
|
|
150
|
+
value: sessionId,
|
|
151
|
+
type: "string",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const browser = await chromium.connectOverCDP(connectUrl);
|
|
158
|
+
const { debuggerUrl } = await browserbase.sessions.debug(sessionId);
|
|
159
|
+
|
|
160
|
+
debugUrl = debuggerUrl;
|
|
161
|
+
sessionUrl = `https://www.browserbase.com/sessions/${sessionId}`;
|
|
162
|
+
|
|
163
|
+
logger({
|
|
164
|
+
category: "init",
|
|
165
|
+
message: browserbaseResumeSessionID
|
|
166
|
+
? "browserbase session resumed"
|
|
167
|
+
: "browserbase session started",
|
|
168
|
+
level: 0,
|
|
169
|
+
auxiliary: {
|
|
170
|
+
sessionUrl: {
|
|
171
|
+
value: sessionUrl,
|
|
172
|
+
type: "string",
|
|
173
|
+
},
|
|
174
|
+
debugUrl: {
|
|
175
|
+
value: debugUrl,
|
|
176
|
+
type: "string",
|
|
177
|
+
},
|
|
178
|
+
sessionId: {
|
|
179
|
+
value: sessionId,
|
|
180
|
+
type: "string",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const context = browser.contexts()[0];
|
|
186
|
+
|
|
187
|
+
return { browser, context, debugUrl, sessionUrl };
|
|
188
|
+
} else {
|
|
189
|
+
logger({
|
|
190
|
+
category: "init",
|
|
191
|
+
message: "launching local browser",
|
|
192
|
+
level: 0,
|
|
193
|
+
auxiliary: {
|
|
194
|
+
headless: {
|
|
195
|
+
value: headless.toString(),
|
|
196
|
+
type: "boolean",
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const tmpDirPath = path.join(os.tmpdir(), "stagehand");
|
|
202
|
+
if (!fs.existsSync(tmpDirPath)) {
|
|
203
|
+
fs.mkdirSync(tmpDirPath, { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const tmpDir = fs.mkdtempSync(path.join(tmpDirPath, "ctx_"));
|
|
207
|
+
fs.mkdirSync(path.join(tmpDir, "userdir/Default"), { recursive: true });
|
|
208
|
+
|
|
209
|
+
const defaultPreferences = {
|
|
210
|
+
plugins: {
|
|
211
|
+
always_open_pdf_externally: true,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
fs.writeFileSync(
|
|
216
|
+
path.join(tmpDir, "userdir/Default/Preferences"),
|
|
217
|
+
JSON.stringify(defaultPreferences),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const downloadsPath = path.join(process.cwd(), "downloads");
|
|
221
|
+
fs.mkdirSync(downloadsPath, { recursive: true });
|
|
222
|
+
|
|
223
|
+
const context = await chromium.launchPersistentContext(
|
|
224
|
+
path.join(tmpDir, "userdir"),
|
|
225
|
+
{
|
|
226
|
+
acceptDownloads: true,
|
|
227
|
+
headless: headless,
|
|
228
|
+
viewport: {
|
|
229
|
+
width: 1250,
|
|
230
|
+
height: 800,
|
|
231
|
+
},
|
|
232
|
+
locale: "en-US",
|
|
233
|
+
timezoneId: "America/New_York",
|
|
234
|
+
deviceScaleFactor: 1,
|
|
235
|
+
args: [
|
|
236
|
+
"--enable-webgl",
|
|
237
|
+
"--use-gl=swiftshader",
|
|
238
|
+
"--enable-accelerated-2d-canvas",
|
|
239
|
+
"--disable-blink-features=AutomationControlled",
|
|
240
|
+
"--disable-web-security",
|
|
241
|
+
],
|
|
242
|
+
bypassCSP: true,
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
logger({
|
|
247
|
+
category: "init",
|
|
248
|
+
message: "local browser started successfully.",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await applyStealthScripts(context);
|
|
252
|
+
|
|
253
|
+
return { context, contextPath: tmpDir };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function applyStealthScripts(context: BrowserContext) {
|
|
258
|
+
await context.addInitScript(() => {
|
|
259
|
+
// Override the navigator.webdriver property
|
|
260
|
+
Object.defineProperty(navigator, "webdriver", {
|
|
261
|
+
get: () => undefined,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Mock languages and plugins to mimic a real browser
|
|
265
|
+
Object.defineProperty(navigator, "languages", {
|
|
266
|
+
get: () => ["en-US", "en"],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
Object.defineProperty(navigator, "plugins", {
|
|
270
|
+
get: () => [1, 2, 3, 4, 5],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Remove Playwright-specific properties
|
|
274
|
+
delete (window as any).__playwright;
|
|
275
|
+
delete (window as any).__pw_manual;
|
|
276
|
+
delete (window as any).__PW_inspect;
|
|
277
|
+
|
|
278
|
+
// Redefine the headless property
|
|
279
|
+
Object.defineProperty(navigator, "headless", {
|
|
280
|
+
get: () => false,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Override the permissions API
|
|
284
|
+
const originalQuery = window.navigator.permissions.query;
|
|
285
|
+
window.navigator.permissions.query = (parameters: any) =>
|
|
286
|
+
parameters.name === "notifications"
|
|
287
|
+
? Promise.resolve({
|
|
288
|
+
state: Notification.permission,
|
|
289
|
+
} as PermissionStatus)
|
|
290
|
+
: originalQuery(parameters);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export class Stagehand {
|
|
295
|
+
private llmProvider: LLMProvider;
|
|
296
|
+
private llmClient: LLMClient;
|
|
297
|
+
public page: Page;
|
|
298
|
+
public context: BrowserContext;
|
|
299
|
+
private env: "LOCAL" | "BROWSERBASE";
|
|
300
|
+
private apiKey: string | undefined;
|
|
301
|
+
private projectId: string | undefined;
|
|
302
|
+
private verbose: 0 | 1 | 2;
|
|
303
|
+
private debugDom: boolean;
|
|
304
|
+
private headless: boolean;
|
|
305
|
+
private logger: (logLine: LogLine) => void;
|
|
306
|
+
private externalLogger?: (logLine: LogLine) => void;
|
|
307
|
+
private domSettleTimeoutMs: number;
|
|
308
|
+
private browserBaseSessionCreateParams?: Browserbase.Sessions.SessionCreateParams;
|
|
309
|
+
private enableCaching: boolean;
|
|
310
|
+
private variables: { [key: string]: any };
|
|
311
|
+
private browserbaseResumeSessionID?: string;
|
|
312
|
+
private contextPath?: string;
|
|
313
|
+
|
|
314
|
+
private actHandler?: StagehandActHandler;
|
|
315
|
+
private extractHandler?: StagehandExtractHandler;
|
|
316
|
+
private observeHandler?: StagehandObserveHandler;
|
|
317
|
+
|
|
318
|
+
constructor(
|
|
319
|
+
{
|
|
320
|
+
env,
|
|
321
|
+
apiKey,
|
|
322
|
+
projectId,
|
|
323
|
+
verbose,
|
|
324
|
+
debugDom,
|
|
325
|
+
llmProvider,
|
|
326
|
+
headless,
|
|
327
|
+
logger,
|
|
328
|
+
browserBaseSessionCreateParams,
|
|
329
|
+
domSettleTimeoutMs,
|
|
330
|
+
enableCaching,
|
|
331
|
+
browserbaseResumeSessionID,
|
|
332
|
+
modelName,
|
|
333
|
+
modelClientOptions,
|
|
334
|
+
}: ConstructorParams = {
|
|
335
|
+
env: "BROWSERBASE",
|
|
336
|
+
},
|
|
337
|
+
) {
|
|
338
|
+
this.externalLogger = logger;
|
|
339
|
+
this.logger = this.log.bind(this);
|
|
340
|
+
this.enableCaching =
|
|
341
|
+
enableCaching ??
|
|
342
|
+
(process.env.ENABLE_CACHING && process.env.ENABLE_CACHING === "true");
|
|
343
|
+
this.llmProvider =
|
|
344
|
+
llmProvider || new LLMProvider(this.logger, this.enableCaching);
|
|
345
|
+
this.env = env;
|
|
346
|
+
this.apiKey = apiKey ?? process.env.BROWSERBASE_API_KEY;
|
|
347
|
+
this.projectId = projectId ?? process.env.BROWSERBASE_PROJECT_ID;
|
|
348
|
+
this.verbose = verbose ?? 0;
|
|
349
|
+
this.debugDom = debugDom ?? false;
|
|
350
|
+
this.llmClient = this.llmProvider.getClient(
|
|
351
|
+
modelName ?? DEFAULT_MODEL_NAME,
|
|
352
|
+
modelClientOptions,
|
|
353
|
+
);
|
|
354
|
+
this.domSettleTimeoutMs = domSettleTimeoutMs ?? 30_000;
|
|
355
|
+
this.headless = headless ?? false;
|
|
356
|
+
this.browserBaseSessionCreateParams = browserBaseSessionCreateParams;
|
|
357
|
+
this.browserbaseResumeSessionID = browserbaseResumeSessionID;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async init({
|
|
361
|
+
modelName,
|
|
362
|
+
modelClientOptions,
|
|
363
|
+
domSettleTimeoutMs,
|
|
364
|
+
}: InitOptions = {}): Promise<InitResult> {
|
|
365
|
+
const llmClient = modelName
|
|
366
|
+
? this.llmProvider.getClient(modelName, modelClientOptions)
|
|
367
|
+
: this.llmClient;
|
|
368
|
+
const { context, debugUrl, sessionUrl, contextPath } = await getBrowser(
|
|
369
|
+
this.apiKey,
|
|
370
|
+
this.projectId,
|
|
371
|
+
this.env,
|
|
372
|
+
this.headless,
|
|
373
|
+
this.logger,
|
|
374
|
+
this.browserBaseSessionCreateParams,
|
|
375
|
+
this.browserbaseResumeSessionID,
|
|
376
|
+
).catch((e) => {
|
|
377
|
+
console.error("Error in init:", e);
|
|
378
|
+
return {
|
|
379
|
+
context: undefined,
|
|
380
|
+
debugUrl: undefined,
|
|
381
|
+
sessionUrl: undefined,
|
|
382
|
+
} as BrowserResult;
|
|
383
|
+
});
|
|
384
|
+
this.contextPath = contextPath;
|
|
385
|
+
this.context = context;
|
|
386
|
+
this.page = context.pages()[0];
|
|
387
|
+
// Redundant but needed for users who are re-connecting to a previously-created session
|
|
388
|
+
await this.page.waitForLoadState("domcontentloaded");
|
|
389
|
+
await this._waitForSettledDom();
|
|
390
|
+
this.domSettleTimeoutMs = domSettleTimeoutMs ?? this.domSettleTimeoutMs;
|
|
391
|
+
|
|
392
|
+
// Overload the page.goto method
|
|
393
|
+
const originalGoto = this.page.goto.bind(this.page);
|
|
394
|
+
this.page.goto = async (url: string, options?: any) => {
|
|
395
|
+
const result = await originalGoto(url, options);
|
|
396
|
+
if (this.debugDom) {
|
|
397
|
+
await this.page.evaluate(() => (window.showChunks = this.debugDom));
|
|
398
|
+
}
|
|
399
|
+
await this.page.waitForLoadState("domcontentloaded");
|
|
400
|
+
await this._waitForSettledDom();
|
|
401
|
+
return result;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Set the browser to headless mode if specified
|
|
405
|
+
if (this.headless) {
|
|
406
|
+
await this.page.setViewportSize({ width: 1280, height: 720 });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
await this.context.addInitScript({
|
|
410
|
+
content: scriptContent,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
this.actHandler = new StagehandActHandler({
|
|
414
|
+
stagehand: this,
|
|
415
|
+
verbose: this.verbose,
|
|
416
|
+
llmProvider: this.llmProvider,
|
|
417
|
+
enableCaching: this.enableCaching,
|
|
418
|
+
logger: this.logger,
|
|
419
|
+
waitForSettledDom: this._waitForSettledDom.bind(this),
|
|
420
|
+
startDomDebug: this.startDomDebug.bind(this),
|
|
421
|
+
cleanupDomDebug: this.cleanupDomDebug.bind(this),
|
|
422
|
+
llmClient,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
this.extractHandler = new StagehandExtractHandler({
|
|
426
|
+
stagehand: this,
|
|
427
|
+
logger: this.logger,
|
|
428
|
+
waitForSettledDom: this._waitForSettledDom.bind(this),
|
|
429
|
+
startDomDebug: this.startDomDebug.bind(this),
|
|
430
|
+
cleanupDomDebug: this.cleanupDomDebug.bind(this),
|
|
431
|
+
llmProvider: this.llmProvider,
|
|
432
|
+
verbose: this.verbose,
|
|
433
|
+
llmClient,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
this.observeHandler = new StagehandObserveHandler({
|
|
437
|
+
stagehand: this,
|
|
438
|
+
logger: this.logger,
|
|
439
|
+
waitForSettledDom: this._waitForSettledDom.bind(this),
|
|
440
|
+
startDomDebug: this.startDomDebug.bind(this),
|
|
441
|
+
cleanupDomDebug: this.cleanupDomDebug.bind(this),
|
|
442
|
+
llmProvider: this.llmProvider,
|
|
443
|
+
verbose: this.verbose,
|
|
444
|
+
llmClient,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
this.llmClient = llmClient;
|
|
448
|
+
return { debugUrl, sessionUrl };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async initFromPage({
|
|
452
|
+
page,
|
|
453
|
+
modelName,
|
|
454
|
+
modelClientOptions,
|
|
455
|
+
}: InitFromPageOptions): Promise<InitFromPageResult> {
|
|
456
|
+
this.page = page;
|
|
457
|
+
this.context = page.context();
|
|
458
|
+
this.llmClient = modelName
|
|
459
|
+
? this.llmProvider.getClient(modelName, modelClientOptions)
|
|
460
|
+
: this.llmClient;
|
|
461
|
+
|
|
462
|
+
const originalGoto = this.page.goto.bind(this.page);
|
|
463
|
+
this.page.goto = async (url: string, options?: any) => {
|
|
464
|
+
const result = await originalGoto(url, options);
|
|
465
|
+
if (this.debugDom) {
|
|
466
|
+
await this.page.evaluate(() => (window.showChunks = this.debugDom));
|
|
467
|
+
}
|
|
468
|
+
await this.page.waitForLoadState("domcontentloaded");
|
|
469
|
+
await this._waitForSettledDom();
|
|
470
|
+
return result;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// Set the browser to headless mode if specified
|
|
474
|
+
if (this.headless) {
|
|
475
|
+
await this.page.setViewportSize({ width: 1280, height: 720 });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Add initialization scripts
|
|
479
|
+
await this.context.addInitScript({
|
|
480
|
+
content: scriptContent,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
return { context: this.context };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private pending_logs_to_send_to_browserbase: LogLine[] = [];
|
|
487
|
+
|
|
488
|
+
private is_processing_browserbase_logs: boolean = false;
|
|
489
|
+
|
|
490
|
+
log(logObj: LogLine): void {
|
|
491
|
+
logObj.level = logObj.level || 1;
|
|
492
|
+
|
|
493
|
+
// Normal Logging
|
|
494
|
+
if (this.externalLogger) {
|
|
495
|
+
this.externalLogger(logObj);
|
|
496
|
+
} else {
|
|
497
|
+
const logMessage = logLineToString(logObj);
|
|
498
|
+
console.log(logMessage);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Add the logs to the browserbase session
|
|
502
|
+
this.pending_logs_to_send_to_browserbase.push({
|
|
503
|
+
...logObj,
|
|
504
|
+
id: randomUUID(),
|
|
505
|
+
});
|
|
506
|
+
this._run_browserbase_log_processing_cycle();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private async _run_browserbase_log_processing_cycle() {
|
|
510
|
+
if (this.is_processing_browserbase_logs) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.is_processing_browserbase_logs = true;
|
|
514
|
+
const pending_logs = [...this.pending_logs_to_send_to_browserbase];
|
|
515
|
+
for (const logObj of pending_logs) {
|
|
516
|
+
await this._log_to_browserbase(logObj);
|
|
517
|
+
}
|
|
518
|
+
this.is_processing_browserbase_logs = false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private async _log_to_browserbase(logObj: LogLine) {
|
|
522
|
+
logObj.level = logObj.level || 1;
|
|
523
|
+
|
|
524
|
+
if (!this.page) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (this.verbose >= logObj.level) {
|
|
529
|
+
await this.page
|
|
530
|
+
.evaluate((logObj) => {
|
|
531
|
+
const logMessage = logLineToString(logObj);
|
|
532
|
+
if (
|
|
533
|
+
logObj.message.toLowerCase().includes("trace") ||
|
|
534
|
+
logObj.message.toLowerCase().includes("error:")
|
|
535
|
+
) {
|
|
536
|
+
console.error(logMessage);
|
|
537
|
+
} else {
|
|
538
|
+
console.log(logMessage);
|
|
539
|
+
}
|
|
540
|
+
}, logObj)
|
|
541
|
+
.then(() => {
|
|
542
|
+
this.pending_logs_to_send_to_browserbase =
|
|
543
|
+
this.pending_logs_to_send_to_browserbase.filter(
|
|
544
|
+
(log) => log.id !== logObj.id,
|
|
545
|
+
);
|
|
546
|
+
})
|
|
547
|
+
.catch((e) => {
|
|
548
|
+
// NAVIDTODO: Rerun the log call on the new page
|
|
549
|
+
// This is expected to happen when the user is changing pages
|
|
550
|
+
// console.error("Logging Error:", e);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private async _waitForSettledDom(timeoutMs?: number) {
|
|
556
|
+
try {
|
|
557
|
+
const timeout = timeoutMs ?? this.domSettleTimeoutMs;
|
|
558
|
+
let timeoutHandle: NodeJS.Timeout;
|
|
559
|
+
|
|
560
|
+
const timeoutPromise = new Promise<void>((resolve, reject) => {
|
|
561
|
+
timeoutHandle = setTimeout(() => {
|
|
562
|
+
this.log({
|
|
563
|
+
category: "dom",
|
|
564
|
+
message: "DOM settle timeout exceeded, continuing anyway",
|
|
565
|
+
level: 1,
|
|
566
|
+
auxiliary: {
|
|
567
|
+
timeout_ms: {
|
|
568
|
+
value: timeout.toString(),
|
|
569
|
+
type: "integer",
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
resolve();
|
|
574
|
+
}, timeout);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
await Promise.race([
|
|
579
|
+
this.page.evaluate(() => {
|
|
580
|
+
return new Promise<void>((resolve) => {
|
|
581
|
+
if (typeof window.waitForDomSettle === "function") {
|
|
582
|
+
window.waitForDomSettle().then(resolve);
|
|
583
|
+
} else {
|
|
584
|
+
console.warn(
|
|
585
|
+
"waitForDomSettle is not defined, considering DOM as settled",
|
|
586
|
+
);
|
|
587
|
+
resolve();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}),
|
|
591
|
+
this.page.waitForLoadState("domcontentloaded"),
|
|
592
|
+
this.page.waitForSelector("body"),
|
|
593
|
+
timeoutPromise,
|
|
594
|
+
]);
|
|
595
|
+
} finally {
|
|
596
|
+
clearTimeout(timeoutHandle!);
|
|
597
|
+
}
|
|
598
|
+
} catch (e) {
|
|
599
|
+
this.log({
|
|
600
|
+
category: "dom",
|
|
601
|
+
message: "Error in waitForSettledDom",
|
|
602
|
+
level: 1,
|
|
603
|
+
auxiliary: {
|
|
604
|
+
error: {
|
|
605
|
+
value: e.message,
|
|
606
|
+
type: "string",
|
|
607
|
+
},
|
|
608
|
+
trace: {
|
|
609
|
+
value: e.stack,
|
|
610
|
+
type: "string",
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private async startDomDebug() {
|
|
618
|
+
try {
|
|
619
|
+
await this.page
|
|
620
|
+
.evaluate(() => {
|
|
621
|
+
if (typeof window.debugDom === "function") {
|
|
622
|
+
window.debugDom();
|
|
623
|
+
} else {
|
|
624
|
+
this.log({
|
|
625
|
+
category: "dom",
|
|
626
|
+
message: "debugDom is not defined",
|
|
627
|
+
level: 1,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
.catch(() => {});
|
|
632
|
+
} catch (e) {
|
|
633
|
+
this.log({
|
|
634
|
+
category: "dom",
|
|
635
|
+
message: "Error in startDomDebug",
|
|
636
|
+
level: 1,
|
|
637
|
+
auxiliary: {
|
|
638
|
+
error: {
|
|
639
|
+
value: e.message,
|
|
640
|
+
type: "string",
|
|
641
|
+
},
|
|
642
|
+
trace: {
|
|
643
|
+
value: e.stack,
|
|
644
|
+
type: "string",
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private async cleanupDomDebug() {
|
|
652
|
+
if (this.debugDom) {
|
|
653
|
+
await this.page.evaluate(() => window.cleanupDebug()).catch(() => {});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async act({
|
|
658
|
+
action,
|
|
659
|
+
modelName,
|
|
660
|
+
modelClientOptions,
|
|
661
|
+
useVision = "fallback",
|
|
662
|
+
variables = {},
|
|
663
|
+
domSettleTimeoutMs,
|
|
664
|
+
}: ActOptions): Promise<ActResult> {
|
|
665
|
+
if (!this.actHandler) {
|
|
666
|
+
throw new Error("Act handler not initialized");
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
useVision = useVision ?? "fallback";
|
|
670
|
+
const requestId = Math.random().toString(36).substring(2);
|
|
671
|
+
const llmClient: LLMClient = modelName
|
|
672
|
+
? this.llmProvider.getClient(modelName, modelClientOptions)
|
|
673
|
+
: this.llmClient;
|
|
674
|
+
|
|
675
|
+
this.log({
|
|
676
|
+
category: "act",
|
|
677
|
+
message: "running act",
|
|
678
|
+
level: 1,
|
|
679
|
+
auxiliary: {
|
|
680
|
+
action: {
|
|
681
|
+
value: action,
|
|
682
|
+
type: "string",
|
|
683
|
+
},
|
|
684
|
+
requestId: {
|
|
685
|
+
value: requestId,
|
|
686
|
+
type: "string",
|
|
687
|
+
},
|
|
688
|
+
modelName: {
|
|
689
|
+
value: llmClient.modelName,
|
|
690
|
+
type: "string",
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (variables) {
|
|
696
|
+
this.variables = { ...this.variables, ...variables };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return this.actHandler
|
|
700
|
+
.act({
|
|
701
|
+
action,
|
|
702
|
+
llmClient,
|
|
703
|
+
chunksSeen: [],
|
|
704
|
+
useVision,
|
|
705
|
+
verifierUseVision: useVision !== false,
|
|
706
|
+
requestId,
|
|
707
|
+
variables,
|
|
708
|
+
previousSelectors: [],
|
|
709
|
+
skipActionCacheForThisStep: false,
|
|
710
|
+
domSettleTimeoutMs,
|
|
711
|
+
})
|
|
712
|
+
.catch((e) => {
|
|
713
|
+
this.log({
|
|
714
|
+
category: "act",
|
|
715
|
+
message: "error acting",
|
|
716
|
+
level: 1,
|
|
717
|
+
auxiliary: {
|
|
718
|
+
error: {
|
|
719
|
+
value: e.message,
|
|
720
|
+
type: "string",
|
|
721
|
+
},
|
|
722
|
+
trace: {
|
|
723
|
+
value: e.stack,
|
|
724
|
+
type: "string",
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
success: false,
|
|
731
|
+
message: `Internal error: Error acting: ${e.message}`,
|
|
732
|
+
action: action,
|
|
733
|
+
};
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async extract<T extends z.AnyZodObject>({
|
|
738
|
+
instruction,
|
|
739
|
+
schema,
|
|
740
|
+
modelName,
|
|
741
|
+
modelClientOptions,
|
|
742
|
+
domSettleTimeoutMs,
|
|
743
|
+
}: ExtractOptions<T>): Promise<ExtractResult<T>> {
|
|
744
|
+
if (!this.extractHandler) {
|
|
745
|
+
throw new Error("Extract handler not initialized");
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const requestId = Math.random().toString(36).substring(2);
|
|
749
|
+
const llmClient = modelName
|
|
750
|
+
? this.llmProvider.getClient(modelName, modelClientOptions)
|
|
751
|
+
: this.llmClient;
|
|
752
|
+
|
|
753
|
+
this.logger({
|
|
754
|
+
category: "extract",
|
|
755
|
+
message: "running extract",
|
|
756
|
+
level: 1,
|
|
757
|
+
auxiliary: {
|
|
758
|
+
instruction: {
|
|
759
|
+
value: instruction,
|
|
760
|
+
type: "string",
|
|
761
|
+
},
|
|
762
|
+
requestId: {
|
|
763
|
+
value: requestId,
|
|
764
|
+
type: "string",
|
|
765
|
+
},
|
|
766
|
+
modelName: {
|
|
767
|
+
value: llmClient.modelName,
|
|
768
|
+
type: "string",
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
return this.extractHandler
|
|
774
|
+
.extract({
|
|
775
|
+
instruction,
|
|
776
|
+
schema,
|
|
777
|
+
llmClient,
|
|
778
|
+
requestId,
|
|
779
|
+
domSettleTimeoutMs,
|
|
780
|
+
})
|
|
781
|
+
.catch((e) => {
|
|
782
|
+
this.logger({
|
|
783
|
+
category: "extract",
|
|
784
|
+
message: "error extracting",
|
|
785
|
+
level: 1,
|
|
786
|
+
auxiliary: {
|
|
787
|
+
error: {
|
|
788
|
+
value: e.message,
|
|
789
|
+
type: "string",
|
|
790
|
+
},
|
|
791
|
+
trace: {
|
|
792
|
+
value: e.stack,
|
|
793
|
+
type: "string",
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
if (this.enableCaching) {
|
|
799
|
+
this.llmProvider.cleanRequestCache(requestId);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
throw e;
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async observe(options?: ObserveOptions): Promise<ObserveResult[]> {
|
|
807
|
+
if (!this.observeHandler) {
|
|
808
|
+
throw new Error("Observe handler not initialized");
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const requestId = Math.random().toString(36).substring(2);
|
|
812
|
+
const llmClient = options?.modelName
|
|
813
|
+
? this.llmProvider.getClient(
|
|
814
|
+
options.modelName,
|
|
815
|
+
options.modelClientOptions,
|
|
816
|
+
)
|
|
817
|
+
: this.llmClient;
|
|
818
|
+
|
|
819
|
+
this.logger({
|
|
820
|
+
category: "observe",
|
|
821
|
+
message: "running observe",
|
|
822
|
+
level: 1,
|
|
823
|
+
auxiliary: {
|
|
824
|
+
instruction: {
|
|
825
|
+
value: options?.instruction,
|
|
826
|
+
type: "string",
|
|
827
|
+
},
|
|
828
|
+
requestId: {
|
|
829
|
+
value: requestId,
|
|
830
|
+
type: "string",
|
|
831
|
+
},
|
|
832
|
+
modelName: {
|
|
833
|
+
value: llmClient.modelName,
|
|
834
|
+
type: "string",
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
return this.observeHandler
|
|
840
|
+
.observe({
|
|
841
|
+
instruction:
|
|
842
|
+
options?.instruction ??
|
|
843
|
+
"Find actions that can be performed on this page.",
|
|
844
|
+
llmClient,
|
|
845
|
+
useVision: options?.useVision ?? false,
|
|
846
|
+
fullPage: false,
|
|
847
|
+
requestId,
|
|
848
|
+
domSettleTimeoutMs: options?.domSettleTimeoutMs,
|
|
849
|
+
})
|
|
850
|
+
.catch((e) => {
|
|
851
|
+
this.logger({
|
|
852
|
+
category: "observe",
|
|
853
|
+
message: "error observing",
|
|
854
|
+
level: 1,
|
|
855
|
+
auxiliary: {
|
|
856
|
+
error: {
|
|
857
|
+
value: e.message,
|
|
858
|
+
type: "string",
|
|
859
|
+
},
|
|
860
|
+
trace: {
|
|
861
|
+
value: e.stack,
|
|
862
|
+
type: "string",
|
|
863
|
+
},
|
|
864
|
+
requestId: {
|
|
865
|
+
value: requestId,
|
|
866
|
+
type: "string",
|
|
867
|
+
},
|
|
868
|
+
instruction: {
|
|
869
|
+
value: options?.instruction,
|
|
870
|
+
type: "string",
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
if (this.enableCaching) {
|
|
876
|
+
this.llmProvider.cleanRequestCache(requestId);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
throw e;
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async close(): Promise<void> {
|
|
884
|
+
await this.context.close();
|
|
885
|
+
|
|
886
|
+
if (this.contextPath) {
|
|
887
|
+
try {
|
|
888
|
+
fs.rmSync(this.contextPath, { recursive: true, force: true });
|
|
889
|
+
} catch (e) {
|
|
890
|
+
console.error("Error deleting context directory:", e);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export * from "../types/browser";
|
|
897
|
+
export * from "../types/log";
|
|
898
|
+
export * from "../types/model";
|
|
899
|
+
export * from "../types/playwright";
|
|
900
|
+
export * from "../types/stagehand";
|