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.
Files changed (77) hide show
  1. optexity/cli.py +1 -1
  2. optexity/examples/__init__.py +0 -0
  3. optexity/examples/add_example.py +88 -0
  4. optexity/examples/download_pdf_url.py +29 -0
  5. optexity/examples/extract_price_stockanalysis.py +44 -0
  6. optexity/examples/file_upload.py +59 -0
  7. optexity/examples/i94.py +126 -0
  8. optexity/examples/i94_travel_history.py +126 -0
  9. optexity/examples/peachstate_medicaid.py +201 -0
  10. optexity/examples/supabase_login.py +75 -0
  11. optexity/inference/__init__.py +0 -0
  12. optexity/inference/agents/__init__.py +0 -0
  13. optexity/inference/agents/error_handler/__init__.py +0 -0
  14. optexity/inference/agents/error_handler/error_handler.py +39 -0
  15. optexity/inference/agents/error_handler/prompt.py +60 -0
  16. optexity/inference/agents/index_prediction/__init__.py +0 -0
  17. optexity/inference/agents/index_prediction/action_prediction_locator_axtree.py +45 -0
  18. optexity/inference/agents/index_prediction/prompt.py +14 -0
  19. optexity/inference/agents/select_value_prediction/__init__.py +0 -0
  20. optexity/inference/agents/select_value_prediction/prompt.py +20 -0
  21. optexity/inference/agents/select_value_prediction/select_value_prediction.py +39 -0
  22. optexity/inference/agents/two_fa_extraction/__init__.py +0 -0
  23. optexity/inference/agents/two_fa_extraction/prompt.py +23 -0
  24. optexity/inference/agents/two_fa_extraction/two_fa_extraction.py +47 -0
  25. optexity/inference/child_process.py +251 -0
  26. optexity/inference/core/__init__.py +0 -0
  27. optexity/inference/core/interaction/__init__.py +0 -0
  28. optexity/inference/core/interaction/handle_agentic_task.py +79 -0
  29. optexity/inference/core/interaction/handle_check.py +57 -0
  30. optexity/inference/core/interaction/handle_click.py +79 -0
  31. optexity/inference/core/interaction/handle_command.py +261 -0
  32. optexity/inference/core/interaction/handle_input.py +76 -0
  33. optexity/inference/core/interaction/handle_keypress.py +16 -0
  34. optexity/inference/core/interaction/handle_select.py +109 -0
  35. optexity/inference/core/interaction/handle_select_utils.py +132 -0
  36. optexity/inference/core/interaction/handle_upload.py +59 -0
  37. optexity/inference/core/interaction/utils.py +81 -0
  38. optexity/inference/core/logging.py +406 -0
  39. optexity/inference/core/run_assertion.py +55 -0
  40. optexity/inference/core/run_automation.py +463 -0
  41. optexity/inference/core/run_extraction.py +240 -0
  42. optexity/inference/core/run_interaction.py +254 -0
  43. optexity/inference/core/run_python_script.py +20 -0
  44. optexity/inference/core/run_two_fa.py +120 -0
  45. optexity/inference/core/two_factor_auth/__init__.py +0 -0
  46. optexity/inference/infra/__init__.py +0 -0
  47. optexity/inference/infra/browser.py +455 -0
  48. optexity/inference/infra/browser_extension.py +20 -0
  49. optexity/inference/models/__init__.py +22 -0
  50. optexity/inference/models/gemini.py +113 -0
  51. optexity/inference/models/human.py +20 -0
  52. optexity/inference/models/llm_model.py +210 -0
  53. optexity/inference/run_local.py +200 -0
  54. optexity/schema/__init__.py +0 -0
  55. optexity/schema/actions/__init__.py +0 -0
  56. optexity/schema/actions/assertion_action.py +66 -0
  57. optexity/schema/actions/extraction_action.py +143 -0
  58. optexity/schema/actions/interaction_action.py +330 -0
  59. optexity/schema/actions/misc_action.py +18 -0
  60. optexity/schema/actions/prompts.py +27 -0
  61. optexity/schema/actions/two_fa_action.py +24 -0
  62. optexity/schema/automation.py +432 -0
  63. optexity/schema/callback.py +16 -0
  64. optexity/schema/inference.py +87 -0
  65. optexity/schema/memory.py +100 -0
  66. optexity/schema/task.py +212 -0
  67. optexity/schema/token_usage.py +48 -0
  68. optexity/utils/__init__.py +0 -0
  69. optexity/utils/settings.py +54 -0
  70. optexity/utils/utils.py +76 -0
  71. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/METADATA +20 -36
  72. optexity-0.1.4.dist-info/RECORD +80 -0
  73. optexity-0.1.2.dist-info/RECORD +0 -11
  74. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/WHEEL +0 -0
  75. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/entry_points.txt +0 -0
  76. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/licenses/LICENSE +0 -0
  77. {optexity-0.1.2.dist-info → optexity-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,81 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Callable
4
+
5
+ import aiofiles
6
+
7
+ from optexity.inference.agents.index_prediction.action_prediction_locator_axtree import (
8
+ ActionPredictionLocatorAxtree,
9
+ )
10
+ from optexity.inference.infra.browser import Browser
11
+ from optexity.schema.memory import BrowserState, Memory
12
+ from optexity.schema.task import Task
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ index_prediction_agent = ActionPredictionLocatorAxtree()
18
+
19
+
20
+ async def get_index_from_prompt(
21
+ memory: Memory, prompt_instructions: str, browser: Browser
22
+ ):
23
+ browser_state_summary = await browser.get_browser_state_summary()
24
+ memory.browser_states[-1] = BrowserState(
25
+ url=browser_state_summary.url,
26
+ screenshot=browser_state_summary.screenshot,
27
+ title=browser_state_summary.title,
28
+ axtree=browser_state_summary.dom_state.llm_representation(),
29
+ )
30
+
31
+ try:
32
+ final_prompt, response, token_usage = index_prediction_agent.predict_action(
33
+ prompt_instructions, memory.browser_states[-1].axtree
34
+ )
35
+ memory.token_usage += token_usage
36
+ memory.browser_states[-1].final_prompt = final_prompt
37
+ memory.browser_states[-1].llm_response = response.model_dump()
38
+
39
+ return response.index
40
+ except Exception as e:
41
+ logger.error(f"Error in get_index_from_prompt: {e}")
42
+
43
+
44
+ async def handle_download(
45
+ func: Callable, memory: Memory, browser: Browser, task: Task, download_filename: str
46
+ ):
47
+ page = await browser.get_current_page()
48
+ if page is None:
49
+ logger.error("No page found for current page")
50
+ return
51
+ download_path: Path = task.downloads_directory / download_filename
52
+ async with page.expect_download() as download_info:
53
+ await func()
54
+ download = await download_info.value
55
+
56
+ if download:
57
+ temp_path = await download.path()
58
+ async with memory.download_lock:
59
+ memory.raw_downloads[temp_path] = (True, None)
60
+
61
+ await download.save_as(download_path)
62
+ memory.downloads.append(download_path)
63
+ await clean_download(download_path)
64
+ else:
65
+ logger.error("No download found")
66
+
67
+
68
+ async def clean_download(download_path: Path):
69
+
70
+ if download_path.suffix == ".csv":
71
+ # Read full file
72
+ async with aiofiles.open(download_path, "r", encoding="utf-8") as f:
73
+ content = await f.read()
74
+ # Remove everything between <script>...</script> (multiline safe)
75
+
76
+ if "</script>" in content:
77
+ clean_content = content.split("</script>")[-1]
78
+
79
+ # Write cleaned CSV back
80
+ async with aiofiles.open(download_path, "w", encoding="utf-8") as f:
81
+ await f.write(clean_content)
@@ -0,0 +1,406 @@
1
+ import base64
2
+ import io
3
+ import json
4
+ import logging
5
+ import shutil
6
+ import tarfile
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from urllib.parse import urljoin
10
+
11
+ import aiofiles
12
+ import httpx
13
+
14
+ from optexity.schema.automation import ActionNode
15
+ from optexity.schema.memory import Memory
16
+ from optexity.schema.task import Task
17
+ from optexity.schema.token_usage import TokenUsage
18
+ from optexity.utils.settings import settings
19
+ from optexity.utils.utils import save_screenshot
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def create_tar_in_memory(directory: Path | str, name: str) -> io.BytesIO:
25
+ if isinstance(directory, str):
26
+ directory = Path(directory)
27
+ tar_bytes = io.BytesIO()
28
+ with tarfile.open(fileobj=tar_bytes, mode="w:gz") as tar:
29
+ tar.add(directory, arcname=name)
30
+ tar_bytes.seek(0) # rewind to start
31
+ return tar_bytes
32
+
33
+
34
+ async def start_task_in_server(task: Task):
35
+ try:
36
+ task.started_at = datetime.now(timezone.utc)
37
+ task.status = "running"
38
+
39
+ url = urljoin(settings.SERVER_URL, settings.START_TASK_ENDPOINT)
40
+ headers = {"x-api-key": task.api_key}
41
+ body = {
42
+ "task_id": task.task_id,
43
+ "started_at": task.started_at.isoformat(),
44
+ }
45
+ if task.allocated_at:
46
+ body["allocated_at"] = task.allocated_at.isoformat()
47
+ async with httpx.AsyncClient(timeout=30.0) as client:
48
+ response = await client.post(
49
+ url,
50
+ headers=headers,
51
+ json=body,
52
+ )
53
+
54
+ response.raise_for_status()
55
+ return response.json()
56
+ except httpx.HTTPStatusError as e:
57
+ raise ValueError(
58
+ f"Failed to start task in server: {e.response.status_code} - {e.response.text}"
59
+ )
60
+ except Exception as e:
61
+ raise ValueError(f"Failed to start task in server: {e}")
62
+
63
+
64
+ async def complete_task_in_server(
65
+ task: Task, token_usage: TokenUsage, child_process_id: int
66
+ ):
67
+ try:
68
+ task.completed_at = datetime.now(timezone.utc)
69
+
70
+ url = urljoin(settings.SERVER_URL, settings.COMPLETE_TASK_ENDPOINT)
71
+ headers = {"x-api-key": task.api_key}
72
+ body = {
73
+ "task_id": task.task_id,
74
+ "child_process_id": child_process_id,
75
+ "completed_at": task.completed_at.isoformat(),
76
+ "status": task.status,
77
+ "error": task.error,
78
+ "token_usage": token_usage.model_dump(),
79
+ }
80
+ async with httpx.AsyncClient(timeout=30.0) as client:
81
+ response = await client.post(
82
+ url,
83
+ headers=headers,
84
+ json=body,
85
+ )
86
+
87
+ response.raise_for_status()
88
+ return response.json()
89
+ except httpx.HTTPStatusError as e:
90
+ logger.error(
91
+ f"Failed to complete task in server: {e.response.status_code} - {e.response.text}"
92
+ )
93
+
94
+ except Exception as e:
95
+ logger.error(f"Failed to complete task in server: {e}")
96
+
97
+
98
+ async def save_output_data_in_server(task: Task, memory: Memory):
99
+ try:
100
+ if len(memory.variables.output_data) == 0 and memory.final_screenshot is None:
101
+ return
102
+
103
+ url = urljoin(settings.SERVER_URL, settings.SAVE_OUTPUT_DATA_ENDPOINT)
104
+ headers = {"x-api-key": task.api_key}
105
+
106
+ output_data = [
107
+ output_data.model_dump(exclude_none=True, exclude={"screenshot"})
108
+ for output_data in memory.variables.output_data
109
+ ]
110
+ output_data = [data for data in output_data if data and len(data.keys()) > 0]
111
+ body = {
112
+ "task_id": task.task_id,
113
+ "output_data": output_data,
114
+ "final_screenshot": memory.final_screenshot,
115
+ }
116
+
117
+ for_loop_status = []
118
+ for loop_status in memory.variables.for_loop_status:
119
+ loop_status = [item.model_dump(exclude_none=True) for item in loop_status]
120
+ for_loop_status.append(loop_status)
121
+
122
+ if len(for_loop_status) > 0:
123
+ body["for_loop_status"] = for_loop_status
124
+
125
+ async with httpx.AsyncClient(timeout=30.0) as client:
126
+ response = await client.post(
127
+ url,
128
+ headers=headers,
129
+ json=body,
130
+ )
131
+
132
+ response.raise_for_status()
133
+ return response.json()
134
+ except httpx.HTTPStatusError as e:
135
+ logger.error(
136
+ f"Failed to save output data in server: {e.response.status_code} - {e.response.text}"
137
+ )
138
+ except Exception as e:
139
+ logger.error(f"Failed to save output data in server: {e}")
140
+
141
+
142
+ async def save_downloads_in_server(task: Task, memory: Memory):
143
+ try:
144
+ # if len(memory.downloads) == 0:
145
+ # return
146
+
147
+ url = urljoin(settings.SERVER_URL, settings.SAVE_DOWNLOADS_ENDPOINT)
148
+ headers = {"x-api-key": task.api_key}
149
+
150
+ payload = {
151
+ "task_id": task.task_id, # form field
152
+ }
153
+
154
+ files = []
155
+ downloads = [
156
+ download
157
+ for download in task.downloads_directory.iterdir()
158
+ if download.is_file()
159
+ ]
160
+ if len(downloads) > 0:
161
+ tar_bytes = create_tar_in_memory(task.downloads_directory, task.task_id)
162
+ # add tar.gz
163
+ files.append(
164
+ (
165
+ "compressed_downloads",
166
+ (f"{task.task_id}.tar.gz", tar_bytes, "application/gzip"),
167
+ )
168
+ )
169
+
170
+ # add screenshots
171
+ for data in memory.variables.output_data:
172
+ if data.screenshot:
173
+ files.append(
174
+ (
175
+ "screenshots",
176
+ (
177
+ data.screenshot.filename,
178
+ base64.b64decode(data.screenshot.base64),
179
+ "image/png",
180
+ ),
181
+ )
182
+ )
183
+
184
+ if memory.final_screenshot:
185
+ files.append(
186
+ (
187
+ "screenshots",
188
+ (
189
+ "final_screenshot.png",
190
+ base64.b64decode(memory.final_screenshot),
191
+ "image/png",
192
+ ),
193
+ )
194
+ )
195
+
196
+ if len(files) == 0:
197
+ return
198
+
199
+ async with httpx.AsyncClient(timeout=30.0) as client:
200
+
201
+ response = await client.post(
202
+ url, headers=headers, data=payload, files=files
203
+ )
204
+
205
+ response.raise_for_status()
206
+ return response.json()
207
+ except httpx.HTTPStatusError as e:
208
+ logger.error(
209
+ f"Failed to save downloads in server: {e.response.status_code} - {e.response.text}"
210
+ )
211
+ except Exception as e:
212
+ logger.error(f"Failed to save downloads in server: {e}")
213
+
214
+
215
+ async def save_trajectory_in_server(task: Task, memory: Memory):
216
+ try:
217
+ url = urljoin(settings.SERVER_URL, settings.SAVE_TRAJECTORY_ENDPOINT)
218
+ headers = {"x-api-key": task.api_key}
219
+
220
+ data = {
221
+ "task_id": task.task_id, # form field
222
+ }
223
+
224
+ tar_bytes = create_tar_in_memory(task.task_directory, task.task_id)
225
+ files = {
226
+ "compressed_trajectory": (
227
+ f"{task.task_id}.tar.gz",
228
+ tar_bytes,
229
+ "application/gzip",
230
+ )
231
+ }
232
+ async with httpx.AsyncClient(timeout=30.0) as client:
233
+
234
+ response = await client.post(url, headers=headers, data=data, files=files)
235
+
236
+ response.raise_for_status()
237
+ return response.json()
238
+ except httpx.HTTPStatusError as e:
239
+ logger.error(
240
+ f"Failed to save trajectory in server: {e.response.status_code} - {e.response.text}"
241
+ )
242
+ except Exception as e:
243
+ logger.error(f"Failed to save trajectory in server: {e}")
244
+
245
+
246
+ async def initiate_callback(task: Task):
247
+
248
+ if settings.DEPLOYMENT == "dev" and settings.LOCAL_CALLBACK_URL is not None:
249
+ logger.info("initiating local callback")
250
+ callback_data = None
251
+ try:
252
+ url = urljoin(settings.SERVER_URL, settings.GET_CALLBACK_DATA_ENDPOINT)
253
+ headers = {"x-api-key": task.api_key}
254
+ data = {
255
+ "task_id": task.task_id,
256
+ "endpoint_name": task.endpoint_name,
257
+ }
258
+ async with httpx.AsyncClient(timeout=30.0) as client:
259
+ response = await client.post(url, headers=headers, json=data)
260
+ response.raise_for_status()
261
+ callback_data = response.json()["data"]
262
+ except Exception as e:
263
+ logger.error(f"Failed to get callback data: {e}")
264
+ return
265
+
266
+ if callback_data is None:
267
+ return
268
+
269
+ try:
270
+ async with httpx.AsyncClient(timeout=30.0) as client:
271
+ response = await client.post(
272
+ settings.LOCAL_CALLBACK_URL, json=callback_data
273
+ )
274
+ response.raise_for_status()
275
+ except Exception as e:
276
+ logger.error(f"Failed to initiate local callback: {e}")
277
+ return
278
+
279
+ return
280
+
281
+ try:
282
+ logger.info("initiating callback")
283
+ if task.callback_url is None:
284
+ return
285
+
286
+ url = urljoin(settings.SERVER_URL, settings.INITIATE_CALLBACK_ENDPOINT)
287
+ headers = {"x-api-key": task.api_key}
288
+
289
+ data = {
290
+ "task_id": task.task_id,
291
+ "endpoint_name": task.endpoint_name,
292
+ "callback_url": task.callback_url.model_dump(),
293
+ }
294
+
295
+ async with httpx.AsyncClient(timeout=30.0) as client:
296
+
297
+ response = await client.post(url, headers=headers, json=data)
298
+
299
+ response.raise_for_status()
300
+ return response.json()
301
+ except httpx.HTTPStatusError as e:
302
+ logger.error(
303
+ f"Failed to save trajectory in server: {e.response.status_code} - {e.response.text}"
304
+ )
305
+ except Exception as e:
306
+ logger.error(f"Failed to save trajectory in server: {e}")
307
+
308
+
309
+ async def save_latest_memory_state_locally(
310
+ task: Task, memory: Memory, node: ActionNode | None
311
+ ):
312
+
313
+ try:
314
+ browser_state = memory.browser_states[-1]
315
+ automation_state = memory.automation_state
316
+ step_directory = (
317
+ task.logs_directory / f"step_{str(automation_state.step_index)}"
318
+ )
319
+ step_directory.mkdir(parents=True, exist_ok=True)
320
+
321
+ if browser_state.screenshot:
322
+ save_screenshot(browser_state.screenshot, step_directory / "screenshot.png")
323
+ else:
324
+ logger.warning(
325
+ "No screenshot found for step %s", automation_state.step_index
326
+ )
327
+
328
+ state_dict = {
329
+ "title": browser_state.title,
330
+ "url": browser_state.url,
331
+ "step_index": automation_state.step_index,
332
+ "try_index": automation_state.try_index,
333
+ "downloaded_files": [
334
+ downloaded_file.name for downloaded_file in memory.downloads
335
+ ],
336
+ "token_usage": memory.token_usage.model_dump(),
337
+ }
338
+
339
+ async with aiofiles.open(step_directory / "state.json", "w") as f:
340
+ await f.write(json.dumps(state_dict, indent=4))
341
+
342
+ if browser_state.axtree:
343
+ async with aiofiles.open(step_directory / "axtree.txt", "w") as f:
344
+ await f.write(browser_state.axtree)
345
+
346
+ if browser_state.final_prompt:
347
+ async with aiofiles.open(step_directory / "final_prompt.txt", "w") as f:
348
+ await f.write(browser_state.final_prompt)
349
+
350
+ if browser_state.llm_response:
351
+ async with aiofiles.open(step_directory / "llm_response.json", "w") as f:
352
+ await f.write(json.dumps(browser_state.llm_response, indent=4))
353
+
354
+ if node:
355
+ async with aiofiles.open(step_directory / "action_node.json", "w") as f:
356
+ await f.write(
357
+ json.dumps(
358
+ node.model_dump(exclude_none=True, exclude_defaults=True),
359
+ indent=4,
360
+ )
361
+ )
362
+
363
+ async with aiofiles.open(step_directory / "input_parameters.json", "w") as f:
364
+ await f.write(json.dumps(task.input_parameters, indent=4))
365
+
366
+ async with aiofiles.open(step_directory / "secure_parameters.json", "w") as f:
367
+ await f.write(json.dumps(task.secure_parameters, indent=4))
368
+
369
+ async with aiofiles.open(step_directory / "generated_variables.json", "w") as f:
370
+ await f.write(json.dumps(memory.variables.generated_variables, indent=4))
371
+
372
+ async with aiofiles.open(step_directory / "output_data.json", "w") as f:
373
+ await f.write(
374
+ json.dumps(
375
+ [
376
+ output_data.model_dump(
377
+ exclude_none=True,
378
+ exclude={"screenshot"},
379
+ exclude_defaults=True,
380
+ )
381
+ for output_data in memory.variables.output_data
382
+ ],
383
+ indent=4,
384
+ )
385
+ )
386
+
387
+ for output_data in memory.variables.output_data:
388
+ if output_data.screenshot:
389
+ async with aiofiles.open(
390
+ step_directory
391
+ / f"screenshot_{output_data.screenshot.filename}.png",
392
+ "wb",
393
+ ) as f:
394
+ await f.write(base64.b64decode(output_data.screenshot.base64))
395
+ except Exception as e:
396
+ logger.error(f"Failed to save latest memory state locally: {e}")
397
+
398
+
399
+ async def delete_local_data(task: Task):
400
+ try:
401
+ if settings.DEPLOYMENT == "dev" or task.task_directory is None:
402
+ return
403
+
404
+ shutil.rmtree(task.task_directory, ignore_errors=True)
405
+ except Exception as e:
406
+ logger.error(f"Failed to delete local data: {e}")
@@ -0,0 +1,55 @@
1
+ import logging
2
+ from copy import deepcopy
3
+
4
+ from optexity.inference.core.run_extraction import handle_llm_extraction
5
+ from optexity.inference.infra.browser import Browser
6
+ from optexity.inference.models import GeminiModels, get_llm_model
7
+ from optexity.schema.actions.assertion_action import AssertionAction, LLMAssertion
8
+ from optexity.schema.memory import Memory
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ llm_model = get_llm_model(GeminiModels.GEMINI_2_5_FLASH, True)
13
+
14
+
15
+ async def run_assertion_action(
16
+ assertion_action: AssertionAction, memory: Memory, browser: Browser
17
+ ):
18
+ logger.debug(
19
+ f"---------Running assertion action {assertion_action.model_dump_json()}---------"
20
+ )
21
+
22
+ if assertion_action.llm:
23
+ await handle_llm_assertion(assertion_action.llm, memory, browser)
24
+ elif assertion_action.network_call:
25
+ raise ValueError("Network call assertions are not supported yet")
26
+ # await handle_network_call_assertion(
27
+ # assertion_action.network_call, memory, browser
28
+ # )
29
+ elif assertion_action.python_script:
30
+ raise ValueError("Python script assertions are not supported yet")
31
+ # await handle_python_script_assertion(
32
+ # assertion_action.python_script, memory, browser
33
+ # )
34
+
35
+
36
+ async def handle_llm_assertion(
37
+ llm_assertion: LLMAssertion, memory: Memory, browser: Browser
38
+ ):
39
+ extra_instruction = """You are a helpful assistant that verifies if the condition is met.
40
+ Use the info supplied below to verify the condition.
41
+ The assertion_reason should be a short explanation of why the condition was met or not met.
42
+ The assertion_result should be True if the condition is met, False otherwise.
43
+ """
44
+ llm_assertion_new = deepcopy(llm_assertion)
45
+ llm_assertion_new.extraction_instructions = (
46
+ extra_instruction + "\n" + llm_assertion_new.extraction_instructions
47
+ )
48
+ output_data = await handle_llm_extraction(llm_assertion_new, memory, browser)
49
+
50
+ if output_data.json_data["assertion_result"]:
51
+ return True
52
+ else:
53
+ raise AssertionError(
54
+ f"Assertion failed on node {memory.automation_state.step_index}: {output_data.json_data['assertion_reason']}"
55
+ )