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.

Files changed (82) hide show
  1. sentience/__init__.py +253 -0
  2. sentience/_extension_loader.py +195 -0
  3. sentience/action_executor.py +215 -0
  4. sentience/actions.py +1020 -0
  5. sentience/agent.py +1181 -0
  6. sentience/agent_config.py +46 -0
  7. sentience/agent_runtime.py +424 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +108 -0
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +343 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +427 -0
  21. sentience/base_agent.py +196 -0
  22. sentience/browser.py +1215 -0
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cli.py +130 -0
  26. sentience/cloud_tracing.py +807 -0
  27. sentience/constants.py +6 -0
  28. sentience/conversational_agent.py +543 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +188 -0
  31. sentience/extension/background.js +104 -0
  32. sentience/extension/content.js +161 -0
  33. sentience/extension/injected_api.js +914 -0
  34. sentience/extension/manifest.json +36 -0
  35. sentience/extension/pkg/sentience_core.d.ts +51 -0
  36. sentience/extension/pkg/sentience_core.js +323 -0
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  39. sentience/extension/release.json +115 -0
  40. sentience/formatting.py +15 -0
  41. sentience/generator.py +202 -0
  42. sentience/inspector.py +367 -0
  43. sentience/llm_interaction_handler.py +191 -0
  44. sentience/llm_provider.py +875 -0
  45. sentience/llm_provider_utils.py +120 -0
  46. sentience/llm_response_builder.py +153 -0
  47. sentience/models.py +846 -0
  48. sentience/ordinal.py +280 -0
  49. sentience/overlay.py +222 -0
  50. sentience/protocols.py +228 -0
  51. sentience/query.py +303 -0
  52. sentience/read.py +188 -0
  53. sentience/recorder.py +589 -0
  54. sentience/schemas/trace_v1.json +335 -0
  55. sentience/screenshot.py +100 -0
  56. sentience/sentience_methods.py +86 -0
  57. sentience/snapshot.py +706 -0
  58. sentience/snapshot_diff.py +126 -0
  59. sentience/text_search.py +262 -0
  60. sentience/trace_event_builder.py +148 -0
  61. sentience/trace_file_manager.py +197 -0
  62. sentience/trace_indexing/__init__.py +27 -0
  63. sentience/trace_indexing/index_schema.py +199 -0
  64. sentience/trace_indexing/indexer.py +414 -0
  65. sentience/tracer_factory.py +322 -0
  66. sentience/tracing.py +449 -0
  67. sentience/utils/__init__.py +40 -0
  68. sentience/utils/browser.py +46 -0
  69. sentience/utils/element.py +257 -0
  70. sentience/utils/formatting.py +59 -0
  71. sentience/utils.py +296 -0
  72. sentience/verification.py +380 -0
  73. sentience/visual_agent.py +2058 -0
  74. sentience/wait.py +139 -0
  75. sentienceapi-0.95.0.dist-info/METADATA +984 -0
  76. sentienceapi-0.95.0.dist-info/RECORD +82 -0
  77. sentienceapi-0.95.0.dist-info/WHEEL +5 -0
  78. sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
  79. sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
  80. sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
  81. sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
  82. sentienceapi-0.95.0.dist-info/top_level.txt +1 -0
