-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsnapshot.py
More file actions
186 lines (158 loc) · 6.11 KB
/
snapshot.py
File metadata and controls
186 lines (158 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""
Snapshot functionality - calls window.sentience.snapshot() or server-side API
"""
from typing import Any
import requests
from .browser import SentienceBrowser
from .models import Snapshot
def snapshot(
browser: SentienceBrowser,
screenshot: bool | None = None,
limit: int | None = None,
filter: dict[str, Any] | None = None,
use_api: bool | None = None,
) -> Snapshot:
"""
Take a snapshot of the current page
Args:
browser: SentienceBrowser instance
screenshot: Whether to capture screenshot (bool or dict with format/quality)
limit: Limit number of elements returned
filter: Filter options (min_area, allowed_roles, min_z_index)
use_api: Force use of server-side API if True, local extension if False.
If None, uses API if api_key is set, otherwise uses local extension.
Returns:
Snapshot object
"""
# Determine if we should use server-side API
should_use_api = use_api if use_api is not None else (browser.api_key is not None)
if should_use_api and browser.api_key:
# Use server-side API (Pro/Enterprise tier)
return _snapshot_via_api(browser, screenshot, limit, filter)
else:
# Use local extension (Free tier)
return _snapshot_via_extension(browser, screenshot, limit, filter)
def _snapshot_via_extension(
browser: SentienceBrowser,
screenshot: bool | None,
limit: int | None,
filter: dict[str, Any] | None,
) -> Snapshot:
"""Take snapshot using local extension (Free tier)"""
if not browser.page:
raise RuntimeError("Browser not started. Call browser.start() first.")
# CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
# The new architecture loads injected_api.js asynchronously, so window.sentience
# may not be immediately available after page load
try:
browser.page.wait_for_function(
"typeof window.sentience !== 'undefined'", timeout=5000 # 5 second timeout
)
except Exception as e:
# Gather diagnostics if wait fails
try:
diag = browser.page.evaluate(
"""() => ({
sentience_defined: typeof window.sentience !== 'undefined',
extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
url: window.location.href
})"""
)
except Exception:
diag = {"error": "Could not gather diagnostics"}
raise RuntimeError(
f"Sentience extension failed to inject window.sentience API. "
f"Is the extension loaded? Diagnostics: {diag}"
) from e
# Build options
options: dict[str, Any] = {}
if screenshot is not None:
options["screenshot"] = screenshot
if limit is not None:
options["limit"] = limit
if filter is not None:
options["filter"] = filter
# Call extension API
result = browser.page.evaluate(
"""
(options) => {
return window.sentience.snapshot(options);
}
""",
options,
)
# Validate and parse with Pydantic
snapshot_obj = Snapshot(**result)
return snapshot_obj
def _snapshot_via_api(
browser: SentienceBrowser,
screenshot: bool | None,
limit: int | None,
filter: dict[str, Any] | None,
) -> Snapshot:
"""Take snapshot using server-side API (Pro/Enterprise tier)"""
if not browser.page:
raise RuntimeError("Browser not started. Call browser.start() first.")
if not browser.api_key:
raise ValueError("API key required for server-side processing")
if not browser.api_url:
raise ValueError("API URL required for server-side processing")
# CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
# Even for API mode, we need the extension to collect raw data locally
try:
browser.page.wait_for_function("typeof window.sentience !== 'undefined'", timeout=5000)
except Exception as e:
raise RuntimeError(
"Sentience extension failed to inject. Cannot collect raw data for API processing."
) from e
# Step 1: Get raw data from local extension (always happens locally)
raw_options: dict[str, Any] = {}
if screenshot is not None:
raw_options["screenshot"] = screenshot
raw_result = browser.page.evaluate(
"""
(options) => {
return window.sentience.snapshot(options);
}
""",
raw_options,
)
# Step 2: Send to server for smart ranking/filtering
# Use raw_elements (raw data) instead of elements (processed data)
# Server validates API key and applies proprietary ranking logic
payload = {
"raw_elements": raw_result.get("raw_elements", []), # Raw data needed for server processing
"url": raw_result.get("url", ""),
"viewport": raw_result.get("viewport"),
"options": {
"limit": limit,
"filter": filter,
},
}
headers = {
"Authorization": f"Bearer {browser.api_key}",
"Content-Type": "application/json",
}
try:
response = requests.post(
f"{browser.api_url}/v1/snapshot",
json=payload,
headers=headers,
timeout=30,
)
response.raise_for_status()
api_result = response.json()
# Merge API result with local data (screenshot, etc.)
snapshot_data = {
"status": api_result.get("status", "success"),
"timestamp": api_result.get("timestamp"),
"url": api_result.get("url", raw_result.get("url", "")),
"viewport": api_result.get("viewport", raw_result.get("viewport")),
"elements": api_result.get("elements", []),
"screenshot": raw_result.get("screenshot"), # Keep local screenshot
"screenshot_format": raw_result.get("screenshot_format"),
"error": api_result.get("error"),
}
return Snapshot(**snapshot_data)
except requests.exceptions.RequestException as e:
raise RuntimeError(f"API request failed: {e}")