sentienceapi 0.90.16__py3-none-any.whl → 0.98.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 +120 -6
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +217 -0
- sentience/actions.py +758 -30
- sentience/agent.py +806 -293
- sentience/agent_config.py +3 -0
- sentience/agent_runtime.py +840 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +89 -1141
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +372 -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 +483 -0
- sentience/base_agent.py +95 -0
- sentience/browser.py +678 -39
- sentience/browser_evaluator.py +299 -0
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +507 -42
- sentience/constants.py +6 -0
- sentience/conversational_agent.py +77 -43
- sentience/cursor_policy.py +142 -0
- sentience/element_filter.py +136 -0
- sentience/expect.py +98 -2
- sentience/extension/background.js +56 -185
- sentience/extension/content.js +150 -287
- sentience/extension/injected_api.js +1088 -1368
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +275 -433
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +47 -47
- sentience/failure_artifacts.py +241 -0
- sentience/formatting.py +9 -53
- sentience/inspector.py +183 -1
- sentience/integrations/__init__.py +6 -0
- sentience/integrations/langchain/__init__.py +12 -0
- sentience/integrations/langchain/context.py +18 -0
- sentience/integrations/langchain/core.py +326 -0
- sentience/integrations/langchain/tools.py +180 -0
- sentience/integrations/models.py +46 -0
- sentience/integrations/pydanticai/__init__.py +15 -0
- sentience/integrations/pydanticai/deps.py +20 -0
- sentience/integrations/pydanticai/toolset.py +468 -0
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +765 -66
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +595 -3
- sentience/ordinal.py +280 -0
- sentience/overlay.py +109 -2
- sentience/protocols.py +228 -0
- sentience/query.py +67 -5
- sentience/read.py +95 -3
- sentience/recorder.py +223 -3
- sentience/schemas/trace_v1.json +128 -9
- sentience/screenshot.py +48 -2
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +599 -55
- sentience/snapshot_diff.py +126 -0
- sentience/text_search.py +120 -5
- sentience/trace_event_builder.py +148 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/index_schema.py +95 -7
- sentience/trace_indexing/indexer.py +105 -48
- sentience/tracer_factory.py +120 -9
- sentience/tracing.py +172 -8
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/{utils.py → utils/element.py} +3 -42
- sentience/utils/formatting.py +59 -0
- sentience/verification.py +618 -0
- sentience/visual_agent.py +2058 -0
- sentience/wait.py +68 -2
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/extension/test-content.js +0 -4
- sentienceapi-0.90.16.dist-info/RECORD +0 -50
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend-agnostic actions for browser-use integration.
|
|
3
|
+
|
|
4
|
+
These actions work with any BrowserBackend implementation,
|
|
5
|
+
enabling Sentience grounding with browser-use or other frameworks.
|
|
6
|
+
|
|
7
|
+
Usage with browser-use:
|
|
8
|
+
from sentience.backends import BrowserUseAdapter
|
|
9
|
+
from sentience.backends.actions import click, type_text, scroll
|
|
10
|
+
|
|
11
|
+
adapter = BrowserUseAdapter(session)
|
|
12
|
+
backend = await adapter.create_backend()
|
|
13
|
+
|
|
14
|
+
# Take snapshot and click element
|
|
15
|
+
snap = await snapshot_from_backend(backend)
|
|
16
|
+
element = find(snap, 'role=button[name="Submit"]')
|
|
17
|
+
await click(backend, element.bbox)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import time
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
23
|
+
|
|
24
|
+
from ..cursor_policy import CursorPolicy, build_human_cursor_path
|
|
25
|
+
from ..models import ActionResult, BBox, Snapshot
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .protocol import BrowserBackend
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def click(
|
|
32
|
+
backend: "BrowserBackend",
|
|
33
|
+
target: BBox | dict[str, float] | tuple[float, float],
|
|
34
|
+
button: Literal["left", "right", "middle"] = "left",
|
|
35
|
+
click_count: int = 1,
|
|
36
|
+
move_first: bool = True,
|
|
37
|
+
cursor_policy: CursorPolicy | None = None,
|
|
38
|
+
) -> ActionResult:
|
|
39
|
+
"""
|
|
40
|
+
Click at coordinates using the backend.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
backend: BrowserBackend implementation
|
|
44
|
+
target: Click target - BBox (clicks center), dict with x/y, or (x, y) tuple
|
|
45
|
+
button: Mouse button to click
|
|
46
|
+
click_count: Number of clicks (1=single, 2=double)
|
|
47
|
+
move_first: Whether to move mouse to position before clicking
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
ActionResult with success status
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
# Click at coordinates
|
|
54
|
+
await click(backend, (100, 200))
|
|
55
|
+
|
|
56
|
+
# Click element bbox center
|
|
57
|
+
await click(backend, element.bbox)
|
|
58
|
+
|
|
59
|
+
# Double-click
|
|
60
|
+
await click(backend, element.bbox, click_count=2)
|
|
61
|
+
"""
|
|
62
|
+
start_time = time.time()
|
|
63
|
+
|
|
64
|
+
# Resolve coordinates
|
|
65
|
+
x, y = _resolve_coordinates(target)
|
|
66
|
+
cursor_meta: dict | None = None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Optional mouse move for hover effects
|
|
70
|
+
if move_first:
|
|
71
|
+
if cursor_policy is not None and cursor_policy.mode == "human":
|
|
72
|
+
pos = getattr(backend, "_sentience_cursor_pos", None)
|
|
73
|
+
if not isinstance(pos, tuple) or len(pos) != 2:
|
|
74
|
+
pos = (float(x), float(y))
|
|
75
|
+
|
|
76
|
+
cursor_meta = build_human_cursor_path(
|
|
77
|
+
start=(float(pos[0]), float(pos[1])),
|
|
78
|
+
target=(float(x), float(y)),
|
|
79
|
+
policy=cursor_policy,
|
|
80
|
+
)
|
|
81
|
+
pts = cursor_meta.get("path", [])
|
|
82
|
+
duration_ms_move = int(cursor_meta.get("duration_ms") or 0)
|
|
83
|
+
per_step_s = (
|
|
84
|
+
(duration_ms_move / max(1, len(pts))) / 1000.0 if duration_ms_move > 0 else 0.0
|
|
85
|
+
)
|
|
86
|
+
for p in pts:
|
|
87
|
+
await backend.mouse_move(float(p["x"]), float(p["y"]))
|
|
88
|
+
if per_step_s > 0:
|
|
89
|
+
await asyncio.sleep(per_step_s)
|
|
90
|
+
pause_ms = int(cursor_meta.get("pause_before_click_ms") or 0)
|
|
91
|
+
if pause_ms > 0:
|
|
92
|
+
await asyncio.sleep(pause_ms / 1000.0)
|
|
93
|
+
else:
|
|
94
|
+
await backend.mouse_move(x, y)
|
|
95
|
+
await asyncio.sleep(0.02) # Brief pause for hover
|
|
96
|
+
|
|
97
|
+
# Perform click
|
|
98
|
+
await backend.mouse_click(x, y, button=button, click_count=click_count)
|
|
99
|
+
setattr(backend, "_sentience_cursor_pos", (float(x), float(y)))
|
|
100
|
+
|
|
101
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
102
|
+
return ActionResult(
|
|
103
|
+
success=True,
|
|
104
|
+
duration_ms=duration_ms,
|
|
105
|
+
outcome="dom_updated",
|
|
106
|
+
cursor=cursor_meta,
|
|
107
|
+
)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
110
|
+
return ActionResult(
|
|
111
|
+
success=False,
|
|
112
|
+
duration_ms=duration_ms,
|
|
113
|
+
outcome="error",
|
|
114
|
+
error={"code": "click_failed", "reason": str(e)},
|
|
115
|
+
cursor=cursor_meta,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def type_text(
|
|
120
|
+
backend: "BrowserBackend",
|
|
121
|
+
text: str,
|
|
122
|
+
target: BBox | dict[str, float] | tuple[float, float] | None = None,
|
|
123
|
+
clear_first: bool = False,
|
|
124
|
+
) -> ActionResult:
|
|
125
|
+
"""
|
|
126
|
+
Type text, optionally clicking a target first.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
backend: BrowserBackend implementation
|
|
130
|
+
text: Text to type
|
|
131
|
+
target: Optional click target before typing (BBox, dict, or tuple)
|
|
132
|
+
clear_first: If True, select all and delete before typing
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
ActionResult with success status
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
# Type into focused element
|
|
139
|
+
await type_text(backend, "Hello World")
|
|
140
|
+
|
|
141
|
+
# Click input then type
|
|
142
|
+
await type_text(backend, "search query", target=search_box.bbox)
|
|
143
|
+
|
|
144
|
+
# Clear and type
|
|
145
|
+
await type_text(backend, "new value", target=input.bbox, clear_first=True)
|
|
146
|
+
"""
|
|
147
|
+
start_time = time.time()
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# Click target if provided
|
|
151
|
+
if target is not None:
|
|
152
|
+
x, y = _resolve_coordinates(target)
|
|
153
|
+
await backend.mouse_click(x, y)
|
|
154
|
+
await asyncio.sleep(0.05) # Wait for focus
|
|
155
|
+
|
|
156
|
+
# Clear existing content if requested
|
|
157
|
+
if clear_first:
|
|
158
|
+
# Select all (Ctrl+A / Cmd+A) and delete
|
|
159
|
+
await backend.eval("document.execCommand('selectAll')")
|
|
160
|
+
await asyncio.sleep(0.02)
|
|
161
|
+
|
|
162
|
+
# Type the text
|
|
163
|
+
await backend.type_text(text)
|
|
164
|
+
|
|
165
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
166
|
+
return ActionResult(
|
|
167
|
+
success=True,
|
|
168
|
+
duration_ms=duration_ms,
|
|
169
|
+
outcome="dom_updated",
|
|
170
|
+
)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
173
|
+
return ActionResult(
|
|
174
|
+
success=False,
|
|
175
|
+
duration_ms=duration_ms,
|
|
176
|
+
outcome="error",
|
|
177
|
+
error={"code": "type_failed", "reason": str(e)},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def scroll(
|
|
182
|
+
backend: "BrowserBackend",
|
|
183
|
+
delta_y: float = 300,
|
|
184
|
+
target: BBox | dict[str, float] | tuple[float, float] | None = None,
|
|
185
|
+
) -> ActionResult:
|
|
186
|
+
"""
|
|
187
|
+
Scroll the page or element.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
backend: BrowserBackend implementation
|
|
191
|
+
delta_y: Scroll amount (positive=down, negative=up)
|
|
192
|
+
target: Optional position for scroll (defaults to viewport center)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
ActionResult with success status
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
# Scroll down 300px
|
|
199
|
+
await scroll(backend, 300)
|
|
200
|
+
|
|
201
|
+
# Scroll up 500px
|
|
202
|
+
await scroll(backend, -500)
|
|
203
|
+
|
|
204
|
+
# Scroll at specific position
|
|
205
|
+
await scroll(backend, 200, target=(500, 300))
|
|
206
|
+
"""
|
|
207
|
+
start_time = time.time()
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
x: float | None = None
|
|
211
|
+
y: float | None = None
|
|
212
|
+
|
|
213
|
+
if target is not None:
|
|
214
|
+
x, y = _resolve_coordinates(target)
|
|
215
|
+
|
|
216
|
+
await backend.wheel(delta_y=delta_y, x=x, y=y)
|
|
217
|
+
|
|
218
|
+
# Wait for scroll to settle
|
|
219
|
+
await asyncio.sleep(0.1)
|
|
220
|
+
|
|
221
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
222
|
+
return ActionResult(
|
|
223
|
+
success=True,
|
|
224
|
+
duration_ms=duration_ms,
|
|
225
|
+
outcome="dom_updated",
|
|
226
|
+
)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
229
|
+
return ActionResult(
|
|
230
|
+
success=False,
|
|
231
|
+
duration_ms=duration_ms,
|
|
232
|
+
outcome="error",
|
|
233
|
+
error={"code": "scroll_failed", "reason": str(e)},
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def scroll_to_element(
|
|
238
|
+
backend: "BrowserBackend",
|
|
239
|
+
element_id: int,
|
|
240
|
+
behavior: Literal["smooth", "instant", "auto"] = "instant",
|
|
241
|
+
block: Literal["start", "center", "end", "nearest"] = "center",
|
|
242
|
+
) -> ActionResult:
|
|
243
|
+
"""
|
|
244
|
+
Scroll element into view using JavaScript scrollIntoView.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
backend: BrowserBackend implementation
|
|
248
|
+
element_id: Element ID from snapshot (requires sentience_registry)
|
|
249
|
+
behavior: Scroll behavior
|
|
250
|
+
block: Vertical alignment
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
ActionResult with success status
|
|
254
|
+
"""
|
|
255
|
+
start_time = time.time()
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
scrolled = await backend.eval(
|
|
259
|
+
f"""
|
|
260
|
+
(() => {{
|
|
261
|
+
const el = window.sentience_registry && window.sentience_registry[{element_id}];
|
|
262
|
+
if (el && el.scrollIntoView) {{
|
|
263
|
+
el.scrollIntoView({{
|
|
264
|
+
behavior: '{behavior}',
|
|
265
|
+
block: '{block}',
|
|
266
|
+
inline: 'nearest'
|
|
267
|
+
}});
|
|
268
|
+
return true;
|
|
269
|
+
}}
|
|
270
|
+
return false;
|
|
271
|
+
}})()
|
|
272
|
+
"""
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Wait for scroll animation
|
|
276
|
+
wait_time = 0.3 if behavior == "smooth" else 0.05
|
|
277
|
+
await asyncio.sleep(wait_time)
|
|
278
|
+
|
|
279
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
280
|
+
|
|
281
|
+
if scrolled:
|
|
282
|
+
return ActionResult(
|
|
283
|
+
success=True,
|
|
284
|
+
duration_ms=duration_ms,
|
|
285
|
+
outcome="dom_updated",
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
return ActionResult(
|
|
289
|
+
success=False,
|
|
290
|
+
duration_ms=duration_ms,
|
|
291
|
+
outcome="error",
|
|
292
|
+
error={"code": "scroll_failed", "reason": "Element not found in registry"},
|
|
293
|
+
)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
296
|
+
return ActionResult(
|
|
297
|
+
success=False,
|
|
298
|
+
duration_ms=duration_ms,
|
|
299
|
+
outcome="error",
|
|
300
|
+
error={"code": "scroll_failed", "reason": str(e)},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def wait_for_stable(
|
|
305
|
+
backend: "BrowserBackend",
|
|
306
|
+
state: Literal["interactive", "complete"] = "complete",
|
|
307
|
+
timeout_ms: int = 10000,
|
|
308
|
+
) -> ActionResult:
|
|
309
|
+
"""
|
|
310
|
+
Wait for page to reach stable state.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
backend: BrowserBackend implementation
|
|
314
|
+
state: Target document.readyState
|
|
315
|
+
timeout_ms: Maximum wait time
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
ActionResult with success status
|
|
319
|
+
"""
|
|
320
|
+
start_time = time.time()
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
await backend.wait_ready_state(state=state, timeout_ms=timeout_ms)
|
|
324
|
+
|
|
325
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
326
|
+
return ActionResult(
|
|
327
|
+
success=True,
|
|
328
|
+
duration_ms=duration_ms,
|
|
329
|
+
outcome="dom_updated",
|
|
330
|
+
)
|
|
331
|
+
except TimeoutError as e:
|
|
332
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
333
|
+
return ActionResult(
|
|
334
|
+
success=False,
|
|
335
|
+
duration_ms=duration_ms,
|
|
336
|
+
outcome="error",
|
|
337
|
+
error={"code": "timeout", "reason": str(e)},
|
|
338
|
+
)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
341
|
+
return ActionResult(
|
|
342
|
+
success=False,
|
|
343
|
+
duration_ms=duration_ms,
|
|
344
|
+
outcome="error",
|
|
345
|
+
error={"code": "wait_failed", "reason": str(e)},
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _resolve_coordinates(
|
|
350
|
+
target: BBox | dict[str, float] | tuple[float, float],
|
|
351
|
+
) -> tuple[float, float]:
|
|
352
|
+
"""
|
|
353
|
+
Resolve target to (x, y) coordinates.
|
|
354
|
+
|
|
355
|
+
- BBox: Returns center point
|
|
356
|
+
- dict: Returns x, y keys (or center if width/height present)
|
|
357
|
+
- tuple: Returns as-is
|
|
358
|
+
"""
|
|
359
|
+
if isinstance(target, BBox):
|
|
360
|
+
return (target.x + target.width / 2, target.y + target.height / 2)
|
|
361
|
+
elif isinstance(target, tuple):
|
|
362
|
+
return target
|
|
363
|
+
elif isinstance(target, dict):
|
|
364
|
+
# If has width/height, compute center
|
|
365
|
+
if "width" in target and "height" in target:
|
|
366
|
+
x = target.get("x", 0) + target["width"] / 2
|
|
367
|
+
y = target.get("y", 0) + target["height"] / 2
|
|
368
|
+
return (x, y)
|
|
369
|
+
# Otherwise use x/y directly
|
|
370
|
+
return (target.get("x", 0), target.get("y", 0))
|
|
371
|
+
else:
|
|
372
|
+
raise ValueError(f"Invalid target type: {type(target)}")
|