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 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
@@ -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
- Examples:
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, prompt, screenshot_base64):
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": 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
- next_goal = data.get('next_goal', "")
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 if no instructions provided
44
- if not initial_instructions:
45
- screenshot_path = bot.take_screenshot()
46
- new_instructions = bot.send_screenshot_to_llm(screenshot_path, current_action_description, next_goal)
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
- def __init__(self, session_id, website_name, browser_type='chromium', headless=True):
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
- self.session_id = session_id
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.screenshot_dir = f"{website_name}_{session_id}"
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
- self.page.goto(url)
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 click(self, selector):
99
- self.page.click(selector)
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
- def scroll(self, direction='down', amount=1):
102
- for _ in range(amount):
241
+ return (x,y)
242
+
243
+ def scroll(self, direction='down', amount=100):
244
+ try:
103
245
  if direction == 'down':
104
- self.page.evaluate("window.scrollBy(0, window.innerHeight)")
246
+ self.page.mouse.wheel(0, amount)
105
247
  elif direction == 'up':
106
- self.page.evaluate("window.scrollBy(0, -window.innerHeight)")
248
+ self.page.mouse.wheel(0, -amount)
107
249
  elif direction == 'left':
108
- self.page.evaluate("window.scrollBy(-window.innerWidth, 0)")
250
+ self.page.mouse.wheel(-amount, 0)
109
251
  elif direction == 'right':
110
- self.page.evaluate("window.scrollBy(window.innerWidth, 0)")
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
- def type_text(self, selector, text):
113
- self.page.fill(selector, text)
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, current_action_description, next_goal):
344
+ def create_prompt_vars(self, last_message):
132
345
  prompt = {
133
- "current_action_description": current_action_description,
134
- "next_goal": next_goal,
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, current_action_description="", next_goal=""):
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.create_prompt(current_action_description, next_goal)
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
- return json.loads(response)
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("This method should be implemented by subclasses: `def send_prompt_to_llm(self, prompt_vars, screenshot_base64)`")
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'), instruction.get('amount', 1))
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
- screenshot_path = self.take_screenshot()
166
- new_instructions = self.send_screenshot_to_llm(screenshot_path, instruction.get('description', ''), instruction.get('next_goal', ''))
167
- if new_instructions:
168
- self.execute_instructions(new_instructions)
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.73.3
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.73.3.tar.gz
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=9NNdIt0pWLZandxtav9Eu7ru9WOBHjh1TN1oP3pqMbM,22852
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=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
- sunholo/tools/web_browser.py,sha256=NgsAeVcndl-vMAbAfIzDJ8eRfCh5LDZan16OCNEKFmI,7094
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=z98cQ1v2_ScnqHxCtApNeAN2the8MdvS6RpKL6vWyOU,5287
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.73.3.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
121
- sunholo-0.73.3.dist-info/METADATA,sha256=oZNyJJifnIAdT0EMyILv8u5vKUaR4lzyPUmNcXU9bvw,6909
122
- sunholo-0.73.3.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
123
- sunholo-0.73.3.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
124
- sunholo-0.73.3.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
125
- sunholo-0.73.3.dist-info/RECORD,,
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,,