optexity 0.1.2__py3-none-any.whl → 0.1.3__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.
Files changed (76) hide show
  1. optexity/examples/__init__.py +0 -0
  2. optexity/examples/add_example.py +88 -0
  3. optexity/examples/download_pdf_url.py +29 -0
  4. optexity/examples/extract_price_stockanalysis.py +44 -0
  5. optexity/examples/file_upload.py +59 -0
  6. optexity/examples/i94.py +126 -0
  7. optexity/examples/i94_travel_history.py +126 -0
  8. optexity/examples/peachstate_medicaid.py +201 -0
  9. optexity/examples/supabase_login.py +75 -0
  10. optexity/inference/__init__.py +0 -0
  11. optexity/inference/agents/__init__.py +0 -0
  12. optexity/inference/agents/error_handler/__init__.py +0 -0
  13. optexity/inference/agents/error_handler/error_handler.py +39 -0
  14. optexity/inference/agents/error_handler/prompt.py +60 -0
  15. optexity/inference/agents/index_prediction/__init__.py +0 -0
  16. optexity/inference/agents/index_prediction/action_prediction_locator_axtree.py +45 -0
  17. optexity/inference/agents/index_prediction/prompt.py +14 -0
  18. optexity/inference/agents/select_value_prediction/__init__.py +0 -0
  19. optexity/inference/agents/select_value_prediction/prompt.py +20 -0
  20. optexity/inference/agents/select_value_prediction/select_value_prediction.py +39 -0
  21. optexity/inference/agents/two_fa_extraction/__init__.py +0 -0
  22. optexity/inference/agents/two_fa_extraction/prompt.py +23 -0
  23. optexity/inference/agents/two_fa_extraction/two_fa_extraction.py +47 -0
  24. optexity/inference/child_process.py +251 -0
  25. optexity/inference/core/__init__.py +0 -0
  26. optexity/inference/core/interaction/__init__.py +0 -0
  27. optexity/inference/core/interaction/handle_agentic_task.py +79 -0
  28. optexity/inference/core/interaction/handle_check.py +57 -0
  29. optexity/inference/core/interaction/handle_click.py +79 -0
  30. optexity/inference/core/interaction/handle_command.py +261 -0
  31. optexity/inference/core/interaction/handle_input.py +76 -0
  32. optexity/inference/core/interaction/handle_keypress.py +16 -0
  33. optexity/inference/core/interaction/handle_select.py +109 -0
  34. optexity/inference/core/interaction/handle_select_utils.py +132 -0
  35. optexity/inference/core/interaction/handle_upload.py +59 -0
  36. optexity/inference/core/interaction/utils.py +81 -0
  37. optexity/inference/core/logging.py +406 -0
  38. optexity/inference/core/run_assertion.py +55 -0
  39. optexity/inference/core/run_automation.py +463 -0
  40. optexity/inference/core/run_extraction.py +240 -0
  41. optexity/inference/core/run_interaction.py +254 -0
  42. optexity/inference/core/run_python_script.py +20 -0
  43. optexity/inference/core/run_two_fa.py +120 -0
  44. optexity/inference/core/two_factor_auth/__init__.py +0 -0
  45. optexity/inference/infra/__init__.py +0 -0
  46. optexity/inference/infra/browser.py +455 -0
  47. optexity/inference/infra/browser_extension.py +20 -0
  48. optexity/inference/models/__init__.py +22 -0
  49. optexity/inference/models/gemini.py +113 -0
  50. optexity/inference/models/human.py +20 -0
  51. optexity/inference/models/llm_model.py +210 -0
  52. optexity/inference/run_local.py +200 -0
  53. optexity/schema/__init__.py +0 -0
  54. optexity/schema/actions/__init__.py +0 -0
  55. optexity/schema/actions/assertion_action.py +66 -0
  56. optexity/schema/actions/extraction_action.py +143 -0
  57. optexity/schema/actions/interaction_action.py +330 -0
  58. optexity/schema/actions/misc_action.py +18 -0
  59. optexity/schema/actions/prompts.py +27 -0
  60. optexity/schema/actions/two_fa_action.py +24 -0
  61. optexity/schema/automation.py +432 -0
  62. optexity/schema/callback.py +16 -0
  63. optexity/schema/inference.py +87 -0
  64. optexity/schema/memory.py +100 -0
  65. optexity/schema/task.py +212 -0
  66. optexity/schema/token_usage.py +48 -0
  67. optexity/utils/__init__.py +0 -0
  68. optexity/utils/settings.py +54 -0
  69. optexity/utils/utils.py +76 -0
  70. {optexity-0.1.2.dist-info → optexity-0.1.3.dist-info}/METADATA +1 -1
  71. optexity-0.1.3.dist-info/RECORD +80 -0
  72. optexity-0.1.2.dist-info/RECORD +0 -11
  73. {optexity-0.1.2.dist-info → optexity-0.1.3.dist-info}/WHEEL +0 -0
  74. {optexity-0.1.2.dist-info → optexity-0.1.3.dist-info}/entry_points.txt +0 -0
  75. {optexity-0.1.2.dist-info → optexity-0.1.3.dist-info}/licenses/LICENSE +0 -0
  76. {optexity-0.1.2.dist-info → optexity-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,132 @@
1
+ import logging
2
+ import re
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from optexity.inference.agents.select_value_prediction.select_value_prediction import (
7
+ SelectValuePredictionAgent,
8
+ )
9
+ from optexity.schema.actions.interaction_action import Locator
10
+ from optexity.schema.memory import Memory
11
+
12
+ logger = logging.getLogger(__name__)
13
+ select_value_prediction_agent = SelectValuePredictionAgent()
14
+
15
+
16
+ class SelectOptionValue(BaseModel):
17
+ value: str
18
+ label: str
19
+
20
+
21
+ def llm_select_match(
22
+ options: list[SelectOptionValue], patterns: list[str], memory: Memory
23
+ ) -> list[str]:
24
+ final_prompt, response, token_usage = (
25
+ select_value_prediction_agent.predict_select_value(
26
+ [o.model_dump() for o in options], patterns
27
+ )
28
+ )
29
+ memory.token_usage += token_usage
30
+ memory.browser_states[-1].final_prompt = final_prompt
31
+ memory.browser_states[-1].llm_response = response.model_dump()
32
+
33
+ matched_values = response.matched_values
34
+
35
+ all_values = [o.value for o in options]
36
+
37
+ final_matched_values = []
38
+ for value in matched_values:
39
+ if value in all_values:
40
+ final_matched_values.append(value)
41
+
42
+ return final_matched_values
43
+
44
+
45
+ def score_match(pat: str, val: str) -> int:
46
+ # higher is better
47
+ if pat == val:
48
+ return 100
49
+ if val.startswith(pat):
50
+ return 80
51
+ if pat in val:
52
+ return 60
53
+ return 0
54
+
55
+
56
+ async def smart_select(
57
+ options: list[SelectOptionValue], patterns: list[str], memory: Memory
58
+ ):
59
+ # Get all options from the <select>
60
+
61
+ matched_values = []
62
+
63
+ for p in patterns:
64
+ # If pattern contains regex characters, treat as regex
65
+ is_regex = p.startswith("^") or p.endswith("$") or ".*" in p
66
+
67
+ ## Check if reggex pattern and then try finding the option by value and label
68
+ if is_regex:
69
+ regex = re.compile(p)
70
+ for opt in options:
71
+ if regex.search(opt.value) or regex.search(opt.label):
72
+ matched_values.append(opt.value)
73
+ else:
74
+ # try exact match
75
+ for opt in options:
76
+ if opt.value == p or opt.label == p:
77
+ matched_values.append(opt.value)
78
+
79
+ if len(matched_values) == 0:
80
+ ## If no matches, check if all values are unique and try score matching of values
81
+
82
+ processed_values = [
83
+ (v.value.lower().replace(" ", ""), v.value) for v in options
84
+ ]
85
+
86
+ if len(processed_values) == len(set(processed_values)):
87
+ for p in patterns:
88
+ processed_pattern = p.lower().replace(" ", "")
89
+
90
+ best_score = 0
91
+ best_value = None
92
+
93
+ for processed_value, value in processed_values:
94
+ score = score_match(processed_pattern, processed_value)
95
+ if score > best_score:
96
+ best_score = score
97
+ best_value = value
98
+
99
+ if best_value is not None and best_score > 0:
100
+ matched_values.append(best_value)
101
+
102
+ if len(matched_values) == 0:
103
+ processed_labels = [
104
+ (v.label.lower().replace(" ", ""), v.label) for v in options
105
+ ]
106
+
107
+ if len(processed_labels) == len(set(processed_labels)):
108
+ for p in patterns:
109
+ processed_pattern = p.lower().replace(" ", "")
110
+
111
+ best_score = 0
112
+ best_label = None
113
+ best_value = None
114
+
115
+ for opt in options:
116
+ processed_label = opt.label.lower().replace(" ", "")
117
+ score = score_match(processed_pattern, processed_label)
118
+ if score > best_score:
119
+ best_score = score
120
+ best_label = opt.label
121
+ best_value = opt.value
122
+
123
+ if best_label is not None and best_score > 0:
124
+ matched_values.append(best_value)
125
+
126
+ if len(matched_values) == 0:
127
+ matched_values = llm_select_match(options, patterns, memory)
128
+
129
+ if len(matched_values) == 0:
130
+ matched_values = patterns
131
+
132
+ return matched_values
@@ -0,0 +1,59 @@
1
+ import logging
2
+
3
+ from optexity.inference.core.interaction.handle_command import (
4
+ command_based_action_with_retry,
5
+ )
6
+ from optexity.inference.core.interaction.utils import get_index_from_prompt
7
+ from optexity.inference.infra.browser import Browser
8
+ from optexity.schema.actions.interaction_action import UploadFileAction
9
+ from optexity.schema.memory import Memory
10
+ from optexity.schema.task import Task
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ async def handle_upload_file(
16
+ upload_file_action: UploadFileAction,
17
+ task: Task,
18
+ memory: Memory,
19
+ browser: Browser,
20
+ max_timeout_seconds_per_try: float,
21
+ max_tries: int,
22
+ ):
23
+ if upload_file_action.command and not upload_file_action.skip_command:
24
+ last_error = await command_based_action_with_retry(
25
+ upload_file_action,
26
+ browser,
27
+ memory,
28
+ task,
29
+ max_tries,
30
+ max_timeout_seconds_per_try,
31
+ )
32
+ if last_error is None:
33
+ return
34
+
35
+ if not upload_file_action.skip_prompt:
36
+ logger.debug(
37
+ f"Executing prompt-based action: {upload_file_action.__class__.__name__}"
38
+ )
39
+ await upload_file_index(upload_file_action, browser, memory)
40
+
41
+
42
+ async def upload_file_index(
43
+ upload_file_action: UploadFileAction, browser: Browser, memory: Memory
44
+ ):
45
+
46
+ try:
47
+ index = await get_index_from_prompt(
48
+ memory, upload_file_action.prompt_instructions, browser
49
+ )
50
+ if index is None:
51
+ return
52
+
53
+ action_model = browser.backend_agent.ActionModel(
54
+ **{"upload_file": {"index": index, "path": upload_file_action.file_path}}
55
+ )
56
+ await browser.backend_agent.multi_act([action_model])
57
+ except Exception as e:
58
+ logger.error(f"Error in upload_file_index: {e}")
59
+ return
@@ -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}")