4
4
from rich .console import Console
5
5
from rich .panel import Panel
6
6
from rich .theme import Theme
7
- from pydantic import BaseModel , Field , HttpUrl
7
+ import json
8
8
from dotenv import load_dotenv
9
- import time
10
9
11
- from stagehand import StagehandConfig , Stagehand
10
+ from stagehand import Stagehand , StagehandConfig
12
11
from 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
15
12
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
+ )
18
19
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
+ )
37
31
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 )
42
34
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 ()
53
36
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
+ )
56
48
49
+ async def main ():
50
+ # Build a unified configuration object for Stagehand
57
51
config = StagehandConfig (
52
+ env = "BROWSERBASE" ,
58
53
api_key = os .getenv ("BROWSERBASE_API_KEY" ),
59
54
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 ,
63
64
)
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 } [/]"
70
75
)
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" )
71
97
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
+
242
125
243
126
if __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