Skip to content

Commit 029c84f

Browse files
authored
feat: Add LangChain Deep Agents Browserbase example (#70)
* Add LangChain Deep Agents Browserbase example * working version of langchain deepagent with browserbase tools * fix(langchain): use Browserbase SDK for search and fetch * fix: update code to work with the lastest version of stagehand v3 * fix langchain tool package
1 parent 000d8a5 commit 029c84f

5 files changed

Lines changed: 559 additions & 1 deletion

File tree

.gitignore

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,15 @@ playwright-report/
130130

131131
# venv
132132
venv/
133-
.venv/
133+
.venv/
134+
135+
# Python
136+
__pycache__/
137+
*.py[cod]
138+
*$py.class
139+
*.so
140+
.Python
141+
*.egg-info/
142+
.pytest_cache/
143+
.mypy_cache/
144+
.ruff_cache/
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# LangChain Deep Agents + Browserbase (Python)
2+
3+
This example shows the implementation pattern that fits LangChain Deep Agents best in Python:
4+
5+
- Give the main Deep Agent cheap Browserbase-backed tools for `search` and `fetch`
6+
- Add a specialized browser subagent for heavier rendered or interactive browser work
7+
- Gate stateful browser actions behind Deep Agents `interrupt_on`
8+
9+
It intentionally does **not** route the agent through the Browserbase CLI. Deep Agents already wants Python tools, subagents, and interrupt handling, so the clean integration is to expose Browserbase as Python tools directly.
10+
11+
## Architecture
12+
13+
- `browserbase_search`: fast discovery with Browserbase Search
14+
- `browserbase_fetch`: cheap page retrieval with Browserbase Fetch
15+
- `browserbase_rendered_extract`: Stagehand-backed rendered extraction for JS-heavy pages
16+
- `browserbase_interactive_task`: a Stagehand `agent().execute(...)` workflow for clicks, typing, login, or form submission
17+
- `browser-specialist` subagent: isolates browser-heavy work from the main planner
18+
19+
## Requirements
20+
21+
- Python 3.11+
22+
- `BROWSERBASE_API_KEY` for Browserbase Search, Fetch, and browser sessions
23+
- An OpenAI-compatible base URL for the Deep Agent model if you are not using direct OpenAI
24+
25+
The sample uses `BROWSERBASE_API_KEY` as the fallback API key for both:
26+
27+
- Browserbase primitives and Stagehand
28+
- the LangChain chat model client
29+
30+
That means you do not need a second model-provider secret in this sample if you point the Deep Agent model at a compatible gateway endpoint.
31+
32+
The sample defaults to:
33+
34+
- Deep Agent model: `gpt-5.4`
35+
- Stagehand rendered-extract model: `google/gemini-3-flash-preview`
36+
- Stagehand interactive-agent model: `anthropic/claude-sonnet-4-6`
37+
38+
You can override either with environment variables.
39+
40+
## Install
41+
42+
```bash
43+
cd /Users/kylejeong/Desktop/integrations/examples/integrations/langchain/deepagents-browserbase
44+
python3 -m venv .venv
45+
source .venv/bin/activate
46+
pip install -r requirements.txt
47+
```
48+
49+
## Environment
50+
51+
```bash
52+
export BROWSERBASE_API_KEY="bb_..."
53+
54+
# Optional overrides
55+
export DEEPAGENT_MODEL="gpt-5.4"
56+
export DEEPAGENT_BASE_URL="https://<your-openai-compatible-gateway>"
57+
export STAGEHAND_MODEL="google/gemini-3-flash-preview"
58+
export STAGEHAND_AGENT_MODEL="anthropic/claude-sonnet-4-6"
59+
```
60+
61+
## Run
62+
63+
Use the default research prompt:
64+
65+
```bash
66+
python main.py
67+
```
68+
69+
Or pass your own:
70+
71+
```bash
72+
python main.py "Research the Browserbase Fetch API and explain when the agent should escalate to a full browser session."
73+
```
74+
75+
## Approval flow
76+
77+
The sample configures `interrupt_on` for `browserbase_interactive_task`.
78+
79+
When the agent wants to click, type, log in, or submit a form, the script pauses and asks you to:
80+
81+
- `approve`
82+
- `edit`
83+
- `reject`
84+
85+
This is the right place to put human approval in a Deep Agents + Browserbase design, because the approval happens at the tool boundary instead of being hidden inside ad hoc shell calls.
86+
87+
## Notes
88+
89+
- The interactive tool now uses `stagehand.agent().execute(...)` instead of a single `sessions.act(...)` call. That makes it better suited to genuine multi-step browser tasks.
90+
- Browserbase’s Stagehand quickstart documents that Model Gateway works with just `BROWSERBASE_API_KEY` for Stagehand browser workflows.
91+
- I did not hardcode a Browserbase model-gateway URL for the LangChain model client because I did not find an official doc page in the Browserbase docs that specifies a general-purpose OpenAI-compatible endpoint for LangChain. The sample therefore accepts `DEEPAGENT_BASE_URL` or `OPENAI_BASE_URL` explicitly.
92+
93+
## Suggested prompts
94+
95+
- `Research Browserbase Search, Fetch, and browser sessions. Give me a decision tree with citations.`
96+
- `Open docs.browserbase.com and extract the limits of the Fetch API from the rendered docs page.`
97+
- `Go to example.com and tell me whether any interactive action would be required to complete the task.`
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import json
5+
import os
6+
import re
7+
from typing import Any
8+
9+
from browserbase import Browserbase
10+
from bs4 import BeautifulSoup
11+
from langchain.tools import tool
12+
from stagehand import AsyncStagehand
13+
14+
# Using the Browserbase Model Gateway, you only need to pass your Browserbase API key to use frontier models
15+
# Docs: https://docs.browserbase.com/platform/model-gateway/overview
16+
17+
DEFAULT_STAGEHAND_MODEL = os.getenv(
18+
"STAGEHAND_MODEL",
19+
"google/gemini-3-flash-preview",
20+
)
21+
DEFAULT_STAGEHAND_AGENT_MODEL = os.getenv(
22+
"STAGEHAND_AGENT_MODEL",
23+
"anthropic/claude-sonnet-4-6",
24+
)
25+
26+
27+
def _require_env(name: str) -> str:
28+
value = os.getenv(name, "").strip()
29+
if not value:
30+
raise ValueError(f"Missing required environment variable: {name}")
31+
return value
32+
33+
34+
def _browserbase_client() -> Browserbase:
35+
return Browserbase(api_key=_require_env("BROWSERBASE_API_KEY"))
36+
37+
38+
def _normalize(value: Any) -> Any:
39+
if value is None or isinstance(value, (str, int, float, bool)):
40+
return value
41+
if isinstance(value, dict):
42+
return {str(key): _normalize(val) for key, val in value.items()}
43+
if isinstance(value, (list, tuple, set)):
44+
return [_normalize(item) for item in value]
45+
if hasattr(value, "model_dump"):
46+
return _normalize(value.model_dump())
47+
if hasattr(value, "dict"):
48+
return _normalize(value.dict())
49+
if hasattr(value, "__dict__"):
50+
public = {
51+
key: val
52+
for key, val in vars(value).items()
53+
if not key.startswith("_") and not callable(val)
54+
}
55+
if public:
56+
return _normalize(public)
57+
return str(value)
58+
59+
60+
def _json(value: Any) -> str:
61+
return json.dumps(_normalize(value), indent=2, default=str)
62+
63+
64+
def _html_to_text(html: str, max_chars: int) -> tuple[str, str]:
65+
soup = BeautifulSoup(html, "html.parser")
66+
title = soup.title.get_text(" ", strip=True) if soup.title else ""
67+
for tag in soup(["script", "style", "noscript"]):
68+
tag.decompose()
69+
body = soup.body or soup
70+
text = body.get_text("\n", strip=True)
71+
text = re.sub(r"\n{3,}", "\n\n", text)
72+
return title, text[:max_chars]
73+
74+
75+
def _stagehand_client() -> AsyncStagehand:
76+
return AsyncStagehand(
77+
browserbase_api_key=_require_env("BROWSERBASE_API_KEY"),
78+
)
79+
80+
81+
def _run_async(coro: Any) -> Any:
82+
return asyncio.run(coro)
83+
84+
85+
@tool
86+
def browserbase_search(query: str, num_results: int = 5) -> str:
87+
"""Search the web with Browserbase. Use this first for discovery before opening pages."""
88+
bb = _browserbase_client()
89+
response = bb.search.web(query=query, num_results=max(1, min(num_results, 10)))
90+
results = []
91+
for result in getattr(response, "results", []):
92+
results.append(
93+
{
94+
"title": getattr(result, "title", ""),
95+
"url": getattr(result, "url", ""),
96+
"author": getattr(result, "author", None),
97+
"published_date": (
98+
getattr(result, "published_date", None)
99+
or getattr(result, "publishedDate", None)
100+
),
101+
}
102+
)
103+
return _json(
104+
{
105+
"query": query,
106+
"request_id": getattr(response, "request_id", None)
107+
or getattr(response, "requestId", None),
108+
"results": results,
109+
}
110+
)
111+
112+
113+
@tool
114+
def browserbase_fetch(url: str, use_proxy: bool = False, max_chars: int = 12000) -> str:
115+
"""Fetch page content without a browser session. Best for static pages and quick reads."""
116+
bb = _browserbase_client()
117+
response = bb.fetch_api.create(url=url, proxies=use_proxy)
118+
content = getattr(response, "content", "")
119+
content_type = (
120+
getattr(response, "content_type", None)
121+
or getattr(response, "contentType", "")
122+
or ""
123+
).lower()
124+
125+
title = ""
126+
text = str(content)[:max_chars]
127+
if "html" in content_type:
128+
title, text = _html_to_text(str(content), max_chars=max_chars)
129+
130+
return _json(
131+
{
132+
"url": url,
133+
"status_code": getattr(response, "status_code", None)
134+
or getattr(response, "statusCode", None),
135+
"content_type": getattr(response, "content_type", None)
136+
or getattr(response, "contentType", None),
137+
"encoding": getattr(response, "encoding", None),
138+
"title": title,
139+
"text": text,
140+
}
141+
)
142+
143+
144+
@tool
145+
def browserbase_rendered_extract(start_url: str, instruction: str) -> str:
146+
"""Open a full Browserbase browser session and extract rendered content from a page with Stagehand."""
147+
return _run_async(_browserbase_rendered_extract_async(start_url=start_url, instruction=instruction))
148+
149+
150+
async def _browserbase_rendered_extract_async(start_url: str, instruction: str) -> str:
151+
client = _stagehand_client()
152+
start_resp = await client.sessions.start(
153+
model_name=DEFAULT_STAGEHAND_MODEL,
154+
)
155+
session_id = start_resp.data.session_id
156+
157+
try:
158+
await client.sessions.navigate(
159+
id=session_id,
160+
url=start_url,
161+
frame_id="",
162+
)
163+
result = await client.sessions.extract(
164+
id=session_id,
165+
instruction=instruction,
166+
)
167+
extracted = getattr(getattr(result, "data", None), "result", None)
168+
return _json(
169+
{
170+
"start_url": start_url,
171+
"session_id": session_id,
172+
"session_url": f"https://browserbase.com/sessions/{session_id}",
173+
"instruction": instruction,
174+
"result": _normalize(extracted),
175+
}
176+
)
177+
finally:
178+
await client.sessions.end(id=session_id)
179+
180+
181+
@tool
182+
def browserbase_interactive_task(start_url: str, task: str) -> str:
183+
"""Open a Browserbase-hosted Stagehand session and let a Stagehand agent execute a multi-step browser task."""
184+
return _run_async(_browserbase_interactive_task_async(start_url=start_url, task=task))
185+
186+
187+
async def _browserbase_interactive_task_async(start_url: str, task: str) -> str:
188+
client = _stagehand_client()
189+
start_resp = await client.sessions.start(
190+
model_name=DEFAULT_STAGEHAND_AGENT_MODEL,
191+
)
192+
session_id = start_resp.data.session_id
193+
194+
try:
195+
await client.sessions.navigate(
196+
id=session_id,
197+
url=start_url,
198+
frame_id="",
199+
)
200+
result = await client.sessions.execute(
201+
id=session_id,
202+
execute_options={
203+
"instruction": task,
204+
"max_steps": 20,
205+
},
206+
agent_config={
207+
"model": DEFAULT_STAGEHAND_AGENT_MODEL,
208+
"instructions": (
209+
"You are executing a browser task on behalf of a LangChain tool. "
210+
"Be precise, avoid unnecessary actions, and stop once the requested task is complete."
211+
),
212+
},
213+
timeout=300.0,
214+
)
215+
return _json(_normalize(result))
216+
finally:
217+
await client.sessions.end(id=session_id)

0 commit comments

Comments
 (0)