sentienceapi 0.90.17__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.

Files changed (50) hide show
  1. sentience/__init__.py +153 -0
  2. sentience/_extension_loader.py +40 -0
  3. sentience/actions.py +837 -0
  4. sentience/agent.py +1246 -0
  5. sentience/agent_config.py +43 -0
  6. sentience/async_api.py +101 -0
  7. sentience/base_agent.py +194 -0
  8. sentience/browser.py +1037 -0
  9. sentience/cli.py +130 -0
  10. sentience/cloud_tracing.py +382 -0
  11. sentience/conversational_agent.py +509 -0
  12. sentience/expect.py +188 -0
  13. sentience/extension/background.js +233 -0
  14. sentience/extension/content.js +298 -0
  15. sentience/extension/injected_api.js +1473 -0
  16. sentience/extension/manifest.json +36 -0
  17. sentience/extension/pkg/sentience_core.d.ts +51 -0
  18. sentience/extension/pkg/sentience_core.js +529 -0
  19. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  20. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  21. sentience/extension/release.json +115 -0
  22. sentience/extension/test-content.js +4 -0
  23. sentience/formatting.py +59 -0
  24. sentience/generator.py +202 -0
  25. sentience/inspector.py +365 -0
  26. sentience/llm_provider.py +637 -0
  27. sentience/models.py +412 -0
  28. sentience/overlay.py +222 -0
  29. sentience/query.py +303 -0
  30. sentience/read.py +185 -0
  31. sentience/recorder.py +589 -0
  32. sentience/schemas/trace_v1.json +216 -0
  33. sentience/screenshot.py +100 -0
  34. sentience/snapshot.py +516 -0
  35. sentience/text_search.py +290 -0
  36. sentience/trace_indexing/__init__.py +27 -0
  37. sentience/trace_indexing/index_schema.py +111 -0
  38. sentience/trace_indexing/indexer.py +357 -0
  39. sentience/tracer_factory.py +211 -0
  40. sentience/tracing.py +285 -0
  41. sentience/utils.py +296 -0
  42. sentience/wait.py +137 -0
  43. sentienceapi-0.90.17.dist-info/METADATA +917 -0
  44. sentienceapi-0.90.17.dist-info/RECORD +50 -0
  45. sentienceapi-0.90.17.dist-info/WHEEL +5 -0
  46. sentienceapi-0.90.17.dist-info/entry_points.txt +2 -0
  47. sentienceapi-0.90.17.dist-info/licenses/LICENSE +24 -0
  48. sentienceapi-0.90.17.dist-info/licenses/LICENSE-APACHE +201 -0
  49. sentienceapi-0.90.17.dist-info/licenses/LICENSE-MIT +21 -0
  50. sentienceapi-0.90.17.dist-info/top_level.txt +1 -0
