simplex 1.2.4__tar.gz → 1.2.5__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.
Potentially problematic release.
This version of simplex might be problematic. Click here for more details.
- {simplex-1.2.4 → simplex-1.2.5}/PKG-INFO +11 -3
- {simplex-1.2.4 → simplex-1.2.5}/setup.py +14 -3
- simplex-1.2.5/simplex/cli.py +31 -0
- simplex-1.2.5/simplex/deploy/__init__.py +3 -0
- simplex-1.2.5/simplex/deploy/push.py +208 -0
- simplex-1.2.5/simplex/simplex.py +234 -0
- {simplex-1.2.4 → simplex-1.2.5}/simplex.egg-info/PKG-INFO +11 -3
- {simplex-1.2.4 → simplex-1.2.5}/simplex.egg-info/SOURCES.txt +3 -3
- simplex-1.2.5/simplex.egg-info/requires.txt +16 -0
- simplex-1.2.4/simplex/constants.py +0 -1
- simplex-1.2.4/simplex/simplex.py +0 -324
- simplex-1.2.4/simplex/utils.py +0 -12
- simplex-1.2.4/simplex.egg-info/requires.txt +0 -8
- simplex-1.2.4/tests/test_local.py +0 -283
- {simplex-1.2.4 → simplex-1.2.5}/LICENSE +0 -0
- {simplex-1.2.4 → simplex-1.2.5}/README.md +0 -0
- {simplex-1.2.4 → simplex-1.2.5}/pyproject.toml +0 -0
- {simplex-1.2.4 → simplex-1.2.5}/setup.cfg +0 -0
- {simplex-1.2.4 → simplex-1.2.5}/simplex/__init__.py +0 -0
- {simplex-1.2.4 → simplex-1.2.5}/simplex.egg-info/dependency_links.txt +0 -0
- {simplex-1.2.4 → simplex-1.2.5}/simplex.egg-info/entry_points.txt +0 -0
- {simplex-1.2.4 → simplex-1.2.5}/simplex.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: simplex
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: Official Python SDK for Simplex API
|
|
5
|
-
Home-page: https://
|
|
5
|
+
Home-page: https://simplex.sh
|
|
6
6
|
Author: Simplex Labs, Inc.
|
|
7
7
|
Author-email: founders@simplex.sh
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -10,7 +10,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Requires-Python: >=3.
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: openai>=1.0.0
|
|
@@ -21,6 +21,14 @@ Requires-Dist: rich>=13.0.0
|
|
|
21
21
|
Requires-Dist: prompt_toolkit>=3.0.0
|
|
22
22
|
Requires-Dist: playwright>=1.0.0
|
|
23
23
|
Requires-Dist: Pillow>=9.0.0
|
|
24
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
25
|
+
Requires-Dist: boto3>=1.28.0
|
|
26
|
+
Requires-Dist: requests>=2.31.0
|
|
27
|
+
Requires-Dist: MainContentExtractor
|
|
28
|
+
Requires-Dist: langchain_core
|
|
29
|
+
Requires-Dist: langchain_community
|
|
30
|
+
Requires-Dist: langchain_openai
|
|
31
|
+
Requires-Dist: langchain_anthropic
|
|
24
32
|
Dynamic: author
|
|
25
33
|
Dynamic: author-email
|
|
26
34
|
Dynamic: classifier
|
|
@@ -2,8 +2,11 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="simplex",
|
|
5
|
-
version="1.2.
|
|
5
|
+
version="1.2.5",
|
|
6
6
|
packages=find_packages(),
|
|
7
|
+
package_data={
|
|
8
|
+
"simplex": ["browser_agent/dom/*.js"], # Include JS files in the dom directory
|
|
9
|
+
},
|
|
7
10
|
install_requires=[
|
|
8
11
|
"openai>=1.0.0",
|
|
9
12
|
"python-dotenv>=0.19.0",
|
|
@@ -13,6 +16,14 @@ setup(
|
|
|
13
16
|
"prompt_toolkit>=3.0.0",
|
|
14
17
|
"playwright>=1.0.0",
|
|
15
18
|
"Pillow>=9.0.0",
|
|
19
|
+
"PyYAML>=6.0.1",
|
|
20
|
+
"boto3>=1.28.0",
|
|
21
|
+
"requests>=2.31.0",
|
|
22
|
+
"MainContentExtractor",
|
|
23
|
+
"langchain_core",
|
|
24
|
+
"langchain_community",
|
|
25
|
+
"langchain_openai",
|
|
26
|
+
"langchain_anthropic"
|
|
16
27
|
],
|
|
17
28
|
entry_points={
|
|
18
29
|
'console_scripts': [
|
|
@@ -24,7 +35,7 @@ setup(
|
|
|
24
35
|
description="Official Python SDK for Simplex API",
|
|
25
36
|
long_description=open("README.md").read(),
|
|
26
37
|
long_description_content_type="text/markdown",
|
|
27
|
-
url="https://
|
|
38
|
+
url="https://simplex.sh",
|
|
28
39
|
classifiers=[
|
|
29
40
|
"Programming Language :: Python :: 3",
|
|
30
41
|
"License :: OSI Approved :: MIT License",
|
|
@@ -32,5 +43,5 @@ setup(
|
|
|
32
43
|
"Development Status :: 4 - Beta",
|
|
33
44
|
"Intended Audience :: Developers",
|
|
34
45
|
],
|
|
35
|
-
python_requires=">=3.
|
|
46
|
+
python_requires=">=3.9",
|
|
36
47
|
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from .deploy import push_directory, run_directory
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def cli():
|
|
7
|
+
"""Simplex CLI tool"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@cli.command()
|
|
11
|
+
@click.argument('directory', type=click.Path(exists=True, file_okay=False, dir_okay=True))
|
|
12
|
+
def push(directory):
|
|
13
|
+
try:
|
|
14
|
+
push_directory(directory)
|
|
15
|
+
except Exception as e:
|
|
16
|
+
print(f"Error running job: {e}")
|
|
17
|
+
raise
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@cli.command()
|
|
22
|
+
@click.argument('directory', type=click.Path(exists=True, file_okay=False, dir_okay=True))
|
|
23
|
+
def run(directory):
|
|
24
|
+
try:
|
|
25
|
+
run_directory(directory)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print(f"Error running job: {e}")
|
|
28
|
+
raise
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
cli()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
import tempfile
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
import requests
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
load_dotenv() # This loads the .env file
|
|
11
|
+
|
|
12
|
+
S3_URL = "https://u3mvtbirxf.us-east-1.awsapprunner.com"
|
|
13
|
+
JOB_URL = "https://simplex--job-scheduler-app-handler.modal.run"
|
|
14
|
+
|
|
15
|
+
def read_config(directory: str) -> dict:
|
|
16
|
+
"""Read and validate the simplex.yaml configuration file"""
|
|
17
|
+
config_path = Path(directory) / "simplex.yaml"
|
|
18
|
+
if not config_path.exists():
|
|
19
|
+
raise FileNotFoundError(f"No simplex.yaml found in {directory}")
|
|
20
|
+
|
|
21
|
+
with open(config_path) as f:
|
|
22
|
+
return yaml.safe_load(f)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_archive(directory: str, config: dict) -> str:
|
|
26
|
+
"""Create a temporary zip archive of the entire directory"""
|
|
27
|
+
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp:
|
|
28
|
+
with zipfile.ZipFile(tmp.name, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
29
|
+
base_path = Path(directory)
|
|
30
|
+
|
|
31
|
+
# Walk through all files and directories
|
|
32
|
+
for root, dirs, files in os.walk(directory):
|
|
33
|
+
for file in files:
|
|
34
|
+
file_path = Path(root) / file
|
|
35
|
+
# Get path relative to the base directory
|
|
36
|
+
relative_path = file_path.relative_to(base_path)
|
|
37
|
+
# Add file to zip with its relative path
|
|
38
|
+
zip_file.write(file_path, arcname=str(relative_path))
|
|
39
|
+
|
|
40
|
+
return tmp.name
|
|
41
|
+
|
|
42
|
+
def push_directory(directory: str):
|
|
43
|
+
"""Main function to handle parsing yaml and creating zip archive"""
|
|
44
|
+
config = read_config(directory)
|
|
45
|
+
archive_path = create_archive(directory, config)
|
|
46
|
+
|
|
47
|
+
project_name = config.get('name')
|
|
48
|
+
if not project_name:
|
|
49
|
+
raise ValueError("Project name not specified in simplex.yaml")
|
|
50
|
+
|
|
51
|
+
cron_schedule = config.get("cron_schedule", None)
|
|
52
|
+
if not cron_schedule:
|
|
53
|
+
raise ValueError("Cron schedule not specified in simplex.yaml")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
s3_uri = upload_files_to_s3(archive_path, project_name)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"Error uploading project: {e}")
|
|
59
|
+
raise
|
|
60
|
+
finally:
|
|
61
|
+
os.unlink(archive_path)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
create_job_scheduled(project_name, cron_schedule, s3_uri)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"Error creating job: {e}")
|
|
67
|
+
raise
|
|
68
|
+
finally:
|
|
69
|
+
print("Successfully created job!")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def upload_files_to_s3(archive_path: str, project_name: str):
|
|
74
|
+
# Read API key from environment
|
|
75
|
+
api_key = os.getenv('SIMPLEX_API_KEY')
|
|
76
|
+
if not api_key:
|
|
77
|
+
raise ValueError("SIMPLEX_API_KEY environment variable not set")
|
|
78
|
+
|
|
79
|
+
url = S3_URL + "/upload_files_to_s3"
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Prepare the multipart form data
|
|
83
|
+
files = {
|
|
84
|
+
'zip_file': ('archive.zip', open(archive_path, 'rb'), 'application/zip')
|
|
85
|
+
}
|
|
86
|
+
data = {
|
|
87
|
+
'project_name': project_name,
|
|
88
|
+
}
|
|
89
|
+
headers = {
|
|
90
|
+
'x-api-key': api_key
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Make the POST request
|
|
94
|
+
response = requests.post(
|
|
95
|
+
url,
|
|
96
|
+
files=files,
|
|
97
|
+
data=data,
|
|
98
|
+
headers=headers
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Check response
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
result = response.json()
|
|
104
|
+
print(f"Successfully uploaded project: {result['project_name']}")
|
|
105
|
+
return result['s3_uri']
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error uploading project: {e}")
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
def create_job_scheduled(project_name: str, cron_schedule: str, s3_uri: str):
|
|
111
|
+
"""Create a new job via the API"""
|
|
112
|
+
# Read API key from environment
|
|
113
|
+
api_key = os.getenv('SIMPLEX_API_KEY')
|
|
114
|
+
if not api_key:
|
|
115
|
+
raise ValueError("SIMPLEX_API_KEY environment variable not set")
|
|
116
|
+
|
|
117
|
+
url = JOB_URL + "/api/v1/jobs/"
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
headers = {
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
'X-API-Key': api_key
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
data = {
|
|
126
|
+
'name': project_name,
|
|
127
|
+
's3_uri': s3_uri,
|
|
128
|
+
'cron_schedule': cron_schedule
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
response = requests.post(
|
|
132
|
+
url,
|
|
133
|
+
json=data,
|
|
134
|
+
headers=headers
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Check response
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
result = response.json()
|
|
140
|
+
print(f"Successfully created job: {result['name']}")
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"Error creating job: {e}")
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def create_job_immediately(project_name: str, s3_uri: str):
|
|
149
|
+
"""Create a new job via the API"""
|
|
150
|
+
# Read API key from environment
|
|
151
|
+
api_key = os.getenv('SIMPLEX_API_KEY')
|
|
152
|
+
if not api_key:
|
|
153
|
+
raise ValueError("SIMPLEX_API_KEY environment variable not set")
|
|
154
|
+
|
|
155
|
+
url = JOB_URL + "/api/v1/jobs/"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
headers = {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'X-API-Key': api_key
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
data = {
|
|
165
|
+
'name': project_name,
|
|
166
|
+
's3_uri': s3_uri,
|
|
167
|
+
'schedule_time': (datetime.utcnow() + timedelta(minutes=2)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
response = requests.post(
|
|
171
|
+
url,
|
|
172
|
+
json=data,
|
|
173
|
+
headers=headers
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Check response
|
|
177
|
+
response.raise_for_status()
|
|
178
|
+
result = response.json()
|
|
179
|
+
print(f"Successfully created job: {result['name']}")
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Error creating job: {e}")
|
|
184
|
+
raise
|
|
185
|
+
|
|
186
|
+
def run_directory(directory: str):
|
|
187
|
+
"""Run a directory immediately"""
|
|
188
|
+
config = read_config(directory)
|
|
189
|
+
archive_path = create_archive(directory, config)
|
|
190
|
+
|
|
191
|
+
project_name = config.get('name')
|
|
192
|
+
if not project_name:
|
|
193
|
+
raise ValueError("Project name not specified in simplex.yaml")
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
s3_uri = upload_files_to_s3(archive_path, project_name)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
print(f"Error uploading project: {e}")
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
finally:
|
|
202
|
+
os.unlink(archive_path)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
create_job_immediately(project_name, s3_uri)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
print(f"Error creating job: {e}")
|
|
208
|
+
raise
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import atexit
|
|
4
|
+
|
|
5
|
+
BASE_URL = "https://api.simplex.sh"
|
|
6
|
+
|
|
7
|
+
class Simplex:
|
|
8
|
+
def __init__(self, api_key: str):
|
|
9
|
+
self.api_key = api_key
|
|
10
|
+
self.session_id = None
|
|
11
|
+
atexit.register(self.cleanup_session)
|
|
12
|
+
|
|
13
|
+
def cleanup_session(self):
|
|
14
|
+
if self.session_id:
|
|
15
|
+
try:
|
|
16
|
+
self.close_session(self.session_id)
|
|
17
|
+
except:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def close_session(self):
|
|
21
|
+
response = requests.post(
|
|
22
|
+
f"{BASE_URL}/close",
|
|
23
|
+
headers={
|
|
24
|
+
'x-api-key': self.api_key
|
|
25
|
+
},
|
|
26
|
+
data={'session_id': self.session_id}
|
|
27
|
+
)
|
|
28
|
+
self.session_id = None
|
|
29
|
+
return response.json()
|
|
30
|
+
|
|
31
|
+
def create_session(self):
|
|
32
|
+
response = requests.post(
|
|
33
|
+
f"{BASE_URL}/create-session",
|
|
34
|
+
headers={
|
|
35
|
+
'x-api-key': self.api_key
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
response_json = response.json()
|
|
40
|
+
self.session_id = response_json['session_id']
|
|
41
|
+
livestream_url = response_json['livestream_url']
|
|
42
|
+
|
|
43
|
+
print(f"\nSession created successfully!")
|
|
44
|
+
print(f"Session ID: {self.session_id}")
|
|
45
|
+
print(f"Livestream URL: {livestream_url}\n")
|
|
46
|
+
|
|
47
|
+
return self.session_id, livestream_url
|
|
48
|
+
|
|
49
|
+
def goto(self, url: str, cdp_url: str = None):
|
|
50
|
+
if not cdp_url and not self.session_id:
|
|
51
|
+
raise ValueError(f"Must call create_session before calling action goto with url='{url}'")
|
|
52
|
+
|
|
53
|
+
if not url.startswith('http://') and not url.startswith('https://'):
|
|
54
|
+
url = 'https://' + url
|
|
55
|
+
|
|
56
|
+
data = {'url': url}
|
|
57
|
+
|
|
58
|
+
if cdp_url:
|
|
59
|
+
data['cdp_url'] = cdp_url
|
|
60
|
+
else:
|
|
61
|
+
data['session_id'] = self.session_id
|
|
62
|
+
|
|
63
|
+
response = requests.post(
|
|
64
|
+
f"{BASE_URL}/goto",
|
|
65
|
+
headers={
|
|
66
|
+
'x-api-key': self.api_key
|
|
67
|
+
},
|
|
68
|
+
data=data
|
|
69
|
+
)
|
|
70
|
+
print(response.json())
|
|
71
|
+
|
|
72
|
+
def click(self, element_description: str, cdp_url: str = None):
|
|
73
|
+
if not cdp_url and not self.session_id:
|
|
74
|
+
raise ValueError(f"Must call create_session before calling action click with element_description='{element_description}'")
|
|
75
|
+
|
|
76
|
+
data = {'element_description': element_description}
|
|
77
|
+
|
|
78
|
+
if cdp_url:
|
|
79
|
+
data['cdp_url'] = cdp_url
|
|
80
|
+
else:
|
|
81
|
+
data['session_id'] = self.session_id
|
|
82
|
+
|
|
83
|
+
response = requests.post(
|
|
84
|
+
f"{BASE_URL}/click",
|
|
85
|
+
headers={
|
|
86
|
+
'x-api-key': self.api_key
|
|
87
|
+
},
|
|
88
|
+
data=data
|
|
89
|
+
)
|
|
90
|
+
print(response.json())
|
|
91
|
+
|
|
92
|
+
def type(self, text: str, cdp_url: str = None):
|
|
93
|
+
if not cdp_url and not self.session_id:
|
|
94
|
+
raise ValueError(f"Must call create_session before calling action type with text='{text}'")
|
|
95
|
+
|
|
96
|
+
data = {'text': text}
|
|
97
|
+
|
|
98
|
+
if cdp_url:
|
|
99
|
+
data['cdp_url'] = cdp_url
|
|
100
|
+
else:
|
|
101
|
+
data['session_id'] = self.session_id
|
|
102
|
+
|
|
103
|
+
response = requests.post(
|
|
104
|
+
f"{BASE_URL}/type",
|
|
105
|
+
headers={
|
|
106
|
+
'x-api-key': self.api_key
|
|
107
|
+
},
|
|
108
|
+
data=data
|
|
109
|
+
)
|
|
110
|
+
print(response.json())
|
|
111
|
+
|
|
112
|
+
def press_enter(self, cdp_url: str = None):
|
|
113
|
+
if not cdp_url and not self.session_id:
|
|
114
|
+
raise ValueError("Must call create_session before calling action press_enter")
|
|
115
|
+
|
|
116
|
+
data = {}
|
|
117
|
+
|
|
118
|
+
if cdp_url:
|
|
119
|
+
data['cdp_url'] = cdp_url
|
|
120
|
+
else:
|
|
121
|
+
data['session_id'] = self.session_id
|
|
122
|
+
|
|
123
|
+
response = requests.post(
|
|
124
|
+
f"{BASE_URL}/press-enter",
|
|
125
|
+
headers={
|
|
126
|
+
'x-api-key': self.api_key
|
|
127
|
+
},
|
|
128
|
+
data=data
|
|
129
|
+
)
|
|
130
|
+
print(response.json())
|
|
131
|
+
|
|
132
|
+
def extract_bbox(self, element_description: str, cdp_url: str = None):
|
|
133
|
+
if not cdp_url and not self.session_id:
|
|
134
|
+
raise ValueError(f"Must call create_session before calling action extract_bbox with element_description='{element_description}'")
|
|
135
|
+
|
|
136
|
+
data = {'element_description': element_description}
|
|
137
|
+
|
|
138
|
+
if cdp_url:
|
|
139
|
+
data['cdp_url'] = cdp_url
|
|
140
|
+
else:
|
|
141
|
+
data['session_id'] = self.session_id
|
|
142
|
+
|
|
143
|
+
response = requests.get(
|
|
144
|
+
f"{BASE_URL}/extract-bbox",
|
|
145
|
+
headers={
|
|
146
|
+
'x-api-key': self.api_key
|
|
147
|
+
},
|
|
148
|
+
params=data
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return response.json()
|
|
152
|
+
|
|
153
|
+
def extract_text(self, element_description: str, cdp_url: str = None, use_vision: bool = False):
|
|
154
|
+
if not cdp_url and not self.session_id:
|
|
155
|
+
raise ValueError(f"Must call create_session before calling action extract_text with element_description='{element_description}'")
|
|
156
|
+
|
|
157
|
+
data = {'element_description': element_description}
|
|
158
|
+
|
|
159
|
+
if cdp_url:
|
|
160
|
+
data['cdp_url'] = cdp_url
|
|
161
|
+
else:
|
|
162
|
+
data['session_id'] = self.session_id
|
|
163
|
+
|
|
164
|
+
if use_vision:
|
|
165
|
+
data['use_vision'] = use_vision
|
|
166
|
+
|
|
167
|
+
response = requests.get(
|
|
168
|
+
f"{BASE_URL}/extract-text",
|
|
169
|
+
headers={
|
|
170
|
+
'x-api-key': self.api_key
|
|
171
|
+
},
|
|
172
|
+
params=data
|
|
173
|
+
)
|
|
174
|
+
return response.json()
|
|
175
|
+
|
|
176
|
+
def extract_image(self, element_description: str, cdp_url: str = None):
|
|
177
|
+
if not cdp_url and not self.session_id:
|
|
178
|
+
raise ValueError(f"Must call create_session before calling action extract_image with element_description='{element_description}'")
|
|
179
|
+
|
|
180
|
+
data = {'element_description': element_description}
|
|
181
|
+
|
|
182
|
+
if cdp_url:
|
|
183
|
+
data['cdp_url'] = cdp_url
|
|
184
|
+
else:
|
|
185
|
+
data['session_id'] = self.session_id
|
|
186
|
+
|
|
187
|
+
response = requests.get(
|
|
188
|
+
f"{BASE_URL}/extract-image",
|
|
189
|
+
headers={
|
|
190
|
+
'x-api-key': self.api_key
|
|
191
|
+
},
|
|
192
|
+
params=data
|
|
193
|
+
)
|
|
194
|
+
return response.json()
|
|
195
|
+
|
|
196
|
+
def scroll(self, pixels: int, cdp_url: str = None):
|
|
197
|
+
if not cdp_url and not self.session_id:
|
|
198
|
+
raise ValueError(f"Must call create_session before calling action scroll with pixels={pixels}")
|
|
199
|
+
|
|
200
|
+
data = {'pixels': pixels}
|
|
201
|
+
|
|
202
|
+
if cdp_url:
|
|
203
|
+
data['cdp_url'] = cdp_url
|
|
204
|
+
else:
|
|
205
|
+
data['session_id'] = self.session_id
|
|
206
|
+
|
|
207
|
+
response = requests.post(
|
|
208
|
+
f"{BASE_URL}/scroll",
|
|
209
|
+
headers={
|
|
210
|
+
'x-api-key': self.api_key
|
|
211
|
+
},
|
|
212
|
+
data=data
|
|
213
|
+
)
|
|
214
|
+
return response.json()
|
|
215
|
+
|
|
216
|
+
def wait(self, milliseconds: int, cdp_url: str = None):
|
|
217
|
+
if not cdp_url and not self.session_id:
|
|
218
|
+
raise ValueError(f"Must call create_session before calling action wait with milliseconds={milliseconds}")
|
|
219
|
+
|
|
220
|
+
data = {'milliseconds': milliseconds}
|
|
221
|
+
|
|
222
|
+
if cdp_url:
|
|
223
|
+
data['cdp_url'] = cdp_url
|
|
224
|
+
else:
|
|
225
|
+
data['session_id'] = self.session_id
|
|
226
|
+
|
|
227
|
+
response = requests.post(
|
|
228
|
+
f"{BASE_URL}/wait",
|
|
229
|
+
headers={
|
|
230
|
+
'x-api-key': self.api_key
|
|
231
|
+
},
|
|
232
|
+
data=data
|
|
233
|
+
)
|
|
234
|
+
return response.json()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: simplex
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: Official Python SDK for Simplex API
|
|
5
|
-
Home-page: https://
|
|
5
|
+
Home-page: https://simplex.sh
|
|
6
6
|
Author: Simplex Labs, Inc.
|
|
7
7
|
Author-email: founders@simplex.sh
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -10,7 +10,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Requires-Python: >=3.
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: openai>=1.0.0
|
|
@@ -21,6 +21,14 @@ Requires-Dist: rich>=13.0.0
|
|
|
21
21
|
Requires-Dist: prompt_toolkit>=3.0.0
|
|
22
22
|
Requires-Dist: playwright>=1.0.0
|
|
23
23
|
Requires-Dist: Pillow>=9.0.0
|
|
24
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
25
|
+
Requires-Dist: boto3>=1.28.0
|
|
26
|
+
Requires-Dist: requests>=2.31.0
|
|
27
|
+
Requires-Dist: MainContentExtractor
|
|
28
|
+
Requires-Dist: langchain_core
|
|
29
|
+
Requires-Dist: langchain_community
|
|
30
|
+
Requires-Dist: langchain_openai
|
|
31
|
+
Requires-Dist: langchain_anthropic
|
|
24
32
|
Dynamic: author
|
|
25
33
|
Dynamic: author-email
|
|
26
34
|
Dynamic: classifier
|
|
@@ -3,13 +3,13 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
setup.py
|
|
5
5
|
simplex/__init__.py
|
|
6
|
-
simplex/
|
|
6
|
+
simplex/cli.py
|
|
7
7
|
simplex/simplex.py
|
|
8
|
-
simplex/utils.py
|
|
9
8
|
simplex.egg-info/PKG-INFO
|
|
10
9
|
simplex.egg-info/SOURCES.txt
|
|
11
10
|
simplex.egg-info/dependency_links.txt
|
|
12
11
|
simplex.egg-info/entry_points.txt
|
|
13
12
|
simplex.egg-info/requires.txt
|
|
14
13
|
simplex.egg-info/top_level.txt
|
|
15
|
-
|
|
14
|
+
simplex/deploy/__init__.py
|
|
15
|
+
simplex/deploy/push.py
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
openai>=1.0.0
|
|
2
|
+
python-dotenv>=0.19.0
|
|
3
|
+
tiktoken>=0.5.0
|
|
4
|
+
click>=8.0.0
|
|
5
|
+
rich>=13.0.0
|
|
6
|
+
prompt_toolkit>=3.0.0
|
|
7
|
+
playwright>=1.0.0
|
|
8
|
+
Pillow>=9.0.0
|
|
9
|
+
PyYAML>=6.0.1
|
|
10
|
+
boto3>=1.28.0
|
|
11
|
+
requests>=2.31.0
|
|
12
|
+
MainContentExtractor
|
|
13
|
+
langchain_core
|
|
14
|
+
langchain_community
|
|
15
|
+
langchain_openai
|
|
16
|
+
langchain_anthropic
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
BASE_URL = "https://u3mvtbirxf.us-east-1.awsapprunner.com"
|
simplex-1.2.4/simplex/simplex.py
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
from playwright.sync_api import Page, sync_playwright, Browser
|
|
2
|
-
from PIL import Image
|
|
3
|
-
import requests
|
|
4
|
-
from typing import List
|
|
5
|
-
import io
|
|
6
|
-
|
|
7
|
-
from .utils import center_bbox, screenshot_to_image
|
|
8
|
-
|
|
9
|
-
BASE_URL = "https://u3mvtbirxf.us-east-1.awsapprunner.com"
|
|
10
|
-
|
|
11
|
-
class Simplex:
|
|
12
|
-
def __init__(self, api_key: str, browser: Browser = None):
|
|
13
|
-
"""
|
|
14
|
-
Initialize Simplex instance
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
api_key (str): API key for authentication
|
|
18
|
-
browser (optional): Browser instance (for Hyperbrowser integration)
|
|
19
|
-
"""
|
|
20
|
-
self.api_key = api_key
|
|
21
|
-
self.browser = browser
|
|
22
|
-
|
|
23
|
-
if browser is None:
|
|
24
|
-
self.playwright = sync_playwright().start()
|
|
25
|
-
self.browser = self.playwright.chromium.launch(headless=True)
|
|
26
|
-
self.driver = self.browser.new_page()
|
|
27
|
-
else:
|
|
28
|
-
self.playwright = None
|
|
29
|
-
self.driver = self.browser.contexts[0].pages[0]
|
|
30
|
-
self.driver.set_default_navigation_timeout(0)
|
|
31
|
-
self.driver.set_default_timeout(0)
|
|
32
|
-
|
|
33
|
-
def extract_bbox(self, element_description: str, state: Image.Image | None = None, annotate: bool = True) -> List[int]:
|
|
34
|
-
"""
|
|
35
|
-
Find an element in the screenshot using the element description
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
element_description (str): Description of the element to find
|
|
39
|
-
screenshot (PIL.Image.Image): Screenshot of the page
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
bounding_box (tuple): [x1, y1, x2, y2] bounding box of the found element
|
|
43
|
-
"""
|
|
44
|
-
print(f"[SIMPLEX] Finding element \"{element_description}\"...")
|
|
45
|
-
if state is None:
|
|
46
|
-
state = self.take_stable_screenshot()
|
|
47
|
-
|
|
48
|
-
endpoint = f"{BASE_URL}/find-element"
|
|
49
|
-
|
|
50
|
-
# Convert PIL Image to bytes
|
|
51
|
-
img_byte_arr = io.BytesIO()
|
|
52
|
-
state.save(img_byte_arr, format='PNG')
|
|
53
|
-
img_byte_arr = img_byte_arr.getvalue()
|
|
54
|
-
|
|
55
|
-
# Prepare multipart form data
|
|
56
|
-
files = {
|
|
57
|
-
'image_data': ('screenshot.png', img_byte_arr, 'image/png'),
|
|
58
|
-
'element_description': (None, element_description),
|
|
59
|
-
'api_key': (None, self.api_key)
|
|
60
|
-
}
|
|
61
|
-
print("[SIMPLEX] Sending screenshot to server...")
|
|
62
|
-
# Make the request
|
|
63
|
-
response = requests.post(
|
|
64
|
-
endpoint,
|
|
65
|
-
files=files
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# Print the results
|
|
70
|
-
print(response.status_code)
|
|
71
|
-
if response.status_code == 200:
|
|
72
|
-
res = response.json()
|
|
73
|
-
bbox = [int(res['x1']), int(res['y1']), int(res['x2']), int(res['y2'])]
|
|
74
|
-
|
|
75
|
-
# Add overlay directly to the page if driver exists
|
|
76
|
-
if hasattr(self, 'driver') and annotate:
|
|
77
|
-
# Create and inject overlay element
|
|
78
|
-
self.driver.evaluate("""
|
|
79
|
-
(bbox) => {
|
|
80
|
-
// Remove any existing overlay
|
|
81
|
-
const existingOverlay = document.getElementById('simplex-bbox-overlay');
|
|
82
|
-
if (existingOverlay) {
|
|
83
|
-
existingOverlay.remove();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Create new overlay
|
|
87
|
-
const overlay = document.createElement('div');
|
|
88
|
-
overlay.id = 'simplex-bbox-overlay';
|
|
89
|
-
overlay.style.position = 'fixed';
|
|
90
|
-
overlay.style.border = '2px dashed rgba(0, 255, 0, 1)';
|
|
91
|
-
overlay.style.background = 'rgba(74, 144, 226, 0.1)';
|
|
92
|
-
overlay.style.animation = 'marching-ants 0.5s linear infinite';
|
|
93
|
-
overlay.style.left = bbox[0] + 'px';
|
|
94
|
-
overlay.style.top = bbox[1] + 'px';
|
|
95
|
-
overlay.style.width = (bbox[2] - bbox[0]) + 'px';
|
|
96
|
-
overlay.style.height = (bbox[3] - bbox[1]) + 'px';
|
|
97
|
-
overlay.style.pointerEvents = 'none';
|
|
98
|
-
overlay.style.zIndex = '10000';
|
|
99
|
-
overlay.style.margin = '0';
|
|
100
|
-
overlay.style.padding = '0';
|
|
101
|
-
|
|
102
|
-
// Add marching ants animation keyframes
|
|
103
|
-
if (!document.querySelector('#marching-ants-keyframes')) {
|
|
104
|
-
const style = document.createElement('style');
|
|
105
|
-
style.id = 'marching-ants-keyframes';
|
|
106
|
-
style.textContent = `
|
|
107
|
-
@keyframes marching-ants {
|
|
108
|
-
0% { border-style: dashed; }
|
|
109
|
-
50% { border-style: solid; }
|
|
110
|
-
100% { border-style: dashed; }
|
|
111
|
-
}
|
|
112
|
-
`;
|
|
113
|
-
document.head.appendChild(style);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
document.body.appendChild(overlay);
|
|
117
|
-
|
|
118
|
-
// Remove overlay after 3 second
|
|
119
|
-
setTimeout(() => {
|
|
120
|
-
overlay.remove();
|
|
121
|
-
}, 2000);
|
|
122
|
-
}
|
|
123
|
-
""", bbox)
|
|
124
|
-
self.driver.wait_for_selector('#simplex-bbox-overlay')
|
|
125
|
-
return bbox
|
|
126
|
-
else:
|
|
127
|
-
print("[SIMPLEX] Error:", response.text)
|
|
128
|
-
|
|
129
|
-
def step_to_action(self, step_description: str, state: Image.Image | None = None) -> List[List[str]]:
|
|
130
|
-
"""
|
|
131
|
-
Convert a step description to an action
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
step_description (str): Description of the step to convert to action
|
|
135
|
-
screenshot (PIL.Image.Image): Screenshot of the page
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
action (List[List[str, str]]): List of actions to perform
|
|
139
|
-
"""
|
|
140
|
-
if state is None:
|
|
141
|
-
state = self.take_stable_screenshot()
|
|
142
|
-
|
|
143
|
-
endpoint = f"{BASE_URL}/step_to_action"
|
|
144
|
-
|
|
145
|
-
# Convert PIL Image to bytes
|
|
146
|
-
img_byte_arr = io.BytesIO()
|
|
147
|
-
state.save(img_byte_arr, format='PNG')
|
|
148
|
-
img_byte_arr = img_byte_arr.getvalue()
|
|
149
|
-
|
|
150
|
-
# Prepare form data
|
|
151
|
-
files = {
|
|
152
|
-
'image_data': ('screenshot.png', img_byte_arr, 'image/png'),
|
|
153
|
-
'step': (None, step_description),
|
|
154
|
-
'api_key': (None, self.api_key)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
# Make the request
|
|
158
|
-
response = requests.post(
|
|
159
|
-
endpoint,
|
|
160
|
-
files=files
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
# Handle response
|
|
164
|
-
if response.status_code == 200:
|
|
165
|
-
res = response.json()
|
|
166
|
-
actions = res.split('\n')
|
|
167
|
-
actions = [action.split(',') for action in actions]
|
|
168
|
-
actions = [[action.strip() for action in action_pair] for action_pair in actions]
|
|
169
|
-
return actions
|
|
170
|
-
else:
|
|
171
|
-
print(f"[SIMPLEX] Error: {response.status_code}")
|
|
172
|
-
print(response.text)
|
|
173
|
-
return []
|
|
174
|
-
|
|
175
|
-
def goto(self, url: str, new_tab: bool = False) -> None:
|
|
176
|
-
"""
|
|
177
|
-
Navigate to a URL
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
url (str): URL to navigate to
|
|
181
|
-
new_tab (bool): Whether to open a new tab or use the current tab
|
|
182
|
-
"""
|
|
183
|
-
if new_tab:
|
|
184
|
-
self.driver = self.browser.new_page()
|
|
185
|
-
self.driver.wait_for_load_state()
|
|
186
|
-
print(f"[SIMPLEX] Navigating to URL {url}...")
|
|
187
|
-
self.driver.goto(url)
|
|
188
|
-
|
|
189
|
-
def click(self, element_description: str, annotate: bool = True) -> None:
|
|
190
|
-
"""
|
|
191
|
-
Click on an element
|
|
192
|
-
"""
|
|
193
|
-
self.execute_action(["CLICK", element_description], annotate=annotate)
|
|
194
|
-
|
|
195
|
-
def type(self, text: str) -> None:
|
|
196
|
-
"""
|
|
197
|
-
Type text into an element
|
|
198
|
-
"""
|
|
199
|
-
self.execute_action(["TYPE", text])
|
|
200
|
-
|
|
201
|
-
def press_enter(self, annotate: bool = True) -> None:
|
|
202
|
-
"""
|
|
203
|
-
Press enter
|
|
204
|
-
"""
|
|
205
|
-
self.execute_action(["ENTER", ""], annotate=annotate)
|
|
206
|
-
|
|
207
|
-
def scroll(self, scroll_amount: int, annotate: bool = True) -> None:
|
|
208
|
-
"""
|
|
209
|
-
Scroll the page
|
|
210
|
-
"""
|
|
211
|
-
self.execute_action(["SCROLL", scroll_amount], annotate=annotate)
|
|
212
|
-
|
|
213
|
-
def wait(self, wait_time: int, annotate: bool = True) -> None:
|
|
214
|
-
"""
|
|
215
|
-
Wait for a given amount of time
|
|
216
|
-
"""
|
|
217
|
-
self.execute_action(["WAIT", wait_time], annotate=annotate)
|
|
218
|
-
|
|
219
|
-
def execute_action(self, action: List[List[str]], state: Image.Image | None = None, annotate: bool = True) -> None:
|
|
220
|
-
"""
|
|
221
|
-
Execute an action with playwright driver
|
|
222
|
-
|
|
223
|
-
Args:
|
|
224
|
-
action (List[List[str]]): List of actions to perform
|
|
225
|
-
"""
|
|
226
|
-
action_type, description = action
|
|
227
|
-
try:
|
|
228
|
-
if action_type == "CLICK":
|
|
229
|
-
bbox = self.extract_bbox(description, state, annotate=annotate)
|
|
230
|
-
center_x, center_y = center_bbox(bbox)
|
|
231
|
-
self.driver.mouse.click(center_x, center_y)
|
|
232
|
-
print(f"[SIMPLEX] Clicked on element \"{description}\"")
|
|
233
|
-
elif action_type == "TYPE":
|
|
234
|
-
self.driver.keyboard.type(description)
|
|
235
|
-
print(f"[SIMPLEX] Typed \"{description}\"")
|
|
236
|
-
|
|
237
|
-
elif action_type == "ENTER":
|
|
238
|
-
self.driver.keyboard.press("Enter")
|
|
239
|
-
print(f"[SIMPLEX] Pressed enter")
|
|
240
|
-
|
|
241
|
-
elif action_type == "SCROLL":
|
|
242
|
-
self.driver.mouse.wheel(0, int(description))
|
|
243
|
-
print(f"[SIMPLEX] Scrolled {description} pixels")
|
|
244
|
-
|
|
245
|
-
elif action_type == "WAIT":
|
|
246
|
-
self.driver.wait_for_timeout(int(description))
|
|
247
|
-
print(f"[SIMPLEX] Waited {description} seconds")
|
|
248
|
-
|
|
249
|
-
except Exception as e:
|
|
250
|
-
print(f"[SIMPLEX] Error executing action: {e}")
|
|
251
|
-
return None
|
|
252
|
-
|
|
253
|
-
def do(self, step_description: str, annotate: bool = True) -> None:
|
|
254
|
-
"""
|
|
255
|
-
Execute a step description
|
|
256
|
-
"""
|
|
257
|
-
state = self.take_stable_screenshot()
|
|
258
|
-
actions = self.step_to_action(step_description, state)
|
|
259
|
-
for action in actions:
|
|
260
|
-
self.execute_action(action, annotate=annotate)
|
|
261
|
-
|
|
262
|
-
def extract_text(self, element_description: str, state: Image.Image | None = None) -> List[str] | None:
|
|
263
|
-
"""
|
|
264
|
-
Extract an element text from the page
|
|
265
|
-
"""
|
|
266
|
-
print(f"[SIMPLEX] Finding element \"{element_description}\"...")
|
|
267
|
-
if state is None:
|
|
268
|
-
state = self.take_stable_screenshot()
|
|
269
|
-
|
|
270
|
-
endpoint = f"{BASE_URL}/extract-text"
|
|
271
|
-
|
|
272
|
-
# Convert PIL Image to bytes
|
|
273
|
-
img_byte_arr = io.BytesIO()
|
|
274
|
-
state.save(img_byte_arr, format='PNG')
|
|
275
|
-
img_byte_arr = img_byte_arr.getvalue()
|
|
276
|
-
|
|
277
|
-
# Prepare multipart form data
|
|
278
|
-
files = {
|
|
279
|
-
'image_data': ('screenshot.png', img_byte_arr, 'image/png'),
|
|
280
|
-
'element_description': (None, element_description),
|
|
281
|
-
'api_key': (None, self.api_key)
|
|
282
|
-
}
|
|
283
|
-
print("[SIMPLEX] Sending screenshot to server...")
|
|
284
|
-
|
|
285
|
-
# Make the request
|
|
286
|
-
response = requests.post(
|
|
287
|
-
endpoint,
|
|
288
|
-
files=files
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
# Print the results
|
|
292
|
-
if response.status_code == 200:
|
|
293
|
-
res = response.json()
|
|
294
|
-
text = res['text']
|
|
295
|
-
return text
|
|
296
|
-
else:
|
|
297
|
-
print("[SIMPLEX] Error:", response.text)
|
|
298
|
-
return None
|
|
299
|
-
|
|
300
|
-
def extract_image(self, element_description: str, state: Image.Image | None = None) -> Image.Image | None:
|
|
301
|
-
"""
|
|
302
|
-
Extract an element image from the page
|
|
303
|
-
"""
|
|
304
|
-
if state is None:
|
|
305
|
-
state = self.take_stable_screenshot()
|
|
306
|
-
|
|
307
|
-
bbox = self.extract_bbox(element_description, state)
|
|
308
|
-
cropped_state = state.crop((bbox[0], bbox[1], bbox[2], bbox[3]))
|
|
309
|
-
return cropped_state
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def take_stable_screenshot(self) -> Image.Image:
|
|
314
|
-
"""
|
|
315
|
-
Take a screenshot after ensuring the page is in a stable state.
|
|
316
|
-
|
|
317
|
-
Returns:
|
|
318
|
-
PIL.Image.Image: Screenshot of the current page
|
|
319
|
-
"""
|
|
320
|
-
print("[SIMPLEX] Taking screenshot of the page...")
|
|
321
|
-
self.driver.wait_for_load_state('networkidle')
|
|
322
|
-
return screenshot_to_image(self.driver.screenshot())
|
|
323
|
-
|
|
324
|
-
|
simplex-1.2.4/simplex/utils.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
from PIL import Image
|
|
3
|
-
import io
|
|
4
|
-
def center_bbox(bbox: List[int]) -> List[int]:
|
|
5
|
-
"""
|
|
6
|
-
Calculate the center coordinates of a bounding box
|
|
7
|
-
"""
|
|
8
|
-
return [(bbox[0] + bbox[2]) // 2, (bbox[1] + bbox[3]) // 2]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def screenshot_to_image(screenshot: bytes) -> Image:
|
|
12
|
-
return Image.open(io.BytesIO(screenshot))
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import os
|
|
3
|
-
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
4
|
-
from simplex import Simplex
|
|
5
|
-
|
|
6
|
-
from PIL import ImageDraw
|
|
7
|
-
|
|
8
|
-
from playwright.sync_api import sync_playwright
|
|
9
|
-
from PIL import Image
|
|
10
|
-
import time
|
|
11
|
-
import os
|
|
12
|
-
from browserbase import Browserbase
|
|
13
|
-
from hyperbrowser import Hyperbrowser
|
|
14
|
-
from hyperbrowser.models.session import CreateSessionParams
|
|
15
|
-
|
|
16
|
-
from dotenv import load_dotenv
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
load_dotenv()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def screenshot_tests():
|
|
23
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"))
|
|
24
|
-
image = "/home/ubuntu/supreme-waffle/images/netflix.png"
|
|
25
|
-
screenshot = Image.open(image)
|
|
26
|
-
|
|
27
|
-
start_time = time.time()
|
|
28
|
-
bbox = simplex.find_element("dark mode icon", screenshot)
|
|
29
|
-
end_time = time.time()
|
|
30
|
-
print(f"Time taken: {end_time - start_time} seconds")
|
|
31
|
-
print(bbox)
|
|
32
|
-
|
|
33
|
-
start_time = time.time()
|
|
34
|
-
action = simplex.step_to_action("click and enter email address", screenshot)
|
|
35
|
-
end_time = time.time()
|
|
36
|
-
print(f"Time taken: {end_time - start_time} seconds")
|
|
37
|
-
print(action)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def cgtrader_test():
|
|
44
|
-
assets = ["apple watch"]
|
|
45
|
-
urls = []
|
|
46
|
-
|
|
47
|
-
with sync_playwright() as p:
|
|
48
|
-
browser = p.chromium.launch(headless=False)
|
|
49
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"), browser=browser)
|
|
50
|
-
simplex.goto("https://www.cgtrader.com/")
|
|
51
|
-
|
|
52
|
-
for asset in assets:
|
|
53
|
-
simplex.goto("https://www.cgtrader.com")
|
|
54
|
-
simplex.do(f"search for {asset}")
|
|
55
|
-
simplex.do("click on search button")
|
|
56
|
-
simplex.do(f"click on the first product")
|
|
57
|
-
simplex.driver.wait_for_timeout(3000)
|
|
58
|
-
|
|
59
|
-
urls.append(simplex.driver.url)
|
|
60
|
-
|
|
61
|
-
print(urls)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_find_element():
|
|
65
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"))
|
|
66
|
-
simplex.goto("https://www.cgtrader.com/")
|
|
67
|
-
|
|
68
|
-
state = simplex.take_stable_screenshot()
|
|
69
|
-
bbox = simplex.find_element("search bar")
|
|
70
|
-
|
|
71
|
-
copy_image = state.copy()
|
|
72
|
-
draw = ImageDraw.Draw(copy_image)
|
|
73
|
-
draw.rectangle(bbox, outline='red', width=2)
|
|
74
|
-
copy_image.save("annotated_state.png")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# Get the page HTML and device scale factor
|
|
78
|
-
html = simplex.driver.content()
|
|
79
|
-
scale_factor = simplex.driver.evaluate("window.devicePixelRatio")
|
|
80
|
-
|
|
81
|
-
# Get viewport dimensions and other relevant settings
|
|
82
|
-
viewport_size = simplex.driver.viewport_size
|
|
83
|
-
zoom_level = simplex.driver.evaluate("document.documentElement.style.zoom || 1")
|
|
84
|
-
|
|
85
|
-
# Debug print
|
|
86
|
-
print(f"Original bbox: {bbox}")
|
|
87
|
-
print(f"Scale factor: {scale_factor}")
|
|
88
|
-
print(f"Viewport size: {viewport_size}")
|
|
89
|
-
print(f"Zoom level: {zoom_level}")
|
|
90
|
-
|
|
91
|
-
# Transform coordinates from screenshot to HTML
|
|
92
|
-
html_bbox = [
|
|
93
|
-
bbox[0] / scale_factor,
|
|
94
|
-
bbox[1] / scale_factor,
|
|
95
|
-
bbox[2] / scale_factor,
|
|
96
|
-
bbox[3] / scale_factor
|
|
97
|
-
]
|
|
98
|
-
print(f"Transformed bbox: {html_bbox}")
|
|
99
|
-
|
|
100
|
-
# Create HTML wrapper with matching viewport settings and scaled overlay
|
|
101
|
-
html_with_settings = f"""
|
|
102
|
-
<!DOCTYPE html>
|
|
103
|
-
<html>
|
|
104
|
-
<head>
|
|
105
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
106
|
-
<style>
|
|
107
|
-
body {{
|
|
108
|
-
margin: 0;
|
|
109
|
-
width: {viewport_size['width']}px;
|
|
110
|
-
height: {viewport_size['height']}px;
|
|
111
|
-
zoom: {zoom_level};
|
|
112
|
-
}}
|
|
113
|
-
.viewport-container {{
|
|
114
|
-
width: 100%;
|
|
115
|
-
height: 100%;
|
|
116
|
-
overflow: hidden;
|
|
117
|
-
position: relative;
|
|
118
|
-
transform-origin: top left;
|
|
119
|
-
transform: scale({1/scale_factor});
|
|
120
|
-
}}
|
|
121
|
-
#page-content {{
|
|
122
|
-
width: {viewport_size['width'] * scale_factor}px;
|
|
123
|
-
height: {viewport_size['height'] * scale_factor}px;
|
|
124
|
-
transform-origin: top left;
|
|
125
|
-
}}
|
|
126
|
-
#bbox-overlay {{
|
|
127
|
-
position: absolute;
|
|
128
|
-
border: 2px solid red;
|
|
129
|
-
left: {bbox[0]}px;
|
|
130
|
-
top: {bbox[1]}px;
|
|
131
|
-
width: {bbox[2] - bbox[0]}px;
|
|
132
|
-
height: {bbox[3] - bbox[1]}px;
|
|
133
|
-
pointer-events: none;
|
|
134
|
-
z-index: 10000;
|
|
135
|
-
}}
|
|
136
|
-
</style>
|
|
137
|
-
</head>
|
|
138
|
-
<body>
|
|
139
|
-
<div class="viewport-container">
|
|
140
|
-
<div id="page-content">
|
|
141
|
-
{html}
|
|
142
|
-
</div>
|
|
143
|
-
<div id="bbox-overlay"></div>
|
|
144
|
-
</div>
|
|
145
|
-
</body>
|
|
146
|
-
</html>
|
|
147
|
-
"""
|
|
148
|
-
|
|
149
|
-
# Save the HTML with viewport settings
|
|
150
|
-
with open('screenshot_with_viewport.html', 'w') as f:
|
|
151
|
-
f.write(html_with_settings)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def test_find_element_2():
|
|
155
|
-
|
|
156
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"))
|
|
157
|
-
# simplex.goto("https://www.cgtrader.com/")
|
|
158
|
-
state = Image.open("nvidia-stock.png")
|
|
159
|
-
bbox = simplex.extract_bbox("nvidia stock graph", state)
|
|
160
|
-
print(bbox)
|
|
161
|
-
|
|
162
|
-
state.crop(bbox)
|
|
163
|
-
state.save("cropped_state.png")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
import requests
|
|
170
|
-
def test_hyperbrowser_integration():
|
|
171
|
-
"""Test Simplex integration with Hyperbrowser"""
|
|
172
|
-
import time
|
|
173
|
-
start_time = time.time()
|
|
174
|
-
|
|
175
|
-
bb = Browserbase(api_key=os.getenv("BROWSERBASE_API_KEY"))
|
|
176
|
-
session_start = time.time()
|
|
177
|
-
session = bb.sessions.create(project_id=os.getenv("BROWSERBASE_PROJECT_ID"))
|
|
178
|
-
print(session.id)
|
|
179
|
-
print(f"Session creation took {time.time() - session_start:.2f}s")
|
|
180
|
-
|
|
181
|
-
# Initialize Hyperbrowser client
|
|
182
|
-
# client = Hyperbrowser(api_key=os.getenv("HYPERBROWSER_API_KEY"))
|
|
183
|
-
|
|
184
|
-
# Create session params with CAPTCHA solving enabled
|
|
185
|
-
session_params = CreateSessionParams(
|
|
186
|
-
solve_captchas=True,
|
|
187
|
-
use_stealth=True # Recommended when solving CAPTCHAs
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
# Create a new session with params
|
|
191
|
-
# session = client.sessions.create(session_params)
|
|
192
|
-
ws_endpoint = session.connect_url
|
|
193
|
-
|
|
194
|
-
try:
|
|
195
|
-
with sync_playwright() as p:
|
|
196
|
-
# Connect browser to Hyperbrowser session
|
|
197
|
-
browser_start = time.time()
|
|
198
|
-
browser = p.chromium.connect_over_cdp(ws_endpoint)
|
|
199
|
-
print(f"Browser connection took {time.time() - browser_start:.2f}s")
|
|
200
|
-
url = f"https://api.browserbase.com/v1/sessions/{session.id}/debug"
|
|
201
|
-
headers = {"X-BB-API-Key": os.getenv("BROWSERBASE_API_KEY")}
|
|
202
|
-
response = requests.get(url, headers=headers)
|
|
203
|
-
print(response.text)
|
|
204
|
-
print(response.json()["pages"][0]["debuggerUrl"])
|
|
205
|
-
|
|
206
|
-
# Initialize Simplex with the Hyperbrowser-connected page
|
|
207
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"), browser=browser)
|
|
208
|
-
|
|
209
|
-
# Test basic functionality
|
|
210
|
-
nav_start = time.time()
|
|
211
|
-
simplex.goto("https://www.turbosquid.com/")
|
|
212
|
-
print(f"Navigation took {time.time() - nav_start:.2f}s")
|
|
213
|
-
|
|
214
|
-
# search_start = time.time()
|
|
215
|
-
# simplex.do("search for iphone")
|
|
216
|
-
# print(f"Search action took {time.time() - search_start:.2f}s")
|
|
217
|
-
|
|
218
|
-
# Verify the search worked by finding the search results
|
|
219
|
-
find_start = time.time()
|
|
220
|
-
bbox = simplex.find_element("search results", annotate=True)
|
|
221
|
-
print(f"Finding results took {time.time() - find_start:.2f}s")
|
|
222
|
-
assert bbox is not None, "Search results not found"
|
|
223
|
-
|
|
224
|
-
finally:
|
|
225
|
-
# Always stop the Hyperbrowser session
|
|
226
|
-
# client.sessions.stop(session.id)
|
|
227
|
-
browser.close()
|
|
228
|
-
print(f"Total test time: {time.time() - start_time:.2f}s")
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def execute_action_test():
|
|
233
|
-
playwright = sync_playwright().start()
|
|
234
|
-
browser = playwright.chromium.launch(headless=False)
|
|
235
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"), browser=browser)
|
|
236
|
-
simplex.goto("https://www.mit.edu/")
|
|
237
|
-
simplex.find_element('search bar')
|
|
238
|
-
|
|
239
|
-
time.sleep(3)
|
|
240
|
-
|
|
241
|
-
# Save HTML content
|
|
242
|
-
html_content = simplex.driver.content()
|
|
243
|
-
with open('page.html', 'w', encoding='utf-8') as f:
|
|
244
|
-
f.write(html_content)
|
|
245
|
-
|
|
246
|
-
# Keep browser open until user interrupts with Ctrl+C
|
|
247
|
-
try:
|
|
248
|
-
print("Browser is kept open. Press Ctrl+C to exit...")
|
|
249
|
-
while True:
|
|
250
|
-
time.sleep(1)
|
|
251
|
-
except KeyboardInterrupt:
|
|
252
|
-
browser.close()
|
|
253
|
-
playwright.stop()
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def test_extract_text():
|
|
257
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"))
|
|
258
|
-
simplex.goto("https://www.cgtrader.com/3d-models/electronics/other/apple-watch-ultra-2022-hq-3d-model")
|
|
259
|
-
time.sleep(2)
|
|
260
|
-
start = time.time()
|
|
261
|
-
text = simplex.extract_text("type of license")
|
|
262
|
-
end = time.time()
|
|
263
|
-
print(f"Time taken: {end - start:.2f}s")
|
|
264
|
-
print(text)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def test_extract_image():
|
|
268
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"))
|
|
269
|
-
state = Image.open("nvidia-stock.png")
|
|
270
|
-
image = simplex.extract_image("stock trends and market summary", state)
|
|
271
|
-
image.save("cropped_image.png")
|
|
272
|
-
|
|
273
|
-
print(type(simplex.driver))
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if __name__ == "__main__":
|
|
277
|
-
simplex = Simplex(api_key=os.getenv("SIMPLEX_API_KEY"))
|
|
278
|
-
simplex.goto("https://www.google.com")
|
|
279
|
-
simplex.click("search bar")
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|