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.
- 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.3.dist-info}/METADATA +1 -1
- optexity-0.1.3.dist-info/RECORD +80 -0
- optexity-0.1.2.dist-info/RECORD +0 -11
- {optexity-0.1.2.dist-info → optexity-0.1.3.dist-info}/WHEEL +0 -0
- {optexity-0.1.2.dist-info → optexity-0.1.3.dist-info}/entry_points.txt +0 -0
- {optexity-0.1.2.dist-info → optexity-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {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}")
|