2525from typing import TYPE_CHECKING , Any
2626
2727from ..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+ )
2833from .exceptions import ExtensionDiagnostics , ExtensionNotLoadedError , SnapshotError
2934
3035if 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
223209async 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+
279397def _build_extension_options (options : SnapshotOptions ) -> dict [str , Any ]:
280398 """Build options dict for extension API call."""
281399 ext_options : dict [str , Any ] = {}
0 commit comments