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
sentience/ordinal.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase 3: Ordinal Intent Detection for Semantic Search
|
|
3
|
+
|
|
4
|
+
This module provides functions to detect ordinal intent in natural language goals
|
|
5
|
+
and select elements based on their position within groups.
|
|
6
|
+
|
|
7
|
+
Ordinal operators supported:
|
|
8
|
+
- Position-based: "first", "second", "third", "1st", "2nd", "3rd", etc.
|
|
9
|
+
- Relative: "top", "bottom", "last", "next", "previous"
|
|
10
|
+
- Numeric: "#1", "#2", "number 1", "item 3"
|
|
11
|
+
|
|
12
|
+
Example usage:
|
|
13
|
+
from sentience.ordinal import detect_ordinal_intent, select_by_ordinal
|
|
14
|
+
|
|
15
|
+
intent = detect_ordinal_intent("click the first search result")
|
|
16
|
+
# OrdinalIntent(kind='nth', n=1, detected=True)
|
|
17
|
+
|
|
18
|
+
element = select_by_ordinal(elements, dominant_group_key, intent)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Literal
|
|
24
|
+
|
|
25
|
+
from sentience.models import Element
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class OrdinalIntent:
|
|
30
|
+
"""Detected ordinal intent from a goal string."""
|
|
31
|
+
|
|
32
|
+
detected: bool
|
|
33
|
+
kind: Literal["first", "last", "nth", "top_k", "next", "previous"] | None = None
|
|
34
|
+
n: int | None = None # For "nth" kind: 1-indexed position (1=first, 2=second)
|
|
35
|
+
k: int | None = None # For "top_k" kind: number of items
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Ordinal word to number mapping
|
|
39
|
+
ORDINAL_WORDS = {
|
|
40
|
+
"first": 1,
|
|
41
|
+
"second": 2,
|
|
42
|
+
"third": 3,
|
|
43
|
+
"fourth": 4,
|
|
44
|
+
"fifth": 5,
|
|
45
|
+
"sixth": 6,
|
|
46
|
+
"seventh": 7,
|
|
47
|
+
"eighth": 8,
|
|
48
|
+
"ninth": 9,
|
|
49
|
+
"tenth": 10,
|
|
50
|
+
"1st": 1,
|
|
51
|
+
"2nd": 2,
|
|
52
|
+
"3rd": 3,
|
|
53
|
+
"4th": 4,
|
|
54
|
+
"5th": 5,
|
|
55
|
+
"6th": 6,
|
|
56
|
+
"7th": 7,
|
|
57
|
+
"8th": 8,
|
|
58
|
+
"9th": 9,
|
|
59
|
+
"10th": 10,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Patterns for detecting ordinal intent
|
|
63
|
+
ORDINAL_PATTERNS = [
|
|
64
|
+
# "first", "second", etc.
|
|
65
|
+
(
|
|
66
|
+
r"\b(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)\b",
|
|
67
|
+
"ordinal_word",
|
|
68
|
+
),
|
|
69
|
+
# "1st", "2nd", "3rd", etc.
|
|
70
|
+
(r"\b(\d+)(st|nd|rd|th)\b", "ordinal_suffix"),
|
|
71
|
+
# "#1", "#2", etc.
|
|
72
|
+
(r"#(\d+)\b", "hash_number"),
|
|
73
|
+
# "number 1", "item 3", "result 5"
|
|
74
|
+
(r"\b(?:number|item|result|option|choice)\s*(\d+)\b", "labeled_number"),
|
|
75
|
+
# "top" (implies first/best)
|
|
76
|
+
(r"\btop\b(?!\s*\d)", "top"),
|
|
77
|
+
# "top 3", "top 5"
|
|
78
|
+
(r"\btop\s+(\d+)\b", "top_k"),
|
|
79
|
+
# "last", "final", "bottom"
|
|
80
|
+
(r"\b(last|final|bottom)\b", "last"),
|
|
81
|
+
# "next", "following"
|
|
82
|
+
(r"\b(next|following)\b", "next"),
|
|
83
|
+
# "previous", "preceding", "prior"
|
|
84
|
+
(r"\b(previous|preceding|prior)\b", "previous"),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def detect_ordinal_intent(goal: str) -> OrdinalIntent:
|
|
89
|
+
"""
|
|
90
|
+
Detect ordinal intent from a goal string.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
goal: Natural language goal (e.g., "click the first search result")
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
OrdinalIntent with detected=True if ordinal intent found, False otherwise.
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
>>> detect_ordinal_intent("click the first item")
|
|
100
|
+
OrdinalIntent(detected=True, kind='nth', n=1)
|
|
101
|
+
|
|
102
|
+
>>> detect_ordinal_intent("select the 3rd option")
|
|
103
|
+
OrdinalIntent(detected=True, kind='nth', n=3)
|
|
104
|
+
|
|
105
|
+
>>> detect_ordinal_intent("show top 5 results")
|
|
106
|
+
OrdinalIntent(detected=True, kind='top_k', k=5)
|
|
107
|
+
|
|
108
|
+
>>> detect_ordinal_intent("click the last button")
|
|
109
|
+
OrdinalIntent(detected=True, kind='last')
|
|
110
|
+
|
|
111
|
+
>>> detect_ordinal_intent("find the submit button")
|
|
112
|
+
OrdinalIntent(detected=False)
|
|
113
|
+
"""
|
|
114
|
+
goal_lower = goal.lower()
|
|
115
|
+
|
|
116
|
+
for pattern, pattern_type in ORDINAL_PATTERNS:
|
|
117
|
+
match = re.search(pattern, goal_lower, re.IGNORECASE)
|
|
118
|
+
if match:
|
|
119
|
+
if pattern_type == "ordinal_word":
|
|
120
|
+
word = match.group(1).lower()
|
|
121
|
+
n = ORDINAL_WORDS.get(word)
|
|
122
|
+
if n:
|
|
123
|
+
return OrdinalIntent(detected=True, kind="nth", n=n)
|
|
124
|
+
|
|
125
|
+
elif pattern_type == "ordinal_suffix":
|
|
126
|
+
n = int(match.group(1))
|
|
127
|
+
return OrdinalIntent(detected=True, kind="nth", n=n)
|
|
128
|
+
|
|
129
|
+
elif pattern_type == "hash_number":
|
|
130
|
+
n = int(match.group(1))
|
|
131
|
+
return OrdinalIntent(detected=True, kind="nth", n=n)
|
|
132
|
+
|
|
133
|
+
elif pattern_type == "labeled_number":
|
|
134
|
+
n = int(match.group(1))
|
|
135
|
+
return OrdinalIntent(detected=True, kind="nth", n=n)
|
|
136
|
+
|
|
137
|
+
elif pattern_type == "top":
|
|
138
|
+
# "top" without a number means "first/best"
|
|
139
|
+
return OrdinalIntent(detected=True, kind="first")
|
|
140
|
+
|
|
141
|
+
elif pattern_type == "top_k":
|
|
142
|
+
k = int(match.group(1))
|
|
143
|
+
return OrdinalIntent(detected=True, kind="top_k", k=k)
|
|
144
|
+
|
|
145
|
+
elif pattern_type == "last":
|
|
146
|
+
return OrdinalIntent(detected=True, kind="last")
|
|
147
|
+
|
|
148
|
+
elif pattern_type == "next":
|
|
149
|
+
return OrdinalIntent(detected=True, kind="next")
|
|
150
|
+
|
|
151
|
+
elif pattern_type == "previous":
|
|
152
|
+
return OrdinalIntent(detected=True, kind="previous")
|
|
153
|
+
|
|
154
|
+
return OrdinalIntent(detected=False)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def select_by_ordinal(
|
|
158
|
+
elements: list[Element],
|
|
159
|
+
dominant_group_key: str | None,
|
|
160
|
+
intent: OrdinalIntent,
|
|
161
|
+
current_element_id: int | None = None,
|
|
162
|
+
) -> Element | list[Element] | None:
|
|
163
|
+
"""
|
|
164
|
+
Select element(s) from a list based on ordinal intent.
|
|
165
|
+
|
|
166
|
+
Uses the dominant_group_key to filter to the "main content" group,
|
|
167
|
+
then selects by group_index based on the ordinal intent.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
elements: List of elements with group_key and group_index populated
|
|
171
|
+
dominant_group_key: The most common group key (main content group)
|
|
172
|
+
intent: Detected ordinal intent
|
|
173
|
+
current_element_id: Current element ID (for next/previous navigation)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Single Element for nth/first/last, list of Elements for top_k,
|
|
177
|
+
or None if no matching element found.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
>>> intent = OrdinalIntent(detected=True, kind='nth', n=1)
|
|
181
|
+
>>> element = select_by_ordinal(elements, "x5-w2-h1", intent)
|
|
182
|
+
# Returns element with group_key="x5-w2-h1" and group_index=0
|
|
183
|
+
"""
|
|
184
|
+
if not intent.detected:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
# Filter to dominant group if available
|
|
188
|
+
if dominant_group_key:
|
|
189
|
+
group_elements = [e for e in elements if e.group_key == dominant_group_key]
|
|
190
|
+
else:
|
|
191
|
+
# Fallback: use all elements with group_index
|
|
192
|
+
group_elements = [e for e in elements if e.group_index is not None]
|
|
193
|
+
|
|
194
|
+
if not group_elements:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
# Sort by group_index to ensure correct ordering
|
|
198
|
+
group_elements.sort(key=lambda e: e.group_index if e.group_index is not None else 0)
|
|
199
|
+
|
|
200
|
+
if intent.kind == "first" or (intent.kind == "nth" and intent.n == 1):
|
|
201
|
+
# First element (group_index=0)
|
|
202
|
+
return group_elements[0] if group_elements else None
|
|
203
|
+
|
|
204
|
+
elif intent.kind == "nth" and intent.n is not None:
|
|
205
|
+
# Nth element (1-indexed, so n=2 means group_index=1)
|
|
206
|
+
target_index = intent.n - 1
|
|
207
|
+
if 0 <= target_index < len(group_elements):
|
|
208
|
+
return group_elements[target_index]
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
elif intent.kind == "last":
|
|
212
|
+
# Last element
|
|
213
|
+
return group_elements[-1] if group_elements else None
|
|
214
|
+
|
|
215
|
+
elif intent.kind == "top_k" and intent.k is not None:
|
|
216
|
+
# Top K elements
|
|
217
|
+
return group_elements[: intent.k]
|
|
218
|
+
|
|
219
|
+
elif intent.kind == "next" and current_element_id is not None:
|
|
220
|
+
# Next element after current
|
|
221
|
+
for i, elem in enumerate(group_elements):
|
|
222
|
+
if elem.id == current_element_id and i + 1 < len(group_elements):
|
|
223
|
+
return group_elements[i + 1]
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
elif intent.kind == "previous" and current_element_id is not None:
|
|
227
|
+
# Previous element before current
|
|
228
|
+
for i, elem in enumerate(group_elements):
|
|
229
|
+
if elem.id == current_element_id and i > 0:
|
|
230
|
+
return group_elements[i - 1]
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def boost_ordinal_elements(
|
|
237
|
+
elements: list[Element],
|
|
238
|
+
dominant_group_key: str | None,
|
|
239
|
+
intent: OrdinalIntent,
|
|
240
|
+
boost_factor: int = 10000,
|
|
241
|
+
) -> list[Element]:
|
|
242
|
+
"""
|
|
243
|
+
Boost the importance of elements matching ordinal intent.
|
|
244
|
+
|
|
245
|
+
This is useful for integrating ordinal selection with existing
|
|
246
|
+
importance-based ranking. Elements matching the ordinal intent
|
|
247
|
+
get a significant importance boost.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
elements: List of elements (not modified)
|
|
251
|
+
dominant_group_key: The most common group key
|
|
252
|
+
intent: Detected ordinal intent
|
|
253
|
+
boost_factor: Amount to add to importance (default: 10000)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
A new list with copies of elements, with boosted importance for matches.
|
|
257
|
+
"""
|
|
258
|
+
if not intent.detected or not dominant_group_key:
|
|
259
|
+
return [e.model_copy() for e in elements]
|
|
260
|
+
|
|
261
|
+
target = select_by_ordinal(elements, dominant_group_key, intent)
|
|
262
|
+
|
|
263
|
+
if target is None:
|
|
264
|
+
return [e.model_copy() for e in elements]
|
|
265
|
+
|
|
266
|
+
# Handle single element or list
|
|
267
|
+
if isinstance(target, list):
|
|
268
|
+
target_ids = {e.id for e in target}
|
|
269
|
+
else:
|
|
270
|
+
target_ids = {target.id}
|
|
271
|
+
|
|
272
|
+
# Create copies and boost matching elements
|
|
273
|
+
result = []
|
|
274
|
+
for elem in elements:
|
|
275
|
+
copy = elem.model_copy()
|
|
276
|
+
if copy.id in target_ids:
|
|
277
|
+
copy.importance = (copy.importance or 0) + boost_factor
|
|
278
|
+
result.append(copy)
|
|
279
|
+
|
|
280
|
+
return result
|
sentience/overlay.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visual overlay utilities - show/clear element highlights in browser
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from .browser import AsyncSentienceBrowser, SentienceBrowser
|
|
8
|
+
from .models import Element, Snapshot
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def show_overlay(
|
|
12
|
+
browser: SentienceBrowser,
|
|
13
|
+
elements: list[Element] | list[dict[str, Any]] | Snapshot,
|
|
14
|
+
target_element_id: int | None = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Display visual overlay highlighting elements in the browser
|
|
18
|
+
|
|
19
|
+
This function shows a Shadow DOM overlay with color-coded borders around
|
|
20
|
+
detected elements. Useful for debugging, learning, and validating element detection.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
browser: SentienceBrowser instance
|
|
24
|
+
elements: Can be:
|
|
25
|
+
- List of Element objects (from snapshot.elements)
|
|
26
|
+
- List of raw element dicts (from snapshot result or API response)
|
|
27
|
+
- Snapshot object (will use snapshot.elements)
|
|
28
|
+
target_element_id: Optional ID of element to highlight in red (default: None)
|
|
29
|
+
|
|
30
|
+
Color Coding:
|
|
31
|
+
- Red: Target element (when target_element_id is specified)
|
|
32
|
+
- Blue: Primary elements (is_primary=true)
|
|
33
|
+
- Green: Regular interactive elements
|
|
34
|
+
|
|
35
|
+
Visual Indicators:
|
|
36
|
+
- Border thickness and opacity scale with importance score
|
|
37
|
+
- Semi-transparent fill for better visibility
|
|
38
|
+
- Importance badges showing scores
|
|
39
|
+
- Star icon for primary elements
|
|
40
|
+
- Target emoji for the target element
|
|
41
|
+
|
|
42
|
+
Auto-clear: Overlay automatically disappears after 5 seconds
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
# Show overlay from snapshot
|
|
46
|
+
snap = snapshot(browser)
|
|
47
|
+
show_overlay(browser, snap)
|
|
48
|
+
|
|
49
|
+
# Show overlay with custom elements
|
|
50
|
+
elements = [{"id": 1, "bbox": {"x": 100, "y": 100, "width": 200, "height": 50}, ...}]
|
|
51
|
+
show_overlay(browser, elements)
|
|
52
|
+
|
|
53
|
+
# Show overlay with target element highlighted in red
|
|
54
|
+
show_overlay(browser, snap, target_element_id=42)
|
|
55
|
+
|
|
56
|
+
# Clear overlay manually before 5 seconds
|
|
57
|
+
clear_overlay(browser)
|
|
58
|
+
"""
|
|
59
|
+
if not browser.page:
|
|
60
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
61
|
+
|
|
62
|
+
# Handle different input types
|
|
63
|
+
if isinstance(elements, Snapshot):
|
|
64
|
+
# Extract elements from Snapshot object
|
|
65
|
+
elements_list = [el.model_dump() for el in elements.elements]
|
|
66
|
+
elif isinstance(elements, list) and len(elements) > 0:
|
|
67
|
+
# Check if it's a list of Element objects or dicts
|
|
68
|
+
if hasattr(elements[0], "model_dump"):
|
|
69
|
+
# List of Element objects
|
|
70
|
+
elements_list = [el.model_dump() for el in elements]
|
|
71
|
+
else:
|
|
72
|
+
# Already a list of dicts
|
|
73
|
+
elements_list = elements
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError("elements must be a Snapshot, list of Element objects, or list of dicts")
|
|
76
|
+
|
|
77
|
+
# Call extension API
|
|
78
|
+
browser.page.evaluate(
|
|
79
|
+
"""
|
|
80
|
+
(args) => {
|
|
81
|
+
if (window.sentience && window.sentience.showOverlay) {
|
|
82
|
+
window.sentience.showOverlay(args.elements, args.targetId);
|
|
83
|
+
} else {
|
|
84
|
+
console.warn('[Sentience SDK] showOverlay not available - is extension loaded?');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
""",
|
|
88
|
+
{"elements": elements_list, "targetId": target_element_id},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def clear_overlay(browser: SentienceBrowser) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Clear the visual overlay manually (before 5-second auto-clear)
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
browser: SentienceBrowser instance
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
show_overlay(browser, snap)
|
|
101
|
+
# ... inspect overlay ...
|
|
102
|
+
clear_overlay(browser) # Remove immediately
|
|
103
|
+
"""
|
|
104
|
+
if not browser.page:
|
|
105
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
106
|
+
|
|
107
|
+
browser.page.evaluate(
|
|
108
|
+
"""
|
|
109
|
+
() => {
|
|
110
|
+
if (window.sentience && window.sentience.clearOverlay) {
|
|
111
|
+
window.sentience.clearOverlay();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def show_overlay_async(
|
|
119
|
+
browser: AsyncSentienceBrowser,
|
|
120
|
+
elements: list[Element] | list[dict[str, Any]] | Snapshot,
|
|
121
|
+
target_element_id: int | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Display visual overlay highlighting elements in the browser (async)
|
|
125
|
+
|
|
126
|
+
This function shows a Shadow DOM overlay with color-coded borders around
|
|
127
|
+
detected elements. Useful for debugging, learning, and validating element detection.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
browser: AsyncSentienceBrowser instance
|
|
131
|
+
elements: Can be:
|
|
132
|
+
- List of Element objects (from snapshot.elements)
|
|
133
|
+
- List of raw element dicts (from snapshot result or API response)
|
|
134
|
+
- Snapshot object (will use snapshot.elements)
|
|
135
|
+
target_element_id: Optional ID of element to highlight in red (default: None)
|
|
136
|
+
|
|
137
|
+
Color Coding:
|
|
138
|
+
- Red: Target element (when target_element_id is specified)
|
|
139
|
+
- Blue: Primary elements (is_primary=true)
|
|
140
|
+
- Green: Regular interactive elements
|
|
141
|
+
|
|
142
|
+
Visual Indicators:
|
|
143
|
+
- Border thickness and opacity scale with importance score
|
|
144
|
+
- Semi-transparent fill for better visibility
|
|
145
|
+
- Importance badges showing scores
|
|
146
|
+
- Star icon for primary elements
|
|
147
|
+
- Target emoji for the target element
|
|
148
|
+
|
|
149
|
+
Auto-clear: Overlay automatically disappears after 5 seconds
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
# Show overlay from snapshot
|
|
153
|
+
snap = await snapshot_async(browser)
|
|
154
|
+
await show_overlay_async(browser, snap)
|
|
155
|
+
|
|
156
|
+
# Show overlay with custom elements
|
|
157
|
+
elements = [{"id": 1, "bbox": {"x": 100, "y": 100, "width": 200, "height": 50}, ...}]
|
|
158
|
+
await show_overlay_async(browser, elements)
|
|
159
|
+
|
|
160
|
+
# Show overlay with target element highlighted in red
|
|
161
|
+
await show_overlay_async(browser, snap, target_element_id=42)
|
|
162
|
+
|
|
163
|
+
# Clear overlay manually before 5 seconds
|
|
164
|
+
await clear_overlay_async(browser)
|
|
165
|
+
"""
|
|
166
|
+
if not browser.page:
|
|
167
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
168
|
+
|
|
169
|
+
# Handle different input types
|
|
170
|
+
if isinstance(elements, Snapshot):
|
|
171
|
+
# Extract elements from Snapshot object
|
|
172
|
+
elements_list = [el.model_dump() for el in elements.elements]
|
|
173
|
+
elif isinstance(elements, list) and len(elements) > 0:
|
|
174
|
+
# Check if it's a list of Element objects or dicts
|
|
175
|
+
if hasattr(elements[0], "model_dump"):
|
|
176
|
+
# List of Element objects
|
|
177
|
+
elements_list = [el.model_dump() for el in elements]
|
|
178
|
+
else:
|
|
179
|
+
# Already a list of dicts
|
|
180
|
+
elements_list = elements
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError("elements must be a Snapshot, list of Element objects, or list of dicts")
|
|
183
|
+
|
|
184
|
+
# Call extension API
|
|
185
|
+
await browser.page.evaluate(
|
|
186
|
+
"""
|
|
187
|
+
(args) => {
|
|
188
|
+
if (window.sentience && window.sentience.showOverlay) {
|
|
189
|
+
window.sentience.showOverlay(args.elements, args.targetId);
|
|
190
|
+
} else {
|
|
191
|
+
console.warn('[Sentience SDK] showOverlay not available - is extension loaded?');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
""",
|
|
195
|
+
{"elements": elements_list, "targetId": target_element_id},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def clear_overlay_async(browser: AsyncSentienceBrowser) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Clear the visual overlay manually (before 5-second auto-clear) (async)
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
browser: AsyncSentienceBrowser instance
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
await show_overlay_async(browser, snap)
|
|
208
|
+
# ... inspect overlay ...
|
|
209
|
+
await clear_overlay_async(browser) # Remove immediately
|
|
210
|
+
"""
|
|
211
|
+
if not browser.page:
|
|
212
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
213
|
+
|
|
214
|
+
await browser.page.evaluate(
|
|
215
|
+
"""
|
|
216
|
+
() => {
|
|
217
|
+
if (window.sentience && window.sentience.clearOverlay) {
|
|
218
|
+
window.sentience.clearOverlay();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
"""
|
|
222
|
+
)
|