sunholo 0.74.7__py3-none-any.whl → 0.74.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sunholo/tools/web_browser.py +100 -37
- {sunholo-0.74.7.dist-info → sunholo-0.74.8.dist-info}/METADATA +2 -2
- {sunholo-0.74.7.dist-info → sunholo-0.74.8.dist-info}/RECORD +7 -7
- {sunholo-0.74.7.dist-info → sunholo-0.74.8.dist-info}/LICENSE.txt +0 -0
- {sunholo-0.74.7.dist-info → sunholo-0.74.8.dist-info}/WHEEL +0 -0
- {sunholo-0.74.7.dist-info → sunholo-0.74.8.dist-info}/entry_points.txt +0 -0
- {sunholo-0.74.7.dist-info → sunholo-0.74.8.dist-info}/top_level.txt +0 -0
sunholo/tools/web_browser.py
CHANGED
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
import urllib.parse
|
|
6
6
|
import time
|
|
7
|
+
from io import BytesIO
|
|
7
8
|
|
|
8
9
|
from ..logging import log
|
|
9
10
|
|
|
@@ -115,7 +116,14 @@ class BrowseWebWithImagePromptsBot:
|
|
|
115
116
|
```
|
|
116
117
|
"""
|
|
117
118
|
#class BrowseWebWithImagePromptsBot:
|
|
118
|
-
def __init__(
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
website_name:str,
|
|
122
|
+
session_id: str=None,
|
|
123
|
+
browser_type:str='chromium',
|
|
124
|
+
headless:bool=True,
|
|
125
|
+
max_steps:int=10
|
|
126
|
+
):
|
|
119
127
|
try:
|
|
120
128
|
from playwright.sync_api import sync_playwright
|
|
121
129
|
except ImportError as err:
|
|
@@ -125,12 +133,19 @@ class BrowseWebWithImagePromptsBot:
|
|
|
125
133
|
if not sync_playwright:
|
|
126
134
|
raise ImportError("playright needed for BrowseWebWithImagePromptsBot class - install via `pip install sunholo[tools]`")
|
|
127
135
|
|
|
128
|
-
|
|
136
|
+
log.info(f"Starting BrowseWebWithImagePromptsBot with {website_name=}, {session_id=}, {browser_type=}, {headless=}, {max_steps=}")
|
|
137
|
+
|
|
138
|
+
# Assign session_id if it is None or 'None'
|
|
139
|
+
if not session_id or session_id == 'None':
|
|
140
|
+
self.session_id = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
141
|
+
else:
|
|
142
|
+
self.session_id = session_id
|
|
143
|
+
|
|
129
144
|
self.website_name = website_name
|
|
130
|
-
self.browser_type = browser_type
|
|
131
|
-
self.max_steps = max_steps
|
|
145
|
+
self.browser_type = browser_type or 'chromium'
|
|
146
|
+
self.max_steps = int(max_steps)
|
|
132
147
|
self.steps = 0
|
|
133
|
-
self.screenshot_dir = f"browser_tool/{get_clean_website_name(website_name)}/{session_id}"
|
|
148
|
+
self.screenshot_dir = f"browser_tool/{get_clean_website_name(website_name)}/{self.session_id}"
|
|
134
149
|
os.makedirs(self.screenshot_dir, exist_ok=True)
|
|
135
150
|
self.cookie_file = os.path.join(self.screenshot_dir, "cookies.json")
|
|
136
151
|
self.action_log_file = os.path.join(self.screenshot_dir, "action_log.json")
|
|
@@ -198,17 +213,48 @@ class BrowseWebWithImagePromptsBot:
|
|
|
198
213
|
except Exception as err:
|
|
199
214
|
log.warning(f"navigate failed with {str(err)}")
|
|
200
215
|
self.action_log.append(f"Tried to navigate to {url} but got an error")
|
|
216
|
+
|
|
217
|
+
def get_locator_via_roles_and_text(self, selector: str):
|
|
218
|
+
interactive_roles = ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"]
|
|
219
|
+
|
|
220
|
+
for role in interactive_roles:
|
|
221
|
+
log.info(f'Trying role {role} for selector {selector}')
|
|
222
|
+
elements = self.page.get_by_role(role).get_by_text(selector).locator("visible=true").all()
|
|
223
|
+
if elements:
|
|
224
|
+
log.info(f"Got {len(elements)} elements for selector {selector} with role {role}")
|
|
225
|
+
for element in elements:
|
|
226
|
+
try:
|
|
227
|
+
log.info(f"Trying {selector} with element.hover locator: {element}")
|
|
228
|
+
try:
|
|
229
|
+
element.hover(timeout=10000, trial=True)
|
|
230
|
+
self.action_log.append(f"Successfully found element via selector: {selector}")
|
|
231
|
+
|
|
232
|
+
return element
|
|
233
|
+
|
|
234
|
+
except Exception as err:
|
|
235
|
+
log.warning(f"Could not hover over element: {element} {str(err)} - trying next element")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
log.error(f"Failed to get locator for selector '{selector}' with role {role}: {str(e)}")
|
|
238
|
+
|
|
239
|
+
time.sleep(0.5) # Wait for a bit before retrying
|
|
240
|
+
|
|
241
|
+
log.info(f"No elements for '{selector}' within role '{role}'")
|
|
242
|
+
|
|
243
|
+
self.action_log.append(f"FAILED: Using page.get_by_role('role').locator('text={selector}').locator('visible=true') could not find any valid element. Try something else.")
|
|
244
|
+
return None
|
|
201
245
|
|
|
202
246
|
def get_locator(self, selector, by_text=True):
|
|
203
247
|
if by_text:
|
|
204
|
-
|
|
205
|
-
if elements:
|
|
206
|
-
return elements[0]
|
|
207
|
-
else:
|
|
208
|
-
log.warning(f"No elements found with text: {selector}")
|
|
209
|
-
return None
|
|
248
|
+
element = self.get_locator_via_roles_and_text(selector)
|
|
210
249
|
else:
|
|
211
|
-
|
|
250
|
+
element = self.page.locator(selector)
|
|
251
|
+
|
|
252
|
+
if element:
|
|
253
|
+
return element
|
|
254
|
+
|
|
255
|
+
log.error(f"Failed to get locator for selector {selector}")
|
|
256
|
+
|
|
257
|
+
return None
|
|
212
258
|
|
|
213
259
|
def click(self, selector, by_text=True):
|
|
214
260
|
(x,y)=(0,0)
|
|
@@ -229,14 +275,14 @@ class BrowseWebWithImagePromptsBot:
|
|
|
229
275
|
try:
|
|
230
276
|
element.click()
|
|
231
277
|
self.page.wait_for_load_state()
|
|
232
|
-
log.info(f"Clicked on element with
|
|
233
|
-
self.action_log.append(f"Clicked on element with
|
|
278
|
+
log.info(f"Clicked on {element=} with {selector=} at {x=},{y=}")
|
|
279
|
+
self.action_log.append(f"Clicked on {element=} with {selector=} at {x=},{y=}")
|
|
234
280
|
|
|
235
281
|
return (x,y)
|
|
236
282
|
|
|
237
283
|
except Exception as err:
|
|
238
284
|
log.warning(f"click failed with {str(err)}")
|
|
239
|
-
self.action_log.append(f"Tried to click on element with
|
|
285
|
+
self.action_log.append(f"Tried to click on {element=} with {selector=} at {x=},{y=} but got an error")
|
|
240
286
|
|
|
241
287
|
return (x,y)
|
|
242
288
|
|
|
@@ -261,7 +307,7 @@ class BrowseWebWithImagePromptsBot:
|
|
|
261
307
|
(x,y)=(0,0)
|
|
262
308
|
element = self.get_locator(selector, by_text=by_text)
|
|
263
309
|
if element is None:
|
|
264
|
-
self.action_log.append(f"Tried to type {text} via website text: {selector} but it was not a valid
|
|
310
|
+
self.action_log.append(f"Tried to type {text} via website text: {selector} but it was not a valid element to add text")
|
|
265
311
|
return (x,y)
|
|
266
312
|
|
|
267
313
|
try:
|
|
@@ -275,32 +321,38 @@ class BrowseWebWithImagePromptsBot:
|
|
|
275
321
|
try:
|
|
276
322
|
element.fill(text)
|
|
277
323
|
self.page.wait_for_load_state()
|
|
278
|
-
log.info(f"Typed text '{text}' into element with
|
|
279
|
-
self.action_log.append(f"Typed text '{text}' into element with
|
|
324
|
+
log.info(f"Typed text '{text}' into {element=} with {selector=} at {x=},{y=}")
|
|
325
|
+
self.action_log.append(f"Typed text '{text}' into {element=} with {selector=} at {x=},{y=}")
|
|
280
326
|
|
|
281
327
|
return (x, y)
|
|
282
328
|
|
|
283
329
|
except Exception as err:
|
|
284
330
|
log.warning(f"Typed text failed with {str(err)}")
|
|
285
|
-
self.action_log.append(f"Tried to type text '{text}' into element with
|
|
331
|
+
self.action_log.append(f"Tried to type text '{text}' into {element=} with {selector=} at {x=},{y=} but got an error")
|
|
286
332
|
|
|
287
333
|
return (x, y)
|
|
288
334
|
|
|
289
|
-
def take_screenshot(self,
|
|
335
|
+
def take_screenshot(self, full_page=False, mark_action=None):
|
|
336
|
+
|
|
337
|
+
from PIL import Image
|
|
338
|
+
|
|
290
339
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
291
340
|
parsed_url = urllib.parse.urlparse(self.page.url)
|
|
292
341
|
|
|
293
342
|
url_path = parsed_url.path
|
|
294
343
|
if url_path == "/":
|
|
295
344
|
url_path = "index.html"
|
|
296
|
-
if final:
|
|
297
|
-
screenshot_path = os.path.join(self.screenshot_dir, f"final/{timestamp}_{url_path}.png")
|
|
298
345
|
else:
|
|
299
|
-
|
|
300
|
-
|
|
346
|
+
url_path = url_path.replace("/","_")
|
|
347
|
+
screenshot_path = os.path.join(self.screenshot_dir, f"{timestamp}_{url_path}.png")
|
|
348
|
+
screenshot_bytes = self.page.screenshot(full_page=full_page, scale='css')
|
|
301
349
|
|
|
302
350
|
if mark_action:
|
|
303
|
-
self.mark_screenshot(
|
|
351
|
+
image = self.mark_screenshot(screenshot_bytes, mark_action)
|
|
352
|
+
else:
|
|
353
|
+
image = Image.open(BytesIO(screenshot_bytes))
|
|
354
|
+
|
|
355
|
+
image.save(screenshot_path, format='png')
|
|
304
356
|
|
|
305
357
|
log.info(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
|
|
306
358
|
#self.action_log.append(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
|
|
@@ -308,17 +360,17 @@ class BrowseWebWithImagePromptsBot:
|
|
|
308
360
|
|
|
309
361
|
return screenshot_path
|
|
310
362
|
|
|
311
|
-
def mark_screenshot(self,
|
|
363
|
+
def mark_screenshot(self, screenshot_bytes, mark_action):
|
|
312
364
|
"""
|
|
313
365
|
Marks the screenshot with the specified action.
|
|
314
366
|
|
|
315
367
|
Parameters:
|
|
316
|
-
|
|
368
|
+
screenshot_bytes (bytes): The bytes of the screenshot.
|
|
317
369
|
mark_action (dict): Action details for marking the screenshot.
|
|
318
370
|
"""
|
|
319
371
|
from PIL import Image, ImageDraw
|
|
320
|
-
|
|
321
|
-
image = Image.open(
|
|
372
|
+
time.sleep(1) # maybe pass in bytes to avoid waiting for file to be written
|
|
373
|
+
image = Image.open(BytesIO(screenshot_bytes))
|
|
322
374
|
draw = ImageDraw.Draw(image)
|
|
323
375
|
|
|
324
376
|
if mark_action['type'] == 'click':
|
|
@@ -329,7 +381,7 @@ class BrowseWebWithImagePromptsBot:
|
|
|
329
381
|
x, y = mark_action['position']
|
|
330
382
|
draw.rectangle((x-5, y-5, x+5, y+5), outline='blue', width=3)
|
|
331
383
|
|
|
332
|
-
image
|
|
384
|
+
return image
|
|
333
385
|
|
|
334
386
|
def get_latest_screenshot_path(self):
|
|
335
387
|
screenshots = sorted(
|
|
@@ -364,6 +416,10 @@ class BrowseWebWithImagePromptsBot:
|
|
|
364
416
|
else:
|
|
365
417
|
log.warning(f'Unknown response: {response=} {type(response)}')
|
|
366
418
|
output = None
|
|
419
|
+
|
|
420
|
+
if not output:
|
|
421
|
+
log.error(f'Got no output from response: {response=}')
|
|
422
|
+
return None
|
|
367
423
|
|
|
368
424
|
if 'status' not in output:
|
|
369
425
|
log.error(f'Response did not contain status')
|
|
@@ -396,6 +452,8 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
|
|
|
396
452
|
""")
|
|
397
453
|
|
|
398
454
|
def close(self):
|
|
455
|
+
log.info(f"Session {self.session_id} finished")
|
|
456
|
+
self.take_screenshot()
|
|
399
457
|
self.save_cookies()
|
|
400
458
|
self.browser.close()
|
|
401
459
|
self.playwright.stop()
|
|
@@ -444,7 +502,7 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
|
|
|
444
502
|
|
|
445
503
|
return next_browser_instructions
|
|
446
504
|
|
|
447
|
-
def create_gif_from_pngs(self, frame_duration=
|
|
505
|
+
def create_gif_from_pngs(self, frame_duration=300):
|
|
448
506
|
"""
|
|
449
507
|
Creates a GIF from a folder of PNG images.
|
|
450
508
|
|
|
@@ -501,9 +559,17 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
|
|
|
501
559
|
log.error('Browser status: "in-progress" but no new_instructions')
|
|
502
560
|
last_message = next_instructions['message']
|
|
503
561
|
self.action_log.append(last_message)
|
|
504
|
-
|
|
505
|
-
next_instructions
|
|
506
|
-
|
|
562
|
+
try:
|
|
563
|
+
next_instructions = self.execute_instructions(
|
|
564
|
+
next_instructions['new_instructions'],
|
|
565
|
+
last_message=last_message)
|
|
566
|
+
except Exception as err:
|
|
567
|
+
log.error(f'session aborted due to: {str(err)}')
|
|
568
|
+
next_instructions = {
|
|
569
|
+
'status': 'error',
|
|
570
|
+
'message': f'session errored with: {str(err)}'
|
|
571
|
+
}
|
|
572
|
+
break
|
|
507
573
|
else:
|
|
508
574
|
log.info(f'Session finished due to status={next_instructions["status"]}')
|
|
509
575
|
in_session=False
|
|
@@ -513,9 +579,6 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
|
|
|
513
579
|
in_session=False
|
|
514
580
|
break
|
|
515
581
|
|
|
516
|
-
log.info("Session finished")
|
|
517
|
-
self.take_screenshot()
|
|
518
|
-
|
|
519
582
|
self.close()
|
|
520
583
|
|
|
521
584
|
answer = None
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sunholo
|
|
3
|
-
Version: 0.74.
|
|
3
|
+
Version: 0.74.8
|
|
4
4
|
Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
|
|
5
5
|
Home-page: https://github.com/sunholo-data/sunholo-py
|
|
6
|
-
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.74.
|
|
6
|
+
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.74.8.tar.gz
|
|
7
7
|
Author: Holosun ApS
|
|
8
8
|
Author-email: multivac@sunholo.com
|
|
9
9
|
License: Apache License, Version 2.0
|
|
@@ -99,7 +99,7 @@ sunholo/streaming/streaming.py,sha256=9z6pXINEopuL_Z1RnmgXAoZJum9dzyuOxqYtEYnjf8
|
|
|
99
99
|
sunholo/summarise/__init__.py,sha256=MZk3dblUMODcPb1crq4v-Z508NrFIpkSWNf9FIO8BcU,38
|
|
100
100
|
sunholo/summarise/summarise.py,sha256=C3HhjepTjUhUC8FLk4jMQIBvq1BcORniwuTFHjPVhVo,3784
|
|
101
101
|
sunholo/tools/__init__.py,sha256=5NuYpwwTX81qGUWvgwfItoSLXteNnp7KjgD7IPZUFjI,53
|
|
102
|
-
sunholo/tools/web_browser.py,sha256
|
|
102
|
+
sunholo/tools/web_browser.py,sha256=-zfuaxnRXTEzPUyBT0a26uhe2D9T7BtQhH3696gy4xo,24542
|
|
103
103
|
sunholo/utils/__init__.py,sha256=Hv02T5L2zYWvCso5hzzwm8FQogwBq0OgtUbN_7Quzqc,89
|
|
104
104
|
sunholo/utils/api_key.py,sha256=Ct4bIAQZxzPEw14hP586LpVxBAVi_W9Serpy0BK-7KI,244
|
|
105
105
|
sunholo/utils/big_context.py,sha256=gJIP7_ZL-YSLhOMq8jmFTMqH1wq8eB1NK7oKPeZAq2s,5578
|
|
@@ -117,9 +117,9 @@ sunholo/vertex/extensions_class.py,sha256=4PsUM9dSYrIPpq9bZ3K2rL9MRb_rlqAgnMsW0o
|
|
|
117
117
|
sunholo/vertex/init.py,sha256=-w7b9GKsyJnAJpYHYz6_zBUtmeJeLXlEkgOfwoe4DEI,2715
|
|
118
118
|
sunholo/vertex/memory_tools.py,sha256=pomHrDKqvY8MZxfUqoEwhdlpCvSGP6KmFJMVKOimXjs,6842
|
|
119
119
|
sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
|
|
120
|
-
sunholo-0.74.
|
|
121
|
-
sunholo-0.74.
|
|
122
|
-
sunholo-0.74.
|
|
123
|
-
sunholo-0.74.
|
|
124
|
-
sunholo-0.74.
|
|
125
|
-
sunholo-0.74.
|
|
120
|
+
sunholo-0.74.8.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
|
|
121
|
+
sunholo-0.74.8.dist-info/METADATA,sha256=aQlv95LPMIjodfSejFXHiaTmfGWAcIuvlDg2hLx-xys,7010
|
|
122
|
+
sunholo-0.74.8.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
|
123
|
+
sunholo-0.74.8.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
|
|
124
|
+
sunholo-0.74.8.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
|
|
125
|
+
sunholo-0.74.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|