sentienceapi 0.92.2__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 (64) hide show
  1. sentience/__init__.py +107 -2
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +2 -0
  4. sentience/actions.py +354 -9
  5. sentience/agent.py +4 -0
  6. sentience/agent_runtime.py +840 -0
  7. sentience/asserts/__init__.py +70 -0
  8. sentience/asserts/expect.py +621 -0
  9. sentience/asserts/query.py +383 -0
  10. sentience/async_api.py +8 -1
  11. sentience/backends/__init__.py +137 -0
  12. sentience/backends/actions.py +372 -0
  13. sentience/backends/browser_use_adapter.py +241 -0
  14. sentience/backends/cdp_backend.py +393 -0
  15. sentience/backends/exceptions.py +211 -0
  16. sentience/backends/playwright_backend.py +194 -0
  17. sentience/backends/protocol.py +216 -0
  18. sentience/backends/sentience_context.py +469 -0
  19. sentience/backends/snapshot.py +483 -0
  20. sentience/browser.py +230 -74
  21. sentience/canonicalization.py +207 -0
  22. sentience/cloud_tracing.py +65 -24
  23. sentience/constants.py +6 -0
  24. sentience/cursor_policy.py +142 -0
  25. sentience/extension/content.js +35 -0
  26. sentience/extension/injected_api.js +310 -15
  27. sentience/extension/manifest.json +1 -1
  28. sentience/extension/pkg/sentience_core.d.ts +22 -22
  29. sentience/extension/pkg/sentience_core.js +192 -144
  30. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  31. sentience/extension/release.json +29 -29
  32. sentience/failure_artifacts.py +241 -0
  33. sentience/integrations/__init__.py +6 -0
  34. sentience/integrations/langchain/__init__.py +12 -0
  35. sentience/integrations/langchain/context.py +18 -0
  36. sentience/integrations/langchain/core.py +326 -0
  37. sentience/integrations/langchain/tools.py +180 -0
  38. sentience/integrations/models.py +46 -0
  39. sentience/integrations/pydanticai/__init__.py +15 -0
  40. sentience/integrations/pydanticai/deps.py +20 -0
  41. sentience/integrations/pydanticai/toolset.py +468 -0
  42. sentience/llm_provider.py +695 -18
  43. sentience/models.py +536 -3
  44. sentience/ordinal.py +280 -0
  45. sentience/query.py +66 -4
  46. sentience/schemas/trace_v1.json +27 -1
  47. sentience/snapshot.py +384 -93
  48. sentience/snapshot_diff.py +39 -54
  49. sentience/text_search.py +1 -0
  50. sentience/trace_event_builder.py +20 -1
  51. sentience/trace_indexing/indexer.py +3 -49
  52. sentience/tracer_factory.py +1 -3
  53. sentience/verification.py +618 -0
  54. sentience/visual_agent.py +3 -1
  55. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +198 -40
  56. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  57. sentience/utils.py +0 -296
  58. sentienceapi-0.92.2.dist-info/RECORD +0 -65
  59. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  60. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  61. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  62. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  63. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  64. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,383 @@
1
+ """
2
+ Element query builders for assertion DSL.
3
+
4
+ This module provides the E() query builder and dominant-group list operations
5
+ for creating element queries that compile to existing Predicates.
6
+
7
+ Key classes:
8
+ - ElementQuery: Pure data object for filtering elements (E())
9
+ - ListQuery: Query over dominant-group elements (in_dominant_list())
10
+ - MultiQuery: Represents multiple elements from ListQuery.top(n)
11
+
12
+ All queries work with existing Snapshot fields only:
13
+ id, tag, role, text (text_norm), bbox, doc_y, group_key, group_index,
14
+ dominant_group_key, in_viewport, is_occluded, href
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ if TYPE_CHECKING:
23
+ from ..models import Element, Snapshot
24
+
25
+
26
+ @dataclass
27
+ class ElementQuery:
28
+ """
29
+ Pure query object for filtering elements.
30
+
31
+ This is the data representation of an E() call. It does not execute
32
+ anything - it just stores the filter criteria.
33
+
34
+ Example:
35
+ E(role="button", text_contains="Save")
36
+ E(role="link", href_contains="/cart")
37
+ E(in_viewport=True, occluded=False)
38
+ """
39
+
40
+ role: str | None = None
41
+ name: str | None = None # Alias for text matching (best-effort)
42
+ text: str | None = None # Exact match against text
43
+ text_contains: str | None = None # Substring match
44
+ href_contains: str | None = None # Substring against href
45
+ in_viewport: bool | None = None
46
+ occluded: bool | None = None
47
+ group: str | None = None # Exact match against group_key
48
+ in_dominant_group: bool | None = None # True => in dominant group
49
+
50
+ # Internal: for ordinal selection from ListQuery
51
+ _group_index: int | None = field(default=None, repr=False)
52
+ _from_dominant_list: bool = field(default=False, repr=False)
53
+
54
+ def matches(self, element: Element, snapshot: Snapshot | None = None) -> bool:
55
+ """
56
+ Check if element matches this query criteria.
57
+
58
+ Args:
59
+ element: Element to check
60
+ snapshot: Snapshot (needed for dominant_group_key comparison)
61
+
62
+ Returns:
63
+ True if element matches all criteria
64
+ """
65
+ # Role filter
66
+ if self.role is not None:
67
+ if element.role != self.role:
68
+ return False
69
+
70
+ # Text exact match (name is alias for text)
71
+ text_to_match = self.text or self.name
72
+ if text_to_match is not None:
73
+ element_text = element.text or ""
74
+ if element_text != text_to_match:
75
+ return False
76
+
77
+ # Text contains (substring, case-insensitive)
78
+ if self.text_contains is not None:
79
+ element_text = element.text or ""
80
+ if self.text_contains.lower() not in element_text.lower():
81
+ return False
82
+
83
+ # Href contains (substring)
84
+ if self.href_contains is not None:
85
+ element_href = element.href or ""
86
+ if self.href_contains.lower() not in element_href.lower():
87
+ return False
88
+
89
+ # In viewport filter
90
+ if self.in_viewport is not None:
91
+ if element.in_viewport != self.in_viewport:
92
+ return False
93
+
94
+ # Occluded filter
95
+ if self.occluded is not None:
96
+ if element.is_occluded != self.occluded:
97
+ return False
98
+
99
+ # Group key exact match
100
+ if self.group is not None:
101
+ if element.group_key != self.group:
102
+ return False
103
+
104
+ # In dominant group check
105
+ if self.in_dominant_group is not None:
106
+ if self.in_dominant_group:
107
+ # Element must be in dominant group
108
+ if snapshot is None:
109
+ return False
110
+ if element.group_key != snapshot.dominant_group_key:
111
+ return False
112
+ else:
113
+ # Element must NOT be in dominant group
114
+ if snapshot is not None and element.group_key == snapshot.dominant_group_key:
115
+ return False
116
+
117
+ # Group index filter (from ListQuery.nth())
118
+ if self._group_index is not None:
119
+ if element.group_index != self._group_index:
120
+ return False
121
+
122
+ # Dominant list filter (from in_dominant_list())
123
+ if self._from_dominant_list:
124
+ if snapshot is None:
125
+ return False
126
+ if element.group_key != snapshot.dominant_group_key:
127
+ return False
128
+
129
+ return True
130
+
131
+ def find_all(self, snapshot: Snapshot) -> list[Element]:
132
+ """
133
+ Find all elements matching this query in the snapshot.
134
+
135
+ Args:
136
+ snapshot: Snapshot to search
137
+
138
+ Returns:
139
+ List of matching elements, sorted by doc_y (top to bottom)
140
+ """
141
+ matches = [el for el in snapshot.elements if self.matches(el, snapshot)]
142
+ # Sort by doc_y for consistent ordering (top to bottom)
143
+ matches.sort(key=lambda el: el.doc_y if el.doc_y is not None else el.bbox.y)
144
+ return matches
145
+
146
+ def find_first(self, snapshot: Snapshot) -> Element | None:
147
+ """
148
+ Find first matching element.
149
+
150
+ Args:
151
+ snapshot: Snapshot to search
152
+
153
+ Returns:
154
+ First matching element or None
155
+ """
156
+ matches = self.find_all(snapshot)
157
+ return matches[0] if matches else None
158
+
159
+
160
+ def E(
161
+ role: str | None = None,
162
+ name: str | None = None,
163
+ text: str | None = None,
164
+ text_contains: str | None = None,
165
+ href_contains: str | None = None,
166
+ in_viewport: bool | None = None,
167
+ occluded: bool | None = None,
168
+ group: str | None = None,
169
+ in_dominant_group: bool | None = None,
170
+ ) -> ElementQuery:
171
+ """
172
+ Create an element query.
173
+
174
+ This is the main entry point for building element queries.
175
+ It returns a pure data object that can be used with expect().
176
+
177
+ Args:
178
+ role: ARIA role to match (e.g., "button", "textbox", "link")
179
+ name: Text to match exactly (alias for text, best-effort)
180
+ text: Exact text match against text_norm
181
+ text_contains: Substring match against text_norm (case-insensitive)
182
+ href_contains: Substring match against href (case-insensitive)
183
+ in_viewport: Filter by viewport visibility
184
+ occluded: Filter by occlusion state
185
+ group: Exact match against group_key
186
+ in_dominant_group: True = must be in dominant group
187
+
188
+ Returns:
189
+ ElementQuery object
190
+
191
+ Example:
192
+ E(role="button", text_contains="Save")
193
+ E(role="link", href_contains="/checkout")
194
+ E(in_viewport=True, occluded=False)
195
+ """
196
+ return ElementQuery(
197
+ role=role,
198
+ name=name,
199
+ text=text,
200
+ text_contains=text_contains,
201
+ href_contains=href_contains,
202
+ in_viewport=in_viewport,
203
+ occluded=occluded,
204
+ group=group,
205
+ in_dominant_group=in_dominant_group,
206
+ )
207
+
208
+
209
+ # Convenience factory methods on E
210
+ class _EFactory:
211
+ """Factory class providing convenience methods for common queries."""
212
+
213
+ def __call__(
214
+ self,
215
+ role: str | None = None,
216
+ name: str | None = None,
217
+ text: str | None = None,
218
+ text_contains: str | None = None,
219
+ href_contains: str | None = None,
220
+ in_viewport: bool | None = None,
221
+ occluded: bool | None = None,
222
+ group: str | None = None,
223
+ in_dominant_group: bool | None = None,
224
+ ) -> ElementQuery:
225
+ """Create an element query."""
226
+ return E(
227
+ role=role,
228
+ name=name,
229
+ text=text,
230
+ text_contains=text_contains,
231
+ href_contains=href_contains,
232
+ in_viewport=in_viewport,
233
+ occluded=occluded,
234
+ group=group,
235
+ in_dominant_group=in_dominant_group,
236
+ )
237
+
238
+ def submit(self) -> ElementQuery:
239
+ """
240
+ Query for submit-like buttons.
241
+
242
+ Matches buttons with text like "Submit", "Save", "Continue", etc.
243
+ """
244
+ # This is a heuristic query - matches common submit button patterns
245
+ return ElementQuery(role="button", text_contains="submit")
246
+
247
+ def search_box(self) -> ElementQuery:
248
+ """
249
+ Query for search input boxes.
250
+
251
+ Matches textbox/combobox with search-related names.
252
+ """
253
+ return ElementQuery(role="textbox", name="search")
254
+
255
+ def link(self, text_contains: str | None = None) -> ElementQuery:
256
+ """
257
+ Query for links with optional text filter.
258
+
259
+ Args:
260
+ text_contains: Optional text substring to match
261
+ """
262
+ return ElementQuery(role="link", text_contains=text_contains)
263
+
264
+
265
+ @dataclass
266
+ class MultiQuery:
267
+ """
268
+ Represents multiple elements from a dominant list query.
269
+
270
+ Created by ListQuery.top(n) to represent the first n elements
271
+ in a dominant group.
272
+
273
+ Example:
274
+ in_dominant_list().top(5) # First 5 items in dominant group
275
+ """
276
+
277
+ limit: int
278
+ _parent_list_query: ListQuery | None = field(default=None, repr=False)
279
+
280
+ def any_text_contains(self, text: str) -> _MultiTextPredicate:
281
+ """
282
+ Create a predicate that checks if any element's text contains the substring.
283
+
284
+ Args:
285
+ text: Substring to search for
286
+
287
+ Returns:
288
+ Predicate that can be used with expect()
289
+ """
290
+ return _MultiTextPredicate(
291
+ multi_query=self,
292
+ text=text,
293
+ check_type="any_contains",
294
+ )
295
+
296
+
297
+ @dataclass
298
+ class _MultiTextPredicate:
299
+ """
300
+ Internal predicate for MultiQuery text checks.
301
+
302
+ Used by expect() to evaluate multi-element text assertions.
303
+ """
304
+
305
+ multi_query: MultiQuery
306
+ text: str
307
+ check_type: str # "any_contains", etc.
308
+
309
+
310
+ @dataclass
311
+ class ListQuery:
312
+ """
313
+ Query over elements in the dominant group.
314
+
315
+ Provides ordinal access to dominant-group elements via .nth(k)
316
+ and range access via .top(n).
317
+
318
+ Created by in_dominant_list().
319
+
320
+ Example:
321
+ in_dominant_list().nth(0) # First item in dominant group
322
+ in_dominant_list().top(5) # First 5 items
323
+ """
324
+
325
+ def nth(self, index: int) -> ElementQuery:
326
+ """
327
+ Select element at specific index in the dominant group.
328
+
329
+ Args:
330
+ index: 0-based index in the dominant group
331
+
332
+ Returns:
333
+ ElementQuery targeting the element at that position
334
+
335
+ Example:
336
+ in_dominant_list().nth(0) # First item
337
+ in_dominant_list().nth(2) # Third item
338
+ """
339
+ query = ElementQuery()
340
+ query._group_index = index
341
+ query._from_dominant_list = True
342
+ return query
343
+
344
+ def top(self, n: int) -> MultiQuery:
345
+ """
346
+ Select the first n elements in the dominant group.
347
+
348
+ Args:
349
+ n: Number of elements to select
350
+
351
+ Returns:
352
+ MultiQuery representing the first n elements
353
+
354
+ Example:
355
+ in_dominant_list().top(5) # First 5 items
356
+ """
357
+ return MultiQuery(limit=n, _parent_list_query=self)
358
+
359
+
360
+ def in_dominant_list() -> ListQuery:
361
+ """
362
+ Create a query over elements in the dominant group.
363
+
364
+ The dominant group is the most common group_key in the snapshot,
365
+ typically representing the main content list (search results,
366
+ news feed items, product listings, etc.).
367
+
368
+ Returns:
369
+ ListQuery for chaining .nth(k) or .top(n)
370
+
371
+ Example:
372
+ in_dominant_list().nth(0) # First item in dominant group
373
+ in_dominant_list().top(5) # First 5 items
374
+
375
+ # With expect():
376
+ expect(in_dominant_list().nth(0)).to_have_text_contains("Show HN")
377
+ """
378
+ return ListQuery()
379
+
380
+
381
+ # Export the factory as E for the Playwright-like API
382
+ # Users can do: from sentience.asserts import E
383
+ # And use: E(role="button"), E.submit(), E.link(text_contains="...")
sentience/async_api.py CHANGED
@@ -23,7 +23,13 @@ You can also import directly from their respective modules:
23
23
 
24
24
  # ========== Actions (Phase 1) ==========
25
25
  # Re-export async action functions from actions.py
26
- from sentience.actions import click_async, click_rect_async, press_async, type_text_async
26
+ from sentience.actions import (
27
+ click_async,
28
+ click_rect_async,
29
+ press_async,
30
+ scroll_to_async,
31
+ type_text_async,
32
+ )
27
33
 
28
34
  # ========== Phase 2C: Agent Layer ==========
29
35
  # Re-export async agent classes from agent.py and base_agent.py
@@ -76,6 +82,7 @@ __all__ = [
76
82
  "click_async", # Re-exported from actions.py
77
83
  "type_text_async", # Re-exported from actions.py
78
84
  "press_async", # Re-exported from actions.py
85
+ "scroll_to_async", # Re-exported from actions.py
79
86
  "click_rect_async", # Re-exported from actions.py
80
87
  # Phase 2A: Core Utilities
81
88
  "wait_for_async", # Re-exported from wait.py
@@ -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
+ ]