sentienceapi 0.92.2__py3-none-any.whl → 0.98.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 +107 -2
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +2 -0
- sentience/actions.py +354 -9
- sentience/agent.py +4 -0
- sentience/agent_runtime.py +840 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +8 -1
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +372 -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 +483 -0
- sentience/browser.py +230 -74
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +65 -24
- sentience/constants.py +6 -0
- sentience/cursor_policy.py +142 -0
- sentience/extension/content.js +35 -0
- sentience/extension/injected_api.js +310 -15
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +192 -144
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +29 -29
- sentience/failure_artifacts.py +241 -0
- sentience/integrations/__init__.py +6 -0
- sentience/integrations/langchain/__init__.py +12 -0
- sentience/integrations/langchain/context.py +18 -0
- sentience/integrations/langchain/core.py +326 -0
- sentience/integrations/langchain/tools.py +180 -0
- sentience/integrations/models.py +46 -0
- sentience/integrations/pydanticai/__init__.py +15 -0
- sentience/integrations/pydanticai/deps.py +20 -0
- sentience/integrations/pydanticai/toolset.py +468 -0
- sentience/llm_provider.py +695 -18
- sentience/models.py +536 -3
- sentience/ordinal.py +280 -0
- sentience/query.py +66 -4
- sentience/schemas/trace_v1.json +27 -1
- sentience/snapshot.py +384 -93
- sentience/snapshot_diff.py +39 -54
- sentience/text_search.py +1 -0
- sentience/trace_event_builder.py +20 -1
- sentience/trace_indexing/indexer.py +3 -49
- sentience/tracer_factory.py +1 -3
- sentience/verification.py +618 -0
- sentience/visual_agent.py +3 -1
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +198 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/utils.py +0 -296
- sentienceapi-0.92.2.dist-info/RECORD +0 -65
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
sentience/snapshot.py
CHANGED
|
@@ -12,6 +12,7 @@ import requests
|
|
|
12
12
|
|
|
13
13
|
from .browser import AsyncSentienceBrowser, SentienceBrowser
|
|
14
14
|
from .browser_evaluator import BrowserEvaluator
|
|
15
|
+
from .constants import SENTIENCE_API_URL
|
|
15
16
|
from .models import Snapshot, SnapshotOptions
|
|
16
17
|
from .sentience_methods import SentienceMethod
|
|
17
18
|
|
|
@@ -19,6 +20,206 @@ from .sentience_methods import SentienceMethod
|
|
|
19
20
|
MAX_PAYLOAD_BYTES = 10 * 1024 * 1024
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _is_execution_context_destroyed_error(e: Exception) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Playwright can throw while a navigation is in-flight, invalidating the JS execution context.
|
|
26
|
+
|
|
27
|
+
Common symptoms:
|
|
28
|
+
- "Execution context was destroyed, most likely because of a navigation"
|
|
29
|
+
- "Cannot find context with specified id"
|
|
30
|
+
"""
|
|
31
|
+
msg = str(e).lower()
|
|
32
|
+
return (
|
|
33
|
+
"execution context was destroyed" in msg
|
|
34
|
+
or "most likely because of a navigation" in msg
|
|
35
|
+
or "cannot find context with specified id" in msg
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _page_evaluate_with_nav_retry(
|
|
40
|
+
page: Any,
|
|
41
|
+
expression: str,
|
|
42
|
+
arg: Any = None,
|
|
43
|
+
*,
|
|
44
|
+
retries: int = 2,
|
|
45
|
+
settle_timeout_ms: int = 10000,
|
|
46
|
+
) -> Any:
|
|
47
|
+
"""
|
|
48
|
+
Evaluate JS with a small retry loop if the page is mid-navigation.
|
|
49
|
+
|
|
50
|
+
This prevents flaky crashes when callers snapshot right after triggering a navigation
|
|
51
|
+
(e.g., pressing Enter on Google).
|
|
52
|
+
"""
|
|
53
|
+
last_err: Exception | None = None
|
|
54
|
+
for attempt in range(retries + 1):
|
|
55
|
+
try:
|
|
56
|
+
if arg is None:
|
|
57
|
+
return await page.evaluate(expression)
|
|
58
|
+
return await page.evaluate(expression, arg)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
last_err = e
|
|
61
|
+
if not _is_execution_context_destroyed_error(e) or attempt >= retries:
|
|
62
|
+
raise
|
|
63
|
+
try:
|
|
64
|
+
await page.wait_for_load_state("domcontentloaded", timeout=settle_timeout_ms)
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
await asyncio.sleep(0.25)
|
|
68
|
+
raise last_err if last_err else RuntimeError("Page.evaluate failed")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _wait_for_function_with_nav_retry(
|
|
72
|
+
page: Any,
|
|
73
|
+
expression: str,
|
|
74
|
+
*,
|
|
75
|
+
timeout_ms: int,
|
|
76
|
+
retries: int = 2,
|
|
77
|
+
) -> None:
|
|
78
|
+
last_err: Exception | None = None
|
|
79
|
+
for attempt in range(retries + 1):
|
|
80
|
+
try:
|
|
81
|
+
await page.wait_for_function(expression, timeout=timeout_ms)
|
|
82
|
+
return
|
|
83
|
+
except Exception as e:
|
|
84
|
+
last_err = e
|
|
85
|
+
if not _is_execution_context_destroyed_error(e) or attempt >= retries:
|
|
86
|
+
raise
|
|
87
|
+
try:
|
|
88
|
+
await page.wait_for_load_state("domcontentloaded", timeout=timeout_ms)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
await asyncio.sleep(0.25)
|
|
92
|
+
raise last_err if last_err else RuntimeError("wait_for_function failed")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _build_snapshot_payload(
|
|
96
|
+
raw_result: dict[str, Any],
|
|
97
|
+
options: SnapshotOptions,
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Build payload dict for gateway snapshot API.
|
|
101
|
+
|
|
102
|
+
Shared helper used by both sync and async snapshot implementations.
|
|
103
|
+
"""
|
|
104
|
+
diagnostics = raw_result.get("diagnostics") or {}
|
|
105
|
+
client_metrics = None
|
|
106
|
+
try:
|
|
107
|
+
client_metrics = diagnostics.get("metrics")
|
|
108
|
+
except Exception:
|
|
109
|
+
client_metrics = None
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"raw_elements": raw_result.get("raw_elements", []),
|
|
113
|
+
"url": raw_result.get("url", ""),
|
|
114
|
+
"viewport": raw_result.get("viewport"),
|
|
115
|
+
"goal": options.goal,
|
|
116
|
+
"options": {
|
|
117
|
+
"limit": options.limit,
|
|
118
|
+
"filter": options.filter.model_dump() if options.filter else None,
|
|
119
|
+
},
|
|
120
|
+
"client_metrics": client_metrics,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _validate_payload_size(payload_json: str) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Validate payload size before sending to gateway.
|
|
127
|
+
|
|
128
|
+
Raises ValueError if payload exceeds server limit.
|
|
129
|
+
"""
|
|
130
|
+
payload_size = len(payload_json.encode("utf-8"))
|
|
131
|
+
if payload_size > MAX_PAYLOAD_BYTES:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Payload size ({payload_size / 1024 / 1024:.2f}MB) exceeds server limit "
|
|
134
|
+
f"({MAX_PAYLOAD_BYTES / 1024 / 1024:.0f}MB). "
|
|
135
|
+
f"Try reducing the number of elements on the page or filtering elements."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _post_snapshot_to_gateway_sync(
|
|
140
|
+
payload: dict[str, Any],
|
|
141
|
+
api_key: str,
|
|
142
|
+
api_url: str = SENTIENCE_API_URL,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""
|
|
145
|
+
Post snapshot payload to gateway (synchronous).
|
|
146
|
+
|
|
147
|
+
Used by sync snapshot() function.
|
|
148
|
+
"""
|
|
149
|
+
payload_json = json.dumps(payload)
|
|
150
|
+
_validate_payload_size(payload_json)
|
|
151
|
+
|
|
152
|
+
headers = {
|
|
153
|
+
"Authorization": f"Bearer {api_key}",
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
response = requests.post(
|
|
158
|
+
f"{api_url}/v1/snapshot",
|
|
159
|
+
data=payload_json,
|
|
160
|
+
headers=headers,
|
|
161
|
+
timeout=30,
|
|
162
|
+
)
|
|
163
|
+
response.raise_for_status()
|
|
164
|
+
return response.json()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def _post_snapshot_to_gateway_async(
|
|
168
|
+
payload: dict[str, Any],
|
|
169
|
+
api_key: str,
|
|
170
|
+
api_url: str = SENTIENCE_API_URL,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Post snapshot payload to gateway (asynchronous).
|
|
174
|
+
|
|
175
|
+
Used by async backend snapshot() function.
|
|
176
|
+
"""
|
|
177
|
+
# Lazy import httpx - only needed for async API calls
|
|
178
|
+
import httpx
|
|
179
|
+
|
|
180
|
+
payload_json = json.dumps(payload)
|
|
181
|
+
_validate_payload_size(payload_json)
|
|
182
|
+
|
|
183
|
+
headers = {
|
|
184
|
+
"Authorization": f"Bearer {api_key}",
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
189
|
+
response = await client.post(
|
|
190
|
+
f"{api_url}/v1/snapshot",
|
|
191
|
+
content=payload_json,
|
|
192
|
+
headers=headers,
|
|
193
|
+
)
|
|
194
|
+
response.raise_for_status()
|
|
195
|
+
return response.json()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _merge_api_result_with_local(
|
|
199
|
+
api_result: dict[str, Any],
|
|
200
|
+
raw_result: dict[str, Any],
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
"""
|
|
203
|
+
Merge API result with local data (screenshot, etc.).
|
|
204
|
+
|
|
205
|
+
Shared helper used by both sync and async snapshot implementations.
|
|
206
|
+
"""
|
|
207
|
+
return {
|
|
208
|
+
"status": api_result.get("status", "success"),
|
|
209
|
+
"timestamp": api_result.get("timestamp"),
|
|
210
|
+
"url": api_result.get("url", raw_result.get("url", "")),
|
|
211
|
+
"viewport": api_result.get("viewport", raw_result.get("viewport")),
|
|
212
|
+
"elements": api_result.get("elements", []),
|
|
213
|
+
"screenshot": raw_result.get("screenshot"), # Keep local screenshot
|
|
214
|
+
"screenshot_format": raw_result.get("screenshot_format"),
|
|
215
|
+
"error": api_result.get("error"),
|
|
216
|
+
# Phase 2: Runtime stability/debug info
|
|
217
|
+
"diagnostics": api_result.get("diagnostics", raw_result.get("diagnostics")),
|
|
218
|
+
# Phase 2: Ordinal support - dominant group key from Gateway
|
|
219
|
+
"dominant_group_key": api_result.get("dominant_group_key"),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
22
223
|
def _save_trace_to_file(raw_elements: list[dict[str, Any]], trace_path: str | None = None) -> None:
|
|
23
224
|
"""
|
|
24
225
|
Save raw_elements to a JSON file for benchmarking/training
|
|
@@ -72,14 +273,18 @@ def snapshot(
|
|
|
72
273
|
if options is None:
|
|
73
274
|
options = SnapshotOptions()
|
|
74
275
|
|
|
276
|
+
# Resolve API key: options.sentience_api_key takes precedence, then browser.api_key
|
|
277
|
+
# This allows browser-use users to pass api_key via options without SentienceBrowser
|
|
278
|
+
effective_api_key = options.sentience_api_key or browser.api_key
|
|
279
|
+
|
|
75
280
|
# Determine if we should use server-side API
|
|
76
281
|
should_use_api = (
|
|
77
|
-
options.use_api if options.use_api is not None else (
|
|
282
|
+
options.use_api if options.use_api is not None else (effective_api_key is not None)
|
|
78
283
|
)
|
|
79
284
|
|
|
80
|
-
if should_use_api and
|
|
285
|
+
if should_use_api and effective_api_key:
|
|
81
286
|
# Use server-side API (Pro/Enterprise tier)
|
|
82
|
-
return _snapshot_via_api(browser, options)
|
|
287
|
+
return _snapshot_via_api(browser, options, effective_api_key)
|
|
83
288
|
else:
|
|
84
289
|
# Use local extension (Free tier)
|
|
85
290
|
return _snapshot_via_extension(browser, options)
|
|
@@ -127,10 +332,15 @@ def _snapshot_via_extension(
|
|
|
127
332
|
if options.save_trace:
|
|
128
333
|
_save_trace_to_file(result.get("raw_elements", []), options.trace_path)
|
|
129
334
|
|
|
335
|
+
# Validate and parse with Pydantic
|
|
336
|
+
snapshot_obj = Snapshot(**result)
|
|
337
|
+
|
|
130
338
|
# Show visual overlay if requested
|
|
131
339
|
if options.show_overlay:
|
|
132
|
-
|
|
133
|
-
|
|
340
|
+
# Prefer processed semantic elements for overlay (have bbox/importance/visual_cues).
|
|
341
|
+
# raw_elements may not match the overlay renderer's expected shape.
|
|
342
|
+
elements_for_overlay = result.get("elements") or result.get("raw_elements") or []
|
|
343
|
+
if elements_for_overlay:
|
|
134
344
|
browser.page.evaluate(
|
|
135
345
|
"""
|
|
136
346
|
(elements) => {
|
|
@@ -139,27 +349,46 @@ def _snapshot_via_extension(
|
|
|
139
349
|
}
|
|
140
350
|
}
|
|
141
351
|
""",
|
|
142
|
-
|
|
352
|
+
elements_for_overlay,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Show grid overlay if requested
|
|
356
|
+
if options.show_grid:
|
|
357
|
+
# Get all grids (don't filter by grid_id here - we want to show all but highlight the target)
|
|
358
|
+
grids = snapshot_obj.get_grid_bounds(grid_id=None)
|
|
359
|
+
if grids:
|
|
360
|
+
# Convert GridInfo to dict for JavaScript
|
|
361
|
+
grid_dicts = [grid.model_dump() for grid in grids]
|
|
362
|
+
# Pass grid_id as targetGridId to highlight it in red
|
|
363
|
+
target_grid_id = options.grid_id if options.grid_id is not None else None
|
|
364
|
+
browser.page.evaluate(
|
|
365
|
+
"""
|
|
366
|
+
(grids, targetGridId) => {
|
|
367
|
+
if (window.sentience && window.sentience.showGrid) {
|
|
368
|
+
window.sentience.showGrid(grids, targetGridId);
|
|
369
|
+
} else {
|
|
370
|
+
console.warn('[SDK] showGrid not available in extension');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
""",
|
|
374
|
+
grid_dicts,
|
|
375
|
+
target_grid_id,
|
|
143
376
|
)
|
|
144
377
|
|
|
145
|
-
# Validate and parse with Pydantic
|
|
146
|
-
snapshot_obj = Snapshot(**result)
|
|
147
378
|
return snapshot_obj
|
|
148
379
|
|
|
149
380
|
|
|
150
381
|
def _snapshot_via_api(
|
|
151
382
|
browser: SentienceBrowser,
|
|
152
383
|
options: SnapshotOptions,
|
|
384
|
+
api_key: str,
|
|
153
385
|
) -> Snapshot:
|
|
154
386
|
"""Take snapshot using server-side API (Pro/Enterprise tier)"""
|
|
155
387
|
if not browser.page:
|
|
156
388
|
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
157
389
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if not browser.api_url:
|
|
162
|
-
raise ValueError("API URL required for server-side processing")
|
|
390
|
+
# Use browser.api_url if set, otherwise default
|
|
391
|
+
api_url = browser.api_url or SENTIENCE_API_URL
|
|
163
392
|
|
|
164
393
|
# CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
|
|
165
394
|
# Even for API mode, we need the extension to collect raw data locally
|
|
@@ -169,6 +398,14 @@ def _snapshot_via_api(
|
|
|
169
398
|
raw_options: dict[str, Any] = {}
|
|
170
399
|
if options.screenshot is not False:
|
|
171
400
|
raw_options["screenshot"] = options.screenshot
|
|
401
|
+
# Important: also pass limit/filter to extension to keep raw_elements payload bounded.
|
|
402
|
+
# Without this, large pages (e.g. Amazon) can exceed gateway request size limits (HTTP 413).
|
|
403
|
+
if options.limit != 50:
|
|
404
|
+
raw_options["limit"] = options.limit
|
|
405
|
+
if options.filter is not None:
|
|
406
|
+
raw_options["filter"] = (
|
|
407
|
+
options.filter.model_dump() if hasattr(options.filter, "model_dump") else options.filter
|
|
408
|
+
)
|
|
172
409
|
|
|
173
410
|
raw_result = BrowserEvaluator.invoke(browser.page, SentienceMethod.SNAPSHOT, **raw_options)
|
|
174
411
|
|
|
@@ -179,54 +416,16 @@ def _snapshot_via_api(
|
|
|
179
416
|
# Step 2: Send to server for smart ranking/filtering
|
|
180
417
|
# Use raw_elements (raw data) instead of elements (processed data)
|
|
181
418
|
# Server validates API key and applies proprietary ranking logic
|
|
182
|
-
payload =
|
|
183
|
-
"raw_elements": raw_result.get("raw_elements", []), # Raw data needed for server processing
|
|
184
|
-
"url": raw_result.get("url", ""),
|
|
185
|
-
"viewport": raw_result.get("viewport"),
|
|
186
|
-
"goal": options.goal, # Optional goal/task description
|
|
187
|
-
"options": {
|
|
188
|
-
"limit": options.limit,
|
|
189
|
-
"filter": options.filter.model_dump() if options.filter else None,
|
|
190
|
-
},
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
# Check payload size before sending (server has 10MB limit)
|
|
194
|
-
payload_json = json.dumps(payload)
|
|
195
|
-
payload_size = len(payload_json.encode("utf-8"))
|
|
196
|
-
if payload_size > MAX_PAYLOAD_BYTES:
|
|
197
|
-
raise ValueError(
|
|
198
|
-
f"Payload size ({payload_size / 1024 / 1024:.2f}MB) exceeds server limit "
|
|
199
|
-
f"({MAX_PAYLOAD_BYTES / 1024 / 1024:.0f}MB). "
|
|
200
|
-
f"Try reducing the number of elements on the page or filtering elements."
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
headers = {
|
|
204
|
-
"Authorization": f"Bearer {browser.api_key}",
|
|
205
|
-
"Content-Type": "application/json",
|
|
206
|
-
}
|
|
419
|
+
payload = _build_snapshot_payload(raw_result, options)
|
|
207
420
|
|
|
208
421
|
try:
|
|
209
|
-
|
|
210
|
-
f"{browser.api_url}/v1/snapshot",
|
|
211
|
-
data=payload_json, # Reuse already-serialized JSON
|
|
212
|
-
headers=headers,
|
|
213
|
-
timeout=30,
|
|
214
|
-
)
|
|
215
|
-
response.raise_for_status()
|
|
216
|
-
|
|
217
|
-
api_result = response.json()
|
|
422
|
+
api_result = _post_snapshot_to_gateway_sync(payload, api_key, api_url)
|
|
218
423
|
|
|
219
424
|
# Merge API result with local data (screenshot, etc.)
|
|
220
|
-
snapshot_data =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"viewport": api_result.get("viewport", raw_result.get("viewport")),
|
|
225
|
-
"elements": api_result.get("elements", []),
|
|
226
|
-
"screenshot": raw_result.get("screenshot"), # Keep local screenshot
|
|
227
|
-
"screenshot_format": raw_result.get("screenshot_format"),
|
|
228
|
-
"error": api_result.get("error"),
|
|
229
|
-
}
|
|
425
|
+
snapshot_data = _merge_api_result_with_local(api_result, raw_result)
|
|
426
|
+
|
|
427
|
+
# Create snapshot object
|
|
428
|
+
snapshot_obj = Snapshot(**snapshot_data)
|
|
230
429
|
|
|
231
430
|
# Show visual overlay if requested (use API-ranked elements)
|
|
232
431
|
if options.show_overlay:
|
|
@@ -243,9 +442,31 @@ def _snapshot_via_api(
|
|
|
243
442
|
elements,
|
|
244
443
|
)
|
|
245
444
|
|
|
246
|
-
|
|
445
|
+
# Show grid overlay if requested
|
|
446
|
+
if options.show_grid:
|
|
447
|
+
# Get all grids (don't filter by grid_id here - we want to show all but highlight the target)
|
|
448
|
+
grids = snapshot_obj.get_grid_bounds(grid_id=None)
|
|
449
|
+
if grids:
|
|
450
|
+
grid_dicts = [grid.model_dump() for grid in grids]
|
|
451
|
+
# Pass grid_id as targetGridId to highlight it in red
|
|
452
|
+
target_grid_id = options.grid_id if options.grid_id is not None else None
|
|
453
|
+
browser.page.evaluate(
|
|
454
|
+
"""
|
|
455
|
+
(grids, targetGridId) => {
|
|
456
|
+
if (window.sentience && window.sentience.showGrid) {
|
|
457
|
+
window.sentience.showGrid(grids, targetGridId);
|
|
458
|
+
} else {
|
|
459
|
+
console.warn('[SDK] showGrid not available in extension');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
""",
|
|
463
|
+
grid_dicts,
|
|
464
|
+
target_grid_id,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
return snapshot_obj
|
|
247
468
|
except requests.exceptions.RequestException as e:
|
|
248
|
-
raise RuntimeError(f"API request failed: {e}")
|
|
469
|
+
raise RuntimeError(f"API request failed: {e}") from e
|
|
249
470
|
|
|
250
471
|
|
|
251
472
|
# ========== Async Snapshot Functions ==========
|
|
@@ -281,14 +502,18 @@ async def snapshot_async(
|
|
|
281
502
|
if options is None:
|
|
282
503
|
options = SnapshotOptions()
|
|
283
504
|
|
|
505
|
+
# Resolve API key: options.sentience_api_key takes precedence, then browser.api_key
|
|
506
|
+
# This allows browser-use users to pass api_key via options without SentienceBrowser
|
|
507
|
+
effective_api_key = options.sentience_api_key or browser.api_key
|
|
508
|
+
|
|
284
509
|
# Determine if we should use server-side API
|
|
285
510
|
should_use_api = (
|
|
286
|
-
options.use_api if options.use_api is not None else (
|
|
511
|
+
options.use_api if options.use_api is not None else (effective_api_key is not None)
|
|
287
512
|
)
|
|
288
513
|
|
|
289
|
-
if should_use_api and
|
|
514
|
+
if should_use_api and effective_api_key:
|
|
290
515
|
# Use server-side API (Pro/Enterprise tier)
|
|
291
|
-
return await _snapshot_via_api_async(browser, options)
|
|
516
|
+
return await _snapshot_via_api_async(browser, options, effective_api_key)
|
|
292
517
|
else:
|
|
293
518
|
# Use local extension (Free tier)
|
|
294
519
|
return await _snapshot_via_extension_async(browser, options)
|
|
@@ -304,18 +529,20 @@ async def _snapshot_via_extension_async(
|
|
|
304
529
|
|
|
305
530
|
# Wait for extension injection to complete
|
|
306
531
|
try:
|
|
307
|
-
await
|
|
532
|
+
await _wait_for_function_with_nav_retry(
|
|
533
|
+
browser.page,
|
|
308
534
|
"typeof window.sentience !== 'undefined'",
|
|
309
|
-
|
|
535
|
+
timeout_ms=5000,
|
|
310
536
|
)
|
|
311
537
|
except Exception as e:
|
|
312
538
|
try:
|
|
313
|
-
diag = await
|
|
539
|
+
diag = await _page_evaluate_with_nav_retry(
|
|
540
|
+
browser.page,
|
|
314
541
|
"""() => ({
|
|
315
542
|
sentience_defined: typeof window.sentience !== 'undefined',
|
|
316
543
|
extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
|
|
317
544
|
url: window.location.href
|
|
318
|
-
})"""
|
|
545
|
+
})""",
|
|
319
546
|
)
|
|
320
547
|
except Exception:
|
|
321
548
|
diag = {"error": "Could not gather diagnostics"}
|
|
@@ -341,7 +568,8 @@ async def _snapshot_via_extension_async(
|
|
|
341
568
|
)
|
|
342
569
|
|
|
343
570
|
# Call extension API
|
|
344
|
-
result = await
|
|
571
|
+
result = await _page_evaluate_with_nav_retry(
|
|
572
|
+
browser.page,
|
|
345
573
|
"""
|
|
346
574
|
(options) => {
|
|
347
575
|
return window.sentience.snapshot(options);
|
|
@@ -356,11 +584,26 @@ async def _snapshot_via_extension_async(
|
|
|
356
584
|
if options.save_trace:
|
|
357
585
|
_save_trace_to_file(result.get("raw_elements", []), options.trace_path)
|
|
358
586
|
|
|
587
|
+
# Extract screenshot_format from data URL if not provided by extension
|
|
588
|
+
if result.get("screenshot") and not result.get("screenshot_format"):
|
|
589
|
+
screenshot_data_url = result.get("screenshot", "")
|
|
590
|
+
if screenshot_data_url.startswith("data:image/"):
|
|
591
|
+
# Extract format from "data:image/jpeg;base64,..." or "data:image/png;base64,..."
|
|
592
|
+
format_match = screenshot_data_url.split(";")[0].split("/")[-1]
|
|
593
|
+
if format_match in ["jpeg", "jpg", "png"]:
|
|
594
|
+
result["screenshot_format"] = "jpeg" if format_match in ["jpeg", "jpg"] else "png"
|
|
595
|
+
|
|
596
|
+
# Validate and parse with Pydantic
|
|
597
|
+
snapshot_obj = Snapshot(**result)
|
|
598
|
+
|
|
359
599
|
# Show visual overlay if requested
|
|
360
600
|
if options.show_overlay:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
601
|
+
# Prefer processed semantic elements for overlay (have bbox/importance/visual_cues).
|
|
602
|
+
# raw_elements may not match the overlay renderer's expected shape.
|
|
603
|
+
elements_for_overlay = result.get("elements") or result.get("raw_elements") or []
|
|
604
|
+
if elements_for_overlay:
|
|
605
|
+
await _page_evaluate_with_nav_retry(
|
|
606
|
+
browser.page,
|
|
364
607
|
"""
|
|
365
608
|
(elements) => {
|
|
366
609
|
if (window.sentience && window.sentience.showOverlay) {
|
|
@@ -368,41 +611,53 @@ async def _snapshot_via_extension_async(
|
|
|
368
611
|
}
|
|
369
612
|
}
|
|
370
613
|
""",
|
|
371
|
-
|
|
614
|
+
elements_for_overlay,
|
|
372
615
|
)
|
|
373
616
|
|
|
374
|
-
#
|
|
375
|
-
if
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
617
|
+
# Show grid overlay if requested
|
|
618
|
+
if options.show_grid:
|
|
619
|
+
# Get all grids (don't filter by grid_id here - we want to show all but highlight the target)
|
|
620
|
+
grids = snapshot_obj.get_grid_bounds(grid_id=None)
|
|
621
|
+
if grids:
|
|
622
|
+
grid_dicts = [grid.model_dump() for grid in grids]
|
|
623
|
+
# Pass grid_id as targetGridId to highlight it in red
|
|
624
|
+
target_grid_id = options.grid_id if options.grid_id is not None else None
|
|
625
|
+
await _page_evaluate_with_nav_retry(
|
|
626
|
+
browser.page,
|
|
627
|
+
"""
|
|
628
|
+
(args) => {
|
|
629
|
+
const [grids, targetGridId] = args;
|
|
630
|
+
if (window.sentience && window.sentience.showGrid) {
|
|
631
|
+
window.sentience.showGrid(grids, targetGridId);
|
|
632
|
+
} else {
|
|
633
|
+
console.warn('[SDK] showGrid not available in extension');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
""",
|
|
637
|
+
[grid_dicts, target_grid_id],
|
|
638
|
+
)
|
|
382
639
|
|
|
383
|
-
# Validate and parse with Pydantic
|
|
384
|
-
snapshot_obj = Snapshot(**result)
|
|
385
640
|
return snapshot_obj
|
|
386
641
|
|
|
387
642
|
|
|
388
643
|
async def _snapshot_via_api_async(
|
|
389
644
|
browser: AsyncSentienceBrowser,
|
|
390
645
|
options: SnapshotOptions,
|
|
646
|
+
api_key: str,
|
|
391
647
|
) -> Snapshot:
|
|
392
648
|
"""Take snapshot using server-side API (Pro/Enterprise tier) - async"""
|
|
393
649
|
if not browser.page:
|
|
394
650
|
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
395
651
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if not browser.api_url:
|
|
400
|
-
raise ValueError("API URL required for server-side processing")
|
|
652
|
+
# Use browser.api_url if set, otherwise default
|
|
653
|
+
api_url = browser.api_url or SENTIENCE_API_URL
|
|
401
654
|
|
|
402
655
|
# Wait for extension injection
|
|
403
656
|
try:
|
|
404
|
-
await
|
|
405
|
-
|
|
657
|
+
await _wait_for_function_with_nav_retry(
|
|
658
|
+
browser.page,
|
|
659
|
+
"typeof window.sentience !== 'undefined'",
|
|
660
|
+
timeout_ms=5000,
|
|
406
661
|
)
|
|
407
662
|
except Exception as e:
|
|
408
663
|
raise RuntimeError(
|
|
@@ -419,8 +674,17 @@ async def _snapshot_via_api_async(
|
|
|
419
674
|
raw_options["screenshot"] = options.screenshot.model_dump()
|
|
420
675
|
else:
|
|
421
676
|
raw_options["screenshot"] = options.screenshot
|
|
677
|
+
# Important: also pass limit/filter to extension to keep raw_elements payload bounded.
|
|
678
|
+
# Without this, large pages (e.g. Amazon) can exceed gateway request size limits (HTTP 413).
|
|
679
|
+
if options.limit != 50:
|
|
680
|
+
raw_options["limit"] = options.limit
|
|
681
|
+
if options.filter is not None:
|
|
682
|
+
raw_options["filter"] = (
|
|
683
|
+
options.filter.model_dump() if hasattr(options.filter, "model_dump") else options.filter
|
|
684
|
+
)
|
|
422
685
|
|
|
423
|
-
raw_result = await
|
|
686
|
+
raw_result = await _page_evaluate_with_nav_retry(
|
|
687
|
+
browser.page,
|
|
424
688
|
"""
|
|
425
689
|
(options) => {
|
|
426
690
|
return window.sentience.snapshot(options);
|
|
@@ -466,7 +730,7 @@ async def _snapshot_via_api_async(
|
|
|
466
730
|
)
|
|
467
731
|
|
|
468
732
|
headers = {
|
|
469
|
-
"Authorization": f"Bearer {
|
|
733
|
+
"Authorization": f"Bearer {api_key}",
|
|
470
734
|
"Content-Type": "application/json",
|
|
471
735
|
}
|
|
472
736
|
|
|
@@ -476,7 +740,7 @@ async def _snapshot_via_api_async(
|
|
|
476
740
|
|
|
477
741
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
478
742
|
response = await client.post(
|
|
479
|
-
f"{
|
|
743
|
+
f"{api_url}/v1/snapshot",
|
|
480
744
|
content=payload_json,
|
|
481
745
|
headers=headers,
|
|
482
746
|
)
|
|
@@ -502,11 +766,15 @@ async def _snapshot_via_api_async(
|
|
|
502
766
|
"error": api_result.get("error"),
|
|
503
767
|
}
|
|
504
768
|
|
|
769
|
+
# Create snapshot object
|
|
770
|
+
snapshot_obj = Snapshot(**snapshot_data)
|
|
771
|
+
|
|
505
772
|
# Show visual overlay if requested
|
|
506
773
|
if options.show_overlay:
|
|
507
774
|
elements = api_result.get("elements", [])
|
|
508
775
|
if elements:
|
|
509
|
-
await
|
|
776
|
+
await _page_evaluate_with_nav_retry(
|
|
777
|
+
browser.page,
|
|
510
778
|
"""
|
|
511
779
|
(elements) => {
|
|
512
780
|
if (window.sentience && window.sentience.showOverlay) {
|
|
@@ -517,7 +785,30 @@ async def _snapshot_via_api_async(
|
|
|
517
785
|
elements,
|
|
518
786
|
)
|
|
519
787
|
|
|
520
|
-
|
|
788
|
+
# Show grid overlay if requested
|
|
789
|
+
if options.show_grid:
|
|
790
|
+
# Get all grids (don't filter by grid_id here - we want to show all but highlight the target)
|
|
791
|
+
grids = snapshot_obj.get_grid_bounds(grid_id=None)
|
|
792
|
+
if grids:
|
|
793
|
+
grid_dicts = [grid.model_dump() for grid in grids]
|
|
794
|
+
# Pass grid_id as targetGridId to highlight it in red
|
|
795
|
+
target_grid_id = options.grid_id if options.grid_id is not None else None
|
|
796
|
+
await _page_evaluate_with_nav_retry(
|
|
797
|
+
browser.page,
|
|
798
|
+
"""
|
|
799
|
+
(args) => {
|
|
800
|
+
const [grids, targetGridId] = args;
|
|
801
|
+
if (window.sentience && window.sentience.showGrid) {
|
|
802
|
+
window.sentience.showGrid(grids, targetGridId);
|
|
803
|
+
} else {
|
|
804
|
+
console.warn('[SDK] showGrid not available in extension');
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
""",
|
|
808
|
+
[grid_dicts, target_grid_id],
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
return snapshot_obj
|
|
521
812
|
except ImportError:
|
|
522
813
|
# Fallback to requests if httpx not available (shouldn't happen in async context)
|
|
523
814
|
raise RuntimeError(
|