Skip to content

Commit a41271b

Browse files
authored
Vercel ai sdk impl (#382)
* ai sdk client (WIP) * unify LLM tool type * replace anthropic types * update ollama tool usage * delete old ai sdk client * new ai sdk client * ai sdk example * add comment * changeset * fix ollama tool usage * remove changeset * use default logger * fixed messages type * message type fix * update deps * update type * migrate to new options syntax * fix * input logger in extract/observe * remove AISdkClient logger * changeset * aisdk use StagehandConfig * change aisdk model to gemini * Revert "Merge branch 'sameel/move-llm-logger' into vercel-ai-sdk-impl" This reverts commit ec63bf4, reversing changes made to e575d88. * lint error * changeset
1 parent 5899ec2 commit a41271b

File tree

6 files changed

+743
-119
lines changed

6 files changed

+743
-119
lines changed

Diff for: .changeset/shiny-ladybugs-shave.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
Added example implementation of the Vercel AI SDK as an LLMClient

Diff for: examples/ai_sdk_example.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { google } from "@ai-sdk/google";
2+
import { z } from "zod";
3+
import { Stagehand } from "../lib";
4+
import { AISdkClient } from "./external_clients/aisdk";
5+
import StagehandConfig from "./stagehand.config";
6+
7+
async function example() {
8+
const stagehand = new Stagehand({
9+
...StagehandConfig,
10+
llmClient: new AISdkClient({
11+
model: google("gemini-1.5-flash-latest"),
12+
}),
13+
});
14+
15+
await stagehand.init();
16+
await stagehand.page.goto("https://news.ycombinator.com");
17+
18+
const headlines = await stagehand.page.extract({
19+
instruction: "Extract only 3 stories from the Hacker News homepage.",
20+
schema: z.object({
21+
stories: z
22+
.array(
23+
z.object({
24+
title: z.string(),
25+
url: z.string(),
26+
points: z.number(),
27+
}),
28+
)
29+
.length(3),
30+
}),
31+
});
32+
33+
console.log(headlines);
34+
35+
await stagehand.close();
36+
}
37+
38+
(async () => {
39+
await example();
40+
})();

Diff for: examples/external_clients/aisdk.ts

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
CoreAssistantMessage,
3+
CoreMessage,
4+
CoreSystemMessage,
5+
CoreTool,
6+
CoreUserMessage,
7+
generateObject,
8+
generateText,
9+
ImagePart,
10+
LanguageModel,
11+
TextPart,
12+
} from "ai";
13+
import { ChatCompletion } from "openai/resources/chat/completions";
14+
import {
15+
CreateChatCompletionOptions,
16+
LLMClient,
17+
} from "../../lib/llm/LLMClient";
18+
import { AvailableModel } from "../../types/model";
19+
20+
export class AISdkClient extends LLMClient {
21+
public type = "aisdk" as const;
22+
private model: LanguageModel;
23+
24+
constructor({ model }: { model: LanguageModel }) {
25+
super(model.modelId as AvailableModel);
26+
this.model = model;
27+
}
28+
29+
async createChatCompletion<T = ChatCompletion>({
30+
options,
31+
}: CreateChatCompletionOptions): Promise<T> {
32+
const formattedMessages: CoreMessage[] = options.messages.map((message) => {
33+
if (Array.isArray(message.content)) {
34+
if (message.role === "system") {
35+
const systemMessage: CoreSystemMessage = {
36+
role: "system",
37+
content: message.content
38+
.map((c) => ("text" in c ? c.text : ""))
39+
.join("\n"),
40+
};
41+
return systemMessage;
42+
}
43+
44+
const contentParts = message.content.map((content) => {
45+
if ("image_url" in content) {
46+
const imageContent: ImagePart = {
47+
type: "image",
48+
image: content.image_url.url,
49+
};
50+
return imageContent;
51+
} else {
52+
const textContent: TextPart = {
53+
type: "text",
54+
text: content.text,
55+
};
56+
return textContent;
57+
}
58+
});
59+
60+
if (message.role === "user") {
61+
const userMessage: CoreUserMessage = {
62+
role: "user",
63+
content: contentParts,
64+
};
65+
return userMessage;
66+
} else {
67+
const textOnlyParts = contentParts.map((part) => ({
68+
type: "text" as const,
69+
text: part.type === "image" ? "[Image]" : part.text,
70+
}));
71+
const assistantMessage: CoreAssistantMessage = {
72+
role: "assistant",
73+
content: textOnlyParts,
74+
};
75+
return assistantMessage;
76+
}
77+
}
78+
79+
return {
80+
role: message.role,
81+
content: message.content,
82+
};
83+
});
84+
85+
if (options.response_model) {
86+
const response = await generateObject({
87+
model: this.model,
88+
messages: formattedMessages,
89+
schema: options.response_model.schema,
90+
});
91+
92+
return response.object;
93+
}
94+
95+
const tools: Record<string, CoreTool> = {};
96+
97+
for (const rawTool of options.tools) {
98+
tools[rawTool.name] = {
99+
description: rawTool.description,
100+
parameters: rawTool.parameters,
101+
};
102+
}
103+
104+
const response = await generateText({
105+
model: this.model,
106+
messages: formattedMessages,
107+
tools,
108+
});
109+
110+
return response as T;
111+
}
112+
}

Diff for: lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ export class Stagehand {
372372
this.llmClient = undefined;
373373
}
374374
}
375+
375376
this.domSettleTimeoutMs = domSettleTimeoutMs ?? 30_000;
376377
this.headless = headless ?? false;
377378
this.browserbaseSessionCreateParams = browserbaseSessionCreateParams;

0 commit comments

Comments
 (0)