Skip to content

Commit 8ff3c21

Browse files
author
Nick Sweeting
committed
fix python client
1 parent 229944b commit 8ff3c21

File tree

1 file changed

+52
-37
lines changed

1 file changed

+52
-37
lines changed

packages/core/examples/stagehand.py

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Connects to a remote Stagehand server (Node.js) and executes browser automation tasks.
66
77
Dependencies:
8-
pip install httpx httpx-sse
8+
pip install httpx
99
1010
Usage:
1111
from stagehand import Stagehand
@@ -24,7 +24,6 @@ async def main():
2424
import json
2525
from typing import Any, Dict, List, Optional, Union
2626
import httpx
27-
from httpx_sse import aconnect_sse
2827

2928

3029
class 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

Comments
 (0)