sunholo 0.74.7__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.7 → sunholo-0.74.8}/PKG-INFO +2 -2
  2. {sunholo-0.74.7 → sunholo-0.74.8}/setup.py +1 -1
  3. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/tools/web_browser.py +100 -37
  4. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo.egg-info/PKG-INFO +2 -2
  5. {sunholo-0.74.7 → sunholo-0.74.8}/LICENSE.txt +0 -0
  6. {sunholo-0.74.7 → sunholo-0.74.8}/MANIFEST.in +0 -0
  7. {sunholo-0.74.7 → sunholo-0.74.8}/README.md +0 -0
  8. {sunholo-0.74.7 → sunholo-0.74.8}/setup.cfg +0 -0
  9. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/__init__.py +0 -0
  10. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/__init__.py +0 -0
  11. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/chat_history.py +0 -0
  12. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/dispatch_to_qa.py +0 -0
  13. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/fastapi/__init__.py +0 -0
  14. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/fastapi/base.py +0 -0
  15. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/fastapi/qna_routes.py +0 -0
  16. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/flask/__init__.py +0 -0
  17. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/flask/base.py +0 -0
  18. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/flask/qna_routes.py +0 -0
  19. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/flask/vac_routes.py +0 -0
  20. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/langserve.py +0 -0
  21. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/pubsub.py +0 -0
  22. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/route.py +0 -0
  23. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/special_commands.py +0 -0
  24. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/agents/swagger.py +0 -0
  25. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/archive/__init__.py +0 -0
  26. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/archive/archive.py +0 -0
  27. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/auth/__init__.py +0 -0
  28. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/auth/run.py +0 -0
  29. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/bots/__init__.py +0 -0
  30. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/bots/discord.py +0 -0
  31. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/bots/github_webhook.py +0 -0
  32. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/bots/webapp.py +0 -0
  33. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/__init__.py +0 -0
  34. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/data_to_embed_pubsub.py +0 -0
  35. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/doc_handling.py +0 -0
  36. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/images.py +0 -0
  37. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/loaders.py +0 -0
  38. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/message_data.py +0 -0
  39. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/pdfs.py +0 -0
  40. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/publish.py +0 -0
  41. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/chunker/splitter.py +0 -0
  42. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/__init__.py +0 -0
  43. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/chat_vac.py +0 -0
  44. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/cli.py +0 -0
  45. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/cli_init.py +0 -0
  46. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/configs.py +0 -0
  47. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/deploy.py +0 -0
  48. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/embedder.py +0 -0
  49. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/merge_texts.py +0 -0
  50. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/run_proxy.py +0 -0
  51. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/sun_rich.py +0 -0
  52. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/cli/swagger.py +0 -0
  53. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/components/__init__.py +0 -0
  54. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/components/llm.py +0 -0
  55. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/components/retriever.py +0 -0
  56. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/components/vectorstore.py +0 -0
  57. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/__init__.py +0 -0
  58. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/alloydb.py +0 -0
  59. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/alloydb_client.py +0 -0
  60. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/database.py +0 -0
  61. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/lancedb.py +0 -0
  62. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/sql/sb/create_function.sql +0 -0
  63. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/sql/sb/create_function_time.sql +0 -0
  64. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/sql/sb/create_table.sql +0 -0
  65. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  66. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/sql/sb/return_sources.sql +0 -0
  67. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/sql/sb/setup.sql +0 -0
  68. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/static_dbs.py +0 -0
  69. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/database/uuid.py +0 -0
  70. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/discovery_engine/__init__.py +0 -0
  71. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/discovery_engine/chunker_handler.py +0 -0
  72. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/discovery_engine/create_new.py +0 -0
  73. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  74. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/embedder/__init__.py +0 -0
  75. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/embedder/embed_chunk.py +0 -0
  76. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/gcs/__init__.py +0 -0
  77. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/gcs/add_file.py +0 -0
  78. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/gcs/download_url.py +0 -0
  79. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/gcs/metadata.py +0 -0
  80. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/invoke/__init__.py +0 -0
  81. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/invoke/invoke_vac_utils.py +0 -0
  82. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/langfuse/__init__.py +0 -0
  83. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/langfuse/callback.py +0 -0
  84. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/langfuse/prompts.py +0 -0
  85. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/llamaindex/__init__.py +0 -0
  86. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/llamaindex/generate.py +0 -0
  87. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/llamaindex/get_files.py +0 -0
  88. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/llamaindex/import_files.py +0 -0
  89. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/logging.py +0 -0
  90. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/lookup/__init__.py +0 -0
  91. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/lookup/model_lookup.yaml +0 -0
  92. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/patches/__init__.py +0 -0
  93. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/patches/langchain/__init__.py +0 -0
  94. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/patches/langchain/lancedb.py +0 -0
  95. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/patches/langchain/vertexai.py +0 -0
  96. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/pubsub/__init__.py +0 -0
  97. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/pubsub/process_pubsub.py +0 -0
  98. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/pubsub/pubsub_manager.py +0 -0
  99. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/qna/__init__.py +0 -0
  100. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/qna/parsers.py +0 -0
  101. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/qna/retry.py +0 -0
  102. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/streaming/__init__.py +0 -0
  103. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/streaming/content_buffer.py +0 -0
  104. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/streaming/langserve.py +0 -0
  105. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/streaming/stream_lookup.py +0 -0
  106. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/streaming/streaming.py +0 -0
  107. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/summarise/__init__.py +0 -0
  108. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/summarise/summarise.py +0 -0
  109. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/tools/__init__.py +0 -0
  110. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/__init__.py +0 -0
  111. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/api_key.py +0 -0
  112. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/big_context.py +0 -0
  113. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/config.py +0 -0
  114. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/config_class.py +0 -0
  115. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/config_schema.py +0 -0
  116. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/gcp.py +0 -0
  117. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/gcp_project.py +0 -0
  118. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/parsers.py +0 -0
  119. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/timedelta.py +0 -0
  120. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/user_ids.py +0 -0
  121. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/utils/version.py +0 -0
  122. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/vertex/__init__.py +0 -0
  123. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/vertex/extensions_class.py +0 -0
  124. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/vertex/init.py +0 -0
  125. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/vertex/memory_tools.py +0 -0
  126. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo/vertex/safety.py +0 -0
  127. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo.egg-info/SOURCES.txt +0 -0
  128. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo.egg-info/dependency_links.txt +0 -0
  129. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo.egg-info/entry_points.txt +0 -0
  130. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo.egg-info/requires.txt +0 -0
  131. {sunholo-0.74.7 → sunholo-0.74.8}/sunholo.egg-info/top_level.txt +0 -0
  132. {sunholo-0.74.7 → sunholo-0.74.8}/tests/test_chat_history.py +0 -0
  133. {sunholo-0.74.7 → sunholo-0.74.8}/tests/test_chunker.py +0 -0
  134. {sunholo-0.74.7 → 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.7
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.7.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.7'
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")
@@ -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
- elements = self.page.get_by_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
248
+ element = self.get_locator_via_roles_and_text(selector)
210
249
  else:
211
- 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
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 selector {selector} at {x=},{y=}")
233
- 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=}")
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 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")
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 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")
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 selector {selector} at {x=},{y=}")
279
- 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=}")
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 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")
286
332
 
287
333
  return (x, y)
288
334
 
289
- 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
+
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
- screenshot_path = os.path.join(self.screenshot_dir, f"{timestamp}_{url_path}.png")
300
- 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')
301
349
 
302
350
  if mark_action:
303
- 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')
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, screenshot_path, mark_action):
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
- screenshot_path (str): The path to the screenshot.
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(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))
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.save(screenshot_path)
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=500):
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
- next_instructions = self.execute_instructions(
505
- next_instructions['new_instructions'],
506
- 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
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.7
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.7.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