55Connects to a remote Stagehand server (Node.js) and executes browser automation tasks.
66
77Dependencies:
8- pip install httpx httpx-sse
8+ pip install httpx
99
1010Usage:
1111 from stagehand import Stagehand
@@ -24,7 +24,6 @@ async def main():
2424import json
2525from typing import Any , Dict , List , Optional , Union
2626import httpx
27- from httpx_sse import aconnect_sse
2827
2928
3029class StagehandError (Exception ):
@@ -186,14 +185,14 @@ async def act(
186185 """
187186 input_data = instruction .to_dict () if isinstance (instruction , Action ) else instruction
188187
189- result = await self . _execute (
190- method = "act" ,
191- args = {
192- "input" : input_data ,
193- "options" : options ,
194- "frameId" : frame_id ,
195- }
196- )
188+ # Build request matching server schema
189+ request_data = { "input" : input_data }
190+ if options is not None :
191+ request_data [ "options" ] = options
192+ if frame_id is not None :
193+ request_data [ "frameId" ] = frame_id
194+
195+ result = await self . _execute ( method = "act" , args = request_data )
197196
198197 return ActResult (result )
199198
@@ -216,15 +215,18 @@ async def extract(
216215 Returns:
217216 Extracted data matching the schema (if provided) or default extraction
218217 """
219- return await self ._execute (
220- method = "extract" ,
221- args = {
222- "instruction" : instruction ,
223- "schema" : schema ,
224- "options" : options ,
225- "frameId" : frame_id ,
226- }
227- )
218+ # Build request matching server schema
219+ request_data = {}
220+ if instruction is not None :
221+ request_data ["instruction" ] = instruction
222+ if schema is not None :
223+ request_data ["schema" ] = schema
224+ if options is not None :
225+ request_data ["options" ] = options
226+ if frame_id is not None :
227+ request_data ["frameId" ] = frame_id
228+
229+ return await self ._execute (method = "extract" , args = request_data )
228230
229231 async def observe (
230232 self ,
@@ -243,14 +245,16 @@ async def observe(
243245 Returns:
244246 List of Action objects representing possible actions
245247 """
246- result = await self ._execute (
247- method = "observe" ,
248- args = {
249- "instruction" : instruction ,
250- "options" : options ,
251- "frameId" : frame_id ,
252- }
253- )
248+ # Build request matching server schema
249+ request_data = {}
250+ if instruction is not None :
251+ request_data ["instruction" ] = instruction
252+ if options is not None :
253+ request_data ["options" ] = options
254+ if frame_id is not None :
255+ request_data ["frameId" ] = frame_id
256+
257+ result = await self ._execute (method = "observe" , args = request_data )
254258
255259 return [Action (action ) for action in result ]
256260
@@ -319,21 +323,28 @@ async def _execute(self, method: str, args: Dict[str, Any]) -> Any:
319323
320324 url = f"{ self .server_url } /v1/sessions/{ self .session_id } /{ method } "
321325
322- # Create a new client for each request to avoid connection reuse issues with SSE
323- async with httpx .AsyncClient (timeout = self .timeout ) as client :
326+ # Create a new client for each request with no connection pooling
327+ limits = httpx .Limits (max_keepalive_connections = 0 , max_connections = 1 )
328+ async with httpx .AsyncClient (timeout = self .timeout , limits = limits ) as client :
324329 try :
325- async with aconnect_sse (
326- client ,
330+ async with client .stream (
327331 "POST" ,
328332 url ,
329333 json = args ,
330334 headers = {"x-stream-response" : "true" },
331- ) as event_source :
335+ ) as response :
336+ response .raise_for_status ()
337+
332338 result = None
333339
334- async for sse in event_source .aiter_sse ():
340+ async for line in response .aiter_lines ():
341+ if not line .strip () or not line .startswith ("data: " ):
342+ continue
343+
344+ # Parse SSE data
345+ data_str = line [6 :] # Remove "data: " prefix
335346 try :
336- event = json .loads (sse . data )
347+ event = json .loads (data_str )
337348 except json .JSONDecodeError :
338349 continue
339350
@@ -367,9 +378,13 @@ async def _execute(self, method: str, args: Dict[str, Any]) -> Any:
367378 return result
368379
369380 except httpx .HTTPStatusError as e :
370- raise StagehandAPIError (
371- f"HTTP { e .response .status_code } : { e .response .text } "
372- )
381+ error_msg = f"HTTP { e .response .status_code } "
382+ try :
383+ error_text = await e .response .aread ()
384+ error_msg += f": { error_text .decode ()} "
385+ except Exception :
386+ pass
387+ raise StagehandAPIError (error_msg )
373388 except httpx .HTTPError as e :
374389 raise StagehandConnectionError (f"Connection error: { e } " )
375390
0 commit comments