sunholo 0.73.3__tar.gz → 0.74.1__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 (136) hide show
  1. {sunholo-0.73.3 → sunholo-0.74.1}/PKG-INFO +6 -4
  2. {sunholo-0.73.3 → sunholo-0.74.1}/setup.py +5 -3
  3. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/chat_vac.py +4 -0
  4. sunholo-0.74.1/sunholo/tools/__init__.py +1 -0
  5. sunholo-0.74.1/sunholo/tools/web_browser.py +479 -0
  6. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/parsers.py +6 -1
  7. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo.egg-info/PKG-INFO +6 -4
  8. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo.egg-info/requires.txt +4 -2
  9. sunholo-0.73.3/sunholo/tools/__init__.py +0 -0
  10. sunholo-0.73.3/sunholo/tools/web_browser.py +0 -169
  11. {sunholo-0.73.3 → sunholo-0.74.1}/LICENSE.txt +0 -0
  12. {sunholo-0.73.3 → sunholo-0.74.1}/MANIFEST.in +0 -0
  13. {sunholo-0.73.3 → sunholo-0.74.1}/README.md +0 -0
  14. {sunholo-0.73.3 → sunholo-0.74.1}/setup.cfg +0 -0
  15. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/__init__.py +0 -0
  16. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/__init__.py +0 -0
  17. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/chat_history.py +0 -0
  18. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/dispatch_to_qa.py +0 -0
  19. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/fastapi/__init__.py +0 -0
  20. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/fastapi/base.py +0 -0
  21. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/fastapi/qna_routes.py +0 -0
  22. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/flask/__init__.py +0 -0
  23. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/flask/base.py +0 -0
  24. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/flask/qna_routes.py +0 -0
  25. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/flask/vac_routes.py +0 -0
  26. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/langserve.py +0 -0
  27. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/pubsub.py +0 -0
  28. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/route.py +0 -0
  29. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/special_commands.py +0 -0
  30. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/agents/swagger.py +0 -0
  31. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/archive/__init__.py +0 -0
  32. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/archive/archive.py +0 -0
  33. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/auth/__init__.py +0 -0
  34. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/auth/run.py +0 -0
  35. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/bots/__init__.py +0 -0
  36. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/bots/discord.py +0 -0
  37. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/bots/github_webhook.py +0 -0
  38. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/bots/webapp.py +0 -0
  39. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/__init__.py +0 -0
  40. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/data_to_embed_pubsub.py +0 -0
  41. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/doc_handling.py +0 -0
  42. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/images.py +0 -0
  43. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/loaders.py +0 -0
  44. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/message_data.py +0 -0
  45. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/pdfs.py +0 -0
  46. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/publish.py +0 -0
  47. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/chunker/splitter.py +0 -0
  48. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/__init__.py +0 -0
  49. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/cli.py +0 -0
  50. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/cli_init.py +0 -0
  51. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/configs.py +0 -0
  52. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/deploy.py +0 -0
  53. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/embedder.py +0 -0
  54. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/merge_texts.py +0 -0
  55. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/run_proxy.py +0 -0
  56. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/sun_rich.py +0 -0
  57. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/cli/swagger.py +0 -0
  58. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/components/__init__.py +0 -0
  59. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/components/llm.py +0 -0
  60. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/components/retriever.py +0 -0
  61. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/components/vectorstore.py +0 -0
  62. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/__init__.py +0 -0
  63. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/alloydb.py +0 -0
  64. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/alloydb_client.py +0 -0
  65. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/database.py +0 -0
  66. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/lancedb.py +0 -0
  67. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/sql/sb/create_function.sql +0 -0
  68. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/sql/sb/create_function_time.sql +0 -0
  69. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/sql/sb/create_table.sql +0 -0
  70. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  71. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/sql/sb/return_sources.sql +0 -0
  72. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/sql/sb/setup.sql +0 -0
  73. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/static_dbs.py +0 -0
  74. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/database/uuid.py +0 -0
  75. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/discovery_engine/__init__.py +0 -0
  76. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/discovery_engine/chunker_handler.py +0 -0
  77. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/discovery_engine/create_new.py +0 -0
  78. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  79. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/embedder/__init__.py +0 -0
  80. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/embedder/embed_chunk.py +0 -0
  81. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/gcs/__init__.py +0 -0
  82. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/gcs/add_file.py +0 -0
  83. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/gcs/download_url.py +0 -0
  84. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/gcs/metadata.py +0 -0
  85. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/invoke/__init__.py +0 -0
  86. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/invoke/invoke_vac_utils.py +0 -0
  87. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/langfuse/__init__.py +0 -0
  88. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/langfuse/callback.py +0 -0
  89. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/langfuse/prompts.py +0 -0
  90. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/llamaindex/__init__.py +0 -0
  91. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/llamaindex/generate.py +0 -0
  92. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/llamaindex/get_files.py +0 -0
  93. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/llamaindex/import_files.py +0 -0
  94. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/logging.py +0 -0
  95. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/lookup/__init__.py +0 -0
  96. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/lookup/model_lookup.yaml +0 -0
  97. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/patches/__init__.py +0 -0
  98. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/patches/langchain/__init__.py +0 -0
  99. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/patches/langchain/lancedb.py +0 -0
  100. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/patches/langchain/vertexai.py +0 -0
  101. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/pubsub/__init__.py +0 -0
  102. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/pubsub/process_pubsub.py +0 -0
  103. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/pubsub/pubsub_manager.py +0 -0
  104. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/qna/__init__.py +0 -0
  105. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/qna/parsers.py +0 -0
  106. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/qna/retry.py +0 -0
  107. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/streaming/__init__.py +0 -0
  108. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/streaming/content_buffer.py +0 -0
  109. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/streaming/langserve.py +0 -0
  110. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/streaming/stream_lookup.py +0 -0
  111. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/streaming/streaming.py +0 -0
  112. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/summarise/__init__.py +0 -0
  113. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/summarise/summarise.py +0 -0
  114. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/__init__.py +0 -0
  115. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/api_key.py +0 -0
  116. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/big_context.py +0 -0
  117. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/config.py +0 -0
  118. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/config_class.py +0 -0
  119. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/config_schema.py +0 -0
  120. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/gcp.py +0 -0
  121. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/gcp_project.py +0 -0
  122. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/timedelta.py +0 -0
  123. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/user_ids.py +0 -0
  124. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/utils/version.py +0 -0
  125. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/vertex/__init__.py +0 -0
  126. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/vertex/extensions_class.py +0 -0
  127. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/vertex/init.py +0 -0
  128. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/vertex/memory_tools.py +0 -0
  129. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo/vertex/safety.py +0 -0
  130. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo.egg-info/SOURCES.txt +0 -0
  131. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo.egg-info/dependency_links.txt +0 -0
  132. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo.egg-info/entry_points.txt +0 -0
  133. {sunholo-0.73.3 → sunholo-0.74.1}/sunholo.egg-info/top_level.txt +0 -0
  134. {sunholo-0.73.3 → sunholo-0.74.1}/tests/test_chat_history.py +0 -0
  135. {sunholo-0.73.3 → sunholo-0.74.1}/tests/test_chunker.py +0 -0
  136. {sunholo-0.73.3 → sunholo-0.74.1}/tests/test_config.py +0 -0
