sentienceapi 0.95.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 +253 -0
- sentience/_extension_loader.py +195 -0
- sentience/action_executor.py +215 -0
- sentience/actions.py +1020 -0
- sentience/agent.py +1181 -0
- sentience/agent_config.py +46 -0
- sentience/agent_runtime.py +424 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +108 -0
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +343 -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 +427 -0
- sentience/base_agent.py +196 -0
- sentience/browser.py +1215 -0
- sentience/browser_evaluator.py +299 -0
- sentience/canonicalization.py +207 -0
- sentience/cli.py +130 -0
- sentience/cloud_tracing.py +807 -0
- sentience/constants.py +6 -0
- sentience/conversational_agent.py +543 -0
- sentience/element_filter.py +136 -0
- sentience/expect.py +188 -0
- sentience/extension/background.js +104 -0
- sentience/extension/content.js +161 -0
- sentience/extension/injected_api.js +914 -0
- sentience/extension/manifest.json +36 -0
- sentience/extension/pkg/sentience_core.d.ts +51 -0
- sentience/extension/pkg/sentience_core.js +323 -0
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
- sentience/extension/release.json +115 -0
- sentience/formatting.py +15 -0
- sentience/generator.py +202 -0
- sentience/inspector.py +367 -0
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +875 -0
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +846 -0
- sentience/ordinal.py +280 -0
- sentience/overlay.py +222 -0
- sentience/protocols.py +228 -0
- sentience/query.py +303 -0
- sentience/read.py +188 -0
- sentience/recorder.py +589 -0
- sentience/schemas/trace_v1.json +335 -0
- sentience/screenshot.py +100 -0
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +706 -0
- sentience/snapshot_diff.py +126 -0
- sentience/text_search.py +262 -0
- sentience/trace_event_builder.py +148 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/__init__.py +27 -0
- sentience/trace_indexing/index_schema.py +199 -0
- sentience/trace_indexing/indexer.py +414 -0
- sentience/tracer_factory.py +322 -0
- sentience/tracing.py +449 -0
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/utils/element.py +257 -0
- sentience/utils/formatting.py +59 -0
- sentience/utils.py +296 -0
- sentience/verification.py +380 -0
- sentience/visual_agent.py +2058 -0
- sentience/wait.py +139 -0
- sentienceapi-0.95.0.dist-info/METADATA +984 -0
- sentienceapi-0.95.0.dist-info/RECORD +82 -0
- sentienceapi-0.95.0.dist-info/WHEEL +5 -0
- sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
- sentienceapi-0.95.0.dist-info/top_level.txt +1 -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
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async API for Sentience SDK - Convenience re-exports
|
|
3
|
+
|
|
4
|
+
This module re-exports all async functions for backward compatibility and developer convenience.
|
|
5
|
+
You can also import directly from their respective modules:
|
|
6
|
+
|
|
7
|
+
# Option 1: From async_api (recommended for convenience)
|
|
8
|
+
from sentience.async_api import (
|
|
9
|
+
AsyncSentienceBrowser,
|
|
10
|
+
snapshot_async,
|
|
11
|
+
click_async,
|
|
12
|
+
wait_for_async,
|
|
13
|
+
screenshot_async,
|
|
14
|
+
find_text_rect_async,
|
|
15
|
+
# ... all async functions in one place
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Option 2: From respective modules (also works)
|
|
19
|
+
from sentience.browser import AsyncSentienceBrowser
|
|
20
|
+
from sentience.snapshot import snapshot_async
|
|
21
|
+
from sentience.actions import click_async
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# ========== Actions (Phase 1) ==========
|
|
25
|
+
# Re-export async action functions from actions.py
|
|
26
|
+
from sentience.actions import (
|
|
27
|
+
click_async,
|
|
28
|
+
click_rect_async,
|
|
29
|
+
press_async,
|
|
30
|
+
scroll_to_async,
|
|
31
|
+
type_text_async,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# ========== Phase 2C: Agent Layer ==========
|
|
35
|
+
# Re-export async agent classes from agent.py and base_agent.py
|
|
36
|
+
from sentience.agent import SentienceAgentAsync
|
|
37
|
+
from sentience.base_agent import BaseAgentAsync
|
|
38
|
+
|
|
39
|
+
# ========== Browser ==========
|
|
40
|
+
# Re-export AsyncSentienceBrowser from browser.py (moved there for better organization)
|
|
41
|
+
from sentience.browser import AsyncSentienceBrowser
|
|
42
|
+
|
|
43
|
+
# Re-export async expect functions from expect.py
|
|
44
|
+
from sentience.expect import ExpectationAsync, expect_async
|
|
45
|
+
from sentience.inspector import InspectorAsync, inspect_async
|
|
46
|
+
|
|
47
|
+
# Re-export async overlay functions from overlay.py
|
|
48
|
+
from sentience.overlay import clear_overlay_async, show_overlay_async
|
|
49
|
+
|
|
50
|
+
# ========== Query Functions (Pure Functions - No Async Needed) ==========
|
|
51
|
+
# Re-export query functions (pure functions, no async needed)
|
|
52
|
+
from sentience.query import find, query
|
|
53
|
+
|
|
54
|
+
# ========== Phase 2B: Supporting Utilities ==========
|
|
55
|
+
# Re-export async read function from read.py
|
|
56
|
+
from sentience.read import read_async
|
|
57
|
+
|
|
58
|
+
# ========== Phase 2D: Developer Tools ==========
|
|
59
|
+
# Re-export async recorder and inspector from their modules
|
|
60
|
+
from sentience.recorder import RecorderAsync, record_async
|
|
61
|
+
|
|
62
|
+
# Re-export async screenshot function from screenshot.py
|
|
63
|
+
from sentience.screenshot import screenshot_async
|
|
64
|
+
|
|
65
|
+
# ========== Snapshot (Phase 1) ==========
|
|
66
|
+
# Re-export async snapshot functions from snapshot.py
|
|
67
|
+
from sentience.snapshot import snapshot_async
|
|
68
|
+
|
|
69
|
+
# Re-export async text search function from text_search.py
|
|
70
|
+
from sentience.text_search import find_text_rect_async
|
|
71
|
+
|
|
72
|
+
# ========== Phase 2A: Core Utilities ==========
|
|
73
|
+
# Re-export async wait function from wait.py
|
|
74
|
+
from sentience.wait import wait_for_async
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
# Browser
|
|
78
|
+
"AsyncSentienceBrowser", # Re-exported from browser.py
|
|
79
|
+
# Snapshot (Phase 1)
|
|
80
|
+
"snapshot_async", # Re-exported from snapshot.py
|
|
81
|
+
# Actions (Phase 1)
|
|
82
|
+
"click_async", # Re-exported from actions.py
|
|
83
|
+
"type_text_async", # Re-exported from actions.py
|
|
84
|
+
"press_async", # Re-exported from actions.py
|
|
85
|
+
"scroll_to_async", # Re-exported from actions.py
|
|
86
|
+
"click_rect_async", # Re-exported from actions.py
|
|
87
|
+
# Phase 2A: Core Utilities
|
|
88
|
+
"wait_for_async", # Re-exported from wait.py
|
|
89
|
+
"screenshot_async", # Re-exported from screenshot.py
|
|
90
|
+
"find_text_rect_async", # Re-exported from text_search.py
|
|
91
|
+
# Phase 2B: Supporting Utilities
|
|
92
|
+
"read_async", # Re-exported from read.py
|
|
93
|
+
"show_overlay_async", # Re-exported from overlay.py
|
|
94
|
+
"clear_overlay_async", # Re-exported from overlay.py
|
|
95
|
+
"expect_async", # Re-exported from expect.py
|
|
96
|
+
"ExpectationAsync", # Re-exported from expect.py
|
|
97
|
+
# Phase 2C: Agent Layer
|
|
98
|
+
"SentienceAgentAsync", # Re-exported from agent.py
|
|
99
|
+
"BaseAgentAsync", # Re-exported from base_agent.py
|
|
100
|
+
# Phase 2D: Developer Tools
|
|
101
|
+
"RecorderAsync", # Re-exported from recorder.py
|
|
102
|
+
"record_async", # Re-exported from recorder.py
|
|
103
|
+
"InspectorAsync", # Re-exported from inspector.py
|
|
104
|
+
"inspect_async", # Re-exported from inspector.py
|
|
105
|
+
# Query Functions
|
|
106
|
+
"find", # Re-exported from query.py
|
|
107
|
+
"query", # Re-exported from query.py
|
|
108
|
+
]
|
|
@@ -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
|
+
]
|