Skip to content

Commit 64cbc6e

Browse files
authored
Remove nest-asyncio dependency (#309)
* Remove nest-asyncio dependency * make sync request
1 parent 9823ad3 commit 64cbc6e

File tree

4 files changed

+107
-56
lines changed

4 files changed

+107
-56
lines changed

.changeset/adamant-black-perch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"stagehand": patch
3+
---
4+
5+
Remove nest-asyncio dependency

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description = "Python SDK for Stagehand"
99
readme = "README.md"
1010
classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent",]
1111
requires-python = ">=3.9"
12-
dependencies = [ "httpx>=0.24.0", "python-dotenv>=1.0.0", "pydantic>=1.10.0", "playwright>=1.42.1", "requests>=2.31.0", "browserbase>=1.4.0", "rich>=13.7.0", "openai>=1.99.6", "anthropic>=0.51.0", "litellm>=1.72.0,<=1.80.0", "nest-asyncio>=1.6.0", "google-genai>=1.40.0",]
12+
dependencies = [ "httpx>=0.24.0", "python-dotenv>=1.0.0", "pydantic>=1.10.0", "playwright>=1.42.1", "requests>=2.31.0", "browserbase>=1.4.0", "rich>=13.7.0", "openai>=1.99.6", "anthropic>=0.51.0", "litellm>=1.72.0,<=1.80.0", "google-genai>=1.40.0",]
1313
[[project.authors]]
1414
name = "Browserbase, Inc."
1515
email = "support@browserbase.com"

stagehand/api.py

Lines changed: 87 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .metrics import StagehandMetrics
77
from .utils import convert_dict_keys_to_camel_case
88

9-
__all__ = ["_create_session", "_execute", "_get_replay_metrics"]
9+
__all__ = ["_create_session", "_execute", "_get_replay_metrics", "_get_replay_metrics_sync"]
1010

1111

1212
async def _create_session(self):
@@ -210,11 +210,59 @@ async def _execute(self, method: str, payload: dict[str, Any]) -> Any:
210210
raise
211211

212212

213-
async def _get_replay_metrics(self):
213+
def _parse_replay_metrics_data(data: dict) -> StagehandMetrics:
214214
"""
215-
Fetch replay metrics from the API and parse them into StagehandMetrics.
215+
Parse raw API response data into StagehandMetrics.
216+
Shared by both async and sync fetch paths.
216217
"""
218+
if not data.get("success"):
219+
raise RuntimeError(
220+
f"Failed to fetch metrics: {data.get('error', 'Unknown error')}"
221+
)
222+
223+
api_data = data.get("data", {})
224+
metrics = StagehandMetrics()
225+
226+
pages = api_data.get("pages", [])
227+
for page in pages:
228+
actions = page.get("actions", [])
229+
for action in actions:
230+
method = action.get("method", "").lower()
231+
token_usage = action.get("tokenUsage", {})
232+
233+
if token_usage:
234+
input_tokens = token_usage.get("inputTokens", 0)
235+
output_tokens = token_usage.get("outputTokens", 0)
236+
time_ms = token_usage.get("timeMs", 0)
237+
238+
if method == "act":
239+
metrics.act_prompt_tokens += input_tokens
240+
metrics.act_completion_tokens += output_tokens
241+
metrics.act_inference_time_ms += time_ms
242+
elif method == "extract":
243+
metrics.extract_prompt_tokens += input_tokens
244+
metrics.extract_completion_tokens += output_tokens
245+
metrics.extract_inference_time_ms += time_ms
246+
elif method == "observe":
247+
metrics.observe_prompt_tokens += input_tokens
248+
metrics.observe_completion_tokens += output_tokens
249+
metrics.observe_inference_time_ms += time_ms
250+
elif method == "agent":
251+
metrics.agent_prompt_tokens += input_tokens
252+
metrics.agent_completion_tokens += output_tokens
253+
metrics.agent_inference_time_ms += time_ms
254+
255+
metrics.total_prompt_tokens += input_tokens
256+
metrics.total_completion_tokens += output_tokens
257+
metrics.total_inference_time_ms += time_ms
258+
259+
return metrics
260+
217261

262+
async def _get_replay_metrics(self):
263+
"""
264+
Fetch replay metrics from the API (async version).
265+
"""
218266
if not self.session_id:
219267
raise ValueError("session_id is required to fetch metrics.")
220268

@@ -241,55 +289,46 @@ async def _get_replay_metrics(self):
241289
f"Failed to fetch metrics with status {response.status_code}: {error_text}"
242290
)
243291

