sunholo 0.74.6__tar.gz → 0.74.8__tar.gz

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.
Files changed (134) hide show
  1. {sunholo-0.74.6 → sunholo-0.74.8}/PKG-INFO +2 -2
  2. {sunholo-0.74.6 → sunholo-0.74.8}/setup.py +1 -1
  3. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/tools/web_browser.py +108 -44
  4. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo.egg-info/PKG-INFO +2 -2
  5. {sunholo-0.74.6 → sunholo-0.74.8}/LICENSE.txt +0 -0
  6. {sunholo-0.74.6 → sunholo-0.74.8}/MANIFEST.in +0 -0
  7. {sunholo-0.74.6 → sunholo-0.74.8}/README.md +0 -0
  8. {sunholo-0.74.6 → sunholo-0.74.8}/setup.cfg +0 -0
  9. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/__init__.py +0 -0
  10. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/__init__.py +0 -0
  11. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/chat_history.py +0 -0
  12. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/dispatch_to_qa.py +0 -0
  13. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/fastapi/__init__.py +0 -0
  14. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/fastapi/base.py +0 -0
  15. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/fastapi/qna_routes.py +0 -0
  16. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/flask/__init__.py +0 -0
  17. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/flask/base.py +0 -0
  18. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/flask/qna_routes.py +0 -0
  19. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/flask/vac_routes.py +0 -0
  20. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/langserve.py +0 -0
  21. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/pubsub.py +0 -0
  22. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/route.py +0 -0
  23. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/special_commands.py +0 -0
  24. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/agents/swagger.py +0 -0
  25. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/archive/__init__.py +0 -0
  26. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/archive/archive.py +0 -0
  27. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/auth/__init__.py +0 -0
  28. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/auth/run.py +0 -0
  29. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/bots/__init__.py +0 -0
  30. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/bots/discord.py +0 -0
  31. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/bots/github_webhook.py +0 -0
  32. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/bots/webapp.py +0 -0
  33. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/__init__.py +0 -0
  34. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/data_to_embed_pubsub.py +0 -0
  35. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/doc_handling.py +0 -0
  36. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/images.py +0 -0
  37. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/loaders.py +0 -0
  38. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/message_data.py +0 -0
  39. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/pdfs.py +0 -0
  40. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/publish.py +0 -0
  41. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/chunker/splitter.py +0 -0
  42. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/__init__.py +0 -0
  43. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/chat_vac.py +0 -0
  44. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/cli.py +0 -0
  45. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/cli_init.py +0 -0
  46. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/configs.py +0 -0
  47. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/deploy.py +0 -0
  48. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/embedder.py +0 -0
  49. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/merge_texts.py +0 -0
  50. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/run_proxy.py +0 -0
  51. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/sun_rich.py +0 -0
  52. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/cli/swagger.py +0 -0
  53. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/components/__init__.py +0 -0
  54. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/components/llm.py +0 -0
  55. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/components/retriever.py +0 -0
  56. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/components/vectorstore.py +0 -0
  57. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/__init__.py +0 -0
  58. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/alloydb.py +0 -0
  59. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/alloydb_client.py +0 -0
  60. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/database.py +0 -0
  61. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/lancedb.py +0 -0
  62. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/sql/sb/create_function.sql +0 -0
  63. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/sql/sb/create_function_time.sql +0 -0
  64. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/sql/sb/create_table.sql +0 -0
  65. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  66. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/sql/sb/return_sources.sql +0 -0
  67. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/sql/sb/setup.sql +0 -0
  68. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/static_dbs.py +0 -0
  69. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/database/uuid.py +0 -0
  70. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/discovery_engine/__init__.py +0 -0
  71. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/discovery_engine/chunker_handler.py +0 -0
  72. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/discovery_engine/create_new.py +0 -0
  73. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  74. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/embedder/__init__.py +0 -0
  75. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/embedder/embed_chunk.py +0 -0
  76. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/gcs/__init__.py +0 -0
  77. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/gcs/add_file.py +0 -0
  78. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/gcs/download_url.py +0 -0
  79. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/gcs/metadata.py +0 -0
  80. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/invoke/__init__.py +0 -0
  81. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/invoke/invoke_vac_utils.py +0 -0
  82. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/langfuse/__init__.py +0 -0
  83. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/langfuse/callback.py +0 -0
  84. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/langfuse/prompts.py +0 -0
  85. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/llamaindex/__init__.py +0 -0
  86. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/llamaindex/generate.py +0 -0
  87. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/llamaindex/get_files.py +0 -0
  88. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/llamaindex/import_files.py +0 -0
  89. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/logging.py +0 -0
  90. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/lookup/__init__.py +0 -0
  91. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/lookup/model_lookup.yaml +0 -0
  92. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/patches/__init__.py +0 -0
  93. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/patches/langchain/__init__.py +0 -0
  94. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/patches/langchain/lancedb.py +0 -0
  95. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/patches/langchain/vertexai.py +0 -0
  96. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/pubsub/__init__.py +0 -0
  97. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/pubsub/process_pubsub.py +0 -0
  98. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/pubsub/pubsub_manager.py +0 -0
  99. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/qna/__init__.py +0 -0
  100. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/qna/parsers.py +0 -0
  101. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/qna/retry.py +0 -0
  102. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/streaming/__init__.py +0 -0
  103. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/streaming/content_buffer.py +0 -0
  104. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/streaming/langserve.py +0 -0
  105. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/streaming/stream_lookup.py +0 -0
  106. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/streaming/streaming.py +0 -0
  107. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/summarise/__init__.py +0 -0
  108. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/summarise/summarise.py +0 -0
  109. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/tools/__init__.py +0 -0
  110. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/__init__.py +0 -0
  111. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/api_key.py +0 -0
  112. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/big_context.py +0 -0
  113. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/config.py +0 -0
  114. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/config_class.py +0 -0
  115. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/config_schema.py +0 -0
  116. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/gcp.py +0 -0
  117. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/gcp_project.py +0 -0
  118. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/parsers.py +0 -0
  119. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/timedelta.py +0 -0
  120. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/user_ids.py +0 -0
  121. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/utils/version.py +0 -0
  122. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/vertex/__init__.py +0 -0
  123. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/vertex/extensions_class.py +0 -0
  124. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/vertex/init.py +0 -0
  125. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/vertex/memory_tools.py +0 -0
  126. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo/vertex/safety.py +0 -0
  127. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo.egg-info/SOURCES.txt +0 -0
  128. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo.egg-info/dependency_links.txt +0 -0
  129. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo.egg-info/entry_points.txt +0 -0
  130. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo.egg-info/requires.txt +0 -0
  131. {sunholo-0.74.6 → sunholo-0.74.8}/sunholo.egg-info/top_level.txt +0 -0
  132. {sunholo-0.74.6 → sunholo-0.74.8}/tests/test_chat_history.py +0 -0
  133. {sunholo-0.74.6 → sunholo-0.74.8}/tests/test_chunker.py +0 -0
  134. {sunholo-0.74.6 → sunholo-0.74.8}/tests/test_config.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.74.6
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.tar.gz
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
@@ -1,7 +1,7 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
3
  # Define your base version
