optexity 0.1.5.1__tar.gz → 0.1.5.2__tar.gz

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 (87) hide show
  1. {optexity-0.1.5.1 → optexity-0.1.5.2}/PKG-INFO +1 -1
  2. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/handle_click.py +4 -1
  3. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/handle_command.py +24 -3
  4. optexity-0.1.5.2/optexity/inference/core/interaction/handle_hover.py +83 -0
  5. optexity-0.1.5.2/optexity/inference/core/interaction/handle_keypress.py +42 -0
  6. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/handle_select_utils.py +11 -1
  7. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/run_automation.py +0 -3
  8. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/run_extraction.py +37 -0
  9. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/run_interaction.py +10 -0
  10. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/run_two_fa.py +12 -7
  11. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/actions/extraction_action.py +9 -3
  12. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/actions/interaction_action.py +38 -4
  13. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/actions/two_fa_action.py +1 -1
  14. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/automation.py +2 -7
  15. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/task.py +6 -2
  16. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity.egg-info/PKG-INFO +1 -1
  17. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity.egg-info/SOURCES.txt +1 -0
  18. {optexity-0.1.5.1 → optexity-0.1.5.2}/pyproject.toml +1 -1
  19. optexity-0.1.5.1/optexity/inference/core/interaction/handle_keypress.py +0 -16
  20. {optexity-0.1.5.1 → optexity-0.1.5.2}/LICENSE +0 -0
  21. {optexity-0.1.5.1 → optexity-0.1.5.2}/README.md +0 -0
  22. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/__init__.py +0 -0
  23. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/cli.py +0 -0
  24. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/__init__.py +0 -0
  25. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/add_example.py +0 -0
  26. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/download_pdf_url.py +0 -0
  27. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/extract_price_stockanalysis.py +0 -0
  28. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/file_upload.py +0 -0
  29. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/i94.py +0 -0
  30. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/i94_travel_history.py +0 -0
  31. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/peachstate_medicaid.py +0 -0
  32. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/examples/supabase_login.py +0 -0
  33. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/exceptions.py +0 -0
  34. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/__init__.py +0 -0
  35. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/__init__.py +0 -0
  36. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/error_handler/__init__.py +0 -0
  37. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/error_handler/error_handler.py +0 -0
  38. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/error_handler/prompt.py +0 -0
  39. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/index_prediction/__init__.py +0 -0
  40. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/index_prediction/action_prediction_locator_axtree.py +0 -0
  41. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/index_prediction/prompt.py +0 -0
  42. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/select_value_prediction/__init__.py +0 -0
  43. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/select_value_prediction/prompt.py +0 -0
  44. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/select_value_prediction/select_value_prediction.py +0 -0
  45. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/two_fa_extraction/__init__.py +0 -0
  46. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/two_fa_extraction/prompt.py +0 -0
  47. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/agents/two_fa_extraction/two_fa_extraction.py +0 -0
  48. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/child_process.py +0 -0
  49. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/__init__.py +0 -0
  50. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/__init__.py +0 -0
  51. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/handle_agentic_task.py +0 -0
  52. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/handle_check.py +0 -0
  53. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/handle_input.py +0 -0
  54. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/handle_select.py +0 -0
  55. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/handle_upload.py +0 -0
  56. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/interaction/utils.py +0 -0
  57. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/logging.py +0 -0
  58. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/run_assertion.py +0 -0
  59. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/run_python_script.py +0 -0
  60. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/core/two_factor_auth/__init__.py +0 -0
  61. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/infra/__init__.py +0 -0
  62. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/infra/browser.py +0 -0
  63. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/infra/browser_extension.py +0 -0
  64. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/models/__init__.py +0 -0
  65. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/models/gemini.py +0 -0
  66. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/models/human.py +0 -0
  67. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/models/llm_model.py +0 -0
  68. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/inference/run_local.py +0 -0
  69. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/onepassword_integration.py +0 -0
  70. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/__init__.py +0 -0
  71. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/actions/__init__.py +0 -0
  72. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/actions/assertion_action.py +0 -0
  73. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/actions/misc_action.py +0 -0
  74. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/actions/prompts.py +0 -0
  75. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/callback.py +0 -0
  76. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/inference.py +0 -0
  77. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/memory.py +0 -0
  78. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/schema/token_usage.py +0 -0
  79. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/test.py +0 -0
  80. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/utils/__init__.py +0 -0
  81. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/utils/settings.py +0 -0
  82. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity/utils/utils.py +0 -0
  83. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity.egg-info/dependency_links.txt +0 -0
  84. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity.egg-info/entry_points.txt +0 -0
  85. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity.egg-info/requires.txt +0 -0
  86. {optexity-0.1.5.1 → optexity-0.1.5.2}/optexity.egg-info/top_level.txt +0 -0
  87. {optexity-0.1.5.1 → optexity-0.1.5.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optexity
3
- Version: 0.1.5.1
3
+ Version: 0.1.5.2
4
4
  Summary: Optexity is a platform for building and running browser and computer agents.
5
5
  Author-email: Optexity <founders@optexity.com>
6
6
  Requires-Python: >=3.11
@@ -59,8 +59,11 @@ async def click_element_index(
59
59
  return
60
60
 
61
61
  async def _actual_click_element():
62
+ print(
63
+ f"Clicking element with index: {index} and button: {click_element_action.button}"
64
+ )
62
65
  action_model = browser.backend_agent.ActionModel(
63
- **{"click": {"index": index}}
66
+ **{"click": {"index": index, "button": click_element_action.button}}
64
67
  )
65
68
  await browser.backend_agent.multi_act([action_model])
66
69
 
@@ -13,6 +13,7 @@ from optexity.inference.infra.browser import Browser
13
13
  from optexity.schema.actions.interaction_action import (
14
14
  CheckAction,
15
15
  ClickElementAction,
16
+ HoverAction,
16
17
  InputTextAction,
17
18
  SelectOptionAction,
18
19
  UncheckAction,
@@ -32,6 +33,7 @@ async def command_based_action_with_retry(
32
33
  | CheckAction
33
34
  | UploadFileAction
34
35
  | UncheckAction
36
+ | HoverAction
35
37
  ),
36
38
  browser: Browser,
37
39
  memory: Memory,
@@ -83,7 +85,7 @@ async def command_based_action_with_retry(
83
85
  )
84
86
  elif isinstance(action, InputTextAction):
85
87
  await input_text_locator(
86
- action, locator, max_timeout_seconds_per_try
88
+ action, locator, browser, max_timeout_seconds_per_try
87
89
  )
88
90
  elif isinstance(action, SelectOptionAction):
89
91
  await select_option_locator(
@@ -102,6 +104,8 @@ async def command_based_action_with_retry(
102
104
  await uncheck_locator(
103
105
  action, locator, max_timeout_seconds_per_try, browser
104
106
  )
107
+ elif isinstance(action, HoverAction):
108
+ await hover_locator(locator, max_timeout_seconds_per_try)
105
109
  elif isinstance(action, UploadFileAction):
106
110
  await upload_file_locator(action, locator)
107
111
  logger.debug(
@@ -149,7 +153,9 @@ async def click_locator(
149
153
  )
150
154
  else:
151
155
  await locator.click(
152
- no_wait_after=True, timeout=max_timeout_seconds_per_try * 1000
156
+ button=click_element_action.button,
157
+ no_wait_after=True,
158
+ timeout=max_timeout_seconds_per_try * 1000,
153
159
  )
154
160
 
155
161
  if click_element_action.expect_download:
@@ -163,6 +169,7 @@ async def click_locator(
163
169
  async def input_text_locator(
164
170
  input_text_action: InputTextAction,
165
171
  locator: Locator,
172
+ browser: Browser,
166
173
  max_timeout_seconds_per_try: float,
167
174
  ):
168
175
 
@@ -172,12 +179,19 @@ async def input_text_locator(
172
179
  no_wait_after=True,
173
180
  timeout=max_timeout_seconds_per_try * 1000,
174
181
  )
175
- else:
182
+ elif input_text_action.fill_or_type == "type":
176
183
  await locator.type(
177
184
  input_text_action.input_text,
178
185
  no_wait_after=True,
179
186
  timeout=max_timeout_seconds_per_try * 1000,
180
187
  )
188
+ else:
189
+ page = await browser.get_current_page()
190
+ if page is None:
191
+ return
192
+ for char in input_text_action.input_text:
193
+ await page.keyboard.press(char)
194
+ await asyncio.sleep(0.1)
181
195
 
182
196
  if input_text_action.press_enter:
183
197
  await locator.press("Enter")
@@ -211,6 +225,13 @@ async def uncheck_locator(
211
225
  )
212
226
 
213
227
 
228
+ async def hover_locator(
229
+ locator: Locator,
230
+ max_timeout_seconds_per_try: float,
231
+ ):
232
+ await locator.hover(no_wait_after=True, timeout=max_timeout_seconds_per_try * 1000)
233
+
234
+
214
235
  async def upload_file_locator(upload_file_action: UploadFileAction, locator: Locator):
215
236
  await locator.set_input_files(upload_file_action.file_path)
216
237
 
@@ -0,0 +1,83 @@
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 HoverAction
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_hover_element(
16
+ hover_element_action: HoverAction,
17
+ task: Task,
18
+ memory: Memory,
19
+ browser: Browser,
20
+ max_timeout_seconds_per_try: float,
21
+ max_tries: int,
22
+ ):
23
+
24
+ if hover_element_action.command and not hover_element_action.skip_command:
25
+ last_error = await command_based_action_with_retry(
26
+ hover_element_action,
27
+ browser,
28
+ memory,
29
+ task,
30
+ max_tries,
31
+ max_timeout_seconds_per_try,
32
+ )
33
+
34
+ if last_error is None:
35
+ return
36
+
37
+ if not hover_element_action.skip_prompt:
38
+ logger.debug(
39
+ f"Executing prompt-based action: {hover_element_action.__class__.__name__}"
40
+ )
41
+ await hover_element_index(hover_element_action, browser, memory, task)
42
+
43
+
44
+ async def hover_element_index(
45
+ hover_element_action: HoverAction,
46
+ browser: Browser,
47
+ memory: Memory,
48
+ task: Task,
49
+ ):
50
+
51
+ try:
52
+ index = await get_index_from_prompt(
53
+ memory, hover_element_action.prompt_instructions, browser, task
54
+ )
55
+ if index is None:
56
+ return
57
+
58
+ print(f"Hovering element with index: {index}")
59
+
60
+ async def _actual_hover_element():
61
+ try:
62
+ action_model = browser.backend_agent.ActionModel(
63
+ **{"hover": {"index": index}}
64
+ )
65
+ await browser.backend_agent.multi_act([action_model])
66
+ except Exception as e:
67
+ logger.error(f"Error in hover_element_index: {e} trying right click")
68
+ node = await browser.backend_agent.browser_session.get_element_by_index(
69
+ index
70
+ )
71
+ if node is None:
72
+ return
73
+
74
+ backend_page = (
75
+ await browser.backend_agent.browser_session.get_current_page()
76
+ )
77
+ element = await backend_page.get_element(node.backend_node_id)
78
+ await element.click(button="right")
79
+
80
+ await _actual_hover_element()
81
+ except Exception as e:
82
+ logger.error(f"Error in hover_element_index: {e}")
83
+ return
@@ -0,0 +1,42 @@
1
+ from optexity.inference.infra.browser import Browser
2
+ from optexity.schema.actions.interaction_action import KeyPressAction, KeyPressType
3
+ from optexity.schema.memory import Memory
4
+
5
+
6
+ async def handle_key_press(
7
+ keypress_action: KeyPressAction,
8
+ memory: Memory,
9
+ browser: Browser,
10
+ ):
11
+ page = await browser.get_current_page()
12
+ if page is None:
13
+ return
14
+
15
+ if keypress_action.type == KeyPressType.ENTER:
16
+ await page.keyboard.press("Enter")
17
+ if keypress_action.type == KeyPressType.TAB:
18
+ await page.keyboard.press("Tab")
19
+ if keypress_action.type == KeyPressType.ZERO:
20
+ await page.keyboard.press("0")
21
+ if keypress_action.type == KeyPressType.ONE:
22
+ await page.keyboard.press("1")
23
+ if keypress_action.type == KeyPressType.TWO:
24
+ await page.keyboard.press("2")
25
+ if keypress_action.type == KeyPressType.THREE:
26
+ await page.keyboard.press("3")
27
+ if keypress_action.type == KeyPressType.FOUR:
28
+ await page.keyboard.press("4")
29
+ if keypress_action.type == KeyPressType.FIVE:
30
+ await page.keyboard.press("5")
31
+ if keypress_action.type == KeyPressType.SIX:
32
+ await page.keyboard.press("6")
33
+ if keypress_action.type == KeyPressType.SEVEN:
34
+ await page.keyboard.press("7")
35
+ if keypress_action.type == KeyPressType.EIGHT:
36
+ await page.keyboard.press("8")
37
+ if keypress_action.type == KeyPressType.NINE:
38
+ await page.keyboard.press("9")
39
+ if keypress_action.type == KeyPressType.SLASH:
40
+ await page.keyboard.press("/")
41
+ if keypress_action.type == KeyPressType.SPACE:
42
+ await page.keyboard.press("Space")
@@ -57,9 +57,19 @@ async def smart_select(
57
57
  options: list[SelectOptionValue], patterns: list[str], memory: Memory
58
58
  ):
59
59
  # Get all options from the <select>
60
-
60
+ ## TODO: remove this once we have a better way to handle select one
61
61
  matched_values = []
62
62
 
63
+ if len(options) == 0:
64
+ return []
65
+ if len(options) == 1:
66
+ return [options[0].value]
67
+ if len(options) == 2 and "Select One" in [o.value for o in options]:
68
+ if options[0].value == "Select One":
69
+ return [options[1].value]
70
+ else:
71
+ return [options[0].value]
72
+
63
73
  for p in patterns:
64
74
  # If pattern contains regex characters, treat as regex
65
75
  is_regex = p.startswith("^") or p.endswith("$") or ".*" in p
@@ -25,7 +25,6 @@ from optexity.inference.core.run_interaction import (
25
25
  run_interaction_action,
26
26
  )
27
27
  from optexity.inference.core.run_python_script import run_python_script_action
28
- from optexity.inference.core.run_two_fa import run_two_fa_action
29
28
  from optexity.inference.infra.browser import Browser
30
29
  from optexity.schema.actions.interaction_action import DownloadUrlAsPdfAction
31
30
  from optexity.schema.automation import ActionNode, ForLoopNode, IfElseNode
@@ -274,8 +273,6 @@ async def run_action_node(
274
273
  await run_extraction_action(
275
274
  action_node.extraction_action, memory, browser, task
276
275
  )
277
- elif action_node.two_fa_action:
278
- await run_two_fa_action(action_node.two_fa_action, memory)
279
276
  elif action_node.python_script_action:
280
277
  await run_python_script_action(
281
278
  action_node.python_script_action, memory, browser
@@ -4,12 +4,14 @@ import traceback
4
4
  import aiofiles
5
5
  import httpx
6
6
 
7
+ from optexity.inference.core.run_two_fa import run_two_fa_action
7
8
  from optexity.inference.infra.browser import Browser
8
9
  from optexity.inference.models import GeminiModels, get_llm_model
9
10
  from optexity.schema.actions.extraction_action import (
10
11
  ExtractionAction,
11
12
  LLMExtraction,
12
13
  NetworkCallExtraction,
14
+ PythonScriptExtraction,
13
15
  ScreenshotExtraction,
14
16
  StateExtraction,
15
17
  )
@@ -51,6 +53,14 @@ async def run_extraction_action(
51
53
  task,
52
54
  extraction_action.unique_identifier,
53
55
  )
56
+ elif extraction_action.python_script:
57
+ await handle_python_script_extraction(
58
+ extraction_action.python_script,
59
+ memory,
60
+ browser,
61
+ task,
62
+ extraction_action.unique_identifier,
63
+ )
54
64
  elif extraction_action.screenshot:
55
65
  await handle_screenshot_extraction(
56
66
  extraction_action.screenshot,
@@ -65,6 +75,8 @@ async def run_extraction_action(
65
75
  browser,
66
76
  extraction_action.unique_identifier,
67
77
  )
78
+ elif extraction_action.two_fa_action:
79
+ await run_two_fa_action(extraction_action.two_fa_action, memory)
68
80
 
69
81
 
70
82
  async def handle_state_extraction(
@@ -225,6 +237,31 @@ async def handle_network_call_extraction(
225
237
  )
226
238
 
227
239
 
240
+ async def handle_python_script_extraction(
241
+ python_script_extraction: PythonScriptExtraction,
242
+ memory: Memory,
243
+ browser: Browser,
244
+ task: Task,
245
+ unique_identifier: str | None = None,
246
+ ):
247
+ local_vars = {}
248
+ exec(python_script_extraction.script, {}, local_vars)
249
+ code_fn = local_vars["code_fn"]
250
+ axtree = memory.browser_states[-1].axtree
251
+ result = await code_fn(axtree)
252
+ if result is not None:
253
+ memory.variables.output_data.append(
254
+ OutputData(
255
+ unique_identifier=unique_identifier,
256
+ json_data=result,
257
+ )
258
+ )
259
+ else:
260
+ logger.warning(
261
+ f"No result from Python script extraction: {python_script_extraction.script}"
262
+ )
263
+
264
+
228
265
  async def download_request(
229
266
  network_call: NetworkRequest, download_filename: str, task: Task, memory: Memory
230
267
  ):
@@ -12,6 +12,7 @@ from optexity.inference.core.interaction.handle_check import (
12
12
  handle_uncheck_element,
13
13
  )
14
14
  from optexity.inference.core.interaction.handle_click import handle_click_element
15
+ from optexity.inference.core.interaction.handle_hover import handle_hover_element
15
16
  from optexity.inference.core.interaction.handle_input import handle_input_text
16
17
  from optexity.inference.core.interaction.handle_keypress import handle_key_press
17
18
  from optexity.inference.core.interaction.handle_select import handle_select_option
@@ -95,6 +96,15 @@ async def run_interaction_action(
95
96
  interaction_action.max_timeout_seconds_per_try,
96
97
  interaction_action.max_tries,
97
98
  )
99
+ elif interaction_action.hover:
100
+ await handle_hover_element(
101
+ interaction_action.hover,
102
+ task,
103
+ memory,
104
+ browser,
105
+ interaction_action.max_timeout_seconds_per_try,
106
+ interaction_action.max_tries,
107
+ )
98
108
  elif interaction_action.go_back:
99
109
  await handle_go_back(interaction_action.go_back, memory, browser)
100
110
  elif interaction_action.download_url_as_pdf:
@@ -33,6 +33,7 @@ async def run_two_fa_action(two_fa_action: TwoFAAction, memory: Memory):
33
33
 
34
34
  elapsed = 0
35
35
  messages = None
36
+ code = None
36
37
 
37
38
  while elapsed < two_fa_action.max_wait_time:
38
39
  messages = await fetch_messages(
@@ -109,12 +110,16 @@ async def fetch_messages(
109
110
  end_2fa_time=end_2fa_time,
110
111
  )
111
112
 
112
- async with httpx.AsyncClient(timeout=30.0) as client:
113
+ try:
114
+ async with httpx.AsyncClient(timeout=30.0) as client:
113
115
 
114
- response = await client.post(
115
- url, json=body.model_dump(mode="json"), headers=headers
116
- )
117
- response.raise_for_status()
118
- response_data = FetchMessagesResponse.model_validate(response.json())
116
+ response = await client.post(
117
+ url, json=body.model_dump(mode="json"), headers=headers
118
+ )
119
+ response.raise_for_status()
120
+ response_data = FetchMessagesResponse.model_validate(response.json())
119
121
 
120
- return response_data.messages
122
+ return response_data.messages
123
+ except Exception as e:
124
+ logger.error(f"Error fetching messages: {e}")
125
+ return []
@@ -3,6 +3,7 @@ from uuid import uuid4
3
3
 
4
4
  from pydantic import BaseModel, Field, field_validator, model_validator
5
5
 
6
+ from optexity.schema.actions.two_fa_action import TwoFAAction
6
7
  from optexity.utils.utils import build_model
7
8
 
8
9
 
@@ -53,13 +54,16 @@ class LLMExtraction(BaseModel):
53
54
  return self
54
55
 
55
56
  def replace(self, pattern: str, replacement: str):
57
+ self.extraction_instructions = self.extraction_instructions.replace(
58
+ pattern, replacement
59
+ )
56
60
  return self
57
61
 
58
62
 
59
63
  class NetworkCallExtraction(BaseModel):
60
64
  url_pattern: Optional[str] = None
61
- extract_from: None | Literal["request", "response"] = None
62
- download_from: None | Literal["request", "response"] = None
65
+ extract_from: None | Literal["request", "response"] = "response"
66
+ download_from: None | Literal["request", "response"] = "response"
63
67
  download_filename: str | None = None
64
68
 
65
69
  @model_validator(mode="before")
@@ -109,6 +113,7 @@ class ExtractionAction(BaseModel):
109
113
  python_script: Optional[PythonScriptExtraction] = None
110
114
  screenshot: Optional[ScreenshotExtraction] = None
111
115
  state: Optional[StateExtraction] = None
116
+ two_fa_action: TwoFAAction | None = None
112
117
 
113
118
  @model_validator(mode="after")
114
119
  def validate_one_extraction(cls, model: "ExtractionAction"):
@@ -119,12 +124,13 @@ class ExtractionAction(BaseModel):
119
124
  "python_script": model.python_script,
120
125
  "screenshot": model.screenshot,
121
126
  "state": model.state,
127
+ "two_fa_action": model.two_fa_action,
122
128
  }
123
129
  non_null = [k for k, v in provided.items() if v is not None]
124
130
 
125
131
  if len(non_null) != 1:
126
132
  raise ValueError(
127
- "Exactly one of llm, networkcall, python_script, or screenshot must be provided"
133
+ "Exactly one of llm, networkcall, python_script, screenshot, state, or two_fa_action must be provided"
128
134
  )
129
135
 
130
136
  return model
@@ -1,5 +1,5 @@
1
1
  from enum import Enum, unique
2
- from typing import Literal
2
+ from typing import Any, Literal
3
3
  from uuid import uuid4
4
4
 
5
5
  from pydantic import BaseModel, Field, model_validator
@@ -69,6 +69,10 @@ class UncheckAction(BaseAction):
69
69
  pass
70
70
 
71
71
 
72
+ class HoverAction(BaseAction):
73
+ pass
74
+
75
+
72
76
  class SelectOptionAction(BaseAction):
73
77
  select_values: list[str]
74
78
  expect_download: bool = False
@@ -100,6 +104,7 @@ class ClickElementAction(BaseAction):
100
104
  double_click: bool = False
101
105
  expect_download: bool = False
102
106
  download_filename: str | None = None
107
+ button: Literal["left", "right", "middle"] = "left"
103
108
 
104
109
  @model_validator(mode="after")
105
110
  def set_download_filename(cls, model: "ClickElementAction"):
@@ -121,7 +126,7 @@ class ClickElementAction(BaseAction):
121
126
  class InputTextAction(BaseAction):
122
127
  input_text: str | None = None
123
128
  is_slider: bool = False
124
- fill_or_type: Literal["fill", "type"] = "fill"
129
+ fill_or_type: Literal["fill", "type", "key_press"] = "fill"
125
130
  press_enter: bool = False
126
131
 
127
132
  @model_validator(mode="after")
@@ -217,10 +222,33 @@ class KeyPressType(str, Enum):
217
222
  DELETE = "Delete"
218
223
  BACKSPACE = "Backspace"
219
224
  ESCAPE = "Escape"
225
+ ZERO = "0"
226
+ ONE = "1"
227
+ TWO = "2"
228
+ THREE = "3"
229
+ FOUR = "4"
230
+ FIVE = "5"
231
+ SIX = "6"
232
+ SEVEN = "7"
233
+ EIGHT = "8"
234
+ NINE = "9"
235
+ SLASH = "/"
236
+ SPACE = "Space"
220
237
 
221
238
 
222
239
  class KeyPressAction(BaseModel):
223
- type: KeyPressType
240
+ type: KeyPressType | Any
241
+
242
+ @model_validator(mode="after")
243
+ def validate_type(self):
244
+ if self.type is None:
245
+ raise ValueError("type is required")
246
+ return self
247
+
248
+ def replace(self, pattern: str, replacement: str):
249
+ if self.type:
250
+ self.type = self.type.replace(pattern, replacement).strip('"')
251
+ return self
224
252
 
225
253
 
226
254
  class AgenticTask(BaseModel):
@@ -252,6 +280,7 @@ class InteractionAction(BaseModel):
252
280
  select_option: SelectOptionAction | None = None
253
281
  check: CheckAction | None = None
254
282
  uncheck: UncheckAction | None = None
283
+ hover: HoverAction | None = None
255
284
  download_url_as_pdf: DownloadUrlAsPdfAction | None = None
256
285
  scroll: ScrollAction | None = None
257
286
  upload_file: UploadFileAction | None = None
@@ -274,6 +303,7 @@ class InteractionAction(BaseModel):
274
303
  "select_option": model.select_option,
275
304
  "check": model.check,
276
305
  "uncheck": model.uncheck,
306
+ "hover": model.hover,
277
307
  "download_url_as_pdf": model.download_url_as_pdf,
278
308
  "scroll": model.scroll,
279
309
  "upload_file": model.upload_file,
@@ -291,7 +321,7 @@ class InteractionAction(BaseModel):
291
321
 
292
322
  if len(non_null) != 1:
293
323
  raise ValueError(
294
- "Exactly one of click_element, input_text, select_option, check, uncheck, download_url_as_pdf, scroll, upload_file, go_to_url, go_back, switch_tab, close_current_tab, close_all_but_last_tab, close_tabs_until, key_press, or agentic_task must be provided"
324
+ "Exactly one of click_element, input_text, select_option, check, uncheck, hover, download_url_as_pdf, scroll, upload_file, go_to_url, go_back, switch_tab, close_current_tab, close_all_but_last_tab, close_tabs_until, key_press, or agentic_task must be provided"
295
325
  )
296
326
 
297
327
  if (
@@ -314,6 +344,8 @@ class InteractionAction(BaseModel):
314
344
  self.check.replace(pattern, replacement)
315
345
  if self.uncheck:
316
346
  self.uncheck.replace(pattern, replacement)
347
+ if self.hover:
348
+ self.hover.replace(pattern, replacement)
317
349
  if self.download_url_as_pdf:
318
350
  self.download_url_as_pdf.replace(pattern, replacement)
319
351
  if self.close_tabs_until:
@@ -326,5 +358,7 @@ class InteractionAction(BaseModel):
326
358
  self.go_to_url.replace(pattern, replacement)
327
359
  if self.upload_file:
328
360
  self.upload_file.replace(pattern, replacement)
361
+ if self.key_press:
362
+ self.key_press.replace(pattern, replacement)
329
363
 
330
364
  return self
@@ -21,4 +21,4 @@ class TwoFAAction(BaseModel):
21
21
  instructions: str | None = None
22
22
  output_variable_name: str
23
23
  max_wait_time: float = 300.0
24
- check_interval: float = 10.0
24
+ check_interval: float = 30.0
@@ -7,7 +7,6 @@ from optexity.schema.actions.assertion_action import AssertionAction
7
7
  from optexity.schema.actions.extraction_action import ExtractionAction
8
8
  from optexity.schema.actions.interaction_action import InteractionAction
9
9
  from optexity.schema.actions.misc_action import PythonScriptAction
10
- from optexity.schema.actions.two_fa_action import TwoFAAction
11
10
  from optexity.utils.utils import get_onepassword_value, get_totp_code
12
11
 
13
12
  logger = logging.getLogger(__name__)
@@ -68,7 +67,6 @@ class ActionNode(BaseModel):
68
67
  assertion_action: AssertionAction | None = None
69
68
  extraction_action: ExtractionAction | None = None
70
69
  python_script_action: PythonScriptAction | None = None
71
- two_fa_action: TwoFAAction | None = None
72
70
  before_sleep_time: float = 0.0
73
71
  end_sleep_time: float = 5.0
74
72
  expect_new_tab: bool = False
@@ -83,13 +81,12 @@ class ActionNode(BaseModel):
83
81
  "assertion_action": model.assertion_action,
84
82
  "extraction_action": model.extraction_action,
85
83
  "python_script_action": model.python_script_action,
86
- "two_fa_action": model.two_fa_action,
87
84
  }
88
85
  non_null = [k for k, v in provided.items() if v is not None]
89
86
 
90
87
  if len(non_null) != 1:
91
88
  raise ValueError(
92
- "Exactly one of interaction_action, assertion_action, extraction_action, python_script_action, or two_fa_action must be provided"
89
+ "Exactly one of interaction_action, assertion_action, extraction_action, python_script_action must be provided"
93
90
  )
94
91
 
95
92
  assert (
@@ -104,7 +101,7 @@ class ActionNode(BaseModel):
104
101
  user_set = model.__pydantic_fields_set__
105
102
 
106
103
  if "end_sleep_time" not in user_set:
107
- if model.assertion_action or model.extraction_action or model.two_fa_action:
104
+ if model.assertion_action or model.extraction_action:
108
105
  model.end_sleep_time = 0.0
109
106
 
110
107
  if "before_sleep_time" not in user_set:
@@ -130,8 +127,6 @@ class ActionNode(BaseModel):
130
127
  self.extraction_action.replace(pattern, replacement)
131
128
  if self.python_script_action:
132
129
  pass
133
- if self.two_fa_action:
134
- pass
135
130
 
136
131
  return self
137
132
 
@@ -60,7 +60,9 @@ class Task(BaseModel):
60
60
  started_at: Optional[datetime] = None
61
61
  completed_at: Optional[datetime] = None
62
62
  error: Optional[str] = None
63
- status: Literal["queued", "allocated", "running", "success", "failed", "cancelled"]
63
+ status: Literal[
64
+ "queued", "allocated", "running", "success", "failed", "cancelled", "killed"
65
+ ]
64
66
  is_cloud: bool = False
65
67
  save_directory: Path = Field(default=Path("/tmp/optexity"))
66
68
  use_proxy: bool = False
@@ -102,7 +104,9 @@ class Task(BaseModel):
102
104
  unique_parameter_name: self.input_parameters[unique_parameter_name]
103
105
  for unique_parameter_name in self.unique_parameter_names
104
106
  }
105
- self.dedup_key = json.dumps(self.unique_parameters, sort_keys=True)
107
+ self.dedup_key = (
108
+ json.dumps(self.unique_parameters, sort_keys=True) + self.user_id
109
+ )
106
110
 
107
111
  for a, b in [
108
112
  (self.automation.parameters.input_parameters, self.input_parameters),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optexity
3
- Version: 0.1.5.1
3
+ Version: 0.1.5.2
4
4
  Summary: Optexity is a platform for building and running browser and computer agents.
5
5
  Author-email: Optexity <founders@optexity.com>
6
6
  Requires-Python: >=3.11
@@ -50,6 +50,7 @@ optexity/inference/core/interaction/handle_agentic_task.py
50
50
  optexity/inference/core/interaction/handle_check.py
51
51
  optexity/inference/core/interaction/handle_click.py
52
52
  optexity/inference/core/interaction/handle_command.py
53
+ optexity/inference/core/interaction/handle_hover.py
53
54
  optexity/inference/core/interaction/handle_input.py
54
55
  optexity/inference/core/interaction/handle_keypress.py
55
56
  optexity/inference/core/interaction/handle_select.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "optexity"
7
- version = "0.1.5.1"
7
+ version = "0.1.5.2"
8
8
  readme = "README.md"
9
9
  description = "Optexity is a platform for building and running browser and computer agents."
10
10
  authors = [{ name = "Optexity", email = "founders@optexity.com" }]
@@ -1,16 +0,0 @@
1
- from optexity.inference.infra.browser import Browser
2
- from optexity.schema.actions.interaction_action import KeyPressAction, KeyPressType
3
- from optexity.schema.memory import Memory
4
-
5
-
6
- async def handle_key_press(
7
- keypress_action: KeyPressAction,
8
- memory: Memory,
9
- browser: Browser,
10
- ):
11
- page = await browser.get_current_page()
12
- if page is None:
13
- return
14
-
15
- if keypress_action.type == KeyPressType.ENTER:
16
- await page.keyboard.press("Enter")
File without changes
File without changes
File without changes
File without changes
File without changes