Skip to content

Commit c598d49

Browse files
author
SentienceDev
committed
backend and regular snapshot consistent
1 parent 6c1405c commit c598d49

File tree

5 files changed

+291
-102
lines changed

5 files changed

+291
-102
lines changed

examples/browser_use_integration.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,8 @@
1414

1515
import asyncio
1616

17-
# browser-use imports (install via: pip install browser-use)
18-
# from browser_use import BrowserSession, BrowserProfile
19-
2017
# Sentience imports
21-
from sentience import (
22-
find,
23-
get_extension_dir,
24-
query,
25-
)
18+
from sentience import find, get_extension_dir, query
2619
from sentience.backends import (
2720
BrowserUseAdapter,
2821
CachedSnapshot,
@@ -33,6 +26,9 @@
3326
type_text,
3427
)
3528

29+
# browser-use imports (install via: pip install browser-use)
30+
# from browser_use import BrowserSession, BrowserProfile
31+
3632

3733
async def main() -> None:
3834
"""

sentience/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
from .visual_agent import SentienceVisualAgent, SentienceVisualAgentAsync
119119
from .wait import wait_for
120120

121-
__version__ = "0.92.3"
121+
__version__ = "0.93.0"
122122

123123
__all__ = [
124124
# Extension helpers (for browser-use integration)

sentience/backends/snapshot.py

Lines changed: 165 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
from typing import TYPE_CHECKING, Any
2626

2727
from ..models import Snapshot, SnapshotOptions
28+
from ..snapshot import (
29+
_build_snapshot_payload,
30+
_merge_api_result_with_local,
31+
_post_snapshot_to_gateway_async,
32+
)
2833
from .exceptions import ExtensionDiagnostics, ExtensionNotLoadedError, SnapshotError
2934

3035
if TYPE_CHECKING:
@@ -145,79 +150,60 @@ async def snapshot(
145150
"""
146151
Take a Sentience snapshot using the backend protocol.
147152
148-
This function calls window.sentience.snapshot() via the backend's eval(),
149-
enabling snapshot collection with any BrowserBackendV0 implementation.
153+
This function respects the `use_api` option and can call either:
154+
- Server-side API (Pro/Enterprise tier) when `use_api=True` and API key is provided
155+
- Local extension (Free tier) when `use_api=False` or no API key
150156
151157
Requires:
152158
- Sentience extension loaded in browser (via --load-extension)
153159
- Extension injected window.sentience API
154160
155161
Args:
156162
backend: BrowserBackendV0 implementation (CDPBackendV0, PlaywrightBackend, etc.)
157-
options: Snapshot options (limit, filter, screenshot, etc.)
163+
options: Snapshot options (limit, filter, screenshot, use_api, sentience_api_key, etc.)
158164
159165
Returns:
160166
Snapshot with elements, viewport, and optional screenshot
161167
162168
Example:
163169
from sentience.backends import BrowserUseAdapter
164-
from sentience.backends.snapshot import snapshot_from_backend
170+
from sentience.backends.snapshot import snapshot
171+
from sentience.models import SnapshotOptions
165172
166173
adapter = BrowserUseAdapter(session)
167174
backend = await adapter.create_backend()
168175
169-
# Basic snapshot
170-
snap = await snapshot_from_backend(backend)
176+
# Basic snapshot (uses local extension)
177+
snap = await snapshot(backend)
171178
172-
# With options
173-
snap = await snapshot_from_backend(backend, SnapshotOptions(
179+
# With server-side API (Pro/Enterprise tier)
180+
snap = await snapshot(backend, SnapshotOptions(
181+
use_api=True,
182+
sentience_api_key="sk_pro_xxxxx",
174183
limit=100,
175184
screenshot=True
176185
))
186+
187+
# Force local extension (Free tier)
188+
snap = await snapshot(backend, SnapshotOptions(
189+
use_api=False
190+
))
177191
"""
178192
if options is None:
179193
options = SnapshotOptions()
180194

181-
# Wait for extension injection
182-
await _wait_for_extension(backend, timeout_ms=5000)
183-
184-
# Build options dict for extension API
185-
ext_options = _build_extension_options(options)
186-
187-
# Call extension's snapshot function
188-
result = await backend.eval(
189-
f"""
190-
(() => {{
191-
const options = {_json_serialize(ext_options)};
192-
return window.sentience.snapshot(options);
193-
}})()
194-
"""
195+
# Determine if we should use server-side API
196+
# Same logic as main snapshot() function in sentience/snapshot.py
197+
should_use_api = (
198+
options.use_api if options.use_api is not None else (options.sentience_api_key is not None)
195199
)
196200

197-
if result is None:
198-
# Try to get URL for better error message
199-
try:
200-
url = await backend.eval("window.location.href")
201-
except Exception:
202-
url = None
203-
raise SnapshotError.from_null_result(url=url)
204-
205-
# Show overlay if requested
206-
if options.show_overlay:
207-
raw_elements = result.get("raw_elements", [])
208-
if raw_elements:
209-
await backend.eval(
210-
f"""
211-
(() => {{
212-
if (window.sentience && window.sentience.showOverlay) {{
213-
window.sentience.showOverlay({_json_serialize(raw_elements)}, null);
214-
}}
215-
}})()
216-
"""
217-
)
218-
219-
# Build and return Snapshot
220-
return Snapshot(**result)
201+
if should_use_api and options.sentience_api_key:
202+
# Use server-side API (Pro/Enterprise tier)
203+
return await _snapshot_via_api(backend, options)
204+
else:
205+
# Use local extension (Free tier)
206+
return await _snapshot_via_extension(backend, options)
221207

222208

223209
async def _wait_for_extension(
@@ -235,12 +221,23 @@ async def _wait_for_extension(
235221
RuntimeError: If extension not injected within timeout
236222
"""
237223
import asyncio
224+
import logging
225+
226+
logger = logging.getLogger("sentience.backends.snapshot")
238227

239228
start = time.monotonic()
240229
timeout_sec = timeout_ms / 1000.0
230+
poll_count = 0
231+
232+
logger.debug(f"Waiting for extension injection (timeout={timeout_ms}ms)...")
241233

242234
while True:
243235
elapsed = time.monotonic() - start
236+
poll_count += 1
237+
238+
if poll_count % 10 == 0: # Log every 10 polls (~1 second)
239+
logger.debug(f"Extension poll #{poll_count}, elapsed={elapsed*1000:.0f}ms")
240+
244241
if elapsed >= timeout_sec:
245242
# Gather diagnostics
246243
try:
@@ -249,11 +246,14 @@ async def _wait_for_extension(
249246
(() => ({
250247
sentience_defined: typeof window.sentience !== 'undefined',
251248
sentience_snapshot: typeof window.sentience?.snapshot === 'function',
252-
url: window.location.href
249+
url: window.location.href,
250+
extension_id: document.documentElement.dataset.sentienceExtensionId || null,
251+
has_content_script: !!document.documentElement.dataset.sentienceExtensionId
253252
}))()
254253
"""
255254
)
256255
diagnostics = ExtensionDiagnostics.from_dict(diag_dict)
256+
logger.debug(f"Extension diagnostics: {diag_dict}")
257257
except Exception as e:
258258
diagnostics = ExtensionDiagnostics(error=f"Could not gather diagnostics: {e}")
259259

@@ -276,6 +276,124 @@ async def _wait_for_extension(
276276
await asyncio.sleep(0.1)
277277

278278

279+
async def _snapshot_via_extension(
280+
backend: "BrowserBackendV0",
281+
options: SnapshotOptions,
282+
) -> Snapshot:
283+
"""Take snapshot using local extension (Free tier)"""
284+
# Wait for extension injection
285+
await _wait_for_extension(backend, timeout_ms=5000)
286+
287+
# Build options dict for extension API
288+
ext_options = _build_extension_options(options)
289+
290+
# Call extension's snapshot function
291+
result = await backend.eval(
292+
f"""
293+
(() => {{
294+
const options = {_json_serialize(ext_options)};
295+
return window.sentience.snapshot(options);
296+
}})()
297+
"""
298+
)
299+
300+
if result is None:
301+
# Try to get URL for better error message
302+
try:
303+
url = await backend.eval("window.location.href")
304+
except Exception:
305+
url = None
306+
raise SnapshotError.from_null_result(url=url)
307+
308+
# Show overlay if requested
309+
if options.show_overlay:
310+
raw_elements = result.get("raw_elements", [])
311+
if raw_elements:
312+
await backend.eval(
313+
f"""
314+
(() => {{
315+
if (window.sentience && window.sentience.showOverlay) {{
316+
window.sentience.showOverlay({_json_serialize(raw_elements)}, null);
317+
}}
318+
}})()
319+
"""
320+
)
321+
322+
# Build and return Snapshot
323+
return Snapshot(**result)
324+
325+
326+
async def _snapshot_via_api(
327+
backend: "BrowserBackendV0",
328+
options: SnapshotOptions,
329+
) -> Snapshot:
330+
"""Take snapshot using server-side API (Pro/Enterprise tier)"""
331+
# Default API URL (same as main snapshot function)
332+
api_url = "https://api.sentienceapi.com"
333+
334+
# Wait for extension injection (needed even for API mode to collect raw data)
335+
await _wait_for_extension(backend, timeout_ms=5000)
336+
337+
# Step 1: Get raw data from local extension (always happens locally)
338+
raw_options: dict[str, Any] = {}
339+
if options.screenshot is not False:
340+
raw_options["screenshot"] = options.screenshot
341+
342+
# Call extension to get raw elements
343+
raw_result = await backend.eval(
344+
f"""
345+
(() => {{
346+
const options = {_json_serialize(raw_options)};
347+
return window.sentience.snapshot(options);
348+
}})()
349+
"""
350+
)
351+
352+
if raw_result is None:
353+
try:
354+
url = await backend.eval("window.location.href")
355+
except Exception:
356+
url = None
357+
raise SnapshotError.from_null_result(url=url)
358+
359+
# Step 2: Send to server for smart ranking/filtering
360+
payload = _build_snapshot_payload(raw_result, options)
361+
362+
try:
363+
api_result = await _post_snapshot_to_gateway_async(
364+
payload, options.sentience_api_key, api_url
365+
)
366+
367+
# Merge API result with local data (screenshot, etc.)
368+
snapshot_data = _merge_api_result_with_local(api_result, raw_result)
369+
370+
# Show visual overlay if requested (use API-ranked elements)
371+
if options.show_overlay:
372+
elements = api_result.get("elements", [])
373+
if elements:
374+
await backend.eval(
375+
f"""
376+
(() => {{
377+
if (window.sentience && window.sentience.showOverlay) {{
378+
window.sentience.showOverlay({_json_serialize(elements)}, null);
379+
}}
380+
}})()
381+
"""
382+
)
383+
384+
return Snapshot(**snapshot_data)
385+
except (RuntimeError, ValueError):
386+
# Re-raise validation errors as-is
387+
raise
388+
except Exception as e:
389+
# Fallback to local extension on API error
390+
# This matches the behavior of the main snapshot function
391+
raise RuntimeError(
392+
f"Server-side snapshot API failed: {e}. "
393+
"Try using use_api=False to use local extension instead."
394+
) from e
395+
396+
279397
def _build_extension_options(options: SnapshotOptions) -> dict[str, Any]:
280398
"""Build options dict for extension API call."""
281399
ext_options: dict[str, Any] = {}

sentience/extension/background.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import init, { analyze_page_with_options, analyze_page, prune_for_api } from "../pkg/sentience_core.js";
1+
import init, { analyze_page_with_options, analyze_page, prune_for_api } from "./pkg/sentience_core.js";
22

33
let wasmReady = !1, wasmInitPromise = null;
44

0 commit comments

Comments
 (0)