Skip to content

Commit 94fe063

Browse files
authored
feat(browse-gpt): upgrade BrowseGPT to AI SDK 6 (#63)
- Bump ai and @ai-sdk packages; tool inputSchema; convertToModelMessages - Client: DefaultChatTransport, sendMessage, parts-based message UI Made-with: Cursor
1 parent 366e19f commit 94fe063

5 files changed

Lines changed: 211 additions & 981 deletions

File tree

examples/integrations/vercel/BrowseGPT/app/api/chat/route.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { openai } from '@ai-sdk/openai';
2-
import { streamText, convertToCoreMessages, tool, generateText } from 'ai';
2+
import { streamText, convertToModelMessages, tool, generateText } from 'ai';
33
import { z } from 'zod';
44
import { chromium } from 'playwright';
55
import {anthropic} from '@ai-sdk/anthropic'
@@ -45,16 +45,13 @@ export const maxDuration = 300; // Set max duration to 300 seconds (5 minutes)
4545
export async function POST(req: Request) {
4646
const { messages } = await req.json();
4747

48-
const result = await streamText({
49-
experimental_toolCallStreaming: true,
50-
model: openai('gpt-4-turbo'),
51-
// model: openai('gpt-4o'),
52-
// model: anthropic('claude-3-5-sonnet-20240620'),
53-
messages: convertToCoreMessages(messages),
48+
const result = streamText({
49+
model: openai('gpt-4.1'),
50+
messages: await convertToModelMessages(messages),
5451
tools: {
5552
createSession: tool({
5653
description: 'Create a new session',
57-
parameters: z.object({}),
54+
inputSchema: z.object({}),
5855
execute: async () => {
5956
const session = await createSession();
6057
const debugUrl = await getDebugUrl(session.id);
@@ -63,13 +60,13 @@ export async function POST(req: Request) {
6360
}),
6461
askForConfirmation: tool({
6562
description: 'Ask the user for confirmation.',
66-
parameters: z.object({
63+
inputSchema: z.object({
6764
message: z.string().describe('The message to ask for confirmation.'),
6865
}),
6966
}),
7067
googleSearch: tool({
7168
description: 'Search Google for a query',
72-
parameters: z.object({
69+
inputSchema: z.object({
7370
toolName: z.string().describe('What the tool is doing'),
7471
query: z.string().describe('The exact and complete search query as provided by the user. Do not modify this in any way.'),
7572
sessionId: z.string().describe('The session ID to use for the search. If there is no session ID, create a new session with createSession Tool.'),
@@ -104,7 +101,7 @@ export async function POST(req: Request) {
104101

105102
const response = await generateText({
106103
// model: openai('gpt-4-turbo'),
107-
model: anthropic('claude-3-5-sonnet-20240620'),
104+
model: anthropic('claude-sonnet-4-6'),
108105
prompt: `Evaluate the following web page content: ${text}`,
109106
});
110107

@@ -125,7 +122,7 @@ export async function POST(req: Request) {
125122
}),
126123
getPageContent: tool({
127124
description: 'Get the content of a page using Playwright',
128-
parameters: z.object({
125+
inputSchema: z.object({
129126
toolName: z.string().describe('What the tool is doing'),
130127
url: z.string().describe('The url to get the content of'),
131128
sessionId: z.string().describe('The session ID to use for the search. If there is no session ID, create a new session with createSession Tool.'),
@@ -151,7 +148,7 @@ export async function POST(req: Request) {
151148

152149
const response = await generateText({
153150
// model: openai('gpt-4-turbo'),
154-
model: anthropic('claude-3-5-sonnet-20240620'),
151+
model: anthropic('claude-sonnet-4-6'),
155152
prompt: `Evaluate the following web page content: ${text}`,
156153
});
157154

@@ -171,5 +168,5 @@ export async function POST(req: Request) {
171168
},
172169
});
173170

174-
return result.toDataStreamResponse();
171+
return result.toUIMessageStreamResponse();
175172
}
Lines changed: 101 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,112 @@
11
'use client';
22

3-
import { useChat } from 'ai/react';
3+
import { useChat } from '@ai-sdk/react';
4+
import { DefaultChatTransport, type UIMessage } from 'ai';
45
import { useState, useEffect } from 'react';
56
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
67
import Markdown from 'react-markdown';
78
import { MarkdownWrapper } from '@/components/ui/markdown';
89
import remarkGfm from 'remark-gfm';
910
import BlurFade from "@/components/ui/blur-fade";
10-
// import Spinner from "@/components/spinner";
1111
import VercelLogo from "@/components/vercel";
1212
import BrowserbaseLogo from "@/components/browserbase"
1313
import FlickeringGrid from '@/components/ui/flickering-grid';
1414
import FlickeringLoad from '@/components/ui/flickering-load';
1515
import { Prompts } from '@/components/prompts';
1616

17+
function messageText(m: UIMessage): string {
18+
return m.parts
19+
.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
20+
.map((p) => p.text)
21+
.join('');
22+
}
23+
24+
function isToolUIPart(p: UIMessage['parts'][number]): boolean {
25+
return typeof p.type === 'string' && p.type.startsWith('tool-');
26+
}
27+
28+
type ToolPartLoose = {
29+
type: string;
30+
state?: string;
31+
input?: Record<string, unknown>;
32+
output?: Record<string, unknown>;
33+
};
34+
1735
export default function Chat() {
18-
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
19-
maxSteps: 5,
36+
const [input, setInput] = useState('');
37+
const { messages, sendMessage, status } = useChat({
38+
transport: new DefaultChatTransport({ api: '/api/chat' }),
2039
});
2140

41+
const isLoading = status === 'streaming' || status === 'submitted';
42+
2243
const [showAlert, setShowAlert] = useState(false);
2344
const [statusMessage, setStatusMessage] = useState('');
2445
const [sessionId, setSessionId] = useState<string | null>(null);
2546
const [hasInteracted, setHasInteracted] = useState(false);
2647

48+
const lastMessage = messages[messages.length - 1];
49+
const lastText = lastMessage ? messageText(lastMessage) : '';
50+
2751
const isGenerating =
2852
isLoading &&
2953
(!messages.length ||
30-
messages[messages.length - 1].role !== 'assistant' ||
31-
!messages[messages.length - 1].content);
54+
lastMessage?.role !== 'assistant' ||
55+
!lastText);
3256

3357
useEffect(() => {
34-
const lastMessage = messages[messages.length - 1];
35-
3658
if (isGenerating) {
3759
setShowAlert(true);
3860

39-
// Check if any tool invocation has dataCollected = true
40-
const dataCollected = lastMessage?.toolInvocations?.some(
41-
invocation => 'result' in invocation &&
42-
typeof invocation.result === 'object' &&
43-
invocation.result !== null &&
44-
'dataCollected' in invocation.result &&
45-
invocation.result.dataCollected === true
46-
);
47-
48-
if (dataCollected && !lastMessage.content) {
49-
// The AI has collected data and is generating a response
61+
const dataCollected = lastMessage?.parts.some((part) => {
62+
if (!isToolUIPart(part)) return false;
63+
const tp = part as ToolPartLoose;
64+
const out = tp.state === 'output-available' ? tp.output : undefined;
65+
return (
66+
out &&
67+
typeof out === 'object' &&
68+
'dataCollected' in out &&
69+
(out as { dataCollected?: boolean }).dataCollected === true
70+
);
71+
});
72+
73+
if (dataCollected && !lastText) {
5074
setStatusMessage('The AI has collected data and is generating a response. Please wait.');
5175
} else {
52-
// The AI is currently processing the request
5376
setStatusMessage('The AI is currently processing your request. Please wait.');
5477
}
5578

5679
setSessionId(null);
5780
} else {
5881
setShowAlert(false);
5982
}
60-
}, [isGenerating, messages]);
83+
}, [isGenerating, messages, lastMessage, lastText]);
6184

6285
useEffect(() => {
63-
const lastMessage = messages[messages.length - 1];
64-
if (lastMessage?.toolInvocations) {
65-
for (const invocation of lastMessage.toolInvocations) {
66-
if ('result' in invocation && invocation.result?.sessionId) {
67-
setSessionId(invocation.result.sessionId);
68-
break;
69-
}
86+
if (!lastMessage?.parts) return;
87+
for (const part of lastMessage.parts) {
88+
if (!isToolUIPart(part)) continue;
89+
const tp = part as ToolPartLoose;
90+
if (tp.state !== 'output-available') continue;
91+
const out = tp.output as { sessionId?: string } | undefined;
92+
if (out?.sessionId) {
93+
setSessionId(out.sessionId);
94+
break;
7095
}
7196
}
72-
}, [messages]);
97+
}, [messages, lastMessage]);
7398

7499
const handleSubmitWrapper = (e: React.FormEvent<HTMLFormElement>) => {
75100
e.preventDefault();
76-
setHasInteracted(true);
77-
handleSubmit(e, { data: { message: input } });
101+
if (!input.trim()) return;
102+
setHasInteracted(true);
103+
void sendMessage({ text: input });
104+
setInput('');
78105
};
79106

80107
const handlePromptClick = (text: string) => {
81108
setHasInteracted(true);
82-
// Set the input value
83-
handleInputChange({ target: { value: text } } as React.ChangeEvent<HTMLInputElement>);
84-
// Submit the form after a short delay
85-
setTimeout(() => {
86-
const submitButton = document.querySelector('button[type="submit"]') as HTMLButtonElement;
87-
if (submitButton) {
88-
submitButton.click();
89-
}
90-
}, 100); // 100ms delay, adjust as needed
109+
void sendMessage({ text });
91110
};
92111

93112
return (
@@ -125,31 +144,42 @@ export default function Chat() {
125144
<Prompts onPromptClick={handlePromptClick} />
126145
</div>
127146
) : (
128-
messages.map((m, index) => (
147+
messages.map((m, index) => {
148+
const text = messageText(m);
149+
const toolParts = m.parts.filter(isToolUIPart);
150+
151+
return (
129152
<div key={m.id} className="whitespace-pre-wrap">
130153
{m.role === 'user' ? (
131154
<>
132155
<strong className="block mb-0 text-xl pb-2">User:</strong>
133-
<p className="mt-0 pb-4 font-mono">{m.content}</p>
156+
<p className="mt-0 pb-4 font-mono">{text}</p>
134157
</>
135-
) : m.toolInvocations ? (
158+
) : toolParts.length > 0 ? (
136159
<BlurFade>
137160
<Alert className="my-4 border-[#E5E7EB]">
138161
<AlertDescription>
139-
{m.toolInvocations?.map((invocation, index) => {
162+
{toolParts.map((part, partIndex) => {
163+
const tp = part as ToolPartLoose;
164+
const input = tp.input;
165+
const out =
166+
tp.state === 'output-available'
167+
? tp.output
168+
: undefined;
169+
140170
let content = '';
141-
if ('result' in invocation) {
142-
if (invocation.result?.sessionId) {
143-
content = `Session ID: ${invocation.result.sessionId}`;
144-
} else if (invocation.result?.content) {
145-
content = `Content: ${invocation.result.content}`;
146-
}
171+
if (out?.sessionId) {
172+
content = `Session ID: ${String(out.sessionId)}`;
173+
} else if (out?.content) {
174+
content = `Content: ${String(out.content)}`;
147175
}
148-
if (invocation.args?.debuggerFullscreenUrl) {
176+
177+
const debuggerUrl = input?.debuggerFullscreenUrl;
178+
if (typeof debuggerUrl === 'string') {
149179
return (
150-
<div key={index}>
180+
<div key={partIndex}>
151181
<iframe
152-
src={`${invocation.args.debuggerFullscreenUrl}&navBar=false`}
182+
src={`${debuggerUrl}&navBar=false`}
153183
className="w-full sm:h-72 h-52"
154184
title="Debugger"
155185
sandbox="allow-same-origin allow-scripts"
@@ -159,7 +189,7 @@ export default function Chat() {
159189
);
160190
}
161191
return content ? (
162-
<div key={index} className="overflow-x-auto">
192+
<div key={partIndex} className="overflow-x-auto">
163193
<pre className="whitespace-pre-wrap break-all">{content}</pre>
164194
</div>
165195
) : null;
@@ -182,36 +212,40 @@ export default function Chat() {
182212
}`}
183213
>
184214
<MarkdownWrapper>
185-
<Markdown remarkPlugins={[remarkGfm]}>{m.content}</Markdown>
215+
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
186216
</MarkdownWrapper>
187217
</div>
188218
</>
189219
)}
190220
</div>
191-
))
221+
);
222+
})
192223
)}
193224

194-
{showAlert && !sessionId && (
225+
{showAlert && !sessionId && lastMessage?.role === 'assistant' && (
195226
<BlurFade>
196227
<Alert className="my-4 border-[#E5E7EB] mb-20">
197228
<div className="flex justify-between items-center">
198229
<div>
199230
<AlertTitle>
200-
{messages[messages.length - 1].toolInvocations
201-
?.map((invocation) => {
202-
if ('result' in invocation) {
203-
return invocation.result?.toolName;
204-
}
205-
return invocation.args?.toolName;
231+
{lastMessage.parts
232+
.filter(isToolUIPart)
233+
.map((part) => {
234+
const tp = part as ToolPartLoose;
235+
const out =
236+
tp.state === 'output-available'
237+
? (tp.output as { toolName?: string } | undefined)
238+
: undefined;
239+
if (out?.toolName) return out.toolName;
240+
const input = tp.input as { toolName?: string } | undefined;
241+
return input?.toolName;
206242
})
207243
.filter(Boolean)
208244
.join(', ')}
209245
</AlertTitle>
210246
<AlertDescription>{statusMessage}</AlertDescription>
211247
</div>
212-
{/* <div role="status" className="w-[70px] h-[40px]"> */}
213248
<FlickeringLoad height={50} width={60} className='p-1'/>
214-
{/* </div> */}
215249
</div>
216250
</Alert>
217251
</BlurFade>
@@ -228,7 +262,7 @@ export default function Chat() {
228262
className="w-full p-2 pr-10 border border-[#E5E7EB] transition-all duration-200 ease-in-out shadow-md shadow-gray-300/50 focus:border-red-300 focus:shadow-lg focus:shadow-red-300/40 outline-none"
229263
value={input}
230264
placeholder="Ask anything..."
231-
onChange={handleInputChange}
265+
onChange={(e) => setInput(e.target.value)}
232266
/>
233267
<button
234268
type="submit"
@@ -244,4 +278,4 @@ export default function Chat() {
244278
</div>
245279
</div>
246280
);
247-
}
281+
}

0 commit comments

Comments
 (0)