simplex 1.2.4__tar.gz → 1.2.6__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.

@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: simplex
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Official Python SDK for Simplex API
5
- Home-page: https://github.com/shreyka/simplex-python
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.6
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.4",
5
+ version="1.2.6",
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://github.com/shreyka/simplex-python",
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.6",
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,3 @@
1
+ from .push import push_directory, run_directory
2
+
3
+ __all__ = ['push_directory', 'run_directory']
@@ -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,256 @@
1
+ import os
2
+ import requests
3
+ import atexit
4
+
5
+ # BASE_URL = "https://api.simplex.sh"
6
+ BASE_URL = "http://localhost:8000"
7
+
8
+ class Simplex:
9
+ def __init__(self, api_key: str):
10
+ self.api_key = api_key
11
+ self.session_id = None
12
+ atexit.register(self.cleanup_session)
13
+
14
+ def cleanup_session(self):
15
+ if self.session_id:
16
+ try:
17
+ self.close_session(self.session_id)
18
+ except:
19
+ pass
20
+
21
+ def close_session(self):
22
+ response = requests.post(
23
+ f"{BASE_URL}/close",
24
+ headers={
25
+ 'x-api-key': self.api_key
26
+ },
27
+ data={'session_id': self.session_id}
28
+ )
29
+ self.session_id = None
30
+ return response.json()
31
+
32
+ def create_session(self):
33
+ response = requests.post(
34
+ f"{BASE_URL}/create-session",
35
+ headers={
36
+ 'x-api-key': self.api_key
37
+ }
38
+ )
39
+
40
+ response_json = response.json()
41
+ self.session_id = response_json['session_id']
42
+ livestream_url = response_json['livestream_url']
43
+
44
+ print(f"\nSession created successfully!")
45
+ print(f"Session ID: {self.session_id}")
46
+ print(f"Livestream URL: {livestream_url}\n")
47
+
48
+ return self.session_id, livestream_url
49
+
50
+ def goto(self, url: str, cdp_url: str = None):
51
+ if not cdp_url and not self.session_id:
52
+ raise ValueError(f"Must call create_session before calling action goto with url='{url}'")
53
+
54
+ if not url.startswith('http://') and not url.startswith('https://'):
55
+ url = 'https://' + url
56
+
57
+ data = {'url': url}
58
+
59
+ if cdp_url:
60
+ data['cdp_url'] = cdp_url
61
+ else:
62
+ data['session_id'] = self.session_id
63
+
64
+ response = requests.post(
65
+ f"{BASE_URL}/goto",
66
+ headers={
67
+ 'x-api-key': self.api_key
68
+ },
69
+ data=data
70
+ )
71
+ print(response.json())
72
+
73
+ def click(self, element_description: str, cdp_url: str = None):
74
+ if not cdp_url and not self.session_id:
75
+ raise ValueError(f"Must call create_session before calling action click with element_description='{element_description}'")
76
+
77
+ data = {'element_description': element_description}
78
+
79
+ if cdp_url:
80
+ data['cdp_url'] = cdp_url
81
+ else:
82
+ data['session_id'] = self.session_id
83
+
84
+ response = requests.post(
85
+ f"{BASE_URL}/click",
86
+ headers={
87
+ 'x-api-key': self.api_key
88
+ },
89
+ data=data
90
+ )
91
+ print(response.json())
92
+
93
+ def type(self, text: str, cdp_url: str = None):
94
+ if not cdp_url and not self.session_id:
95
+ raise ValueError(f"Must call create_session before calling action type with text='{text}'")
96
+
97
+ data = {'text': text}
98
+
99
+ if cdp_url:
100
+ data['cdp_url'] = cdp_url
101
+ else:
102
+ data['session_id'] = self.session_id
103
+
104
+ response = requests.post(
105
+ f"{BASE_URL}/type",
106
+ headers={
107
+ 'x-api-key': self.api_key
108
+ },
109
+ data=data
110
+ )
111
+ print(response.json())
112
+
113
+ def press_enter(self, cdp_url: str = None):
114
+ if not cdp_url and not self.session_id:
115
+ raise ValueError("Must call create_session before calling action press_enter")
116
+
117
+ data = {}
118
+
119
+ if cdp_url:
120
+ data['cdp_url'] = cdp_url
121
+ else:
122
+ data['session_id'] = self.session_id
123
+
124
+ response = requests.post(
125
+ f"{BASE_URL}/press-enter",
126
+ headers={
127
+ 'x-api-key': self.api_key
128
+ },
129
+ data=data
130
+ )
131
+ print(response.json())
132
+
133
+ def extract_bbox(self, element_description: str, cdp_url: str = None):
134
+ if not cdp_url and not self.session_id:
135
+ raise ValueError(f"Must call create_session before calling action extract_bbox with element_description='{element_description}'")
136
+
137
+ data = {'element_description': element_description}
138
+
139
+ if cdp_url:
140
+ data['cdp_url'] = cdp_url
141
+ else:
142
+ data['session_id'] = self.session_id
143
+
144
+ response = requests.get(
145
+ f"{BASE_URL}/extract-bbox",
146
+ headers={
147
+ 'x-api-key': self.api_key
148
+ },
149
+ params=data
150
+ )
151
+
152
+ return response.json()
153
+
154
+ def extract_text(self, element_description: str, cdp_url: str = None, use_vision: bool = False):
155
+ if not cdp_url and not self.session_id:
156
+ raise ValueError(f"Must call create_session before calling action extract_text with element_description='{element_description}'")
157
+
158
+ data = {'element_description': element_description}
159
+
160
+ if cdp_url:
161
+ data['cdp_url'] = cdp_url
162
+ else:
163
+ data['session_id'] = self.session_id
164
+
165
+ if use_vision:
166
+ data['use_vision'] = use_vision
167
+
168
+ response = requests.get(
169
+ f"{BASE_URL}/extract-text",
170
+ headers={
171
+ 'x-api-key': self.api_key
172
+ },
173
+ params=data
174
+ )
175
+ return response.json()
176
+
177
+ def extract_image(self, element_description: str, cdp_url: str = None):
178
+ if not cdp_url and not self.session_id:
179
+ raise ValueError(f"Must call create_session before calling action extract_image with element_description='{element_description}'")
180
+
181
+ data = {'element_description': element_description}
182
+
183
+ if cdp_url:
184
+ data['cdp_url'] = cdp_url
185
+ else:
186
+ data['session_id'] = self.session_id
187
+
188
+ response = requests.get(
189
+ f"{BASE_URL}/extract-image",
190
+ headers={
191
+ 'x-api-key': self.api_key
192
+ },
193
+ params=data
194
+ )
195
+ return response.json()
196
+
197
+ def scroll(self, pixels: float, cdp_url: str = None):
198
+ if not cdp_url and not self.session_id:
199
+ raise ValueError(f"Must call create_session before calling action scroll with pixels={pixels}")
200
+
201
+ data = {'pixels': pixels}
202
+ print(data)
203
+
204
+ if cdp_url:
205
+ data['cdp_url'] = cdp_url
206
+ else:
207
+ data['session_id'] = self.session_id
208
+
209
+ response = requests.post(
210
+ f"{BASE_URL}/scroll",
211
+ headers={
212
+ 'x-api-key': self.api_key
213
+ },
214
+ data=data
215
+ )
216
+ return response.json()
217
+
218
+ def wait(self, milliseconds: int, cdp_url: str = None):
219
+ if not cdp_url and not self.session_id:
220
+ raise ValueError(f"Must call create_session before calling action wait with milliseconds={milliseconds}")
221
+
222
+ data = {'milliseconds': milliseconds}
223
+
224
+ if cdp_url:
225
+ data['cdp_url'] = cdp_url
226
+ else:
227
+ data['session_id'] = self.session_id
228
+
229
+ response = requests.post(
230
+ f"{BASE_URL}/wait",
231
+ headers={
232
+ 'x-api-key': self.api_key
233
+ },
234
+ data=data
235
+ )
236
+ return response.json()
237
+
238
+ def run_agent(self, prompt: str, cdp_url: str = None):
239
+ if not cdp_url and not self.session_id:
240
+ raise ValueError(f"Must call create_session before calling action run_agent with and prompt='{prompt}'")
241
+
242
+ data = {'prompt': prompt}
243
+
244
+ if cdp_url:
245
+ data['cdp_url'] = cdp_url
246
+ else:
247
+ data['session_id'] = self.session_id
248
+
249
+ response = requests.post(
250
+ f"{BASE_URL}/run-agent",
251
+ headers={
252
+ 'x-api-key': self.api_key
253
+ },
254
+ data=data
255
+ )
256
+ return response.json()
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: simplex
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Official Python SDK for Simplex API
5
- Home-page: https://github.com/shreyka/simplex-python
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.6
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/constants.py
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
- tests/test_local.py
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"
@@ -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
-
@@ -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,8 +0,0 @@
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
@@ -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