@@ -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: cli
72
73
  Requires-Dist: jsonschema>=4.21.1; extra == "cli"
73
74
  Requires-Dist: rich; extra == "cli"
@@ -83,10 +84,11 @@ Requires-Dist: tantivy; extra == "database"
83
84
  Provides-Extra: pipeline
84
85
  Requires-Dist: GitPython; extra == "pipeline"
85
86
  Requires-Dist: lark; extra == "pipeline"
87
+ Requires-Dist: psutil; extra == "pipeline"
86
88
  Requires-Dist: pypdf; extra == "pipeline"
87
89
  Requires-Dist: pytesseract; extra == "pipeline"
88
90
  Requires-Dist: tabulate; extra == "pipeline"
89
- Requires-Dist: unstructured[local-inference]; extra == "pipeline"
91
+ Requires-Dist: unstructured[local-inference]==0.14.9; extra == "pipeline"
90
92
  Provides-Extra: gcp
91
93
  Requires-Dist: google-api-python-client; extra == "gcp"
92
94
  Requires-Dist: google-cloud-alloydb-connector[pg8000]; extra == "gcp"
@@ -1,7 +1,7 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
3
  # Define your base version
4
- version = '0.73.3'
4
+ version = '0.74.1'
5
5
 
6
6
  setup(
7
7
  name='sunholo',
@@ -73,6 +73,7 @@ setup(
73
73
  "pgvector",
74
74
  "pillow",
75
75
  "playwright",
76
+ "psutil",
76
77
  "psycopg2-binary",
77
78
  "pypdf",
78
79
  "python-socketio",
@@ -82,7 +83,7 @@ setup(
82
83
  "tabulate",
83
84
  "tantivy",
84
85
  "tiktoken",
85
- "unstructured[local-inference]",
86
+ "unstructured[local-inference]==0.14.9",
86
87
 
87
88
  ],
88
89
  'cli': [
@@ -102,10 +103,11 @@ setup(
102
103
  'pipeline': [
103
104
  "GitPython",
104
105
  "lark",
106
+ "psutil",
105
107
  "pypdf",
106
108
  "pytesseract",
107
109
  "tabulate",
108
- "unstructured[local-inference]",
110
+ "unstructured[local-inference]==0.14.9",
109
111
  ],
110
112
  'gcp': [
111
113
  "google-api-python-client",
@@ -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}"
@@ -0,0 +1 @@
1
+ from .web_browser import BrowseWebWithImagePromptsBot
@@ -0,0 +1,479 @@
1
+ import os
2
+ import base64
3
+ import json
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
+
11
+ try:
12
+ from playwright.sync_api import sync_playwright, Response
13
+ except ImportError:
14
+ sync_playwright = None
15
+ Response = None
16
+
17
+ class BrowseWebWithImagePromptsBot:
18
+ """
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:
68
+
69
+ ```python
70
+ class ProductionBot(BrowseWebWithImagePromptsBot):
71
+ def send_prompt_to_llm(self, prompt_vars, screenshot_base64):
72
+ # Implement the actual logic to send the prompt and screenshot to the LLM and return the response
73
+ api_url = "https://api.example.com/process" # Replace with the actual LLM API endpoint
74
+ headers = {"Content-Type": "application/json"}
75
+ data = {
76
+ "prompt": prompt_vars,
77
+ "screenshot": screenshot_base64
78
+ }
79
+ response = requests.post(api_url, headers=headers, data=json.dumps(data))
80
+ return response.text # Assuming the response is in JSON format
81
+
82
+ @app.route('/run-bot', methods=['POST'])
83
+ def run_bot():
84
+ data = request.json
85
+ session_id = data.get('session_id')
86
+ website_name = data.get('website_name')
87
+ browser_type = data.get('browser_type', 'chromium')
88
+ current_action_description = data.get('current_action_description', "")
89
+ session_goal = data.get('session_goal', "")
90
+
91
+ bot = ProductionBot(session_id=session_id, website_name=website_name, browser_type=browser_type, headless=True)
92
+
93
+ # Check if initial instructions are provided
94
+ initial_instructions = data.get('instructions')
95
+ if initial_instructions:
96
+ bot.execute_instructions(initial_instructions)
97
+ else:
98
+ bot.execute_instructions([{'action':'navigate', 'url': website_name}])
99
+
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)
104
+
105
+ # Take final screenshot
106
+ bot.take_screenshot()
107
+
108
+ bot.close()
109
+
110
+ return jsonify({"status": "completed", "new_instructions": new_instructions})
111
+
112
+ if __name__ == "__main__":
113
+ app.run(host='0.0.0.0', port=8080)
114
+ ```
115
+ """
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
+
124
+ if not sync_playwright:
125
+ raise ImportError("playright needed for BrowseWebWithImagePromptsBot class - install via `pip install sunholo[tools]`")
126
+
127
+ self.session_id = session_id or datetime.now().strftime("%Y%m%d%H%M%S")
128
+ self.website_name = website_name
129
+ self.browser_type = browser_type
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}"
133
+ os.makedirs(self.screenshot_dir, exist_ok=True)
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")
136
+ self.playwright = sync_playwright().start()
137
+
138
+ if browser_type == 'chromium':
139
+ self.browser = self.playwright.chromium.launch(headless=headless)
140
+ elif browser_type == 'firefox':
141
+ self.browser = self.playwright.firefox.launch(headless=headless)
142
+ elif browser_type == 'webkit':
143
+ self.browser = self.playwright.webkit.launch(headless=headless)
144
+ else:
145
+ raise ValueError(f"Unsupported browser type: {browser_type}")
146
+
147
+ self.context = self.browser.new_context()
148
+ self.page = self.context.new_page()
149
+ self.load_cookies()
150
+ self.action_log = []
151
+ self.session_goal = None
152
+ self.session_screenshots = []
153
+
154
+ def load_cookies(self):
155
+ if os.path.exists(self.cookie_file):
156
+ with open(self.cookie_file, 'r') as f:
157
+ cookies = json.load(f)
158
+ self.context.add_cookies(cookies)
159
+
160
+ def save_cookies(self):
161
+ cookies = self.context.cookies()
162
+ with open(self.cookie_file, 'w') as f:
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
174
+
175
+ def navigate(self, 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")
201
+
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")
240
+
241
+ return (x,y)
242
+
243
+ def scroll(self, direction='down', amount=100):
244
+ try:
245
+ if direction == 'down':
246
+ self.page.mouse.wheel(0, amount)
247
+ elif direction == 'up':
248
+ self.page.mouse.wheel(0, -amount)
249
+ elif direction == 'left':
250
+ self.page.mouse.wheel(-amount, 0)
251
+ elif direction == 'right':
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=}")
280
+
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)
308
+
309
+ return screenshot_path
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
+
334
+ def get_latest_screenshot_path(self):
335
+ screenshots = sorted(
336
+ [f for f in os.listdir(self.screenshot_dir) if f.startswith('screenshot_')],
337
+ key=lambda x: os.path.getmtime(os.path.join(self.screenshot_dir, x)),
338
+ reverse=True
339
+ )
340
+ if screenshots:
341
+ return os.path.join(self.screenshot_dir, screenshots[0])
342
+ return None
343
+
344
+ def create_prompt_vars(self, last_message):
345
+ prompt = {
346
+ "last_actions": self.action_log,
347
+ "session_goal": self.session_goal,
348
+ "last_message": last_message
349
+ }
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
371
+
372
+ def send_screenshot_to_llm(self, screenshot_path, last_message):
373
+ with open(screenshot_path, "rb") as image_file:
374
+ encoded_image = base64.b64encode(image_file.read()).decode('utf-8')
375
+
376
+ prompt_vars = self.create_prompt_vars(last_message)
377
+ response = self.send_prompt_to_llm(prompt_vars, encoded_image) # Sending prompt and image separately
378
+
379
+ return self.check_llm_response(response)
380
+
381
+ 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
+ """)
389
+
390
+ def close(self):
391
+ self.save_cookies()
392
+ self.browser.close()
393
+ self.playwright.stop()
394
+
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)}")
406
+ for instruction in instructions:
407
+ mark_action = None
408
+ if not isinstance(instruction, dict):
409
+ log.error(f"{instruction} {type(instruction)}")
410
+ action = instruction['action']
411
+ if action == 'navigate':
412
+ self.navigate(instruction['url'])
413
+ elif action == 'click':
414
+ x,y = self.click(instruction['selector'])
415
+ if (x,y) != (0,0):
416
+ mark_action = {'type':'click', 'position': (x,y)}
417
+ elif action == 'scroll':
418
+ self.scroll(instruction.get('direction', 'down'),
419
+ int(instruction.get('amount', 1))
420
+ )
421
+ elif action == 'type':
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
+ }
479
+
@@ -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: cli
72
73
  Requires-Dist: jsonschema>=4.21.1; extra == "cli"
73
74
  Requires-Dist: rich; extra == "cli"
@@ -83,10 +84,11 @@ Requires-Dist: tantivy; extra == "database"
83
84
  Provides-Extra: pipeline
84
85
  Requires-Dist: GitPython; extra == "pipeline"
85
86
  Requires-Dist: lark; extra == "pipeline"
87
+ Requires-Dist: psutil; extra == "pipeline"
86
88
  Requires-Dist: pypdf; extra == "pipeline"
87
89
  Requires-Dist: pytesseract; extra == "pipeline"
88
90
  Requires-Dist: tabulate; extra == "pipeline"
89
- Requires-Dist: unstructured[local-inference]; extra == "pipeline"
91
+ Requires-Dist: unstructured[local-inference]==0.14.9; extra == "pipeline"
90
92
  Provides-Extra: gcp
91
93
  Requires-Dist: google-api-python-client; extra == "gcp"
92
94
  Requires-Dist: google-cloud-alloydb-connector[pg8000]; extra == "gcp"
@@ -39,6 +39,7 @@ pg8000
39
39
  pgvector
40
40
  pillow
41
41
  playwright
42
+ psutil
42
43
  psycopg2-binary
43
44
  pypdf
44
45
  python-socketio
@@ -48,7 +49,7 @@ supabase
48
49
  tabulate
49
50
  tantivy
50
51
  tiktoken
51
- unstructured[local-inference]
52
+ unstructured[local-inference]==0.14.9
52
53
 
53
54
  [anthropic]
54
55
  langchain-anthropic>=0.1.13
@@ -102,10 +103,11 @@ tiktoken
102
103
  [pipeline]
103
104
  GitPython
104
105
  lark
106
+ psutil
105
107
  pypdf
106
108
  pytesseract
107
109
  tabulate
108
- unstructured[local-inference]
110
+ unstructured[local-inference]==0.14.9
109
111
 
110
112
  [tools]
111
113
  openapi-spec-validator
File without changes