4
- version = '0.74.6'
4
+ version = '0.74.8'
5
5
 
6
6
  setup(
7
7
  name='sunholo',
@@ -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__(self, session_id, website_name, browser_type='chromium', headless=True, max_steps=10):
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
- self.session_id = session_id or datetime.now().strftime("%Y%m%d%H%M%S")
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")
@@ -189,27 +204,57 @@ class BrowseWebWithImagePromptsBot:
189
204
  if status != 200:
190
205
  log.error(f"Failed to navigate to {url}: HTTP {status}")
191
206
  self.action_log.append(f"Tried to navigate to {url} but failed: HTTP {status} - browsing back to {previous_url}")
192
- url = previous_url
193
- self.page.goto(previous_url)
207
+ self.page.go_back()
194
208
 
195
209
  self.page.wait_for_load_state()
196
- log.info(f'Navigated to {url}')
197
- self.action_log.append(f"Navigated to {url}")
210
+ log.info(f'Navigated to {self.page.url}')
211
+ self.action_log.append(f"Navigated to {self.page.url}")
198
212
 
199
213
  except Exception as err:
200
214
  log.warning(f"navigate failed with {str(err)}")
201
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
202
245
 
203
246
  def get_locator(self, selector, by_text=True):
204
247
  if by_text:
205
- elements = self.page.locator(f"text={selector}").all()
206
- if elements:
207
- return elements[0]
208
- else:
209
- log.warning(f"No elements found with text: {selector}")
210
- return None
248
+ element = self.get_locator_via_roles_and_text(selector)
211
249
  else:
212
- return self.page.locator(selector)
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
213
258
 
214
259
  def click(self, selector, by_text=True):
215
260
  (x,y)=(0,0)
@@ -230,14 +275,14 @@ class BrowseWebWithImagePromptsBot:
230
275
  try:
231
276
  element.click()
232
277
  self.page.wait_for_load_state()
233
- log.info(f"Clicked on element with selector {selector} at {x=},{y=}")
234
- self.action_log.append(f"Clicked on element with selector {selector} at {x=},{y=}")
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=}")
235
280
 
