sunholo 0.73.3__py3-none-any.whl → 0.74.1__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/cli/chat_vac.py +4 -0
- sunholo/tools/__init__.py +1 -0
- sunholo/tools/web_browser.py +353 -43
- sunholo/utils/parsers.py +6 -1
- {sunholo-0.73.3.dist-info → sunholo-0.74.1.dist-info}/METADATA +6 -4
- {sunholo-0.73.3.dist-info → sunholo-0.74.1.dist-info}/RECORD +10 -10
- {sunholo-0.73.3.dist-info → sunholo-0.74.1.dist-info}/LICENSE.txt +0 -0
- {sunholo-0.73.3.dist-info → sunholo-0.74.1.dist-info}/WHEEL +0 -0
- {sunholo-0.73.3.dist-info → sunholo-0.74.1.dist-info}/entry_points.txt +0 -0
- {sunholo-0.73.3.dist-info → sunholo-0.74.1.dist-info}/top_level.txt +0 -0
sunholo/cli/chat_vac.py
CHANGED
|
@@ -183,6 +183,10 @@ def stream_chat_session(service_url, service_name, stream=True):
|
|
|
183
183
|
read_file = None
|
|
184
184
|
read_file_count = None
|
|
185
185
|
continue
|
|
186
|
+
|
|
187
|
+
if user_input.lower().startswith("!"):
|
|
188
|
+
console.print("[bold red]Could find no valid chat command for you, sorry[/bold red]")
|
|
189
|
+
continue
|
|
186
190
|
|
|
187
191
|
if read_file:
|
|
188
192
|
user_input = f"<user added file>{read_file}</user added file>\n{user_input}"
|
sunholo/tools/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .web_browser import BrowseWebWithImagePromptsBot
|
sunholo/tools/web_browser.py
CHANGED
|
@@ -2,23 +2,78 @@ import os
|
|
|
2
2
|
import base64
|
|
3
3
|
import json
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
import urllib.parse
|
|
6
|
+
|
|
7
|
+
from ..logging import log
|
|
8
|
+
|
|
9
|
+
from ..utils.parsers import get_clean_website_name
|
|
10
|
+
|
|
5
11
|
try:
|
|
6
|
-
from playwright.sync_api import sync_playwright
|
|
12
|
+
from playwright.sync_api import sync_playwright, Response
|
|
7
13
|
except ImportError:
|
|
8
14
|
sync_playwright = None
|
|
15
|
+
Response = None
|
|
9
16
|
|
|
10
17
|
class BrowseWebWithImagePromptsBot:
|
|
11
18
|
"""
|
|
12
|
-
|
|
19
|
+
BrowseWebWithImagePromptsBot is a base class for creating bots that interact with web pages using Playwright.
|
|
20
|
+
The bot can perform actions such as navigating, clicking, scrolling, typing text, and taking screenshots.
|
|
21
|
+
It also supports cookie management to maintain session state across interactions.
|
|
22
|
+
|
|
23
|
+
Methods:
|
|
24
|
+
- __init__(session_id, website_name, browser_type='chromium', headless=True):
|
|
25
|
+
Initializes the bot with the given session ID, website name, browser type, and headless mode.
|
|
26
|
+
Supported browser types: 'chromium', 'firefox', 'webkit'.
|
|
27
|
+
|
|
28
|
+
- load_cookies():
|
|
29
|
+
Loads cookies from a file and adds them to the browser context.
|
|
30
|
+
|
|
31
|
+
- save_cookies():
|
|
32
|
+
Saves the current cookies to a file.
|
|
33
|
+
|
|
34
|
+
- navigate(url):
|
|
35
|
+
Navigates to the specified URL.
|
|
36
|
+
|
|
37
|
+
- click(selector):
|
|
38
|
+
Clicks on the element specified by the selector.
|
|
39
|
+
|
|
40
|
+
- scroll(direction='down', amount=1):
|
|
41
|
+
Scrolls the page in the specified direction ('down', 'up', 'left', 'right') by the specified amount.
|
|
42
|
+
|
|
43
|
+
- type_text(selector, text):
|
|
44
|
+
Types the specified text into the element specified by the selector.
|
|
45
|
+
|
|
46
|
+
- take_screenshot():
|
|
47
|
+
Takes a screenshot and saves it with a timestamp in the session-specific directory. Returns the path to the screenshot.
|
|
48
|
+
|
|
49
|
+
- get_latest_screenshot_path():
|
|
50
|
+
Retrieves the path to the most recent screenshot in the session-specific directory.
|
|
51
|
+
|
|
52
|
+
- create_prompt_vars(current_action_description, session_goal):
|
|
53
|
+
Creates a dictionary of prompt variables from the current action description and session goal.
|
|
54
|
+
|
|
55
|
+
- send_screenshot_to_llm(screenshot_path, current_action_description="", session_goal=""):
|
|
56
|
+
Encodes the screenshot in base64, creates prompt variables, and sends them to the LLM. Returns the new instructions from the LLM.
|
|
57
|
+
|
|
58
|
+
- send_prompt_to_llm(prompt_vars, screenshot_base64):
|
|
59
|
+
Abstract method to be implemented by subclasses. Sends the prompt variables and screenshot to the LLM and returns the response.
|
|
60
|
+
|
|
61
|
+
- close():
|
|
62
|
+
Saves cookies, closes the browser, and stops Playwright.
|
|
63
|
+
|
|
64
|
+
- execute_instructions(instructions):
|
|
65
|
+
Executes the given set of instructions, takes a screenshot after each step, and sends the screenshot to the LLM for further instructions.
|
|
66
|
+
|
|
67
|
+
Example usage:
|
|
13
68
|
|
|
14
69
|
```python
|
|
15
70
|
class ProductionBot(BrowseWebWithImagePromptsBot):
|
|
16
|
-
def send_prompt_to_llm(self,
|
|
71
|
+
def send_prompt_to_llm(self, prompt_vars, screenshot_base64):
|
|
17
72
|
# Implement the actual logic to send the prompt and screenshot to the LLM and return the response
|
|
18
73
|
api_url = "https://api.example.com/process" # Replace with the actual LLM API endpoint
|
|
19
74
|
headers = {"Content-Type": "application/json"}
|
|
20
75
|
data = {
|
|
21
|
-
"prompt":
|
|
76
|
+
"prompt": prompt_vars,
|
|
22
77
|
"screenshot": screenshot_base64
|
|
23
78
|
}
|
|
24
79
|
response = requests.post(api_url, headers=headers, data=json.dumps(data))
|
|
@@ -31,7 +86,7 @@ class BrowseWebWithImagePromptsBot:
|
|
|
31
86
|
website_name = data.get('website_name')
|
|
32
87
|
browser_type = data.get('browser_type', 'chromium')
|
|
33
88
|
current_action_description = data.get('current_action_description', "")
|
|
34
|
-
|
|
89
|
+
session_goal = data.get('session_goal', "")
|
|
35
90
|
|
|
36
91
|
bot = ProductionBot(session_id=session_id, website_name=website_name, browser_type=browser_type, headless=True)
|
|
37
92
|
|
|
@@ -39,12 +94,13 @@ class BrowseWebWithImagePromptsBot:
|
|
|
39
94
|
initial_instructions = data.get('instructions')
|
|
40
95
|
if initial_instructions:
|
|
41
96
|
bot.execute_instructions(initial_instructions)
|
|
97
|
+
else:
|
|
98
|
+
bot.execute_instructions([{'action':'navigate', 'url': website_name}])
|
|
42
99
|
|
|
43
|
-
# Take initial screenshot and send to LLM
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
bot.execute_instructions(new_instructions)
|
|
100
|
+
# Take initial screenshot and send to LLM
|
|
101
|
+
screenshot_path = bot.take_screenshot()
|
|
102
|
+
new_instructions = bot.send_screenshot_to_llm(screenshot_path, current_action_description, session_goal)
|
|
103
|
+
bot.execute_instructions(new_instructions)
|
|
48
104
|
|
|
49
105
|
# Take final screenshot
|
|
50
106
|
bot.take_screenshot()
|
|
@@ -57,15 +113,26 @@ class BrowseWebWithImagePromptsBot:
|
|
|
57
113
|
app.run(host='0.0.0.0', port=8080)
|
|
58
114
|
```
|
|
59
115
|
"""
|
|
60
|
-
|
|
116
|
+
#class BrowseWebWithImagePromptsBot:
|
|
117
|
+
def __init__(self, session_id, website_name, browser_type='chromium', headless=True, max_steps=10):
|
|
118
|
+
try:
|
|
119
|
+
from playwright.sync_api import sync_playwright
|
|
120
|
+
except ImportError as err:
|
|
121
|
+
print(err)
|
|
122
|
+
sync_playwright = None
|
|
123
|
+
|
|
61
124
|
if not sync_playwright:
|
|
62
125
|
raise ImportError("playright needed for BrowseWebWithImagePromptsBot class - install via `pip install sunholo[tools]`")
|
|
63
|
-
|
|
126
|
+
|
|
127
|
+
self.session_id = session_id or datetime.now().strftime("%Y%m%d%H%M%S")
|
|
64
128
|
self.website_name = website_name
|
|
65
129
|
self.browser_type = browser_type
|
|
66
|
-
self.
|
|
130
|
+
self.max_steps = max_steps
|
|
131
|
+
self.steps = 0
|
|
132
|
+
self.screenshot_dir = f"browser_tool/{get_clean_website_name(website_name)}/{session_id}"
|
|
67
133
|
os.makedirs(self.screenshot_dir, exist_ok=True)
|
|
68
134
|
self.cookie_file = os.path.join(self.screenshot_dir, "cookies.json")
|
|
135
|
+
self.action_log_file = os.path.join(self.screenshot_dir, "action_log.json")
|
|
69
136
|
self.playwright = sync_playwright().start()
|
|
70
137
|
|
|
71
138
|
if browser_type == 'chromium':
|
|
@@ -80,6 +147,9 @@ class BrowseWebWithImagePromptsBot:
|
|
|
80
147
|
self.context = self.browser.new_context()
|
|
81
148
|
self.page = self.context.new_page()
|
|
82
149
|
self.load_cookies()
|
|
150
|
+
self.action_log = []
|
|
151
|
+
self.session_goal = None
|
|
152
|
+
self.session_screenshots = []
|
|
83
153
|
|
|
84
154
|
def load_cookies(self):
|
|
85
155
|
if os.path.exists(self.cookie_file):
|
|
@@ -91,33 +161,176 @@ class BrowseWebWithImagePromptsBot:
|
|
|
91
161
|
cookies = self.context.cookies()
|
|
92
162
|
with open(self.cookie_file, 'w') as f:
|
|
93
163
|
json.dump(cookies, f)
|
|
164
|
+
|
|
165
|
+
def save_action_log(self):
|
|
166
|
+
with open(self.action_log_file, 'w') as f:
|
|
167
|
+
json.dump(self.action_log, f)
|
|
168
|
+
|
|
169
|
+
def load_action_log(self):
|
|
170
|
+
if os.path.exists(self.action_log_file):
|
|
171
|
+
with open(self.action_log_file, 'r') as f:
|
|
172
|
+
action_log = json.load(f)
|
|
173
|
+
self.action_log = action_log
|
|
94
174
|
|
|
95
175
|
def navigate(self, url):
|
|
96
|
-
|
|
176
|
+
def handle_response(response: Response): # type: ignore
|
|
177
|
+
status = response.status
|
|
178
|
+
url = response.url
|
|
179
|
+
if 300 <= status < 400:
|
|
180
|
+
log.info(f"Redirecting from {url}")
|
|
181
|
+
try:
|
|
182
|
+
self.page.on("response", handle_response)
|
|
183
|
+
|
|
184
|
+
previous_url = self.page.url
|
|
185
|
+
|
|
186
|
+
response = self.page.goto(url)
|
|
187
|
+
status = response.status
|
|
188
|
+
if status != 200:
|
|
189
|
+
log.error(f"Failed to navigate to {url}: HTTP {status}")
|
|
190
|
+
self.action_log.append(f"Tried to navigate to {url} but failed: HTTP {status} - browsing back to {previous_url}")
|
|
191
|
+
url = previous_url
|
|
192
|
+
self.page.goto(previous_url)
|
|
193
|
+
|
|
194
|
+
self.page.wait_for_load_state()
|
|
195
|
+
log.info(f'Navigated to {url}')
|
|
196
|
+
self.action_log.append(f"Navigated to {url}")
|
|
197
|
+
|
|
198
|
+
except Exception as err:
|
|
199
|
+
log.warning(f"navigate failed with {str(err)}")
|
|
200
|
+
self.action_log.append(f"Tried to navigate to {url} but got an error")
|
|
97
201
|
|
|
98
|
-
def
|
|
99
|
-
|
|
202
|
+
def get_locator(self, selector, by_text=True):
|
|
203
|
+
if by_text:
|
|
204
|
+
elements = self.page.locator(f"text={selector}").all()
|
|
205
|
+
if elements:
|
|
206
|
+
return elements[0]
|
|
207
|
+
else:
|
|
208
|
+
log.warning(f"No elements found with text: {selector}")
|
|
209
|
+
return None
|
|
210
|
+
else:
|
|
211
|
+
return self.page.locator(selector)
|
|
212
|
+
|
|
213
|
+
def click(self, selector, by_text=True):
|
|
214
|
+
(x,y)=(0,0)
|
|
215
|
+
|
|
216
|
+
element = self.get_locator(selector, by_text=by_text)
|
|
217
|
+
if element is None:
|
|
218
|
+
self.action_log.append(f"Tried to click on text {selector} but it was not a valid location to click")
|
|
219
|
+
return (x,y)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
bounding_box = element.bounding_box()
|
|
223
|
+
if bounding_box:
|
|
224
|
+
x = bounding_box['x'] + bounding_box['width'] / 2
|
|
225
|
+
y = bounding_box['y'] + bounding_box['height'] / 2
|
|
226
|
+
except Exception as err:
|
|
227
|
+
log.warning(f"Could not do bounding box - {str(err)}")
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
element.click()
|
|
231
|
+
self.page.wait_for_load_state()
|
|
232
|
+
log.info(f"Clicked on element with selector {selector} at {x=},{y=}")
|
|
233
|
+
self.action_log.append(f"Clicked on element with selector {selector} at {x=},{y=}")
|
|
234
|
+
|
|
235
|
+
return (x,y)
|
|
236
|
+
|
|
237
|
+
except Exception as err:
|
|
238
|
+
log.warning(f"click failed with {str(err)}")
|
|
239
|
+
self.action_log.append(f"Tried to click on element with selector {selector} at {x=},{y=} but got an error")
|
|
100
240
|
|
|
101
|
-
|
|
102
|
-
|
|
241
|
+
return (x,y)
|
|
242
|
+
|
|
243
|
+
def scroll(self, direction='down', amount=100):
|
|
244
|
+
try:
|
|
103
245
|
if direction == 'down':
|
|
104
|
-
self.page.
|
|
246
|
+
self.page.mouse.wheel(0, amount)
|
|
105
247
|
elif direction == 'up':
|
|
106
|
-
self.page.
|
|
248
|
+
self.page.mouse.wheel(0, -amount)
|
|
107
249
|
elif direction == 'left':
|
|
108
|
-
self.page.
|
|
250
|
+
self.page.mouse.wheel(-amount, 0)
|
|
109
251
|
elif direction == 'right':
|
|
110
|
-
self.page.
|
|
252
|
+
self.page.mouse.wheel(amount, 0)
|
|
253
|
+
self.page.wait_for_timeout(500)
|
|
254
|
+
log.info(f"Scrolled {direction} by {amount} pixels")
|
|
255
|
+
self.action_log.append(f"Scrolled {direction} by {amount} pixels")
|
|
256
|
+
except Exception as err:
|
|
257
|
+
log.warning(f"Scrolled failed with {str(err)}")
|
|
258
|
+
self.action_log.append(f"Tried to scroll {direction} by {amount} pixels but got an error")
|
|
259
|
+
|
|
260
|
+
def type_text(self, selector, text, by_text=True):
|
|
261
|
+
(x,y)=(0,0)
|
|
262
|
+
element = self.get_locator(selector, by_text=by_text)
|
|
263
|
+
if element is None:
|
|
264
|
+
self.action_log.append(f"Tried to type {text} via website text: {selector} but it was not a valid location to add text")
|
|
265
|
+
return (x,y)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
bounding_box = element.bounding_box()
|
|
269
|
+
if bounding_box:
|
|
270
|
+
x = bounding_box['x'] + bounding_box['width'] / 2
|
|
271
|
+
y = bounding_box['y'] + bounding_box['height'] / 2
|
|
272
|
+
except Exception as err:
|
|
273
|
+
log.warning(f"Could not do bounding box - {str(err)}")
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
element.fill(text)
|
|
277
|
+
self.page.wait_for_load_state()
|
|
278
|
+
log.info(f"Typed text '{text}' into element with selector {selector} at {x=},{y=}")
|
|
279
|
+
self.action_log.append(f"Typed text '{text}' into element with selector {selector} at {x=},{y=}")
|
|
111
280
|
|
|
112
|
-
|
|
113
|
-
|
|
281
|
+
return (x, y)
|
|
282
|
+
|
|
283
|
+
except Exception as err:
|
|
284
|
+
log.warning(f"Typed text failed with {str(err)}")
|
|
285
|
+
self.action_log.append(f"Tried to type text '{text}' into element with selector {selector} at {x=},{y=} but got an error")
|
|
286
|
+
|
|
287
|
+
return (x, y)
|
|
288
|
+
|
|
289
|
+
def take_screenshot(self, final=False, full_page=False, mark_action=None):
|
|
290
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
291
|
+
parsed_url = urllib.parse.urlparse(self.page.url)
|
|
292
|
+
|
|
293
|
+
url_path = parsed_url.path
|
|
294
|
+
if url_path == "/":
|
|
295
|
+
url_path = "index.html"
|
|
296
|
+
if final:
|
|
297
|
+
screenshot_path = os.path.join(self.screenshot_dir, f"final/{timestamp}_{url_path}.png")
|
|
298
|
+
else:
|
|
299
|
+
screenshot_path = os.path.join(self.screenshot_dir, f"{timestamp}_{url_path}.png")
|
|
300
|
+
self.page.screenshot(path=screenshot_path, full_page=full_page)
|
|
301
|
+
|
|
302
|
+
if mark_action:
|
|
303
|
+
self.mark_screenshot(screenshot_path, mark_action)
|
|
304
|
+
|
|
305
|
+
log.info(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
|
|
306
|
+
#self.action_log.append(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
|
|
307
|
+
self.session_screenshots.append(screenshot_path)
|
|
114
308
|
|
|
115
|
-
def take_screenshot(self):
|
|
116
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
117
|
-
screenshot_path = os.path.join(self.screenshot_dir, f"screenshot_{timestamp}.png")
|
|
118
|
-
self.page.screenshot(path=screenshot_path)
|
|
119
309
|
return screenshot_path
|
|
120
310
|
|
|
311
|
+
def mark_screenshot(self, screenshot_path, mark_action):
|
|
312
|
+
"""
|
|
313
|
+
Marks the screenshot with the specified action.
|
|
314
|
+
|
|
315
|
+
Parameters:
|
|
316
|
+
screenshot_path (str): The path to the screenshot.
|
|
317
|
+
mark_action (dict): Action details for marking the screenshot.
|
|
318
|
+
"""
|
|
319
|
+
from PIL import Image, ImageDraw
|
|
320
|
+
|
|
321
|
+
image = Image.open(screenshot_path)
|
|
322
|
+
draw = ImageDraw.Draw(image)
|
|
323
|
+
|
|
324
|
+
if mark_action['type'] == 'click':
|
|
325
|
+
x, y = mark_action['position']
|
|
326
|
+
radius = 10
|
|
327
|
+
draw.ellipse((x-radius, y-radius, x+radius, y+radius), outline='red', width=3)
|
|
328
|
+
elif mark_action['type'] == 'type':
|
|
329
|
+
x, y = mark_action['position']
|
|
330
|
+
draw.rectangle((x-5, y-5, x+5, y+5), outline='blue', width=3)
|
|
331
|
+
|
|
332
|
+
image.save(screenshot_path)
|
|
333
|
+
|
|
121
334
|
def get_latest_screenshot_path(self):
|
|
122
335
|
screenshots = sorted(
|
|
123
336
|
[f for f in os.listdir(self.screenshot_dir) if f.startswith('screenshot_')],
|
|
@@ -128,42 +341,139 @@ class BrowseWebWithImagePromptsBot:
|
|
|
128
341
|
return os.path.join(self.screenshot_dir, screenshots[0])
|
|
129
342
|
return None
|
|
130
343
|
|
|
131
|
-
def create_prompt_vars(self,
|
|
344
|
+
def create_prompt_vars(self, last_message):
|
|
132
345
|
prompt = {
|
|
133
|
-
"
|
|
134
|
-
"
|
|
346
|
+
"last_actions": self.action_log,
|
|
347
|
+
"session_goal": self.session_goal,
|
|
348
|
+
"last_message": last_message
|
|
135
349
|
}
|
|
136
350
|
return prompt
|
|
351
|
+
|
|
352
|
+
def check_llm_response(self, response):
|
|
353
|
+
if isinstance(response, dict):
|
|
354
|
+
output = response
|
|
355
|
+
elif isinstance(response, str):
|
|
356
|
+
output = json.loads(response)
|
|
357
|
+
|
|
358
|
+
#TODO: more validation
|
|
359
|
+
log.info(f'Response: {output=}')
|
|
360
|
+
|
|
361
|
+
if 'status' not in output:
|
|
362
|
+
log.error(f'Response did not contain status')
|
|
363
|
+
|
|
364
|
+
if 'new_instructions' not in output:
|
|
365
|
+
log.warning(f'Response did not include new_instructions')
|
|
366
|
+
|
|
367
|
+
if 'message' not in output:
|
|
368
|
+
log.warning(f'Response did not include message')
|
|
369
|
+
|
|
370
|
+
return output
|
|
137
371
|
|
|
138
|
-
def send_screenshot_to_llm(self, screenshot_path,
|
|
372
|
+
def send_screenshot_to_llm(self, screenshot_path, last_message):
|
|
139
373
|
with open(screenshot_path, "rb") as image_file:
|
|
140
374
|
encoded_image = base64.b64encode(image_file.read()).decode('utf-8')
|
|
141
375
|
|
|
142
|
-
prompt_vars = self.
|
|
376
|
+
prompt_vars = self.create_prompt_vars(last_message)
|
|
143
377
|
response = self.send_prompt_to_llm(prompt_vars, encoded_image) # Sending prompt and image separately
|
|
144
|
-
|
|
378
|
+
|
|
379
|
+
return self.check_llm_response(response)
|
|
145
380
|
|
|
146
381
|
def send_prompt_to_llm(self, prompt_vars, screenshot_base64):
|
|
147
|
-
raise NotImplementedError("
|
|
382
|
+
raise NotImplementedError("""
|
|
383
|
+
This method should be implemented by subclasses: `def send_prompt_to_llm(self, prompt_vars, screenshot_base64)`")
|
|
384
|
+
prompt = {
|
|
385
|
+
"last_actions": self.action_log,
|
|
386
|
+
"session_goal": self.session_goal,
|
|
387
|
+
}
|
|
388
|
+
""")
|
|
148
389
|
|
|
149
390
|
def close(self):
|
|
150
391
|
self.save_cookies()
|
|
151
392
|
self.browser.close()
|
|
152
393
|
self.playwright.stop()
|
|
153
394
|
|
|
154
|
-
def execute_instructions(self, instructions):
|
|
395
|
+
def execute_instructions(self, instructions: list, last_message: str=None):
|
|
396
|
+
if not instructions:
|
|
397
|
+
log.info("No instructions found, returning immediately")
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
if self.steps >= self.max_steps:
|
|
401
|
+
log.warning(f"Reached the maximum number of steps: {self.max_steps}")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
if not isinstance(instructions, list):
|
|
405
|
+
log.error(f"{instructions} {type(instructions)}")
|
|
155
406
|
for instruction in instructions:
|
|
407
|
+
mark_action = None
|
|
408
|
+
if not isinstance(instruction, dict):
|
|
409
|
+
log.error(f"{instruction} {type(instruction)}")
|
|
156
410
|
action = instruction['action']
|
|
157
411
|
if action == 'navigate':
|
|
158
412
|
self.navigate(instruction['url'])
|
|
159
413
|
elif action == 'click':
|
|
160
|
-
self.click(instruction['selector'])
|
|
414
|
+
x,y = self.click(instruction['selector'])
|
|
415
|
+
if (x,y) != (0,0):
|
|
416
|
+
mark_action = {'type':'click', 'position': (x,y)}
|
|
161
417
|
elif action == 'scroll':
|
|
162
|
-
self.scroll(instruction.get('direction', 'down'),
|
|
418
|
+
self.scroll(instruction.get('direction', 'down'),
|
|
419
|
+
int(instruction.get('amount', 1))
|
|
420
|
+
)
|
|
163
421
|
elif action == 'type':
|
|
164
|
-
self.type_text(instruction['selector'], instruction['text'])
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
422
|
+
x,y = self.type_text(instruction['selector'], instruction['text'])
|
|
423
|
+
if (x,y) != (0,0):
|
|
424
|
+
mark_action = {'type':'type', 'position': (x,y)}
|
|
425
|
+
self.steps += 1
|
|
426
|
+
if self.steps >= self.max_steps:
|
|
427
|
+
log.warning(f"Reached the maximum number of steps: {self.max_steps}")
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
screenshot_path = self.take_screenshot(mark_action=mark_action)
|
|
431
|
+
next_browser_instructions = self.send_screenshot_to_llm(
|
|
432
|
+
screenshot_path,
|
|
433
|
+
last_message=last_message)
|
|
434
|
+
|
|
435
|
+
return next_browser_instructions
|
|
436
|
+
|
|
437
|
+
def start_session(self, instructions, session_goal):
|
|
438
|
+
self.session_goal = session_goal
|
|
439
|
+
|
|
440
|
+
if not instructions:
|
|
441
|
+
instructions = [{'action': 'navigate', 'url': self.website_name}]
|
|
442
|
+
|
|
443
|
+
next_instructions = self.execute_instructions(instructions)
|
|
444
|
+
|
|
445
|
+
in_session = True
|
|
446
|
+
while in_session:
|
|
447
|
+
if next_instructions and 'status' in next_instructions:
|
|
448
|
+
if next_instructions['status'] == 'in-progress':
|
|
449
|
+
log.info(f'Browser message: {next_instructions.get('message')}')
|
|
450
|
+
if 'new_instructions' not in next_instructions:
|
|
451
|
+
log.error('Browser status: "in-progress" but no new_instructions')
|
|
452
|
+
last_message = next_instructions['message']
|
|
453
|
+
self.action_log.append(last_message)
|
|
454
|
+
next_instructions = self.execute_instructions(
|
|
455
|
+
next_instructions['new_instructions'],
|
|
456
|
+
last_message=last_message)
|
|
457
|
+
else:
|
|
458
|
+
log.info(f'Session finished due to status={next_instructions["status"]}')
|
|
459
|
+
in_session=False
|
|
460
|
+
break
|
|
461
|
+
else:
|
|
462
|
+
log.info('Session finished due to next_instructions being empty')
|
|
463
|
+
in_session=False
|
|
464
|
+
break
|
|
465
|
+
|
|
466
|
+
log.info("Session finished")
|
|
467
|
+
final_path = self.take_screenshot(final=True)
|
|
468
|
+
self.close()
|
|
469
|
+
self.save_action_log()
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
"website": self.website_name,
|
|
473
|
+
"log": self.action_log,
|
|
474
|
+
"next_instructions": next_instructions,
|
|
475
|
+
"session_screenshots": self.session_screenshots,
|
|
476
|
+
"final_page": final_path,
|
|
477
|
+
"session_goal": self.session_goal
|
|
478
|
+
}
|
|
169
479
|
|
sunholo/utils/parsers.py
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
import re
|
|
15
15
|
import hashlib
|
|
16
|
+
import urllib.parse
|
|
16
17
|
|
|
17
18
|
def validate_extension_id(ext_id):
|
|
18
19
|
"""
|
|
@@ -183,4 +184,8 @@ def escape_braces(text):
|
|
|
183
184
|
# Replace single braces with double braces
|
|
184
185
|
text = re.sub(r'(?<!{){(?!{)', '{{', text) # Replace '{' with '{{' if not already double braced
|
|
185
186
|
text = re.sub(r'(?<!})}(?!})', '}}', text) # Replace '}' with '}}' if not already double braced
|
|
186
|
-
return text
|
|
187
|
+
return text
|
|
188
|
+
|
|
189
|
+
def get_clean_website_name(url):
|
|
190
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
191
|
+
return parsed_url.netloc
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sunholo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.74.1
|
|
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.
|
|
6
|
+
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.74.1.tar.gz
|
|
7
7
|
Author: Holosun ApS
|
|
8
8
|
Author-email: multivac@sunholo.com
|
|
9
9
|
License: Apache License, Version 2.0
|
|
@@ -58,6 +58,7 @@ Requires-Dist: pg8000 ; extra == 'all'
|
|
|
58
58
|
Requires-Dist: pgvector ; extra == 'all'
|
|
59
59
|
Requires-Dist: pillow ; extra == 'all'
|
|
60
60
|
Requires-Dist: playwright ; extra == 'all'
|
|
61
|
+
Requires-Dist: psutil ; extra == 'all'
|
|
61
62
|
Requires-Dist: psycopg2-binary ; extra == 'all'
|
|
62
63
|
Requires-Dist: pypdf ; extra == 'all'
|
|
63
64
|
Requires-Dist: python-socketio ; extra == 'all'
|
|
@@ -67,7 +68,7 @@ Requires-Dist: supabase ; extra == 'all'
|
|
|
67
68
|
Requires-Dist: tabulate ; extra == 'all'
|
|
68
69
|
Requires-Dist: tantivy ; extra == 'all'
|
|
69
70
|
Requires-Dist: tiktoken ; extra == 'all'
|
|
70
|
-
Requires-Dist: unstructured[local-inference] ; extra == 'all'
|
|
71
|
+
Requires-Dist: unstructured[local-inference] ==0.14.9 ; extra == 'all'
|
|
71
72
|
Provides-Extra: anthropic
|
|
72
73
|
Requires-Dist: langchain-anthropic >=0.1.13 ; extra == 'anthropic'
|
|
73
74
|
Provides-Extra: cli
|
|
@@ -114,10 +115,11 @@ Requires-Dist: tiktoken ; extra == 'openai'
|
|
|
114
115
|
Provides-Extra: pipeline
|
|
115
116
|
Requires-Dist: GitPython ; extra == 'pipeline'
|
|
116
117
|
Requires-Dist: lark ; extra == 'pipeline'
|
|
118
|
+
Requires-Dist: psutil ; extra == 'pipeline'
|
|
117
119
|
Requires-Dist: pypdf ; extra == 'pipeline'
|
|
118
120
|
Requires-Dist: pytesseract ; extra == 'pipeline'
|
|
119
121
|
Requires-Dist: tabulate ; extra == 'pipeline'
|
|
120
|
-
Requires-Dist: unstructured[local-inference] ; extra == 'pipeline'
|
|
122
|
+
Requires-Dist: unstructured[local-inference] ==0.14.9 ; extra == 'pipeline'
|
|
121
123
|
Provides-Extra: tools
|
|
122
124
|
Requires-Dist: openapi-spec-validator ; extra == 'tools'
|
|
123
125
|
Requires-Dist: playwright ; extra == 'tools'
|
|
@@ -33,7 +33,7 @@ sunholo/chunker/pdfs.py,sha256=daCZ1xjn1YvxlifIyxskWNpLJLe-Q9D_Jq12MWx3tZo,2473
|
|
|
33
33
|
sunholo/chunker/publish.py,sha256=tiO615A2uo_ZjzdFDzNH1PL_1kJeLMUQwLJ4w67rNIc,2932
|
|
34
34
|
sunholo/chunker/splitter.py,sha256=jtGfi_ZdhVdyFhfw0e4ynEpmwIyrxQtV63OituYWy6o,6729
|
|
35
35
|
sunholo/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
sunholo/cli/chat_vac.py,sha256=
|
|
36
|
+
sunholo/cli/chat_vac.py,sha256=MjwGJQUJOkHV4vLAlhyYVQ02JoI5pE7zaLSSaBfcTco,23019
|
|
37
37
|
sunholo/cli/cli.py,sha256=u70fcSQzQx2iPvE23SVCVYRFabmZ-XtgEd6vHcrABi0,3725
|
|
38
38
|
sunholo/cli/cli_init.py,sha256=JMZ9AX2cPDZ-_mv3adiv2ToFVNyRPtjk9Biszl1kiR0,2358
|
|
39
39
|
sunholo/cli/configs.py,sha256=QUM9DvKOdZmEQRM5uI3Nh887T0YDiSMr7O240zTLqws,4546
|
|
@@ -98,8 +98,8 @@ sunholo/streaming/stream_lookup.py,sha256=uTTUjf96mV7OCc-Sc8N09Fpu5g0T_mD_HbSziv
|
|
|
98
98
|
sunholo/streaming/streaming.py,sha256=9z6pXINEopuL_Z1RnmgXAoZJum9dzyuOxqYtEYnjf8w,16405
|
|
99
99
|
sunholo/summarise/__init__.py,sha256=MZk3dblUMODcPb1crq4v-Z508NrFIpkSWNf9FIO8BcU,38
|
|
100
100
|
sunholo/summarise/summarise.py,sha256=C3HhjepTjUhUC8FLk4jMQIBvq1BcORniwuTFHjPVhVo,3784
|
|
101
|
-
sunholo/tools/__init__.py,sha256=
|
|
102
|
-
sunholo/tools/web_browser.py,sha256=
|
|
101
|
+
sunholo/tools/__init__.py,sha256=5NuYpwwTX81qGUWvgwfItoSLXteNnp7KjgD7IPZUFjI,53
|
|
102
|
+
sunholo/tools/web_browser.py,sha256=ElwIBtVptyYcPd0wo7WXLNYCC02FJL_Lv3cfTzOJpnQ,19663
|
|
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
|
|
@@ -108,7 +108,7 @@ sunholo/utils/config_class.py,sha256=uyAsPXdxOY47CbQ8RifhUDL2BlxWP2QI-DIWBNlv6yk
|
|
|
108
108
|
sunholo/utils/config_schema.py,sha256=Wv-ncitzljOhgbDaq9qnFqH5LCuxNv59dTGDWgd1qdk,4189
|
|
109
109
|
sunholo/utils/gcp.py,sha256=uueODEpA-P6O15-t0hmcGC9dONLO_hLfzSsSoQnkUss,4854
|
|
110
110
|
sunholo/utils/gcp_project.py,sha256=0ozs6tzI4qEvEeXb8MxLnCdEVoWKxlM6OH05htj7_tc,1325
|
|
111
|
-
sunholo/utils/parsers.py,sha256=
|
|
111
|
+
sunholo/utils/parsers.py,sha256=aCIT08VjVbu8E3BAxepIiqFQa8zwu4bTgEstU_qjyg8,5414
|
|
112
112
|
sunholo/utils/timedelta.py,sha256=BbLabEx7_rbErj_YbNM0MBcaFN76DC4PTe4zD2ucezg,493
|
|
113
113
|
sunholo/utils/user_ids.py,sha256=SQd5_H7FE7vcTZp9AQuQDWBXd4FEEd7TeVMQe1H4Ny8,292
|
|
114
114
|
sunholo/utils/version.py,sha256=P1QAJQdZfT2cMqdTSmXmcxrD2PssMPEGM-WI6083Fck,237
|
|
@@ -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.
|
|
121
|
-
sunholo-0.
|
|
122
|
-
sunholo-0.
|
|
123
|
-
sunholo-0.
|
|
124
|
-
sunholo-0.
|
|
125
|
-
sunholo-0.
|
|
120
|
+
sunholo-0.74.1.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
|
|
121
|
+
sunholo-0.74.1.dist-info/METADATA,sha256=6QFlkGilosGyFUklfh5uzkTD4ghMdfCNuzwyLmiSyCE,7010
|
|
122
|
+
sunholo-0.74.1.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
|
123
|
+
sunholo-0.74.1.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
|
|
124
|
+
sunholo-0.74.1.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
|
|
125
|
+
sunholo-0.74.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|