optexity 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
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/cli.py +1 -1
- optexity/examples/__init__.py +0 -0
- optexity/examples/add_example.py +88 -0
- optexity/examples/download_pdf_url.py +29 -0
- optexity/examples/extract_price_stockanalysis.py +44 -0
- optexity/examples/file_upload.py +59 -0
- optexity/examples/i94.py +126 -0
- optexity/examples/i94_travel_history.py +126 -0
- optexity/examples/peachstate_medicaid.py +201 -0
- optexity/examples/supabase_login.py +75 -0
- optexity/inference/__init__.py +0 -0
- optexity/inference/agents/__init__.py +0 -0
- optexity/inference/agents/error_handler/__init__.py +0 -0
- optexity/inference/agents/error_handler/error_handler.py +39 -0
- optexity/inference/agents/error_handler/prompt.py +60 -0
- optexity/inference/agents/index_prediction/__init__.py +0 -0
- optexity/inference/agents/index_prediction/action_prediction_locator_axtree.py +45 -0
- optexity/inference/agents/index_prediction/prompt.py +14 -0
- optexity/inference/agents/select_value_prediction/__init__.py +0 -0
- optexity/inference/agents/select_value_prediction/prompt.py +20 -0
- optexity/inference/agents/select_value_prediction/select_value_prediction.py +39 -0
- optexity/inference/agents/two_fa_extraction/__init__.py +0 -0
- optexity/inference/agents/two_fa_extraction/prompt.py +23 -0
- optexity/inference/agents/two_fa_extraction/two_fa_extraction.py +47 -0
- optexity/inference/child_process.py +251 -0
- optexity/inference/core/__init__.py +0 -0
- optexity/inference/core/interaction/__init__.py +0 -0
- optexity/inference/core/interaction/handle_agentic_task.py +79 -0
- optexity/inference/core/interaction/handle_check.py +57 -0
- optexity/inference/core/interaction/handle_click.py +79 -0
- optexity/inference/core/interaction/handle_command.py +261 -0
- optexity/inference/core/interaction/handle_input.py +76 -0
- optexity/inference/core/interaction/handle_keypress.py +16 -0
- optexity/inference/core/interaction/handle_select.py +109 -0
- optexity/inference/core/interaction/handle_select_utils.py +132 -0
- optexity/inference/core/interaction/handle_upload.py +59 -0
- optexity/inference/core/interaction/utils.py +81 -0
- optexity/inference/core/logging.py +406 -0
- optexity/inference/core/run_assertion.py +55 -0
- optexity/inference/core/run_automation.py +463 -0
- optexity/inference/core/run_extraction.py +240 -0
- optexity/inference/core/run_interaction.py +254 -0
- optexity/inference/core/run_python_script.py +20 -0
- optexity/inference/core/run_two_fa.py +120 -0
- optexity/inference/core/two_factor_auth/__init__.py +0 -0
- optexity/inference/infra/__init__.py +0 -0
- optexity/inference/infra/browser.py +455 -0
- optexity/inference/infra/browser_extension.py +20 -0
- optexity/inference/models/__init__.py +22 -0
- optexity/inference/models/gemini.py +113 -0
- optexity/inference/models/human.py +20 -0
- optexity/inference/models/llm_model.py +210 -0
- optexity/inference/run_local.py +200 -0
- optexity/schema/__init__.py +0 -0
- optexity/schema/actions/__init__.py +0 -0
- optexity/schema/actions/assertion_action.py +66 -0
- optexity/schema/actions/extraction_action.py +143 -0
- optexity/schema/actions/interaction_action.py +330 -0
- optexity/schema/actions/misc_action.py +18 -0
- optexity/schema/actions/prompts.py +27 -0
- optexity/schema/actions/two_fa_action.py +24 -0
- optexity/schema/automation.py +432 -0
- optexity/schema/callback.py +16 -0
- optexity/schema/inference.py +87 -0
- optexity/schema/memory.py +100 -0
- optexity/schema/task.py +212 -0
- optexity/schema/token_usage.py +48 -0
- optexity/utils/__init__.py +0 -0
- optexity/utils/settings.py +54 -0
- optexity/utils/utils.py +76 -0
- {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/METADATA +20 -36
- optexity-0.1.4.dist-info/RECORD +80 -0
- optexity-0.1.2.dist-info/RECORD +0 -11
- {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/WHEEL +0 -0
- {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/entry_points.txt +0 -0
- {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
import aiofiles
|
|
6
|
+
|
|
7
|
+
from optexity.inference.agents.index_prediction.action_prediction_locator_axtree import (
|
|
8
|
+
ActionPredictionLocatorAxtree,
|
|
9
|
+
)
|
|
10
|
+
from optexity.inference.infra.browser import Browser
|
|
11
|
+
from optexity.schema.memory import BrowserState, Memory
|
|
12
|
+
from optexity.schema.task import Task
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
index_prediction_agent = ActionPredictionLocatorAxtree()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def get_index_from_prompt(
|
|
21
|
+
memory: Memory, prompt_instructions: str, browser: Browser
|
|
22
|
+
):
|
|
23
|
+
browser_state_summary = await browser.get_browser_state_summary()
|
|
24
|
+
memory.browser_states[-1] = BrowserState(
|
|
25
|
+
url=browser_state_summary.url,
|
|
26
|
+
screenshot=browser_state_summary.screenshot,
|
|
27
|
+
title=browser_state_summary.title,
|
|
28
|
+
axtree=browser_state_summary.dom_state.llm_representation(),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
final_prompt, response, token_usage = index_prediction_agent.predict_action(
|
|
33
|
+
prompt_instructions, memory.browser_states[-1].axtree
|
|
34
|
+
)
|
|
35
|
+
memory.token_usage += token_usage
|
|
36
|
+
memory.browser_states[-1].final_prompt = final_prompt
|
|
37
|
+
memory.browser_states[-1].llm_response = response.model_dump()
|
|
38
|
+
|
|
39
|
+
return response.index
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Error in get_index_from_prompt: {e}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def handle_download(
|
|
45
|
+
func: Callable, memory: Memory, browser: Browser, task: Task, download_filename: str
|
|
46
|
+
):
|
|
47
|
+
page = await browser.get_current_page()
|
|
48
|
+
if page is None:
|
|
49
|
+
logger.error("No page found for current page")
|
|
50
|
+
return
|
|
51
|
+
download_path: Path = task.downloads_directory / download_filename
|
|
52
|
+
async with page.expect_download() as download_info:
|
|
53
|
+
await func()
|
|
54
|
+
download = await download_info.value
|
|
55
|
+
|
|
56
|
+
if download:
|
|
57
|
+
temp_path = await download.path()
|
|
58
|
+
async with memory.download_lock:
|
|
59
|
+
memory.raw_downloads[temp_path] = (True, None)
|
|
60
|
+
|
|
61
|
+
await download.save_as(download_path)
|
|
62
|
+
memory.downloads.append(download_path)
|
|
63
|
+
await clean_download(download_path)
|
|
64
|
+
else:
|
|
65
|
+
logger.error("No download found")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def clean_download(download_path: Path):
|
|
69
|
+
|
|
70
|
+
if download_path.suffix == ".csv":
|
|
71
|
+
# Read full file
|
|
72
|
+
async with aiofiles.open(download_path, "r", encoding="utf-8") as f:
|
|
73
|
+
content = await f.read()
|
|
74
|
+
# Remove everything between <script>...</script> (multiline safe)
|
|
75
|
+
|
|
76
|
+
if "</script>" in content:
|
|
77
|
+
clean_content = content.split("</script>")[-1]
|
|
78
|
+
|
|
79
|
+
# Write cleaned CSV back
|
|
80
|
+
async with aiofiles.open(download_path, "w", encoding="utf-8") as f:
|
|
81
|
+
await f.write(clean_content)
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import tarfile
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import urljoin
|
|
10
|
+
|
|
11
|
+
import aiofiles
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from optexity.schema.automation import ActionNode
|
|
15
|
+
from optexity.schema.memory import Memory
|
|
16
|
+
from optexity.schema.task import Task
|
|
17
|
+
from optexity.schema.token_usage import TokenUsage
|
|
18
|
+
from optexity.utils.settings import settings
|
|
19
|
+
from optexity.utils.utils import save_screenshot
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_tar_in_memory(directory: Path | str, name: str) -> io.BytesIO:
|
|
25
|
+
if isinstance(directory, str):
|
|
26
|
+
directory = Path(directory)
|
|
27
|
+
tar_bytes = io.BytesIO()
|
|
28
|
+
with tarfile.open(fileobj=tar_bytes, mode="w:gz") as tar:
|
|
29
|
+
tar.add(directory, arcname=name)
|
|
30
|
+
tar_bytes.seek(0) # rewind to start
|
|
31
|
+
return tar_bytes
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def start_task_in_server(task: Task):
|
|
35
|
+
try:
|
|
36
|
+
task.started_at = datetime.now(timezone.utc)
|
|
37
|
+
task.status = "running"
|
|
38
|
+
|
|
39
|
+
url = urljoin(settings.SERVER_URL, settings.START_TASK_ENDPOINT)
|
|
40
|
+
headers = {"x-api-key": task.api_key}
|
|
41
|
+
body = {
|
|
42
|
+
"task_id": task.task_id,
|
|
43
|
+
"started_at": task.started_at.isoformat(),
|
|
44
|
+
}
|
|
45
|
+
if task.allocated_at:
|
|
46
|
+
body["allocated_at"] = task.allocated_at.isoformat()
|
|
47
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
48
|
+
response = await client.post(
|
|
49
|
+
url,
|
|
50
|
+
headers=headers,
|
|
51
|
+
json=body,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
return response.json()
|
|
56
|
+
except httpx.HTTPStatusError as e:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Failed to start task in server: {e.response.status_code} - {e.response.text}"
|
|
59
|
+
)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise ValueError(f"Failed to start task in server: {e}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def complete_task_in_server(
|
|
65
|
+
task: Task, token_usage: TokenUsage, child_process_id: int
|
|
66
|
+
):
|
|
67
|
+
try:
|
|
68
|
+
task.completed_at = datetime.now(timezone.utc)
|
|
69
|
+
|
|
70
|
+
url = urljoin(settings.SERVER_URL, settings.COMPLETE_TASK_ENDPOINT)
|
|
71
|
+
headers = {"x-api-key": task.api_key}
|
|
72
|
+
body = {
|
|
73
|
+
"task_id": task.task_id,
|
|
74
|
+
"child_process_id": child_process_id,
|
|
75
|
+
"completed_at": task.completed_at.isoformat(),
|
|
76
|
+
"status": task.status,
|
|
77
|
+
"error": task.error,
|
|
78
|
+
"token_usage": token_usage.model_dump(),
|
|
79
|
+
}
|
|
80
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
81
|
+
response = await client.post(
|
|
82
|
+
url,
|
|
83
|
+
headers=headers,
|
|
84
|
+
json=body,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
return response.json()
|
|
89
|
+
except httpx.HTTPStatusError as e:
|
|
90
|
+
logger.error(
|
|
91
|
+
f"Failed to complete task in server: {e.response.status_code} - {e.response.text}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Failed to complete task in server: {e}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def save_output_data_in_server(task: Task, memory: Memory):
|
|
99
|
+
try:
|
|
100
|
+
if len(memory.variables.output_data) == 0 and memory.final_screenshot is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
url = urljoin(settings.SERVER_URL, settings.SAVE_OUTPUT_DATA_ENDPOINT)
|
|
104
|
+
headers = {"x-api-key": task.api_key}
|
|
105
|
+
|
|
106
|
+
output_data = [
|
|
107
|
+
output_data.model_dump(exclude_none=True, exclude={"screenshot"})
|
|
108
|
+
for output_data in memory.variables.output_data
|
|
109
|
+
]
|
|
110
|
+
output_data = [data for data in output_data if data and len(data.keys()) > 0]
|
|
111
|
+
body = {
|
|
112
|
+
"task_id": task.task_id,
|
|
113
|
+
"output_data": output_data,
|
|
114
|
+
"final_screenshot": memory.final_screenshot,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for_loop_status = []
|
|
118
|
+
for loop_status in memory.variables.for_loop_status:
|
|
119
|
+
loop_status = [item.model_dump(exclude_none=True) for item in loop_status]
|
|
120
|
+
for_loop_status.append(loop_status)
|
|
121
|
+
|
|
122
|
+
if len(for_loop_status) > 0:
|
|
123
|
+
body["for_loop_status"] = for_loop_status
|
|
124
|
+
|
|
125
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
126
|
+
response = await client.post(
|
|
127
|
+
url,
|
|
128
|
+
headers=headers,
|
|
129
|
+
json=body,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
response.raise_for_status()
|
|
133
|
+
return response.json()
|
|
134
|
+
except httpx.HTTPStatusError as e:
|
|
135
|
+
logger.error(
|
|
136
|
+
f"Failed to save output data in server: {e.response.status_code} - {e.response.text}"
|
|
137
|
+
)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error(f"Failed to save output data in server: {e}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def save_downloads_in_server(task: Task, memory: Memory):
|
|
143
|
+
try:
|
|
144
|
+
# if len(memory.downloads) == 0:
|
|
145
|
+
# return
|
|
146
|
+
|
|
147
|
+
url = urljoin(settings.SERVER_URL, settings.SAVE_DOWNLOADS_ENDPOINT)
|
|
148
|
+
headers = {"x-api-key": task.api_key}
|
|
149
|
+
|
|
150
|
+
payload = {
|
|
151
|
+
"task_id": task.task_id, # form field
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
files = []
|
|
155
|
+
downloads = [
|
|
156
|
+
download
|
|
157
|
+
for download in task.downloads_directory.iterdir()
|
|
158
|
+
if download.is_file()
|
|
159
|
+
]
|
|
160
|
+
if len(downloads) > 0:
|
|
161
|
+
tar_bytes = create_tar_in_memory(task.downloads_directory, task.task_id)
|
|
162
|
+
# add tar.gz
|
|
163
|
+
files.append(
|
|
164
|
+
(
|
|
165
|
+
"compressed_downloads",
|
|
166
|
+
(f"{task.task_id}.tar.gz", tar_bytes, "application/gzip"),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# add screenshots
|
|
171
|
+
for data in memory.variables.output_data:
|
|
172
|
+
if data.screenshot:
|
|
173
|
+
files.append(
|
|
174
|
+
(
|
|
175
|
+
"screenshots",
|
|
176
|
+
(
|
|
177
|
+
data.screenshot.filename,
|
|
178
|
+
base64.b64decode(data.screenshot.base64),
|
|
179
|
+
"image/png",
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if memory.final_screenshot:
|
|
185
|
+
files.append(
|
|
186
|
+
(
|
|
187
|
+
"screenshots",
|
|
188
|
+
(
|
|
189
|
+
"final_screenshot.png",
|
|
190
|
+
base64.b64decode(memory.final_screenshot),
|
|
191
|
+
"image/png",
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if len(files) == 0:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
200
|
+
|
|
201
|
+
response = await client.post(
|
|
202
|
+
url, headers=headers, data=payload, files=files
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
response.raise_for_status()
|
|
206
|
+
return response.json()
|
|
207
|
+
except httpx.HTTPStatusError as e:
|
|
208
|
+
logger.error(
|
|
209
|
+
f"Failed to save downloads in server: {e.response.status_code} - {e.response.text}"
|
|
210
|
+
)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"Failed to save downloads in server: {e}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def save_trajectory_in_server(task: Task, memory: Memory):
|
|
216
|
+
try:
|
|
217
|
+
url = urljoin(settings.SERVER_URL, settings.SAVE_TRAJECTORY_ENDPOINT)
|
|
218
|
+
headers = {"x-api-key": task.api_key}
|
|
219
|
+
|
|
220
|
+
data = {
|
|
221
|
+
"task_id": task.task_id, # form field
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
tar_bytes = create_tar_in_memory(task.task_directory, task.task_id)
|
|
225
|
+
files = {
|
|
226
|
+
"compressed_trajectory": (
|
|
227
|
+
f"{task.task_id}.tar.gz",
|
|
228
|
+
tar_bytes,
|
|
229
|
+
"application/gzip",
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
233
|
+
|
|
234
|
+
response = await client.post(url, headers=headers, data=data, files=files)
|
|
235
|
+
|
|
236
|
+
response.raise_for_status()
|
|
237
|
+
return response.json()
|
|
238
|
+
except httpx.HTTPStatusError as e:
|
|
239
|
+
logger.error(
|
|
240
|
+
f"Failed to save trajectory in server: {e.response.status_code} - {e.response.text}"
|
|
241
|
+
)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.error(f"Failed to save trajectory in server: {e}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def initiate_callback(task: Task):
|
|
247
|
+
|
|
248
|
+
if settings.DEPLOYMENT == "dev" and settings.LOCAL_CALLBACK_URL is not None:
|
|
249
|
+
logger.info("initiating local callback")
|
|
250
|
+
callback_data = None
|
|
251
|
+
try:
|
|
252
|
+
url = urljoin(settings.SERVER_URL, settings.GET_CALLBACK_DATA_ENDPOINT)
|
|
253
|
+
headers = {"x-api-key": task.api_key}
|
|
254
|
+
data = {
|
|
255
|
+
"task_id": task.task_id,
|
|
256
|
+
"endpoint_name": task.endpoint_name,
|
|
257
|
+
}
|
|
258
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
259
|
+
response = await client.post(url, headers=headers, json=data)
|
|
260
|
+
response.raise_for_status()
|
|
261
|
+
callback_data = response.json()["data"]
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.error(f"Failed to get callback data: {e}")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
if callback_data is None:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
271
|
+
response = await client.post(
|
|
272
|
+
settings.LOCAL_CALLBACK_URL, json=callback_data
|
|
273
|
+
)
|
|
274
|
+
response.raise_for_status()
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.error(f"Failed to initiate local callback: {e}")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
logger.info("initiating callback")
|
|
283
|
+
if task.callback_url is None:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
url = urljoin(settings.SERVER_URL, settings.INITIATE_CALLBACK_ENDPOINT)
|
|
287
|
+
headers = {"x-api-key": task.api_key}
|
|
288
|
+
|
|
289
|
+
data = {
|
|
290
|
+
"task_id": task.task_id,
|
|
291
|
+
"endpoint_name": task.endpoint_name,
|
|
292
|
+
"callback_url": task.callback_url.model_dump(),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
296
|
+
|
|
297
|
+
response = await client.post(url, headers=headers, json=data)
|
|
298
|
+
|
|
299
|
+
response.raise_for_status()
|
|
300
|
+
return response.json()
|
|
301
|
+
except httpx.HTTPStatusError as e:
|
|
302
|
+
logger.error(
|
|
303
|
+
f"Failed to save trajectory in server: {e.response.status_code} - {e.response.text}"
|
|
304
|
+
)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"Failed to save trajectory in server: {e}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def save_latest_memory_state_locally(
|
|
310
|
+
task: Task, memory: Memory, node: ActionNode | None
|
|
311
|
+
):
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
browser_state = memory.browser_states[-1]
|
|
315
|
+
automation_state = memory.automation_state
|
|
316
|
+
step_directory = (
|
|
317
|
+
task.logs_directory / f"step_{str(automation_state.step_index)}"
|
|
318
|
+
)
|
|
319
|
+
step_directory.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
|
|
321
|
+
if browser_state.screenshot:
|
|
322
|
+
save_screenshot(browser_state.screenshot, step_directory / "screenshot.png")
|
|
323
|
+
else:
|
|
324
|
+
logger.warning(
|
|
325
|
+
"No screenshot found for step %s", automation_state.step_index
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
state_dict = {
|
|
329
|
+
"title": browser_state.title,
|
|
330
|
+
"url": browser_state.url,
|
|
331
|
+
"step_index": automation_state.step_index,
|
|
332
|
+
"try_index": automation_state.try_index,
|
|
333
|
+
"downloaded_files": [
|
|
334
|
+
downloaded_file.name for downloaded_file in memory.downloads
|
|
335
|
+
],
|
|
336
|
+
"token_usage": memory.token_usage.model_dump(),
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async with aiofiles.open(step_directory / "state.json", "w") as f:
|
|
340
|
+
await f.write(json.dumps(state_dict, indent=4))
|
|
341
|
+
|
|
342
|
+
if browser_state.axtree:
|
|
343
|
+
async with aiofiles.open(step_directory / "axtree.txt", "w") as f:
|
|
344
|
+
await f.write(browser_state.axtree)
|
|
345
|
+
|
|
346
|
+
if browser_state.final_prompt:
|
|
347
|
+
async with aiofiles.open(step_directory / "final_prompt.txt", "w") as f:
|
|
348
|
+
await f.write(browser_state.final_prompt)
|
|
349
|
+
|
|
350
|
+
if browser_state.llm_response:
|
|
351
|
+
async with aiofiles.open(step_directory / "llm_response.json", "w") as f:
|
|
352
|
+
await f.write(json.dumps(browser_state.llm_response, indent=4))
|
|
353
|
+
|
|
354
|
+
if node:
|
|
355
|
+
async with aiofiles.open(step_directory / "action_node.json", "w") as f:
|
|
356
|
+
await f.write(
|
|
357
|
+
json.dumps(
|
|
358
|
+
node.model_dump(exclude_none=True, exclude_defaults=True),
|
|
359
|
+
indent=4,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
async with aiofiles.open(step_directory / "input_parameters.json", "w") as f:
|
|
364
|
+
await f.write(json.dumps(task.input_parameters, indent=4))
|
|
365
|
+
|
|
366
|
+
async with aiofiles.open(step_directory / "secure_parameters.json", "w") as f:
|
|
367
|
+
await f.write(json.dumps(task.secure_parameters, indent=4))
|
|
368
|
+
|
|
369
|
+
async with aiofiles.open(step_directory / "generated_variables.json", "w") as f:
|
|
370
|
+
await f.write(json.dumps(memory.variables.generated_variables, indent=4))
|
|
371
|
+
|
|
372
|
+
async with aiofiles.open(step_directory / "output_data.json", "w") as f:
|
|
373
|
+
await f.write(
|
|
374
|
+
json.dumps(
|
|
375
|
+
[
|
|
376
|
+
output_data.model_dump(
|
|
377
|
+
exclude_none=True,
|
|
378
|
+
exclude={"screenshot"},
|
|
379
|
+
exclude_defaults=True,
|
|
380
|
+
)
|
|
381
|
+
for output_data in memory.variables.output_data
|
|
382
|
+
],
|
|
383
|
+
indent=4,
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for output_data in memory.variables.output_data:
|
|
388
|
+
if output_data.screenshot:
|
|
389
|
+
async with aiofiles.open(
|
|
390
|
+
step_directory
|
|
391
|
+
/ f"screenshot_{output_data.screenshot.filename}.png",
|
|
392
|
+
"wb",
|
|
393
|
+
) as f:
|
|
394
|
+
await f.write(base64.b64decode(output_data.screenshot.base64))
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.error(f"Failed to save latest memory state locally: {e}")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
async def delete_local_data(task: Task):
|
|
400
|
+
try:
|
|
401
|
+
if settings.DEPLOYMENT == "dev" or task.task_directory is None:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
shutil.rmtree(task.task_directory, ignore_errors=True)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logger.error(f"Failed to delete local data: {e}")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
|
|
4
|
+
from optexity.inference.core.run_extraction import handle_llm_extraction
|
|
5
|
+
from optexity.inference.infra.browser import Browser
|
|
6
|
+
from optexity.inference.models import GeminiModels, get_llm_model
|
|
7
|
+
from optexity.schema.actions.assertion_action import AssertionAction, LLMAssertion
|
|
8
|
+
from optexity.schema.memory import Memory
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
llm_model = get_llm_model(GeminiModels.GEMINI_2_5_FLASH, True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def run_assertion_action(
|
|
16
|
+
assertion_action: AssertionAction, memory: Memory, browser: Browser
|
|
17
|
+
):
|
|
18
|
+
logger.debug(
|
|
19
|
+
f"---------Running assertion action {assertion_action.model_dump_json()}---------"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if assertion_action.llm:
|
|
23
|
+
await handle_llm_assertion(assertion_action.llm, memory, browser)
|
|
24
|
+
elif assertion_action.network_call:
|
|
25
|
+
raise ValueError("Network call assertions are not supported yet")
|
|
26
|
+
# await handle_network_call_assertion(
|
|
27
|
+
# assertion_action.network_call, memory, browser
|
|
28
|
+
# )
|
|
29
|
+
elif assertion_action.python_script:
|
|
30
|
+
raise ValueError("Python script assertions are not supported yet")
|
|
31
|
+
# await handle_python_script_assertion(
|
|
32
|
+
# assertion_action.python_script, memory, browser
|
|
33
|
+
# )
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def handle_llm_assertion(
|
|
37
|
+
llm_assertion: LLMAssertion, memory: Memory, browser: Browser
|
|
38
|
+
):
|
|
39
|
+
extra_instruction = """You are a helpful assistant that verifies if the condition is met.
|
|
40
|
+
Use the info supplied below to verify the condition.
|
|
41
|
+
The assertion_reason should be a short explanation of why the condition was met or not met.
|
|
42
|
+
The assertion_result should be True if the condition is met, False otherwise.
|
|
43
|
+
"""
|
|
44
|
+
llm_assertion_new = deepcopy(llm_assertion)
|
|
45
|
+
llm_assertion_new.extraction_instructions = (
|
|
46
|
+
extra_instruction + "\n" + llm_assertion_new.extraction_instructions
|
|
47
|
+
)
|
|
48
|
+
output_data = await handle_llm_extraction(llm_assertion_new, memory, browser)
|
|
49
|
+
|
|
50
|
+
if output_data.json_data["assertion_result"]:
|
|
51
|
+
return True
|
|
52
|
+
else:
|
|
53
|
+
raise AssertionError(
|
|
54
|
+
f"Assertion failed on node {memory.automation_state.step_index}: {output_data.json_data['assertion_reason']}"
|
|
55
|
+
)
|