244-
data = response.json()
292+
return _parse_replay_metrics_data(response.json())
293+
294+
except Exception as e:
295+
self.logger.error(f"[EXCEPTION] Error fetching replay metrics: {str(e)}")
296+
raise
297+
298+
299+
def _get_replay_metrics_sync(self):
300+
"""
301+
Fetch replay metrics from the API (sync version).
302+
Uses a synchronous httpx request so it can be called from sync contexts
303+
even when an async event loop is already running.
304+
"""
305+
import httpx
306+
307+
if not self.session_id:
308+
raise ValueError("session_id is required to fetch metrics.")
309+
310+
headers = {
311+
"x-bb-api-key": self.browserbase_api_key,
312+
"x-bb-project-id": self.browserbase_project_id,
313+
"Content-Type": "application/json",
314+
}
245315

246-
if not data.get("success"):
316+
try:
317+
response = httpx.get(
318+
f"{self.api_url}/sessions/{self.session_id}/replay",
319+
headers=headers,
320+
timeout=self.timeout_settings,
321+
)
322+
323+
if response.status_code != 200:
324+
self.logger.error(
325+
f"[HTTP ERROR] Failed to fetch metrics. Status {response.status_code}: {response.text}"
326+
)
247327
raise RuntimeError(
248-
f"Failed to fetch metrics: {data.get('error', 'Unknown error')}"
328+
f"Failed to fetch metrics with status {response.status_code}: {response.text}"
249329
)
250330

251-
# Parse the API data into StagehandMetrics format
252-
api_data = data.get("data", {})
253-
metrics = StagehandMetrics()
254-
255-
# Parse pages and their actions
256-
pages = api_data.get("pages", [])
257-
for page in pages:
258-
actions = page.get("actions", [])
259-
for action in actions:
260-
# Get method name and token usage
261-
method = action.get("method", "").lower()
262-
token_usage = action.get("tokenUsage", {})
263-
264-
if token_usage:
265-
input_tokens = token_usage.get("inputTokens", 0)
266-
output_tokens = token_usage.get("outputTokens", 0)
267-
time_ms = token_usage.get("timeMs", 0)
268-
269-
# Map method to metrics fields
270-
if method == "act":
271-
metrics.act_prompt_tokens += input_tokens
272-
metrics.act_completion_tokens += output_tokens
273-
metrics.act_inference_time_ms += time_ms
274-
elif method == "extract":
275-
metrics.extract_prompt_tokens += input_tokens
276-
metrics.extract_completion_tokens += output_tokens
277-
metrics.extract_inference_time_ms += time_ms
278-
elif method == "observe":
279-
metrics.observe_prompt_tokens += input_tokens
280-
metrics.observe_completion_tokens += output_tokens
281-
metrics.observe_inference_time_ms += time_ms
282-
elif method == "agent":
283-
metrics.agent_prompt_tokens += input_tokens
284-
metrics.agent_completion_tokens += output_tokens
285-
metrics.agent_inference_time_ms += time_ms
286-
287-
# Always update totals for any method with token usage
288-
metrics.total_prompt_tokens += input_tokens
289-
metrics.total_completion_tokens += output_tokens
290-
metrics.total_inference_time_ms += time_ms
291-
292-
return metrics
331+
return _parse_replay_metrics_data(response.json())
293332

294333
except Exception as e:
295334
self.logger.error(f"[EXCEPTION] Error fetching replay metrics: {str(e)}")

stagehand/main.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from typing import Any, Optional
88

99
import httpx
10-
import nest_asyncio
1110
from dotenv import load_dotenv
1211
from playwright.async_api import (
1312
BrowserContext,
@@ -17,7 +16,12 @@
1716
from playwright.async_api import Page as PlaywrightPage
1817

1918
from .agent import Agent
20-
from .api import _create_session, _execute, _get_replay_metrics
19+
from .api import (
20+
_create_session,
21+
_execute,
22+
_get_replay_metrics,
23+
_get_replay_metrics_sync,
24+
)
2125
from .browser import (
2226
cleanup_browser_resources,
2327
connect_browserbase_browser,
@@ -782,12 +786,14 @@ def __getattribute__(self, name):
782786
# Try to get current event loop
783787
try:
784788
asyncio.get_running_loop()
785-
# We're in an async context, need to handle this carefully
786-
# Create a new task and wait for it
787-
nest_asyncio.apply()
788-
return asyncio.run(get_replay_metrics())
789+
# Already in async context - use sync HTTP to avoid
790+
# event loop nesting issues
791+
get_replay_metrics_sync = object.__getattribute__(
792+
self, "_get_replay_metrics_sync"
793+
)
794+
return get_replay_metrics_sync()
789795
except RuntimeError:
790-
# No event loop running, we can use asyncio.run directly
796+
# No event loop running, safe to use asyncio.run
791797
return asyncio.run(get_replay_metrics())
792798
except Exception as e:
793799
# Log error and return empty metrics
@@ -807,3 +813,4 @@ def __getattribute__(self, name):
807813
Stagehand._create_session = _create_session
808814
Stagehand._execute = _execute
809815
Stagehand._get_replay_metrics = _get_replay_metrics
816+
Stagehand._get_replay_metrics_sync = _get_replay_metrics_sync

0 commit comments

Comments
 (0)