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.
- sentience/__init__.py +153 -0
- sentience/_extension_loader.py +40 -0
- sentience/actions.py +837 -0
- sentience/agent.py +1246 -0
- sentience/agent_config.py +43 -0
- sentience/async_api.py +101 -0
- sentience/base_agent.py +194 -0
- sentience/browser.py +1037 -0
- sentience/cli.py +130 -0
- sentience/cloud_tracing.py +382 -0
- sentience/conversational_agent.py +509 -0
- sentience/expect.py +188 -0
- sentience/extension/background.js +233 -0
- sentience/extension/content.js +298 -0
- sentience/extension/injected_api.js +1473 -0
- sentience/extension/manifest.json +36 -0
- sentience/extension/pkg/sentience_core.d.ts +51 -0
- sentience/extension/pkg/sentience_core.js +529 -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/extension/test-content.js +4 -0
- sentience/formatting.py +59 -0
- sentience/generator.py +202 -0
- sentience/inspector.py +365 -0
- sentience/llm_provider.py +637 -0
- sentience/models.py +412 -0
- sentience/overlay.py +222 -0
- sentience/query.py +303 -0
- sentience/read.py +185 -0
- sentience/recorder.py +589 -0
- sentience/schemas/trace_v1.json +216 -0
- sentience/screenshot.py +100 -0
- sentience/snapshot.py +516 -0
- sentience/text_search.py +290 -0
- sentience/trace_indexing/__init__.py +27 -0
- sentience/trace_indexing/index_schema.py +111 -0
- sentience/trace_indexing/indexer.py +357 -0
- sentience/tracer_factory.py +211 -0
- sentience/tracing.py +285 -0
- sentience/utils.py +296 -0
- sentience/wait.py +137 -0
- sentienceapi-0.90.17.dist-info/METADATA +917 -0
- sentienceapi-0.90.17.dist-info/RECORD +50 -0
- sentienceapi-0.90.17.dist-info/WHEEL +5 -0
- sentienceapi-0.90.17.dist-info/entry_points.txt +2 -0
- sentienceapi-0.90.17.dist-info/licenses/LICENSE +24 -0
- sentienceapi-0.90.17.dist-info/licenses/LICENSE-APACHE +201 -0
- sentienceapi-0.90.17.dist-info/licenses/LICENSE-MIT +21 -0
- sentienceapi-0.90.17.dist-info/top_level.txt +1 -0
sentience/text_search.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Text search utilities - find text and get pixel coordinates
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .browser import AsyncSentienceBrowser, SentienceBrowser
|
|
6
|
+
from .models import TextRectSearchResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def find_text_rect(
|
|
10
|
+
browser: SentienceBrowser,
|
|
11
|
+
text: str,
|
|
12
|
+
case_sensitive: bool = False,
|
|
13
|
+
whole_word: bool = False,
|
|
14
|
+
max_results: int = 10,
|
|
15
|
+
) -> TextRectSearchResult:
|
|
16
|
+
"""
|
|
17
|
+
Find all occurrences of text on the page and get their exact pixel coordinates.
|
|
18
|
+
|
|
19
|
+
This function searches for text in all visible text nodes on the page and returns
|
|
20
|
+
the bounding rectangles for each match. Useful for:
|
|
21
|
+
- Finding specific UI elements by their text content
|
|
22
|
+
- Locating buttons, links, or labels without element IDs
|
|
23
|
+
- Getting exact coordinates for click automation
|
|
24
|
+
- Highlighting search results visually
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
browser: SentienceBrowser instance
|
|
28
|
+
text: Text to search for (required)
|
|
29
|
+
case_sensitive: If True, search is case-sensitive (default: False)
|
|
30
|
+
whole_word: If True, only match whole words surrounded by whitespace (default: False)
|
|
31
|
+
max_results: Maximum number of matches to return (default: 10, max: 100)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
TextRectSearchResult with:
|
|
35
|
+
- status: "success" or "error"
|
|
36
|
+
- query: The search text
|
|
37
|
+
- case_sensitive: Whether search was case-sensitive
|
|
38
|
+
- whole_word: Whether whole-word matching was used
|
|
39
|
+
- matches: Number of matches found
|
|
40
|
+
- results: List of TextMatch objects, each containing:
|
|
41
|
+
- text: The matched text
|
|
42
|
+
- rect: Absolute rectangle (with scroll offset)
|
|
43
|
+
- viewport_rect: Viewport-relative rectangle
|
|
44
|
+
- context: Surrounding text (before/after)
|
|
45
|
+
- in_viewport: Whether visible in current viewport
|
|
46
|
+
- viewport: Current viewport dimensions and scroll position
|
|
47
|
+
- error: Error message if status is "error"
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
# Find "Sign In" button
|
|
51
|
+
result = find_text_rect(browser, "Sign In")
|
|
52
|
+
if result.status == "success" and result.results:
|
|
53
|
+
first_match = result.results[0]
|
|
54
|
+
print(f"Found at: ({first_match.rect.x}, {first_match.rect.y})")
|
|
55
|
+
print(f"Size: {first_match.rect.width}x{first_match.rect.height}")
|
|
56
|
+
print(f"In viewport: {first_match.in_viewport}")
|
|
57
|
+
|
|
58
|
+
# Case-sensitive search
|
|
59
|
+
result = find_text_rect(browser, "LOGIN", case_sensitive=True)
|
|
60
|
+
|
|
61
|
+
# Whole word only
|
|
62
|
+
result = find_text_rect(browser, "log", whole_word=True) # Won't match "login"
|
|
63
|
+
|
|
64
|
+
# Find all matches and click the first visible one
|
|
65
|
+
result = find_text_rect(browser, "Buy Now", max_results=5)
|
|
66
|
+
if result.status == "success" and result.results:
|
|
67
|
+
for match in result.results:
|
|
68
|
+
if match.in_viewport:
|
|
69
|
+
# Use click_rect from actions module
|
|
70
|
+
from sentience import click_rect
|
|
71
|
+
click_result = click_rect(browser, {
|
|
72
|
+
"x": match.rect.x,
|
|
73
|
+
"y": match.rect.y,
|
|
74
|
+
"w": match.rect.width,
|
|
75
|
+
"h": match.rect.height
|
|
76
|
+
})
|
|
77
|
+
break
|
|
78
|
+
"""
|
|
79
|
+
if not browser.page:
|
|
80
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
81
|
+
|
|
82
|
+
if not text or not text.strip():
|
|
83
|
+
return TextRectSearchResult(
|
|
84
|
+
status="error",
|
|
85
|
+
error="Text parameter is required and cannot be empty",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Limit max_results to prevent performance issues
|
|
89
|
+
max_results = min(max_results, 100)
|
|
90
|
+
|
|
91
|
+
# CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
|
|
92
|
+
# The new architecture loads injected_api.js asynchronously, so window.sentience
|
|
93
|
+
# may not be immediately available after page load
|
|
94
|
+
try:
|
|
95
|
+
browser.page.wait_for_function(
|
|
96
|
+
"typeof window.sentience !== 'undefined'",
|
|
97
|
+
timeout=5000, # 5 second timeout
|
|
98
|
+
)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
# Gather diagnostics if wait fails
|
|
101
|
+
try:
|
|
102
|
+
diag = browser.page.evaluate(
|
|
103
|
+
"""() => ({
|
|
104
|
+
sentience_defined: typeof window.sentience !== 'undefined',
|
|
105
|
+
extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
|
|
106
|
+
url: window.location.href
|
|
107
|
+
})"""
|
|
108
|
+
)
|
|
109
|
+
except Exception:
|
|
110
|
+
diag = {"error": "Could not gather diagnostics"}
|
|
111
|
+
|
|
112
|
+
raise RuntimeError(
|
|
113
|
+
f"Sentience extension failed to inject window.sentience API. "
|
|
114
|
+
f"Is the extension loaded? Diagnostics: {diag}"
|
|
115
|
+
) from e
|
|
116
|
+
|
|
117
|
+
# Verify findTextRect method exists (for older extension versions that don't have it)
|
|
118
|
+
try:
|
|
119
|
+
has_find_text_rect = browser.page.evaluate(
|
|
120
|
+
"typeof window.sentience.findTextRect !== 'undefined'"
|
|
121
|
+
)
|
|
122
|
+
if not has_find_text_rect:
|
|
123
|
+
raise RuntimeError(
|
|
124
|
+
"window.sentience.findTextRect is not available. "
|
|
125
|
+
"Please update the Sentience extension to the latest version."
|
|
126
|
+
)
|
|
127
|
+
except RuntimeError:
|
|
128
|
+
raise
|
|
129
|
+
except Exception as e:
|
|
130
|
+
raise RuntimeError(f"Failed to verify findTextRect availability: {e}") from e
|
|
131
|
+
|
|
132
|
+
# Call the extension's findTextRect method
|
|
133
|
+
result_dict = browser.page.evaluate(
|
|
134
|
+
"""
|
|
135
|
+
(options) => {
|
|
136
|
+
return window.sentience.findTextRect(options);
|
|
137
|
+
}
|
|
138
|
+
""",
|
|
139
|
+
{
|
|
140
|
+
"text": text,
|
|
141
|
+
"caseSensitive": case_sensitive,
|
|
142
|
+
"wholeWord": whole_word,
|
|
143
|
+
"maxResults": max_results,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Parse and validate with Pydantic
|
|
148
|
+
return TextRectSearchResult(**result_dict)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def find_text_rect_async(
|
|
152
|
+
browser: AsyncSentienceBrowser,
|
|
153
|
+
text: str,
|
|
154
|
+
case_sensitive: bool = False,
|
|
155
|
+
whole_word: bool = False,
|
|
156
|
+
max_results: int = 10,
|
|
157
|
+
) -> TextRectSearchResult:
|
|
158
|
+
"""
|
|
159
|
+
Find all occurrences of text on the page and get their exact pixel coordinates (async).
|
|
160
|
+
|
|
161
|
+
This function searches for text in all visible text nodes on the page and returns
|
|
162
|
+
the bounding rectangles for each match. Useful for:
|
|
163
|
+
- Finding specific UI elements by their text content
|
|
164
|
+
- Locating buttons, links, or labels without element IDs
|
|
165
|
+
- Getting exact coordinates for click automation
|
|
166
|
+
- Highlighting search results visually
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
browser: AsyncSentienceBrowser instance
|
|
170
|
+
text: Text to search for (required)
|
|
171
|
+
case_sensitive: If True, search is case-sensitive (default: False)
|
|
172
|
+
whole_word: If True, only match whole words surrounded by whitespace (default: False)
|
|
173
|
+
max_results: Maximum number of matches to return (default: 10, max: 100)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
TextRectSearchResult with:
|
|
177
|
+
- status: "success" or "error"
|
|
178
|
+
- query: The search text
|
|
179
|
+
- case_sensitive: Whether search was case-sensitive
|
|
180
|
+
- whole_word: Whether whole-word matching was used
|
|
181
|
+
- matches: Number of matches found
|
|
182
|
+
- results: List of TextMatch objects, each containing:
|
|
183
|
+
- text: The matched text
|
|
184
|
+
- rect: Absolute rectangle (with scroll offset)
|
|
185
|
+
- viewport_rect: Viewport-relative rectangle
|
|
186
|
+
- context: Surrounding text (before/after)
|
|
187
|
+
- in_viewport: Whether visible in current viewport
|
|
188
|
+
- viewport: Current viewport dimensions and scroll position
|
|
189
|
+
- error: Error message if status is "error"
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
# Find "Sign In" button
|
|
193
|
+
result = await find_text_rect_async(browser, "Sign In")
|
|
194
|
+
if result.status == "success" and result.results:
|
|
195
|
+
first_match = result.results[0]
|
|
196
|
+
print(f"Found at: ({first_match.rect.x}, {first_match.rect.y})")
|
|
197
|
+
print(f"Size: {first_match.rect.width}x{first_match.rect.height}")
|
|
198
|
+
print(f"In viewport: {first_match.in_viewport}")
|
|
199
|
+
|
|
200
|
+
# Case-sensitive search
|
|
201
|
+
result = await find_text_rect_async(browser, "LOGIN", case_sensitive=True)
|
|
202
|
+
|
|
203
|
+
# Whole word only
|
|
204
|
+
result = await find_text_rect_async(browser, "log", whole_word=True) # Won't match "login"
|
|
205
|
+
|
|
206
|
+
# Find all matches and click the first visible one
|
|
207
|
+
result = await find_text_rect_async(browser, "Buy Now", max_results=5)
|
|
208
|
+
if result.status == "success" and result.results:
|
|
209
|
+
for match in result.results:
|
|
210
|
+
if match.in_viewport:
|
|
211
|
+
# Use click_rect_async from actions module
|
|
212
|
+
from sentience.actions import click_rect_async
|
|
213
|
+
click_result = await click_rect_async(browser, {
|
|
214
|
+
"x": match.rect.x,
|
|
215
|
+
"y": match.rect.y,
|
|
216
|
+
"w": match.rect.width,
|
|
217
|
+
"h": match.rect.height
|
|
218
|
+
})
|
|
219
|
+
break
|
|
220
|
+
"""
|
|
221
|
+
if not browser.page:
|
|
222
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
223
|
+
|
|
224
|
+
if not text or not text.strip():
|
|
225
|
+
return TextRectSearchResult(
|
|
226
|
+
status="error",
|
|
227
|
+
error="Text parameter is required and cannot be empty",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Limit max_results to prevent performance issues
|
|
231
|
+
max_results = min(max_results, 100)
|
|
232
|
+
|
|
233
|
+
# CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
|
|
234
|
+
# The new architecture loads injected_api.js asynchronously, so window.sentience
|
|
235
|
+
# may not be immediately available after page load
|
|
236
|
+
try:
|
|
237
|
+
await browser.page.wait_for_function(
|
|
238
|
+
"typeof window.sentience !== 'undefined'",
|
|
239
|
+
timeout=5000, # 5 second timeout
|
|
240
|
+
)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
# Gather diagnostics if wait fails
|
|
243
|
+
try:
|
|
244
|
+
diag = await browser.page.evaluate(
|
|
245
|
+
"""() => ({
|
|
246
|
+
sentience_defined: typeof window.sentience !== 'undefined',
|
|
247
|
+
extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
|
|
248
|
+
url: window.location.href
|
|
249
|
+
})"""
|
|
250
|
+
)
|
|
251
|
+
except Exception:
|
|
252
|
+
diag = {"error": "Could not gather diagnostics"}
|
|
253
|
+
|
|
254
|
+
raise RuntimeError(
|
|
255
|
+
f"Sentience extension failed to inject window.sentience API. "
|
|
256
|
+
f"Is the extension loaded? Diagnostics: {diag}"
|
|
257
|
+
) from e
|
|
258
|
+
|
|
259
|
+
# Verify findTextRect method exists (for older extension versions that don't have it)
|
|
260
|
+
try:
|
|
261
|
+
has_find_text_rect = await browser.page.evaluate(
|
|
262
|
+
"typeof window.sentience.findTextRect !== 'undefined'"
|
|
263
|
+
)
|
|
264
|
+
if not has_find_text_rect:
|
|
265
|
+
raise RuntimeError(
|
|
266
|
+
"window.sentience.findTextRect is not available. "
|
|
267
|
+
"Please update the Sentience extension to the latest version."
|
|
268
|
+
)
|
|
269
|
+
except RuntimeError:
|
|
270
|
+
raise
|
|
271
|
+
except Exception as e:
|
|
272
|
+
raise RuntimeError(f"Failed to verify findTextRect availability: {e}") from e
|
|
273
|
+
|
|
274
|
+
# Call the extension's findTextRect method
|
|
275
|
+
result_dict = await browser.page.evaluate(
|
|
276
|
+
"""
|
|
277
|
+
(options) => {
|
|
278
|
+
return window.sentience.findTextRect(options);
|
|
279
|
+
}
|
|
280
|
+
""",
|
|
281
|
+
{
|
|
282
|
+
"text": text,
|
|
283
|
+
"caseSensitive": case_sensitive,
|
|
284
|
+
"wholeWord": whole_word,
|
|
285
|
+
"maxResults": max_results,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Parse and validate with Pydantic
|
|
290
|
+
return TextRectSearchResult(**result_dict)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace indexing module for Sentience SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .index_schema import (
|
|
6
|
+
ActionInfo,
|
|
7
|
+
SnapshotInfo,
|
|
8
|
+
StepCounters,
|
|
9
|
+
StepIndex,
|
|
10
|
+
TraceFileInfo,
|
|
11
|
+
TraceIndex,
|
|
12
|
+
TraceSummary,
|
|
13
|
+
)
|
|
14
|
+
from .indexer import build_trace_index, read_step_events, write_trace_index
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"build_trace_index",
|
|
18
|
+
"write_trace_index",
|
|
19
|
+
"read_step_events",
|
|
20
|
+
"TraceIndex",
|
|
21
|
+
"StepIndex",
|
|
22
|
+
"TraceSummary",
|
|
23
|
+
"TraceFileInfo",
|
|
24
|
+
"SnapshotInfo",
|
|
25
|
+
"ActionInfo",
|
|
26
|
+
"StepCounters",
|
|
27
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions for trace index schema using concrete classes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from typing import List, Literal, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class TraceFileInfo:
|
|
11
|
+
"""Metadata about the trace file."""
|
|
12
|
+
|
|
13
|
+
path: str
|
|
14
|
+
size_bytes: int
|
|
15
|
+
sha256: str
|
|
16
|
+
|
|
17
|
+
def to_dict(self) -> dict:
|
|
18
|
+
return asdict(self)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class TraceSummary:
|
|
23
|
+
"""High-level summary of the trace."""
|
|
24
|
+
|
|
25
|
+
first_ts: str
|
|
26
|
+
last_ts: str
|
|
27
|
+
event_count: int
|
|
28
|
+
step_count: int
|
|
29
|
+
error_count: int
|
|
30
|
+
final_url: str | None
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return asdict(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class SnapshotInfo:
|
|
38
|
+
"""Snapshot metadata for index."""
|
|
39
|
+
|
|
40
|
+
snapshot_id: str | None = None
|
|
41
|
+
digest: str | None = None
|
|
42
|
+
url: str | None = None
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ActionInfo:
|
|
50
|
+
"""Action metadata for index."""
|
|
51
|
+
|
|
52
|
+
type: str | None = None
|
|
53
|
+
target_element_id: int | None = None
|
|
54
|
+
args_digest: str | None = None
|
|
55
|
+
success: bool | None = None
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return asdict(self)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class StepCounters:
|
|
63
|
+
"""Event counters per step."""
|
|
64
|
+
|
|
65
|
+
events: int = 0
|
|
66
|
+
snapshots: int = 0
|
|
67
|
+
actions: int = 0
|
|
68
|
+
llm_calls: int = 0
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict:
|
|
71
|
+
return asdict(self)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class StepIndex:
|
|
76
|
+
"""Index entry for a single step."""
|
|
77
|
+
|
|
78
|
+
step_index: int
|
|
79
|
+
step_id: str
|
|
80
|
+
goal: str | None
|
|
81
|
+
status: Literal["ok", "error", "partial"]
|
|
82
|
+
ts_start: str
|
|
83
|
+
ts_end: str
|
|
84
|
+
offset_start: int
|
|
85
|
+
offset_end: int
|
|
86
|
+
url_before: str | None
|
|
87
|
+
url_after: str | None
|
|
88
|
+
snapshot_before: SnapshotInfo
|
|
89
|
+
snapshot_after: SnapshotInfo
|
|
90
|
+
action: ActionInfo
|
|
91
|
+
counters: StepCounters
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict:
|
|
94
|
+
result = asdict(self)
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class TraceIndex:
|
|
100
|
+
"""Complete trace index schema."""
|
|
101
|
+
|
|
102
|
+
version: int
|
|
103
|
+
run_id: str
|
|
104
|
+
created_at: str
|
|
105
|
+
trace_file: TraceFileInfo
|
|
106
|
+
summary: TraceSummary
|
|
107
|
+
steps: list[StepIndex] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> dict:
|
|
110
|
+
"""Convert to dictionary for JSON serialization."""
|
|
111
|
+
return asdict(self)
|