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.
- sentience/__init__.py +153 -0
- sentience/_extension_loader.py +40 -0
- sentience/actions.py +837 -0
- sentience/agent.py +1246 -0
- sentience/agent_config.py +43 -0
- sentience/async_api.py +101 -0
- sentience/base_agent.py +194 -0
- sentience/browser.py +1037 -0
- sentience/cli.py +130 -0
- sentience/cloud_tracing.py +382 -0
- sentience/conversational_agent.py +509 -0
- sentience/expect.py +188 -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 +365 -0
- sentience/llm_provider.py +637 -0
- sentience/models.py +412 -0
- sentience/overlay.py +222 -0
- sentience/query.py +303 -0
- sentience/read.py +185 -0
- sentience/recorder.py +589 -0
- sentience/schemas/trace_v1.json +216 -0
- sentience/screenshot.py +100 -0
- sentience/snapshot.py +516 -0
- sentience/text_search.py +290 -0
- sentience/trace_indexing/__init__.py +27 -0
- sentience/trace_indexing/index_schema.py +111 -0
- sentience/trace_indexing/indexer.py +357 -0
- sentience/tracer_factory.py +211 -0
- sentience/tracing.py +285 -0
- sentience/utils.py +296 -0
- sentience/wait.py +137 -0
- sentienceapi-0.90.17.dist-info/METADATA +917 -0
- sentienceapi-0.90.17.dist-info/RECORD +50 -0
- sentienceapi-0.90.17.dist-info/WHEEL +5 -0
- sentienceapi-0.90.17.dist-info/entry_points.txt +2 -0
- sentienceapi-0.90.17.dist-info/licenses/LICENSE +24 -0
- sentienceapi-0.90.17.dist-info/licenses/LICENSE-APACHE +201 -0
- sentienceapi-0.90.17.dist-info/licenses/LICENSE-MIT +21 -0
- sentienceapi-0.90.17.dist-info/top_level.txt +1 -0
sentience/actions.py
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actions v1 - click, type, press
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .browser import AsyncSentienceBrowser, SentienceBrowser
|
|
8
|
+
from .models import ActionResult, BBox, Snapshot
|
|
9
|
+
from .snapshot import snapshot, snapshot_async
|
|
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
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ========== Async Action Functions ==========
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
async def click_async(
|
|
446
|
+
browser: AsyncSentienceBrowser,
|
|
447
|
+
element_id: int,
|
|
448
|
+
use_mouse: bool = True,
|
|
449
|
+
take_snapshot: bool = False,
|
|
450
|
+
) -> ActionResult:
|
|
451
|
+
"""
|
|
452
|
+
Click an element by ID using hybrid approach (async)
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
browser: AsyncSentienceBrowser instance
|
|
456
|
+
element_id: Element ID from snapshot
|
|
457
|
+
use_mouse: If True, use Playwright's mouse.click() at element center
|
|
458
|
+
take_snapshot: Whether to take snapshot after action
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
ActionResult
|
|
462
|
+
"""
|
|
463
|
+
if not browser.page:
|
|
464
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
465
|
+
|
|
466
|
+
start_time = time.time()
|
|
467
|
+
url_before = browser.page.url
|
|
468
|
+
|
|
469
|
+
if use_mouse:
|
|
470
|
+
try:
|
|
471
|
+
snap = await snapshot_async(browser)
|
|
472
|
+
element = None
|
|
473
|
+
for el in snap.elements:
|
|
474
|
+
if el.id == element_id:
|
|
475
|
+
element = el
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
if element:
|
|
479
|
+
center_x = element.bbox.x + element.bbox.width / 2
|
|
480
|
+
center_y = element.bbox.y + element.bbox.height / 2
|
|
481
|
+
try:
|
|
482
|
+
await browser.page.mouse.click(center_x, center_y)
|
|
483
|
+
success = True
|
|
484
|
+
except Exception:
|
|
485
|
+
success = True
|
|
486
|
+
else:
|
|
487
|
+
try:
|
|
488
|
+
success = await browser.page.evaluate(
|
|
489
|
+
"""
|
|
490
|
+
(id) => {
|
|
491
|
+
return window.sentience.click(id);
|
|
492
|
+
}
|
|
493
|
+
""",
|
|
494
|
+
element_id,
|
|
495
|
+
)
|
|
496
|
+
except Exception:
|
|
497
|
+
success = True
|
|
498
|
+
except Exception:
|
|
499
|
+
try:
|
|
500
|
+
success = await browser.page.evaluate(
|
|
501
|
+
"""
|
|
502
|
+
(id) => {
|
|
503
|
+
return window.sentience.click(id);
|
|
504
|
+
}
|
|
505
|
+
""",
|
|
506
|
+
element_id,
|
|
507
|
+
)
|
|
508
|
+
except Exception:
|
|
509
|
+
success = True
|
|
510
|
+
else:
|
|
511
|
+
success = await browser.page.evaluate(
|
|
512
|
+
"""
|
|
513
|
+
(id) => {
|
|
514
|
+
return window.sentience.click(id);
|
|
515
|
+
}
|
|
516
|
+
""",
|
|
517
|
+
element_id,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Wait a bit for navigation/DOM updates
|
|
521
|
+
try:
|
|
522
|
+
await browser.page.wait_for_timeout(500)
|
|
523
|
+
except Exception:
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
527
|
+
|
|
528
|
+
# Check if URL changed
|
|
529
|
+
try:
|
|
530
|
+
url_after = browser.page.url
|
|
531
|
+
url_changed = url_before != url_after
|
|
532
|
+
except Exception:
|
|
533
|
+
url_after = url_before
|
|
534
|
+
url_changed = True
|
|
535
|
+
|
|
536
|
+
# Determine outcome
|
|
537
|
+
outcome: str | None = None
|
|
538
|
+
if url_changed:
|
|
539
|
+
outcome = "navigated"
|
|
540
|
+
elif success:
|
|
541
|
+
outcome = "dom_updated"
|
|
542
|
+
else:
|
|
543
|
+
outcome = "error"
|
|
544
|
+
|
|
545
|
+
# Optional snapshot after
|
|
546
|
+
snapshot_after: Snapshot | None = None
|
|
547
|
+
if take_snapshot:
|
|
548
|
+
try:
|
|
549
|
+
snapshot_after = await snapshot_async(browser)
|
|
550
|
+
except Exception:
|
|
551
|
+
pass
|
|
552
|
+
|
|
553
|
+
return ActionResult(
|
|
554
|
+
success=success,
|
|
555
|
+
duration_ms=duration_ms,
|
|
556
|
+
outcome=outcome,
|
|
557
|
+
url_changed=url_changed,
|
|
558
|
+
snapshot_after=snapshot_after,
|
|
559
|
+
error=(
|
|
560
|
+
None
|
|
561
|
+
if success
|
|
562
|
+
else {
|
|
563
|
+
"code": "click_failed",
|
|
564
|
+
"reason": "Element not found or not clickable",
|
|
565
|
+
}
|
|
566
|
+
),
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
async def type_text_async(
|
|
571
|
+
browser: AsyncSentienceBrowser, element_id: int, text: str, take_snapshot: bool = False
|
|
572
|
+
) -> ActionResult:
|
|
573
|
+
"""
|
|
574
|
+
Type text into an element (async)
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
browser: AsyncSentienceBrowser instance
|
|
578
|
+
element_id: Element ID from snapshot
|
|
579
|
+
text: Text to type
|
|
580
|
+
take_snapshot: Whether to take snapshot after action
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
ActionResult
|
|
584
|
+
"""
|
|
585
|
+
if not browser.page:
|
|
586
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
587
|
+
|
|
588
|
+
start_time = time.time()
|
|
589
|
+
url_before = browser.page.url
|
|
590
|
+
|
|
591
|
+
# Focus element first
|
|
592
|
+
focused = await browser.page.evaluate(
|
|
593
|
+
"""
|
|
594
|
+
(id) => {
|
|
595
|
+
const el = window.sentience_registry[id];
|
|
596
|
+
if (el) {
|
|
597
|
+
el.focus();
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
""",
|
|
603
|
+
element_id,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
if not focused:
|
|
607
|
+
return ActionResult(
|
|
608
|
+
success=False,
|
|
609
|
+
duration_ms=int((time.time() - start_time) * 1000),
|
|
610
|
+
outcome="error",
|
|
611
|
+
error={"code": "focus_failed", "reason": "Element not found"},
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Type using Playwright keyboard
|
|
615
|
+
await browser.page.keyboard.type(text)
|
|
616
|
+
|
|
617
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
618
|
+
url_after = browser.page.url
|
|
619
|
+
url_changed = url_before != url_after
|
|
620
|
+
|
|
621
|
+
outcome = "navigated" if url_changed else "dom_updated"
|
|
622
|
+
|
|
623
|
+
snapshot_after: Snapshot | None = None
|
|
624
|
+
if take_snapshot:
|
|
625
|
+
snapshot_after = await snapshot_async(browser)
|
|
626
|
+
|
|
627
|
+
return ActionResult(
|
|
628
|
+
success=True,
|
|
629
|
+
duration_ms=duration_ms,
|
|
630
|
+
outcome=outcome,
|
|
631
|
+
url_changed=url_changed,
|
|
632
|
+
snapshot_after=snapshot_after,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
async def press_async(
|
|
637
|
+
browser: AsyncSentienceBrowser, key: str, take_snapshot: bool = False
|
|
638
|
+
) -> ActionResult:
|
|
639
|
+
"""
|
|
640
|
+
Press a keyboard key (async)
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
browser: AsyncSentienceBrowser instance
|
|
644
|
+
key: Key to press (e.g., "Enter", "Escape", "Tab")
|
|
645
|
+
take_snapshot: Whether to take snapshot after action
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
ActionResult
|
|
649
|
+
"""
|
|
650
|
+
if not browser.page:
|
|
651
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
652
|
+
|
|
653
|
+
start_time = time.time()
|
|
654
|
+
url_before = browser.page.url
|
|
655
|
+
|
|
656
|
+
# Press key using Playwright
|
|
657
|
+
await browser.page.keyboard.press(key)
|
|
658
|
+
|
|
659
|
+
# Wait a bit for navigation/DOM updates
|
|
660
|
+
await browser.page.wait_for_timeout(500)
|
|
661
|
+
|
|
662
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
663
|
+
url_after = browser.page.url
|
|
664
|
+
url_changed = url_before != url_after
|
|
665
|
+
|
|
666
|
+
outcome = "navigated" if url_changed else "dom_updated"
|
|
667
|
+
|
|
668
|
+
snapshot_after: Snapshot | None = None
|
|
669
|
+
if take_snapshot:
|
|
670
|
+
snapshot_after = await snapshot_async(browser)
|
|
671
|
+
|
|
672
|
+
return ActionResult(
|
|
673
|
+
success=True,
|
|
674
|
+
duration_ms=duration_ms,
|
|
675
|
+
outcome=outcome,
|
|
676
|
+
url_changed=url_changed,
|
|
677
|
+
snapshot_after=snapshot_after,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
async def _highlight_rect_async(
|
|
682
|
+
browser: AsyncSentienceBrowser, rect: dict[str, float], duration_sec: float = 2.0
|
|
683
|
+
) -> None:
|
|
684
|
+
"""Highlight a rectangle with a red border overlay (async)"""
|
|
685
|
+
if not browser.page:
|
|
686
|
+
return
|
|
687
|
+
|
|
688
|
+
highlight_id = f"sentience_highlight_{int(time.time() * 1000)}"
|
|
689
|
+
|
|
690
|
+
args = {
|
|
691
|
+
"rect": {
|
|
692
|
+
"x": rect["x"],
|
|
693
|
+
"y": rect["y"],
|
|
694
|
+
"w": rect["w"],
|
|
695
|
+
"h": rect["h"],
|
|
696
|
+
},
|
|
697
|
+
"highlightId": highlight_id,
|
|
698
|
+
"durationSec": duration_sec,
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
await browser.page.evaluate(
|
|
702
|
+
"""
|
|
703
|
+
(args) => {
|
|
704
|
+
const { rect, highlightId, durationSec } = args;
|
|
705
|
+
const overlay = document.createElement('div');
|
|
706
|
+
overlay.id = highlightId;
|
|
707
|
+
overlay.style.position = 'fixed';
|
|
708
|
+
overlay.style.left = `${rect.x}px`;
|
|
709
|
+
overlay.style.top = `${rect.y}px`;
|
|
710
|
+
overlay.style.width = `${rect.w}px`;
|
|
711
|
+
overlay.style.height = `${rect.h}px`;
|
|
712
|
+
overlay.style.border = '3px solid red';
|
|
713
|
+
overlay.style.borderRadius = '2px';
|
|
714
|
+
overlay.style.boxSizing = 'border-box';
|
|
715
|
+
overlay.style.pointerEvents = 'none';
|
|
716
|
+
overlay.style.zIndex = '999999';
|
|
717
|
+
overlay.style.backgroundColor = 'rgba(255, 0, 0, 0.1)';
|
|
718
|
+
overlay.style.transition = 'opacity 0.3s ease-out';
|
|
719
|
+
|
|
720
|
+
document.body.appendChild(overlay);
|
|
721
|
+
|
|
722
|
+
setTimeout(() => {
|
|
723
|
+
overlay.style.opacity = '0';
|
|
724
|
+
setTimeout(() => {
|
|
725
|
+
if (overlay.parentNode) {
|
|
726
|
+
overlay.parentNode.removeChild(overlay);
|
|
727
|
+
}
|
|
728
|
+
}, 300);
|
|
729
|
+
}, durationSec * 1000);
|
|
730
|
+
}
|
|
731
|
+
""",
|
|
732
|
+
args,
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
async def click_rect_async(
|
|
737
|
+
browser: AsyncSentienceBrowser,
|
|
738
|
+
rect: dict[str, float] | BBox,
|
|
739
|
+
highlight: bool = True,
|
|
740
|
+
highlight_duration: float = 2.0,
|
|
741
|
+
take_snapshot: bool = False,
|
|
742
|
+
) -> ActionResult:
|
|
743
|
+
"""
|
|
744
|
+
Click at the center of a rectangle (async)
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
browser: AsyncSentienceBrowser instance
|
|
748
|
+
rect: Dictionary with x, y, width (w), height (h) keys, or BBox object
|
|
749
|
+
highlight: Whether to show a red border highlight when clicking
|
|
750
|
+
highlight_duration: How long to show the highlight in seconds
|
|
751
|
+
take_snapshot: Whether to take snapshot after action
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
ActionResult
|
|
755
|
+
"""
|
|
756
|
+
if not browser.page:
|
|
757
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
758
|
+
|
|
759
|
+
# Handle BBox object or dict
|
|
760
|
+
if isinstance(rect, BBox):
|
|
761
|
+
x = rect.x
|
|
762
|
+
y = rect.y
|
|
763
|
+
w = rect.width
|
|
764
|
+
h = rect.height
|
|
765
|
+
else:
|
|
766
|
+
x = rect.get("x", 0)
|
|
767
|
+
y = rect.get("y", 0)
|
|
768
|
+
w = rect.get("w") or rect.get("width", 0)
|
|
769
|
+
h = rect.get("h") or rect.get("height", 0)
|
|
770
|
+
|
|
771
|
+
if w <= 0 or h <= 0:
|
|
772
|
+
return ActionResult(
|
|
773
|
+
success=False,
|
|
774
|
+
duration_ms=0,
|
|
775
|
+
outcome="error",
|
|
776
|
+
error={
|
|
777
|
+
"code": "invalid_rect",
|
|
778
|
+
"reason": "Rectangle width and height must be positive",
|
|
779
|
+
},
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
start_time = time.time()
|
|
783
|
+
url_before = browser.page.url
|
|
784
|
+
|
|
785
|
+
# Calculate center of rectangle
|
|
786
|
+
center_x = x + w / 2
|
|
787
|
+
center_y = y + h / 2
|
|
788
|
+
|
|
789
|
+
# Show highlight before clicking
|
|
790
|
+
if highlight:
|
|
791
|
+
await _highlight_rect_async(browser, {"x": x, "y": y, "w": w, "h": h}, highlight_duration)
|
|
792
|
+
await browser.page.wait_for_timeout(50)
|
|
793
|
+
|
|
794
|
+
# Use Playwright's native mouse click
|
|
795
|
+
try:
|
|
796
|
+
await browser.page.mouse.click(center_x, center_y)
|
|
797
|
+
success = True
|
|
798
|
+
except Exception as e:
|
|
799
|
+
success = False
|
|
800
|
+
error_msg = str(e)
|
|
801
|
+
|
|
802
|
+
# Wait a bit for navigation/DOM updates
|
|
803
|
+
await browser.page.wait_for_timeout(500)
|
|
804
|
+
|
|
805
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
806
|
+
url_after = browser.page.url
|
|
807
|
+
url_changed = url_before != url_after
|
|
808
|
+
|
|
809
|
+
# Determine outcome
|
|
810
|
+
outcome: str | None = None
|
|
811
|
+
if url_changed:
|
|
812
|
+
outcome = "navigated"
|
|
813
|
+
elif success:
|
|
814
|
+
outcome = "dom_updated"
|
|
815
|
+
else:
|
|
816
|
+
outcome = "error"
|
|
817
|
+
|
|
818
|
+
# Optional snapshot after
|
|
819
|
+
snapshot_after: Snapshot | None = None
|
|
820
|
+
if take_snapshot:
|
|
821
|
+
snapshot_after = await snapshot_async(browser)
|
|
822
|
+
|
|
823
|
+
return ActionResult(
|
|
824
|
+
success=success,
|
|
825
|
+
duration_ms=duration_ms,
|
|
826
|
+
outcome=outcome,
|
|
827
|
+
url_changed=url_changed,
|
|
828
|
+
snapshot_after=snapshot_after,
|
|
829
|
+
error=(
|
|
830
|
+
None
|
|
831
|
+
if success
|
|
832
|
+
else {
|
|
833
|
+
"code": "click_failed",
|
|
834
|
+
"reason": error_msg if not success else "Click failed",
|
|
835
|
+
}
|
|
836
|
+
),
|
|
837
|
+
)
|