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

Files changed (90) hide show
  1. sentience/__init__.py +120 -6
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +217 -0
  4. sentience/actions.py +758 -30
  5. sentience/agent.py +806 -293
  6. sentience/agent_config.py +3 -0
  7. sentience/agent_runtime.py +840 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +89 -1141
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +372 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +483 -0
  21. sentience/base_agent.py +95 -0
  22. sentience/browser.py +678 -39
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cloud_tracing.py +507 -42
  26. sentience/constants.py +6 -0
  27. sentience/conversational_agent.py +77 -43
  28. sentience/cursor_policy.py +142 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +98 -2
  31. sentience/extension/background.js +56 -185
  32. sentience/extension/content.js +150 -287
  33. sentience/extension/injected_api.js +1088 -1368
  34. sentience/extension/manifest.json +1 -1
  35. sentience/extension/pkg/sentience_core.d.ts +22 -22
  36. sentience/extension/pkg/sentience_core.js +275 -433
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/release.json +47 -47
  39. sentience/failure_artifacts.py +241 -0
  40. sentience/formatting.py +9 -53
  41. sentience/inspector.py +183 -1
  42. sentience/integrations/__init__.py +6 -0
  43. sentience/integrations/langchain/__init__.py +12 -0
  44. sentience/integrations/langchain/context.py +18 -0
  45. sentience/integrations/langchain/core.py +326 -0
  46. sentience/integrations/langchain/tools.py +180 -0
  47. sentience/integrations/models.py +46 -0
  48. sentience/integrations/pydanticai/__init__.py +15 -0
  49. sentience/integrations/pydanticai/deps.py +20 -0
  50. sentience/integrations/pydanticai/toolset.py +468 -0
  51. sentience/llm_interaction_handler.py +191 -0
  52. sentience/llm_provider.py +765 -66
  53. sentience/llm_provider_utils.py +120 -0
  54. sentience/llm_response_builder.py +153 -0
  55. sentience/models.py +595 -3
  56. sentience/ordinal.py +280 -0
  57. sentience/overlay.py +109 -2
  58. sentience/protocols.py +228 -0
  59. sentience/query.py +67 -5
  60. sentience/read.py +95 -3
  61. sentience/recorder.py +223 -3
  62. sentience/schemas/trace_v1.json +128 -9
  63. sentience/screenshot.py +48 -2
  64. sentience/sentience_methods.py +86 -0
  65. sentience/snapshot.py +599 -55
  66. sentience/snapshot_diff.py +126 -0
  67. sentience/text_search.py +120 -5
  68. sentience/trace_event_builder.py +148 -0
  69. sentience/trace_file_manager.py +197 -0
  70. sentience/trace_indexing/index_schema.py +95 -7
  71. sentience/trace_indexing/indexer.py +105 -48
  72. sentience/tracer_factory.py +120 -9
  73. sentience/tracing.py +172 -8
  74. sentience/utils/__init__.py +40 -0
  75. sentience/utils/browser.py +46 -0
  76. sentience/{utils.py → utils/element.py} +3 -42
  77. sentience/utils/formatting.py +59 -0
  78. sentience/verification.py +618 -0
  79. sentience/visual_agent.py +2058 -0
  80. sentience/wait.py +68 -2
  81. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
  82. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  83. sentience/extension/test-content.js +0 -4
  84. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  85. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  86. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  87. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  88. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  89. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  90. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,137 @@