sentience/snapshot.py ADDED
@@ -0,0 +1,516 @@
1
+ """
2
+ Snapshot functionality - calls window.sentience.snapshot() or server-side API
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import time
9
+ from typing import Any, Optional
10
+
11
+ import requests
12
+
13
+ from .browser import AsyncSentienceBrowser, SentienceBrowser
14
+ from .models import Snapshot, SnapshotOptions
15
+
16
+ # Maximum payload size for API requests (10MB server limit)
17
+ MAX_PAYLOAD_BYTES = 10 * 1024 * 1024
18
+
19
+
20
+ def _save_trace_to_file(raw_elements: list[dict[str, Any]], trace_path: str | None = None) -> None:
21
+ """
22
+ Save raw_elements to a JSON file for benchmarking/training
23
+
24
+ Args:
25
+ raw_elements: Raw elements data from snapshot
26
+ trace_path: Path to save trace file. If None, uses "trace_{timestamp}.json"
27
+ """
28
+ # Default filename if none provided
29
+ filename = trace_path or f"trace_{int(time.time())}.json"
30
+
31
+ # Ensure directory exists
32
+ directory = os.path.dirname(filename)
33
+ if directory:
34
+ os.makedirs(directory, exist_ok=True)
35
+
36
+ # Save the raw elements to JSON
37
+ with open(filename, "w") as f:
38
+ json.dump(raw_elements, f, indent=2)
39
+
40
+ print(f"[SDK] Trace saved to: {filename}")
41
+
42
+
43
+ def snapshot(
44
+ browser: SentienceBrowser,
45
+ options: SnapshotOptions | None = None,
46
+ ) -> Snapshot:
47
+ """
48
+ Take a snapshot of the current page
49
+
50
+ Args:
51
+ browser: SentienceBrowser instance
52
+ options: Snapshot options (screenshot, limit, filter, etc.)
53
+ If None, uses default options.
54
+
55
+ Returns:
56
+ Snapshot object
57
+
58
+ Example:
59
+ # Basic snapshot with defaults
60
+ snap = snapshot(browser)
61
+
62
+ # With options
63
+ snap = snapshot(browser, SnapshotOptions(
64
+ screenshot=True,
65
+ limit=100,
66
+ show_overlay=True
67
+ ))
68
+ """
69
+ # Use default options if none provided
70
+ if options is None:
71
+ options = SnapshotOptions()
72
+
73
+ # Determine if we should use server-side API
74
+ should_use_api = (
75
+ options.use_api if options.use_api is not None else (browser.api_key is not None)
76
+ )
77
+
78
+ if should_use_api and browser.api_key:
79
+ # Use server-side API (Pro/Enterprise tier)
80
+ return _snapshot_via_api(browser, options)
81
+ else:
82
+ # Use local extension (Free tier)
83
+ return _snapshot_via_extension(browser, options)
84
+
85
+
86
+ def _snapshot_via_extension(
87
+ browser: SentienceBrowser,
88
+ options: SnapshotOptions,
89
+ ) -> Snapshot:
90
+ """Take snapshot using local extension (Free tier)"""
91
+ if not browser.page:
92
+ raise RuntimeError("Browser not started. Call browser.start() first.")
93
+
94
+ # CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
95
+ # The new architecture loads injected_api.js asynchronously, so window.sentience
96
+ # may not be immediately available after page load
97
+ try:
98
+ browser.page.wait_for_function(
99
+ "typeof window.sentience !== 'undefined'",
100
+ timeout=5000, # 5 second timeout
101
+ )
102
+ except Exception as e:
103
+ # Gather diagnostics if wait fails
104
+ try:
105
+ diag = browser.page.evaluate(
106
+ """() => ({
107
+ sentience_defined: typeof window.sentience !== 'undefined',
108
+ extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
109
+ url: window.location.href
110
+ })"""
111
+ )
112
+ except Exception:
113
+ diag = {"error": "Could not gather diagnostics"}
114
+
115
+ raise RuntimeError(
116
+ f"Sentience extension failed to inject window.sentience API. "
117
+ f"Is the extension loaded? Diagnostics: {diag}"
118
+ ) from e
119
+
120
+ # Build options dict for extension API (exclude save_trace/trace_path)
121
+ ext_options: dict[str, Any] = {}
122
+ if options.screenshot is not False:
123
+ ext_options["screenshot"] = options.screenshot
124
+ if options.limit != 50:
125
+ ext_options["limit"] = options.limit
126
+ if options.filter is not None:
127
+ ext_options["filter"] = (
128
+ options.filter.model_dump() if hasattr(options.filter, "model_dump") else options.filter
129
+ )
130
+
131
+ # Call extension API
132
+ result = browser.page.evaluate(
133
+ """
134
+ (options) => {
135
+ return window.sentience.snapshot(options);
136
+ }
137
+ """,
138
+ ext_options,
139
+ )
140
+
141
+ # Save trace if requested
142
+ if options.save_trace:
143
+ _save_trace_to_file(result.get("raw_elements", []), options.trace_path)
144
+
145
+ # Show visual overlay if requested
146
+ if options.show_overlay:
147
+ raw_elements = result.get("raw_elements", [])
148
+ if raw_elements:
149
+ browser.page.evaluate(
150
+ """
151
+ (elements) => {
152
+ if (window.sentience && window.sentience.showOverlay) {
153
+ window.sentience.showOverlay(elements, null);
154
+ }
155
+ }
156
+ """,
157
+ raw_elements,
158
+ )
159
+
160
+ # Validate and parse with Pydantic
161
+ snapshot_obj = Snapshot(**result)
162
+ return snapshot_obj
163
+
164
+
165
+ def _snapshot_via_api(
166
+ browser: SentienceBrowser,
167
+ options: SnapshotOptions,
168
+ ) -> Snapshot:
169
+ """Take snapshot using server-side API (Pro/Enterprise tier)"""
170
+ if not browser.page:
171
+ raise RuntimeError("Browser not started. Call browser.start() first.")
172
+
173
+ if not browser.api_key:
174
+ raise ValueError("API key required for server-side processing")
175
+
176
+ if not browser.api_url:
177
+ raise ValueError("API URL required for server-side processing")
178
+
179
+ # CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
180
+ # Even for API mode, we need the extension to collect raw data locally
181
+ try:
182
+ browser.page.wait_for_function("typeof window.sentience !== 'undefined'", timeout=5000)
183
+ except Exception as e:
184
+ raise RuntimeError(
185
+ "Sentience extension failed to inject. Cannot collect raw data for API processing."
186
+ ) from e
187
+
188
+ # Step 1: Get raw data from local extension (always happens locally)
189
+ raw_options: dict[str, Any] = {}
190
+ if options.screenshot is not False:
191
+ raw_options["screenshot"] = options.screenshot
192
+
193
+ raw_result = browser.page.evaluate(
194
+ """
195
+ (options) => {
196
+ return window.sentience.snapshot(options);
197
+ }
198
+ """,
199
+ raw_options,
200
+ )
201
+
202
+ # Save trace if requested (save raw data before API processing)
203
+ if options.save_trace:
204
+ _save_trace_to_file(raw_result.get("raw_elements", []), options.trace_path)
205
+
206
+ # Step 2: Send to server for smart ranking/filtering
207
+ # Use raw_elements (raw data) instead of elements (processed data)
208
+ # Server validates API key and applies proprietary ranking logic
209
+ payload = {
210
+ "raw_elements": raw_result.get("raw_elements", []), # Raw data needed for server processing
211
+ "url": raw_result.get("url", ""),
212
+ "viewport": raw_result.get("viewport"),
213
+ "goal": options.goal, # Optional goal/task description
214
+ "options": {
215
+ "limit": options.limit,
216
+ "filter": options.filter.model_dump() if options.filter else None,
217
+ },
218
+ }
219
+
220
+ # Check payload size before sending (server has 10MB limit)
221
+ payload_json = json.dumps(payload)
222
+ payload_size = len(payload_json.encode("utf-8"))
223
+ if payload_size > MAX_PAYLOAD_BYTES:
224
+ raise ValueError(
225
+ f"Payload size ({payload_size / 1024 / 1024:.2f}MB) exceeds server limit "
226
+ f"({MAX_PAYLOAD_BYTES / 1024 / 1024:.0f}MB). "
227
+ f"Try reducing the number of elements on the page or filtering elements."
228
+ )
229
+
230
+ headers = {
231
+ "Authorization": f"Bearer {browser.api_key}",
232
+ "Content-Type": "application/json",
233
+ }
234
+
235
+ try:
236
+ response = requests.post(
237
+ f"{browser.api_url}/v1/snapshot",
238
+ data=payload_json, # Reuse already-serialized JSON
239
+ headers=headers,
240
+ timeout=30,
241
+ )
242
+ response.raise_for_status()
243
+
244
+ api_result = response.json()
245
+
246
+ # Merge API result with local data (screenshot, etc.)
247
+ snapshot_data = {
248
+ "status": api_result.get("status", "success"),
249
+ "timestamp": api_result.get("timestamp"),
250
+ "url": api_result.get("url", raw_result.get("url", "")),
251
+ "viewport": api_result.get("viewport", raw_result.get("viewport")),
252
+ "elements": api_result.get("elements", []),
253
+ "screenshot": raw_result.get("screenshot"), # Keep local screenshot
254
+ "screenshot_format": raw_result.get("screenshot_format"),
255
+ "error": api_result.get("error"),
256
+ }
257
+
258
+ # Show visual overlay if requested (use API-ranked elements)
259
+ if options.show_overlay:
260
+ elements = api_result.get("elements", [])
261
+ if elements:
262
+ browser.page.evaluate(
263
+ """
264
+ (elements) => {
265
+ if (window.sentience && window.sentience.showOverlay) {
266
+ window.sentience.showOverlay(elements, null);
267
+ }
268
+ }
269
+ """,
270
+ elements,
271
+ )
272
+
273
+ return Snapshot(**snapshot_data)
274
+ except requests.exceptions.RequestException as e:
275
+ raise RuntimeError(f"API request failed: {e}")
276
+
277
+
278
+ # ========== Async Snapshot Functions ==========
279
+
280
+
281
+ async def snapshot_async(
282
+ browser: AsyncSentienceBrowser,
283
+ options: SnapshotOptions | None = None,
284
+ ) -> Snapshot:
285
+ """
286
+ Take a snapshot of the current page (async)
287
+
288
+ Args:
289
+ browser: AsyncSentienceBrowser instance
290
+ options: Snapshot options (screenshot, limit, filter, etc.)
291
+ If None, uses default options.
292
+
293
+ Returns:
294
+ Snapshot object
295
+
296
+ Example:
297
+ # Basic snapshot with defaults
298
+ snap = await snapshot_async(browser)
299
+
300
+ # With options
301
+ snap = await snapshot_async(browser, SnapshotOptions(
302
+ screenshot=True,
303
+ limit=100,
304
+ show_overlay=True
305
+ ))
306
+ """
307
+ # Use default options if none provided
308
+ if options is None:
309
+ options = SnapshotOptions()
310
+
311
+ # Determine if we should use server-side API
312
+ should_use_api = (
313
+ options.use_api if options.use_api is not None else (browser.api_key is not None)
314
+ )
315
+
316
+ if should_use_api and browser.api_key:
317
+ # Use server-side API (Pro/Enterprise tier)
318
+ return await _snapshot_via_api_async(browser, options)
319
+ else:
320
+ # Use local extension (Free tier)
321
+ return await _snapshot_via_extension_async(browser, options)
322
+
323
+
324
+ async def _snapshot_via_extension_async(
325
+ browser: AsyncSentienceBrowser,
326
+ options: SnapshotOptions,
327
+ ) -> Snapshot:
328
+ """Take snapshot using local extension (Free tier) - async"""
329
+ if not browser.page:
330
+ raise RuntimeError("Browser not started. Call await browser.start() first.")
331
+
332
+ # Wait for extension injection to complete
333
+ try:
334
+ await browser.page.wait_for_function(
335
+ "typeof window.sentience !== 'undefined'",
336
+ timeout=5000,
337
+ )
338
+ except Exception as e:
339
+ try:
340
+ diag = await browser.page.evaluate(
341
+ """() => ({
342
+ sentience_defined: typeof window.sentience !== 'undefined',
343
+ extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
344
+ url: window.location.href
345
+ })"""
346
+ )
347
+ except Exception:
348
+ diag = {"error": "Could not gather diagnostics"}
349
+
350
+ raise RuntimeError(
351
+ f"Sentience extension failed to inject window.sentience API. "
352
+ f"Is the extension loaded? Diagnostics: {diag}"
353
+ ) from e
354
+
355
+ # Build options dict for extension API
356
+ ext_options: dict[str, Any] = {}
357
+ if options.screenshot is not False:
358
+ ext_options["screenshot"] = options.screenshot
359
+ if options.limit != 50:
360
+ ext_options["limit"] = options.limit
361
+ if options.filter is not None:
362
+ ext_options["filter"] = (
363
+ options.filter.model_dump() if hasattr(options.filter, "model_dump") else options.filter
364
+ )
365
+
366
+ # Call extension API
367
+ result = await browser.page.evaluate(
368
+ """
369
+ (options) => {
370
+ return window.sentience.snapshot(options);
371
+ }
372
+ """,
373
+ ext_options,
374
+ )
375
+
376
+ # Save trace if requested
377
+ if options.save_trace:
378
+ _save_trace_to_file(result.get("raw_elements", []), options.trace_path)
379
+
380
+ # Show visual overlay if requested
381
+ if options.show_overlay:
382
+ raw_elements = result.get("raw_elements", [])
383
+ if raw_elements:
384
+ await browser.page.evaluate(
385
+ """
386
+ (elements) => {
387
+ if (window.sentience && window.sentience.showOverlay) {
388
+ window.sentience.showOverlay(elements, null);
389
+ }
390
+ }
391
+ """,
392
+ raw_elements,
393
+ )
394
+
395
+ # Validate and parse with Pydantic
396
+ snapshot_obj = Snapshot(**result)
397
+ return snapshot_obj
398
+
399
+
400
+ async def _snapshot_via_api_async(
401
+ browser: AsyncSentienceBrowser,
402
+ options: SnapshotOptions,
403
+ ) -> Snapshot:
404
+ """Take snapshot using server-side API (Pro/Enterprise tier) - async"""
405
+ if not browser.page:
406
+ raise RuntimeError("Browser not started. Call await browser.start() first.")
407
+
408
+ if not browser.api_key:
409
+ raise ValueError("API key required for server-side processing")
410
+
411
+ if not browser.api_url:
412
+ raise ValueError("API URL required for server-side processing")
413
+
414
+ # Wait for extension injection
415
+ try:
416
+ await browser.page.wait_for_function(
417
+ "typeof window.sentience !== 'undefined'", timeout=5000
418
+ )
419
+ except Exception as e:
420
+ raise RuntimeError(
421
+ "Sentience extension failed to inject. Cannot collect raw data for API processing."
422
+ ) from e
423
+
424
+ # Step 1: Get raw data from local extension
425
+ raw_options: dict[str, Any] = {}
426
+ if options.screenshot is not False:
427
+ raw_options["screenshot"] = options.screenshot
428
+
429
+ raw_result = await browser.page.evaluate(
430
+ """
431
+ (options) => {
432
+ return window.sentience.snapshot(options);
433
+ }
434
+ """,
435
+ raw_options,
436
+ )
437
+
438
+ # Save trace if requested
439
+ if options.save_trace:
440
+ _save_trace_to_file(raw_result.get("raw_elements", []), options.trace_path)
441
+
442
+ # Step 2: Send to server for smart ranking/filtering
443
+ payload = {
444
+ "raw_elements": raw_result.get("raw_elements", []),
445
+ "url": raw_result.get("url", ""),
446
+ "viewport": raw_result.get("viewport"),
447
+ "goal": options.goal,
448
+ "options": {
449
+ "limit": options.limit,
450
+ "filter": options.filter.model_dump() if options.filter else None,
451
+ },
452
+ }
453
+
454
+ # Check payload size
455
+ payload_json = json.dumps(payload)
456
+ payload_size = len(payload_json.encode("utf-8"))
457
+ if payload_size > MAX_PAYLOAD_BYTES:
458
+ raise ValueError(
459
+ f"Payload size ({payload_size / 1024 / 1024:.2f}MB) exceeds server limit "
460
+ f"({MAX_PAYLOAD_BYTES / 1024 / 1024:.0f}MB). "
461
+ f"Try reducing the number of elements on the page or filtering elements."
462
+ )
463
+
464
+ headers = {
465
+ "Authorization": f"Bearer {browser.api_key}",
466
+ "Content-Type": "application/json",
467
+ }
468
+
469
+ try:
470
+ # Lazy import httpx - only needed for async API calls
471
+ import httpx
472
+
473
+ async with httpx.AsyncClient(timeout=30.0) as client:
474
+ response = await client.post(
475
+ f"{browser.api_url}/v1/snapshot",
476
+ content=payload_json,
477
+ headers=headers,
478
+ )
479
+ response.raise_for_status()
480
+ api_result = response.json()
481
+
482
+ # Merge API result with local data
483
+ snapshot_data = {
484
+ "status": api_result.get("status", "success"),
485
+ "timestamp": api_result.get("timestamp"),
486
+ "url": api_result.get("url", raw_result.get("url", "")),
487
+ "viewport": api_result.get("viewport", raw_result.get("viewport")),
488
+ "elements": api_result.get("elements", []),
489
+ "screenshot": raw_result.get("screenshot"),
490
+ "screenshot_format": raw_result.get("screenshot_format"),
491
+ "error": api_result.get("error"),
492
+ }
493
+
494
+ # Show visual overlay if requested
495
+ if options.show_overlay:
496
+ elements = api_result.get("elements", [])
497
+ if elements:
498
+ await browser.page.evaluate(
499
+ """
500
+ (elements) => {
501
+ if (window.sentience && window.sentience.showOverlay) {
502
+ window.sentience.showOverlay(elements, null);
503
+ }
504
+ }
505
+ """,
506
+ elements,
507
+ )
508
+
509
+ return Snapshot(**snapshot_data)
510
+ except ImportError:
511
+ # Fallback to requests if httpx not available (shouldn't happen in async context)
512
+ raise RuntimeError(
513
+ "httpx is required for async API calls. Install it with: pip install httpx"
514
+ )
515
+ except Exception as e:
516
+ raise RuntimeError(f"API request failed: {e}")