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,432 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Annotated, Any, ForwardRef, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, model_validator
|
|
5
|
+
|
|
6
|
+
from optexity.schema.actions.assertion_action import AssertionAction
|
|
7
|
+
from optexity.schema.actions.extraction_action import ExtractionAction
|
|
8
|
+
from optexity.schema.actions.interaction_action import InteractionAction
|
|
9
|
+
from optexity.schema.actions.misc_action import PythonScriptAction
|
|
10
|
+
from optexity.schema.actions.two_fa_action import TwoFAAction
|
|
11
|
+
from optexity.utils.utils import get_onepassword_value, get_totp_code
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
IfElseNodeRef = ForwardRef("IfElseNode")
|
|
16
|
+
ForLoopNodeRef = ForwardRef("ForLoopNode")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OnePasswordParameter(BaseModel):
|
|
20
|
+
vault_name: str
|
|
21
|
+
item_name: str
|
|
22
|
+
field_name: str
|
|
23
|
+
type: Literal["raw", "totp_secret"] = "raw"
|
|
24
|
+
digits: int | None = None
|
|
25
|
+
|
|
26
|
+
@model_validator(mode="after")
|
|
27
|
+
def validate_onepassword_parameter(self):
|
|
28
|
+
if self.type == "totp_secret":
|
|
29
|
+
assert self.digits is not None, "digits must be provided for totp_secret"
|
|
30
|
+
else:
|
|
31
|
+
assert self.digits is None, "digits must not be provided for raw"
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AmazonSecretsManagerParameter(BaseModel):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@model_validator(mode="after")
|
|
39
|
+
def validate_amazon_secrets_manager_parameter(
|
|
40
|
+
cls, model: "AmazonSecretsManagerParameter"
|
|
41
|
+
):
|
|
42
|
+
raise NotImplementedError("Amazon Secrets Manager is not implemented yet")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TOTPParameter(BaseModel):
|
|
46
|
+
totp_secret: str
|
|
47
|
+
digits: int = 6
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SecureParameter(BaseModel):
|
|
51
|
+
onepassword: OnePasswordParameter | None = None
|
|
52
|
+
amazon_secrets_manager: AmazonSecretsManagerParameter | None = None
|
|
53
|
+
totp: TOTPParameter | None = None
|
|
54
|
+
|
|
55
|
+
@model_validator(mode="after")
|
|
56
|
+
def validate_secure_parameter(self):
|
|
57
|
+
non_null = [k for k, v in self.model_dump().items() if v is not None]
|
|
58
|
+
if len(non_null) != 1:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"Exactly one of onepassword or amazon_secrets_manager or totp must be provided"
|
|
61
|
+
)
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ActionNode(BaseModel):
|
|
66
|
+
type: Literal["action_node"]
|
|
67
|
+
interaction_action: InteractionAction | None = None
|
|
68
|
+
assertion_action: AssertionAction | None = None
|
|
69
|
+
extraction_action: ExtractionAction | None = None
|
|
70
|
+
python_script_action: PythonScriptAction | None = None
|
|
71
|
+
two_fa_action: TwoFAAction | None = None
|
|
72
|
+
before_sleep_time: float = 0.0
|
|
73
|
+
end_sleep_time: float = 5.0
|
|
74
|
+
expect_new_tab: bool = False
|
|
75
|
+
max_new_tab_wait_time: float = 0.0
|
|
76
|
+
localized_axtree_string: str | None = None
|
|
77
|
+
|
|
78
|
+
@model_validator(mode="after")
|
|
79
|
+
def validate_one_node(cls, model: "ActionNode"):
|
|
80
|
+
"""Ensure exactly one of the node types is set and matches the type."""
|
|
81
|
+
provided = {
|
|
82
|
+
"interaction_action": model.interaction_action,
|
|
83
|
+
"assertion_action": model.assertion_action,
|
|
84
|
+
"extraction_action": model.extraction_action,
|
|
85
|
+
"python_script_action": model.python_script_action,
|
|
86
|
+
"two_fa_action": model.two_fa_action,
|
|
87
|
+
}
|
|
88
|
+
non_null = [k for k, v in provided.items() if v is not None]
|
|
89
|
+
|
|
90
|
+
if len(non_null) != 1:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"Exactly one of interaction_action, assertion_action, extraction_action, python_script_action, or two_fa_action must be provided"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert (
|
|
96
|
+
model.end_sleep_time >= 0 and model.end_sleep_time <= 30
|
|
97
|
+
), "end_sleep_time must be greater than 0 and less than 30"
|
|
98
|
+
assert (
|
|
99
|
+
model.max_new_tab_wait_time >= 0 and model.max_new_tab_wait_time <= 30
|
|
100
|
+
), "max_new_tab_wait_time must be greater than 0 and less than 30"
|
|
101
|
+
|
|
102
|
+
# --- Adjust defaults only if user didn't override them ---
|
|
103
|
+
# We detect user-provided fields using model.__pydantic_fields_set__
|
|
104
|
+
user_set = model.__pydantic_fields_set__
|
|
105
|
+
|
|
106
|
+
if "end_sleep_time" not in user_set:
|
|
107
|
+
if model.assertion_action or model.extraction_action or model.two_fa_action:
|
|
108
|
+
model.end_sleep_time = 0.0
|
|
109
|
+
|
|
110
|
+
if "before_sleep_time" not in user_set:
|
|
111
|
+
model.before_sleep_time = 3.0 if model.extraction_action else 0.0
|
|
112
|
+
|
|
113
|
+
if model.expect_new_tab:
|
|
114
|
+
assert (
|
|
115
|
+
model.interaction_action is not None
|
|
116
|
+
), "expect_new_tab is only allowed for interaction actions"
|
|
117
|
+
model.max_new_tab_wait_time = 10.0
|
|
118
|
+
else:
|
|
119
|
+
model.max_new_tab_wait_time = 0.0
|
|
120
|
+
|
|
121
|
+
return model
|
|
122
|
+
|
|
123
|
+
def replace(self, pattern: str, replacement: str | int | float | bool | None):
|
|
124
|
+
replacement = str(replacement)
|
|
125
|
+
if self.interaction_action:
|
|
126
|
+
self.interaction_action.replace(pattern, replacement)
|
|
127
|
+
if self.assertion_action:
|
|
128
|
+
self.assertion_action.replace(pattern, replacement)
|
|
129
|
+
if self.extraction_action:
|
|
130
|
+
self.extraction_action.replace(pattern, replacement)
|
|
131
|
+
if self.python_script_action:
|
|
132
|
+
pass
|
|
133
|
+
if self.two_fa_action:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
async def replace_variables(
|
|
139
|
+
self, variables: dict[str, list[str | SecureParameter]]
|
|
140
|
+
):
|
|
141
|
+
for key, values in variables.items():
|
|
142
|
+
|
|
143
|
+
for index, value in enumerate(values):
|
|
144
|
+
pattern = f"{{{key}[{index}]}}"
|
|
145
|
+
|
|
146
|
+
if isinstance(value, SecureParameter):
|
|
147
|
+
if value.onepassword:
|
|
148
|
+
str_value = await get_onepassword_value(
|
|
149
|
+
value.onepassword.vault_name,
|
|
150
|
+
value.onepassword.item_name,
|
|
151
|
+
value.onepassword.field_name,
|
|
152
|
+
)
|
|
153
|
+
if value.onepassword.type == "totp_secret":
|
|
154
|
+
str_value = get_totp_code(
|
|
155
|
+
str_value, value.onepassword.digits
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
elif value.amazon_secrets_manager:
|
|
159
|
+
raise NotImplementedError(
|
|
160
|
+
"Amazon Secrets Manager is not implemented yet"
|
|
161
|
+
)
|
|
162
|
+
elif value.totp:
|
|
163
|
+
str_value = get_totp_code(
|
|
164
|
+
value.totp.totp_secret, value.totp.digits
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
elif (
|
|
168
|
+
isinstance(value, str)
|
|
169
|
+
or isinstance(value, int)
|
|
170
|
+
or isinstance(value, float)
|
|
171
|
+
or isinstance(value, bool)
|
|
172
|
+
):
|
|
173
|
+
str_value = str(value)
|
|
174
|
+
else:
|
|
175
|
+
raise ValueError(f"Invalid value type for {key}: {type(value)}")
|
|
176
|
+
|
|
177
|
+
self.replace(pattern, str_value)
|
|
178
|
+
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ForLoopNode(BaseModel):
|
|
183
|
+
# Loops through range of values of {variable_name[index]}
|
|
184
|
+
type: Literal["for_loop_node"]
|
|
185
|
+
variable_name: str
|
|
186
|
+
nodes: list[Annotated[ActionNode | IfElseNodeRef, Field(discriminator="type")]]
|
|
187
|
+
reset_nodes: list[
|
|
188
|
+
Annotated[ActionNode | IfElseNodeRef, Field(discriminator="type")]
|
|
189
|
+
] = []
|
|
190
|
+
on_error_in_loop: Literal["continue", "break", "raise"] = "raise"
|
|
191
|
+
|
|
192
|
+
@model_validator(mode="before")
|
|
193
|
+
def migrate_old_nodes(cls, data: dict[str, Any]):
|
|
194
|
+
for key in ["nodes"]:
|
|
195
|
+
raw_nodes = data.get(key, [])
|
|
196
|
+
new_nodes = []
|
|
197
|
+
used_old_format = False
|
|
198
|
+
|
|
199
|
+
for item in raw_nodes:
|
|
200
|
+
if (
|
|
201
|
+
isinstance(item, ActionNode)
|
|
202
|
+
or isinstance(item, ForLoopNode)
|
|
203
|
+
or isinstance(item, IfElseNode)
|
|
204
|
+
):
|
|
205
|
+
new_nodes.append(item)
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# --- new format: already has a type ---
|
|
209
|
+
if isinstance(item, dict) and "type" in item:
|
|
210
|
+
new_nodes.append(item)
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# --- old format cases ---
|
|
214
|
+
used_old_format = True
|
|
215
|
+
|
|
216
|
+
if isinstance(item, dict) and "condition" in item:
|
|
217
|
+
new_nodes.append({"type": "if_else_node", **item})
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
if isinstance(item, dict) and "variable_name" in item:
|
|
221
|
+
new_nodes.append({"type": "for_loop_node", **item})
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
new_nodes.append({"type": "action_node", **item})
|
|
225
|
+
|
|
226
|
+
if used_old_format:
|
|
227
|
+
logger.warning(
|
|
228
|
+
"Old node format without 'type' is deprecated. "
|
|
229
|
+
"Use the new format: {'type': 'action_node'|'for_loop_node'|'if_else_node', ...}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
data[key] = new_nodes
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class IfElseNode(BaseModel):
|
|
237
|
+
type: Literal["if_else_node"]
|
|
238
|
+
condition: str
|
|
239
|
+
if_nodes: list[ActionNode | IfElseNodeRef | ForLoopNodeRef]
|
|
240
|
+
else_nodes: list[ActionNode | IfElseNodeRef | ForLoopNodeRef] = []
|
|
241
|
+
|
|
242
|
+
@model_validator(mode="before")
|
|
243
|
+
def migrate_old_nodes(cls, data: dict[str, Any]):
|
|
244
|
+
for key in ["if_nodes", "else_nodes"]:
|
|
245
|
+
raw_nodes = data.get(key, [])
|
|
246
|
+
new_nodes = []
|
|
247
|
+
used_old_format = False
|
|
248
|
+
|
|
249
|
+
for item in raw_nodes:
|
|
250
|
+
if (
|
|
251
|
+
isinstance(item, ActionNode)
|
|
252
|
+
or isinstance(item, ForLoopNode)
|
|
253
|
+
or isinstance(item, IfElseNode)
|
|
254
|
+
):
|
|
255
|
+
new_nodes.append(item)
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
# --- new format: already has a type ---
|
|
259
|
+
if isinstance(item, dict) and "type" in item:
|
|
260
|
+
new_nodes.append(item)
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# --- old format cases ---
|
|
264
|
+
used_old_format = True
|
|
265
|
+
|
|
266
|
+
if isinstance(item, dict) and "condition" in item:
|
|
267
|
+
new_nodes.append({"type": "if_else_node", **item})
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
if isinstance(item, dict) and "variable_name" in item:
|
|
271
|
+
new_nodes.append({"type": "for_loop_node", **item})
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
new_nodes.append({"type": "action_node", **item})
|
|
275
|
+
|
|
276
|
+
if used_old_format:
|
|
277
|
+
logger.warning(
|
|
278
|
+
"Old node format without 'type' is deprecated. "
|
|
279
|
+
"Use the new format: {'type': 'action_node'|'for_loop_node'|'if_else_node', ...}"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
data[key] = new_nodes
|
|
283
|
+
return data
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class Parameters(BaseModel):
|
|
287
|
+
input_parameters: dict[str, list[str | int | float | bool]]
|
|
288
|
+
secure_parameters: dict[str, list[SecureParameter]] = Field(default_factory=dict)
|
|
289
|
+
generated_parameters: dict[str, list[str | int | float | bool | None]]
|
|
290
|
+
|
|
291
|
+
@model_validator(mode="after")
|
|
292
|
+
def validate_parameters(self):
|
|
293
|
+
reserved_parameter_names = set(["current_page_url"])
|
|
294
|
+
|
|
295
|
+
for d in [
|
|
296
|
+
self.input_parameters,
|
|
297
|
+
self.generated_parameters,
|
|
298
|
+
self.secure_parameters,
|
|
299
|
+
]:
|
|
300
|
+
for key in d.keys():
|
|
301
|
+
if key in reserved_parameter_names:
|
|
302
|
+
raise ValueError(f"Parameter name {key} is reserved")
|
|
303
|
+
if not key.isidentifier():
|
|
304
|
+
raise ValueError(
|
|
305
|
+
f"Parameter name {key} is not a valid variable name"
|
|
306
|
+
)
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
## TODO: fix expected downloads for ForLoop
|
|
311
|
+
class Automation(BaseModel):
|
|
312
|
+
browser_channel: Literal["chromium", "chrome"] = "chromium"
|
|
313
|
+
expected_downloads: int = 0
|
|
314
|
+
url: str
|
|
315
|
+
parameters: Parameters
|
|
316
|
+
nodes: list[
|
|
317
|
+
Annotated[ActionNode | ForLoopNode | IfElseNode, Field(discriminator="type")]
|
|
318
|
+
]
|
|
319
|
+
automation_description: str | None = None
|
|
320
|
+
automation_endpoint: str | None = None
|
|
321
|
+
|
|
322
|
+
@model_validator(mode="before")
|
|
323
|
+
def migrate_old_nodes(cls, data: dict[str, Any]):
|
|
324
|
+
raw_nodes = data.get("nodes", [])
|
|
325
|
+
new_nodes = []
|
|
326
|
+
used_old_format = False
|
|
327
|
+
|
|
328
|
+
for item in raw_nodes:
|
|
329
|
+
if (
|
|
330
|
+
isinstance(item, ActionNode)
|
|
331
|
+
or isinstance(item, ForLoopNode)
|
|
332
|
+
or isinstance(item, IfElseNode)
|
|
333
|
+
):
|
|
334
|
+
new_nodes.append(item)
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
# --- new format: already has a type ---
|
|
338
|
+
if isinstance(item, dict) and "type" in item:
|
|
339
|
+
new_nodes.append(item)
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# --- old format cases ---
|
|
343
|
+
used_old_format = True
|
|
344
|
+
|
|
345
|
+
if isinstance(item, dict) and "condition" in item:
|
|
346
|
+
new_nodes.append({"type": "if_else_node", **item})
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
if isinstance(item, dict) and "variable_name" in item:
|
|
350
|
+
new_nodes.append({"type": "for_loop_node", **item})
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
new_nodes.append({"type": "action_node", **item})
|
|
354
|
+
|
|
355
|
+
if used_old_format:
|
|
356
|
+
logger.warning(
|
|
357
|
+
"Old node format without 'type' is deprecated. "
|
|
358
|
+
"Use the new format: {'type': 'action_node'|'for_loop_node'|'if_else_node', ...}"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
data["nodes"] = new_nodes
|
|
362
|
+
return data
|
|
363
|
+
|
|
364
|
+
@model_validator(mode="after")
|
|
365
|
+
def validate_parameters_with_examples(cls, model: "Automation"):
|
|
366
|
+
## TODO: static check that all parameters with examples are used in the nodes
|
|
367
|
+
return model
|
|
368
|
+
|
|
369
|
+
def model_dump(self, *, sort_params_by_nodes: bool = False, **kwargs):
|
|
370
|
+
"""
|
|
371
|
+
Extended model_dump with option to sort parameters by node order
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
sort_params_by_nodes: If True, sort input_parameters by their
|
|
375
|
+
appearance order in nodes. Fails gracefully
|
|
376
|
+
if sorting encounters any errors.
|
|
377
|
+
**kwargs: All standard Pydantic model_dump arguments (exclude,
|
|
378
|
+
exclude_none, exclude_defaults, etc.)
|
|
379
|
+
"""
|
|
380
|
+
data = super().model_dump(**kwargs)
|
|
381
|
+
|
|
382
|
+
if sort_params_by_nodes:
|
|
383
|
+
data = self._sort_parameters_by_node_order(data)
|
|
384
|
+
|
|
385
|
+
return data
|
|
386
|
+
|
|
387
|
+
def _sort_parameters_by_node_order(self, data: dict) -> dict:
|
|
388
|
+
"""
|
|
389
|
+
Sort input_parameters based on their first appearance in nodes.
|
|
390
|
+
Returns data unchanged if any error occurs.
|
|
391
|
+
|
|
392
|
+
This method searches for parameter references in the format {param_name[index]}
|
|
393
|
+
throughout the entire nodes array and reorders input_parameters accordingly.
|
|
394
|
+
Parameters that don't appear in nodes are placed at the end.
|
|
395
|
+
"""
|
|
396
|
+
try:
|
|
397
|
+
import json
|
|
398
|
+
import re
|
|
399
|
+
|
|
400
|
+
# Convert nodes to string to search for all parameter references
|
|
401
|
+
nodes_str = json.dumps(data.get("nodes", []))
|
|
402
|
+
# Extract all {param_name[index]} references
|
|
403
|
+
pattern = r"\{(\w+)\[\d+\]\}"
|
|
404
|
+
matches = re.findall(pattern, nodes_str)
|
|
405
|
+
# Preserve order of first occurrence
|
|
406
|
+
param_order = []
|
|
407
|
+
seen = set()
|
|
408
|
+
for param in matches:
|
|
409
|
+
if param not in seen:
|
|
410
|
+
param_order.append(param)
|
|
411
|
+
seen.add(param)
|
|
412
|
+
# Reorder input_parameters if they exist
|
|
413
|
+
if "parameters" in data and "input_parameters" in data["parameters"]:
|
|
414
|
+
old_params = data["parameters"]["input_parameters"]
|
|
415
|
+
sorted_params = {}
|
|
416
|
+
# Add params in order they appear in nodes
|
|
417
|
+
for param_name in param_order:
|
|
418
|
+
if param_name in old_params:
|
|
419
|
+
sorted_params[param_name] = old_params[param_name]
|
|
420
|
+
# Add remaining params that don't appear in nodes (at the end)
|
|
421
|
+
for param_name, param_value in old_params.items():
|
|
422
|
+
if param_name not in sorted_params:
|
|
423
|
+
sorted_params[param_name] = param_value
|
|
424
|
+
data["parameters"]["input_parameters"] = sorted_params
|
|
425
|
+
return data
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
# Log the error if logging is available
|
|
429
|
+
logger.warning(f"Failed to sort parameters by node order: {e}")
|
|
430
|
+
|
|
431
|
+
# Return original data unchanged
|
|
432
|
+
return data
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CallbackResponse(BaseModel):
|
|
7
|
+
task_id: str
|
|
8
|
+
recording_id: str
|
|
9
|
+
output_data: list[dict | str] | None
|
|
10
|
+
status: Literal["queued", "allocated", "running", "success", "failed", "cancelled"]
|
|
11
|
+
error: str | None
|
|
12
|
+
final_screenshot: str | None = None
|
|
13
|
+
endpoint_name: str
|
|
14
|
+
downloads: list[dict] | None = None
|
|
15
|
+
input_parameters: dict[str, list[str | int | float | bool]] | None = None
|
|
16
|
+
unique_parameter_names: list[str] | None = None
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, model_validator
|
|
4
|
+
|
|
5
|
+
from optexity.schema.automation import SecureParameter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InferenceRequest(BaseModel):
|
|
9
|
+
endpoint_name: str
|
|
10
|
+
input_parameters: dict[str, list[str | int | float | bool]]
|
|
11
|
+
unique_parameter_names: list[str] = Field(default_factory=list)
|
|
12
|
+
secure_parameters: dict[str, list[SecureParameter]] = Field(default_factory=dict)
|
|
13
|
+
use_proxy: bool = False
|
|
14
|
+
|
|
15
|
+
@model_validator(mode="after")
|
|
16
|
+
def validate_unique_parameter_names(self):
|
|
17
|
+
for unique_parameter_name in self.unique_parameter_names:
|
|
18
|
+
if unique_parameter_name not in self.input_parameters and (
|
|
19
|
+
self.secure_parameters is None
|
|
20
|
+
or unique_parameter_name not in self.secure_parameters
|
|
21
|
+
):
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"unique_parameter_name {unique_parameter_name} not found in input_parameters or secure_parameters"
|
|
24
|
+
)
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FetchEmailMessagesRequest(BaseModel):
|
|
29
|
+
receiver_email_address: str # receiver's email address
|
|
30
|
+
sender_email_address: str # sender's email address
|
|
31
|
+
start_2fa_time: datetime
|
|
32
|
+
end_2fa_time: datetime
|
|
33
|
+
|
|
34
|
+
@model_validator(mode="after")
|
|
35
|
+
def validate_time_parameters(self):
|
|
36
|
+
assert (
|
|
37
|
+
self.start_2fa_time.tzinfo is not None
|
|
38
|
+
), "start_2fa_time must be timezone-aware"
|
|
39
|
+
assert (
|
|
40
|
+
self.end_2fa_time.tzinfo is not None
|
|
41
|
+
), "end_2fa_time must be timezone-aware"
|
|
42
|
+
assert (
|
|
43
|
+
self.start_2fa_time < self.end_2fa_time
|
|
44
|
+
), "start_2fa_time must be before end_2fa_time"
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
class Config:
|
|
48
|
+
json_encoders = {datetime: lambda v: v.isoformat() if v is not None else None}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FetchSlackMessagesRequest(BaseModel):
|
|
52
|
+
slack_workspace_domain: str
|
|
53
|
+
channel_name: str
|
|
54
|
+
sender_name: str
|
|
55
|
+
start_2fa_time: datetime
|
|
56
|
+
end_2fa_time: datetime
|
|
57
|
+
|
|
58
|
+
@model_validator(mode="after")
|
|
59
|
+
def validate_time_parameters(self):
|
|
60
|
+
assert (
|
|
61
|
+
self.start_2fa_time.tzinfo is not None
|
|
62
|
+
), "start_2fa_time must be timezone-aware"
|
|
63
|
+
assert (
|
|
64
|
+
self.end_2fa_time.tzinfo is not None
|
|
65
|
+
), "end_2fa_time must be timezone-aware"
|
|
66
|
+
assert (
|
|
67
|
+
self.start_2fa_time < self.end_2fa_time
|
|
68
|
+
), "start_2fa_time must be before end_2fa_time"
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
class Config:
|
|
72
|
+
json_encoders = {datetime: lambda v: v.isoformat() if v is not None else None}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Message(BaseModel):
|
|
76
|
+
message_id: str | None = None
|
|
77
|
+
message_text: str
|
|
78
|
+
timestamp: datetime
|
|
79
|
+
|
|
80
|
+
@model_validator(mode="after")
|
|
81
|
+
def validate_timestamp(self):
|
|
82
|
+
assert self.timestamp.tzinfo is not None, "timestamp must be timezone-aware"
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FetchMessagesResponse(BaseModel):
|
|
87
|
+
messages: list[Message]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from playwright.async_api import Download
|
|
7
|
+
from pydantic import BaseModel, Field, model_validator
|
|
8
|
+
|
|
9
|
+
from optexity.schema.token_usage import TokenUsage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NetworkRequest(BaseModel):
|
|
13
|
+
url: str
|
|
14
|
+
method: str
|
|
15
|
+
headers: dict
|
|
16
|
+
body: str | bytes | None | dict | Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NetworkError(BaseModel):
|
|
20
|
+
url: str = Field(...)
|
|
21
|
+
message: str = Field(...)
|
|
22
|
+
stack_trace: str = Field(...)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NetworkResponse(BaseModel):
|
|
26
|
+
url: str = Field(...)
|
|
27
|
+
status: int = Field(...)
|
|
28
|
+
headers: dict = Field(...)
|
|
29
|
+
body: dict | str | None | bytes | Any = Field(default=None)
|
|
30
|
+
method: str = Field(...)
|
|
31
|
+
content_length: int = Field(...)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AutomationState(BaseModel):
|
|
35
|
+
step_index: int = Field(default_factory=lambda: -1)
|
|
36
|
+
try_index: int = Field(default_factory=lambda: -1)
|
|
37
|
+
start_2fa_time: datetime | None = Field(default=None)
|
|
38
|
+
|
|
39
|
+
@model_validator(mode="after")
|
|
40
|
+
def validate_start_2fa_time(self):
|
|
41
|
+
if self.start_2fa_time is not None:
|
|
42
|
+
assert (
|
|
43
|
+
self.start_2fa_time.tzinfo is not None
|
|
44
|
+
), "start_2fa_time must be timezone-aware"
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BrowserState(BaseModel):
|
|
49
|
+
url: str = Field(...)
|
|
50
|
+
title: str | None = Field(default=None)
|
|
51
|
+
screenshot: str | None = Field(default=None)
|
|
52
|
+
html: str | None = Field(default=None)
|
|
53
|
+
axtree: str | None = Field(default=None)
|
|
54
|
+
final_prompt: str | None = Field(default=None)
|
|
55
|
+
llm_response: str | dict | None = Field(default=None)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ScreenshotData(BaseModel):
|
|
59
|
+
filename: str = Field(...)
|
|
60
|
+
base64: str = Field(...)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OutputData(BaseModel):
|
|
64
|
+
unique_identifier: str | None = None
|
|
65
|
+
json_data: dict | None = Field(default=None)
|
|
66
|
+
screenshot: ScreenshotData = Field(default=None)
|
|
67
|
+
text: str | None = Field(default=None)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ForLoopStatus(BaseModel):
|
|
71
|
+
variable_name: str
|
|
72
|
+
index: int
|
|
73
|
+
value: str | int | float | bool
|
|
74
|
+
error: str | None = None
|
|
75
|
+
status: Literal["success", "error", "skipped"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Variables(BaseModel):
|
|
79
|
+
output_data: list[OutputData] = Field(default_factory=list)
|
|
80
|
+
for_loop_status: list[list[ForLoopStatus]] = Field(default_factory=list)
|
|
81
|
+
generated_variables: dict = Field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Memory(BaseModel):
|
|
85
|
+
variables: Variables = Field(default_factory=Variables)
|
|
86
|
+
automation_state: AutomationState = Field(default_factory=AutomationState)
|
|
87
|
+
browser_states: list[BrowserState] = Field(default_factory=list)
|
|
88
|
+
token_usage: TokenUsage = Field(default_factory=TokenUsage)
|
|
89
|
+
download_lock: asyncio.Lock = Field(default_factory=asyncio.Lock)
|
|
90
|
+
raw_downloads: dict[Path, tuple[bool, Download | None]] = Field(
|
|
91
|
+
default_factory=dict
|
|
92
|
+
)
|
|
93
|
+
urls_to_downloads: list[tuple[str, str]] = Field(default_factory=list)
|
|
94
|
+
downloads: list[Path] = Field(default_factory=list)
|
|
95
|
+
final_screenshot: str | None = Field(default=None)
|
|
96
|
+
|
|
97
|
+
model_config = {
|
|
98
|
+
"arbitrary_types_allowed": True,
|
|
99
|
+
"exclude": {"download_lock"},
|
|
100
|
+
}
|