sentienceapi 0.95.0__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.

Potentially problematic release.


This version of sentienceapi might be problematic. Click here for more details.

Files changed (82) hide show
  1. sentience/__init__.py +253 -0
  2. sentience/_extension_loader.py +195 -0
  3. sentience/action_executor.py +215 -0
  4. sentience/actions.py +1020 -0
  5. sentience/agent.py +1181 -0
  6. sentience/agent_config.py +46 -0
  7. sentience/agent_runtime.py +424 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +108 -0
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +343 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +427 -0
  21. sentience/base_agent.py +196 -0
  22. sentience/browser.py +1215 -0
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cli.py +130 -0
  26. sentience/cloud_tracing.py +807 -0
  27. sentience/constants.py +6 -0
  28. sentience/conversational_agent.py +543 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +188 -0
  31. sentience/extension/background.js +104 -0
  32. sentience/extension/content.js +161 -0
  33. sentience/extension/injected_api.js +914 -0
  34. sentience/extension/manifest.json +36 -0
  35. sentience/extension/pkg/sentience_core.d.ts +51 -0
  36. sentience/extension/pkg/sentience_core.js +323 -0
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  39. sentience/extension/release.json +115 -0
  40. sentience/formatting.py +15 -0
  41. sentience/generator.py +202 -0
  42. sentience/inspector.py +367 -0
  43. sentience/llm_interaction_handler.py +191 -0
  44. sentience/llm_provider.py +875 -0
  45. sentience/llm_provider_utils.py +120 -0
  46. sentience/llm_response_builder.py +153 -0
  47. sentience/models.py +846 -0
  48. sentience/ordinal.py +280 -0
  49. sentience/overlay.py +222 -0
  50. sentience/protocols.py +228 -0
  51. sentience/query.py +303 -0
  52. sentience/read.py +188 -0
  53. sentience/recorder.py +589 -0
  54. sentience/schemas/trace_v1.json +335 -0
  55. sentience/screenshot.py +100 -0
  56. sentience/sentience_methods.py +86 -0
  57. sentience/snapshot.py +706 -0
  58. sentience/snapshot_diff.py +126 -0
  59. sentience/text_search.py +262 -0
  60. sentience/trace_event_builder.py +148 -0
  61. sentience/trace_file_manager.py +197 -0
  62. sentience/trace_indexing/__init__.py +27 -0
  63. sentience/trace_indexing/index_schema.py +199 -0
  64. sentience/trace_indexing/indexer.py +414 -0
  65. sentience/tracer_factory.py +322 -0
  66. sentience/tracing.py +449 -0
  67. sentience/utils/__init__.py +40 -0
  68. sentience/utils/browser.py +46 -0
  69. sentience/utils/element.py +257 -0
  70. sentience/utils/formatting.py +59 -0
  71. sentience/utils.py +296 -0
  72. sentience/verification.py +380 -0
  73. sentience/visual_agent.py +2058 -0
  74. sentience/wait.py +139 -0
  75. sentienceapi-0.95.0.dist-info/METADATA +984 -0
  76. sentienceapi-0.95.0.dist-info/RECORD +82 -0
  77. sentienceapi-0.95.0.dist-info/WHEEL +5 -0
  78. sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
  79. sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
  80. sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
  81. sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
  82. sentienceapi-0.95.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,126 @@
