From 516c8612dfa652ff683a1e1da361af0451f8d466 Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Sun, 19 Apr 2026 13:45:40 -0700 Subject: [PATCH 1/4] Add LangChain Deep Agents Browserbase example --- .../deepagents-browserbase/README.md | 97 ++++++++ .../deepagents-browserbase/browser_tools.py | 208 ++++++++++++++++ .../langchain/deepagents-browserbase/main.py | 227 ++++++++++++++++++ .../deepagents-browserbase/requirements.txt | 6 + 4 files changed, 538 insertions(+) create mode 100644 examples/integrations/langchain/deepagents-browserbase/README.md create mode 100644 examples/integrations/langchain/deepagents-browserbase/browser_tools.py create mode 100644 examples/integrations/langchain/deepagents-browserbase/main.py create mode 100644 examples/integrations/langchain/deepagents-browserbase/requirements.txt diff --git a/examples/integrations/langchain/deepagents-browserbase/README.md b/examples/integrations/langchain/deepagents-browserbase/README.md new file mode 100644 index 0000000..6f1eb0b --- /dev/null +++ b/examples/integrations/langchain/deepagents-browserbase/README.md @@ -0,0 +1,97 @@ +# LangChain Deep Agents + Browserbase (Python) + +This example shows the implementation pattern that fits LangChain Deep Agents best in Python: + +- Give the main Deep Agent cheap Browserbase-backed tools for `search` and `fetch` +- Add a specialized browser subagent for heavier rendered or interactive browser work +- Gate stateful browser actions behind Deep Agents `interrupt_on` + +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. + +## Architecture + +- `browserbase_search`: fast discovery with Browserbase Search +- `browserbase_fetch`: cheap page retrieval with Browserbase Fetch +- `browserbase_rendered_extract`: Stagehand-backed rendered extraction for JS-heavy pages +- `browserbase_interactive_task`: a Stagehand `agent().execute(...)` workflow for clicks, typing, login, or form submission +- `browser-specialist` subagent: isolates browser-heavy work from the main planner + +## Requirements + +- Python 3.11+ +- `BROWSERBASE_API_KEY` for Browserbase Search, Fetch, and browser sessions +- An OpenAI-compatible base URL for the Deep Agent model if you are not using direct OpenAI + +The sample uses `BROWSERBASE_API_KEY` as the fallback API key for both: + +- Browserbase primitives and Stagehand +- the LangChain chat model client + +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. + +The sample defaults to: + +- Deep Agent model: `gpt-5.4` +- Stagehand rendered-extract model: `google/gemini-3-flash-preview` +- Stagehand interactive-agent model: `anthropic/claude-sonnet-4-6` + +You can override either with environment variables. + +## Install + +```bash +cd /Users/kylejeong/Desktop/integrations/examples/integrations/langchain/deepagents-browserbase +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +## Environment + +```bash +export BROWSERBASE_API_KEY="bb_..." + +# Optional overrides +export DEEPAGENT_MODEL="gpt-5.4" +export DEEPAGENT_BASE_URL="https://" +export STAGEHAND_MODEL="google/gemini-3-flash-preview" +export STAGEHAND_AGENT_MODEL="anthropic/claude-sonnet-4-6" +``` + +## Run + +Use the default research prompt: + +```bash +python main.py +``` + +Or pass your own: + +```bash +python main.py "Research the Browserbase Fetch API and explain when the agent should escalate to a full browser session." +``` + +## Approval flow + +The sample configures `interrupt_on` for `browserbase_interactive_task`. + +When the agent wants to click, type, log in, or submit a form, the script pauses and asks you to: + +- `approve` +- `edit` +- `reject` + +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. + +## Notes + +- 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. +- Browserbase’s Stagehand quickstart documents that Model Gateway works with just `BROWSERBASE_API_KEY` for Stagehand browser workflows. +- 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. + +## Suggested prompts + +- `Research Browserbase Search, Fetch, and browser sessions. Give me a decision tree with citations.` +- `Open docs.browserbase.com and extract the limits of the Fetch API from the rendered docs page.` +- `Go to example.com and tell me whether any interactive action would be required to complete the task.` diff --git a/examples/integrations/langchain/deepagents-browserbase/browser_tools.py b/examples/integrations/langchain/deepagents-browserbase/browser_tools.py new file mode 100644 index 0000000..0619aed --- /dev/null +++ b/examples/integrations/langchain/deepagents-browserbase/browser_tools.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +import asyncio +import json +import os +import re +from typing import Any + +from browserbase import Browserbase +from bs4 import BeautifulSoup +from langchain_core.tools import tool +from stagehand import Stagehand, StagehandConfig + + +DEFAULT_STAGEHAND_MODEL = os.getenv( + "STAGEHAND_MODEL", + "google/gemini-3-flash-preview", +) +DEFAULT_STAGEHAND_AGENT_MODEL = os.getenv( + "STAGEHAND_AGENT_MODEL", + "anthropic/claude-sonnet-4-6", +) + + +def _require_env(name: str) -> str: + value = os.getenv(name, "").strip() + if not value: + raise ValueError(f"Missing required environment variable: {name}") + return value + + +def _browserbase_client() -> Browserbase: + return Browserbase(api_key=_require_env("BROWSERBASE_API_KEY")) + + +def _normalize(value: Any) -> Any: + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, dict): + return {str(key): _normalize(val) for key, val in value.items()} + if isinstance(value, (list, tuple, set)): + return [_normalize(item) for item in value] + if hasattr(value, "model_dump"): + return _normalize(value.model_dump()) + if hasattr(value, "dict"): + return _normalize(value.dict()) + if hasattr(value, "__dict__"): + public = { + key: val + for key, val in vars(value).items() + if not key.startswith("_") and not callable(val) + } + if public: + return _normalize(public) + return str(value) + + +def _json(value: Any) -> str: + return json.dumps(_normalize(value), indent=2, default=str) + + +def _html_to_text(html: str, max_chars: int) -> tuple[str, str]: + soup = BeautifulSoup(html, "html.parser") + title = soup.title.get_text(" ", strip=True) if soup.title else "" + for tag in soup(["script", "style", "noscript"]): + tag.decompose() + body = soup.body or soup + text = body.get_text("\n", strip=True) + text = re.sub(r"\n{3,}", "\n\n", text) + return title, text[:max_chars] + + +def _stagehand_client() -> Stagehand: + _require_env("BROWSERBASE_API_KEY") + return Stagehand() + + +def _stagehand_config(model_name: str) -> StagehandConfig: + return StagehandConfig( + env="BROWSERBASE", + api_key=_require_env("BROWSERBASE_API_KEY"), + project_id=os.getenv("BROWSERBASE_PROJECT_ID"), + model_name=model_name, + ) + + +def _session_id(response: Any) -> str: + data = getattr(response, "data", None) + session_id = getattr(data, "session_id", None) or getattr(response, "session_id", None) + if not session_id: + raise RuntimeError(f"Could not extract session id from Stagehand response: {_json(response)}") + return session_id + + +def _extract_result_payload(result: Any) -> Any: + data = getattr(result, "data", None) + extracted = getattr(data, "result", None) + if extracted is not None: + return _normalize(extracted) + return _normalize(result) + + +def _close_stagehand(client: Any) -> None: + closer = getattr(client, "close", None) + if callable(closer): + closer() + + +def _run_async(coro: Any) -> Any: + return asyncio.run(coro) + + +@tool +def browserbase_search(query: str, num_results: int = 5) -> str: + """Search the web with Browserbase. Use this first for discovery before opening pages.""" + bb = _browserbase_client() + response = bb.search.web(query=query, num_results=max(1, min(num_results, 10))) + results = [] + for result in response.results: + results.append( + { + "title": getattr(result, "title", ""), + "url": getattr(result, "url", ""), + "author": getattr(result, "author", None), + "published_date": getattr(result, "published_date", None), + } + ) + return _json( + { + "query": query, + "request_id": getattr(response, "request_id", None), + "results": results, + } + ) + + +@tool +def browserbase_fetch(url: str, use_proxy: bool = False, max_chars: int = 12000) -> str: + """Fetch page content without a browser session. Best for static pages and quick reads.""" + bb = _browserbase_client() + response = bb.fetch_api.create(url=url, proxies=use_proxy) + content = getattr(response, "content", "") + content_type = (getattr(response, "content_type", "") or "").lower() + + title = "" + text = str(content)[:max_chars] + if "html" in content_type: + title, text = _html_to_text(str(content), max_chars=max_chars) + + return _json( + { + "url": url, + "status_code": getattr(response, "status_code", None), + "content_type": getattr(response, "content_type", None), + "encoding": getattr(response, "encoding", None), + "title": title, + "text": text, + } + ) + + +@tool +def browserbase_rendered_extract(start_url: str, instruction: str) -> str: + """Open a full Browserbase browser session and extract rendered content from a page with Stagehand.""" + client = _stagehand_client() + response = client.sessions.start(model_name=DEFAULT_STAGEHAND_MODEL) + session_id = _session_id(response) + + try: + client.sessions.navigate(id=session_id, url=start_url) + result = client.sessions.extract(id=session_id, instruction=instruction) + return _json( + { + "start_url": start_url, + "session_id": session_id, + "session_url": f"https://browserbase.com/sessions/{session_id}", + "instruction": instruction, + "result": _extract_result_payload(result), + } + ) + finally: + try: + client.sessions.end(id=session_id) + finally: + _close_stagehand(client) + + +@tool +def browserbase_interactive_task(start_url: str, task: str) -> str: + """Open a Browserbase-hosted Stagehand session and let a Stagehand agent execute a multi-step browser task.""" + return _run_async(_browserbase_interactive_task_async(start_url=start_url, task=task)) + + +async def _browserbase_interactive_task_async(start_url: str, task: str) -> str: + config = _stagehand_config(model_name=DEFAULT_STAGEHAND_AGENT_MODEL) + async with Stagehand(config) as stagehand: + page = stagehand.page + await page.goto(start_url) + + agent = stagehand.agent( + model=DEFAULT_STAGEHAND_AGENT_MODEL, + instructions=( + "You are executing a browser task on behalf of a LangChain tool. " + "Be precise, avoid unnecessary actions, and stop once the requested task is complete." + ), + ) + agent_result = await agent.execute(task) + return _json(_normalize(agent_result)) diff --git a/examples/integrations/langchain/deepagents-browserbase/main.py b/examples/integrations/langchain/deepagents-browserbase/main.py new file mode 100644 index 0000000..af4259f --- /dev/null +++ b/examples/integrations/langchain/deepagents-browserbase/main.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +import argparse +import json +import os +import uuid +from typing import Any + +from deepagents import create_deep_agent +from dotenv import load_dotenv +from langgraph.checkpoint.memory import MemorySaver +from langgraph.types import Command +from langchain_openai import ChatOpenAI + +from browser_tools import ( + browserbase_fetch, + browserbase_interactive_task, + browserbase_rendered_extract, + browserbase_search, +) + + +SYSTEM_PROMPT = """You are a research-oriented Deep Agent with Browserbase tools. + +Workflow rules: +- Start with browserbase_search for discovery unless the user already gave you a precise URL. +- Prefer browserbase_fetch for quick reads of static pages. +- Delegate JS-heavy, rendered, or multi-step browsing work to the browser-specialist subagent. +- Use browserbase_rendered_extract for read-only browser work on rendered pages. +- Use browserbase_interactive_task only when the task requires clicking, typing, login, or form submission. +- Keep answers concise and cite the exact URLs you used. +- Avoid interactive browser actions when fetch or rendered extraction is enough. +""" + + +BROWSER_SUBAGENT = { + "name": "browser-specialist", + "description": ( + "Handles JS-heavy browsing, rendered extraction, and interactive browser tasks through Browserbase." + ), + "system_prompt": """You are a Browserbase browsing specialist. + +Use browserbase_rendered_extract for read-only work on rendered pages. +Use browserbase_interactive_task only for stateful actions such as clicking, typing, logging in, or submitting forms. +Return concise summaries with the relevant page URL, what you observed, and whether the task succeeded. +""", + "tools": [browserbase_rendered_extract, browserbase_interactive_task], +} + + +def _normalize_chat_model_name(model: str) -> str: + if ":" in model: + provider, raw_model = model.split(":", 1) + if provider == "openai": + return raw_model + return model + + +def build_model(model: str) -> ChatOpenAI: + base_url = os.getenv("DEEPAGENT_BASE_URL") or os.getenv("OPENAI_BASE_URL") + openai_api_key = os.getenv("OPENAI_API_KEY") + browserbase_api_key = os.getenv("BROWSERBASE_API_KEY") + + if openai_api_key: + api_key = openai_api_key + elif browserbase_api_key and base_url: + api_key = browserbase_api_key + else: + raise ValueError( + "Missing Deep Agent model configuration. Set OPENAI_API_KEY for direct OpenAI access, " + "or set BROWSERBASE_API_KEY together with DEEPAGENT_BASE_URL/OPENAI_BASE_URL for a " + "Browserbase-backed OpenAI-compatible gateway." + ) + + kwargs: dict[str, Any] = { + "model": _normalize_chat_model_name(model), + "api_key": api_key, + } + if base_url: + kwargs["base_url"] = base_url + return ChatOpenAI(**kwargs) + + +def build_agent(model: str): + return create_deep_agent( + model=build_model(model), + tools=[browserbase_search, browserbase_fetch], + subagents=[BROWSER_SUBAGENT], + system_prompt=SYSTEM_PROMPT, + interrupt_on={ + "browserbase_interactive_task": { + "allowed_decisions": ["approve", "edit", "reject"] + } + }, + checkpointer=MemorySaver(), + ) + + +def _stringify_content(content: Any) -> str: + if isinstance(content, str): + return content + if isinstance(content, list): + parts = [] + for item in content: + if isinstance(item, str): + parts.append(item) + elif isinstance(item, dict): + if item.get("type") == "text": + parts.append(str(item.get("text", ""))) + else: + parts.append(json.dumps(item, default=str)) + else: + parts.append(str(item)) + return "\n".join(part for part in parts if part) + return json.dumps(content, indent=2, default=str) + + +def _final_text(result: Any) -> str: + state = getattr(result, "value", result) + if isinstance(state, dict): + messages = state.get("messages", []) + for message in reversed(messages): + msg_type = getattr(message, "type", None) + if msg_type is None and isinstance(message, dict): + msg_type = message.get("type") or message.get("role") + if msg_type in {"ai", "assistant"}: + content = getattr(message, "content", None) + if content is None and isinstance(message, dict): + content = message.get("content") + return _stringify_content(content) + return json.dumps(state, indent=2, default=str) + return str(state) + + +def _review_actions(result: Any) -> list[dict[str, Any]]: + interrupt_value = result.interrupts[0].value + action_requests = interrupt_value["action_requests"] + review_configs = interrupt_value["review_configs"] + config_by_name = {config["action_name"]: config for config in review_configs} + decisions: list[dict[str, Any]] = [] + + for action in action_requests: + review = config_by_name[action["name"]] + allowed = review["allowed_decisions"] + + print("\nPending tool call") + print(f"Tool: {action['name']}") + print("Arguments:") + print(json.dumps(action["args"], indent=2, default=str)) + print(f"Allowed decisions: {', '.join(allowed)}") + + while True: + raw = input("Decision [approve/edit/reject]: ").strip().lower() + if raw in allowed: + if raw == "approve": + decisions.append({"type": "approve"}) + break + if raw == "reject": + decisions.append({"type": "reject"}) + break + + edited = input("Enter replacement JSON args: ").strip() + try: + edited_args = json.loads(edited) + except json.JSONDecodeError: + print("Invalid JSON. Try again.") + continue + decisions.append( + { + "type": "edit", + "edited_action": { + "name": action["name"], + "args": edited_args, + }, + } + ) + break + + print("Invalid decision for this tool call.") + + return decisions + + +def run(query: str, model: str) -> str: + agent = build_agent(model=model) + config = {"configurable": {"thread_id": str(uuid.uuid4())}} + result = agent.invoke( + {"messages": [{"role": "user", "content": query}]}, + config=config, + version="v2", + ) + + while result.interrupts: + decisions = _review_actions(result) + result = agent.invoke( + Command(resume={"decisions": decisions}), + config=config, + version="v2", + ) + + return _final_text(result) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Sample LangChain Deep Agents app with Browserbase tools." + ) + parser.add_argument( + "query", + nargs="?", + default=( + "Research the Browserbase Search API and explain when to use Search, Fetch, " + "and a full browser session. Cite the URLs you used." + ), + ) + parser.add_argument( + "--model", + default=os.getenv("DEEPAGENT_MODEL", "gpt-5.4"), + help="Deep Agents model name. OpenAI-compatible raw names are recommended.", + ) + return parser.parse_args() + + +if __name__ == "__main__": + load_dotenv() + args = parse_args() + print(run(query=args.query, model=args.model)) diff --git a/examples/integrations/langchain/deepagents-browserbase/requirements.txt b/examples/integrations/langchain/deepagents-browserbase/requirements.txt new file mode 100644 index 0000000..767d909 --- /dev/null +++ b/examples/integrations/langchain/deepagents-browserbase/requirements.txt @@ -0,0 +1,6 @@ +beautifulsoup4>=4.13.0 +browserbase>=1.0.0 +deepagents>=0.0.5 +langchain-openai>=0.3.0 +python-dotenv>=1.0.0 +stagehand>=0.3.0 From 10942133cd6fb45ac82fd59ef29be5474ab8dc5e Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Tue, 21 Apr 2026 18:03:30 -0700 Subject: [PATCH 2/4] working version of langchain deepagent with browserbase tools --- .gitignore | 13 ++++- .../deepagents-browserbase/browser_tools.py | 57 +++++++++++++------ 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 09d6d0f..e8b1129 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,15 @@ playwright-report/ # venv venv/ -.venv/ \ No newline at end of file +.venv/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ \ No newline at end of file diff --git a/examples/integrations/langchain/deepagents-browserbase/browser_tools.py b/examples/integrations/langchain/deepagents-browserbase/browser_tools.py index 0619aed..052d53b 100644 --- a/examples/integrations/langchain/deepagents-browserbase/browser_tools.py +++ b/examples/integrations/langchain/deepagents-browserbase/browser_tools.py @@ -6,8 +6,8 @@ import re from typing import Any -from browserbase import Browserbase from bs4 import BeautifulSoup +import httpx from langchain_core.tools import tool from stagehand import Stagehand, StagehandConfig @@ -29,8 +29,11 @@ def _require_env(name: str) -> str: return value -def _browserbase_client() -> Browserbase: - return Browserbase(api_key=_require_env("BROWSERBASE_API_KEY")) +def _browserbase_headers() -> dict[str, str]: + return { + "x-bb-api-key": _require_env("BROWSERBASE_API_KEY"), + "Content-Type": "application/json", + } def _normalize(value: Any) -> Any: @@ -113,22 +116,31 @@ def _run_async(coro: Any) -> Any: @tool def browserbase_search(query: str, num_results: int = 5) -> str: """Search the web with Browserbase. Use this first for discovery before opening pages.""" - bb = _browserbase_client() - response = bb.search.web(query=query, num_results=max(1, min(num_results, 10))) + response = httpx.post( + "https://api.browserbase.com/v1/search", + headers=_browserbase_headers(), + json={ + "query": query, + "numResults": max(1, min(num_results, 10)), + }, + timeout=30.0, + ) + response.raise_for_status() + payload = response.json() results = [] - for result in response.results: + for result in payload.get("results", []): results.append( { - "title": getattr(result, "title", ""), - "url": getattr(result, "url", ""), - "author": getattr(result, "author", None), - "published_date": getattr(result, "published_date", None), + "title": result.get("title", ""), + "url": result.get("url", ""), + "author": result.get("author"), + "published_date": result.get("publishedDate"), } ) return _json( { "query": query, - "request_id": getattr(response, "request_id", None), + "request_id": payload.get("requestId"), "results": results, } ) @@ -137,10 +149,19 @@ def browserbase_search(query: str, num_results: int = 5) -> str: @tool def browserbase_fetch(url: str, use_proxy: bool = False, max_chars: int = 12000) -> str: """Fetch page content without a browser session. Best for static pages and quick reads.""" - bb = _browserbase_client() - response = bb.fetch_api.create(url=url, proxies=use_proxy) - content = getattr(response, "content", "") - content_type = (getattr(response, "content_type", "") or "").lower() + response = httpx.post( + "https://api.browserbase.com/v1/fetch", + headers=_browserbase_headers(), + json={ + "url": url, + "proxies": use_proxy, + }, + timeout=30.0, + ) + response.raise_for_status() + payload = response.json() + content = payload.get("content", "") + content_type = (payload.get("contentType", "") or "").lower() title = "" text = str(content)[:max_chars] @@ -150,9 +171,9 @@ def browserbase_fetch(url: str, use_proxy: bool = False, max_chars: int = 12000) return _json( { "url": url, - "status_code": getattr(response, "status_code", None), - "content_type": getattr(response, "content_type", None), - "encoding": getattr(response, "encoding", None), + "status_code": payload.get("statusCode"), + "content_type": payload.get("contentType"), + "encoding": payload.get("encoding"), "title": title, "text": text, } From 5070bafe46f3d173ee5381a5ea38842a45a22027 Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Wed, 22 Apr 2026 11:43:43 -0700 Subject: [PATCH 3/4] fix(langchain): use Browserbase SDK for search and fetch --- .../deepagents-browserbase/browser_tools.py | 67 ++++++++----------- .../deepagents-browserbase/requirements.txt | 2 +- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/examples/integrations/langchain/deepagents-browserbase/browser_tools.py b/examples/integrations/langchain/deepagents-browserbase/browser_tools.py index 052d53b..ca523a6 100644 --- a/examples/integrations/langchain/deepagents-browserbase/browser_tools.py +++ b/examples/integrations/langchain/deepagents-browserbase/browser_tools.py @@ -6,8 +6,8 @@ import re from typing import Any +from browserbase import Browserbase from bs4 import BeautifulSoup -import httpx from langchain_core.tools import tool from stagehand import Stagehand, StagehandConfig @@ -29,11 +29,8 @@ def _require_env(name: str) -> str: return value -def _browserbase_headers() -> dict[str, str]: - return { - "x-bb-api-key": _require_env("BROWSERBASE_API_KEY"), - "Content-Type": "application/json", - } +def _browserbase_client() -> Browserbase: + return Browserbase(api_key=_require_env("BROWSERBASE_API_KEY")) def _normalize(value: Any) -> Any: @@ -116,31 +113,26 @@ def _run_async(coro: Any) -> Any: @tool def browserbase_search(query: str, num_results: int = 5) -> str: """Search the web with Browserbase. Use this first for discovery before opening pages.""" - response = httpx.post( - "https://api.browserbase.com/v1/search", - headers=_browserbase_headers(), - json={ - "query": query, - "numResults": max(1, min(num_results, 10)), - }, - timeout=30.0, - ) - response.raise_for_status() - payload = response.json() + bb = _browserbase_client() + response = bb.search.web(query=query, num_results=max(1, min(num_results, 10))) results = [] - for result in payload.get("results", []): + for result in getattr(response, "results", []): results.append( { - "title": result.get("title", ""), - "url": result.get("url", ""), - "author": result.get("author"), - "published_date": result.get("publishedDate"), + "title": getattr(result, "title", ""), + "url": getattr(result, "url", ""), + "author": getattr(result, "author", None), + "published_date": ( + getattr(result, "published_date", None) + or getattr(result, "publishedDate", None) + ), } ) return _json( { "query": query, - "request_id": payload.get("requestId"), + "request_id": getattr(response, "request_id", None) + or getattr(response, "requestId", None), "results": results, } ) @@ -149,19 +141,14 @@ def browserbase_search(query: str, num_results: int = 5) -> str: @tool def browserbase_fetch(url: str, use_proxy: bool = False, max_chars: int = 12000) -> str: """Fetch page content without a browser session. Best for static pages and quick reads.""" - response = httpx.post( - "https://api.browserbase.com/v1/fetch", - headers=_browserbase_headers(), - json={ - "url": url, - "proxies": use_proxy, - }, - timeout=30.0, - ) - response.raise_for_status() - payload = response.json() - content = payload.get("content", "") - content_type = (payload.get("contentType", "") or "").lower() + bb = _browserbase_client() + response = bb.fetch_api.create(url=url, proxies=use_proxy) + content = getattr(response, "content", "") + content_type = ( + getattr(response, "content_type", None) + or getattr(response, "contentType", "") + or "" + ).lower() title = "" text = str(content)[:max_chars] @@ -171,9 +158,11 @@ def browserbase_fetch(url: str, use_proxy: bool = False, max_chars: int = 12000) return _json( { "url": url, - "status_code": payload.get("statusCode"), - "content_type": payload.get("contentType"), - "encoding": payload.get("encoding"), + "status_code": getattr(response, "status_code", None) + or getattr(response, "statusCode", None), + "content_type": getattr(response, "content_type", None) + or getattr(response, "contentType", None), + "encoding": getattr(response, "encoding", None), "title": title, "text": text, } diff --git a/examples/integrations/langchain/deepagents-browserbase/requirements.txt b/examples/integrations/langchain/deepagents-browserbase/requirements.txt index 767d909..36b0706 100644 --- a/examples/integrations/langchain/deepagents-browserbase/requirements.txt +++ b/examples/integrations/langchain/deepagents-browserbase/requirements.txt @@ -1,5 +1,5 @@ beautifulsoup4>=4.13.0 -browserbase>=1.0.0 +browserbase>=1.8.0 deepagents>=0.0.5 langchain-openai>=0.3.0 python-dotenv>=1.0.0 From 8c1ebdd818c80bcd69ae5187ed9ebcf36047a9f0 Mon Sep 17 00:00:00 2001 From: Kylejeong2 Date: Sat, 25 Apr 2026 17:00:19 -0700 Subject: [PATCH 4/4] fix: update code to work with the lastest version of stagehand v3 --- .../deepagents-browserbase/browser_tools.py | 112 +++++++++--------- .../deepagents-browserbase/requirements.txt | 2 +- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/examples/integrations/langchain/deepagents-browserbase/browser_tools.py b/examples/integrations/langchain/deepagents-browserbase/browser_tools.py index ca523a6..e41a273 100644 --- a/examples/integrations/langchain/deepagents-browserbase/browser_tools.py +++ b/examples/integrations/langchain/deepagents-browserbase/browser_tools.py @@ -9,8 +9,10 @@ from browserbase import Browserbase from bs4 import BeautifulSoup from langchain_core.tools import tool -from stagehand import Stagehand, StagehandConfig +from stagehand import AsyncStagehand +# Using the Browserbase Model Gateway, you only need to pass your Browserbase API key to use frontier models +# Docs: https://docs.browserbase.com/platform/model-gateway/overview DEFAULT_STAGEHAND_MODEL = os.getenv( "STAGEHAND_MODEL", @@ -70,42 +72,12 @@ def _html_to_text(html: str, max_chars: int) -> tuple[str, str]: return title, text[:max_chars] -def _stagehand_client() -> Stagehand: - _require_env("BROWSERBASE_API_KEY") - return Stagehand() - - -def _stagehand_config(model_name: str) -> StagehandConfig: - return StagehandConfig( - env="BROWSERBASE", - api_key=_require_env("BROWSERBASE_API_KEY"), - project_id=os.getenv("BROWSERBASE_PROJECT_ID"), - model_name=model_name, +def _stagehand_client() -> AsyncStagehand: + return AsyncStagehand( + browserbase_api_key=_require_env("BROWSERBASE_API_KEY"), ) -def _session_id(response: Any) -> str: - data = getattr(response, "data", None) - session_id = getattr(data, "session_id", None) or getattr(response, "session_id", None) - if not session_id: - raise RuntimeError(f"Could not extract session id from Stagehand response: {_json(response)}") - return session_id - - -def _extract_result_payload(result: Any) -> Any: - data = getattr(result, "data", None) - extracted = getattr(data, "result", None) - if extracted is not None: - return _normalize(extracted) - return _normalize(result) - - -def _close_stagehand(client: Any) -> None: - closer = getattr(client, "close", None) - if callable(closer): - closer() - - def _run_async(coro: Any) -> Any: return asyncio.run(coro) @@ -172,27 +144,38 @@ def browserbase_fetch(url: str, use_proxy: bool = False, max_chars: int = 12000) @tool def browserbase_rendered_extract(start_url: str, instruction: str) -> str: """Open a full Browserbase browser session and extract rendered content from a page with Stagehand.""" + return _run_async(_browserbase_rendered_extract_async(start_url=start_url, instruction=instruction)) + + +async def _browserbase_rendered_extract_async(start_url: str, instruction: str) -> str: client = _stagehand_client() - response = client.sessions.start(model_name=DEFAULT_STAGEHAND_MODEL) - session_id = _session_id(response) + start_resp = await client.sessions.start( + model_name=DEFAULT_STAGEHAND_MODEL, + ) + session_id = start_resp.data.session_id try: - client.sessions.navigate(id=session_id, url=start_url) - result = client.sessions.extract(id=session_id, instruction=instruction) + await client.sessions.navigate( + id=session_id, + url=start_url, + frame_id="", + ) + result = await client.sessions.extract( + id=session_id, + instruction=instruction, + ) + extracted = getattr(getattr(result, "data", None), "result", None) return _json( { "start_url": start_url, "session_id": session_id, "session_url": f"https://browserbase.com/sessions/{session_id}", "instruction": instruction, - "result": _extract_result_payload(result), + "result": _normalize(extracted), } ) finally: - try: - client.sessions.end(id=session_id) - finally: - _close_stagehand(client) + await client.sessions.end(id=session_id) @tool @@ -202,17 +185,34 @@ def browserbase_interactive_task(start_url: str, task: str) -> str: async def _browserbase_interactive_task_async(start_url: str, task: str) -> str: - config = _stagehand_config(model_name=DEFAULT_STAGEHAND_AGENT_MODEL) - async with Stagehand(config) as stagehand: - page = stagehand.page - await page.goto(start_url) - - agent = stagehand.agent( - model=DEFAULT_STAGEHAND_AGENT_MODEL, - instructions=( - "You are executing a browser task on behalf of a LangChain tool. " - "Be precise, avoid unnecessary actions, and stop once the requested task is complete." - ), + client = _stagehand_client() + start_resp = await client.sessions.start( + model_name=DEFAULT_STAGEHAND_AGENT_MODEL, + ) + session_id = start_resp.data.session_id + + try: + await client.sessions.navigate( + id=session_id, + url=start_url, + frame_id="", ) - agent_result = await agent.execute(task) - return _json(_normalize(agent_result)) + result = await client.sessions.execute( + id=session_id, + execute_options={ + "instruction": task, + "max_steps": 20, + }, + agent_config={ + "model": DEFAULT_STAGEHAND_AGENT_MODEL, + "instructions": ( + "You are executing a browser task on behalf of a LangChain tool. " + "Be precise, avoid unnecessary actions, and stop once the requested task is complete." + ), + }, + timeout=300.0, + ) + return _json(_normalize(result)) + finally: + await client.sessions.end(id=session_id) + \ No newline at end of file diff --git a/examples/integrations/langchain/deepagents-browserbase/requirements.txt b/examples/integrations/langchain/deepagents-browserbase/requirements.txt index 36b0706..0a9bb1a 100644 --- a/examples/integrations/langchain/deepagents-browserbase/requirements.txt +++ b/examples/integrations/langchain/deepagents-browserbase/requirements.txt @@ -3,4 +3,4 @@ browserbase>=1.8.0 deepagents>=0.0.5 langchain-openai>=0.3.0 python-dotenv>=1.0.0 -stagehand>=0.3.0 +stagehand>=3.19.5