sentience/protocols.py ADDED
@@ -0,0 +1,228 @@
1
+ """
2
+ Protocol definitions for testability and dependency injection.
3
+
4
+ These protocols define the minimal interface required by agent classes,
5
+ enabling better testability through mocking while maintaining type safety.
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
9
+
10
+ if TYPE_CHECKING:
11
+ from playwright.async_api import Page as AsyncPage
12
+ from playwright.sync_api import Page
13
+
14
+ from .models import Snapshot
15
+
16
+
17
+ @runtime_checkable
18
+ class PageProtocol(Protocol):
19
+ """
20
+ Protocol for Playwright Page operations used by agents.
21
+
22
+ This protocol defines the minimal interface required from Playwright's Page object.
23
+ Agents use this interface to interact with the browser page.
24
+ """
25
+
26
+ @property
27
+ def url(self) -> str:
28
+ """Current page URL."""
29
+ ...
30
+
31
+ def evaluate(self, script: str, *args: Any, **kwargs: Any) -> Any:
32
+ """
33
+ Evaluate JavaScript in the page context.
34
+
35
+ Args:
36
+ script: JavaScript code to evaluate
37
+ *args: Arguments to pass to the script
38
+ **kwargs: Keyword arguments to pass to the script
39
+
40
+ Returns:
41
+ Result of the JavaScript evaluation
42
+ """
43
+ ...
44
+
45
+ def goto(self, url: str, **kwargs: Any) -> Any | None:
46
+ """
47
+ Navigate to a URL.
48
+
49
+ Args:
50
+ url: URL to navigate to
51
+ **kwargs: Additional navigation options
52
+
53
+ Returns:
54
+ Response object or None
55
+ """
56
+ ...
57
+
58
+ def wait_for_timeout(self, timeout: int) -> None:
59
+ """
60
+ Wait for a specified timeout.
61
+
62
+ Args:
63
+ timeout: Timeout in milliseconds
64
+ """
65
+ ...
66
+
67
+ def wait_for_load_state(self, state: str = "load", timeout: int | None = None) -> None:
68
+ """
69
+ Wait for page load state.
70
+
71
+ Args:
72
+ state: Load state to wait for (e.g., "load", "domcontentloaded", "networkidle")
73
+ timeout: Optional timeout in milliseconds
74
+ """
75
+ ...
76
+
77
+
78
+ @runtime_checkable
79
+ class BrowserProtocol(Protocol):
80
+ """
81
+ Protocol for browser operations used by agents.
82
+
83
+ This protocol defines the minimal interface required from SentienceBrowser.
84
+ Agents use this interface to interact with the browser and take snapshots.
85
+
86
+ Note: SentienceBrowser naturally implements this protocol, so no changes
87
+ are required to existing code. This protocol enables better testability
88
+ through mocking.
89
+ """
90
+
91
+ @property
92
+ def page(self) -> PageProtocol | None:
93
+ """
94
+ Current Playwright Page object.
95
+
96
+ Returns:
97
+ Page object if browser is started, None otherwise
98
+ """
99
+ ...
100
+
101
+ def start(self) -> None:
102
+ """Start the browser session."""
103
+ ...
104
+
105
+ def close(self, output_path: str | None = None) -> str | None:
106
+ """
107
+ Close the browser session.
108
+
109
+ Args:
110
+ output_path: Optional path to save browser state/output
111
+
112
+ Returns:
113
+ Path to saved output or None
114
+ """
115
+ ...
116
+
117
+ def goto(self, url: str) -> None:
118
+ """
119
+ Navigate to a URL.
120
+
121
+ Args:
122
+ url: URL to navigate to
123
+ """
124
+ ...
125
+
126
+
127
+ @runtime_checkable
128
+ class AsyncPageProtocol(Protocol):
129
+ """
130
+ Protocol for async Playwright Page operations.
131
+
132
+ Similar to PageProtocol but for async operations.
133
+ """
134
+
135
+ @property
136
+ def url(self) -> str:
137
+ """Current page URL."""
138
+ ...
139
+
140
+ async def evaluate(self, script: str, *args: Any, **kwargs: Any) -> Any:
141
+ """
142
+ Evaluate JavaScript in the page context (async).
143
+
144
+ Args:
145
+ script: JavaScript code to evaluate
146
+ *args: Arguments to pass to the script
147
+ **kwargs: Keyword arguments to pass to the script
148
+
149
+ Returns:
150
+ Result of the JavaScript evaluation
151
+ """
152
+ ...
153
+
154
+ async def goto(self, url: str, **kwargs: Any) -> Any | None:
155
+ """
156
+ Navigate to a URL (async).
157
+
158
+ Args:
159
+ url: URL to navigate to
160
+ **kwargs: Additional navigation options
161
+
162
+ Returns:
163
+ Response object or None
164
+ """
165
+ ...
166
+
167
+ async def wait_for_timeout(self, timeout: int) -> None:
168
+ """
169
+ Wait for a specified timeout (async).
170
+
171
+ Args:
172
+ timeout: Timeout in milliseconds
173
+ """
174
+ ...
175
+
176
+ async def wait_for_load_state(self, state: str = "load", timeout: int | None = None) -> None:
177
+ """
178
+ Wait for page load state (async).
179
+
180
+ Args:
181
+ state: Load state to wait for (e.g., "load", "domcontentloaded", "networkidle")
182
+ timeout: Optional timeout in milliseconds
183
+ """
184
+ ...
185
+
186
+
187
+ @runtime_checkable
188
+ class AsyncBrowserProtocol(Protocol):
189
+ """
190
+ Protocol for async browser operations.
191
+
192
+ Similar to BrowserProtocol but for async operations.
193
+ """
194
+
195
+ @property
196
+ def page(self) -> AsyncPageProtocol | None:
197
+ """
198
+ Current Playwright AsyncPage object.
199
+
200
+ Returns:
201
+ AsyncPage object if browser is started, None otherwise
202
+ """
203
+ ...
204
+
205
+ async def start(self) -> None:
206
+ """Start the browser session (async)."""
207
+ ...
208
+
209
+ async def close(self, output_path: str | None = None) -> str | None:
210
+ """
211
+ Close the browser session (async).
212
+
213
+ Args:
214
+ output_path: Optional path to save browser state/output
215
+
216
+ Returns:
217
+ Path to saved output or None
218
+ """
219
+ ...
220
+
221
+ async def goto(self, url: str) -> None:
222
+ """
223
+ Navigate to a URL (async).
224
+
225
+ Args:
226
+ url: URL to navigate to
227
+ """
228
+ ...
sentience/query.py ADDED
@@ -0,0 +1,303 @@
1
+ """
2
+ Query engine v1 - semantic selector matching
3
+ """
4
+
5
+ import re
6
+ from typing import Any, Optional
7
+
8
+ from .models import Element, Snapshot
9
+
10
+
11
+ def parse_selector(selector: str) -> dict[str, Any]: # noqa: C901
12
+ """
13
+ Parse string DSL selector into structured query
14
+
15
+ Examples:
16
+ "role=button text~'Sign in'"
17
+ "role=textbox name~'email'"
18
+ "clickable=true role=link"
19
+ "role!=link"
20
+ "importance>500"
21
+ "text^='Sign'"
22
+ "text$='in'"
23
+ """
24
+ query: dict[str, Any] = {}
25
+
26
+ # Match patterns like: key=value, key~'value', key!="value", key>123, key^='prefix', key$='suffix'
27
+ # Updated regex to support: =, !=, ~, ^=, $=, >, >=, <, <=
28
+ # Supports dot notation: attr.id, css.color
29
+ # Note: Handle ^= and $= first (before single char operators) to avoid regex conflicts
30
+ # Pattern matches: key, operator (including ^= and $=), and value (quoted or unquoted)
31
+ pattern = r"([\w.]+)(\^=|\$=|>=|<=|!=|[=~<>])((?:\'[^\']+\'|\"[^\"]+\"|[^\s]+))"
32
+ matches = re.findall(pattern, selector)
33
+
34
+ for key, op, value in matches:
35
+ # Remove quotes from value
36
+ value = value.strip().strip("\"'")
37
+
38
+ # Handle numeric comparisons
39
+ is_numeric = False
40
+ try:
41
+ numeric_value = float(value)
42
+ is_numeric = True
43
+ except ValueError:
44
+ pass
45
+
46
+ if op == "!=":
47
+ if key == "role":
48
+ query["role_exclude"] = value
49
+ elif key == "clickable":
50
+ query["clickable"] = False
51
+ elif key == "visible":
52
+ query["visible"] = False
53
+ elif op == "~":
54
+ # Substring match (case-insensitive)
55
+ if key == "text" or key == "name":
56
+ query["text_contains"] = value
57
+ elif op == "^=":
58
+ # Prefix match
59
+ if key == "text" or key == "name":
60
+ query["text_prefix"] = value
61
+ elif op == "$=":
62
+ # Suffix match
63
+ if key == "text" or key == "name":
64
+ query["text_suffix"] = value
65
+ elif op == ">":
66
+ # Greater than
67
+ if is_numeric:
68
+ if key == "importance":
69
+ query["importance_min"] = numeric_value + 0.0001 # Exclusive
70
+ elif key.startswith("bbox."):
71
+ query[f"{key}_min"] = numeric_value + 0.0001
72
+ elif key == "z_index":
73
+ query["z_index_min"] = numeric_value + 0.0001
74
+ elif key.startswith("attr.") or key.startswith("css."):
75
+ query[f"{key}_gt"] = value
76
+ elif op == ">=":
77
+ # Greater than or equal
78
+ if is_numeric:
79
+ if key == "importance":
80
+ query["importance_min"] = numeric_value
81
+ elif key.startswith("bbox."):
82
+ query[f"{key}_min"] = numeric_value
83
+ elif key == "z_index":
84
+ query["z_index_min"] = numeric_value
85
+ elif key.startswith("attr.") or key.startswith("css."):
86
+ query[f"{key}_gte"] = value
87
+ elif op == "<":
88
+ # Less than
89
+ if is_numeric:
90
+ if key == "importance":
91
+ query["importance_max"] = numeric_value - 0.0001 # Exclusive
92
+ elif key.startswith("bbox."):
93
+ query[f"{key}_max"] = numeric_value - 0.0001
94
+ elif key == "z_index":
95
+ query["z_index_max"] = numeric_value - 0.0001
96
+ elif key.startswith("attr.") or key.startswith("css."):
97
+ query[f"{key}_lt"] = value
98
+ elif op == "<=":
99
+ # Less than or equal
100
+ if is_numeric:
101
+ if key == "importance":
102
+ query["importance_max"] = numeric_value
103
+ elif key.startswith("bbox."):
104
+ query[f"{key}_max"] = numeric_value
105
+ elif key == "z_index":
106
+ query["z_index_max"] = numeric_value
107
+ elif key.startswith("attr.") or key.startswith("css."):
108
+ query[f"{key}_lte"] = value
109
+ elif op == "=":
110
+ # Exact match
111
+ if key == "role":
112
+ query["role"] = value
113
+ elif key == "clickable":
114
+ query["clickable"] = value.lower() == "true"
115
+ elif key == "visible":
116
+ query["visible"] = value.lower() == "true"
117
+ elif key == "tag":
118
+ query["tag"] = value
119
+ elif key == "name" or key == "text":
120
+ query["text"] = value
121
+ elif key == "importance" and is_numeric:
122
+ query["importance"] = numeric_value
123
+ elif key.startswith("attr."):
124
+ # Dot notation for attributes: attr.id="submit-btn"
125
+ attr_key = key[5:] # Remove "attr." prefix
126
+ if "attr" not in query:
127
+ query["attr"] = {}
128
+ query["attr"][attr_key] = value
129
+ elif key.startswith("css."):
130
+ # Dot notation for CSS: css.color="red"
131
+ css_key = key[4:] # Remove "css." prefix
132
+ if "css" not in query:
133
+ query["css"] = {}
134
+ query["css"][css_key] = value
135
+
136
+ return query
137
+
138
+
139
+ def match_element(element: Element, query: dict[str, Any]) -> bool: # noqa: C901
140
+ """Check if element matches query criteria"""
141
+
142
+ # Role exact match
143
+ if "role" in query:
144
+ if element.role != query["role"]:
145
+ return False
146
+
147
+ # Role exclusion
148
+ if "role_exclude" in query:
149
+ if element.role == query["role_exclude"]:
150
+ return False
151
+
152
+ # Clickable
153
+ if "clickable" in query:
154
+ if element.visual_cues.is_clickable != query["clickable"]:
155
+ return False
156
+
157
+ # Visible (using in_viewport and !is_occluded)
158
+ if "visible" in query:
159
+ is_visible = element.in_viewport and not element.is_occluded
160
+ if is_visible != query["visible"]:
161
+ return False
162
+
163
+ # Tag (not yet in Element model, but prepare for future)
164
+ if "tag" in query:
165
+ # For now, this will always fail since tag is not in Element model
166
+ # This is a placeholder for future implementation
167
+ pass
168
+
169
+ # Text exact match
170
+ if "text" in query:
171
+ if not element.text or element.text != query["text"]:
172
+ return False
173
+
174
+ # Text contains (case-insensitive)
175
+ if "text_contains" in query:
176
+ if not element.text:
177
+ return False
178
+ if query["text_contains"].lower() not in element.text.lower():
179
+ return False
180
+
181
+ # Text prefix match
182
+ if "text_prefix" in query:
183
+ if not element.text:
184
+ return False
185
+ if not element.text.lower().startswith(query["text_prefix"].lower()):
186
+ return False
187
+
188
+ # Text suffix match
189
+ if "text_suffix" in query:
190
+ if not element.text:
191
+ return False
192
+ if not element.text.lower().endswith(query["text_suffix"].lower()):
193
+ return False
194
+
195
+ # Importance filtering
196
+ if "importance" in query:
197
+ if element.importance != query["importance"]:
198
+ return False
199
+ if "importance_min" in query:
200
+ if element.importance < query["importance_min"]:
201
+ return False
202
+ if "importance_max" in query:
203
+ if element.importance > query["importance_max"]:
204
+ return False
205
+
206
+ # BBox filtering (spatial)
207
+ if "bbox.x_min" in query:
208
+ if element.bbox.x < query["bbox.x_min"]:
209
+ return False
210
+ if "bbox.x_max" in query:
211
+ if element.bbox.x > query["bbox.x_max"]:
212
+ return False
213
+ if "bbox.y_min" in query:
214
+ if element.bbox.y < query["bbox.y_min"]:
215
+ return False
216
+ if "bbox.y_max" in query:
217
+ if element.bbox.y > query["bbox.y_max"]:
218
+ return False
219
+ if "bbox.width_min" in query:
220
+ if element.bbox.width < query["bbox.width_min"]:
221
+ return False
222
+ if "bbox.width_max" in query:
223
+ if element.bbox.width > query["bbox.width_max"]:
224
+ return False
225
+ if "bbox.height_min" in query:
226
+ if element.bbox.height < query["bbox.height_min"]:
227
+ return False
228
+ if "bbox.height_max" in query:
229
+ if element.bbox.height > query["bbox.height_max"]:
230
+ return False
231
+
232
+ # Z-index filtering
233
+ if "z_index_min" in query:
234
+ if element.z_index < query["z_index_min"]:
235
+ return False
236
+ if "z_index_max" in query:
237
+ if element.z_index > query["z_index_max"]:
238
+ return False
239
+
240
+ # In viewport filtering
241
+ if "in_viewport" in query:
242
+ if element.in_viewport != query["in_viewport"]:
243
+ return False
244
+
245
+ # Occlusion filtering
246
+ if "is_occluded" in query:
247
+ if element.is_occluded != query["is_occluded"]:
248
+ return False
249
+
250
+ # Attribute filtering (dot notation: attr.id="submit-btn")
251
+ if "attr" in query:
252
+ # This requires DOM access, which is not available in the Element model
253
+ # This is a placeholder for future implementation when we add DOM access
254
+ pass
255
+
256
+ # CSS property filtering (dot notation: css.color="red")
257
+ if "css" in query:
258
+ # This requires DOM access, which is not available in the Element model
259
+ # This is a placeholder for future implementation when we add DOM access
260
+ pass
261
+
262
+ return True
263
+
264
+
265
+ def query(snapshot: Snapshot, selector: str | dict[str, Any]) -> list[Element]:
266
+ """
267
+ Query elements from snapshot using semantic selector
268
+
269
+ Args:
270
+ snapshot: Snapshot object
271
+ selector: String DSL (e.g., "role=button text~'Sign in'") or dict query
272
+
273
+ Returns:
274
+ List of matching elements, sorted by importance (descending)
275
+ """
276
+ # Parse selector if string
277
+ if isinstance(selector, str):
278
+ query_dict = parse_selector(selector)
279
+ else:
280
+ query_dict = selector
281
+
282
+ # Filter elements
283
+ matches = [el for el in snapshot.elements if match_element(el, query_dict)]
284
+
285
+ # Sort by importance (descending)
286
+ matches.sort(key=lambda el: el.importance, reverse=True)
287
+
288
+ return matches
289
+
290
+
291
+ def find(snapshot: Snapshot, selector: str | dict[str, Any]) -> Element | None:
292
+ """
293
+ Find single element matching selector (best match by importance)
294
+
295
+ Args:
296
+ snapshot: Snapshot object
297
+ selector: String DSL or dict query
298
+
299
+ Returns:
300
+ Best matching element or None
301
+ """
302
+ results = query(snapshot, selector)
303
+ return results[0] if results else None