1
+ """
2
+ Browser backend abstractions for Sentience SDK.
3
+
4
+ This module provides backend protocols and implementations that allow
5
+ Sentience actions (click, type, scroll) to work with different browser
6
+ automation frameworks.
7
+
8
+ Supported Backends
9
+ ------------------
10
+
11
+ **PlaywrightBackend**
12
+ Wraps Playwright Page objects. Use this when integrating with existing
13
+ SentienceBrowser or Playwright-based code.
14
+
15
+ **CDPBackendV0**
16
+ Low-level CDP (Chrome DevTools Protocol) backend. Use this when you have
17
+ direct access to a CDP client and session.
18
+
19
+ **BrowserUseAdapter**
20
+ High-level adapter for browser-use framework. Automatically creates a
21
+ CDPBackendV0 from a BrowserSession.
22
+
23
+ Quick Start with browser-use
24
+ ----------------------------
25
+
26
+ .. code-block:: python
27
+
28
+ from browser_use import BrowserSession, BrowserProfile
29
+ from sentience import get_extension_dir, find
30
+ from sentience.backends import BrowserUseAdapter, snapshot, click, type_text
31
+
32
+ # Setup browser-use with Sentience extension
33
+ profile = BrowserProfile(args=[f"--load-extension={get_extension_dir()}"])
34
+ session = BrowserSession(browser_profile=profile)
35
+ await session.start()
36
+
37
+ # Create adapter and backend
38
+ adapter = BrowserUseAdapter(session)
39
+ backend = await adapter.create_backend()
40
+
41
+ # Take snapshot and interact with elements
42
+ snap = await snapshot(backend)
43
+ search_box = find(snap, 'role=textbox[name*="Search"]')
44
+ await click(backend, search_box.bbox)
45
+ await type_text(backend, "Sentience AI")
46
+
47
+ Snapshot Caching
48
+ ----------------
49
+
50
+ Use CachedSnapshot to reduce redundant snapshot calls in action loops:
51
+
52
+ .. code-block:: python
53
+
54
+ from sentience.backends import CachedSnapshot
55
+
56
+ cache = CachedSnapshot(backend, max_age_ms=2000)
57
+
58
+ snap1 = await cache.get() # Takes fresh snapshot
59
+ snap2 = await cache.get() # Returns cached if < 2s old
60
+
61
+ await click(backend, element.bbox)
62
+ cache.invalidate() # Force refresh on next get()
63
+
64
+ Error Handling
65
+ --------------
66
+
67
+ The module provides specific exceptions for common failure modes:
68
+
69
+ - ``ExtensionNotLoadedError``: Extension not loaded in browser launch args
70
+ - ``SnapshotError``: window.sentience.snapshot() failed
71
+ - ``ActionError``: Click/type/scroll operation failed
72
+
73
+ All exceptions inherit from ``SentienceBackendError`` and include helpful
74
+ fix suggestions in their error messages.
75
+
76
+ .. code-block:: python
77
+
78
+ from sentience.backends import ExtensionNotLoadedError, snapshot
79
+
80
+ try:
81
+ snap = await snapshot(backend)
82
+ except ExtensionNotLoadedError as e:
83
+ print(f"Fix suggestion: {e}")
84
+ """
85
+
86
+ from .actions import click, scroll, scroll_to_element, type_text, wait_for_stable
87
+ from .browser_use_adapter import BrowserUseAdapter, BrowserUseCDPTransport
88
+ from .cdp_backend import CDPBackendV0, CDPTransport
89
+ from .exceptions import (
90
+ ActionError,
91
+ BackendEvalError,
92
+ ExtensionDiagnostics,
93
+ ExtensionInjectionError,
94
+ ExtensionNotLoadedError,
95
+ SentienceBackendError,
96
+ SnapshotError,
97
+ )
98
+ from .playwright_backend import PlaywrightBackend
99
+ from .protocol import BrowserBackend, LayoutMetrics, ViewportInfo
100
+ from .sentience_context import SentienceContext, SentienceContextState, TopElementSelector
101
+ from .snapshot import CachedSnapshot, snapshot
102
+
103
+ __all__ = [
104
+ # Protocol
105
+ "BrowserBackend",
106
+ # Models
107
+ "ViewportInfo",
108
+ "LayoutMetrics",
109
+ # CDP Backend
110
+ "CDPTransport",
111
+ "CDPBackendV0",
112
+ # Playwright Backend
113
+ "PlaywrightBackend",
114
+ # browser-use adapter
115
+ "BrowserUseAdapter",
116
+ "BrowserUseCDPTransport",
117
+ # SentienceContext (Token-Slasher Context Middleware)
118
+ "SentienceContext",
119
+ "SentienceContextState",
120
+ "TopElementSelector",
121
+ # Backend-agnostic functions
122
+ "snapshot",
123
+ "CachedSnapshot",
124
+ "click",
125
+ "type_text",
126
+ "scroll",
127
+ "scroll_to_element",
128
+ "wait_for_stable",
129
+ # Exceptions
130
+ "SentienceBackendError",
131
+ "ExtensionNotLoadedError",
132
+ "ExtensionInjectionError",
133
+ "ExtensionDiagnostics",
134
+ "BackendEvalError",
135
+ "SnapshotError",
136
+ "ActionError",
137
+ ]
@@ -0,0 +1,372 @@
1
+ """
2
+ Backend-agnostic actions for browser-use integration.
3
+
4
+ These actions work with any BrowserBackend implementation,
5
+ enabling Sentience grounding with browser-use or other frameworks.
6
+
7
+ Usage with browser-use:
8
+ from sentience.backends import BrowserUseAdapter
9
+ from sentience.backends.actions import click, type_text, scroll
10
+
11
+ adapter = BrowserUseAdapter(session)
12
+ backend = await adapter.create_backend()
13
+
14
+ # Take snapshot and click element
15
+ snap = await snapshot_from_backend(backend)
16
+ element = find(snap, 'role=button[name="Submit"]')
17
+ await click(backend, element.bbox)
18
+ """
19
+
20
+ import asyncio
21
+ import time
22
+ from typing import TYPE_CHECKING, Any, Literal
23
+
24
+ from ..cursor_policy import CursorPolicy, build_human_cursor_path
25
+ from ..models import ActionResult, BBox, Snapshot
26
+
27
+ if TYPE_CHECKING:
28
+ from .protocol import BrowserBackend
29
+
30
+
31
+ async def click(
32
+ backend: "BrowserBackend",
33
+ target: BBox | dict[str, float] | tuple[float, float],
34
+ button: Literal["left", "right", "middle"] = "left",
35
+ click_count: int = 1,
36
+ move_first: bool = True,
37
+ cursor_policy: CursorPolicy | None = None,
38
+ ) -> ActionResult:
39
+ """
40
+ Click at coordinates using the backend.
41
+
42
+ Args:
43
+ backend: BrowserBackend implementation
44
+ target: Click target - BBox (clicks center), dict with x/y, or (x, y) tuple
45
+ button: Mouse button to click
46
+ click_count: Number of clicks (1=single, 2=double)
47
+ move_first: Whether to move mouse to position before clicking
48
+
49
+ Returns:
50
+ ActionResult with success status
51
+
52
+ Example:
53
+ # Click at coordinates
54
+ await click(backend, (100, 200))
55
+
56
+ # Click element bbox center
57
+ await click(backend, element.bbox)
58
+
59
+ # Double-click
60
+ await click(backend, element.bbox, click_count=2)
61
+ """
62
+ start_time = time.time()
63
+
64
+ # Resolve coordinates
65
+ x, y = _resolve_coordinates(target)
66
+ cursor_meta: dict | None = None
67
+
68
+ try:
69
+ # Optional mouse move for hover effects
70
+ if move_first:
71
+ if cursor_policy is not None and cursor_policy.mode == "human":
72
+ pos = getattr(backend, "_sentience_cursor_pos", None)
73
+ if not isinstance(pos, tuple) or len(pos) != 2:
74
+ pos = (float(x), float(y))
75
+
76
+ cursor_meta = build_human_cursor_path(
77
+ start=(float(pos[0]), float(pos[1])),
78
+ target=(float(x), float(y)),
79
+ policy=cursor_policy,
80
+ )
81
+ pts = cursor_meta.get("path", [])
82
+ duration_ms_move = int(cursor_meta.get("duration_ms") or 0)
83
+ per_step_s = (
84
+ (duration_ms_move / max(1, len(pts))) / 1000.0 if duration_ms_move > 0 else 0.0
85
+ )
86
+ for p in pts:
87
+ await backend.mouse_move(float(p["x"]), float(p["y"]))
88
+ if per_step_s > 0:
89
+ await asyncio.sleep(per_step_s)
90
+ pause_ms = int(cursor_meta.get("pause_before_click_ms") or 0)
91
+ if pause_ms > 0:
92
+ await asyncio.sleep(pause_ms / 1000.0)
93
+ else:
94
+ await backend.mouse_move(x, y)
95
+ await asyncio.sleep(0.02) # Brief pause for hover
96
+
97
+ # Perform click
98
+ await backend.mouse_click(x, y, button=button, click_count=click_count)
99
+ setattr(backend, "_sentience_cursor_pos", (float(x), float(y)))
100
+
101
+ duration_ms = int((time.time() - start_time) * 1000)
102
+ return ActionResult(
103
+ success=True,
104
+ duration_ms=duration_ms,
105
+ outcome="dom_updated",
106
+ cursor=cursor_meta,
107
+ )
108
+ except Exception as e:
109
+ duration_ms = int((time.time() - start_time) * 1000)
110
+ return ActionResult(
111
+ success=False,
112
+ duration_ms=duration_ms,
113
+ outcome="error",
114
+ error={"code": "click_failed", "reason": str(e)},
115
+ cursor=cursor_meta,
116
+ )
117
+
118
+
119
+ async def type_text(
120
+ backend: "BrowserBackend",
121
+ text: str,
122
+ target: BBox | dict[str, float] | tuple[float, float] | None = None,
123
+ clear_first: bool = False,
124
+ ) -> ActionResult:
125
+ """
126
+ Type text, optionally clicking a target first.
127
+
128
+ Args:
129
+ backend: BrowserBackend implementation
130
+ text: Text to type
131
+ target: Optional click target before typing (BBox, dict, or tuple)
132
+ clear_first: If True, select all and delete before typing
133
+
134
+ Returns:
135
+ ActionResult with success status
136
+
137
+ Example:
138
+ # Type into focused element
139
+ await type_text(backend, "Hello World")
140
+
141
+ # Click input then type
142
+ await type_text(backend, "search query", target=search_box.bbox)
143
+
144
+ # Clear and type
145
+ await type_text(backend, "new value", target=input.bbox, clear_first=True)
146
+ """
147
+ start_time = time.time()
148
+
149
+ try:
150
+ # Click target if provided
151
+ if target is not None:
152
+ x, y = _resolve_coordinates(target)
153
+ await backend.mouse_click(x, y)
154
+ await asyncio.sleep(0.05) # Wait for focus
155
+
156
+ # Clear existing content if requested
157
+ if clear_first:
158
+ # Select all (Ctrl+A / Cmd+A) and delete
159
+ await backend.eval("document.execCommand('selectAll')")
160
+ await asyncio.sleep(0.02)
161
+
162
+ # Type the text
163
+ await backend.type_text(text)
164
+
165
+ duration_ms = int((time.time() - start_time) * 1000)
166
+ return ActionResult(
167
+ success=True,
168
+ duration_ms=duration_ms,
169
+ outcome="dom_updated",
170
+ )
171
+ except Exception as e:
172
+ duration_ms = int((time.time() - start_time) * 1000)
173
+ return ActionResult(
174
+ success=False,
175
+ duration_ms=duration_ms,
176
+ outcome="error",
177
+ error={"code": "type_failed", "reason": str(e)},
178
+ )
179
+
180
+
181
+ async def scroll(
182
+ backend: "BrowserBackend",
183
+ delta_y: float = 300,
184
+ target: BBox | dict[str, float] | tuple[float, float] | None = None,
185
+ ) -> ActionResult:
186
+ """
187
+ Scroll the page or element.
188
+
189
+ Args:
190
+ backend: BrowserBackend implementation
191
+ delta_y: Scroll amount (positive=down, negative=up)
192
+ target: Optional position for scroll (defaults to viewport center)
193
+
194
+ Returns:
195
+ ActionResult with success status
196
+
197
+ Example:
198
+ # Scroll down 300px
199
+ await scroll(backend, 300)
200
+
201
+ # Scroll up 500px
202
+ await scroll(backend, -500)
203
+
204
+ # Scroll at specific position
205
+ await scroll(backend, 200, target=(500, 300))
206
+ """
207
+ start_time = time.time()
208
+
209
+ try:
210
+ x: float | None = None
211
+ y: float | None = None
212
+
213
+ if target is not None:
214
+ x, y = _resolve_coordinates(target)
215
+
216
+ await backend.wheel(delta_y=delta_y, x=x, y=y)
217
+
218
+ # Wait for scroll to settle
219
+ await asyncio.sleep(0.1)
220
+
221
+ duration_ms = int((time.time() - start_time) * 1000)
222
+ return ActionResult(
223
+ success=True,
224
+ duration_ms=duration_ms,
225
+ outcome="dom_updated",
226
+ )
227
+ except Exception as e:
228
+ duration_ms = int((time.time() - start_time) * 1000)
229
+ return ActionResult(
230
+ success=False,
231
+ duration_ms=duration_ms,
232
+ outcome="error",
233
+ error={"code": "scroll_failed", "reason": str(e)},
234
+ )
235
+
236
+
237
+ async def scroll_to_element(
238
+ backend: "BrowserBackend",
239
+ element_id: int,
240
+ behavior: Literal["smooth", "instant", "auto"] = "instant",
241
+ block: Literal["start", "center", "end", "nearest"] = "center",
242
+ ) -> ActionResult:
243
+ """
244
+ Scroll element into view using JavaScript scrollIntoView.
245
+
246
+ Args:
247
+ backend: BrowserBackend implementation
248
+ element_id: Element ID from snapshot (requires sentience_registry)
249
+ behavior: Scroll behavior
250
+ block: Vertical alignment
251
+
252
+ Returns:
253
+ ActionResult with success status
254
+ """
255
+ start_time = time.time()
256
+
257
+ try:
258
+ scrolled = await backend.eval(
259
+ f"""
260
+ (() => {{
261
+ const el = window.sentience_registry && window.sentience_registry[{element_id}];
262
+ if (el && el.scrollIntoView) {{
263
+ el.scrollIntoView({{
264
+ behavior: '{behavior}',
265
+ block: '{block}',
266
+ inline: 'nearest'
267
+ }});
268
+ return true;
269
+ }}
270
+ return false;
271
+ }})()
272
+ """
273
+ )
274
+
275
+ # Wait for scroll animation
276
+ wait_time = 0.3 if behavior == "smooth" else 0.05
277
+ await asyncio.sleep(wait_time)
278
+
279
+ duration_ms = int((time.time() - start_time) * 1000)
280
+
281
+ if scrolled:
282
+ return ActionResult(
283
+ success=True,
284
+ duration_ms=duration_ms,
285
+ outcome="dom_updated",
286
+ )
287
+ else:
288
+ return ActionResult(
289
+ success=False,
290
+ duration_ms=duration_ms,
291
+ outcome="error",
292
+ error={"code": "scroll_failed", "reason": "Element not found in registry"},
293
+ )
294
+ except Exception as e:
295
+ duration_ms = int((time.time() - start_time) * 1000)
296
+ return ActionResult(
297
+ success=False,
298
+ duration_ms=duration_ms,
299
+ outcome="error",
300
+ error={"code": "scroll_failed", "reason": str(e)},
301
+ )
302
+
303
+
304
+ async def wait_for_stable(
305
+ backend: "BrowserBackend",
306
+ state: Literal["interactive", "complete"] = "complete",
307
+ timeout_ms: int = 10000,
308
+ ) -> ActionResult:
309
+ """
310
+ Wait for page to reach stable state.
311
+
312
+ Args:
313
+ backend: BrowserBackend implementation
314
+ state: Target document.readyState
315
+ timeout_ms: Maximum wait time
316
+
317
+ Returns:
318
+ ActionResult with success status
319
+ """
320
+ start_time = time.time()
321
+
322
+ try:
323
+ await backend.wait_ready_state(state=state, timeout_ms=timeout_ms)
324
+
325
+ duration_ms = int((time.time() - start_time) * 1000)
326
+ return ActionResult(
327
+ success=True,
328
+ duration_ms=duration_ms,
329
+ outcome="dom_updated",
330
+ )
331
+ except TimeoutError as e:
332
+ duration_ms = int((time.time() - start_time) * 1000)
333
+ return ActionResult(
334
+ success=False,
335
+ duration_ms=duration_ms,
336
+ outcome="error",
337
+ error={"code": "timeout", "reason": str(e)},
338
+ )
339
+ except Exception as e:
340
+ duration_ms = int((time.time() - start_time) * 1000)
341
+ return ActionResult(
342
+ success=False,
343
+ duration_ms=duration_ms,
344
+ outcome="error",
345
+ error={"code": "wait_failed", "reason": str(e)},
346
+ )
347
+
348
+
349
+ def _resolve_coordinates(
350
+ target: BBox | dict[str, float] | tuple[float, float],
351
+ ) -> tuple[float, float]:
352
+ """
353
+ Resolve target to (x, y) coordinates.
354
+
355
+ - BBox: Returns center point
356
+ - dict: Returns x, y keys (or center if width/height present)
357
+ - tuple: Returns as-is
358
+ """
359
+ if isinstance(target, BBox):
360
+ return (target.x + target.width / 2, target.y + target.height / 2)
361
+ elif isinstance(target, tuple):
362
+ return target
363
+ elif isinstance(target, dict):
364
+ # If has width/height, compute center
365
+ if "width" in target and "height" in target:
366
+ x = target.get("x", 0) + target["width"] / 2
367
+ y = target.get("y", 0) + target["height"] / 2
368
+ return (x, y)
369
+ # Otherwise use x/y directly
370
+ return (target.get("x", 0), target.get("y", 0))
371
+ else:
372
+ raise ValueError(f"Invalid target type: {type(target)}")