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,393 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CDP Backend implementation for browser-use integration.
|
|
3
|
+
|
|
4
|
+
This module provides CDPBackendV0, which implements BrowserBackend protocol
|
|
5
|
+
using Chrome DevTools Protocol (CDP) commands.
|
|
6
|
+
|
|
7
|
+
Usage with browser-use:
|
|
8
|
+
from browser_use import BrowserSession
|
|
9
|
+
from sentience.backends import CDPBackendV0
|
|
10
|
+
from sentience.backends.browser_use_adapter import BrowserUseAdapter
|
|
11
|
+
|
|
12
|
+
session = BrowserSession(...)
|
|
13
|
+
await session.start()
|
|
14
|
+
|
|
15
|
+
adapter = BrowserUseAdapter(session)
|
|
16
|
+
backend = await adapter.create_backend()
|
|
17
|
+
|
|
18
|
+
# Now use backend for Sentience operations
|
|
19
|
+
viewport = await backend.refresh_page_info()
|
|
20
|
+
await backend.mouse_click(100, 200)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import base64
|
|
25
|
+
import time
|
|
26
|
+
from typing import Any, Literal, Protocol, runtime_checkable
|
|
27
|
+
|
|
28
|
+
from .protocol import BrowserBackend, LayoutMetrics, ViewportInfo
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class CDPTransport(Protocol):
|
|
33
|
+
"""
|
|
34
|
+
Protocol for CDP transport layer.
|
|
35
|
+
|
|
36
|
+
This abstracts the actual CDP communication, allowing different
|
|
37
|
+
implementations (browser-use, Playwright CDP, raw WebSocket).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
async def send(self, method: str, params: dict | None = None) -> dict:
|
|
41
|
+
"""
|
|
42
|
+
Send a CDP command and return the result.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
method: CDP method name, e.g., "Runtime.evaluate"
|
|
46
|
+
params: Method parameters
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
CDP response dict
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CDPBackendV0:
|
|
55
|
+
"""
|
|
56
|
+
CDP-based implementation of BrowserBackend.
|
|
57
|
+
|
|
58
|
+
This backend uses CDP commands to interact with the browser,
|
|
59
|
+
making it compatible with browser-use's CDP client.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, transport: CDPTransport) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Initialize CDP backend.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
transport: CDP transport for sending commands
|
|
68
|
+
"""
|
|
69
|
+
self._transport = transport
|
|
70
|
+
self._cached_viewport: ViewportInfo | None = None
|
|
71
|
+
self._execution_context_id: int | None = None
|
|
72
|
+
|
|
73
|
+
async def _get_execution_context(self) -> int:
|
|
74
|
+
"""Get or create execution context ID for Runtime.callFunctionOn."""
|
|
75
|
+
if self._execution_context_id is not None:
|
|
76
|
+
return self._execution_context_id
|
|
77
|
+
|
|
78
|
+
# Enable Runtime domain if not already enabled
|
|
79
|
+
try:
|
|
80
|
+
await self._transport.send("Runtime.enable")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass # May already be enabled
|
|
83
|
+
|
|
84
|
+
# Get the main frame's execution context
|
|
85
|
+
result = await self._transport.send(
|
|
86
|
+
"Runtime.evaluate",
|
|
87
|
+
{
|
|
88
|
+
"expression": "1",
|
|
89
|
+
"returnByValue": True,
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Extract context ID from the result
|
|
94
|
+
if "executionContextId" in result:
|
|
95
|
+
self._execution_context_id = result["executionContextId"]
|
|
96
|
+
else:
|
|
97
|
+
# Fallback: use context ID 1 (main frame)
|
|
98
|
+
self._execution_context_id = 1
|
|
99
|
+
|
|
100
|
+
return self._execution_context_id
|
|
101
|
+
|
|
102
|
+
async def refresh_page_info(self) -> ViewportInfo:
|
|
103
|
+
"""Cache viewport + scroll offsets; cheap & safe to call often."""
|
|
104
|
+
result = await self.eval(
|
|
105
|
+
"""(() => ({
|
|
106
|
+
width: window.innerWidth,
|
|
107
|
+
height: window.innerHeight,
|
|
108
|
+
scroll_x: window.scrollX,
|
|
109
|
+
scroll_y: window.scrollY,
|
|
110
|
+
content_width: document.documentElement.scrollWidth,
|
|
111
|
+
content_height: document.documentElement.scrollHeight
|
|
112
|
+
}))()"""
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self._cached_viewport = ViewportInfo(
|
|
116
|
+
width=result.get("width", 0),
|
|
117
|
+
height=result.get("height", 0),
|
|
118
|
+
scroll_x=result.get("scroll_x", 0),
|
|
119
|
+
scroll_y=result.get("scroll_y", 0),
|
|
120
|
+
content_width=result.get("content_width"),
|
|
121
|
+
content_height=result.get("content_height"),
|
|
122
|
+
)
|
|
123
|
+
return self._cached_viewport
|
|
124
|
+
|
|
125
|
+
async def eval(self, expression: str) -> Any:
|
|
126
|
+
"""Evaluate JavaScript expression using Runtime.evaluate."""
|
|
127
|
+
result = await self._transport.send(
|
|
128
|
+
"Runtime.evaluate",
|
|
129
|
+
{
|
|
130
|
+
"expression": expression,
|
|
131
|
+
"returnByValue": True,
|
|
132
|
+
"awaitPromise": True,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Check for exceptions
|
|
137
|
+
if "exceptionDetails" in result:
|
|
138
|
+
exc = result["exceptionDetails"]
|
|
139
|
+
text = exc.get("text", "Unknown error")
|
|
140
|
+
raise RuntimeError(f"JavaScript evaluation failed: {text}")
|
|
141
|
+
|
|
142
|
+
# Extract value from result
|
|
143
|
+
if "result" in result:
|
|
144
|
+
res = result["result"]
|
|
145
|
+
if res.get("type") == "undefined":
|
|
146
|
+
return None
|
|
147
|
+
return res.get("value")
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
async def call(
|
|
152
|
+
self,
|
|
153
|
+
function_declaration: str,
|
|
154
|
+
args: list[Any] | None = None,
|
|
155
|
+
) -> Any:
|
|
156
|
+
"""Call JavaScript function using Runtime.callFunctionOn."""
|
|
157
|
+
# Build call arguments
|
|
158
|
+
call_args = []
|
|
159
|
+
if args:
|
|
160
|
+
for arg in args:
|
|
161
|
+
if arg is None:
|
|
162
|
+
call_args.append({"value": None})
|
|
163
|
+
elif isinstance(arg, bool):
|
|
164
|
+
call_args.append({"value": arg})
|
|
165
|
+
elif isinstance(arg, (int, float)):
|
|
166
|
+
call_args.append({"value": arg})
|
|
167
|
+
elif isinstance(arg, str):
|
|
168
|
+
call_args.append({"value": arg})
|
|
169
|
+
elif isinstance(arg, dict):
|
|
170
|
+
call_args.append({"value": arg})
|
|
171
|
+
elif isinstance(arg, list):
|
|
172
|
+
call_args.append({"value": arg})
|
|
173
|
+
else:
|
|
174
|
+
# Serialize complex objects to JSON
|
|
175
|
+
call_args.append({"value": str(arg)})
|
|
176
|
+
|
|
177
|
+
# We need an object ID to call function on
|
|
178
|
+
# Use globalThis (window) as the target
|
|
179
|
+
global_result = await self._transport.send(
|
|
180
|
+
"Runtime.evaluate",
|
|
181
|
+
{
|
|
182
|
+
"expression": "globalThis",
|
|
183
|
+
"returnByValue": False,
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
object_id = global_result.get("result", {}).get("objectId")
|
|
188
|
+
if not object_id:
|
|
189
|
+
# Fallback: evaluate the function directly
|
|
190
|
+
if args:
|
|
191
|
+
args_json = ", ".join(repr(a) if isinstance(a, str) else str(a) for a in args)
|
|
192
|
+
expression = f"({function_declaration})({args_json})"
|
|
193
|
+
else:
|
|
194
|
+
expression = f"({function_declaration})()"
|
|
195
|
+
return await self.eval(expression)
|
|
196
|
+
|
|
197
|
+
result = await self._transport.send(
|
|
198
|
+
"Runtime.callFunctionOn",
|
|
199
|
+
{
|
|
200
|
+
"functionDeclaration": function_declaration,
|
|
201
|
+
"objectId": object_id,
|
|
202
|
+
"arguments": call_args,
|
|
203
|
+
"returnByValue": True,
|
|
204
|
+
"awaitPromise": True,
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Check for exceptions
|
|
209
|
+
if "exceptionDetails" in result:
|
|
210
|
+
exc = result["exceptionDetails"]
|
|
211
|
+
text = exc.get("text", "Unknown error")
|
|
212
|
+
raise RuntimeError(f"JavaScript call failed: {text}")
|
|
213
|
+
|
|
214
|
+
# Extract value from result
|
|
215
|
+
if "result" in result:
|
|
216
|
+
res = result["result"]
|
|
217
|
+
if res.get("type") == "undefined":
|
|
218
|
+
return None
|
|
219
|
+
return res.get("value")
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
async def get_layout_metrics(self) -> LayoutMetrics:
|
|
224
|
+
"""Get page layout metrics using Page.getLayoutMetrics."""
|
|
225
|
+
result = await self._transport.send("Page.getLayoutMetrics")
|
|
226
|
+
|
|
227
|
+
# Extract metrics from result
|
|
228
|
+
layout_viewport = result.get("layoutViewport", {})
|
|
229
|
+
content_size = result.get("contentSize", {})
|
|
230
|
+
visual_viewport = result.get("visualViewport", {})
|
|
231
|
+
|
|
232
|
+
return LayoutMetrics(
|
|
233
|
+
viewport_x=visual_viewport.get("pageX", 0),
|
|
234
|
+
viewport_y=visual_viewport.get("pageY", 0),
|
|
235
|
+
viewport_width=visual_viewport.get(
|
|
236
|
+
"clientWidth", layout_viewport.get("clientWidth", 0)
|
|
237
|
+
),
|
|
238
|
+
viewport_height=visual_viewport.get(
|
|
239
|
+
"clientHeight", layout_viewport.get("clientHeight", 0)
|
|
240
|
+
),
|
|
241
|
+
content_width=content_size.get("width", 0),
|
|
242
|
+
content_height=content_size.get("height", 0),
|
|
243
|
+
device_scale_factor=visual_viewport.get("scale", 1.0),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
async def screenshot_png(self) -> bytes:
|
|
247
|
+
"""Capture viewport screenshot as PNG bytes."""
|
|
248
|
+
result = await self._transport.send(
|
|
249
|
+
"Page.captureScreenshot",
|
|
250
|
+
{
|
|
251
|
+
"format": "png",
|
|
252
|
+
"captureBeyondViewport": False,
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
data = result.get("data", "")
|
|
257
|
+
return base64.b64decode(data)
|
|
258
|
+
|
|
259
|
+
async def mouse_move(self, x: float, y: float) -> None:
|
|
260
|
+
"""Move mouse to viewport coordinates."""
|
|
261
|
+
await self._transport.send(
|
|
262
|
+
"Input.dispatchMouseEvent",
|
|
263
|
+
{
|
|
264
|
+
"type": "mouseMoved",
|
|
265
|
+
"x": x,
|
|
266
|
+
"y": y,
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
async def mouse_click(
|
|
271
|
+
self,
|
|
272
|
+
x: float,
|
|
273
|
+
y: float,
|
|
274
|
+
button: Literal["left", "right", "middle"] = "left",
|
|
275
|
+
click_count: int = 1,
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Click at viewport coordinates."""
|
|
278
|
+
# Mouse down
|
|
279
|
+
await self._transport.send(
|
|
280
|
+
"Input.dispatchMouseEvent",
|
|
281
|
+
{
|
|
282
|
+
"type": "mousePressed",
|
|
283
|
+
"x": x,
|
|
284
|
+
"y": y,
|
|
285
|
+
"button": button,
|
|
286
|
+
"clickCount": click_count,
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Small delay between press and release
|
|
291
|
+
await asyncio.sleep(0.05)
|
|
292
|
+
|
|
293
|
+
# Mouse up
|
|
294
|
+
await self._transport.send(
|
|
295
|
+
"Input.dispatchMouseEvent",
|
|
296
|
+
{
|
|
297
|
+
"type": "mouseReleased",
|
|
298
|
+
"x": x,
|
|
299
|
+
"y": y,
|
|
300
|
+
"button": button,
|
|
301
|
+
"clickCount": click_count,
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
async def wheel(
|
|
306
|
+
self,
|
|
307
|
+
delta_y: float,
|
|
308
|
+
x: float | None = None,
|
|
309
|
+
y: float | None = None,
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Scroll using mouse wheel."""
|
|
312
|
+
# Get viewport center if coordinates not provided
|
|
313
|
+
if x is None or y is None:
|
|
314
|
+
if self._cached_viewport is None:
|
|
315
|
+
await self.refresh_page_info()
|
|
316
|
+
assert self._cached_viewport is not None
|
|
317
|
+
x = x if x is not None else self._cached_viewport.width / 2
|
|
318
|
+
y = y if y is not None else self._cached_viewport.height / 2
|
|
319
|
+
|
|
320
|
+
await self._transport.send(
|
|
321
|
+
"Input.dispatchMouseEvent",
|
|
322
|
+
{
|
|
323
|
+
"type": "mouseWheel",
|
|
324
|
+
"x": x,
|
|
325
|
+
"y": y,
|
|
326
|
+
"deltaX": 0,
|
|
327
|
+
"deltaY": delta_y,
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
async def type_text(self, text: str) -> None:
|
|
332
|
+
"""Type text using keyboard input."""
|
|
333
|
+
for char in text:
|
|
334
|
+
# Key down
|
|
335
|
+
await self._transport.send(
|
|
336
|
+
"Input.dispatchKeyEvent",
|
|
337
|
+
{
|
|
338
|
+
"type": "keyDown",
|
|
339
|
+
"text": char,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Char event (for text input)
|
|
344
|
+
await self._transport.send(
|
|
345
|
+
"Input.dispatchKeyEvent",
|
|
346
|
+
{
|
|
347
|
+
"type": "char",
|
|
348
|
+
"text": char,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Key up
|
|
353
|
+
await self._transport.send(
|
|
354
|
+
"Input.dispatchKeyEvent",
|
|
355
|
+
{
|
|
356
|
+
"type": "keyUp",
|
|
357
|
+
"text": char,
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Small delay between characters
|
|
362
|
+
await asyncio.sleep(0.01)
|
|
363
|
+
|
|
364
|
+
async def wait_ready_state(
|
|
365
|
+
self,
|
|
366
|
+
state: Literal["interactive", "complete"] = "interactive",
|
|
367
|
+
timeout_ms: int = 15000,
|
|
368
|
+
) -> None:
|
|
369
|
+
"""Wait for document.readyState using polling."""
|
|
370
|
+
start = time.monotonic()
|
|
371
|
+
timeout_sec = timeout_ms / 1000.0
|
|
372
|
+
|
|
373
|
+
# Map state to acceptable states
|
|
374
|
+
acceptable_states = {"complete"} if state == "complete" else {"interactive", "complete"}
|
|
375
|
+
|
|
376
|
+
while True:
|
|
377
|
+
elapsed = time.monotonic() - start
|
|
378
|
+
if elapsed >= timeout_sec:
|
|
379
|
+
raise TimeoutError(
|
|
380
|
+
f"Timed out waiting for document.readyState='{state}' " f"after {timeout_ms}ms"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
current_state = await self.eval("document.readyState")
|
|
384
|
+
if current_state in acceptable_states:
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
# Poll every 100ms
|
|
388
|
+
await asyncio.sleep(0.1)
|
|
389
|
+
|
|
390
|
+
async def get_url(self) -> str:
|
|
391
|
+
"""Get current page URL."""
|
|
392
|
+
result = await self.eval("window.location.href")
|
|
393
|
+
return result if result else ""
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for Sentience backends.
|
|
3
|
+
|
|
4
|
+
These exceptions provide clear, actionable error messages when things go wrong
|
|
5
|
+
during browser-use integration or backend operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SentienceBackendError(Exception):
|
|
13
|
+
"""Base exception for all Sentience backend errors."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ExtensionDiagnostics:
|
|
20
|
+
"""Diagnostics collected when extension loading fails."""
|
|
21
|
+
|
|
22
|
+
sentience_defined: bool = False
|
|
23
|
+
sentience_snapshot: bool = False
|
|
24
|
+
url: str = ""
|
|
25
|
+
error: str | None = None
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, data: dict[str, Any]) -> "ExtensionDiagnostics":
|
|
29
|
+
"""Create from diagnostic dict returned by browser eval."""
|
|
30
|
+
return cls(
|
|
31
|
+
sentience_defined=data.get("sentience_defined", False),
|
|
32
|
+
sentience_snapshot=data.get("sentience_snapshot", False),
|
|
33
|
+
url=data.get("url", ""),
|
|
34
|
+
error=data.get("error"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
"""Convert to dict for serialization."""
|
|
39
|
+
return {
|
|
40
|
+
"sentience_defined": self.sentience_defined,
|
|
41
|
+
"sentience_snapshot": self.sentience_snapshot,
|
|
42
|
+
"url": self.url,
|
|
43
|
+
"error": self.error,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExtensionNotLoadedError(SentienceBackendError):
|
|
48
|
+
"""
|
|
49
|
+
Raised when the Sentience extension is not loaded in the browser.
|
|
50
|
+
|
|
51
|
+
This typically means:
|
|
52
|
+
1. Browser was launched without --load-extension flag
|
|
53
|
+
2. Extension path is incorrect
|
|
54
|
+
3. Extension failed to initialize
|
|
55
|
+
|
|
56
|
+
Example fix for browser-use:
|
|
57
|
+
from sentience import get_extension_dir
|
|
58
|
+
from browser_use import BrowserSession, BrowserProfile
|
|
59
|
+
|
|
60
|
+
profile = BrowserProfile(
|
|
61
|
+
args=[f"--load-extension={get_extension_dir()}"],
|
|
62
|
+
)
|
|
63
|
+
session = BrowserSession(browser_profile=profile)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
message: str,
|
|
69
|
+
timeout_ms: int | None = None,
|
|
70
|
+
diagnostics: ExtensionDiagnostics | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
self.timeout_ms = timeout_ms
|
|
73
|
+
self.diagnostics = diagnostics
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_timeout(
|
|
78
|
+
cls,
|
|
79
|
+
timeout_ms: int,
|
|
80
|
+
diagnostics: ExtensionDiagnostics | None = None,
|
|
81
|
+
) -> "ExtensionNotLoadedError":
|
|
82
|
+
"""Create error from timeout during extension wait."""
|
|
83
|
+
diag_info = ""
|
|
84
|
+
if diagnostics:
|
|
85
|
+
if diagnostics.error:
|
|
86
|
+
diag_info = f"\n Error: {diagnostics.error}"
|
|
87
|
+
else:
|
|
88
|
+
diag_info = (
|
|
89
|
+
f"\n window.sentience defined: {diagnostics.sentience_defined}"
|
|
90
|
+
f"\n window.sentience.snapshot available: {diagnostics.sentience_snapshot}"
|
|
91
|
+
f"\n Page URL: {diagnostics.url}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
message = (
|
|
95
|
+
f"Sentience extension not loaded after {timeout_ms}ms.{diag_info}\n\n"
|
|
96
|
+
"To fix this, ensure the extension is loaded when launching the browser:\n\n"
|
|
97
|
+
" from sentience import get_extension_dir\n"
|
|
98
|
+
" from browser_use import BrowserSession, BrowserProfile\n\n"
|
|
99
|
+
" profile = BrowserProfile(\n"
|
|
100
|
+
f' args=[f"--load-extension={{get_extension_dir()}}"],\n'
|
|
101
|
+
" )\n"
|
|
102
|
+
" session = BrowserSession(browser_profile=profile)\n"
|
|
103
|
+
)
|
|
104
|
+
return cls(message, timeout_ms=timeout_ms, diagnostics=diagnostics)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ExtensionInjectionError(SentienceBackendError):
|
|
108
|
+
"""
|
|
109
|
+
Raised when window.sentience API is not available on the page.
|
|
110
|
+
|
|
111
|
+
This can happen when:
|
|
112
|
+
1. Page loaded before extension could inject
|
|
113
|
+
2. Page has Content Security Policy blocking extension
|
|
114
|
+
3. Extension crashed or was disabled
|
|
115
|
+
|
|
116
|
+
Call snapshot() with a longer timeout or wait for page load.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
message: str,
|
|
122
|
+
url: str | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
self.url = url
|
|
125
|
+
super().__init__(message)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_page(cls, url: str) -> "ExtensionInjectionError":
|
|
129
|
+
"""Create error for a specific page."""
|
|
130
|
+
message = (
|
|
131
|
+
f"window.sentience API not available on page: {url}\n\n"
|
|
132
|
+
"Possible causes:\n"
|
|
133
|
+
" 1. Page loaded before extension could inject (try increasing timeout)\n"
|
|
134
|
+
" 2. Page has Content Security Policy blocking the extension\n"
|
|
135
|
+
" 3. Extension was disabled or crashed\n\n"
|
|
136
|
+
"Try:\n"
|
|
137
|
+
" snap = await snapshot(backend, options=SnapshotOptions(timeout_ms=10000))"
|
|
138
|
+
)
|
|
139
|
+
return cls(message, url=url)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class BackendEvalError(SentienceBackendError):
|
|
143
|
+
"""
|
|
144
|
+
Raised when JavaScript evaluation fails in the browser.
|
|
145
|
+
|
|
146
|
+
This wraps underlying CDP or Playwright errors with context.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
message: str,
|
|
152
|
+
expression: str | None = None,
|
|
153
|
+
original_error: Exception | None = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
self.expression = expression
|
|
156
|
+
self.original_error = original_error
|
|
157
|
+
super().__init__(message)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SnapshotError(SentienceBackendError):
|
|
161
|
+
"""
|
|
162
|
+
Raised when taking a snapshot fails.
|
|
163
|
+
|
|
164
|
+
This can happen when:
|
|
165
|
+
1. Extension returned null or invalid data
|
|
166
|
+
2. Page is in an invalid state
|
|
167
|
+
3. Extension threw an error
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
message: str,
|
|
173
|
+
url: str | None = None,
|
|
174
|
+
raw_result: Any = None,
|
|
175
|
+
) -> None:
|
|
176
|
+
self.url = url
|
|
177
|
+
self.raw_result = raw_result
|
|
178
|
+
super().__init__(message)
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_null_result(cls, url: str | None = None) -> "SnapshotError":
|
|
182
|
+
"""Create error for null snapshot result."""
|
|
183
|
+
message = (
|
|
184
|
+
"window.sentience.snapshot() returned null.\n\n"
|
|
185
|
+
"Possible causes:\n"
|
|
186
|
+
" 1. Extension is not properly initialized\n"
|
|
187
|
+
" 2. Page DOM is in an invalid state\n"
|
|
188
|
+
" 3. Extension encountered an internal error\n\n"
|
|
189
|
+
"Try refreshing the page and taking a new snapshot."
|
|
190
|
+
)
|
|
191
|
+
if url:
|
|
192
|
+
message = f"{message}\n Page URL: {url}"
|
|
193
|
+
return cls(message, url=url, raw_result=None)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ActionError(SentienceBackendError):
|
|
197
|
+
"""
|
|
198
|
+
Raised when a browser action (click, type, scroll) fails.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
action: str,
|
|
204
|
+
message: str,
|
|
205
|
+
coordinates: tuple[float, float] | None = None,
|
|
206
|
+
original_error: Exception | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
self.action = action
|
|
209
|
+
self.coordinates = coordinates
|
|
210
|
+
self.original_error = original_error
|
|
211
|
+
super().__init__(f"{action} failed: {message}")
|