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,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
+ }