optexity 0.1.5.2__tar.gz → 0.1.5.3__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.
- {optexity-0.1.5.2 → optexity-0.1.5.3}/PKG-INFO +1 -1
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/logging.py +8 -1
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/run_automation.py +6 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/infra/browser.py +187 -52
- optexity-0.1.5.3/optexity/inference/infra/utils.py +98 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/task.py +1 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/utils/utils.py +14 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity.egg-info/PKG-INFO +1 -1
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity.egg-info/SOURCES.txt +1 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/pyproject.toml +1 -1
- {optexity-0.1.5.2 → optexity-0.1.5.3}/LICENSE +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/README.md +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/cli.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/add_example.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/download_pdf_url.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/extract_price_stockanalysis.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/file_upload.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/i94.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/i94_travel_history.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/peachstate_medicaid.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/examples/supabase_login.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/exceptions.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/error_handler/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/error_handler/error_handler.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/error_handler/prompt.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/index_prediction/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/index_prediction/action_prediction_locator_axtree.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/index_prediction/prompt.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/select_value_prediction/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/select_value_prediction/prompt.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/select_value_prediction/select_value_prediction.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/two_fa_extraction/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/two_fa_extraction/prompt.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/two_fa_extraction/two_fa_extraction.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/child_process.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_agentic_task.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_check.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_click.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_command.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_hover.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_input.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_keypress.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_select.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_select_utils.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_upload.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/utils.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/run_assertion.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/run_extraction.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/run_interaction.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/run_python_script.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/run_two_fa.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/two_factor_auth/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/infra/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/infra/browser_extension.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/models/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/models/gemini.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/models/human.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/models/llm_model.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/run_local.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/onepassword_integration.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/actions/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/actions/assertion_action.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/actions/extraction_action.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/actions/interaction_action.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/actions/misc_action.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/actions/prompts.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/actions/two_fa_action.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/automation.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/callback.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/inference.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/memory.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/schema/token_usage.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/test.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/utils/__init__.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/utils/settings.py +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity.egg-info/dependency_links.txt +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity.egg-info/entry_points.txt +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity.egg-info/requires.txt +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/optexity.egg-info/top_level.txt +0 -0
- {optexity-0.1.5.2 → optexity-0.1.5.3}/setup.cfg +0 -0
|
@@ -364,7 +364,14 @@ async def save_latest_memory_state_locally(
|
|
|
364
364
|
await f.write(json.dumps(task.input_parameters, indent=4))
|
|
365
365
|
|
|
366
366
|
async with aiofiles.open(step_directory / "secure_parameters.json", "w") as f:
|
|
367
|
-
|
|
367
|
+
secure_parameters = {
|
|
368
|
+
key: [
|
|
369
|
+
a.model_dump(exclude_none=True, exclude_defaults=True)
|
|
370
|
+
for a in value
|
|
371
|
+
]
|
|
372
|
+
for key, value in task.secure_parameters.items()
|
|
373
|
+
}
|
|
374
|
+
await f.write(json.dumps(secure_parameters, indent=4))
|
|
368
375
|
|
|
369
376
|
async with aiofiles.open(step_directory / "generated_variables.json", "w") as f:
|
|
370
377
|
await f.write(json.dumps(memory.variables.generated_variables, indent=4))
|
|
@@ -54,11 +54,14 @@ async def run_automation(task: Task, child_process_id: int):
|
|
|
54
54
|
logger.info(f"Task {task.task_id} started running")
|
|
55
55
|
memory = None
|
|
56
56
|
browser = None
|
|
57
|
+
|
|
57
58
|
try:
|
|
58
59
|
await start_task_in_server(task)
|
|
59
60
|
memory = Memory()
|
|
61
|
+
|
|
60
62
|
browser = Browser(
|
|
61
63
|
memory=memory,
|
|
64
|
+
user_data_dir=f"/tmp/userdata_{task.task_id}",
|
|
62
65
|
headless=False,
|
|
63
66
|
channel=task.automation.browser_channel,
|
|
64
67
|
debug_port=9222 + child_process_id,
|
|
@@ -66,9 +69,12 @@ async def run_automation(task: Task, child_process_id: int):
|
|
|
66
69
|
proxy_session_id=task.proxy_session_id(
|
|
67
70
|
settings.PROXY_PROVIDER if task.use_proxy else None
|
|
68
71
|
),
|
|
72
|
+
is_dedicated=task.is_dedicated,
|
|
69
73
|
)
|
|
70
74
|
await browser.start()
|
|
71
75
|
|
|
76
|
+
browser.memory = memory
|
|
77
|
+
|
|
72
78
|
automation = task.automation
|
|
73
79
|
|
|
74
80
|
memory.automation_state.step_index = -1
|
|
@@ -2,35 +2,48 @@ import asyncio
|
|
|
2
2
|
import base64
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
import pathlib
|
|
5
6
|
import re
|
|
7
|
+
import shutil
|
|
6
8
|
from typing import Literal
|
|
7
9
|
from uuid import uuid4
|
|
8
10
|
|
|
11
|
+
import patchright.async_api
|
|
12
|
+
import playwright.async_api
|
|
9
13
|
from browser_use import Agent, BrowserSession, ChatGoogle
|
|
10
14
|
from browser_use.browser.views import BrowserStateSummary
|
|
11
15
|
from patchright._impl._errors import TimeoutError as PatchrightTimeoutError
|
|
12
16
|
from playwright._impl._errors import TimeoutError as PlaywrightTimeoutError
|
|
13
17
|
from playwright.async_api import Download, Locator, Page, Request, Response
|
|
14
18
|
|
|
19
|
+
from optexity.inference.infra.utils import _download_extension, _extract_extension
|
|
15
20
|
from optexity.schema.memory import Memory, NetworkRequest, NetworkResponse
|
|
16
21
|
from optexity.utils.settings import settings
|
|
17
22
|
|
|
18
23
|
logger = logging.getLogger(__name__)
|
|
19
24
|
|
|
25
|
+
_global_playwright: (
|
|
26
|
+
playwright.async_api.Playwright | patchright.async_api.Playwright | None
|
|
27
|
+
) = None
|
|
28
|
+
_global_context: (
|
|
29
|
+
playwright.async_api.BrowserContext | patchright.async_api.BrowserContext | None
|
|
30
|
+
) = None
|
|
31
|
+
|
|
20
32
|
|
|
21
33
|
class Browser:
|
|
22
34
|
def __init__(
|
|
23
35
|
self,
|
|
24
36
|
memory: Memory,
|
|
25
|
-
user_data_dir: str
|
|
37
|
+
user_data_dir: str,
|
|
26
38
|
headless: bool = False,
|
|
27
|
-
proxy: str = None,
|
|
39
|
+
proxy: str | None = None,
|
|
28
40
|
stealth: bool = True,
|
|
29
41
|
backend: Literal["browser-use", "browserbase"] = "browser-use",
|
|
30
42
|
debug_port: int = 9222,
|
|
31
43
|
channel: Literal["chromium", "chrome"] = "chromium",
|
|
32
44
|
use_proxy: bool = False,
|
|
33
45
|
proxy_session_id: str | None = None,
|
|
46
|
+
is_dedicated: bool = False,
|
|
34
47
|
):
|
|
35
48
|
|
|
36
49
|
if proxy:
|
|
@@ -44,9 +57,15 @@ class Browser:
|
|
|
44
57
|
self.debug_port = debug_port
|
|
45
58
|
self.use_proxy = use_proxy
|
|
46
59
|
self.proxy_session_id = proxy_session_id
|
|
47
|
-
self.playwright
|
|
60
|
+
self.playwright: (
|
|
61
|
+
playwright.async_api.Playwright | patchright.async_api.Playwright | None
|
|
62
|
+
) = None
|
|
48
63
|
self.browser = None
|
|
49
|
-
self.context
|
|
64
|
+
self.context: (
|
|
65
|
+
playwright.async_api.BrowserContext
|
|
66
|
+
| patchright.async_api.BrowserContext
|
|
67
|
+
| None
|
|
68
|
+
) = None
|
|
50
69
|
self.page = None
|
|
51
70
|
self.cdp_url = f"http://localhost:{self.debug_port}"
|
|
52
71
|
self.backend_agent = None
|
|
@@ -54,16 +73,108 @@ class Browser:
|
|
|
54
73
|
self.memory = memory
|
|
55
74
|
self.page_to_target_id = []
|
|
56
75
|
self.previous_total_pages = 0
|
|
57
|
-
|
|
76
|
+
self.is_dedicated = is_dedicated
|
|
58
77
|
self.active_downloads = 0
|
|
59
78
|
self.all_active_downloads_done = asyncio.Event()
|
|
60
79
|
self.all_active_downloads_done.set()
|
|
61
80
|
|
|
62
81
|
self.network_calls: list[NetworkResponse | NetworkRequest] = []
|
|
63
82
|
|
|
83
|
+
self.extensions = [
|
|
84
|
+
# {
|
|
85
|
+
# "name": "optexity recorder",
|
|
86
|
+
# "id": "pbaganbicadeoacahamnbgohafchgakp",
|
|
87
|
+
# "url": "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=133&acceptformat=crx3&x=id%3Dpbaganbicadeoacahamnbgohafchgakp%26uc",
|
|
88
|
+
# },
|
|
89
|
+
{
|
|
90
|
+
"name": "I still don't care about cookies",
|
|
91
|
+
"id": "edibdbjcniadpccecjdfdjjppcpchdlm",
|
|
92
|
+
"url": "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=133&acceptformat=crx3&x=id%3Dedibdbjcniadpccecjdfdjjppcpchdlm%26uc",
|
|
93
|
+
},
|
|
94
|
+
# {
|
|
95
|
+
# "name": "popupoff",
|
|
96
|
+
# "id": "kiodaajmphnkcajieajajinghpejdjai",
|
|
97
|
+
# "url": "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=133&acceptformat=crx3&x=id%3Dkiodaajmphnkcajieajajinghpejdjai%26uc",
|
|
98
|
+
# },
|
|
99
|
+
{
|
|
100
|
+
"name": "ublock origin",
|
|
101
|
+
"id": "ddkjiahejlhfcafbddmgiahcphecmpfh",
|
|
102
|
+
"url": "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=133&acceptformat=crx3&x=id%3Dddkjiahejlhfcafbddmgiahcphecmpfh%26uc",
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
|
|
64
106
|
async def start(self):
|
|
107
|
+
global _global_playwright, _global_context
|
|
65
108
|
logger.debug("Starting browser")
|
|
66
109
|
try:
|
|
110
|
+
cache_dir = pathlib.Path("/tmp/extensions")
|
|
111
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
extension_paths = []
|
|
113
|
+
loaded_extension_names = []
|
|
114
|
+
for ext in self.extensions:
|
|
115
|
+
ext_dir = cache_dir / ext["id"]
|
|
116
|
+
crx_file = cache_dir / f'{ext["id"]}.crx'
|
|
117
|
+
|
|
118
|
+
# Check if extension is already extracted
|
|
119
|
+
if ext_dir.exists() and (ext_dir / "manifest.json").exists():
|
|
120
|
+
logger.info(
|
|
121
|
+
f'✅ Using cached {ext["name"]} extension from {ext_dir}'
|
|
122
|
+
)
|
|
123
|
+
extension_paths.append(str(ext_dir))
|
|
124
|
+
loaded_extension_names.append(ext["name"])
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Download extension if not cached
|
|
129
|
+
if not crx_file.exists():
|
|
130
|
+
logger.info(f'📦 Downloading {ext["name"]} extension...')
|
|
131
|
+
_download_extension(ext["url"], crx_file)
|
|
132
|
+
else:
|
|
133
|
+
logger.info(f'📦 Found cached {ext["name"]} .crx file')
|
|
134
|
+
|
|
135
|
+
# Extract extension
|
|
136
|
+
logger.info(f'📂 Extracting {ext["name"]} extension...')
|
|
137
|
+
_extract_extension(crx_file, ext_dir)
|
|
138
|
+
|
|
139
|
+
extension_paths.append(str(ext_dir))
|
|
140
|
+
loaded_extension_names.append(ext["name"])
|
|
141
|
+
logger.info(f'✅ Successfully loaded {ext["name"]}')
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(
|
|
145
|
+
f'❌ Failed to setup {ext["name"]} extension: {e}',
|
|
146
|
+
exc_info=True,
|
|
147
|
+
)
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if not extension_paths:
|
|
151
|
+
logger.error("⚠️ No extensions were loaded successfully!")
|
|
152
|
+
|
|
153
|
+
logger.info(f"Loaded extensions: {', '.join(loaded_extension_names)}")
|
|
154
|
+
|
|
155
|
+
args = [
|
|
156
|
+
"--disable-site-isolation-trials",
|
|
157
|
+
"--disable-web-security",
|
|
158
|
+
"--disable-features=IsolateOrigins,site-per-process",
|
|
159
|
+
"--allow-running-insecure-content",
|
|
160
|
+
"--ignore-certificate-errors",
|
|
161
|
+
"--ignore-ssl-errors",
|
|
162
|
+
"--ignore-certificate-errors-spki-list",
|
|
163
|
+
"--enable-extensions",
|
|
164
|
+
"--disable-extensions-file-access-check",
|
|
165
|
+
"--disable-extensions-http-throttling",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
if extension_paths:
|
|
169
|
+
disable_except = (
|
|
170
|
+
f'--disable-extensions-except={",".join(extension_paths)}'
|
|
171
|
+
)
|
|
172
|
+
load_extension = f'--load-extension={",".join(extension_paths)}'
|
|
173
|
+
args.append(disable_except)
|
|
174
|
+
args.append(load_extension)
|
|
175
|
+
logger.info(f"Extension args: {disable_except}")
|
|
176
|
+
logger.info(f"Extension args: {load_extension}")
|
|
177
|
+
|
|
67
178
|
if self.playwright is not None:
|
|
68
179
|
await self.playwright.stop()
|
|
69
180
|
|
|
@@ -98,44 +209,64 @@ class Browser:
|
|
|
98
209
|
if settings.PROXY_PASSWORD is not None:
|
|
99
210
|
proxy["password"] = settings.PROXY_PASSWORD
|
|
100
211
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
212
|
+
if (
|
|
213
|
+
_global_playwright is None
|
|
214
|
+
or _global_context is None
|
|
215
|
+
or not self.is_dedicated
|
|
216
|
+
):
|
|
217
|
+
self.playwright = await async_playwright().start()
|
|
218
|
+
self.context = await self.playwright.chromium.launch_persistent_context(
|
|
219
|
+
channel=self.channel,
|
|
220
|
+
user_data_dir=self.user_data_dir,
|
|
221
|
+
headless=self.headless,
|
|
222
|
+
proxy=proxy,
|
|
223
|
+
args=[
|
|
224
|
+
# "--start-fullscreen",
|
|
225
|
+
"--disable-popup-blocking",
|
|
226
|
+
"--window-size=1920,1080",
|
|
227
|
+
f"--remote-debugging-port={self.debug_port}",
|
|
228
|
+
"--disable-gpu",
|
|
229
|
+
"--disable-background-networking",
|
|
230
|
+
]
|
|
231
|
+
+ args,
|
|
232
|
+
chromium_sandbox=False,
|
|
233
|
+
no_viewport=True,
|
|
234
|
+
)
|
|
235
|
+
_global_playwright = self.playwright
|
|
236
|
+
_global_context = self.context
|
|
117
237
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
238
|
+
async def log_request(req: Request):
|
|
239
|
+
await self.log_request(req)
|
|
121
240
|
|
|
122
|
-
|
|
123
|
-
|
|
241
|
+
async def handle_random_download(download: Download):
|
|
242
|
+
await self.handle_random_download(download)
|
|
124
243
|
|
|
125
|
-
|
|
126
|
-
|
|
244
|
+
async def handle_random_url_downloads(resp: Response):
|
|
245
|
+
await self.handle_random_url_downloads(resp)
|
|
127
246
|
|
|
128
|
-
|
|
129
|
-
|
|
247
|
+
self.context.on("request", log_request)
|
|
248
|
+
self.context.on("response", handle_random_url_downloads)
|
|
130
249
|
|
|
131
|
-
|
|
132
|
-
|
|
250
|
+
self.context.on(
|
|
251
|
+
"page", lambda p: (p.on("download", handle_random_download))
|
|
252
|
+
)
|
|
133
253
|
|
|
134
|
-
self.
|
|
135
|
-
|
|
136
|
-
|
|
254
|
+
elif self.is_dedicated:
|
|
255
|
+
self.context = _global_context
|
|
256
|
+
self.playwright = _global_playwright
|
|
257
|
+
for i in range(len(self.context.pages) - 1, 0, -1):
|
|
258
|
+
await self.context.pages[i].close()
|
|
259
|
+
else:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"Browser is not dedicated and global playwright and context are not set"
|
|
262
|
+
)
|
|
137
263
|
|
|
138
|
-
self.
|
|
264
|
+
# self.context = await self.browser.new_context(
|
|
265
|
+
# no_viewport=True, ignore_https_errors=True
|
|
266
|
+
# )
|
|
267
|
+
|
|
268
|
+
# self.page = await self.context.new_page()
|
|
269
|
+
self.page = self.context.pages[0]
|
|
139
270
|
|
|
140
271
|
browser_session = BrowserSession(cdp_url=self.cdp_url, keep_alive=True)
|
|
141
272
|
|
|
@@ -162,7 +293,7 @@ class Browser:
|
|
|
162
293
|
raise e
|
|
163
294
|
|
|
164
295
|
async def stop(self):
|
|
165
|
-
logger.debug("Stopping
|
|
296
|
+
logger.debug("Stopping backend agent")
|
|
166
297
|
if self.backend_agent is not None:
|
|
167
298
|
logger.debug("Stopping backend agent")
|
|
168
299
|
self.backend_agent.stop()
|
|
@@ -174,21 +305,25 @@ class Browser:
|
|
|
174
305
|
logger.debug("Browser session reset")
|
|
175
306
|
self.backend_agent = None
|
|
176
307
|
|
|
177
|
-
if self.
|
|
178
|
-
logger.debug("Stopping context")
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
308
|
+
if not self.is_dedicated:
|
|
309
|
+
logger.debug("Stopping context and playwright and browser as not dedicated")
|
|
310
|
+
if self.context is not None:
|
|
311
|
+
logger.debug("Stopping context")
|
|
312
|
+
await self.context.close()
|
|
313
|
+
self.context = None
|
|
314
|
+
|
|
315
|
+
if self.browser is not None:
|
|
316
|
+
logger.debug("Stopping browser")
|
|
317
|
+
await self.browser.close()
|
|
318
|
+
self.browser = None
|
|
319
|
+
|
|
320
|
+
if self.playwright is not None:
|
|
321
|
+
logger.debug("Stopping playwright")
|
|
322
|
+
await self.playwright.stop()
|
|
323
|
+
self.playwright = None
|
|
324
|
+
shutil.rmtree(self.user_data_dir, ignore_errors=True)
|
|
325
|
+
else:
|
|
326
|
+
logger.debug("browser not stopped as dedicated")
|
|
192
327
|
|
|
193
328
|
async def get_current_page(self) -> Page | None:
|
|
194
329
|
if self.context is None:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
logging.basicConfig(level=logging.INFO)
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _download_extension(url: str, output_path: Path) -> None:
|
|
10
|
+
"""Download extension .crx file."""
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
logger.info(f"Downloading from: {url}")
|
|
15
|
+
with urllib.request.urlopen(url) as response:
|
|
16
|
+
content = response.read()
|
|
17
|
+
logger.info(f"Downloaded {len(content)} bytes")
|
|
18
|
+
with open(output_path, "wb") as f:
|
|
19
|
+
f.write(content)
|
|
20
|
+
logger.info(f"Saved to: {output_path}")
|
|
21
|
+
except Exception as e:
|
|
22
|
+
raise Exception(f"Failed to download extension: {e}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _extract_extension(crx_path: Path, extract_dir: Path) -> None:
|
|
26
|
+
"""Extract .crx file to directory."""
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import zipfile
|
|
30
|
+
|
|
31
|
+
# Remove existing directory
|
|
32
|
+
if extract_dir.exists():
|
|
33
|
+
shutil.rmtree(extract_dir)
|
|
34
|
+
|
|
35
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
# CRX files are ZIP files with a header, try to extract as ZIP
|
|
39
|
+
with zipfile.ZipFile(crx_path, "r") as zip_ref:
|
|
40
|
+
zip_ref.extractall(extract_dir)
|
|
41
|
+
|
|
42
|
+
# Verify manifest exists
|
|
43
|
+
if not (extract_dir / "manifest.json").exists():
|
|
44
|
+
raise Exception("No manifest.json found in extension")
|
|
45
|
+
|
|
46
|
+
logger.info("✅ Extracted as regular ZIP file")
|
|
47
|
+
|
|
48
|
+
except zipfile.BadZipFile:
|
|
49
|
+
logger.info("📦 Processing CRX header...")
|
|
50
|
+
# CRX files have a header before the ZIP data
|
|
51
|
+
with open(crx_path, "rb") as f:
|
|
52
|
+
# Read CRX header to find ZIP start
|
|
53
|
+
magic = f.read(4)
|
|
54
|
+
if magic != b"Cr24":
|
|
55
|
+
raise Exception(f"Invalid CRX file format. Magic: {magic}")
|
|
56
|
+
|
|
57
|
+
version = int.from_bytes(f.read(4), "little")
|
|
58
|
+
logger.info(f"CRX version: {version}")
|
|
59
|
+
|
|
60
|
+
if version == 2:
|
|
61
|
+
pubkey_len = int.from_bytes(f.read(4), "little")
|
|
62
|
+
sig_len = int.from_bytes(f.read(4), "little")
|
|
63
|
+
f.seek(16 + pubkey_len + sig_len)
|
|
64
|
+
elif version == 3:
|
|
65
|
+
header_len = int.from_bytes(f.read(4), "little")
|
|
66
|
+
f.seek(12 + header_len)
|
|
67
|
+
else:
|
|
68
|
+
raise Exception(f"Unsupported CRX version: {version}")
|
|
69
|
+
|
|
70
|
+
# Extract ZIP data
|
|
71
|
+
zip_data = f.read()
|
|
72
|
+
logger.info(f"ZIP data size: {len(zip_data)} bytes")
|
|
73
|
+
|
|
74
|
+
# Write ZIP data to temp file and extract
|
|
75
|
+
import tempfile
|
|
76
|
+
|
|
77
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
|
|
78
|
+
temp_zip.write(zip_data)
|
|
79
|
+
temp_zip.flush()
|
|
80
|
+
|
|
81
|
+
with zipfile.ZipFile(temp_zip.name, "r") as zip_ref:
|
|
82
|
+
zip_ref.extractall(extract_dir)
|
|
83
|
+
|
|
84
|
+
os.unlink(temp_zip.name)
|
|
85
|
+
|
|
86
|
+
# Remove 'key' from manifest if present (can cause issues)
|
|
87
|
+
manifest_path = extract_dir / "manifest.json"
|
|
88
|
+
if manifest_path.exists():
|
|
89
|
+
data = json.loads(manifest_path.read_text())
|
|
90
|
+
logger.info(f"Manifest version: {data.get('manifest_version')}")
|
|
91
|
+
logger.info(f"Extension name: {data.get('name')}")
|
|
92
|
+
|
|
93
|
+
if "key" in data:
|
|
94
|
+
logger.info("Removing 'key' field from manifest")
|
|
95
|
+
del data["key"]
|
|
96
|
+
manifest_path.write_text(json.dumps(data, indent=2))
|
|
97
|
+
else:
|
|
98
|
+
raise Exception("manifest.json not found after extraction")
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import List, Optional
|
|
6
|
+
from urllib.parse import urlparse
|
|
6
7
|
|
|
7
8
|
import aiofiles
|
|
8
9
|
import pyotp
|
|
@@ -74,3 +75,16 @@ async def get_onepassword_value(vault_name: str, item_name: str, field_name: str
|
|
|
74
75
|
)
|
|
75
76
|
|
|
76
77
|
return str_value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def clean_url(url: str) -> str:
|
|
81
|
+
if not url.startswith(("http://", "https://")):
|
|
82
|
+
url = "http://" + url # needed for urlparse
|
|
83
|
+
|
|
84
|
+
parsed = urlparse(url)
|
|
85
|
+
domain = parsed.netloc.lower()
|
|
86
|
+
|
|
87
|
+
if domain.startswith("www."):
|
|
88
|
+
domain = domain[4:]
|
|
89
|
+
|
|
90
|
+
return domain
|
|
@@ -61,6 +61,7 @@ optexity/inference/core/two_factor_auth/__init__.py
|
|
|
61
61
|
optexity/inference/infra/__init__.py
|
|
62
62
|
optexity/inference/infra/browser.py
|
|
63
63
|
optexity/inference/infra/browser_extension.py
|
|
64
|
+
optexity/inference/infra/utils.py
|
|
64
65
|
optexity/inference/models/__init__.py
|
|
65
66
|
optexity/inference/models/gemini.py
|
|
66
67
|
optexity/inference/models/human.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "optexity"
|
|
7
|
-
version = "0.1.5.
|
|
7
|
+
version = "0.1.5.3"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
description = "Optexity is a platform for building and running browser and computer agents."
|
|
10
10
|
authors = [{ name = "Optexity", email = "founders@optexity.com" }]
|
|
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
|
{optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/error_handler/error_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
{optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/index_prediction/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/select_value_prediction/__init__.py
RENAMED
|
File without changes
|
{optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/select_value_prediction/prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
{optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/agents/two_fa_extraction/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_agentic_task.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_keypress.py
RENAMED
|
File without changes
|
|
File without changes
|
{optexity-0.1.5.2 → optexity-0.1.5.3}/optexity/inference/core/interaction/handle_select_utils.py
RENAMED
|
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
|