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.

Files changed (50) hide show
  1. sentience/__init__.py +153 -0
  2. sentience/_extension_loader.py +40 -0
  3. sentience/actions.py +837 -0
  4. sentience/agent.py +1246 -0
  5. sentience/agent_config.py +43 -0
  6. sentience/async_api.py +101 -0
  7. sentience/base_agent.py +194 -0
  8. sentience/browser.py +1037 -0
  9. sentience/cli.py +130 -0
  10. sentience/cloud_tracing.py +382 -0
  11. sentience/conversational_agent.py +509 -0
  12. sentience/expect.py +188 -0
  13. sentience/extension/background.js +233 -0
  14. sentience/extension/content.js +298 -0
  15. sentience/extension/injected_api.js +1473 -0
  16. sentience/extension/manifest.json +36 -0
  17. sentience/extension/pkg/sentience_core.d.ts +51 -0
  18. sentience/extension/pkg/sentience_core.js +529 -0
  19. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  20. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  21. sentience/extension/release.json +115 -0
  22. sentience/extension/test-content.js +4 -0
  23. sentience/formatting.py +59 -0
  24. sentience/generator.py +202 -0
  25. sentience/inspector.py +365 -0
  26. sentience/llm_provider.py +637 -0
  27. sentience/models.py +412 -0
  28. sentience/overlay.py +222 -0
  29. sentience/query.py +303 -0
  30. sentience/read.py +185 -0
  31. sentience/recorder.py +589 -0
  32. sentience/schemas/trace_v1.json +216 -0
  33. sentience/screenshot.py +100 -0
  34. sentience/snapshot.py +516 -0
  35. sentience/text_search.py +290 -0
  36. sentience/trace_indexing/__init__.py +27 -0
  37. sentience/trace_indexing/index_schema.py +111 -0
  38. sentience/trace_indexing/indexer.py +357 -0
  39. sentience/tracer_factory.py +211 -0
  40. sentience/tracing.py +285 -0
  41. sentience/utils.py +296 -0
  42. sentience/wait.py +137 -0
  43. sentienceapi-0.90.17.dist-info/METADATA +917 -0
  44. sentienceapi-0.90.17.dist-info/RECORD +50 -0
  45. sentienceapi-0.90.17.dist-info/WHEEL +5 -0
  46. sentienceapi-0.90.17.dist-info/entry_points.txt +2 -0
  47. sentienceapi-0.90.17.dist-info/licenses/LICENSE +24 -0
  48. sentienceapi-0.90.17.dist-info/licenses/LICENSE-APACHE +201 -0
  49. sentienceapi-0.90.17.dist-info/licenses/LICENSE-MIT +21 -0
  50. sentienceapi-0.90.17.dist-info/top_level.txt +1 -0
@@ -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)