sunholo 0.73.3__tar.gz → 0.74.0__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.
- {sunholo-0.73.3 → sunholo-0.74.0}/PKG-INFO +6 -4
- {sunholo-0.73.3 → sunholo-0.74.0}/setup.py +5 -3
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/chat_vac.py +4 -0
- sunholo-0.74.0/sunholo/tools/__init__.py +1 -0
- sunholo-0.74.0/sunholo/tools/web_browser.py +355 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/parsers.py +6 -1
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo.egg-info/PKG-INFO +6 -4
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo.egg-info/requires.txt +4 -2
- sunholo-0.73.3/sunholo/tools/__init__.py +0 -0
- sunholo-0.73.3/sunholo/tools/web_browser.py +0 -169
- {sunholo-0.73.3 → sunholo-0.74.0}/LICENSE.txt +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/MANIFEST.in +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/README.md +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/setup.cfg +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/chat_history.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/dispatch_to_qa.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/fastapi/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/fastapi/base.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/fastapi/qna_routes.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/flask/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/flask/base.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/flask/qna_routes.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/flask/vac_routes.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/langserve.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/pubsub.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/route.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/special_commands.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/agents/swagger.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/archive/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/archive/archive.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/auth/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/auth/run.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/bots/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/bots/discord.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/bots/github_webhook.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/bots/webapp.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/data_to_embed_pubsub.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/doc_handling.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/images.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/loaders.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/message_data.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/pdfs.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/publish.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/chunker/splitter.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/cli.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/cli_init.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/configs.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/deploy.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/embedder.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/merge_texts.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/run_proxy.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/sun_rich.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/cli/swagger.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/components/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/components/llm.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/components/retriever.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/components/vectorstore.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/alloydb.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/alloydb_client.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/database.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/lancedb.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/sql/sb/create_function.sql +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/sql/sb/create_function_time.sql +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/sql/sb/create_table.sql +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/sql/sb/delete_source_row.sql +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/sql/sb/return_sources.sql +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/sql/sb/setup.sql +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/static_dbs.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/database/uuid.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/discovery_engine/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/discovery_engine/chunker_handler.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/discovery_engine/create_new.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/discovery_engine/discovery_engine_client.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/embedder/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/embedder/embed_chunk.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/gcs/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/gcs/add_file.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/gcs/download_url.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/gcs/metadata.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/invoke/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/invoke/invoke_vac_utils.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/langfuse/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/langfuse/callback.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/langfuse/prompts.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/llamaindex/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/llamaindex/generate.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/llamaindex/get_files.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/llamaindex/import_files.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/logging.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/lookup/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/lookup/model_lookup.yaml +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/patches/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/patches/langchain/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/patches/langchain/lancedb.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/patches/langchain/vertexai.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/pubsub/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/pubsub/process_pubsub.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/pubsub/pubsub_manager.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/qna/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/qna/parsers.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/qna/retry.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/streaming/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/streaming/content_buffer.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/streaming/langserve.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/streaming/stream_lookup.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/streaming/streaming.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/summarise/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/summarise/summarise.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/api_key.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/big_context.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/config.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/config_class.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/config_schema.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/gcp.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/gcp_project.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/timedelta.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/user_ids.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/utils/version.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/vertex/__init__.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/vertex/extensions_class.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/vertex/init.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/vertex/memory_tools.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo/vertex/safety.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo.egg-info/SOURCES.txt +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo.egg-info/dependency_links.txt +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo.egg-info/entry_points.txt +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/sunholo.egg-info/top_level.txt +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/tests/test_chat_history.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/tests/test_chunker.py +0 -0
- {sunholo-0.73.3 → sunholo-0.74.0}/tests/test_config.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sunholo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.74.0
|
|
4
4
|
Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
|
|
5
5
|
Home-page: https://github.com/sunholo-data/sunholo-py
|
|
6
|
-
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.
|
|
6
|
+
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.74.0.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.
|
|
4
|
+
version = '0.74.0'
|
|
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,355 @@
|
|
|
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
|
+
class BrowseWebWithImagePromptsBot:
|
|
12
|
+
"""
|
|
13
|
+
BrowseWebWithImagePromptsBot is a base class for creating bots that interact with web pages using Playwright.
|
|
14
|
+
The bot can perform actions such as navigating, clicking, scrolling, typing text, and taking screenshots.
|
|
15
|
+
It also supports cookie management to maintain session state across interactions.
|
|
16
|
+
|
|
17
|
+
Methods:
|
|
18
|
+
- __init__(session_id, website_name, browser_type='chromium', headless=True):
|
|
19
|
+
Initializes the bot with the given session ID, website name, browser type, and headless mode.
|
|
20
|
+
Supported browser types: 'chromium', 'firefox', 'webkit'.
|
|
21
|
+
|
|
22
|
+
- load_cookies():
|
|
23
|
+
Loads cookies from a file and adds them to the browser context.
|
|
24
|
+
|
|
25
|
+
- save_cookies():
|
|
26
|
+
Saves the current cookies to a file.
|
|
27
|
+
|
|
28
|
+
- navigate(url):
|
|
29
|
+
Navigates to the specified URL.
|
|
30
|
+
|
|
31
|
+
- click(selector):
|
|
32
|
+
Clicks on the element specified by the selector.
|
|
33
|
+
|
|
34
|
+
- scroll(direction='down', amount=1):
|
|
35
|
+
Scrolls the page in the specified direction ('down', 'up', 'left', 'right') by the specified amount.
|
|
36
|
+
|
|
37
|
+
- type_text(selector, text):
|
|
38
|
+
Types the specified text into the element specified by the selector.
|
|
39
|
+
|
|
40
|
+
- take_screenshot():
|
|
41
|
+
Takes a screenshot and saves it with a timestamp in the session-specific directory. Returns the path to the screenshot.
|
|
42
|
+
|
|
43
|
+
- get_latest_screenshot_path():
|
|
44
|
+
Retrieves the path to the most recent screenshot in the session-specific directory.
|
|
45
|
+
|
|
46
|
+
- create_prompt_vars(current_action_description, session_goal):
|
|
47
|
+
Creates a dictionary of prompt variables from the current action description and session goal.
|
|
48
|
+
|
|
49
|
+
- send_screenshot_to_llm(screenshot_path, current_action_description="", session_goal=""):
|
|
50
|
+
Encodes the screenshot in base64, creates prompt variables, and sends them to the LLM. Returns the new instructions from the LLM.
|
|
51
|
+
|
|
52
|
+
- send_prompt_to_llm(prompt_vars, screenshot_base64):
|
|
53
|
+
Abstract method to be implemented by subclasses. Sends the prompt variables and screenshot to the LLM and returns the response.
|
|
54
|
+
|
|
55
|
+
- close():
|
|
56
|
+
Saves cookies, closes the browser, and stops Playwright.
|
|
57
|
+
|
|
58
|
+
- execute_instructions(instructions):
|
|
59
|
+
Executes the given set of instructions, takes a screenshot after each step, and sends the screenshot to the LLM for further instructions.
|
|
60
|
+
|
|
61
|
+
Example usage:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class ProductionBot(BrowseWebWithImagePromptsBot):
|
|
65
|
+
def send_prompt_to_llm(self, prompt_vars, screenshot_base64):
|
|
66
|
+
# Implement the actual logic to send the prompt and screenshot to the LLM and return the response
|
|
67
|
+
api_url = "https://api.example.com/process" # Replace with the actual LLM API endpoint
|
|
68
|
+
headers = {"Content-Type": "application/json"}
|
|
69
|
+
data = {
|
|
70
|
+
"prompt": prompt_vars,
|
|
71
|
+
"screenshot": screenshot_base64
|
|
72
|
+
}
|
|
73
|
+
response = requests.post(api_url, headers=headers, data=json.dumps(data))
|
|
74
|
+
return response.text # Assuming the response is in JSON format
|
|
75
|
+
|
|
76
|
+
@app.route('/run-bot', methods=['POST'])
|
|
77
|
+
def run_bot():
|
|
78
|
+
data = request.json
|
|
79
|
+
session_id = data.get('session_id')
|
|
80
|
+
website_name = data.get('website_name')
|
|
81
|
+
browser_type = data.get('browser_type', 'chromium')
|
|
82
|
+
current_action_description = data.get('current_action_description', "")
|
|
83
|
+
session_goal = data.get('session_goal', "")
|
|
84
|
+
|
|
85
|
+
bot = ProductionBot(session_id=session_id, website_name=website_name, browser_type=browser_type, headless=True)
|
|
86
|
+
|
|
87
|
+
# Check if initial instructions are provided
|
|
88
|
+
initial_instructions = data.get('instructions')
|
|
89
|
+
if initial_instructions:
|
|
90
|
+
bot.execute_instructions(initial_instructions)
|
|
91
|
+
else:
|
|
92
|
+
bot.execute_instructions([{'action':'navigate', 'url': website_name}])
|
|
93
|
+
|
|
94
|
+
# Take initial screenshot and send to LLM
|
|
95
|
+
screenshot_path = bot.take_screenshot()
|
|
96
|
+
new_instructions = bot.send_screenshot_to_llm(screenshot_path, current_action_description, session_goal)
|
|
97
|
+
bot.execute_instructions(new_instructions)
|
|
98
|
+
|
|
99
|
+
# Take final screenshot
|
|
100
|
+
bot.take_screenshot()
|
|
101
|
+
|
|
102
|
+
bot.close()
|
|
103
|
+
|
|
104
|
+
return jsonify({"status": "completed", "new_instructions": new_instructions})
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
app.run(host='0.0.0.0', port=8080)
|
|
108
|
+
```
|
|
109
|
+
"""
|
|
110
|
+
#class BrowseWebWithImagePromptsBot:
|
|
111
|
+
def __init__(self, session_id, website_name, browser_type='chromium', headless=True, max_steps=10):
|
|
112
|
+
try:
|
|
113
|
+
from playwright.sync_api import sync_playwright
|
|
114
|
+
except ImportError as err:
|
|
115
|
+
print(err)
|
|
116
|
+
sync_playwright = None
|
|
117
|
+
if not sync_playwright:
|
|
118
|
+
raise ImportError("playright needed for BrowseWebWithImagePromptsBot class - install via `pip install sunholo[tools]`")
|
|
119
|
+
self.session_id = session_id or datetime.now().strftime("%Y%m%d%H%M%S")
|
|
120
|
+
self.website_name = website_name
|
|
121
|
+
self.browser_type = browser_type
|
|
122
|
+
self.max_steps = max_steps
|
|
123
|
+
self.steps = 0
|
|
124
|
+
self.screenshot_dir = f"browser_tool/{get_clean_website_name(website_name)}/{session_id}"
|
|
125
|
+
os.makedirs(self.screenshot_dir, exist_ok=True)
|
|
126
|
+
self.cookie_file = os.path.join(self.screenshot_dir, "cookies.json")
|
|
127
|
+
self.playwright = sync_playwright().start()
|
|
128
|
+
|
|
129
|
+
if browser_type == 'chromium':
|
|
130
|
+
self.browser = self.playwright.chromium.launch(headless=headless)
|
|
131
|
+
elif browser_type == 'firefox':
|
|
132
|
+
self.browser = self.playwright.firefox.launch(headless=headless)
|
|
133
|
+
elif browser_type == 'webkit':
|
|
134
|
+
self.browser = self.playwright.webkit.launch(headless=headless)
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError(f"Unsupported browser type: {browser_type}")
|
|
137
|
+
|
|
138
|
+
self.context = self.browser.new_context()
|
|
139
|
+
self.page = self.context.new_page()
|
|
140
|
+
self.load_cookies()
|
|
141
|
+
self.actions_log = []
|
|
142
|
+
self.session_goal = None
|
|
143
|
+
self.session_screenshots = []
|
|
144
|
+
|
|
145
|
+
def load_cookies(self):
|
|
146
|
+
if os.path.exists(self.cookie_file):
|
|
147
|
+
with open(self.cookie_file, 'r') as f:
|
|
148
|
+
cookies = json.load(f)
|
|
149
|
+
self.context.add_cookies(cookies)
|
|
150
|
+
|
|
151
|
+
def save_cookies(self):
|
|
152
|
+
cookies = self.context.cookies()
|
|
153
|
+
with open(self.cookie_file, 'w') as f:
|
|
154
|
+
json.dump(cookies, f)
|
|
155
|
+
|
|
156
|
+
def navigate(self, url):
|
|
157
|
+
try:
|
|
158
|
+
self.page.goto(url)
|
|
159
|
+
self.page.wait_for_load_state()
|
|
160
|
+
log.info(f'Navigated to {url}')
|
|
161
|
+
self.actions_log.append(f"Navigated to {url}")
|
|
162
|
+
except Exception as err:
|
|
163
|
+
log.warning(f"navigate failed with {str(err)}")
|
|
164
|
+
self.actions_log.append(f"Tried to navigate to {url} but got an error")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def click(self, selector):
|
|
168
|
+
try:
|
|
169
|
+
self.page.click(selector)
|
|
170
|
+
self.page.wait_for_load_state()
|
|
171
|
+
log.info(f"Clicked on element with selector {selector}")
|
|
172
|
+
self.actions_log.append(f"Clicked on element with selector {selector}")
|
|
173
|
+
except Exception as err:
|
|
174
|
+
log.warning(f"click failed with {str(err)}")
|
|
175
|
+
self.actions_log.append(f"Tried to click on element with selector {selector} but got an error")
|
|
176
|
+
|
|
177
|
+
def scroll(self, direction='down', amount=1):
|
|
178
|
+
try:
|
|
179
|
+
for _ in range(amount):
|
|
180
|
+
if direction == 'down':
|
|
181
|
+
self.page.evaluate("window.scrollBy(0, window.innerHeight)")
|
|
182
|
+
elif direction == 'up':
|
|
183
|
+
self.page.evaluate("window.scrollBy(0, -window.innerHeight)")
|
|
184
|
+
elif direction == 'left':
|
|
185
|
+
self.page.evaluate("window.scrollBy(-window.innerWidth, 0)")
|
|
186
|
+
elif direction == 'right':
|
|
187
|
+
self.page.evaluate("window.scrollBy(window.innerWidth, 0)")
|
|
188
|
+
self.page.wait_for_timeout(500)
|
|
189
|
+
log.info(f"Scrolled {direction} by {amount} page heights")
|
|
190
|
+
self.actions_log.append(f"Scrolled {direction} by {amount} page heights")
|
|
191
|
+
except Exception as err:
|
|
192
|
+
log.warning(f"Scrolled failed with {str(err)}")
|
|
193
|
+
self.actions_log.append(f"Tried to scroll {direction} by {amount} page heights but got an error")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def type_text(self, selector, text):
|
|
197
|
+
try:
|
|
198
|
+
self.page.fill(selector, text)
|
|
199
|
+
self.page.wait_for_load_state()
|
|
200
|
+
log.info(f"Typed text '{text}' into element with selector {selector}")
|
|
201
|
+
self.actions_log.append(f"Typed text '{text}' into element with selector {selector}")
|
|
202
|
+
except Exception as err:
|
|
203
|
+
log.warning(f"Typed text failed with {str(err)}")
|
|
204
|
+
self.actions_log.append(f"Tried to type text '{text}' into element with selector {selector} but got an error")
|
|
205
|
+
|
|
206
|
+
def take_screenshot(self, final=False):
|
|
207
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
208
|
+
parsed_url = urllib.parse.urlparse({self.page.url})
|
|
209
|
+
url_path = parsed_url.path
|
|
210
|
+
if final:
|
|
211
|
+
screenshot_path = os.path.join(self.screenshot_dir, f"final/{timestamp}_{url_path}.png")
|
|
212
|
+
else:
|
|
213
|
+
screenshot_path = os.path.join(self.screenshot_dir, f"{timestamp}_{url_path}.png")
|
|
214
|
+
self.page.screenshot(path=screenshot_path)
|
|
215
|
+
log.info(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
|
|
216
|
+
#self.actions_log.append(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
|
|
217
|
+
self.session_screenshots.append(screenshot_path)
|
|
218
|
+
|
|
219
|
+
return screenshot_path
|
|
220
|
+
|
|
221
|
+
def get_latest_screenshot_path(self):
|
|
222
|
+
screenshots = sorted(
|
|
223
|
+
[f for f in os.listdir(self.screenshot_dir) if f.startswith('screenshot_')],
|
|
224
|
+
key=lambda x: os.path.getmtime(os.path.join(self.screenshot_dir, x)),
|
|
225
|
+
reverse=True
|
|
226
|
+
)
|
|
227
|
+
if screenshots:
|
|
228
|
+
return os.path.join(self.screenshot_dir, screenshots[0])
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
def create_prompt_vars(self, last_message):
|
|
232
|
+
prompt = {
|
|
233
|
+
"last_actions": self.actions_log,
|
|
234
|
+
"session_goal": self.session_goal,
|
|
235
|
+
"last_message": last_message
|
|
236
|
+
}
|
|
237
|
+
return prompt
|
|
238
|
+
|
|
239
|
+
def check_llm_response(self, response):
|
|
240
|
+
if isinstance(response, dict):
|
|
241
|
+
output = response
|
|
242
|
+
elif isinstance(response, str):
|
|
243
|
+
output = json.loads(response)
|
|
244
|
+
|
|
245
|
+
#TODO: more validation
|
|
246
|
+
log.info(f'Response: {output=}')
|
|
247
|
+
|
|
248
|
+
if 'status' not in output:
|
|
249
|
+
log.error(f'Response did not contain status')
|
|
250
|
+
|
|
251
|
+
if 'new_instructions' not in output:
|
|
252
|
+
log.warning(f'Response did not include new_instructions')
|
|
253
|
+
|
|
254
|
+
if 'message' not in output:
|
|
255
|
+
log.warning(f'Response did not include message')
|
|
256
|
+
|
|
257
|
+
return output
|
|
258
|
+
|
|
259
|
+
def send_screenshot_to_llm(self, screenshot_path, last_message):
|
|
260
|
+
with open(screenshot_path, "rb") as image_file:
|
|
261
|
+
encoded_image = base64.b64encode(image_file.read()).decode('utf-8')
|
|
262
|
+
|
|
263
|
+
prompt_vars = self.create_prompt_vars(last_message)
|
|
264
|
+
response = self.send_prompt_to_llm(prompt_vars, encoded_image) # Sending prompt and image separately
|
|
265
|
+
|
|
266
|
+
return self.check_llm_response(response)
|
|
267
|
+
|
|
268
|
+
def send_prompt_to_llm(self, prompt_vars, screenshot_base64):
|
|
269
|
+
raise NotImplementedError("""
|
|
270
|
+
This method should be implemented by subclasses: `def send_prompt_to_llm(self, prompt_vars, screenshot_base64)`")
|
|
271
|
+
prompt = {
|
|
272
|
+
"last_actions": self.action_log,
|
|
273
|
+
"session_goal": self.session_goal,
|
|
274
|
+
}
|
|
275
|
+
""")
|
|
276
|
+
|
|
277
|
+
def close(self):
|
|
278
|
+
self.save_cookies()
|
|
279
|
+
self.browser.close()
|
|
280
|
+
self.playwright.stop()
|
|
281
|
+
|
|
282
|
+
def execute_instructions(self, instructions: list, last_message: str=None):
|
|
283
|
+
if not instructions:
|
|
284
|
+
log.info("No instructions found, returning immediately")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
if self.steps >= self.max_steps:
|
|
288
|
+
log.warning(f"Reached the maximum number of steps: {self.max_steps}")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
if not isinstance(instructions, list):
|
|
292
|
+
log.error(f"{instructions} {type(instructions)}")
|
|
293
|
+
for instruction in instructions:
|
|
294
|
+
if not isinstance(instruction, dict):
|
|
295
|
+
log.error(f"{instruction} {type(instruction)}")
|
|
296
|
+
action = instruction['action']
|
|
297
|
+
if action == 'navigate':
|
|
298
|
+
self.navigate(instruction['url'])
|
|
299
|
+
elif action == 'click':
|
|
300
|
+
self.click(instruction['selector'])
|
|
301
|
+
elif action == 'scroll':
|
|
302
|
+
self.scroll(instruction.get('direction', 'down'), instruction.get('amount', 1))
|
|
303
|
+
elif action == 'type':
|
|
304
|
+
self.type_text(instruction['selector'], instruction['text'])
|
|
305
|
+
self.steps += 1
|
|
306
|
+
if self.steps >= self.max_steps:
|
|
307
|
+
log.warning(f"Reached the maximum number of steps: {self.max_steps}")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
screenshot_path = self.take_screenshot()
|
|
311
|
+
next_browser_instructions = self.send_screenshot_to_llm(
|
|
312
|
+
screenshot_path,
|
|
313
|
+
last_message=last_message)
|
|
314
|
+
|
|
315
|
+
return next_browser_instructions
|
|
316
|
+
|
|
317
|
+
def start_session(self, instructions, session_goal):
|
|
318
|
+
self.session_goal = session_goal
|
|
319
|
+
|
|
320
|
+
if not instructions:
|
|
321
|
+
instructions = [{'action': 'navigate', 'url': self.website_name}]
|
|
322
|
+
|
|
323
|
+
next_instructions = self.execute_instructions(instructions)
|
|
324
|
+
|
|
325
|
+
in_session = True
|
|
326
|
+
while in_session:
|
|
327
|
+
if next_instructions and 'status' in next_instructions:
|
|
328
|
+
if next_instructions['status'] == 'in-progress':
|
|
329
|
+
log.info(f'Browser message: {next_instructions.get('message')}')
|
|
330
|
+
if 'new_instructions' not in next_instructions:
|
|
331
|
+
log.error('Browser status: "in-progress" but no new_instructions')
|
|
332
|
+
last_message = next_instructions['message']
|
|
333
|
+
log.info(f'Browser message: {last_message}')
|
|
334
|
+
next_instructions = self.execute_instructions(next_instructions['new_instructions'], last_message=last_message)
|
|
335
|
+
else:
|
|
336
|
+
log.info(f'Session finished due to status={next_instructions["status"]}')
|
|
337
|
+
in_session=False
|
|
338
|
+
break
|
|
339
|
+
else:
|
|
340
|
+
log.info('Session finished due to next_instructions being empty')
|
|
341
|
+
in_session=False
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
log.info("Session finished")
|
|
345
|
+
final_path = self.take_screenshot(final=True)
|
|
346
|
+
self.close()
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
"website": self.website_name,
|
|
350
|
+
"log": self.actions_log,
|
|
351
|
+
"next_instructions": next_instructions,
|
|
352
|
+
"session_screenshots": self.session_screenshots,
|
|
353
|
+
"final_page": final_path,
|
|
354
|
+
}
|
|
355
|
+
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
import re
|
|
15
15
|
import hashlib
|
|
16
|
+
import urllib.parse
|
|
16
17
|
|
|
17
18
|
def validate_extension_id(ext_id):
|
|
18
19
|
"""
|
|
@@ -183,4 +184,8 @@ def escape_braces(text):
|
|
|
183
184
|
# Replace single braces with double braces
|
|
184
185
|
text = re.sub(r'(?<!{){(?!{)', '{{', text) # Replace '{' with '{{' if not already double braced
|
|
185
186
|
text = re.sub(r'(?<!})}(?!})', '}}', text) # Replace '}' with '}}' if not already double braced
|
|
186
|
-
return text
|
|
187
|
+
return text
|
|
188
|
+
|
|
189
|
+
def get_clean_website_name(url):
|
|
190
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
191
|
+
return parsed_url.netloc
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sunholo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.74.0
|
|
4
4
|
Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
|
|
5
5
|
Home-page: https://github.com/sunholo-data/sunholo-py
|
|
6
|
-
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.
|
|
6
|
+
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.74.0.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
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import base64
|
|
3
|
-
import json
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
try:
|
|
6
|
-
from playwright.sync_api import sync_playwright
|
|
7
|
-
except ImportError:
|
|
8
|
-
sync_playwright = None
|
|
9
|
-
|
|
10
|
-
class BrowseWebWithImagePromptsBot:
|
|
11
|
-
"""
|
|
12
|
-
Examples:
|
|
13
|
-
|
|
14
|
-
```python
|
|
15
|
-
class ProductionBot(BrowseWebWithImagePromptsBot):
|
|
16
|
-
def send_prompt_to_llm(self, prompt, screenshot_base64):
|
|
17
|
-
# Implement the actual logic to send the prompt and screenshot to the LLM and return the response
|
|
18
|
-
api_url = "https://api.example.com/process" # Replace with the actual LLM API endpoint
|
|
19
|
-
headers = {"Content-Type": "application/json"}
|
|
20
|
-
data = {
|
|
21
|
-
"prompt": prompt,
|
|
22
|
-
"screenshot": screenshot_base64
|
|
23
|
-
}
|
|
24
|
-
response = requests.post(api_url, headers=headers, data=json.dumps(data))
|
|
25
|
-
return response.text # Assuming the response is in JSON format
|
|
26
|
-
|
|
27
|
-
@app.route('/run-bot', methods=['POST'])
|
|
28
|
-
def run_bot():
|
|
29
|
-
data = request.json
|
|
30
|
-
session_id = data.get('session_id')
|
|
31
|
-
website_name = data.get('website_name')
|
|
32
|
-
browser_type = data.get('browser_type', 'chromium')
|
|
33
|
-
current_action_description = data.get('current_action_description', "")
|
|
34
|
-
next_goal = data.get('next_goal', "")
|
|
35
|
-
|
|
36
|
-
bot = ProductionBot(session_id=session_id, website_name=website_name, browser_type=browser_type, headless=True)
|
|
37
|
-
|
|
38
|
-
# Check if initial instructions are provided
|
|
39
|
-
initial_instructions = data.get('instructions')
|
|
40
|
-
if initial_instructions:
|
|
41
|
-
bot.execute_instructions(initial_instructions)
|
|
42
|
-
|
|
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)
|
|
48
|
-
|
|
49
|
-
# Take final screenshot
|
|
50
|
-
bot.take_screenshot()
|
|
51
|
-
|
|
52
|
-
bot.close()
|
|
53
|
-
|
|
54
|
-
return jsonify({"status": "completed", "new_instructions": new_instructions})
|
|
55
|
-
|
|
56
|
-
if __name__ == "__main__":
|
|
57
|
-
app.run(host='0.0.0.0', port=8080)
|
|
58
|
-
```
|
|
59
|
-
"""
|
|
60
|
-
def __init__(self, session_id, website_name, browser_type='chromium', headless=True):
|
|
61
|
-
if not sync_playwright:
|
|
62
|
-
raise ImportError("playright needed for BrowseWebWithImagePromptsBot class - install via `pip install sunholo[tools]`")
|
|
63
|
-
self.session_id = session_id
|
|
64
|
-
self.website_name = website_name
|
|
65
|
-
self.browser_type = browser_type
|
|
66
|
-
self.screenshot_dir = f"{website_name}_{session_id}"
|
|
67
|
-
os.makedirs(self.screenshot_dir, exist_ok=True)
|
|
68
|
-
self.cookie_file = os.path.join(self.screenshot_dir, "cookies.json")
|
|
69
|
-
self.playwright = sync_playwright().start()
|
|
70
|
-
|
|
71
|
-
if browser_type == 'chromium':
|
|
72
|
-
self.browser = self.playwright.chromium.launch(headless=headless)
|
|
73
|
-
elif browser_type == 'firefox':
|
|
74
|
-
self.browser = self.playwright.firefox.launch(headless=headless)
|
|
75
|
-
elif browser_type == 'webkit':
|
|
76
|
-
self.browser = self.playwright.webkit.launch(headless=headless)
|
|
77
|
-
else:
|
|
78
|
-
raise ValueError(f"Unsupported browser type: {browser_type}")
|
|
79
|
-
|
|
80
|
-
self.context = self.browser.new_context()
|
|
81
|
-
self.page = self.context.new_page()
|
|
82
|
-
self.load_cookies()
|
|
83
|
-
|
|
84
|
-
def load_cookies(self):
|
|
85
|
-
if os.path.exists(self.cookie_file):
|
|
86
|
-
with open(self.cookie_file, 'r') as f:
|
|
87
|
-
cookies = json.load(f)
|
|
88
|
-
self.context.add_cookies(cookies)
|
|
89
|
-
|
|
90
|
-
def save_cookies(self):
|
|
91
|
-
cookies = self.context.cookies()
|
|
92
|
-
with open(self.cookie_file, 'w') as f:
|
|
93
|
-
json.dump(cookies, f)
|
|
94
|
-
|
|
95
|
-
def navigate(self, url):
|
|
96
|
-
self.page.goto(url)
|
|
97
|
-
|
|
98
|
-
def click(self, selector):
|
|
99
|
-
self.page.click(selector)
|
|
100
|
-
|
|
101
|
-
def scroll(self, direction='down', amount=1):
|
|
102
|
-
for _ in range(amount):
|
|
103
|
-
if direction == 'down':
|
|
104
|
-
self.page.evaluate("window.scrollBy(0, window.innerHeight)")
|
|
105
|
-
elif direction == 'up':
|
|
106
|
-
self.page.evaluate("window.scrollBy(0, -window.innerHeight)")
|
|
107
|
-
elif direction == 'left':
|
|
108
|
-
self.page.evaluate("window.scrollBy(-window.innerWidth, 0)")
|
|
109
|
-
elif direction == 'right':
|
|
110
|
-
self.page.evaluate("window.scrollBy(window.innerWidth, 0)")
|
|
111
|
-
|
|
112
|
-
def type_text(self, selector, text):
|
|
113
|
-
self.page.fill(selector, text)
|
|
114
|
-
|
|
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
|
-
return screenshot_path
|
|
120
|
-
|
|
121
|
-
def get_latest_screenshot_path(self):
|
|
122
|
-
screenshots = sorted(
|
|
123
|
-
[f for f in os.listdir(self.screenshot_dir) if f.startswith('screenshot_')],
|
|
124
|
-
key=lambda x: os.path.getmtime(os.path.join(self.screenshot_dir, x)),
|
|
125
|
-
reverse=True
|
|
126
|
-
)
|
|
127
|
-
if screenshots:
|
|
128
|
-
return os.path.join(self.screenshot_dir, screenshots[0])
|
|
129
|
-
return None
|
|
130
|
-
|
|
131
|
-
def create_prompt_vars(self, current_action_description, next_goal):
|
|
132
|
-
prompt = {
|
|
133
|
-
"current_action_description": current_action_description,
|
|
134
|
-
"next_goal": next_goal,
|
|
135
|
-
}
|
|
136
|
-
return prompt
|
|
137
|
-
|
|
138
|
-
def send_screenshot_to_llm(self, screenshot_path, current_action_description="", next_goal=""):
|
|
139
|
-
with open(screenshot_path, "rb") as image_file:
|
|
140
|
-
encoded_image = base64.b64encode(image_file.read()).decode('utf-8')
|
|
141
|
-
|
|
142
|
-
prompt_vars = self.create_prompt(current_action_description, next_goal)
|
|
143
|
-
response = self.send_prompt_to_llm(prompt_vars, encoded_image) # Sending prompt and image separately
|
|
144
|
-
return json.loads(response)
|
|
145
|
-
|
|
146
|
-
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)`")
|
|
148
|
-
|
|
149
|
-
def close(self):
|
|
150
|
-
self.save_cookies()
|
|
151
|
-
self.browser.close()
|
|
152
|
-
self.playwright.stop()
|
|
153
|
-
|
|
154
|
-
def execute_instructions(self, instructions):
|
|
155
|
-
for instruction in instructions:
|
|
156
|
-
action = instruction['action']
|
|
157
|
-
if action == 'navigate':
|
|
158
|
-
self.navigate(instruction['url'])
|
|
159
|
-
elif action == 'click':
|
|
160
|
-
self.click(instruction['selector'])
|
|
161
|
-
elif action == 'scroll':
|
|
162
|
-
self.scroll(instruction.get('direction', 'down'), instruction.get('amount', 1))
|
|
163
|
-
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)
|
|
169
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|