236
281
  return (x,y)
237
282
 
238
283
  except Exception as err:
239
284
  log.warning(f"click failed with {str(err)}")
240
- self.action_log.append(f"Tried to click on element with selector {selector} at {x=},{y=} but got an error")
285
+ self.action_log.append(f"Tried to click on {element=} with {selector=} at {x=},{y=} but got an error")
241
286
 
242
287
  return (x,y)
243
288
 
@@ -262,7 +307,7 @@ class BrowseWebWithImagePromptsBot:
262
307
  (x,y)=(0,0)
263
308
  element = self.get_locator(selector, by_text=by_text)
264
309
  if element is None:
265
- self.action_log.append(f"Tried to type {text} via website text: {selector} but it was not a valid location to add text")
310
+ self.action_log.append(f"Tried to type {text} via website text: {selector} but it was not a valid element to add text")
266
311
  return (x,y)
267
312
 
268
313
  try:
@@ -276,32 +321,38 @@ class BrowseWebWithImagePromptsBot:
276
321
  try:
277
322
  element.fill(text)
278
323
  self.page.wait_for_load_state()
279
- log.info(f"Typed text '{text}' into element with selector {selector} at {x=},{y=}")
280
- self.action_log.append(f"Typed text '{text}' into element with selector {selector} at {x=},{y=}")
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=}")
281
326
 
282
327
  return (x, y)
283
328
 
284
329
  except Exception as err:
285
330
  log.warning(f"Typed text failed with {str(err)}")
286
- self.action_log.append(f"Tried to type text '{text}' into element with selector {selector} at {x=},{y=} but got an error")
331
+ self.action_log.append(f"Tried to type text '{text}' into {element=} with {selector=} at {x=},{y=} but got an error")
287
332
 
288
333
  return (x, y)
289
334
 
290
- def take_screenshot(self, final=False, full_page=False, mark_action=None):
335
+ def take_screenshot(self, full_page=False, mark_action=None):
336
+
337
+ from PIL import Image
338
+
291
339
  timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
292
340
  parsed_url = urllib.parse.urlparse(self.page.url)
293
341
 
294
342
  url_path = parsed_url.path
295
343
  if url_path == "/":
296
344
  url_path = "index.html"
297
- if final:
298
- screenshot_path = os.path.join(self.screenshot_dir, f"final/{timestamp}_{url_path}.png")
299
345
  else:
300
- screenshot_path = os.path.join(self.screenshot_dir, f"{timestamp}_{url_path}.png")
301
- self.page.screenshot(path=screenshot_path, full_page=full_page)
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')
302
349
 
