Skip to content

Commit 82bb896

Browse files
3coinsmichaelnchin
andauthored
Browser and code interpreter toolkits (#543)
This PR adds browser and code interpreter toolkits based on the [Amazon Bedrock AgentCore](https://aws.amazon.com/bedrock/agentcore) API. ### Browser Toolkit - Provides tools for automated web browsing and interaction - Features thread-aware browser session management - Capabilities include URL navigation, clicking elements, text extraction ```python from langchain_aws.tools import create_browser_toolkit # Create browser toolkit toolkit, browser_tools = create_browser_toolkit(region="us-west-2") # Available tools print([tool.name for tool in browser_tools]) # ['navigate_browser', 'click_element', 'extract_text', 'extract_hyperlinks', ...] ``` ### Code Interpreter Toolkit - Enables secure code execution in managed AWS environments - Thread-aware code interpreter session management - Capabilities include Python code execution, shell commands, file operations ```python from langchain_aws.tools import create_code_interpreter_toolkit # Create code interpreter toolkit toolkit, code_tools = await create_code_interpreter_toolkit(region="us-west-2") # Available tools print([tool.name for tool in code_tools]) # ['execute_code', 'execute_command', 'read_files', 'list_files', 'delete_files', ...] ``` --------- Co-authored-by: Michael Chin <[email protected]>
1 parent 7919d4f commit 82bb896

File tree

10 files changed

+2706
-14
lines changed

10 files changed

+2706
-14
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .browser_toolkit import create_browser_toolkit
2+
from .code_interpreter_toolkit import create_code_interpreter_toolkit
3+
4+
__all__ = [
5+
"create_browser_toolkit",
6+
"create_code_interpreter_toolkit"
7+
]
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import TYPE_CHECKING, Dict, Tuple
5+
6+
if TYPE_CHECKING:
7+
from playwright.async_api import Browser as AsyncBrowser
8+
from playwright.sync_api import Browser as SyncBrowser
9+
10+
from bedrock_agentcore.tools.browser_client import BrowserClient
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class BrowserSessionManager:
16+
"""
17+
Manages browser sessions for different threads.
18+
19+
This class maintains separate browser sessions for different threads,
20+
enabling concurrent usage of browsers in multi-threaded environments.
21+
Browsers are created lazily only when needed by tools.
22+
23+
Concurrency protection is also implemented. Each browser session is tied
24+
to a specific thread_id and includes protection against concurrent usage.
25+
When a browser is obtained via get_async_browser() or get_sync_browser(),
26+
it is marked as "in use", and subsequent attempts to access the same
27+
browser session will raise a RuntimeError until it is released. In general,
28+
different callers should use different thread_ids to avoid concurrency issues.
29+
"""
30+
31+
def __init__(self, region: str = "us-west-2"):
32+
"""
33+
Initialize the browser session manager.
34+
35+
Args:
36+
region: AWS region for browser client
37+
"""
38+
self.region = region
39+
self._async_sessions: Dict[str, Tuple[BrowserClient, AsyncBrowser, bool]] = {}
40+
self._sync_sessions: Dict[str, Tuple[BrowserClient, SyncBrowser, bool]] = {}
41+
42+
async def get_async_browser(self, thread_id: str) -> AsyncBrowser:
43+
"""
44+
Get or create an async browser for the specified thread.
45+
46+
Args:
47+
thread_id: Unique identifier for the thread requesting the browser
48+
49+
Returns:
50+
An async browser instance specific to the thread
51+
52+
Raises:
53+
RuntimeError: If the browser session is already in use by another caller
54+
"""
55+
if thread_id in self._async_sessions:
56+
client, browser, in_use = self._async_sessions[thread_id]
57+
if in_use:
58+
raise RuntimeError(
59+
f"Browser session for thread {thread_id} is already in use. "
60+
"Use a different thread_id for concurrent operations."
61+
)
62+
self._async_sessions[thread_id] = (client, browser, True)
63+
return browser
64+
65+
return await self._create_async_browser_session(thread_id)
66+
67+
def get_sync_browser(self, thread_id: str) -> SyncBrowser:
68+
"""
69+
Get or create a sync browser for the specified thread.
70+
71+
Args:
72+
thread_id: Unique identifier for the thread requesting the browser
73+
74+
Returns:
75+
A sync browser instance specific to the thread
76+
77+
Raises:
78+
RuntimeError: If the browser session is already in use by another caller
79+
"""
80+
if thread_id in self._sync_sessions:
81+
client, browser, in_use = self._sync_sessions[thread_id]
82+
if in_use:
83+
raise RuntimeError(
84+
f"Browser session for thread {thread_id} is already in use. "
85+
"Use a different thread_id for concurrent operations."
86+
)
87+
self._sync_sessions[thread_id] = (client, browser, True)
88+
return browser
89+
90+
return self._create_sync_browser_session(thread_id)
91+
92+
async def _create_async_browser_session(self, thread_id: str) -> AsyncBrowser:
93+
"""
94+
Create a new async browser session for the specified thread.
95+
96+
Args:
97+
thread_id: Unique identifier for the thread
98+
99+
Returns:
100+
The newly created async browser instance
101+
102+
Raises:
103+
Exception: If browser session creation fails
104+
"""
105+
browser_client = BrowserClient(region=self.region)
106+
107+
try:
108+
# Start browser session
109+
browser_client.start()
110+
111+
# Get WebSocket connection info
112+
ws_url, headers = browser_client.generate_ws_headers()
113+
114+
logger.info(
115+
f"Connecting to async WebSocket endpoint for thread {thread_id}: {ws_url}"
116+
)
117+
118+
from playwright.async_api import async_playwright
119+
120+
# Connect to browser using Playwright
121+
playwright = await async_playwright().start()
122+
browser = await playwright.chromium.connect_over_cdp(
123+
endpoint_url=ws_url, headers=headers, timeout=30000
124+
)
125+
logger.info(
126+
f"Successfully connected to async browser for thread {thread_id}"
127+
)
128+
129+
self._async_sessions[thread_id] = (browser_client, browser, True)
130+
131+
return browser
132+
133+
except Exception as e:
134+
logger.error(
135+
f"Failed to create async browser session for thread {thread_id}: {e}"
136+
)
137+
138+
# Clean up resources if session creation fails
139+
if browser_client:
140+
try:
141+
browser_client.stop()
142+
except Exception as cleanup_error:
143+
logger.warning(f"Error cleaning up browser client: {cleanup_error}")
144+
145+
raise
146+
147+
def _create_sync_browser_session(self, thread_id: str) -> SyncBrowser:
148+
"""
149+
Create a new sync browser session for the specified thread.
150+
151+
Args:
152+
thread_id: Unique identifier for the thread
153+
154+
Returns:
155+
The newly created sync browser instance
156+
157+
Raises:
158+
Exception: If browser session creation fails
159+
"""
160+
browser_client = BrowserClient(region=self.region)
161+
162+
try:
163+
# Start browser session
164+
browser_client.start()
165+
166+
# Get WebSocket connection info
167+
ws_url, headers = browser_client.generate_ws_headers()
168+
169+
logger.info(
170+
f"Connecting to sync WebSocket endpoint for thread {thread_id}: {ws_url}"
171+
)
172+
173+
from playwright.sync_api import sync_playwright
174+
175+
# Connect to browser using Playwright
176+
playwright = sync_playwright().start()
177+
browser = playwright.chromium.connect_over_cdp(
178+
endpoint_url=ws_url, headers=headers, timeout=30000
179+
)
180+
logger.info(
181+
f"Successfully connected to sync browser for thread {thread_id}"
182+
)
183+
184+
self._sync_sessions[thread_id] = (browser_client, browser, True)
185+
186+
return browser
187+
188+
except Exception as e:
189+
logger.error(
190+
f"Failed to create sync browser session for thread {thread_id}: {e}"
191+
)
192+
193+
# Clean up resources if session creation fails
194+
if browser_client:
195+
try:
196+
browser_client.stop()
197+
except Exception as cleanup_error:
198+
logger.warning(f"Error cleaning up browser client: {cleanup_error}")
199+
200+
raise
201+
202+
async def release_async_browser(self, thread_id: str) -> None:
203+
"""
204+
Release the async browser session for the specified thread.
205+
206+
Args:
207+
thread_id: Unique identifier for the thread
208+
209+
Raises:
210+
KeyError: If no browser session exists for the specified thread_id
211+
"""
212+
if thread_id not in self._async_sessions:
213+
raise KeyError(f"No async browser session found for thread {thread_id}")
214+
215+
client, browser, _ = self._async_sessions[thread_id]
216+
self._async_sessions[thread_id] = (client, browser, False)
217+
logger.debug(f"Async browser session released for thread {thread_id}")
218+
219+
def release_sync_browser(self, thread_id: str) -> None:
220+
"""
221+
Release the sync browser session for the specified thread.
222+
223+
Args:
224+
thread_id: Unique identifier for the thread
225+
226+
Raises:
227+
KeyError: If no browser session exists for the specified thread_id
228+
"""
229+
if thread_id not in self._sync_sessions:
230+
raise KeyError(f"No sync browser session found for thread {thread_id}")
231+
232+
client, browser, _ = self._sync_sessions[thread_id]
233+
self._sync_sessions[thread_id] = (client, browser, False)
234+
logger.debug(f"Sync browser session released for thread {thread_id}")
235+
236+
async def close_async_browser(self, thread_id: str) -> None:
237+
"""
238+
Close the async browser session for the specified thread.
239+
240+
Args:
241+
thread_id: Unique identifier for the thread
242+
"""
243+
if thread_id not in self._async_sessions:
244+
logger.warning(f"No async browser session found for thread {thread_id}")
245+
return
246+
247+
browser_client, browser, _ = self._async_sessions[thread_id]
248+
249+
# Close browser
250+
if browser:
251+
try:
252+
await browser.close()
253+
except Exception as e:
254+
logger.warning(
255+
f"Error closing async browser for thread {thread_id}: {e}"
256+
)
257+
258+
# Stop browser client
259+
if browser_client:
260+
try:
261+
browser_client.stop()
262+
except Exception as e:
263+
logger.warning(
264+
f"Error stopping browser client for thread {thread_id}: {e}"
265+
)
266+
267+
# Remove session from dictionary
268+
del self._async_sessions[thread_id]
269+
logger.info(f"Async browser session cleaned up for thread {thread_id}")
270+
271+
def close_sync_browser(self, thread_id: str) -> None:
272+
"""
273+
Close the sync browser session for the specified thread.
274+
275+
Args:
276+
thread_id: Unique identifier for the thread
277+
"""
278+
if thread_id not in self._sync_sessions:
279+
logger.warning(f"No sync browser session found for thread {thread_id}")
280+
return
281+
282+
browser_client, browser, _ = self._sync_sessions[thread_id]
283+
284+
# Close browser
285+
if browser:
286+
try:
287+
browser.close()
288+
except Exception as e:
289+
logger.warning(
290+
f"Error closing sync browser for thread {thread_id}: {e}"
291+
)
292+
293+
# Stop browser client
294+
if browser_client:
295+
try:
296+
browser_client.stop()
297+
except Exception as e:
298+
logger.warning(
299+
f"Error stopping browser client for thread {thread_id}: {e}"
300+
)
301+
302+
# Remove session from dictionary
303+
del self._sync_sessions[thread_id]
304+
logger.info(f"Sync browser session cleaned up for thread {thread_id}")
305+
306+
async def close_all_browsers(self) -> None:
307+
"""Close all browser sessions."""
308+
# Close all async browsers
309+
async_thread_ids = list(self._async_sessions.keys())
310+
for thread_id in async_thread_ids:
311+
await self.close_async_browser(thread_id)
312+
313+
# Close all sync browsers
314+
sync_thread_ids = list(self._sync_sessions.keys())
315+
for thread_id in sync_thread_ids:
316+
self.close_sync_browser(thread_id)
317+
318+
logger.info("All browser sessions closed")

0 commit comments

Comments
 (0)