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,240 @@
1
+ import logging
2
+ import traceback
3
+
4
+ import aiofiles
5
+ import httpx
6
+
7
+ from optexity.inference.infra.browser import Browser
8
+ from optexity.inference.models import GeminiModels, get_llm_model
9
+ from optexity.schema.actions.extraction_action import (
10
+ ExtractionAction,
11
+ LLMExtraction,
12
+ NetworkCallExtraction,
13
+ ScreenshotExtraction,
14
+ StateExtraction,
15
+ )
16
+ from optexity.schema.memory import (
17
+ BrowserState,
18
+ Memory,
19
+ NetworkRequest,
20
+ NetworkResponse,
21
+ OutputData,
22
+ ScreenshotData,
23
+ )
24
+ from optexity.schema.task import Task
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ llm_model = get_llm_model(GeminiModels.GEMINI_2_5_FLASH, True)
29
+
30
+
31
+ async def run_extraction_action(
32
+ extraction_action: ExtractionAction, memory: Memory, browser: Browser, task: Task
33
+ ):
34
+ logger.debug(
35
+ f"---------Running extraction action {extraction_action.model_dump_json()}---------"
36
+ )
37
+
38
+ if extraction_action.llm:
39
+ await handle_llm_extraction(
40
+ extraction_action.llm, memory, browser, extraction_action.unique_identifier
41
+ )
42
+ elif extraction_action.network_call:
43
+ await handle_network_call_extraction(
44
+ extraction_action.network_call,
45
+ memory,
46
+ browser,
47
+ task,
48
+ extraction_action.unique_identifier,
49
+ )
50
+ elif extraction_action.screenshot:
51
+ await handle_screenshot_extraction(
52
+ extraction_action.screenshot,
53
+ memory,
54
+ browser,
55
+ extraction_action.unique_identifier,
56
+ )
57
+ elif extraction_action.state:
58
+ await handle_state_extraction(
59
+ extraction_action.state,
60
+ memory,
61
+ browser,
62
+ extraction_action.unique_identifier,
63
+ )
64
+
65
+
66
+ async def handle_state_extraction(
67
+ state_extraction: StateExtraction,
68
+ memory: Memory,
69
+ browser: Browser,
70
+ unique_identifier: str | None = None,
71
+ ):
72
+ page = await browser.get_current_page()
73
+ if page is None:
74
+ return
75
+
76
+ memory.variables.output_data.append(
77
+ OutputData(
78
+ unique_identifier=unique_identifier,
79
+ json_data={"page_url": page.url, "page_title": await page.title()},
80
+ )
81
+ )
82
+
83
+
84
+ async def handle_screenshot_extraction(
85
+ screenshot_extraction: ScreenshotExtraction,
86
+ memory: Memory,
87
+ browser: Browser,
88
+ unique_identifier: str | None = None,
89
+ ):
90
+
91
+ screenshot_base64 = await browser.get_screenshot(
92
+ full_page=screenshot_extraction.full_page
93
+ )
94
+ if screenshot_base64 is None:
95
+ return
96
+
97
+ memory.variables.output_data.append(
98
+ OutputData(
99
+ unique_identifier=unique_identifier,
100
+ screenshot=ScreenshotData(
101
+ filename=screenshot_extraction.filename, base64=screenshot_base64
102
+ ),
103
+ )
104
+ )
105
+
106
+
107
+ async def handle_llm_extraction(
108
+ llm_extraction: LLMExtraction,
109
+ memory: Memory,
110
+ browser: Browser,
111
+ unique_identifier: str | None = None,
112
+ ):
113
+ browser_state_summary = await browser.get_browser_state_summary()
114
+ memory.browser_states[-1] = BrowserState(
115
+ url=browser_state_summary.url,
116
+ screenshot=browser_state_summary.screenshot,
117
+ title=browser_state_summary.title,
118
+ axtree=browser_state_summary.dom_state.llm_representation(),
119
+ )
120
+
121
+ # TODO: fix this double calling of screenshot and axtree
122
+ if "axtree" in llm_extraction.source:
123
+ axtree = memory.browser_states[-1].axtree
124
+ else:
125
+ axtree = None
126
+
127
+ if "screenshot" in llm_extraction.source:
128
+ screenshot = memory.browser_states[-1].screenshot
129
+ else:
130
+ screenshot = None
131
+
132
+ system_instruction = f"""
133
+ You are an expert in extracting information from a website. You will be given an axtree of a webpage.
134
+ Your task is to extract the information from the webpage and return it in the format specified by the instructions.
135
+ {llm_extraction.extraction_instructions}
136
+ """
137
+
138
+ prompt = f"""
139
+ [INPUT]
140
+ Axtree: {axtree}
141
+ [/INPUT]
142
+ """
143
+
144
+ if llm_extraction.llm_provider == "gemini":
145
+ model_name = GeminiModels(llm_extraction.llm_model_name)
146
+ llm_model.model_name = model_name
147
+ else:
148
+ raise ValueError(f"Invalid LLM provider: {llm_extraction.llm_provider}")
149
+
150
+ response, token_usage = llm_model.get_model_response_with_structured_output(
151
+ prompt=prompt,
152
+ response_schema=llm_extraction.build_model(),
153
+ screenshot=screenshot,
154
+ system_instruction=system_instruction,
155
+ )
156
+ response_dict = response.model_dump()
157
+ output_data = OutputData(
158
+ unique_identifier=unique_identifier, json_data=response_dict
159
+ )
160
+
161
+ logger.debug(f"Response: {response_dict}")
162
+
163
+ memory.token_usage += token_usage
164
+ memory.variables.output_data.append(output_data)
165
+
166
+ if llm_extraction.output_variable_names is not None:
167
+ for output_variable_name in llm_extraction.output_variable_names:
168
+ v = response_dict[output_variable_name]
169
+ if isinstance(v, list):
170
+ memory.variables.generated_variables[output_variable_name] = v
171
+ elif (
172
+ isinstance(v, str)
173
+ or isinstance(v, int)
174
+ or isinstance(v, float)
175
+ or isinstance(v, bool)
176
+ ):
177
+ memory.variables.generated_variables[output_variable_name] = [v]
178
+ else:
179
+ raise ValueError(
180
+ f"Output variable {output_variable_name} must be a string, int, float, bool, or a list of strings, ints, floats, or bools. Extracted values: {response_dict[output_variable_name]}"
181
+ )
182
+ return output_data
183
+
184
+
185
+ async def handle_network_call_extraction(
186
+ network_call_extraction: NetworkCallExtraction,
187
+ memory: Memory,
188
+ browser: Browser,
189
+ task: Task,
190
+ unique_identifier: str | None = None,
191
+ ):
192
+
193
+ for network_call in browser.network_calls:
194
+ if network_call_extraction.url_pattern not in network_call.url:
195
+ continue
196
+
197
+ if network_call_extraction.download_from == "request" and isinstance(
198
+ network_call, NetworkRequest
199
+ ):
200
+ await download_request(
201
+ network_call, network_call_extraction.download_filename, task, memory
202
+ )
203
+
204
+ if (
205
+ network_call_extraction.extract_from == "request"
206
+ and isinstance(network_call, NetworkRequest)
207
+ ) or (
208
+ network_call_extraction.extract_from == "response"
209
+ and isinstance(network_call, NetworkResponse)
210
+ ):
211
+ memory.variables.output_data.append(
212
+ OutputData(
213
+ unique_identifier=unique_identifier,
214
+ json_data=network_call.model_dump(include={"body"}),
215
+ )
216
+ )
217
+
218
+
219
+ async def download_request(
220
+ network_call: NetworkRequest, download_filename: str, task: Task, memory: Memory
221
+ ):
222
+ try:
223
+ async with httpx.AsyncClient(follow_redirects=True) as client:
224
+ response = await client.request(
225
+ network_call.method,
226
+ network_call.url,
227
+ headers=network_call.headers,
228
+ content=network_call.body, # not data=
229
+ )
230
+
231
+ response.raise_for_status()
232
+
233
+ # Save raw response to PDF
234
+ download_path = task.downloads_directory / download_filename
235
+ async with aiofiles.open(download_path, "wb") as f:
236
+ await f.write(response.content)
237
+
238
+ memory.downloads.append(download_path)
239
+ except Exception as e:
240
+ logger.error(f"Failed to download request: {e}, {traceback.format_exc()}")
@@ -0,0 +1,254 @@
1
+ import asyncio
2
+ import logging
3
+ from datetime import datetime, timezone
4
+
5
+ import aiofiles
6
+
7
+ from optexity.exceptions import AssertLocatorPresenceException
8
+ from optexity.inference.agents.error_handler.error_handler import ErrorHandlerAgent
9
+ from optexity.inference.core.interaction.handle_agentic_task import handle_agentic_task
10
+ from optexity.inference.core.interaction.handle_check import (
11
+ handle_check_element,
12
+ handle_uncheck_element,
13
+ )
14
+ from optexity.inference.core.interaction.handle_click import handle_click_element
15
+ from optexity.inference.core.interaction.handle_input import handle_input_text
16
+ from optexity.inference.core.interaction.handle_keypress import handle_key_press
17
+ from optexity.inference.core.interaction.handle_select import handle_select_option
18
+ from optexity.inference.core.interaction.handle_upload import handle_upload_file
19
+ from optexity.inference.infra.browser import Browser
20
+ from optexity.schema.actions.interaction_action import (
21
+ CloseOverlayPopupAction,
22
+ CloseTabsUntil,
23
+ DownloadUrlAsPdfAction,
24
+ GoBackAction,
25
+ GoToUrlAction,
26
+ InteractionAction,
27
+ )
28
+ from optexity.schema.memory import BrowserState, Memory, OutputData
29
+ from optexity.schema.task import Task
30
+
31
+ error_handler_agent = ErrorHandlerAgent()
32
+
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ async def run_interaction_action(
38
+ interaction_action: InteractionAction,
39
+ task: Task,
40
+ memory: Memory,
41
+ browser: Browser,
42
+ retries_left: int,
43
+ ):
44
+ if retries_left <= 0:
45
+ return
46
+
47
+ logger.debug(
48
+ f"---------Running interaction action {interaction_action.model_dump_json(exclude_none=True)}---------"
49
+ )
50
+
51
+ try:
52
+ memory.automation_state.start_2fa_time = datetime.now(timezone.utc)
53
+ if interaction_action.click_element:
54
+ await handle_click_element(
55
+ interaction_action.click_element,
56
+ task,
57
+ memory,
58
+ browser,
59
+ interaction_action.max_timeout_seconds_per_try,
60
+ interaction_action.max_tries,
61
+ )
62
+ elif interaction_action.input_text:
63
+ await handle_input_text(
64
+ interaction_action.input_text,
65
+ task,
66
+ memory,
67
+ browser,
68
+ interaction_action.max_timeout_seconds_per_try,
69
+ interaction_action.max_tries,
70
+ )
71
+ elif interaction_action.select_option:
72
+ await handle_select_option(
73
+ interaction_action.select_option,
74
+ task,
75
+ memory,
76
+ browser,
77
+ interaction_action.max_timeout_seconds_per_try,
78
+ interaction_action.max_tries,
79
+ )
80
+ elif interaction_action.check:
81
+ await handle_check_element(
82
+ interaction_action.check,
83
+ task,
84
+ memory,
85
+ browser,
86
+ interaction_action.max_timeout_seconds_per_try,
87
+ interaction_action.max_tries,
88
+ )
89
+ elif interaction_action.uncheck:
90
+ await handle_uncheck_element(
91
+ interaction_action.uncheck,
92
+ task,
93
+ memory,
94
+ browser,
95
+ interaction_action.max_timeout_seconds_per_try,
96
+ interaction_action.max_tries,
97
+ )
98
+ elif interaction_action.go_back:
99
+ await handle_go_back(interaction_action.go_back, memory, browser)
100
+ elif interaction_action.download_url_as_pdf:
101
+ await handle_download_url_as_pdf(
102
+ interaction_action.download_url_as_pdf, task, memory, browser
103
+ )
104
+ elif interaction_action.agentic_task:
105
+ await handle_agentic_task(
106
+ interaction_action.agentic_task, task, memory, browser
107
+ )
108
+ elif interaction_action.close_overlay_popup:
109
+ await handle_agentic_task(
110
+ interaction_action.close_overlay_popup, task, memory, browser
111
+ )
112
+ elif interaction_action.go_to_url:
113
+ await handle_go_to_url(interaction_action.go_to_url, task, memory, browser)
114
+ elif interaction_action.upload_file:
115
+ await handle_upload_file(
116
+ interaction_action.upload_file,
117
+ task,
118
+ memory,
119
+ browser,
120
+ interaction_action.max_timeout_seconds_per_try,
121
+ interaction_action.max_tries,
122
+ )
123
+ elif interaction_action.close_current_tab:
124
+ await browser.close_current_tab()
125
+ elif interaction_action.switch_tab:
126
+ await browser.switch_tab(interaction_action.switch_tab.tab_index)
127
+ elif interaction_action.close_tabs_until:
128
+ await handle_close_tabs_until(
129
+ interaction_action.close_tabs_until, task, memory, browser
130
+ )
131
+ elif interaction_action.key_press:
132
+ await handle_key_press(interaction_action.key_press, memory, browser)
133
+ except AssertLocatorPresenceException as e:
134
+ await handle_assert_locator_presence_error(
135
+ e, interaction_action, task, memory, browser, retries_left
136
+ )
137
+
138
+
139
+ async def handle_close_tabs_until(
140
+ close_tabs_until_action: CloseTabsUntil,
141
+ task: Task,
142
+ memory: Memory,
143
+ browser: Browser,
144
+ ):
145
+
146
+ while True:
147
+ page = await browser.get_current_page()
148
+ if page is None:
149
+ return
150
+
151
+ if close_tabs_until_action.matching_url is not None:
152
+ if close_tabs_until_action.matching_url in page.url:
153
+ break
154
+ elif close_tabs_until_action.tab_index is not None:
155
+ if len(browser.context.pages) == close_tabs_until_action.tab_index + 1:
156
+ break
157
+
158
+ await browser.close_current_tab()
159
+
160
+
161
+ async def handle_go_to_url(
162
+ go_to_url_action: GoToUrlAction, task: Task, memory: Memory, browser: Browser
163
+ ):
164
+ await browser.go_to_url(go_to_url_action.url)
165
+
166
+
167
+ async def handle_go_back(
168
+ go_back_action: GoBackAction, memory: Memory, browser: Browser
169
+ ):
170
+ page = await browser.get_current_page()
171
+ if page is None:
172
+ return
173
+ await page.go_back()
174
+
175
+
176
+ async def handle_download_url_as_pdf(
177
+ download_url_as_pdf_action: DownloadUrlAsPdfAction,
178
+ task: Task,
179
+ memory: Memory,
180
+ browser: Browser,
181
+ ):
182
+ if download_url_as_pdf_action.url is not None:
183
+ pdf_url = download_url_as_pdf_action.url
184
+ else:
185
+ pdf_url = await browser.get_current_page_url()
186
+
187
+ if pdf_url is None:
188
+ logger.error("No PDF URL found for current page")
189
+ return
190
+ download_path = (
191
+ task.downloads_directory / download_url_as_pdf_action.download_filename
192
+ )
193
+
194
+ resp = await browser.context.request.get(pdf_url)
195
+
196
+ if not resp.ok:
197
+ logger.error(f"Failed to download PDF: {resp.status}")
198
+ return
199
+
200
+ content = await resp.body()
201
+ async with aiofiles.open(download_path, "wb") as f:
202
+ await f.write(content)
203
+
204
+ memory.downloads.append(download_path)
205
+
206
+
207
+ async def handle_assert_locator_presence_error(
208
+ error: AssertLocatorPresenceException,
209
+ interaction_action: InteractionAction,
210
+ task: Task,
211
+ memory: Memory,
212
+ browser: Browser,
213
+ retries_left: int,
214
+ ):
215
+ logger.debug(f"Handling assert locator presence error: {error.command}")
216
+ if retries_left > 1:
217
+ browser_state_summary = await browser.get_browser_state_summary()
218
+ memory.browser_states[-1] = BrowserState(
219
+ url=browser_state_summary.url,
220
+ screenshot=browser_state_summary.screenshot,
221
+ title=browser_state_summary.title,
222
+ axtree=browser_state_summary.dom_state.llm_representation(),
223
+ )
224
+ final_prompt, response, token_usage = error_handler_agent.classify_error(
225
+ error.command, memory.browser_states[-1].screenshot
226
+ )
227
+ memory.token_usage += token_usage
228
+
229
+ if response.error_type == "website_not_loaded":
230
+ await asyncio.sleep(5)
231
+ await run_interaction_action(
232
+ interaction_action, task, memory, browser, retries_left - 1
233
+ )
234
+ elif response.error_type == "overlay_popup_blocking":
235
+ close_overlay_popup_action = CloseOverlayPopupAction()
236
+ await handle_agentic_task(close_overlay_popup_action, task, memory, browser)
237
+ await run_interaction_action(
238
+ interaction_action, task, memory, browser, retries_left - 1
239
+ )
240
+ elif response.error_type == "fatal_error":
241
+ logger.error(
242
+ f"Fatal error running node {memory.automation_state.step_index} after {retries_left} retries: {error.original_error}. Error: {response.detailed_reason}"
243
+ )
244
+ memory.variables.output_data.append(
245
+ OutputData(unique_identifier="error", text=response.detailed_reason)
246
+ )
247
+ raise Exception(
248
+ f"Fatal error running node {memory.automation_state.step_index} after {retries_left} retries: {error.original_error}. Final reason: {response.detailed_reason}"
249
+ )
250
+ else:
251
+ logger.error(
252
+ f"Error running node {memory.automation_state.step_index} after {retries_left} retries: {error.original_error}"
253
+ )
254
+ raise error
@@ -0,0 +1,20 @@
1
+ import logging
2
+
3
+ from optexity.inference.infra.browser import Browser
4
+ from optexity.schema.actions.misc_action import PythonScriptAction
5
+ from optexity.schema.memory import Memory
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ async def run_python_script_action(
11
+ python_script_action: PythonScriptAction, memory: Memory, browser: Browser
12
+ ):
13
+ local_vars = {}
14
+ exec(python_script_action.execution_code, {}, local_vars)
15
+
16
+ # Get the function
17
+ code_fn = local_vars["code_fn"]
18
+
19
+ page = await browser.get_current_page()
20
+ await code_fn(page)
@@ -0,0 +1,120 @@
1
+ import asyncio
2
+ import logging
3
+ from datetime import timedelta
4
+ from urllib.parse import urljoin
5
+
6
+ import httpx
7
+
8
+ from optexity.inference.agents.two_fa_extraction.two_fa_extraction import (
9
+ TwoFAExtraction,
10
+ )
11
+ from optexity.schema.actions.two_fa_action import (
12
+ EmailTwoFAAction,
13
+ SlackTwoFAAction,
14
+ TwoFAAction,
15
+ )
16
+ from optexity.schema.inference import (
17
+ FetchEmailMessagesRequest,
18
+ FetchMessagesResponse,
19
+ FetchSlackMessagesRequest,
20
+ )
21
+ from optexity.schema.memory import Memory
22
+ from optexity.utils.settings import settings
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ two_fa_extraction_agent = TwoFAExtraction()
27
+
28
+
29
+ async def run_two_fa_action(two_fa_action: TwoFAAction, memory: Memory):
30
+ logger.debug(
31
+ f"---------Running 2fa action {two_fa_action.model_dump_json()}---------"
32
+ )
33
+
34
+ elapsed = 0
35
+ messages = None
36
+
37
+ while elapsed < two_fa_action.max_wait_time:
38
+ messages = await fetch_messages(
39
+ two_fa_action.action, memory, two_fa_action.max_wait_time
40
+ )
41
+ if messages and len(messages) > 0:
42
+ final_prompt, response, token_usage = two_fa_extraction_agent.extract_code(
43
+ two_fa_action.instructions, messages
44
+ )
45
+ memory.token_usage += token_usage
46
+ code = None
47
+ if response.code is not None:
48
+ if isinstance(response.code, str):
49
+ code = response.code
50
+ elif isinstance(response.code, list):
51
+ if len(response.code) > 0:
52
+ raise ValueError(f"Multiple 2FA codes found, {response.code}")
53
+ else:
54
+ code = response.code[0]
55
+
56
+ if code is not None:
57
+ logger.debug(
58
+ f"2FA code {code} found after {elapsed} seconds from {messages}"
59
+ )
60
+ break
61
+ logger.debug(
62
+ f"No 2FA code found in messages, {messages}, waiting for {two_fa_action.check_interval} seconds"
63
+ )
64
+ else:
65
+ logger.debug(
66
+ f"No messages found for 2FA code after {elapsed} seconds, waiting for {two_fa_action.check_interval} seconds"
67
+ )
68
+
69
+ await asyncio.sleep(two_fa_action.check_interval)
70
+ elapsed += two_fa_action.check_interval
71
+
72
+ memory.automation_state.start_2fa_time = None
73
+ if code is None:
74
+ raise ValueError("2FA code not found")
75
+
76
+ memory.variables.generated_variables[two_fa_action.output_variable_name] = [code]
77
+
78
+ return code
79
+
80
+
81
+ async def fetch_messages(
82
+ action: EmailTwoFAAction | SlackTwoFAAction,
83
+ memory: Memory,
84
+ max_wait_time: float,
85
+ ):
86
+
87
+ start_2fa_time = memory.automation_state.start_2fa_time
88
+ end_2fa_time = memory.automation_state.start_2fa_time + timedelta(
89
+ seconds=max_wait_time
90
+ )
91
+
92
+ headers = {"x-api-key": settings.API_KEY}
93
+
94
+ if isinstance(action, EmailTwoFAAction):
95
+ url = urljoin(settings.SERVER_URL, settings.FETCH_EMAIL_MESSAGES_ENDPOINT)
96
+ body = FetchEmailMessagesRequest(
97
+ receiver_email_address=action.receiver_email_address,
98
+ sender_email_address=action.sender_email_address,
99
+ start_2fa_time=start_2fa_time,
100
+ end_2fa_time=end_2fa_time,
101
+ )
102
+ elif isinstance(action, SlackTwoFAAction):
103
+ url = urljoin(settings.SERVER_URL, settings.FETCH_SLACK_MESSAGES_ENDPOINT)
104
+ body = FetchSlackMessagesRequest(
105
+ slack_workspace_domain=action.slack_workspace_domain,
106
+ channel_name=action.channel_name,
107
+ sender_name=action.sender_name,
108
+ start_2fa_time=start_2fa_time,
109
+ end_2fa_time=end_2fa_time,
110
+ )
111
+
112
+ async with httpx.AsyncClient(timeout=30.0) as client:
113
+
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())
119
+
120
+ return response_data.messages
File without changes
File without changes