1
+ """
2
+ Snapshot comparison utilities for diff_status detection.
3
+
4
+ Implements change detection logic for the Diff Overlay feature.
5
+
6
+ Uses shared canonicalization helpers from canonicalization.py to ensure
7
+ consistent comparison behavior with trace_indexing/indexer.py.
8
+ """
9
+
10
+ from .canonicalization import bbox_changed, content_changed
11
+ from .models import Element, Snapshot
12
+
13
+
14
+ class SnapshotDiff:
15
+ """
16
+ Utility for comparing snapshots and computing diff_status for elements.
17
+
18
+ Implements the logic described in DIFF_STATUS_GAP_ANALYSIS.md:
19
+ - ADDED: Element exists in current but not in previous
20
+ - REMOVED: Element existed in previous but not in current
21
+ - MODIFIED: Element exists in both but has changed
22
+ - MOVED: Element exists in both but position changed
23
+
24
+ Uses canonicalized comparisons (normalized text, rounded bbox) to reduce
25
+ noise from insignificant changes like sub-pixel rendering differences
26
+ or whitespace variations.
27
+ """
28
+
29
+ @staticmethod
30
+ def _element_to_dict(el: Element) -> dict:
31
+ """Convert Element model to dict for canonicalization helpers."""
32
+ return {
33
+ "id": el.id,
34
+ "role": el.role,
35
+ "text": el.text,
36
+ "bbox": {
37
+ "x": el.bbox.x,
38
+ "y": el.bbox.y,
39
+ "width": el.bbox.width,
40
+ "height": el.bbox.height,
41
+ },
42
+ "visual_cues": {
43
+ "is_primary": el.visual_cues.is_primary,
44
+ "is_clickable": el.visual_cues.is_clickable,
45
+ },
46
+ }
47
+
48
+ @staticmethod
49
+ def compute_diff_status(
50
+ current: Snapshot,
51
+ previous: Snapshot | None,
52
+ ) -> list[Element]:
53
+ """
54
+ Compare current snapshot with previous and set diff_status on elements.
55
+
56
+ Uses canonicalized comparisons:
57
+ - Text is normalized (trimmed, collapsed whitespace, lowercased)
58
+ - Bbox is rounded to 2px grid to ignore sub-pixel differences
59
+
60
+ Args:
61
+ current: Current snapshot
62
+ previous: Previous snapshot (None if this is the first snapshot)
63
+
64
+ Returns:
65
+ List of elements with diff_status set (includes REMOVED elements from previous)
66
+ """
67
+ # If no previous snapshot, all current elements are ADDED
68
+ if previous is None:
69
+ result = []
70
+ for el in current.elements:
71
+ # Create a copy with diff_status set
72
+ el_dict = el.model_dump()
73
+ el_dict["diff_status"] = "ADDED"
74
+ result.append(Element(**el_dict))
75
+ return result
76
+
77
+ # Build lookup maps by element ID
78
+ current_by_id = {el.id: el for el in current.elements}
79
+ previous_by_id = {el.id: el for el in previous.elements}
80
+
81
+ current_ids = set(current_by_id.keys())
82
+ previous_ids = set(previous_by_id.keys())
83
+
84
+ result: list[Element] = []
85
+
86
+ # Process current elements
87
+ for el in current.elements:
88
+ el_dict = el.model_dump()
89
+
90
+ if el.id not in previous_ids:
91
+ # Element is new - mark as ADDED
92
+ el_dict["diff_status"] = "ADDED"
93
+ else:
94
+ # Element existed before - check for changes using canonicalized comparisons
95
+ prev_el = previous_by_id[el.id]
96
+
97
+ # Convert to dicts for canonicalization helpers
98
+ el_data = SnapshotDiff._element_to_dict(el)
99
+ prev_el_data = SnapshotDiff._element_to_dict(prev_el)
100
+
101
+ has_bbox_changed = bbox_changed(el_data["bbox"], prev_el_data["bbox"])
102
+ has_content_changed = content_changed(el_data, prev_el_data)
103
+
104
+ if has_bbox_changed and has_content_changed:
105
+ # Both position and content changed - mark as MODIFIED
106
+ el_dict["diff_status"] = "MODIFIED"
107
+ elif has_bbox_changed:
108
+ # Only position changed - mark as MOVED
109
+ el_dict["diff_status"] = "MOVED"
110
+ elif has_content_changed:
111
+ # Only content changed - mark as MODIFIED
112
+ el_dict["diff_status"] = "MODIFIED"
113
+ else:
114
+ # No change - don't set diff_status (frontend expects undefined)
115
+ el_dict["diff_status"] = None
116
+
117
+ result.append(Element(**el_dict))
118
+
119
+ # Process removed elements (existed in previous but not in current)
120
+ for prev_id in previous_ids - current_ids:
121
+ prev_el = previous_by_id[prev_id]
122
+ el_dict = prev_el.model_dump()
123
+ el_dict["diff_status"] = "REMOVED"
124
+ result.append(Element(**el_dict))
125
+
126
+ return result
@@ -0,0 +1,262 @@
1
+ """
2
+ Text search utilities - find text and get pixel coordinates
3
+ """
4
+
5
+ from .browser import AsyncSentienceBrowser, SentienceBrowser
6
+ from .browser_evaluator import BrowserEvaluator
7
+ from .models import TextRectSearchResult
8
+
9
+
10
+ def find_text_rect(
11
+ browser: SentienceBrowser,
12
+ text: str,
13
+ case_sensitive: bool = False,
14
+ whole_word: bool = False,
15
+ max_results: int = 10,
16
+ ) -> TextRectSearchResult:
17
+ """
18
+ Find all occurrences of text on the page and get their exact pixel coordinates.
19
+
20
+ This function searches for text in all visible text nodes on the page and returns
21
+ the bounding rectangles for each match. Useful for:
22
+ - Finding specific UI elements by their text content
23
+ - Locating buttons, links, or labels without element IDs
24
+ - Getting exact coordinates for click automation
25
+ - Highlighting search results visually
26
+
27
+ Args:
28
+ browser: SentienceBrowser instance
29
+ text: Text to search for (required)
30
+ case_sensitive: If True, search is case-sensitive (default: False)
31
+ whole_word: If True, only match whole words surrounded by whitespace (default: False)
32
+ max_results: Maximum number of matches to return (default: 10, max: 100)
33
+
34
+ Returns:
35
+ TextRectSearchResult with:
36
+ - status: "success" or "error"
37
+ - query: The search text
38
+ - case_sensitive: Whether search was case-sensitive
39
+ - whole_word: Whether whole-word matching was used
40
+ - matches: Number of matches found
41
+ - results: List of TextMatch objects, each containing:
42
+ - text: The matched text
43
+ - rect: Absolute rectangle (with scroll offset)
44
+ - viewport_rect: Viewport-relative rectangle
45
+ - context: Surrounding text (before/after)
46
+ - in_viewport: Whether visible in current viewport
47
+ - viewport: Current viewport dimensions and scroll position
48
+ - error: Error message if status is "error"
49
+
50
+ Examples:
51
+ # Find "Sign In" button
52
+ result = find_text_rect(browser, "Sign In")
53
+ if result.status == "success" and result.results:
54
+ first_match = result.results[0]
55
+ print(f"Found at: ({first_match.rect.x}, {first_match.rect.y})")
56
+ print(f"Size: {first_match.rect.width}x{first_match.rect.height}")
57
+ print(f"In viewport: {first_match.in_viewport}")
58
+
59
+ # Case-sensitive search
60
+ result = find_text_rect(browser, "LOGIN", case_sensitive=True)
61
+
62
+ # Whole word only
63
+ result = find_text_rect(browser, "log", whole_word=True) # Won't match "login"
64
+
65
+ # Find all matches and click the first visible one
66
+ result = find_text_rect(browser, "Buy Now", max_results=5)
67
+ if result.status == "success" and result.results:
68
+ for match in result.results:
69
+ if match.in_viewport:
70
+ # Use click_rect from actions module
71
+ from sentience import click_rect
72
+ click_result = click_rect(browser, {
73
+ "x": match.rect.x,
74
+ "y": match.rect.y,
75
+ "w": match.rect.width,
76
+ "h": match.rect.height
77
+ })
78
+ break
79
+ """
80
+ if not browser.page:
81
+ raise RuntimeError("Browser not started. Call browser.start() first.")
82
+
83
+ if not text or not text.strip():
84
+ return TextRectSearchResult(
85
+ status="error",
86
+ error="Text parameter is required and cannot be empty",
87
+ )
88
+
89
+ # Limit max_results to prevent performance issues
90
+ max_results = min(max_results, 100)
91
+
92
+ # CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
93
+ # The new architecture loads injected_api.js asynchronously, so window.sentience
94
+ # may not be immediately available after page load
95
+ BrowserEvaluator.wait_for_extension(browser.page, timeout_ms=5000)
96
+
97
+ # Verify findTextRect method exists (for older extension versions that don't have it)
98
+ if not BrowserEvaluator.verify_method_exists(browser.page, SentienceMethod.FIND_TEXT_RECT):
99
+ raise RuntimeError(
100
+ "window.sentience.findTextRect is not available. "
101
+ "Please update the Sentience extension to the latest version."
102
+ )
103
+
104
+ # Call the extension's findTextRect method
105
+ result_dict = browser.page.evaluate(
106
+ """
107
+ (options) => {
108
+ return window.sentience.findTextRect(options);
109
+ }
110
+ """,
111
+ {
112
+ "text": text,
113
+ "caseSensitive": case_sensitive,
114
+ "wholeWord": whole_word,
115
+ "maxResults": max_results,
116
+ },
117
+ )
118
+
119
+ # Parse and validate with Pydantic
120
+ return TextRectSearchResult(**result_dict)
121
+
122
+
123
+ async def find_text_rect_async(
124
+ browser: AsyncSentienceBrowser,
125
+ text: str,
126
+ case_sensitive: bool = False,
127
+ whole_word: bool = False,
128
+ max_results: int = 10,
129
+ ) -> TextRectSearchResult:
130
+ """
131
+ Find all occurrences of text on the page and get their exact pixel coordinates (async).
132
+
133
+ This function searches for text in all visible text nodes on the page and returns
134
+ the bounding rectangles for each match. Useful for:
135
+ - Finding specific UI elements by their text content
136
+ - Locating buttons, links, or labels without element IDs
137
+ - Getting exact coordinates for click automation
138
+ - Highlighting search results visually
139
+
140
+ Args:
141
+ browser: AsyncSentienceBrowser instance
142
+ text: Text to search for (required)
143
+ case_sensitive: If True, search is case-sensitive (default: False)
144
+ whole_word: If True, only match whole words surrounded by whitespace (default: False)
145
+ max_results: Maximum number of matches to return (default: 10, max: 100)
146
+
147
+ Returns:
148
+ TextRectSearchResult with:
149
+ - status: "success" or "error"
150
+ - query: The search text
151
+ - case_sensitive: Whether search was case-sensitive
152
+ - whole_word: Whether whole-word matching was used
153
+ - matches: Number of matches found
154
+ - results: List of TextMatch objects, each containing:
155
+ - text: The matched text
156
+ - rect: Absolute rectangle (with scroll offset)
157
+ - viewport_rect: Viewport-relative rectangle
158
+ - context: Surrounding text (before/after)
159
+ - in_viewport: Whether visible in current viewport
160
+ - viewport: Current viewport dimensions and scroll position
161
+ - error: Error message if status is "error"
162
+
163
+ Examples:
164
+ # Find "Sign In" button
165
+ result = await find_text_rect_async(browser, "Sign In")
166
+ if result.status == "success" and result.results:
167
+ first_match = result.results[0]
168
+ print(f"Found at: ({first_match.rect.x}, {first_match.rect.y})")
169
+ print(f"Size: {first_match.rect.width}x{first_match.rect.height}")
170
+ print(f"In viewport: {first_match.in_viewport}")
171
+
172
+ # Case-sensitive search
173
+ result = await find_text_rect_async(browser, "LOGIN", case_sensitive=True)
174
+
175
+ # Whole word only
176
+ result = await find_text_rect_async(browser, "log", whole_word=True) # Won't match "login"
177
+
178
+ # Find all matches and click the first visible one
179
+ result = await find_text_rect_async(browser, "Buy Now", max_results=5)
180
+ if result.status == "success" and result.results:
181
+ for match in result.results:
182
+ if match.in_viewport:
183
+ # Use click_rect_async from actions module
184
+ from sentience.actions import click_rect_async
185
+ click_result = await click_rect_async(browser, {
186
+ "x": match.rect.x,
187
+ "y": match.rect.y,
188
+ "w": match.rect.width,
189
+ "h": match.rect.height
190
+ })
191
+ break
192
+ """
193
+ if not browser.page:
194
+ raise RuntimeError("Browser not started. Call await browser.start() first.")
195
+
196
+ if not text or not text.strip():
197
+ return TextRectSearchResult(
198
+ status="error",
199
+ error="Text parameter is required and cannot be empty",
200
+ )
201
+
202
+ # Limit max_results to prevent performance issues
203
+ max_results = min(max_results, 100)
204
+
205
+ # CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
206
+ # The new architecture loads injected_api.js asynchronously, so window.sentience
207
+ # may not be immediately available after page load
208
+ try:
209
+ await browser.page.wait_for_function(
210
+ "typeof window.sentience !== 'undefined'",
211
+ timeout=5000, # 5 second timeout
212
+ )
213
+ except Exception as e:
214
+ # Gather diagnostics if wait fails
215
+ try:
216
+ diag = await browser.page.evaluate(
217
+ """() => ({
218
+ sentience_defined: typeof window.sentience !== 'undefined',
219
+ extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
220
+ url: window.location.href
221
+ })"""
222
+ )
223
+ except Exception:
224
+ diag = {"error": "Could not gather diagnostics"}
225
+
226
+ raise RuntimeError(
227
+ f"Sentience extension failed to inject window.sentience API. "
228
+ f"Is the extension loaded? Diagnostics: {diag}"
229
+ ) from e
230
+
231
+ # Verify findTextRect method exists (for older extension versions that don't have it)
232
+ try:
233
+ has_find_text_rect = await browser.page.evaluate(
234
+ "typeof window.sentience.findTextRect !== 'undefined'"
235
+ )
236
+ if not has_find_text_rect:
237
+ raise RuntimeError(
238
+ "window.sentience.findTextRect is not available. "
239
+ "Please update the Sentience extension to the latest version."
240
+ )
241
+ except RuntimeError:
242
+ raise
243
+ except Exception as e:
244
+ raise RuntimeError(f"Failed to verify findTextRect availability: {e}") from e
245
+
246
+ # Call the extension's findTextRect method
247
+ result_dict = await browser.page.evaluate(
248
+ """
249
+ (options) => {
250
+ return window.sentience.findTextRect(options);
251
+ }
252
+ """,
253
+ {
254
+ "text": text,
255
+ "caseSensitive": case_sensitive,
256
+ "wholeWord": whole_word,
257
+ "maxResults": max_results,
258
+ },
259
+ )
260
+
261
+ # Parse and validate with Pydantic
262
+ return TextRectSearchResult(**result_dict)
@@ -0,0 +1,148 @@
1
+ """
2
+ Trace event building utilities for agent-based tracing.
3
+
4
+ This module provides centralized trace event building logic to reduce duplication
5
+ across agent implementations.
6
+ """
7
+
8
+ from typing import Any, Optional
9
+
10
+ from .models import AgentActionResult, Element, Snapshot
11
+
12
+
13
+ class TraceEventBuilder:
14
+ """
15
+ Helper for building trace events with consistent structure.
16
+
17
+ Provides static methods for building common trace event types:
18
+ - snapshot_taken events
19
+ - step_end events
20
+ """
21
+
22
+ @staticmethod
23
+ def build_snapshot_event(
24
+ snapshot: Snapshot,
25
+ include_all_elements: bool = True,
26
+ ) -> dict[str, Any]:
27
+ """
28
+ Build snapshot_taken trace event data.
29
+
30
+ Args:
31
+ snapshot: Snapshot to build event from
32
+ include_all_elements: If True, include all elements (for DOM tree display).
33
+ If False, use filtered elements only.
34
+
35
+ Returns:
36
+ Dictionary with snapshot event data
37
+ """
38
+ # Normalize importance values to importance_score (0-1 range) per snapshot
39
+ # Min-max normalization: (value - min) / (max - min)
40
+ importance_values = [el.importance for el in snapshot.elements]
41
+
42
+ if importance_values:
43
+ min_importance = min(importance_values)
44
+ max_importance = max(importance_values)
45
+ importance_range = max_importance - min_importance
46
+ else:
47
+ min_importance = 0
48
+ max_importance = 0
49
+ importance_range = 0
50
+
51
+ # Include ALL elements with full data for DOM tree display
52
+ # Add importance_score field normalized to [0, 1]
53
+ elements_data = []
54
+ for el in snapshot.elements:
55
+ el_dict = el.model_dump()
56
+
57
+ # Compute normalized importance_score
58
+ if importance_range > 0:
59
+ importance_score = (el.importance - min_importance) / importance_range
60
+ else:
61
+ # If all elements have same importance, set to 0.5
62
+ importance_score = 0.5
63
+
64
+ el_dict["importance_score"] = importance_score
65
+ elements_data.append(el_dict)
66
+
67
+ return {
68
+ "url": snapshot.url,
69
+ "element_count": len(snapshot.elements),
70
+ "timestamp": snapshot.timestamp,
71
+ "elements": elements_data, # Full element data for DOM tree
72
+ }
73
+
74
+ @staticmethod
75
+ def build_step_end_event(
76
+ step_id: str,
77
+ step_index: int,
78
+ goal: str,
79
+ attempt: int,
80
+ pre_url: str,
81
+ post_url: str,
82
+ snapshot_digest: str | None,
83
+ llm_data: dict[str, Any],
84
+ exec_data: dict[str, Any],
85
+ verify_data: dict[str, Any],
86
+ pre_elements: list[dict[str, Any]] | None = None,
87
+ assertions: list[dict[str, Any]] | None = None,
88
+ ) -> dict[str, Any]:
89
+ """
90
+ Build step_end trace event data.
91
+
92
+ Args:
93
+ step_id: Unique step identifier
94
+ step_index: Step index (0-based)
95
+ goal: User's goal for this step
96
+ attempt: Attempt number (0-based)
97
+ pre_url: URL before action execution
98
+ post_url: URL after action execution
99
+ snapshot_digest: Digest of snapshot before action
100
+ llm_data: LLM interaction data
101
+ exec_data: Action execution data
102
+ verify_data: Verification data
103
+ pre_elements: Optional list of elements from pre-snapshot (with diff_status)
104
+ assertions: Optional list of assertion results from AgentRuntime
105
+
106
+ Returns:
107
+ Dictionary with step_end event data
108
+ """
109
+ pre_data: dict[str, Any] = {
110
+ "url": pre_url,
111
+ "snapshot_digest": snapshot_digest,
112
+ }
113
+
114
+ # Add elements to pre field if provided (for diff overlay support)
115
+ if pre_elements is not None:
116
+ pre_data["elements"] = pre_elements
117
+
118
+ # Build verify data with assertions if provided
119
+ final_verify_data = verify_data.copy()
120
+ if assertions:
121
+ # Ensure signals dict exists
122
+ if "signals" not in final_verify_data:
123
+ final_verify_data["signals"] = {}
124
+
125
+ # Add assertions to signals
126
+ final_verify_data["signals"]["assertions"] = assertions
127
+
128
+ # Check for task completion (assertions marked as required that passed)
129
+ for a in assertions:
130
+ if a.get("passed") and a.get("required"):
131
+ final_verify_data["signals"]["task_done"] = True
132
+ final_verify_data["signals"]["task_done_label"] = a.get("label")
133
+ break
134
+
135
+ return {
136
+ "v": 1,
137
+ "step_id": step_id,
138
+ "step_index": step_index,
139
+ "goal": goal,
140
+ "attempt": attempt,
141
+ "pre": pre_data,
142
+ "llm": llm_data,
143
+ "exec": exec_data,
144
+ "post": {
145
+ "url": post_url,
146
+ },
147
+ "verify": final_verify_data,
148
+ }