playwright headless fix
frdel committed
Jun 11, 2025 at 13:58 UTC
aadc111b312179c78815bba7bacd6fe630f33204
5 files changed
+467
-409
docker/run/fs/ins/install_playwright.sh
+4
-1
@@ -6,11 +6,14 @@
6
# install playwright if not installed (should be from requirements.txt)
7
uv pip install playwright
8
9
+# set PW installation path to /a0/tmp/playwright
10
+export PLAYWRIGHT_BROWSERS_PATH=/a0/tmp/playwright
11
+
12
# install chromium with dependencies
13
# for kali-based
14
# if [ "$@" = "hacking" ]; then
15
apt-get install -y fonts-unifont libnss3 libnspr4 libatk1.0-0 libatspi2.0-0 libxcomposite1 libxdamage1 libatk-bridge2.0-0 libcups2
13
- playwright install chromium-headless-shell
16
+ playwright install chromium --only-shell
17
# else
18
# # for debian based
19
# playwright install --with-deps chromium
python/helpers/browser.py
+385
-385
@@ -1,385 +1,385 @@
1
-import asyncio
2
-import re
3
-from bs4 import BeautifulSoup
4
-from playwright.async_api import (
5
- async_playwright,
6
- Browser as PlaywrightBrowser,
7
- Page,
8
- Frame,
9
- BrowserContext,
10
-)
11
-
12
-from python.helpers import files
13
-
14
-
15
-class NoPageError(Exception):
16
- pass
17
-
18
-
19
-class Browser:
20
-
21
- load_timeout = 10000
22
- interact_timeout = 3000
23
- selector_name = "data-a0sel3ct0r"
24
-
25
- def __init__(self, headless=True):
26
- self.browser: PlaywrightBrowser = None # type: ignore
27
- self.context: BrowserContext = None # type: ignore
28
- self.page: Page = None # type: ignore
29
- self._playwright = None
30
- self.headless = headless
31
- self.contexts = {}
32
- self.last_selector = ""
33
- self.page_loaded = False
34
- self.navigation_count = 0
35
-
36
- async def __aenter__(self):
37
- await self.start()
38
- return self
39
-
40
- async def __aexit__(self, exc_type, exc_val, exc_tb):
41
- await self.close()
42
-
43
- async def start(self):
44
- """Start browser session"""
45
- self._playwright = await async_playwright().start()
46
- if not self.browser:
47
- self.browser = await self._playwright.chromium.launch(
48
- headless=self.headless, args=["--disable-http2"]
49
- )
50
- if not self.context:
51
- self.context = await self.browser.new_context(
52
- user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.141 Safari/537.36"
53
- )
54
-
55
- self.page = await self.context.new_page()
56
- await self.page.set_viewport_size({"width": 1200, "height": 1200})
57
-
58
- # Inject the JavaScript to modify the attachShadow method
59
- js_override = files.read_file("lib/browser/init_override.js")
60
- await self.page.add_init_script(js_override)
61
-
62
- # Setup frame handling
63
- async def inject_script_into_frames(frame):
64
- try:
65
- await self.wait_tick()
66
- if not frame.is_detached():
67
- async with asyncio.timeout(0.25):
68
- await frame.evaluate(js_override)
69
- print(f"Injected script into frame: {frame.url[:100]}")
70
- except Exception as e:
71
- # Frame might have been detached during injection, which is normal
72
- print(
73
- f"Could not inject into frame (possibly detached): {str(e)[:100]}"
74
- )
75
-
76
- self.page.on(
77
- "frameattached",
78
- lambda frame: asyncio.ensure_future(inject_script_into_frames(frame)),
79
- )
80
-
81
- # Handle page navigation events
82
- async def handle_navigation(frame):
83
- if frame == self.page.main_frame:
84
- print(f"Page navigated to: {frame.url[:100]}")
85
- self.page_loaded = False
86
- self.navigation_count += 1
87
-
88
- async def handle_load(dummy):
89
- print("Page load completed")
90
- self.page_loaded = True
91
-
92
- async def handle_request(request):
93
- if (
94
- request.is_navigation_request()
95
- and request.frame == self.page.main_frame
96
- ):
97
- print(f"Navigation started to: {request.url[:100]}")
98
- self.page_loaded = False
99
- self.navigation_count += 1
100
-
101
- self.page.on("request", handle_request)
102
- self.page.on("framenavigated", handle_navigation)
103
- self.page.on("load", handle_load)
104
-
105
- async def close(self):
106
- """Close browser session"""
107
- if self.browser:
108
- await self.browser.close()
109
- if self._playwright:
110
- await self._playwright.stop()
111
-
112
- async def open(self, url: str):
113
- """Open a URL in the browser"""
114
- self.last_selector = ""
115
- self.contexts = {}
116
- if self.page:
117
- await self.page.close()
118
- await self.start()
119
- try:
120
- await self.page.goto(
121
- url, wait_until="networkidle", timeout=Browser.load_timeout
122
- )
123
- except TimeoutError as e:
124
- pass
125
- except Exception as e:
126
- print(f"Error opening page: {e}")
127
- raise e
128
- await self.wait_tick()
129
-
130
- async def get_full_dom(self) -> str:
131
- """Get full DOM with unique selectors"""
132
- await self._check_page()
133
- js_code = files.read_file("lib/browser/extract_dom.js")
134
-
135
- # Get all frames
136
- self.contexts = {}
137
- frame_contents = {}
138
-
139
- # Extract content from each frame
140
- i = -1
141
- for frame in self.page.frames:
142
- try:
143
- if frame.url: # and frame != self.page.main_frame:
144
- i += 1
145
- frame_mark = self._num_to_alpha(i)
146
-
147
- # Check if frame is still valid
148
- await self.wait_tick()
149
- if not frame.is_detached():
150
- try:
151
- # short timeout to identify and skip unresponsive frames
152
- async with asyncio.timeout(0.25):
153
- await frame.evaluate("window.location.href")
154
- except TimeoutError as e:
155
- print(f"Skipping unresponsive frame: {frame.url}")
156
- continue
157
-
158
- await frame.wait_for_load_state(
159
- "domcontentloaded", timeout=1000
160
- )
161
-
162
- async with asyncio.timeout(1):
163
- content = await frame.evaluate(
164
- js_code, [frame_mark, self.selector_name]
165
- )
166
- self.contexts[frame_mark] = frame
167
- frame_contents[frame.url] = content
168
- else:
169
- print(f"Warning: Frame was detached: {frame.url}")
170
- except Exception as e:
171
- print(f"Error extracting from frame {frame.url}: {e}")
172
-
173
- # # Get main frame content
174
- # main_mark = self._num_to_alpha(0)
175
- # main_content = ""
176
- # try:
177
- # async with asyncio.timeout(1):
178
- # main_content = await self.page.evaluate(js_code, [main_mark, self.selector_name])
179
- # self.contexts[main_mark] = self.page
180
- # except Exception as e:
181
- # print(f"Error when extracting from main frame: {e}")
182
-
183
- # Replace iframe placeholders with actual content
184
- # for url, content in frame_contents.items():
185
- # placeholder = f'<iframe src="{url}"'
186
- # main_content = main_content.replace(placeholder, f'{placeholder}>\n<!-- IFrame Content Start -->\n{content}\n<!-- IFrame Content End -->\n</iframe')
187
-
188
- # return main_content + "".join(frame_contents.values())
189
- return "".join(frame_contents.values())
190
-
191
- def strip_html_dom(self, html_content: str) -> str:
192
- """Clean and strip HTML content"""
193
- if not html_content:
194
- return ""
195
-
196
- soup = BeautifulSoup(html_content, "html.parser")
197
-
198
- for tag in soup.find_all(
199
- ["br", "hr", "style", "script", "noscript", "meta", "link", "svg"]
200
- ):
201
- tag.decompose()
202
-
203
- for tag in soup.find_all(True):
204
- if tag.attrs and "invisible" in tag.attrs:
205
- tag.decompose()
206
-
207
- for tag in soup.find_all(True):
208
- allowed_attrs = [
209
- self.selector_name,
210
- "aria-label",
211
- "placeholder",
212
- "name",
213
- "value",
214
- "type",
215
- ]
216
- attrs = {
217
- "selector" if key == self.selector_name else key: tag.attrs[key]
218
- for key in allowed_attrs
219
- if key in tag.attrs and tag.attrs[key]
220
- }
221
- tag.attrs = attrs
222
-
223
- def remove_empty(tag_name: str) -> None:
224
- for tag in soup.find_all(tag_name):
225
- if not tag.attrs:
226
- tag.unwrap()
227
-
228
- remove_empty("span")
229
- remove_empty("p")
230
- remove_empty("strong")
231
-
232
- return soup.prettify(formatter="minimal")
233
-
234
- def process_html_with_selectors(self, html_content: str) -> str:
235
- """Process HTML content and add selectors to interactive elements"""
236
- if not html_content:
237
- return ""
238
-
239
- html_content = re.sub(r"\s+", " ", html_content)
240
- soup = BeautifulSoup(html_content, "html.parser")
241
-
242
- structural_tags = [
243
- "html",
244
- "head",
245
- "body",
246
- "div",
247
- "span",
248
- "section",
249
- "main",
250
- "article",
251
- "header",
252
- "footer",
253
- "nav",
254
- "ul",
255
- "ol",
256
- "li",
257
- "tr",
258
- "td",
259
- "th",
260
- ]
261
- for tag in structural_tags:
262
- for element in soup.find_all(tag):
263
- element.unwrap()
264
-
265
- out = str(soup).strip()
266
- out = re.sub(r">\s*<", "><", out)
267
- out = re.sub(r'aria-label="', 'label="', out)
268
-
269
- # out = re.sub(r'selector="(\d+[a-zA-Z]+)"', r'selector=\1', out)
270
- return out
271
-
272
- async def get_clean_dom(self) -> str:
273
- """Get clean DOM with selectors"""
274
- full_dom = await self.get_full_dom()
275
- clean_dom = self.strip_html_dom(full_dom)
276
- return self.process_html_with_selectors(clean_dom)
277
-
278
- async def click(self, selector: str):
279
- await self._check_page()
280
- ctx, selector = self._parse_selector(selector)
281
- self.last_selector = selector
282
- # js_code = files.read_file("lib/browser/click.js")
283
- # result = await self.page.evaluate(js_code, [selector])
284
- # if not result:
285
- result = await ctx.hover(selector, force=True, timeout=Browser.interact_timeout)
286
- await self.wait_tick()
287
- result = await ctx.click(selector, force=True, timeout=Browser.interact_timeout)
288
- await self.wait_tick()
289
-
290
- # await self.page.wait_for_load_state("networkidle")
291
- return result
292
-
293
- async def press(self, key: str):
294
- await self._check_page()
295
- if self.last_selector:
296
- await self.page.press(
297
- self.last_selector, key, timeout=Browser.interact_timeout
298
- )
299
- else:
300
- await self.page.keyboard.press(key)
301
-
302
- async def fill(self, selector: str, text: str):
303
- await self._check_page()
304
- ctx, selector = self._parse_selector(selector)
305
- self.last_selector = selector
306
- try:
307
- await self.click(selector)
308
- except Exception as e:
309
- pass
310
- await ctx.fill(selector, text, force=True, timeout=Browser.interact_timeout)
311
- await self.wait_tick()
312
-
313
- async def execute(self, js_code: str):
314
- await self._check_page()
315
- result = await self.page.evaluate(js_code)
316
- return result
317
-
318
- async def screenshot(self, path: str, full_page=False):
319
- await self._check_page()
320
- await self.page.screenshot(path=path, full_page=full_page)
321
-
322
- def _parse_selector(self, selector: str) -> tuple[Page | Frame, str]:
323
- try:
324
- ctx = self.page
325
- # Check if selector is our UID, return
326
- if re.match(r"^\d+[a-zA-Z]+$", selector):
327
- alpha_part = "".join(filter(str.isalpha, selector))
328
- ctx = self.contexts[alpha_part]
329
- selector = f"[{self.selector_name}='{selector}']"
330
- return (ctx, selector)
331
- except Exception as e:
332
- raise Exception(f"Error evaluating selector: {selector}")
333
-
334
- async def _check_page(self):
335
- for _ in range(2):
336
- try:
337
- await self.wait_tick()
338
- self.page = self.context.pages[0]
339
- if not self.page:
340
- raise NoPageError(
341
- "No page is open in the browser. Please open a URL first."
342
- )
343
- # await self.page.wait_for_load_state("networkidle",)
344
- async with asyncio.timeout(self.load_timeout / 1000):
345
- if not self.page_loaded:
346
- while not self.page_loaded:
347
- await asyncio.sleep(0.1)
348
- await self.wait_tick()
349
- return
350
- except TimeoutError as e:
351
- self.page_loaded = True
352
- return
353
- except NoPageError as e:
354
- raise e
355
- except Exception as e:
356
- print(f"Error checking page: {e}")
357
-
358
- def _num_to_alpha(self, num: int) -> str:
359
- if num < 0:
360
- return ""
361
-
362
- result = ""
363
- while num >= 0:
364
- result = chr(num % 26 + 97) + result
365
- num = num // 26 - 1
366
-
367
- return result
368
-
369
- async def wait_tick(self):
370
- if self.page:
371
- await self.page.evaluate("window.location.href")
372
-
373
- async def wait(self, seconds: float = 1.0):
374
- await asyncio.sleep(seconds)
375
- await self.wait_tick()
376
-
377
- async def wait_for_action(self):
378
- nav_count = self.navigation_count
379
- for _ in range(5):
380
- await self._check_page()
381
- if nav_count != self.navigation_count:
382
- print("Navigation detected")
383
- await asyncio.sleep(1)
384
- return
385
- await asyncio.sleep(0.1)
1
+# import asyncio
2
+# import re
3
+# from bs4 import BeautifulSoup
4
+# from playwright.async_api import (
5
+# async_playwright,
6
+# Browser as PlaywrightBrowser,
7
+# Page,
8
+# Frame,
9
+# BrowserContext,
10
+# )
11
+
12
+# from python.helpers import files
13
+
14
+
15
+# class NoPageError(Exception):
16
+# pass
17
+
18
+
19
+# class Browser:
20
+
21
+# load_timeout = 10000
22
+# interact_timeout = 3000
23
+# selector_name = "data-a0sel3ct0r"
24
+
25
+# def __init__(self, headless=True):
26
+# self.browser: PlaywrightBrowser = None # type: ignore
27
+# self.context: BrowserContext = None # type: ignore
28
+# self.page: Page = None # type: ignore
29
+# self._playwright = None
30
+# self.headless = headless
31
+# self.contexts = {}
32
+# self.last_selector = ""
33
+# self.page_loaded = False
34
+# self.navigation_count = 0
35
+
36
+# async def __aenter__(self):
37
+# await self.start()
38
+# return self
39
+
40
+# async def __aexit__(self, exc_type, exc_val, exc_tb):
41
+# await self.close()
42
+
43
+# async def start(self):
44
+# """Start browser session"""
45
+# self._playwright = await async_playwright().start()
46
+# if not self.browser:
47
+# self.browser = await self._playwright.chromium.launch(
48
+# headless=self.headless, args=["--disable-http2"]
49
+# )
50
+# if not self.context:
51
+# self.context = await self.browser.new_context(
52
+# user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.141 Safari/537.36"
53
+# )
54
+
55
+# self.page = await self.context.new_page()
56
+# await self.page.set_viewport_size({"width": 1200, "height": 1200})
57
+
58
+# # Inject the JavaScript to modify the attachShadow method
59
+# js_override = files.read_file("lib/browser/init_override.js")
60
+# await self.page.add_init_script(js_override)
61
+
62
+# # Setup frame handling
63
+# async def inject_script_into_frames(frame):
64
+# try:
65
+# await self.wait_tick()
66
+# if not frame.is_detached():
67
+# async with asyncio.timeout(0.25):
68
+# await frame.evaluate(js_override)
69
+# print(f"Injected script into frame: {frame.url[:100]}")
70
+# except Exception as e:
71
+# # Frame might have been detached during injection, which is normal
72
+# print(
73
+# f"Could not inject into frame (possibly detached): {str(e)[:100]}"
74
+# )
75
+
76
+# self.page.on(
77
+# "frameattached",
78
+# lambda frame: asyncio.ensure_future(inject_script_into_frames(frame)),
79
+# )
80
+
81
+# # Handle page navigation events
82
+# async def handle_navigation(frame):
83
+# if frame == self.page.main_frame:
84
+# print(f"Page navigated to: {frame.url[:100]}")
85
+# self.page_loaded = False
86
+# self.navigation_count += 1
87
+
88
+# async def handle_load(dummy):
89
+# print("Page load completed")
90
+# self.page_loaded = True
91
+
92
+# async def handle_request(request):
93
+# if (
94
+# request.is_navigation_request()
95
+# and request.frame == self.page.main_frame
96
+# ):
97
+# print(f"Navigation started to: {request.url[:100]}")
98
+# self.page_loaded = False
99
+# self.navigation_count += 1
100
+
101
+# self.page.on("request", handle_request)
102
+# self.page.on("framenavigated", handle_navigation)
103
+# self.page.on("load", handle_load)
104
+
105
+# async def close(self):
106
+# """Close browser session"""
107
+# if self.browser:
108
+# await self.browser.close()
109
+# if self._playwright:
110
+# await self._playwright.stop()
111
+
112
+# async def open(self, url: str):
113
+# """Open a URL in the browser"""
114
+# self.last_selector = ""
115
+# self.contexts = {}
116
+# if self.page:
117
+# await self.page.close()
118
+# await self.start()
119
+# try:
120
+# await self.page.goto(
121
+# url, wait_until="networkidle", timeout=Browser.load_timeout
122
+# )
123
+# except TimeoutError as e:
124
+# pass
125
+# except Exception as e:
126
+# print(f"Error opening page: {e}")
127
+# raise e
128
+# await self.wait_tick()
129
+
130
+# async def get_full_dom(self) -> str:
131
+# """Get full DOM with unique selectors"""
132
+# await self._check_page()
133
+# js_code = files.read_file("lib/browser/extract_dom.js")
134
+
135
+# # Get all frames
136
+# self.contexts = {}
137
+# frame_contents = {}
138
+
139
+# # Extract content from each frame
140
+# i = -1
141
+# for frame in self.page.frames:
142
+# try:
143
+# if frame.url: # and frame != self.page.main_frame:
144
+# i += 1
145
+# frame_mark = self._num_to_alpha(i)
146
+
147
+# # Check if frame is still valid
148
+# await self.wait_tick()
149
+# if not frame.is_detached():
150
+# try:
151
+# # short timeout to identify and skip unresponsive frames
152
+# async with asyncio.timeout(0.25):
153
+# await frame.evaluate("window.location.href")
154
+# except TimeoutError as e:
155
+# print(f"Skipping unresponsive frame: {frame.url}")
156
+# continue
157
+
158
+# await frame.wait_for_load_state(
159
+# "domcontentloaded", timeout=1000
160
+# )
161
+
162
+# async with asyncio.timeout(1):
163
+# content = await frame.evaluate(
164
+# js_code, [frame_mark, self.selector_name]
165
+# )
166
+# self.contexts[frame_mark] = frame
167
+# frame_contents[frame.url] = content
168
+# else:
169
+# print(f"Warning: Frame was detached: {frame.url}")
170
+# except Exception as e:
171
+# print(f"Error extracting from frame {frame.url}: {e}")
172
+
173
+# # # Get main frame content
174
+# # main_mark = self._num_to_alpha(0)
175
+# # main_content = ""
176
+# # try:
177
+# # async with asyncio.timeout(1):
178
+# # main_content = await self.page.evaluate(js_code, [main_mark, self.selector_name])
179
+# # self.contexts[main_mark] = self.page
180
+# # except Exception as e:
181
+# # print(f"Error when extracting from main frame: {e}")
182
+
183
+# # Replace iframe placeholders with actual content
184
+# # for url, content in frame_contents.items():
185
+# # placeholder = f'<iframe src="{url}"'
186
+# # main_content = main_content.replace(placeholder, f'{placeholder}>\n<!-- IFrame Content Start -->\n{content}\n<!-- IFrame Content End -->\n</iframe')
187
+
188
+# # return main_content + "".join(frame_contents.values())
189
+# return "".join(frame_contents.values())
190
+
191
+# def strip_html_dom(self, html_content: str) -> str:
192
+# """Clean and strip HTML content"""
193
+# if not html_content:
194
+# return ""
195
+
196
+# soup = BeautifulSoup(html_content, "html.parser")
197
+
198
+# for tag in soup.find_all(
199
+# ["br", "hr", "style", "script", "noscript", "meta", "link", "svg"]
200
+# ):
201
+# tag.decompose()
202
+
203
+# for tag in soup.find_all(True):
204
+# if tag.attrs and "invisible" in tag.attrs:
205
+# tag.decompose()
206
+
207
+# for tag in soup.find_all(True):
208
+# allowed_attrs = [
209
+# self.selector_name,
210
+# "aria-label",
211
+# "placeholder",
212
+# "name",
213
+# "value",
214
+# "type",
215
+# ]
216
+# attrs = {
217
+# "selector" if key == self.selector_name else key: tag.attrs[key]
218
+# for key in allowed_attrs
219
+# if key in tag.attrs and tag.attrs[key]
220
+# }
221
+# tag.attrs = attrs
222
+
223
+# def remove_empty(tag_name: str) -> None:
224
+# for tag in soup.find_all(tag_name):
225
+# if not tag.attrs:
226
+# tag.unwrap()
227
+
228
+# remove_empty("span")
229
+# remove_empty("p")
230
+# remove_empty("strong")
231
+
232
+# return soup.prettify(formatter="minimal")
233
+
234
+# def process_html_with_selectors(self, html_content: str) -> str:
235
+# """Process HTML content and add selectors to interactive elements"""
236
+# if not html_content:
237
+# return ""
238
+
239
+# html_content = re.sub(r"\s+", " ", html_content)
240
+# soup = BeautifulSoup(html_content, "html.parser")
241
+
242
+# structural_tags = [
243
+# "html",
244
+# "head",
245
+# "body",
246
+# "div",
247
+# "span",
248
+# "section",
249
+# "main",
250
+# "article",
251
+# "header",
252
+# "footer",
253
+# "nav",
254
+# "ul",
255
+# "ol",
256
+# "li",
257
+# "tr",
258
+# "td",
259
+# "th",
260
+# ]
261
+# for tag in structural_tags:
262
+# for element in soup.find_all(tag):
263
+# element.unwrap()
264
+
265
+# out = str(soup).strip()
266
+# out = re.sub(r">\s*<", "><", out)
267
+# out = re.sub(r'aria-label="', 'label="', out)
268
+
269
+# # out = re.sub(r'selector="(\d+[a-zA-Z]+)"', r'selector=\1', out)
270
+# return out
271
+
272
+# async def get_clean_dom(self) -> str:
273
+# """Get clean DOM with selectors"""
274
+# full_dom = await self.get_full_dom()
275
+# clean_dom = self.strip_html_dom(full_dom)
276
+# return self.process_html_with_selectors(clean_dom)
277
+
278
+# async def click(self, selector: str):
279
+# await self._check_page()
280
+# ctx, selector = self._parse_selector(selector)
281
+# self.last_selector = selector
282
+# # js_code = files.read_file("lib/browser/click.js")
283
+# # result = await self.page.evaluate(js_code, [selector])
284
+# # if not result:
285
+# result = await ctx.hover(selector, force=True, timeout=Browser.interact_timeout)
286
+# await self.wait_tick()
287
+# result = await ctx.click(selector, force=True, timeout=Browser.interact_timeout)
288
+# await self.wait_tick()
289
+
290
+# # await self.page.wait_for_load_state("networkidle")
291
+# return result
292
+
293
+# async def press(self, key: str):
294
+# await self._check_page()
295
+# if self.last_selector:
296
+# await self.page.press(
297
+# self.last_selector, key, timeout=Browser.interact_timeout
298
+# )
299
+# else:
300
+# await self.page.keyboard.press(key)
301
+
302
+# async def fill(self, selector: str, text: str):
303
+# await self._check_page()
304
+# ctx, selector = self._parse_selector(selector)
305
+# self.last_selector = selector
306
+# try:
307
+# await self.click(selector)
308
+# except Exception as e:
309
+# pass
310
+# await ctx.fill(selector, text, force=True, timeout=Browser.interact_timeout)
311
+# await self.wait_tick()
312
+
313
+# async def execute(self, js_code: str):
314
+# await self._check_page()
315
+# result = await self.page.evaluate(js_code)
316
+# return result
317
+
318
+# async def screenshot(self, path: str, full_page=False):
319
+# await self._check_page()
320
+# await self.page.screenshot(path=path, full_page=full_page)
321
+
322
+# def _parse_selector(self, selector: str) -> tuple[Page | Frame, str]:
323
+# try:
324
+# ctx = self.page
325
+# # Check if selector is our UID, return
326
+# if re.match(r"^\d+[a-zA-Z]+$", selector):
327
+# alpha_part = "".join(filter(str.isalpha, selector))
328
+# ctx = self.contexts[alpha_part]
329
+# selector = f"[{self.selector_name}='{selector}']"
330
+# return (ctx, selector)
331
+# except Exception as e:
332
+# raise Exception(f"Error evaluating selector: {selector}")
333
+
334
+# async def _check_page(self):
335
+# for _ in range(2):
336
+# try:
337
+# await self.wait_tick()
338
+# self.page = self.context.pages[0]
339
+# if not self.page:
340
+# raise NoPageError(
341
+# "No page is open in the browser. Please open a URL first."
342
+# )
343
+# # await self.page.wait_for_load_state("networkidle",)
344
+# async with asyncio.timeout(self.load_timeout / 1000):
345
+# if not self.page_loaded:
346
+# while not self.page_loaded:
347
+# await asyncio.sleep(0.1)
348
+# await self.wait_tick()
349
+# return
350
+# except TimeoutError as e:
351
+# self.page_loaded = True
352
+# return
353
+# except NoPageError as e:
354
+# raise e
355
+# except Exception as e:
356
+# print(f"Error checking page: {e}")
357
+
358
+# def _num_to_alpha(self, num: int) -> str:
359
+# if num < 0:
360
+# return ""
361
+
362
+# result = ""
363
+# while num >= 0:
364
+# result = chr(num % 26 + 97) + result
365
+# num = num // 26 - 1
366
+
367
+# return result
368
+
369
+# async def wait_tick(self):
370
+# if self.page:
371
+# await self.page.evaluate("window.location.href")
372
+
373
+# async def wait(self, seconds: float = 1.0):
374
+# await asyncio.sleep(seconds)
375
+# await self.wait_tick()
376
+
377
+# async def wait_for_action(self):
378
+# nav_count = self.navigation_count
379
+# for _ in range(5):
380
+# await self._check_page()
381
+# if nav_count != self.navigation_count:
382
+# print("Navigation detected")
383
+# await asyncio.sleep(1)
384
+# return
385
+# await asyncio.sleep(0.1)
python/helpers/playwright.py
new
+32
@@ -0,0 +1,32 @@
1
+
2
+from pathlib import Path
3
+import subprocess
4
+from python.helpers import files
5
+
6
+
7
+# this helper ensures that playwright is installed in /lib/playwright
8
+# should work for both docker and local installation
9
+
10
+def get_playwright_binary():
11
+ pw_cache = Path(get_playwright_cache_dir())
12
+ headless_shell = next(pw_cache.glob("chromium_headless_shell-*/chrome-*/headless_shell"), None)
13
+ return headless_shell
14
+
15
+def get_playwright_cache_dir():
16
+ return files.get_abs_path("tmp/playwright")
17
+
18
+def ensure_playwright_binary():
19
+ bin = get_playwright_binary()
20
+ if not bin:
21
+ cache = get_playwright_cache_dir()
22
+ import os
23
+ env = os.environ.copy()
24
+ env["PLAYWRIGHT_BROWSERS_PATH"] = cache
25
+ subprocess.check_call(
26
+ ["playwright", "install", "chromium", "--only-shell"],
27
+ env=env
28
+ )
29
+ bin = get_playwright_binary()
30
+ if not bin:
31
+ raise Exception("Playwright binary not found after installation")
32
+ return bin
\ No newline at end of file
python/tools/browser_agent.py
+46
-22
@@ -3,13 +3,15 @@ import json
3
import time
4
from typing import Optional
5
from agent import Agent, InterventionException
6
+from pathlib import Path
7
+
8
9
import models
10
from python.helpers.tool import Tool, Response
11
from python.helpers import files, defer, persist_chat, strings
12
from python.helpers.browser_use import browser_use
13
from python.helpers.print_style import PrintStyle
12
-
14
+from python.helpers.playwright import ensure_playwright_binary
15
from python.extensions.message_loop_start._10_iteration_no import get_iter_no
16
from pydantic import BaseModel
17
import uuid
@@ -36,19 +38,23 @@ class State:
38
if self.browser_session:
39
return
40
41
+ # for some reason we need to provide exact path to headless shell, otherwise it looks for headed browser
42
+ pw_binary = ensure_playwright_binary()
43
+
44
self.browser_session = browser_use.BrowserSession(
45
browser_profile=browser_use.BrowserProfile(
46
headless=True,
47
disable_security=True,
48
chromium_sandbox=False,
49
accept_downloads=True,
50
+ executable_path=pw_binary,
51
keep_alive=True,
52
minimum_wait_page_load_time=1.0,
53
wait_for_network_idle_page_load_time=2.0,
54
maximum_wait_page_load_time=10.0,
49
- screen={'width': 1024, 'height': 1024},
50
- viewport={'width': 1024, 'height': 1024},
51
- args=['--headless=new'],
55
+ screen={"width": 1024, "height": 1024},
56
+ viewport={"width": 1024, "height": 1024},
57
+ args=["--headless=new"],
58
)
59
)
60
@@ -79,6 +85,7 @@ class State:
85
if self.browser_session:
86
try:
87
import asyncio
88
+
89
loop = asyncio.new_event_loop()
90
asyncio.set_event_loop(loop)
91
loop.run_until_complete(self.browser_session.close())
@@ -105,9 +112,7 @@ class State:
112
@controller.registry.action("Complete task", param_model=DoneResult)
113
async def complete_task(params: DoneResult):
114
result = browser_use.ActionResult(
108
- is_done=True,
109
- success=True,
110
- extracted_content=params.model_dump_json()
115
+ is_done=True, success=True, extracted_content=params.model_dump_json()
116
)
117
return result
118
@@ -123,9 +128,12 @@ class State:
128
browser_session=self.browser_session,
129
llm=model,
130
use_vision=self.agent.config.browser_model.vision,
126
- extend_system_message=self.agent.read_prompt("prompts/browser_agent.system.md"),
131
+ extend_system_message=self.agent.read_prompt(
132
+ "prompts/browser_agent.system.md"
133
+ ),
134
controller=controller,
135
enable_memory=False, # Disable memory to avoid state conflicts
136
+ # available_file_paths=[],
137
)
138
139
self.iter_no = get_iter_no(self.agent)
@@ -150,10 +158,13 @@ class State:
158
if self.iter_no != get_iter_no(self.agent):
159
raise InterventionException("Task cancelled")
160
return await func(*args, **kwargs)
161
+
162
return wrapper
163
155
- if self.browser_session and hasattr(self.browser_session, 'remove_highlights'):
156
- self.browser_session.remove_highlights = override_hook(self.browser_session.remove_highlights)
164
+ if self.browser_session and hasattr(self.browser_session, "remove_highlights"):
165
+ self.browser_session.remove_highlights = override_hook(
166
+ self.browser_session.remove_highlights
167
+ )
168
169
async def get_page(self):
170
if self.use_agent and self.browser_session:
@@ -167,7 +178,9 @@ class State:
178
async def get_selector_map(self):
179
"""Get the selector map for the current page state."""
180
if self.use_agent:
170
- await self.use_agent.browser_session.get_state_summary(cache_clickable_elements_hashes=True)
181
+ await self.use_agent.browser_session.get_state_summary(
182
+ cache_clickable_elements_hashes=True
183
+ )
184
return await self.use_agent.browser_session.get_selector_map()
185
return {}
186
@@ -187,14 +200,16 @@ class BrowserAgent(Tool):
200
while not task.is_ready():
201
# Check for timeout to prevent infinite waiting
202
if time.time() - start_time > timeout_seconds:
190
- PrintStyle().warning(f"Browser agent task timeout after {timeout_seconds} seconds, forcing completion")
203
+ PrintStyle().warning(
204
+ f"Browser agent task timeout after {timeout_seconds} seconds, forcing completion"
205
+ )
206
self.state.kill_task()
207
break
208
209
await self.agent.handle_intervention()
210
await asyncio.sleep(1)
196
- try:
197
- if task.is_ready(): # otherwise get_update hangs
211
+ try:
212
+ if task.is_ready(): # otherwise get_update hangs
213
break
214
update = await self.get_update()
215
log = update.get("log")
@@ -228,15 +243,23 @@ class BrowserAgent(Tool):
243
answer_data = DirtyJson.parse_string(answer)
244
answer_text = strings.dict_to_text(answer_data) # type: ignore
245
else:
231
- answer_text = str(answer) if answer else "Task completed successfully"
246
+ answer_text = (
247
+ str(answer) if answer else "Task completed successfully"
248
+ )
249
except Exception as e:
233
- answer_text = str(answer) if answer else f"Task completed with parse error: {str(e)}"
250
+ answer_text = (
251
+ str(answer)
252
+ if answer
253
+ else f"Task completed with parse error: {str(e)}"
254
+ )
255
else:
256
# Task hit max_steps without calling done()
257
urls = result.urls()
258
current_url = urls[-1] if urls else "unknown"
238
- answer_text = (f"Task reached step limit without completion. Last page: {current_url}. "
239
- f"The browser agent may need clearer instructions on when to finish.")
259
+ answer_text = (
260
+ f"Task reached step limit without completion. Last page: {current_url}. "
261
+ f"The browser agent may need clearer instructions on when to finish."
262
+ )
263
264
self.log.update(answer=answer_text)
265
return Response(message=answer_text, break_loop=False)
@@ -265,7 +288,6 @@ class BrowserAgent(Tool):
288
await agent.wait_if_paused()
289
290
log = []
268
-
291
292
# for message in ua.message_manager.get_messages():
293
# if message.type == "system":
@@ -291,9 +313,11 @@ class BrowserAgent(Tool):
313
# for res in hist.result:
314
# log.append(res.extracted_content)
315
log = ua.state.history.extracted_content()
294
-
295
-
296
- result["log"] = log
316
+ short_log = []
317
+ for item in log:
318
+ first_line = str(item).split("\n", 1)[0][:200]
319
+ short_log.append(first_line)
320
+ result["log"] = short_log
321
322
path = files.get_abs_path(
323
persist_chat.get_chat_folder_path(agent.context.id),
requirements.txt
-1
@@ -1,6 +1,5 @@
1
a2wsgi==1.10.8
2
ansio==0.0.1
3
-beautifulsoup4==4.13.4
3
browser-use==0.2.5
4
docker==7.1.0
5
duckduckgo-search==6.1.12