303
350
  if mark_action:
304
- self.mark_screenshot(screenshot_path, mark_action)
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')
305
356
 
306
357
  log.info(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
307
358
  #self.action_log.append(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
@@ -309,17 +360,17 @@ class BrowseWebWithImagePromptsBot:
309
360
 
310
361
  return screenshot_path
311
362
 
312
- def mark_screenshot(self, screenshot_path, mark_action):
363
+ def mark_screenshot(self, screenshot_bytes, mark_action):
313
364
  """
314
365
  Marks the screenshot with the specified action.
315
366
 
316
367
  Parameters:
317
- screenshot_path (str): The path to the screenshot.
368
+ screenshot_bytes (bytes): The bytes of the screenshot.
318
369
  mark_action (dict): Action details for marking the screenshot.
319
370
  """
320
371
  from PIL import Image, ImageDraw
321
-
322
- image = Image.open(screenshot_path)
372
+ time.sleep(1) # maybe pass in bytes to avoid waiting for file to be written
373
+ image = Image.open(BytesIO(screenshot_bytes))
323
374
  draw = ImageDraw.Draw(image)
324
375
 
325
376
  if mark_action['type'] == 'click':
@@ -330,7 +381,7 @@ class BrowseWebWithImagePromptsBot:
330
381
  x, y = mark_action['position']
331
382
  draw.rectangle((x-5, y-5, x+5, y+5), outline='blue', width=3)
332
383
 
333
- image.save(screenshot_path)
384
+ return image
334
385
 
335
386
  def get_latest_screenshot_path(self):
336
387
  screenshots = sorted(
@@ -357,12 +408,18 @@ class BrowseWebWithImagePromptsBot:
357
408
  output = json.loads(response)
358
409
  elif isinstance(response, list):
359
410
  log.warning(f'Response was a list, assuming its only new_instructions: {response=}')
360
- output['new_instructions'] = response
361
- output['status'] = 'in-progress'
362
- output['message'] = 'No message was received, which is a mistake by the assistant'
411
+ output = {
412
+ 'new_instructions' : response,
413
+ 'status': 'in-progress',
414
+ 'message': 'No message was received, which is a mistake by the assistant'
415
+ }
363
416
  else:
364
417
  log.warning(f'Unknown response: {response=} {type(response)}')
365
418
  output = None
419
+
420
+ if not output:
421
+ log.error(f'Got no output from response: {response=}')
422
+ return None
366
423
 
367
424
  if 'status' not in output:
368
425
  log.error(f'Response did not contain status')
@@ -395,6 +452,8 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
395
452
  """)
396
453
 
397
454
  def close(self):
455
+ log.info(f"Session {self.session_id} finished")
456
+ self.take_screenshot()
398
457
  self.save_cookies()
399
458
  self.browser.close()
400
459
  self.playwright.stop()
@@ -443,7 +502,7 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
443
502
 
444
503
  return next_browser_instructions
445
504
 
446
- def create_gif_from_pngs(self, frame_duration=500):
505
+ def create_gif_from_pngs(self, frame_duration=300):
447
506
  """
448
507
  Creates a GIF from a folder of PNG images.
449
508
 
@@ -500,9 +559,17 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
500
559
  log.error('Browser status: "in-progress" but no new_instructions')
501
560
  last_message = next_instructions['message']
502
561
  self.action_log.append(last_message)
503
- next_instructions = self.execute_instructions(
504
- next_instructions['new_instructions'],
505
- last_message=last_message)
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
506
573
  else:
507
574
  log.info(f'Session finished due to status={next_instructions["status"]}')
508
575
  in_session=False
@@ -512,9 +579,6 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
512
579
  in_session=False
513
580
  break
514
581
 
515
- log.info("Session finished")
516
- self.take_screenshot()
517
-
518
582
  self.close()
519
583
 
520
584
  answer = None
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sunholo
3
- Version: 0.74.6
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.tar.gz
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes