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.
- sentience/__init__.py +120 -6
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +217 -0
- sentience/actions.py +758 -30
- sentience/agent.py +806 -293
- sentience/agent_config.py +3 -0
- sentience/agent_runtime.py +840 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +89 -1141
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +372 -0
- sentience/backends/browser_use_adapter.py +241 -0
- sentience/backends/cdp_backend.py +393 -0
- sentience/backends/exceptions.py +211 -0
- sentience/backends/playwright_backend.py +194 -0
- sentience/backends/protocol.py +216 -0
- sentience/backends/sentience_context.py +469 -0
- sentience/backends/snapshot.py +483 -0
- sentience/base_agent.py +95 -0
- sentience/browser.py +678 -39
- sentience/browser_evaluator.py +299 -0
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +507 -42
- sentience/constants.py +6 -0
- sentience/conversational_agent.py +77 -43
- sentience/cursor_policy.py +142 -0
- sentience/element_filter.py +136 -0
- sentience/expect.py +98 -2
- sentience/extension/background.js +56 -185
- sentience/extension/content.js +150 -287
- sentience/extension/injected_api.js +1088 -1368
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +275 -433
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +47 -47
- sentience/failure_artifacts.py +241 -0
- sentience/formatting.py +9 -53
- sentience/inspector.py +183 -1
- sentience/integrations/__init__.py +6 -0
- sentience/integrations/langchain/__init__.py +12 -0
- sentience/integrations/langchain/context.py +18 -0
- sentience/integrations/langchain/core.py +326 -0
- sentience/integrations/langchain/tools.py +180 -0
- sentience/integrations/models.py +46 -0
- sentience/integrations/pydanticai/__init__.py +15 -0
- sentience/integrations/pydanticai/deps.py +20 -0
- sentience/integrations/pydanticai/toolset.py +468 -0
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +765 -66
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +595 -3
- sentience/ordinal.py +280 -0
- sentience/overlay.py +109 -2
- sentience/protocols.py +228 -0
- sentience/query.py +67 -5
- sentience/read.py +95 -3
- sentience/recorder.py +223 -3
- sentience/schemas/trace_v1.json +128 -9
- sentience/screenshot.py +48 -2
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +599 -55
- sentience/snapshot_diff.py +126 -0
- sentience/text_search.py +120 -5
- sentience/trace_event_builder.py +148 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/index_schema.py +95 -7
- sentience/trace_indexing/indexer.py +105 -48
- sentience/tracer_factory.py +120 -9
- sentience/tracing.py +172 -8
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/{utils.py → utils/element.py} +3 -42
- sentience/utils/formatting.py +59 -0
- sentience/verification.py +618 -0
- sentience/visual_agent.py +2058 -0
- sentience/wait.py +68 -2
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/extension/test-content.js +0 -4
- sentienceapi-0.90.16.dist-info/RECORD +0 -50
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.90.16.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="...")
|