sentienceapi 0.90.9__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/actions.py +439 -0
- sentience/agent.py +687 -0
- sentience/agent_config.py +43 -0
- sentience/base_agent.py +101 -0
- sentience/browser.py +409 -0
- sentience/cli.py +130 -0
- sentience/cloud_tracing.py +292 -0
- sentience/conversational_agent.py +509 -0
- sentience/expect.py +92 -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 +185 -0
- sentience/llm_provider.py +431 -0
- sentience/models.py +406 -0
- sentience/overlay.py +115 -0
- sentience/query.py +303 -0
- sentience/read.py +96 -0
- sentience/recorder.py +369 -0
- sentience/schemas/trace_v1.json +216 -0
- sentience/screenshot.py +54 -0
- sentience/snapshot.py +282 -0
- sentience/text_search.py +107 -0
- sentience/trace_indexing/__init__.py +27 -0
- sentience/trace_indexing/index_schema.py +111 -0
- sentience/trace_indexing/indexer.py +363 -0
- sentience/tracer_factory.py +211 -0
- sentience/tracing.py +285 -0
- sentience/utils.py +296 -0
- sentience/wait.py +73 -0
- sentienceapi-0.90.9.dist-info/METADATA +878 -0
- sentienceapi-0.90.9.dist-info/RECORD +46 -0
- sentienceapi-0.90.9.dist-info/WHEEL +5 -0
- sentienceapi-0.90.9.dist-info/entry_points.txt +2 -0
- sentienceapi-0.90.9.dist-info/licenses/LICENSE.md +43 -0
- sentienceapi-0.90.9.dist-info/top_level.txt +1 -0
sentience/__init__.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sentience Python SDK - AI Agent Browser Automation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .actions import click, click_rect, press, type_text
|
|
6
|
+
from .agent import SentienceAgent
|
|
7
|
+
from .agent_config import AgentConfig
|
|
8
|
+
|
|
9
|
+
# Agent Layer (Phase 1 & 2)
|
|
10
|
+
from .base_agent import BaseAgent
|
|
11
|
+
from .browser import SentienceBrowser
|
|
12
|
+
|
|
13
|
+
# Tracing (v0.12.0+)
|
|
14
|
+
from .cloud_tracing import CloudTraceSink, SentienceLogger
|
|
15
|
+
from .conversational_agent import ConversationalAgent
|
|
16
|
+
from .expect import expect
|
|
17
|
+
|
|
18
|
+
# Formatting (v0.12.0+)
|
|
19
|
+
from .formatting import format_snapshot_for_llm
|
|
20
|
+
from .generator import ScriptGenerator, generate
|
|
21
|
+
from .inspector import Inspector, inspect
|
|
22
|
+
from .llm_provider import (
|
|
23
|
+
AnthropicProvider,
|
|
24
|
+
LLMProvider,
|
|
25
|
+
LLMResponse,
|
|
26
|
+
LocalLLMProvider,
|
|
27
|
+
OpenAIProvider,
|
|
28
|
+
)
|
|
29
|
+
from .models import ( # Agent Layer Models
|
|
30
|
+
ActionHistory,
|
|
31
|
+
ActionResult,
|
|
32
|
+
ActionTokenUsage,
|
|
33
|
+
AgentActionResult,
|
|
34
|
+
BBox,
|
|
35
|
+
Cookie,
|
|
36
|
+
Element,
|
|
37
|
+
LocalStorageItem,
|
|
38
|
+
OriginStorage,
|
|
39
|
+
ScreenshotConfig,
|
|
40
|
+
Snapshot,
|
|
41
|
+
SnapshotFilter,
|
|
42
|
+
SnapshotOptions,
|
|
43
|
+
StorageState,
|
|
44
|
+
TextContext,
|
|
45
|
+
TextMatch,
|
|
46
|
+
TextRect,
|
|
47
|
+
TextRectSearchResult,
|
|
48
|
+
TokenStats,
|
|
49
|
+
Viewport,
|
|
50
|
+
ViewportRect,
|
|
51
|
+
WaitResult,
|
|
52
|
+
)
|
|
53
|
+
from .overlay import clear_overlay, show_overlay
|
|
54
|
+
from .query import find, query
|
|
55
|
+
from .read import read
|
|
56
|
+
from .recorder import Recorder, Trace, TraceStep, record
|
|
57
|
+
from .screenshot import screenshot
|
|
58
|
+
from .snapshot import snapshot
|
|
59
|
+
from .text_search import find_text_rect
|
|
60
|
+
from .tracer_factory import SENTIENCE_API_URL, create_tracer
|
|
61
|
+
from .tracing import JsonlTraceSink, TraceEvent, Tracer, TraceSink
|
|
62
|
+
|
|
63
|
+
# Utilities (v0.12.0+)
|
|
64
|
+
from .utils import (
|
|
65
|
+
canonical_snapshot_loose,
|
|
66
|
+
canonical_snapshot_strict,
|
|
67
|
+
compute_snapshot_digests,
|
|
68
|
+
save_storage_state,
|
|
69
|
+
sha256_digest,
|
|
70
|
+
)
|
|
71
|
+
from .wait import wait_for
|
|
72
|
+
|
|
73
|
+
__version__ = "0.90.9"
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
# Core SDK
|
|
77
|
+
"SentienceBrowser",
|
|
78
|
+
"Snapshot",
|
|
79
|
+
"Element",
|
|
80
|
+
"BBox",
|
|
81
|
+
"Viewport",
|
|
82
|
+
"ActionResult",
|
|
83
|
+
"WaitResult",
|
|
84
|
+
"snapshot",
|
|
85
|
+
"query",
|
|
86
|
+
"find",
|
|
87
|
+
"click",
|
|
88
|
+
"type_text",
|
|
89
|
+
"press",
|
|
90
|
+
"click_rect",
|
|
91
|
+
"wait_for",
|
|
92
|
+
"expect",
|
|
93
|
+
"Inspector",
|
|
94
|
+
"inspect",
|
|
95
|
+
"Recorder",
|
|
96
|
+
"Trace",
|
|
97
|
+
"TraceStep",
|
|
98
|
+
"record",
|
|
99
|
+
"ScriptGenerator",
|
|
100
|
+
"generate",
|
|
101
|
+
"read",
|
|
102
|
+
"screenshot",
|
|
103
|
+
"show_overlay",
|
|
104
|
+
"clear_overlay",
|
|
105
|
+
# Text Search
|
|
106
|
+
"find_text_rect",
|
|
107
|
+
"TextRectSearchResult",
|
|
108
|
+
"TextMatch",
|
|
109
|
+
"TextRect",
|
|
110
|
+
"ViewportRect",
|
|
111
|
+
"TextContext",
|
|
112
|
+
# Agent Layer (Phase 1 & 2)
|
|
113
|
+
"BaseAgent",
|
|
114
|
+
"LLMProvider",
|
|
115
|
+
"LLMResponse",
|
|
116
|
+
"OpenAIProvider",
|
|
117
|
+
"AnthropicProvider",
|
|
118
|
+
"LocalLLMProvider",
|
|
119
|
+
"SentienceAgent",
|
|
120
|
+
"ConversationalAgent",
|
|
121
|
+
# Agent Layer Models
|
|
122
|
+
"AgentActionResult",
|
|
123
|
+
"TokenStats",
|
|
124
|
+
"ActionHistory",
|
|
125
|
+
"ActionTokenUsage",
|
|
126
|
+
"SnapshotOptions",
|
|
127
|
+
"SnapshotFilter",
|
|
128
|
+
"ScreenshotConfig",
|
|
129
|
+
# Storage State Models (Auth Injection)
|
|
130
|
+
"StorageState",
|
|
131
|
+
"Cookie",
|
|
132
|
+
"LocalStorageItem",
|
|
133
|
+
"OriginStorage",
|
|
134
|
+
# Tracing (v0.12.0+)
|
|
135
|
+
"Tracer",
|
|
136
|
+
"TraceSink",
|
|
137
|
+
"JsonlTraceSink",
|
|
138
|
+
"CloudTraceSink",
|
|
139
|
+
"SentienceLogger",
|
|
140
|
+
"TraceEvent",
|
|
141
|
+
"create_tracer",
|
|
142
|
+
"SENTIENCE_API_URL",
|
|
143
|
+
# Utilities (v0.12.0+)
|
|
144
|
+
"canonical_snapshot_strict",
|
|
145
|
+
"canonical_snapshot_loose",
|
|
146
|
+
"compute_snapshot_digests",
|
|
147
|
+
"sha256_digest",
|
|
148
|
+
"save_storage_state",
|
|
149
|
+
# Formatting (v0.12.0+)
|
|
150
|
+
"format_snapshot_for_llm",
|
|
151
|
+
# Agent Config (v0.12.0+)
|
|
152
|
+
"AgentConfig",
|
|
153
|
+
]
|
sentience/actions.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actions v1 - click, type, press
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .browser import SentienceBrowser
|
|
8
|
+
from .models import ActionResult, BBox, Snapshot
|
|
9
|
+
from .snapshot import snapshot
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def click( # noqa: C901
|
|
13
|
+
browser: SentienceBrowser,
|
|
14
|
+
element_id: int,
|
|
15
|
+
use_mouse: bool = True,
|
|
16
|
+
take_snapshot: bool = False,
|
|
17
|
+
) -> ActionResult:
|
|
18
|
+
"""
|
|
19
|
+
Click an element by ID using hybrid approach (mouse simulation by default)
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
browser: SentienceBrowser instance
|
|
23
|
+
element_id: Element ID from snapshot
|
|
24
|
+
use_mouse: If True, use Playwright's mouse.click() at element center (hybrid approach).
|
|
25
|
+
If False, use JS-based window.sentience.click() (legacy).
|
|
26
|
+
take_snapshot: Whether to take snapshot after action
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ActionResult
|
|
30
|
+
"""
|
|
31
|
+
if not browser.page:
|
|
32
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
33
|
+
|
|
34
|
+
start_time = time.time()
|
|
35
|
+
url_before = browser.page.url
|
|
36
|
+
|
|
37
|
+
if use_mouse:
|
|
38
|
+
# Hybrid approach: Get element bbox from snapshot, calculate center, use mouse.click()
|
|
39
|
+
try:
|
|
40
|
+
snap = snapshot(browser)
|
|
41
|
+
element = None
|
|
42
|
+
for el in snap.elements:
|
|
43
|
+
if el.id == element_id:
|
|
44
|
+
element = el
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
if element:
|
|
48
|
+
# Calculate center of element bbox
|
|
49
|
+
center_x = element.bbox.x + element.bbox.width / 2
|
|
50
|
+
center_y = element.bbox.y + element.bbox.height / 2
|
|
51
|
+
# Use Playwright's native mouse click for realistic simulation
|
|
52
|
+
try:
|
|
53
|
+
browser.page.mouse.click(center_x, center_y)
|
|
54
|
+
success = True
|
|
55
|
+
except Exception:
|
|
56
|
+
# If navigation happens, mouse.click might fail, but that's OK
|
|
57
|
+
# The click still happened, just check URL change
|
|
58
|
+
success = True
|
|
59
|
+
else:
|
|
60
|
+
# Fallback to JS click if element not found in snapshot
|
|
61
|
+
try:
|
|
62
|
+
success = browser.page.evaluate(
|
|
63
|
+
"""
|
|
64
|
+
(id) => {
|
|
65
|
+
return window.sentience.click(id);
|
|
66
|
+
}
|
|
67
|
+
""",
|
|
68
|
+
element_id,
|
|
69
|
+
)
|
|
70
|
+
except Exception:
|
|
71
|
+
# Navigation might have destroyed context, assume success if URL changed
|
|
72
|
+
success = True
|
|
73
|
+
except Exception:
|
|
74
|
+
# Fallback to JS click on error
|
|
75
|
+
try:
|
|
76
|
+
success = browser.page.evaluate(
|
|
77
|
+
"""
|
|
78
|
+
(id) => {
|
|
79
|
+
return window.sentience.click(id);
|
|
80
|
+
}
|
|
81
|
+
""",
|
|
82
|
+
element_id,
|
|
83
|
+
)
|
|
84
|
+
except Exception:
|
|
85
|
+
# Navigation might have destroyed context, assume success if URL changed
|
|
86
|
+
success = True
|
|
87
|
+
else:
|
|
88
|
+
# Legacy JS-based click
|
|
89
|
+
success = browser.page.evaluate(
|
|
90
|
+
"""
|
|
91
|
+
(id) => {
|
|
92
|
+
return window.sentience.click(id);
|
|
93
|
+
}
|
|
94
|
+
""",
|
|
95
|
+
element_id,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Wait a bit for navigation/DOM updates
|
|
99
|
+
try:
|
|
100
|
+
browser.page.wait_for_timeout(500)
|
|
101
|
+
except Exception:
|
|
102
|
+
# Navigation might have happened, context destroyed
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
106
|
+
|
|
107
|
+
# Check if URL changed (handle navigation gracefully)
|
|
108
|
+
try:
|
|
109
|
+
url_after = browser.page.url
|
|
110
|
+
url_changed = url_before != url_after
|
|
111
|
+
except Exception:
|
|
112
|
+
# Context destroyed due to navigation - assume URL changed
|
|
113
|
+
url_after = url_before
|
|
114
|
+
url_changed = True
|
|
115
|
+
|
|
116
|
+
# Determine outcome
|
|
117
|
+
outcome: str | None = None
|
|
118
|
+
if url_changed:
|
|
119
|
+
outcome = "navigated"
|
|
120
|
+
elif success:
|
|
121
|
+
outcome = "dom_updated"
|
|
122
|
+
else:
|
|
123
|
+
outcome = "error"
|
|
124
|
+
|
|
125
|
+
# Optional snapshot after
|
|
126
|
+
snapshot_after: Snapshot | None = None
|
|
127
|
+
if take_snapshot:
|
|
128
|
+
try:
|
|
129
|
+
snapshot_after = snapshot(browser)
|
|
130
|
+
except Exception:
|
|
131
|
+
# Navigation might have destroyed context
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
return ActionResult(
|
|
135
|
+
success=success,
|
|
136
|
+
duration_ms=duration_ms,
|
|
137
|
+
outcome=outcome,
|
|
138
|
+
url_changed=url_changed,
|
|
139
|
+
snapshot_after=snapshot_after,
|
|
140
|
+
error=(
|
|
141
|
+
None
|
|
142
|
+
if success
|
|
143
|
+
else {
|
|
144
|
+
"code": "click_failed",
|
|
145
|
+
"reason": "Element not found or not clickable",
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def type_text(
|
|
152
|
+
browser: SentienceBrowser, element_id: int, text: str, take_snapshot: bool = False
|
|
153
|
+
) -> ActionResult:
|
|
154
|
+
"""
|
|
155
|
+
Type text into an element (focus then input)
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
browser: SentienceBrowser instance
|
|
159
|
+
element_id: Element ID from snapshot
|
|
160
|
+
text: Text to type
|
|
161
|
+
take_snapshot: Whether to take snapshot after action
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
ActionResult
|
|
165
|
+
"""
|
|
166
|
+
if not browser.page:
|
|
167
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
168
|
+
|
|
169
|
+
start_time = time.time()
|
|
170
|
+
url_before = browser.page.url
|
|
171
|
+
|
|
172
|
+
# Focus element first using extension registry
|
|
173
|
+
focused = browser.page.evaluate(
|
|
174
|
+
"""
|
|
175
|
+
(id) => {
|
|
176
|
+
const el = window.sentience_registry[id];
|
|
177
|
+
if (el) {
|
|
178
|
+
el.focus();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
""",
|
|
184
|
+
element_id,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not focused:
|
|
188
|
+
return ActionResult(
|
|
189
|
+
success=False,
|
|
190
|
+
duration_ms=int((time.time() - start_time) * 1000),
|
|
191
|
+
outcome="error",
|
|
192
|
+
error={"code": "focus_failed", "reason": "Element not found"},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Type using Playwright keyboard
|
|
196
|
+
browser.page.keyboard.type(text)
|
|
197
|
+
|
|
198
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
199
|
+
url_after = browser.page.url
|
|
200
|
+
url_changed = url_before != url_after
|
|
201
|
+
|
|
202
|
+
outcome = "navigated" if url_changed else "dom_updated"
|
|
203
|
+
|
|
204
|
+
snapshot_after: Snapshot | None = None
|
|
205
|
+
if take_snapshot:
|
|
206
|
+
snapshot_after = snapshot(browser)
|
|
207
|
+
|
|
208
|
+
return ActionResult(
|
|
209
|
+
success=True,
|
|
210
|
+
duration_ms=duration_ms,
|
|
211
|
+
outcome=outcome,
|
|
212
|
+
url_changed=url_changed,
|
|
213
|
+
snapshot_after=snapshot_after,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def press(browser: SentienceBrowser, key: str, take_snapshot: bool = False) -> ActionResult:
|
|
218
|
+
"""
|
|
219
|
+
Press a keyboard key
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
browser: SentienceBrowser instance
|
|
223
|
+
key: Key to press (e.g., "Enter", "Escape", "Tab")
|
|
224
|
+
take_snapshot: Whether to take snapshot after action
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
ActionResult
|
|
228
|
+
"""
|
|
229
|
+
if not browser.page:
|
|
230
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
231
|
+
|
|
232
|
+
start_time = time.time()
|
|
233
|
+
url_before = browser.page.url
|
|
234
|
+
|
|
235
|
+
# Press key using Playwright
|
|
236
|
+
browser.page.keyboard.press(key)
|
|
237
|
+
|
|
238
|
+
# Wait a bit for navigation/DOM updates
|
|
239
|
+
browser.page.wait_for_timeout(500)
|
|
240
|
+
|
|
241
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
242
|
+
url_after = browser.page.url
|
|
243
|
+
url_changed = url_before != url_after
|
|
244
|
+
|
|
245
|
+
outcome = "navigated" if url_changed else "dom_updated"
|
|
246
|
+
|
|
247
|
+
snapshot_after: Snapshot | None = None
|
|
248
|
+
if take_snapshot:
|
|
249
|
+
snapshot_after = snapshot(browser)
|
|
250
|
+
|
|
251
|
+
return ActionResult(
|
|
252
|
+
success=True,
|
|
253
|
+
duration_ms=duration_ms,
|
|
254
|
+
outcome=outcome,
|
|
255
|
+
url_changed=url_changed,
|
|
256
|
+
snapshot_after=snapshot_after,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _highlight_rect(
|
|
261
|
+
browser: SentienceBrowser, rect: dict[str, float], duration_sec: float = 2.0
|
|
262
|
+
) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Highlight a rectangle with a red border overlay
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
browser: SentienceBrowser instance
|
|
268
|
+
rect: Dictionary with x, y, width (w), height (h) keys
|
|
269
|
+
duration_sec: How long to show the highlight (default: 2 seconds)
|
|
270
|
+
"""
|
|
271
|
+
if not browser.page:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
# Create a unique ID for this highlight
|
|
275
|
+
highlight_id = f"sentience_highlight_{int(time.time() * 1000)}"
|
|
276
|
+
|
|
277
|
+
# Combine all arguments into a single object for Playwright
|
|
278
|
+
args = {
|
|
279
|
+
"rect": {
|
|
280
|
+
"x": rect["x"],
|
|
281
|
+
"y": rect["y"],
|
|
282
|
+
"w": rect["w"],
|
|
283
|
+
"h": rect["h"],
|
|
284
|
+
},
|
|
285
|
+
"highlightId": highlight_id,
|
|
286
|
+
"durationSec": duration_sec,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Inject CSS and create overlay element
|
|
290
|
+
browser.page.evaluate(
|
|
291
|
+
"""
|
|
292
|
+
(args) => {
|
|
293
|
+
const { rect, highlightId, durationSec } = args;
|
|
294
|
+
// Create overlay div
|
|
295
|
+
const overlay = document.createElement('div');
|
|
296
|
+
overlay.id = highlightId;
|
|
297
|
+
overlay.style.position = 'fixed';
|
|
298
|
+
overlay.style.left = `${rect.x}px`;
|
|
299
|
+
overlay.style.top = `${rect.y}px`;
|
|
300
|
+
overlay.style.width = `${rect.w}px`;
|
|
301
|
+
overlay.style.height = `${rect.h}px`;
|
|
302
|
+
overlay.style.border = '3px solid red';
|
|
303
|
+
overlay.style.borderRadius = '2px';
|
|
304
|
+
overlay.style.boxSizing = 'border-box';
|
|
305
|
+
overlay.style.pointerEvents = 'none';
|
|
306
|
+
overlay.style.zIndex = '999999';
|
|
307
|
+
overlay.style.backgroundColor = 'rgba(255, 0, 0, 0.1)';
|
|
308
|
+
overlay.style.transition = 'opacity 0.3s ease-out';
|
|
309
|
+
|
|
310
|
+
document.body.appendChild(overlay);
|
|
311
|
+
|
|
312
|
+
// Remove after duration
|
|
313
|
+
setTimeout(() => {
|
|
314
|
+
overlay.style.opacity = '0';
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
if (overlay.parentNode) {
|
|
317
|
+
overlay.parentNode.removeChild(overlay);
|
|
318
|
+
}
|
|
319
|
+
}, 300); // Wait for fade-out transition
|
|
320
|
+
}, durationSec * 1000);
|
|
321
|
+
}
|
|
322
|
+
""",
|
|
323
|
+
args,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def click_rect(
|
|
328
|
+
browser: SentienceBrowser,
|
|
329
|
+
rect: dict[str, float],
|
|
330
|
+
highlight: bool = True,
|
|
331
|
+
highlight_duration: float = 2.0,
|
|
332
|
+
take_snapshot: bool = False,
|
|
333
|
+
) -> ActionResult:
|
|
334
|
+
"""
|
|
335
|
+
Click at the center of a rectangle using Playwright's native mouse simulation.
|
|
336
|
+
This uses a hybrid approach: calculates center coordinates and uses mouse.click()
|
|
337
|
+
for realistic event simulation (triggers hover, focus, mousedown, mouseup).
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
browser: SentienceBrowser instance
|
|
341
|
+
rect: Dictionary with x, y, width (w), height (h) keys, or BBox object
|
|
342
|
+
highlight: Whether to show a red border highlight when clicking (default: True)
|
|
343
|
+
highlight_duration: How long to show the highlight in seconds (default: 2.0)
|
|
344
|
+
take_snapshot: Whether to take snapshot after action
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
ActionResult
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
>>> click_rect(browser, {"x": 100, "y": 200, "w": 50, "h": 30})
|
|
351
|
+
>>> # Or using BBox object
|
|
352
|
+
>>> from sentience import BBox
|
|
353
|
+
>>> bbox = BBox(x=100, y=200, width=50, height=30)
|
|
354
|
+
>>> click_rect(browser, {"x": bbox.x, "y": bbox.y, "w": bbox.width, "h": bbox.height})
|
|
355
|
+
"""
|
|
356
|
+
if not browser.page:
|
|
357
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
358
|
+
|
|
359
|
+
# Handle BBox object or dict
|
|
360
|
+
if isinstance(rect, BBox):
|
|
361
|
+
x = rect.x
|
|
362
|
+
y = rect.y
|
|
363
|
+
w = rect.width
|
|
364
|
+
h = rect.height
|
|
365
|
+
else:
|
|
366
|
+
x = rect.get("x", 0)
|
|
367
|
+
y = rect.get("y", 0)
|
|
368
|
+
w = rect.get("w") or rect.get("width", 0)
|
|
369
|
+
h = rect.get("h") or rect.get("height", 0)
|
|
370
|
+
|
|
371
|
+
if w <= 0 or h <= 0:
|
|
372
|
+
return ActionResult(
|
|
373
|
+
success=False,
|
|
374
|
+
duration_ms=0,
|
|
375
|
+
outcome="error",
|
|
376
|
+
error={
|
|
377
|
+
"code": "invalid_rect",
|
|
378
|
+
"reason": "Rectangle width and height must be positive",
|
|
379
|
+
},
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
start_time = time.time()
|
|
383
|
+
url_before = browser.page.url
|
|
384
|
+
|
|
385
|
+
# Calculate center of rectangle
|
|
386
|
+
center_x = x + w / 2
|
|
387
|
+
center_y = y + h / 2
|
|
388
|
+
|
|
389
|
+
# Show highlight before clicking (if enabled)
|
|
390
|
+
if highlight:
|
|
391
|
+
_highlight_rect(browser, {"x": x, "y": y, "w": w, "h": h}, highlight_duration)
|
|
392
|
+
# Small delay to ensure highlight is visible
|
|
393
|
+
browser.page.wait_for_timeout(50)
|
|
394
|
+
|
|
395
|
+
# Use Playwright's native mouse click for realistic simulation
|
|
396
|
+
# This triggers hover, focus, mousedown, mouseup sequences
|
|
397
|
+
try:
|
|
398
|
+
browser.page.mouse.click(center_x, center_y)
|
|
399
|
+
success = True
|
|
400
|
+
except Exception as e:
|
|
401
|
+
success = False
|
|
402
|
+
error_msg = str(e)
|
|
403
|
+
|
|
404
|
+
# Wait a bit for navigation/DOM updates
|
|
405
|
+
browser.page.wait_for_timeout(500)
|
|
406
|
+
|
|
407
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
408
|
+
url_after = browser.page.url
|
|
409
|
+
url_changed = url_before != url_after
|
|
410
|
+
|
|
411
|
+
# Determine outcome
|
|
412
|
+
outcome: str | None = None
|
|
413
|
+
if url_changed:
|
|
414
|
+
outcome = "navigated"
|
|
415
|
+
elif success:
|
|
416
|
+
outcome = "dom_updated"
|
|
417
|
+
else:
|
|
418
|
+
outcome = "error"
|
|
419
|
+
|
|
420
|
+
# Optional snapshot after
|
|
421
|
+
snapshot_after: Snapshot | None = None
|
|
422
|
+
if take_snapshot:
|
|
423
|
+
snapshot_after = snapshot(browser)
|
|
424
|
+
|
|
425
|
+
return ActionResult(
|
|
426
|
+
success=success,
|
|
427
|
+
duration_ms=duration_ms,
|
|
428
|
+
outcome=outcome,
|
|
429
|
+
url_changed=url_changed,
|
|
430
|
+
snapshot_after=snapshot_after,
|
|
431
|
+
error=(
|
|
432
|
+
None
|
|
433
|
+
if success
|
|
434
|
+
else {
|
|
435
|
+
"code": "click_failed",
|
|
436
|
+
"reason": error_msg if not success else "Click failed",
|
|
437
|
+
}
|
|
438
|
+
),
|
|
439
|
+
)
|