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.
- sentience/__init__.py +253 -0
- sentience/_extension_loader.py +195 -0
- sentience/action_executor.py +215 -0
- sentience/actions.py +1020 -0
- sentience/agent.py +1181 -0
- sentience/agent_config.py +46 -0
- sentience/agent_runtime.py +424 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +108 -0
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +343 -0
- sentience/backends/browser_use_adapter.py +241 -0
- sentience/backends/cdp_backend.py +393 -0
- sentience/backends/exceptions.py +211 -0
- sentience/backends/playwright_backend.py +194 -0
- sentience/backends/protocol.py +216 -0
- sentience/backends/sentience_context.py +469 -0
- sentience/backends/snapshot.py +427 -0
- sentience/base_agent.py +196 -0
- sentience/browser.py +1215 -0
- sentience/browser_evaluator.py +299 -0
- sentience/canonicalization.py +207 -0
- sentience/cli.py +130 -0
- sentience/cloud_tracing.py +807 -0
- sentience/constants.py +6 -0
- sentience/conversational_agent.py +543 -0
- sentience/element_filter.py +136 -0
- sentience/expect.py +188 -0
- sentience/extension/background.js +104 -0
- sentience/extension/content.js +161 -0
- sentience/extension/injected_api.js +914 -0
- sentience/extension/manifest.json +36 -0
- sentience/extension/pkg/sentience_core.d.ts +51 -0
- sentience/extension/pkg/sentience_core.js +323 -0
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
- sentience/extension/release.json +115 -0
- sentience/formatting.py +15 -0
- sentience/generator.py +202 -0
- sentience/inspector.py +367 -0
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +875 -0
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +846 -0
- sentience/ordinal.py +280 -0
- sentience/overlay.py +222 -0
- sentience/protocols.py +228 -0
- sentience/query.py +303 -0
- sentience/read.py +188 -0
- sentience/recorder.py +589 -0
- sentience/schemas/trace_v1.json +335 -0
- sentience/screenshot.py +100 -0
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +706 -0
- sentience/snapshot_diff.py +126 -0
- sentience/text_search.py +262 -0
- sentience/trace_event_builder.py +148 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/__init__.py +27 -0
- sentience/trace_indexing/index_schema.py +199 -0
- sentience/trace_indexing/indexer.py +414 -0
- sentience/tracer_factory.py +322 -0
- sentience/tracing.py +449 -0
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/utils/element.py +257 -0
- sentience/utils/formatting.py +59 -0
- sentience/utils.py +296 -0
- sentience/verification.py +380 -0
- sentience/visual_agent.py +2058 -0
- sentience/wait.py +139 -0
- sentienceapi-0.95.0.dist-info/METADATA +984 -0
- sentienceapi-0.95.0.dist-info/RECORD +82 -0
- sentienceapi-0.95.0.dist-info/WHEEL +5 -0
- sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
- 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
|
sentience/text_search.py
ADDED
|
@@ -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
|
+
}
|