44from rich .console import Console
55from rich .panel import Panel
66from rich .theme import Theme
7- from pydantic import BaseModel , Field , HttpUrl
7+ import json
88from dotenv import load_dotenv
9- import time
109
11- from stagehand import StagehandConfig , Stagehand
10+ from stagehand import Stagehand , StagehandConfig
1211from stagehand .utils import configure_logging
13- from stagehand .schemas import ObserveOptions , ActOptions , ExtractOptions
14- from stagehand .a11y .utils import get_accessibility_tree , get_xpath_by_resolved_object_id
1512
16- # Load environment variables
17- load_dotenv ()
13+ # Configure logging with cleaner format
14+ configure_logging (
15+ level = logging .INFO ,
16+ remove_logger_name = True , # Remove the redundant stagehand.client prefix
17+ quiet_dependencies = True , # Suppress httpx and other noisy logs
18+ )
1819
19- # Configure Rich console
20- console = Console (theme = Theme ({
21- "info" : "cyan" ,
22- "success" : "green" ,
23- "warning" : "yellow" ,
24- "error" : "red bold" ,
25- "highlight" : "magenta" ,
26- "url" : "blue underline" ,
27- }))
28-
29- # Define Pydantic models for testing
30- class Company (BaseModel ):
31- name : str = Field (..., description = "The name of the company" )
32- # todo - URL needs to be pydantic type HttpUrl otherwise it does not extract the URL
33- url : HttpUrl = Field (..., description = "The URL of the company website or relevant page" )
34-
35- class Companies (BaseModel ):
36- companies : list [Company ] = Field (..., description = "List of companies extracted from the page, maximum of 5 companies" )
20+ # Create a custom theme for consistent styling
21+ custom_theme = Theme (
22+ {
23+ "info" : "cyan" ,
24+ "success" : "green" ,
25+ "warning" : "yellow" ,
26+ "error" : "red bold" ,
27+ "highlight" : "magenta" ,
28+ "url" : "blue underline" ,
29+ }
30+ )
3731
38- class ElementAction (BaseModel ):
39- action : str
40- id : int
41- arguments : list [str ]
32+ # Create a Rich console instance with our theme
33+ console = Console (theme = custom_theme )
4234
43- async def main ():
44- # Display header
45- console .print (
46- "\n " ,
47- Panel .fit (
48- "[light_gray]New Stagehand 🤘 Python Test[/]" ,
49- border_style = "green" ,
50- padding = (1 , 10 ),
51- ),
52- )
35+ load_dotenv ()
5336
54- # Create configuration
55- model_name = "google/gemini-2.5-flash-preview-04-17"
37+ console .print (
38+ Panel .fit (
39+ "[yellow]Logging Levels:[/]\n "
40+ "[white]- Set [bold]verbose=0[/] for errors (ERROR)[/]\n "
41+ "[white]- Set [bold]verbose=1[/] for minimal logs (INFO)[/]\n "
42+ "[white]- Set [bold]verbose=2[/] for medium logs (WARNING)[/]\n "
43+ "[white]- Set [bold]verbose=3[/] for detailed logs (DEBUG)[/]" ,
44+ title = "Verbosity Options" ,
45+ border_style = "blue" ,
46+ )
47+ )
5648
49+ async def main ():
50+ # Build a unified configuration object for Stagehand
5751 config = StagehandConfig (
52+ env = "BROWSERBASE" ,
5853 api_key = os .getenv ("BROWSERBASE_API_KEY" ),
5954 project_id = os .getenv ("BROWSERBASE_PROJECT_ID" ),
60- model_name = model_name , # todo - unify gemini/google model names
61- model_client_options = {"apiKey" : os .getenv ("MODEL_API_KEY" )}, # this works locally even if there is a model provider mismatch
62- verbose = 3 ,
55+ headless = False ,
56+ dom_settle_timeout_ms = 3000 ,
57+ model_name = "google/gemini-2.0-flash" ,
58+ self_heal = True ,
59+ wait_for_captcha_solves = True ,
60+ system_prompt = "You are a browser automation assistant that helps users navigate websites effectively." ,
61+ model_client_options = {"apiKey" : os .getenv ("MODEL_API_KEY" )},
62+ # Use verbose=2 for medium-detail logs (1=minimal, 3=debug)
63+ verbose = 2 ,
6364 )
64-
65- # Initialize async client
66- stagehand = Stagehand (
67- env = os .getenv ("STAGEHAND_ENV" ),
68- config = config ,
69- api_url = os .getenv ("STAGEHAND_SERVER_URL" ),
65+
66+ stagehand = Stagehand (config )
67+
68+ # Initialize - this creates a new session automatically.
69+ console .print ("\n 🚀 [info]Initializing Stagehand...[/]" )
70+ await stagehand .init ()
71+ page = stagehand .page
72+ console .print (f"\n [yellow]Created new session:[/] { stagehand .session_id } " )
73+ console .print (
74+ f"🌐 [white]View your live browser:[/] [url]https://www.browserbase.com/sessions/{ stagehand .session_id } [/]"
7075 )
76+
77+ await asyncio .sleep (2 )
78+
79+ console .print ("\n ▶️ [highlight] Navigating[/] to Google" )
80+ await page .goto ("https://google.com/" )
81+ console .print ("✅ [success]Navigated to Google[/]" )
82+
83+ console .print ("\n ▶️ [highlight] Clicking[/] on About link" )
84+ # Click on the "About" link using Playwright
85+ await page .get_by_role ("link" , name = "About" , exact = True ).click ()
86+ console .print ("✅ [success]Clicked on About link[/]" )
87+
88+ await asyncio .sleep (2 )
89+ console .print ("\n ▶️ [highlight] Navigating[/] back to Google" )
90+ await page .goto ("https://google.com/" )
91+ console .print ("✅ [success]Navigated back to Google[/]" )
92+
93+ console .print ("\n ▶️ [highlight] Performing action:[/] search for openai" )
94+ await page .act ("search for openai" )
95+ await page .keyboard .press ("Enter" )
96+ console .print ("✅ [success]Performing Action:[/] Action completed successfully" )
7197
72- try :
73- # Initialize the client
74- await stagehand .init ()
75- console .print ("[success]✓ Successfully initialized Stagehand async client[/]" )
76- console .print (f"[info]Environment: { stagehand .env } [/]" )
77- console .print (f"[info]LLM Client Available: { stagehand .llm is not None } [/]" )
78-
79- # Navigate to AIgrant (as in the original test)
80- await stagehand .page .goto ("https://www.aigrant.com" )
81- console .print ("[success]✓ Navigated to AIgrant[/]" )
82- await asyncio .sleep (2 )
83-
84- # Get accessibility tree
85- tree = await get_accessibility_tree (stagehand .page , stagehand .logger )
86- console .print ("[success]✓ Extracted accessibility tree[/]" )
87-
88- print ("ID to URL mapping:" , tree .get ("idToUrl" ))
89- print ("IFrames:" , tree .get ("iframes" ))
90-
91- # Click the "Get Started" button
92- await stagehand .page .act ("click the button with text 'Get Started'" )
93- console .print ("[success]✓ Clicked 'Get Started' button[/]" )
94-
95- # Observe the button
96- await stagehand .page .observe ("the button with text 'Get Started'" )
97- console .print ("[success]✓ Observed 'Get Started' button[/]" )
98-
99- # Extract companies using schema
100- extract_options = ExtractOptions (
101- instruction = "Extract the names and URLs of up to 5 companies mentioned on this page" ,
102- schema_definition = Companies
103- )
104-
105- extract_result = await stagehand .page .extract (extract_options )
106- console .print ("[success]✓ Extracted companies data[/]" )
107-
108- # Display results
109- print ("Extract result:" , extract_result )
110- print ("Extract result data:" , extract_result .data if hasattr (extract_result , 'data' ) else 'No data field' )
111-
112- # Parse the result into the Companies model
113- companies_data = None
114-
115- # Handle different result formats between LOCAL and BROWSERBASE
116- if hasattr (extract_result , 'data' ) and extract_result .data :
117- # BROWSERBASE mode - data is in the 'data' field
118- try :
119- raw_data = extract_result .data
120- console .print (f"[info]Raw extract data: { raw_data } [/]" )
121-
122- # Check if the data needs URL resolution from ID mapping
123- if isinstance (raw_data , dict ) and 'companies' in raw_data :
124- id_to_url = tree .get ("idToUrl" , {})
125- for company in raw_data ['companies' ]:
126- if 'url' in company and isinstance (company ['url' ], str ):
127- # Check if URL is just an ID that needs to be resolved
128- if company ['url' ].isdigit () and company ['url' ] in id_to_url :
129- company ['url' ] = id_to_url [company ['url' ]]
130- console .print (f"[success]✓ Resolved URL for { company ['name' ]} : { company ['url' ]} [/]" )
131-
132- companies_data = Companies .model_validate (raw_data )
133- console .print ("[success]✓ Successfully parsed extract result into Companies model[/]" )
134- except Exception as e :
135- console .print (f"[error]Failed to parse extract result: { e } [/]" )
136- print ("Raw data:" , extract_result .data )
137- elif hasattr (extract_result , 'companies' ):
138- # LOCAL mode - companies field is directly available
139- try :
140- companies_data = Companies .model_validate (extract_result .model_dump ())
141- console .print ("[success]✓ Successfully parsed extract result into Companies model[/]" )
142- except Exception as e :
143- console .print (f"[error]Failed to parse extract result: { e } [/]" )
144- print ("Raw companies data:" , extract_result .companies )
145-
146- print ("\n Extracted Companies:" )
147- if companies_data and hasattr (companies_data , "companies" ):
148- for idx , company in enumerate (companies_data .companies , 1 ):
149- print (f"{ idx } . { company .name } : { company .url } " )
150- else :
151- print ("No companies were found in the extraction result" )
152-
153- # XPath click
154- await stagehand .page .locator ("xpath=/html/body/div/ul[2]/li[2]/a" ).click ()
155- await stagehand .page .wait_for_load_state ('networkidle' )
156- console .print ("[success]✓ Clicked element using XPath[/]" )
157-
158- # Open a new page with Google
159- console .print ("\n [info]Creating a new page...[/]" )
160- new_page = await stagehand .context .new_page ()
161- await new_page .goto ("https://www.google.com" )
162- console .print ("[success]✓ Opened Google in a new page[/]" )
163-
164- # Get accessibility tree for the new page
165- tree = await get_accessibility_tree (new_page , stagehand .logger )
166- console .print ("[success]✓ Extracted accessibility tree for new page[/]" )
167-
168- # Try clicking Get Started button on Google
169- await new_page .act ("click the button with text 'Get Started'" )
170-
171- # Only use LLM directly if in LOCAL mode
172- if stagehand .llm is not None :
173- console .print ("[info]LLM client available - using direct LLM call[/]" )
174-
175- # Use LLM to analyze the page
176- response = stagehand .llm .create_response (
177- messages = [
178- {
179- "role" : "system" ,
180- "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."
181- },
182- {
183- "role" : "user" ,
184- "content" : [
185- {
186- "type" : "text" ,
187- "text" : f"fill the search bar with the text 'Hello'\n Page Tree:\n { tree .get ('simplified' )} "
188- }
189- ]
190- }
191- ],
192- model = model_name ,
193- response_format = ElementAction ,
194- )
195-
196- action = ElementAction .model_validate_json (response .choices [0 ].message .content )
197- console .print (f"[success]✓ LLM identified element ID: { action .id } [/]" )
198-
199- # Test CDP functionality
200- args = {"backendNodeId" : action .id }
201- result = await new_page .send_cdp ("DOM.resolveNode" , args )
202- object_info = result .get ("object" )
203- print (object_info )
204-
205- xpath = await get_xpath_by_resolved_object_id (await new_page .get_cdp_client (), object_info ["objectId" ])
206- console .print (f"[success]✓ Retrieved XPath: { xpath } [/]" )
207-
208- # Interact with the element
209- if xpath :
210- await new_page .locator (f"xpath={ xpath } " ).click ()
211- await new_page .locator (f"xpath={ xpath } " ).fill (action .arguments [0 ])
212- console .print ("[success]✓ Filled search bar with 'Hello'[/]" )
213- else :
214- print ("No xpath found" )
215- else :
216- console .print ("[warning]LLM client not available in BROWSERBASE mode - skipping direct LLM test[/]" )
217- # Alternative: use page.observe to find the search bar
218- observe_result = await new_page .observe ("the search bar or search input field" )
219- console .print (f"[info]Observed search elements: { observe_result } [/]" )
220-
221- # Use page.act to fill the search bar
222- try :
223- await new_page .act ("fill the search bar with 'Hello'" )
224- console .print ("[success]✓ Filled search bar using act()[/]" )
225- except Exception as e :
226- console .print (f"[warning]Could not fill search bar: { e } [/]" )
227-
228- # Final test summary
229- console .print ("\n [success]All tests completed successfully![/]" )
230-
231- except Exception as e :
232- console .print (f"[error]Error during testing: { str (e )} [/]" )
233- import traceback
234- traceback .print_exc ()
235- raise
236- finally :
237- # Close the client
238- # wait for 5 seconds
239- await asyncio .sleep (5 )
240- await stagehand .close ()
241- console .print ("[info]Stagehand async client closed[/]" )
98+ await asyncio .sleep (2 )
99+
100+ console .print ("\n ▶️ [highlight] Observing page[/] for news button" )
101+ observed = await page .observe ("find all articles" )
102+
103+ if len (observed ) > 0 :
104+ element = observed [0 ]
105+ console .print ("✅ [success]Found element:[/] News button" )
106+ console .print ("\n ▶️ [highlight] Performing action on observed element:" )
107+ console .print (element )
108+ await page .act (element )
109+ console .print ("✅ [success]Performing Action:[/] Action completed successfully" )
110+
111+ else :
112+ console .print ("❌ [error]No element found[/]" )
113+
114+ console .print ("\n ▶️ [highlight] Extracting[/] first search result" )
115+ data = await page .extract ("extract the first result from the search" )
116+ console .print ("📊 [info]Extracted data:[/]" )
117+ console .print_json (f"{ data .model_dump_json ()} " )
118+
119+ # Close the session
120+ console .print ("\n ⏹️ [warning]Closing session...[/]" )
121+ await stagehand .close ()
122+ console .print ("✅ [success]Session closed successfully![/]" )
123+ console .rule ("[bold]End of Example[/]" )
124+
242125
243126if __name__ == "__main__" :
244- asyncio .run (main ())
127+ # Add a fancy header
128+ console .print (
129+ "\n " ,
130+ Panel .fit (
131+ "[light_gray]Stagehand 🤘 Python Example[/]" ,
132+ border_style = "green" ,
133+ padding = (1 , 10 ),
134+ ),
135+ )
136+ asyncio .run (main ())
137+
0 commit comments