Skip to content

Commit d9174e0

Browse files
committed
make sync request
1 parent de5966f commit d9174e0

2 files changed

Lines changed: 100 additions & 51 deletions

File tree

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: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
from playwright.async_api import Page as PlaywrightPage
1717

1818
from .agent import Agent
19-
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+
)
2025
from .browser import (
2126
cleanup_browser_resources,
2227
connect_browserbase_browser,
@@ -781,8 +786,12 @@ def __getattribute__(self, name):
781786
# Try to get current event loop
782787
try:
783788
asyncio.get_running_loop()
784-
# Already in async context, return empty metrics
785-
return StagehandMetrics()
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()
786795
except RuntimeError:
787796
# No event loop running, safe to use asyncio.run
788797
return asyncio.run(get_replay_metrics())
@@ -804,3 +813,4 @@ def __getattribute__(self, name):
804813
Stagehand._create_session = _create_session
805814
Stagehand._execute = _execute
806815
Stagehand._get_replay_metrics = _get_replay_metrics
816+
Stagehand._get_replay_metrics_sync = _get_replay_metrics_sync

0 commit comments

Comments
 (0)