diff --git a/ .env.example b/.env.example similarity index 60% rename from .env.example rename to .env.example index fc61ab3c..45f5ae1b 100644 --- a/ .env.example +++ b/.env.example @@ -1,4 +1,5 @@ -MODEL_API_KEY = "anthropic-or-openai-api-key" +MODEL_API_KEY = "your-favorite-llm-api-key" BROWSERBASE_API_KEY = "browserbase-api-key" BROWSERBASE_PROJECT_ID = "browserbase-project-id" STAGEHAND_API_URL = "api_url" +STAGEHAND_ENV= "LOCAL or BROWSERBASE" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1ca635a6..4a024830 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel diff --git a/README.md b/README.md index edb3b90d..83685e99 100644 --- a/README.md +++ b/README.md @@ -62,109 +62,80 @@ await stagehand.agent.execute("book a reservation for 2 people for a trip to the ## Installation -Install the Python package via pip: +### Creating a Virtual Environment (Recommended) -```bash -pip install stagehand -``` -## Requirements - -- Python 3.9+ -- httpx (for async client) -- requests (for sync client) -- asyncio (for async client) -- pydantic -- python-dotenv (optional, for .env support) -- playwright -- rich (for `examples/` terminal support) - -You can simply run: +First, create and activate a virtual environment to keep your project dependencies isolated: ```bash -pip install -r requirements.txt +# Create a virtual environment +python -m venv stagehand-env + +# Activate the environment +# On macOS/Linux: +source stagehand-env/bin/activate +# On Windows: +stagehand-env\Scripts\activate ``` -**requirements.txt** -```txt -httpx>=0.24.0 -asyncio>=3.4.3 -python-dotenv>=1.0.0 -pydantic>=1.10.0 -playwright>=1.42.1 -requests>=2.31.0 -rich -browserbase -``` - - -## Environment Variables - -Before running your script, set the following environment variables: +### Install Stagehand +**Normal Installation:** ```bash -export BROWSERBASE_API_KEY="your-api-key" -export BROWSERBASE_PROJECT_ID="your-project-id" -export MODEL_API_KEY="your-openai-api-key" # or your preferred model's API key -export STAGEHAND_API_URL="url-of-stagehand-server" +pip install stagehand ``` -You can also make a copy of `.env.example` and add these to your `.env` file. - -## Quickstart - -Stagehand supports both synchronous and asynchronous usage. Here are examples for both approaches: - -### Sync Client +**Local Development Installation:** +If you're contributing to Stagehand or want to modify the source code: -```python -import os -from stagehand.sync import Stagehand -from stagehand import StagehandConfig -from dotenv import load_dotenv +```bash +# Clone the repository +git clone https://github.com/browserbase/stagehand-python.git +cd stagehand-python -load_dotenv() +# Install in editable mode with development dependencies +pip install -e ".[dev]" -def main(): - # Configure Stagehand - config = StagehandConfig( - env="BROWSERBASE", - api_key=os.getenv("BROWSERBASE_API_KEY"), - project_id=os.getenv("BROWSERBASE_PROJECT_ID"), - model_name="gpt-4o", - model_client_options={"apiKey": os.getenv("MODEL_API_KEY")} - ) +### INSTRUCTION TO BE REMOVED BEFORE RELEASE +# install google cua +pip install temp/path-to-the-cua-wheel.wheel +``` - # Initialize Stagehand - stagehand = Stagehand(config=config, api_url=os.getenv("STAGEHAND_API_URL")) - stagehand.init() - print(f"Session created: {stagehand.session_id}") +## Requirements - # Navigate to a page - stagehand.page.goto("https://google.com/") +- Python 3.9+ +- All dependencies are automatically handled when installing via `pip` - # Use Stagehand AI primitives - stagehand.page.act("search for openai") +The main dependencies include: +- httpx (for async HTTP client) +- requests (for sync HTTP client) +- pydantic (for data validation) +- playwright (for browser automation) +- python-dotenv (for environment variable support) +- browserbase (for Browserbase integration) - # Combine with Playwright - stagehand.page.keyboard.press("Enter") +### Development Dependencies - # Observe elements on the page - observed = stagehand.page.observe("find the news button") - if observed: - stagehand.page.act(observed[0]) # Act on the first observed element +The development dependencies are automatically installed when using `pip install -e ".[dev]"` and include: +- pytest, pytest-asyncio, pytest-mock, pytest-cov (testing) +- black, isort, ruff (code formatting and linting) +- mypy (type checking) +- rich (enhanced terminal output) - # Extract data from the page - data = stagehand.page.extract("extract the first result from the search") - print(f"Extracted data: {data}") +## Environment Variables - # Close the session - stagehand.close() +Before running your script, copy `.env.example` to `.env.` set the following environment variables: -if __name__ == "__main__": - main() +```bash +export BROWSERBASE_API_KEY="your-api-key" # if running remotely +export BROWSERBASE_PROJECT_ID="your-project-id" # if running remotely +export MODEL_API_KEY="your-openai-api-key" # or your preferred model's API key +export STAGEHAND_API_URL="url-of-stagehand-server" # if running remotely +export STAGEHAND_ENV="BROWSERBASE" # or "LOCAL" to run Stagehand locally ``` -### Async Client +You can also make a copy of `.env.example` and add these to your `.env` file. + +## Quickstart ```python import os diff --git a/examples/example.py b/examples/example.py index 78219966..5d089f51 100644 --- a/examples/example.py +++ b/examples/example.py @@ -4,133 +4,241 @@ from rich.console import Console from rich.panel import Panel from rich.theme import Theme -import json +from pydantic import BaseModel, Field, HttpUrl from dotenv import load_dotenv +import time -from stagehand import Stagehand, StagehandConfig +from stagehand import StagehandConfig, Stagehand from stagehand.utils import configure_logging +from stagehand.schemas import ObserveOptions, ActOptions, ExtractOptions +from stagehand.a11y.utils import get_accessibility_tree, get_xpath_by_resolved_object_id -# Configure logging with cleaner format -configure_logging( - level=logging.INFO, - remove_logger_name=True, # Remove the redundant stagehand.client prefix - quiet_dependencies=True, # Suppress httpx and other noisy logs -) +# Load environment variables +load_dotenv() -# Create a custom theme for consistent styling -custom_theme = Theme( - { - "info": "cyan", - "success": "green", - "warning": "yellow", - "error": "red bold", - "highlight": "magenta", - "url": "blue underline", - } -) +# Configure Rich console +console = Console(theme=Theme({ + "info": "cyan", + "success": "green", + "warning": "yellow", + "error": "red bold", + "highlight": "magenta", + "url": "blue underline", +})) + +# Define Pydantic models for testing +class Company(BaseModel): + name: str = Field(..., description="The name of the company") + # todo - URL needs to be pydantic type HttpUrl otherwise it does not extract the URL + url: HttpUrl = Field(..., description="The URL of the company website or relevant page") + +class Companies(BaseModel): + companies: list[Company] = Field(..., description="List of companies extracted from the page, maximum of 5 companies") -# Create a Rich console instance with our theme -console = Console(theme=custom_theme) +class ElementAction(BaseModel): + action: str + id: int + arguments: list[str] -load_dotenv() - -console.print( - Panel.fit( - "[yellow]Logging Levels:[/]\n" - "[white]- Set [bold]verbose=0[/] for errors (ERROR)[/]\n" - "[white]- Set [bold]verbose=1[/] for minimal logs (INFO)[/]\n" - "[white]- Set [bold]verbose=2[/] for medium logs (WARNING)[/]\n" - "[white]- Set [bold]verbose=3[/] for detailed logs (DEBUG)[/]", - title="Verbosity Options", - border_style="blue", +async def main(): + # Display header + console.print( + "\n", + Panel.fit( + "[light_gray]New Stagehand 🤘 Python Test[/]", + border_style="green", + padding=(1, 10), + ), ) -) -async def main(): - # Build a unified configuration object for Stagehand + # Create configuration + model_name = "google/gemini-2.5-flash-preview-04-17" + config = StagehandConfig( - env="BROWSERBASE", api_key=os.getenv("BROWSERBASE_API_KEY"), project_id=os.getenv("BROWSERBASE_PROJECT_ID"), - headless=False, - dom_settle_timeout_ms=3000, - model_name="google/gemini-2.0-flash", - self_heal=True, - wait_for_captcha_solves=True, - system_prompt="You are a browser automation assistant that helps users navigate websites effectively.", - model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}, - # Use verbose=2 for medium-detail logs (1=minimal, 3=debug) - verbose=2, + model_name=model_name, # todo - unify gemini/google model names + model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}, # this works locally even if there is a model provider mismatch + verbose=3, ) - - stagehand = Stagehand(config) - - # Initialize - this creates a new session automatically. - console.print("\nšŸš€ [info]Initializing Stagehand...[/]") - await stagehand.init() - page = stagehand.page - console.print(f"\n[yellow]Created new session:[/] {stagehand.session_id}") - console.print( - f"🌐 [white]View your live browser:[/] [url]https://www.browserbase.com/sessions/{stagehand.session_id}[/]" - ) - - await asyncio.sleep(2) - - console.print("\nā–¶ļø [highlight] Navigating[/] to Google") - await page.goto("https://google.com/") - console.print("āœ… [success]Navigated to Google[/]") - - console.print("\nā–¶ļø [highlight] Clicking[/] on About link") - # Click on the "About" link using Playwright - await page.get_by_role("link", name="About", exact=True).click() - console.print("āœ… [success]Clicked on About link[/]") - - await asyncio.sleep(2) - console.print("\nā–¶ļø [highlight] Navigating[/] back to Google") - await page.goto("https://google.com/") - console.print("āœ… [success]Navigated back to Google[/]") - - console.print("\nā–¶ļø [highlight] Performing action:[/] search for openai") - await page.act("search for openai") - await page.keyboard.press("Enter") - console.print("āœ… [success]Performing Action:[/] Action completed successfully") - await asyncio.sleep(2) - - console.print("\nā–¶ļø [highlight] Observing page[/] for news button") - observed = await page.observe("find all articles") + # Initialize async client + stagehand = Stagehand( + env=os.getenv("STAGEHAND_ENV"), + config=config, + api_url=os.getenv("STAGEHAND_SERVER_URL"), + ) - if len(observed) > 0: - element = observed[0] - console.print("āœ… [success]Found element:[/] News button") - console.print("\nā–¶ļø [highlight] Performing action on observed element:") - console.print(element) - await page.act(element) - console.print("āœ… [success]Performing Action:[/] Action completed successfully") - - else: - console.print("āŒ [error]No element found[/]") - - console.print("\nā–¶ļø [highlight] Extracting[/] first search result") - data = await page.extract("extract the first result from the search") - console.print("šŸ“Š [info]Extracted data:[/]") - console.print_json(f"{data.model_dump_json()}") - - # Close the session - console.print("\nā¹ļø [warning]Closing session...[/]") - await stagehand.close() - console.print("āœ… [success]Session closed successfully![/]") - console.rule("[bold]End of Example[/]") - + try: + # Initialize the client + await stagehand.init() + console.print("[success]āœ“ Successfully initialized Stagehand async client[/]") + console.print(f"[info]Environment: {stagehand.env}[/]") + console.print(f"[info]LLM Client Available: {stagehand.llm is not None}[/]") + + # Navigate to AIgrant (as in the original test) + await stagehand.page.goto("https://www.aigrant.com") + console.print("[success]āœ“ Navigated to AIgrant[/]") + await asyncio.sleep(2) + + # Get accessibility tree + tree = await get_accessibility_tree(stagehand.page, stagehand.logger) + console.print("[success]āœ“ Extracted accessibility tree[/]") + + print("ID to URL mapping:", tree.get("idToUrl")) + print("IFrames:", tree.get("iframes")) + + # Click the "Get Started" button + await stagehand.page.act("click the button with text 'Get Started'") + console.print("[success]āœ“ Clicked 'Get Started' button[/]") + + # Observe the button + await stagehand.page.observe("the button with text 'Get Started'") + console.print("[success]āœ“ Observed 'Get Started' button[/]") + + # Extract companies using schema + extract_options = ExtractOptions( + instruction="Extract the names and URLs of up to 5 companies mentioned on this page", + schema_definition=Companies + ) + + extract_result = await stagehand.page.extract(extract_options) + console.print("[success]āœ“ Extracted companies data[/]") + + # Display results + print("Extract result:", extract_result) + print("Extract result data:", extract_result.data if hasattr(extract_result, 'data') else 'No data field') + + # Parse the result into the Companies model + companies_data = None + + # Handle different result formats between LOCAL and BROWSERBASE + if hasattr(extract_result, 'data') and extract_result.data: + # BROWSERBASE mode - data is in the 'data' field + try: + raw_data = extract_result.data + console.print(f"[info]Raw extract data: {raw_data}[/]") + + # Check if the data needs URL resolution from ID mapping + if isinstance(raw_data, dict) and 'companies' in raw_data: + id_to_url = tree.get("idToUrl", {}) + for company in raw_data['companies']: + if 'url' in company and isinstance(company['url'], str): + # Check if URL is just an ID that needs to be resolved + if company['url'].isdigit() and company['url'] in id_to_url: + company['url'] = id_to_url[company['url']] + console.print(f"[success]āœ“ Resolved URL for {company['name']}: {company['url']}[/]") + + companies_data = Companies.model_validate(raw_data) + console.print("[success]āœ“ Successfully parsed extract result into Companies model[/]") + except Exception as e: + console.print(f"[error]Failed to parse extract result: {e}[/]") + print("Raw data:", extract_result.data) + elif hasattr(extract_result, 'companies'): + # LOCAL mode - companies field is directly available + try: + companies_data = Companies.model_validate(extract_result.model_dump()) + console.print("[success]āœ“ Successfully parsed extract result into Companies model[/]") + except Exception as e: + console.print(f"[error]Failed to parse extract result: {e}[/]") + print("Raw companies data:", extract_result.companies) + + print("\nExtracted Companies:") + if companies_data and hasattr(companies_data, "companies"): + for idx, company in enumerate(companies_data.companies, 1): + print(f"{idx}. {company.name}: {company.url}") + else: + print("No companies were found in the extraction result") + + # XPath click + await stagehand.page.locator("xpath=/html/body/div/ul[2]/li[2]/a").click() + await stagehand.page.wait_for_load_state('networkidle') + console.print("[success]āœ“ Clicked element using XPath[/]") + + # Open a new page with Google + console.print("\n[info]Creating a new page...[/]") + new_page = await stagehand.context.new_page() + await new_page.goto("https://www.google.com") + console.print("[success]āœ“ Opened Google in a new page[/]") + + # Get accessibility tree for the new page + tree = await get_accessibility_tree(new_page, stagehand.logger) + console.print("[success]āœ“ Extracted accessibility tree for new page[/]") + + # Try clicking Get Started button on Google + await new_page.act("click the button with text 'Get Started'") + + # Only use LLM directly if in LOCAL mode + if stagehand.llm is not None: + console.print("[info]LLM client available - using direct LLM call[/]") + + # Use LLM to analyze the page + response = stagehand.llm.create_response( + messages=[ + { + "role": "system", + "content": "Based on the provided accessibility tree of the page, find the element and the action the user is expecting to perform. The tree consists of an enhanced a11y tree from a website with unique identifiers prepended to each element's role, and name. The actions you can take are playwright compatible locator actions." + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": f"fill the search bar with the text 'Hello'\nPage Tree:\n{tree.get('simplified')}" + } + ] + } + ], + model=model_name, + response_format=ElementAction, + ) + + action = ElementAction.model_validate_json(response.choices[0].message.content) + console.print(f"[success]āœ“ LLM identified element ID: {action.id}[/]") + + # Test CDP functionality + args = {"backendNodeId": action.id} + result = await new_page.send_cdp("DOM.resolveNode", args) + object_info = result.get("object") + print(object_info) + + xpath = await get_xpath_by_resolved_object_id(await new_page.get_cdp_client(), object_info["objectId"]) + console.print(f"[success]āœ“ Retrieved XPath: {xpath}[/]") + + # Interact with the element + if xpath: + await new_page.locator(f"xpath={xpath}").click() + await new_page.locator(f"xpath={xpath}").fill(action.arguments[0]) + console.print("[success]āœ“ Filled search bar with 'Hello'[/]") + else: + print("No xpath found") + else: + console.print("[warning]LLM client not available in BROWSERBASE mode - skipping direct LLM test[/]") + # Alternative: use page.observe to find the search bar + observe_result = await new_page.observe("the search bar or search input field") + console.print(f"[info]Observed search elements: {observe_result}[/]") + + # Use page.act to fill the search bar + try: + await new_page.act("fill the search bar with 'Hello'") + console.print("[success]āœ“ Filled search bar using act()[/]") + except Exception as e: + console.print(f"[warning]Could not fill search bar: {e}[/]") + + # Final test summary + console.print("\n[success]All tests completed successfully![/]") + + except Exception as e: + console.print(f"[error]Error during testing: {str(e)}[/]") + import traceback + traceback.print_exc() + raise + finally: + # Close the client + # wait for 5 seconds + await asyncio.sleep(5) + await stagehand.close() + console.print("[info]Stagehand async client closed[/]") if __name__ == "__main__": - # Add a fancy header - console.print( - "\n", - Panel.fit( - "[light_gray]Stagehand 🤘 Python Example[/]", - border_style="green", - padding=(1, 10), - ), - ) - asyncio.run(main()) + asyncio.run(main()) \ No newline at end of file diff --git a/examples/second_example.py b/examples/second_example.py new file mode 100644 index 00000000..f3b39f5f --- /dev/null +++ b/examples/second_example.py @@ -0,0 +1,138 @@ +import asyncio +import logging +import os +from rich.console import Console +from rich.panel import Panel +from rich.theme import Theme +import json +from dotenv import load_dotenv + +from stagehand import Stagehand, StagehandConfig +from stagehand.utils import configure_logging + +# Configure logging with cleaner format +configure_logging( + level=logging.INFO, + remove_logger_name=True, # Remove the redundant stagehand.client prefix + quiet_dependencies=True, # Suppress httpx and other noisy logs +) + +# Create a custom theme for consistent styling +custom_theme = Theme( + { + "info": "cyan", + "success": "green", + "warning": "yellow", + "error": "red bold", + "highlight": "magenta", + "url": "blue underline", + } +) + +# Create a Rich console instance with our theme +console = Console(theme=custom_theme) + +load_dotenv() + +console.print( + Panel.fit( + "[yellow]Logging Levels:[/]\n" + "[white]- Set [bold]verbose=0[/] for errors (ERROR)[/]\n" + "[white]- Set [bold]verbose=1[/] for minimal logs (INFO)[/]\n" + "[white]- Set [bold]verbose=2[/] for medium logs (WARNING)[/]\n" + "[white]- Set [bold]verbose=3[/] for detailed logs (DEBUG)[/]", + title="Verbosity Options", + border_style="blue", + ) +) + +async def main(): + # Build a unified configuration object for Stagehand + config = StagehandConfig( + env="BROWSERBASE", + api_key=os.getenv("BROWSERBASE_API_KEY"), + project_id=os.getenv("BROWSERBASE_PROJECT_ID"), + headless=False, + dom_settle_timeout_ms=3000, + model_name="google/gemini-2.0-flash", + self_heal=True, + wait_for_captcha_solves=True, + system_prompt="You are a browser automation assistant that helps users navigate websites effectively.", + model_client_options={"apiKey": os.getenv("MODEL_API_KEY")}, + # Use verbose=2 for medium-detail logs (1=minimal, 3=debug) + verbose=2, + ) + + stagehand = Stagehand(config, + api_url=os.getenv("STAGEHAND_SERVER_URL"), + env=os.getenv("STAGEHAND_ENV")) + + # Initialize - this creates a new session automatically. + console.print("\nšŸš€ [info]Initializing Stagehand...[/]") + await stagehand.init() + page = stagehand.page + console.print(f"\n[yellow]Created new session:[/] {stagehand.session_id}") + console.print( + f"🌐 [white]View your live browser:[/] [url]https://www.browserbase.com/sessions/{stagehand.session_id}[/]" + ) + + await asyncio.sleep(2) + + console.print("\nā–¶ļø [highlight] Navigating[/] to Google") + await page.goto("https://google.com/") + console.print("āœ… [success]Navigated to Google[/]") + + console.print("\nā–¶ļø [highlight] Clicking[/] on About link") + # Click on the "About" link using Playwright + await page.get_by_role("link", name="About", exact=True).click() + console.print("āœ… [success]Clicked on About link[/]") + + await asyncio.sleep(2) + console.print("\nā–¶ļø [highlight] Navigating[/] back to Google") + await page.goto("https://google.com/") + console.print("āœ… [success]Navigated back to Google[/]") + + console.print("\nā–¶ļø [highlight] Performing action:[/] search for openai") + await page.act("search for openai") + await page.keyboard.press("Enter") + console.print("āœ… [success]Performing Action:[/] Action completed successfully") + + await asyncio.sleep(2) + + console.print("\nā–¶ļø [highlight] Observing page[/] for news button") + observed = await page.observe("find all articles") + + if len(observed) > 0: + element = observed[0] + console.print("āœ… [success]Found element:[/] News button") + console.print("\nā–¶ļø [highlight] Performing action on observed element:") + console.print(element) + await page.act(element) + console.print("āœ… [success]Performing Action:[/] Action completed successfully") + + else: + console.print("āŒ [error]No element found[/]") + + console.print("\nā–¶ļø [highlight] Extracting[/] first search result") + data = await page.extract("extract the first result from the search") + console.print("šŸ“Š [info]Extracted data:[/]") + console.print_json(data=data.model_dump()) + + # Close the session + console.print("\nā¹ļø [warning]Closing session...[/]") + await stagehand.close() + console.print("āœ… [success]Session closed successfully![/]") + console.rule("[bold]End of Example[/]") + + +if __name__ == "__main__": + # Add a fancy header + console.print( + "\n", + Panel.fit( + "[light_gray]Stagehand 🤘 Python Example[/]", + border_style="green", + padding=(1, 10), + ), + ) + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 7080ba31..ce941c87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,9 @@ dependencies = [ "playwright>=1.42.1", "requests>=2.31.0", "browserbase>=1.4.0", + "anthropic>=0.52.2", + "openai>=1.83.0", + "litellm>=1.72.0" ] [project.optional-dependencies] diff --git a/stagehand/client.py b/stagehand/client.py index 53c28611..d771f671 100644 --- a/stagehand/client.py +++ b/stagehand/client.py @@ -123,7 +123,27 @@ def __init__( self.wait_for_captcha_solves = self.config.wait_for_captcha_solves self.system_prompt = self.config.system_prompt self.verbose = self.config.verbose - self.env = self.config.env.upper() if self.config.env else "BROWSERBASE" + + # Smart environment detection + if self.config.env: + self.env = self.config.env.upper() + else: + # Auto-detect environment based on available configuration + has_browserbase_config = bool( + self.browserbase_api_key and self.browserbase_project_id + ) + has_local_config = bool(self.config.local_browser_launch_options) + + if has_local_config and not has_browserbase_config: + # Local browser options specified but no Browserbase config + self.env = "LOCAL" + elif not has_browserbase_config and not has_local_config: + # No configuration specified, default to LOCAL for easier local development + self.env = "LOCAL" + else: + # Default to BROWSERBASE if Browserbase config is available + self.env = "BROWSERBASE" + self.local_browser_launch_options = ( self.config.local_browser_launch_options or {} ) @@ -232,9 +252,14 @@ def cleanup_handler(sig, frame): return self.__class__._cleanup_called = True - print( - f"\n[{signal.Signals(sig).name}] received. Ending Browserbase session..." - ) + if self.env == "BROWSERBASE": + print( + f"\n[{signal.Signals(sig).name}] received. Ending Browserbase session..." + ) + else: + print( + f"\n[{signal.Signals(sig).name}] received. Cleaning up Stagehand resources..." + ) try: # Try to get the current event loop @@ -273,9 +298,15 @@ async def _async_cleanup(self): """Async cleanup method called from signal handler.""" try: await self.close() - print(f"Session {self.session_id} ended successfully") + if self.env == "BROWSERBASE" and self.session_id: + print(f"Session {self.session_id} ended successfully") + else: + print("Stagehand resources cleaned up successfully") except Exception as e: - print(f"Error ending Browserbase session: {str(e)}") + if self.env == "BROWSERBASE": + print(f"Error ending Browserbase session: {str(e)}") + else: + print(f"Error cleaning up Stagehand resources: {str(e)}") finally: # Force exit after cleanup completes (or fails) # Use os._exit to avoid any further Python cleanup that might hang diff --git a/stagehand/utils.py b/stagehand/utils.py index ef45d954..0d3817f5 100644 --- a/stagehand/utils.py +++ b/stagehand/utils.py @@ -856,6 +856,9 @@ def transform_model(model_cls, path=[]): # noqa: F841 B006 Returns: Tuple of (transformed_model_cls, url_paths) """ + if path is None: + path = [] + # Get model fields based on Pydantic version try: # Pydantic V2 approach