sentienceapi 0.92.2__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 +107 -2
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +2 -0
- sentience/actions.py +354 -9
- sentience/agent.py +4 -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 +8 -1
- 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/browser.py +230 -74
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +65 -24
- sentience/constants.py +6 -0
- sentience/cursor_policy.py +142 -0
- sentience/extension/content.js +35 -0
- sentience/extension/injected_api.js +310 -15
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +192 -144
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +29 -29
- sentience/failure_artifacts.py +241 -0
- 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_provider.py +695 -18
- sentience/models.py +536 -3
- sentience/ordinal.py +280 -0
- sentience/query.py +66 -4
- sentience/schemas/trace_v1.json +27 -1
- sentience/snapshot.py +384 -93
- sentience/snapshot_diff.py +39 -54
- sentience/text_search.py +1 -0
- sentience/trace_event_builder.py +20 -1
- sentience/trace_indexing/indexer.py +3 -49
- sentience/tracer_factory.py +1 -3
- sentience/verification.py +618 -0
- sentience/visual_agent.py +3 -1
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +198 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/utils.py +0 -296
- sentienceapi-0.92.2.dist-info/RECORD +0 -65
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
sentience/actions.py
CHANGED
|
@@ -4,10 +4,12 @@ from typing import Optional
|
|
|
4
4
|
Actions v1 - click, type, press
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import time
|
|
8
9
|
|
|
9
10
|
from .browser import AsyncSentienceBrowser, SentienceBrowser
|
|
10
11
|
from .browser_evaluator import BrowserEvaluator
|
|
12
|
+
from .cursor_policy import CursorPolicy, build_human_cursor_path
|
|
11
13
|
from .models import ActionResult, BBox, Snapshot
|
|
12
14
|
from .sentience_methods import SentienceMethod
|
|
13
15
|
from .snapshot import snapshot, snapshot_async
|
|
@@ -18,6 +20,7 @@ def click( # noqa: C901
|
|
|
18
20
|
element_id: int,
|
|
19
21
|
use_mouse: bool = True,
|
|
20
22
|
take_snapshot: bool = False,
|
|
23
|
+
cursor_policy: CursorPolicy | None = None,
|
|
21
24
|
) -> ActionResult:
|
|
22
25
|
"""
|
|
23
26
|
Click an element by ID using hybrid approach (mouse simulation by default)
|
|
@@ -37,6 +40,7 @@ def click( # noqa: C901
|
|
|
37
40
|
|
|
38
41
|
start_time = time.time()
|
|
39
42
|
url_before = browser.page.url
|
|
43
|
+
cursor_meta: dict | None = None
|
|
40
44
|
|
|
41
45
|
if use_mouse:
|
|
42
46
|
# Hybrid approach: Get element bbox from snapshot, calculate center, use mouse.click()
|
|
@@ -52,9 +56,49 @@ def click( # noqa: C901
|
|
|
52
56
|
# Calculate center of element bbox
|
|
53
57
|
center_x = element.bbox.x + element.bbox.width / 2
|
|
54
58
|
center_y = element.bbox.y + element.bbox.height / 2
|
|
55
|
-
#
|
|
59
|
+
# Optional: human-like cursor movement (opt-in)
|
|
56
60
|
try:
|
|
57
|
-
|
|
61
|
+
if cursor_policy is not None and cursor_policy.mode == "human":
|
|
62
|
+
# Best-effort cursor state on browser instance
|
|
63
|
+
pos = getattr(browser, "_sentience_cursor_pos", None)
|
|
64
|
+
if not isinstance(pos, tuple) or len(pos) != 2:
|
|
65
|
+
try:
|
|
66
|
+
vp = browser.page.viewport_size or {}
|
|
67
|
+
pos = (
|
|
68
|
+
float(vp.get("width", 0)) / 2.0,
|
|
69
|
+
float(vp.get("height", 0)) / 2.0,
|
|
70
|
+
)
|
|
71
|
+
except Exception:
|
|
72
|
+
pos = (0.0, 0.0)
|
|
73
|
+
|
|
74
|
+
cursor_meta = build_human_cursor_path(
|
|
75
|
+
start=(float(pos[0]), float(pos[1])),
|
|
76
|
+
target=(float(center_x), float(center_y)),
|
|
77
|
+
policy=cursor_policy,
|
|
78
|
+
)
|
|
79
|
+
pts = cursor_meta.get("path", [])
|
|
80
|
+
steps = int(cursor_meta.get("steps") or max(1, len(pts)))
|
|
81
|
+
duration_ms = int(cursor_meta.get("duration_ms") or 0)
|
|
82
|
+
per_step_s = (
|
|
83
|
+
(duration_ms / max(1, len(pts))) / 1000.0 if duration_ms > 0 else 0.0
|
|
84
|
+
)
|
|
85
|
+
for p in pts:
|
|
86
|
+
browser.page.mouse.move(float(p["x"]), float(p["y"]))
|
|
87
|
+
if per_step_s > 0:
|
|
88
|
+
time.sleep(per_step_s)
|
|
89
|
+
pause_ms = int(cursor_meta.get("pause_before_click_ms") or 0)
|
|
90
|
+
if pause_ms > 0:
|
|
91
|
+
time.sleep(pause_ms / 1000.0)
|
|
92
|
+
browser.page.mouse.click(center_x, center_y)
|
|
93
|
+
setattr(
|
|
94
|
+
browser, "_sentience_cursor_pos", (float(center_x), float(center_y))
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
# Default behavior (no regression)
|
|
98
|
+
browser.page.mouse.click(center_x, center_y)
|
|
99
|
+
setattr(
|
|
100
|
+
browser, "_sentience_cursor_pos", (float(center_x), float(center_y))
|
|
101
|
+
)
|
|
58
102
|
success = True
|
|
59
103
|
except Exception:
|
|
60
104
|
# If navigation happens, mouse.click might fail, but that's OK
|
|
@@ -122,6 +166,7 @@ def click( # noqa: C901
|
|
|
122
166
|
outcome=outcome,
|
|
123
167
|
url_changed=url_changed,
|
|
124
168
|
snapshot_after=snapshot_after,
|
|
169
|
+
cursor=cursor_meta,
|
|
125
170
|
error=(
|
|
126
171
|
None
|
|
127
172
|
if success
|
|
@@ -134,7 +179,11 @@ def click( # noqa: C901
|
|
|
134
179
|
|
|
135
180
|
|
|
136
181
|
def type_text(
|
|
137
|
-
browser: SentienceBrowser,
|
|
182
|
+
browser: SentienceBrowser,
|
|
183
|
+
element_id: int,
|
|
184
|
+
text: str,
|
|
185
|
+
take_snapshot: bool = False,
|
|
186
|
+
delay_ms: float = 0,
|
|
138
187
|
) -> ActionResult:
|
|
139
188
|
"""
|
|
140
189
|
Type text into an element (focus then input)
|
|
@@ -144,9 +193,16 @@ def type_text(
|
|
|
144
193
|
element_id: Element ID from snapshot
|
|
145
194
|
text: Text to type
|
|
146
195
|
take_snapshot: Whether to take snapshot after action
|
|
196
|
+
delay_ms: Delay between keystrokes in milliseconds for human-like typing (default: 0)
|
|
147
197
|
|
|
148
198
|
Returns:
|
|
149
199
|
ActionResult
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
>>> # Type instantly (default behavior)
|
|
203
|
+
>>> type_text(browser, element_id, "Hello World")
|
|
204
|
+
>>> # Type with human-like delay (~10ms between keystrokes)
|
|
205
|
+
>>> type_text(browser, element_id, "Hello World", delay_ms=10)
|
|
150
206
|
"""
|
|
151
207
|
if not browser.page:
|
|
152
208
|
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
@@ -177,8 +233,8 @@ def type_text(
|
|
|
177
233
|
error={"code": "focus_failed", "reason": "Element not found"},
|
|
178
234
|
)
|
|
179
235
|
|
|
180
|
-
# Type using Playwright keyboard
|
|
181
|
-
browser.page.keyboard.type(text)
|
|
236
|
+
# Type using Playwright keyboard with optional delay between keystrokes
|
|
237
|
+
browser.page.keyboard.type(text, delay=delay_ms)
|
|
182
238
|
|
|
183
239
|
duration_ms = int((time.time() - start_time) * 1000)
|
|
184
240
|
url_after = browser.page.url
|
|
@@ -242,6 +298,94 @@ def press(browser: SentienceBrowser, key: str, take_snapshot: bool = False) -> A
|
|
|
242
298
|
)
|
|
243
299
|
|
|
244
300
|
|
|
301
|
+
def scroll_to(
|
|
302
|
+
browser: SentienceBrowser,
|
|
303
|
+
element_id: int,
|
|
304
|
+
behavior: str = "smooth",
|
|
305
|
+
block: str = "center",
|
|
306
|
+
take_snapshot: bool = False,
|
|
307
|
+
) -> ActionResult:
|
|
308
|
+
"""
|
|
309
|
+
Scroll an element into view
|
|
310
|
+
|
|
311
|
+
Scrolls the page so that the specified element is visible in the viewport.
|
|
312
|
+
Uses the element registry to find the element and scrollIntoView() to scroll it.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
browser: SentienceBrowser instance
|
|
316
|
+
element_id: Element ID from snapshot to scroll into view
|
|
317
|
+
behavior: Scroll behavior - 'smooth', 'instant', or 'auto' (default: 'smooth')
|
|
318
|
+
block: Vertical alignment - 'start', 'center', 'end', or 'nearest' (default: 'center')
|
|
319
|
+
take_snapshot: Whether to take snapshot after action
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
ActionResult
|
|
323
|
+
|
|
324
|
+
Example:
|
|
325
|
+
>>> snap = snapshot(browser)
|
|
326
|
+
>>> button = find(snap, 'role=button[name="Submit"]')
|
|
327
|
+
>>> if button:
|
|
328
|
+
>>> # Scroll element into view with smooth animation
|
|
329
|
+
>>> scroll_to(browser, button.id)
|
|
330
|
+
>>> # Scroll instantly to top of viewport
|
|
331
|
+
>>> scroll_to(browser, button.id, behavior='instant', block='start')
|
|
332
|
+
"""
|
|
333
|
+
if not browser.page:
|
|
334
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
335
|
+
|
|
336
|
+
start_time = time.time()
|
|
337
|
+
url_before = browser.page.url
|
|
338
|
+
|
|
339
|
+
# Scroll element into view using the element registry
|
|
340
|
+
scrolled = browser.page.evaluate(
|
|
341
|
+
"""
|
|
342
|
+
(args) => {
|
|
343
|
+
const el = window.sentience_registry[args.id];
|
|
344
|
+
if (el && el.scrollIntoView) {
|
|
345
|
+
el.scrollIntoView({
|
|
346
|
+
behavior: args.behavior,
|
|
347
|
+
block: args.block,
|
|
348
|
+
inline: 'nearest'
|
|
349
|
+
});
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
""",
|
|
355
|
+
{"id": element_id, "behavior": behavior, "block": block},
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if not scrolled:
|
|
359
|
+
return ActionResult(
|
|
360
|
+
success=False,
|
|
361
|
+
duration_ms=int((time.time() - start_time) * 1000),
|
|
362
|
+
outcome="error",
|
|
363
|
+
error={"code": "scroll_failed", "reason": "Element not found or not scrollable"},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Wait a bit for scroll to complete (especially for smooth scrolling)
|
|
367
|
+
wait_time = 500 if behavior == "smooth" else 100
|
|
368
|
+
browser.page.wait_for_timeout(wait_time)
|
|
369
|
+
|
|
370
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
371
|
+
url_after = browser.page.url
|
|
372
|
+
url_changed = url_before != url_after
|
|
373
|
+
|
|
374
|
+
outcome = "navigated" if url_changed else "dom_updated"
|
|
375
|
+
|
|
376
|
+
snapshot_after: Snapshot | None = None
|
|
377
|
+
if take_snapshot:
|
|
378
|
+
snapshot_after = snapshot(browser)
|
|
379
|
+
|
|
380
|
+
return ActionResult(
|
|
381
|
+
success=True,
|
|
382
|
+
duration_ms=duration_ms,
|
|
383
|
+
outcome=outcome,
|
|
384
|
+
url_changed=url_changed,
|
|
385
|
+
snapshot_after=snapshot_after,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
245
389
|
def _highlight_rect(
|
|
246
390
|
browser: SentienceBrowser, rect: dict[str, float], duration_sec: float = 2.0
|
|
247
391
|
) -> None:
|
|
@@ -315,6 +459,7 @@ def click_rect(
|
|
|
315
459
|
highlight: bool = True,
|
|
316
460
|
highlight_duration: float = 2.0,
|
|
317
461
|
take_snapshot: bool = False,
|
|
462
|
+
cursor_policy: CursorPolicy | None = None,
|
|
318
463
|
) -> ActionResult:
|
|
319
464
|
"""
|
|
320
465
|
Click at the center of a rectangle using Playwright's native mouse simulation.
|
|
@@ -370,6 +515,7 @@ def click_rect(
|
|
|
370
515
|
# Calculate center of rectangle
|
|
371
516
|
center_x = x + w / 2
|
|
372
517
|
center_y = y + h / 2
|
|
518
|
+
cursor_meta: dict | None = None
|
|
373
519
|
|
|
374
520
|
# Show highlight before clicking (if enabled)
|
|
375
521
|
if highlight:
|
|
@@ -380,7 +526,35 @@ def click_rect(
|
|
|
380
526
|
# Use Playwright's native mouse click for realistic simulation
|
|
381
527
|
# This triggers hover, focus, mousedown, mouseup sequences
|
|
382
528
|
try:
|
|
529
|
+
if cursor_policy is not None and cursor_policy.mode == "human":
|
|
530
|
+
pos = getattr(browser, "_sentience_cursor_pos", None)
|
|
531
|
+
if not isinstance(pos, tuple) or len(pos) != 2:
|
|
532
|
+
try:
|
|
533
|
+
vp = browser.page.viewport_size or {}
|
|
534
|
+
pos = (float(vp.get("width", 0)) / 2.0, float(vp.get("height", 0)) / 2.0)
|
|
535
|
+
except Exception:
|
|
536
|
+
pos = (0.0, 0.0)
|
|
537
|
+
|
|
538
|
+
cursor_meta = build_human_cursor_path(
|
|
539
|
+
start=(float(pos[0]), float(pos[1])),
|
|
540
|
+
target=(float(center_x), float(center_y)),
|
|
541
|
+
policy=cursor_policy,
|
|
542
|
+
)
|
|
543
|
+
pts = cursor_meta.get("path", [])
|
|
544
|
+
duration_ms_move = int(cursor_meta.get("duration_ms") or 0)
|
|
545
|
+
per_step_s = (
|
|
546
|
+
(duration_ms_move / max(1, len(pts))) / 1000.0 if duration_ms_move > 0 else 0.0
|
|
547
|
+
)
|
|
548
|
+
for p in pts:
|
|
549
|
+
browser.page.mouse.move(float(p["x"]), float(p["y"]))
|
|
550
|
+
if per_step_s > 0:
|
|
551
|
+
time.sleep(per_step_s)
|
|
552
|
+
pause_ms = int(cursor_meta.get("pause_before_click_ms") or 0)
|
|
553
|
+
if pause_ms > 0:
|
|
554
|
+
time.sleep(pause_ms / 1000.0)
|
|
555
|
+
|
|
383
556
|
browser.page.mouse.click(center_x, center_y)
|
|
557
|
+
setattr(browser, "_sentience_cursor_pos", (float(center_x), float(center_y)))
|
|
384
558
|
success = True
|
|
385
559
|
except Exception as e:
|
|
386
560
|
success = False
|
|
@@ -413,6 +587,7 @@ def click_rect(
|
|
|
413
587
|
outcome=outcome,
|
|
414
588
|
url_changed=url_changed,
|
|
415
589
|
snapshot_after=snapshot_after,
|
|
590
|
+
cursor=cursor_meta,
|
|
416
591
|
error=(
|
|
417
592
|
None
|
|
418
593
|
if success
|
|
@@ -432,6 +607,7 @@ async def click_async(
|
|
|
432
607
|
element_id: int,
|
|
433
608
|
use_mouse: bool = True,
|
|
434
609
|
take_snapshot: bool = False,
|
|
610
|
+
cursor_policy: CursorPolicy | None = None,
|
|
435
611
|
) -> ActionResult:
|
|
436
612
|
"""
|
|
437
613
|
Click an element by ID using hybrid approach (async)
|
|
@@ -450,6 +626,7 @@ async def click_async(
|
|
|
450
626
|
|
|
451
627
|
start_time = time.time()
|
|
452
628
|
url_before = browser.page.url
|
|
629
|
+
cursor_meta: dict | None = None
|
|
453
630
|
|
|
454
631
|
if use_mouse:
|
|
455
632
|
try:
|
|
@@ -464,7 +641,44 @@ async def click_async(
|
|
|
464
641
|
center_x = element.bbox.x + element.bbox.width / 2
|
|
465
642
|
center_y = element.bbox.y + element.bbox.height / 2
|
|
466
643
|
try:
|
|
467
|
-
|
|
644
|
+
if cursor_policy is not None and cursor_policy.mode == "human":
|
|
645
|
+
pos = getattr(browser, "_sentience_cursor_pos", None)
|
|
646
|
+
if not isinstance(pos, tuple) or len(pos) != 2:
|
|
647
|
+
try:
|
|
648
|
+
vp = browser.page.viewport_size or {}
|
|
649
|
+
pos = (
|
|
650
|
+
float(vp.get("width", 0)) / 2.0,
|
|
651
|
+
float(vp.get("height", 0)) / 2.0,
|
|
652
|
+
)
|
|
653
|
+
except Exception:
|
|
654
|
+
pos = (0.0, 0.0)
|
|
655
|
+
|
|
656
|
+
cursor_meta = build_human_cursor_path(
|
|
657
|
+
start=(float(pos[0]), float(pos[1])),
|
|
658
|
+
target=(float(center_x), float(center_y)),
|
|
659
|
+
policy=cursor_policy,
|
|
660
|
+
)
|
|
661
|
+
pts = cursor_meta.get("path", [])
|
|
662
|
+
duration_ms = int(cursor_meta.get("duration_ms") or 0)
|
|
663
|
+
per_step_s = (
|
|
664
|
+
(duration_ms / max(1, len(pts))) / 1000.0 if duration_ms > 0 else 0.0
|
|
665
|
+
)
|
|
666
|
+
for p in pts:
|
|
667
|
+
await browser.page.mouse.move(float(p["x"]), float(p["y"]))
|
|
668
|
+
if per_step_s > 0:
|
|
669
|
+
await asyncio.sleep(per_step_s)
|
|
670
|
+
pause_ms = int(cursor_meta.get("pause_before_click_ms") or 0)
|
|
671
|
+
if pause_ms > 0:
|
|
672
|
+
await asyncio.sleep(pause_ms / 1000.0)
|
|
673
|
+
await browser.page.mouse.click(center_x, center_y)
|
|
674
|
+
setattr(
|
|
675
|
+
browser, "_sentience_cursor_pos", (float(center_x), float(center_y))
|
|
676
|
+
)
|
|
677
|
+
else:
|
|
678
|
+
await browser.page.mouse.click(center_x, center_y)
|
|
679
|
+
setattr(
|
|
680
|
+
browser, "_sentience_cursor_pos", (float(center_x), float(center_y))
|
|
681
|
+
)
|
|
468
682
|
success = True
|
|
469
683
|
except Exception:
|
|
470
684
|
success = True
|
|
@@ -541,6 +755,7 @@ async def click_async(
|
|
|
541
755
|
outcome=outcome,
|
|
542
756
|
url_changed=url_changed,
|
|
543
757
|
snapshot_after=snapshot_after,
|
|
758
|
+
cursor=cursor_meta,
|
|
544
759
|
error=(
|
|
545
760
|
None
|
|
546
761
|
if success
|
|
@@ -553,7 +768,11 @@ async def click_async(
|
|
|
553
768
|
|
|
554
769
|
|
|
555
770
|
async def type_text_async(
|
|
556
|
-
browser: AsyncSentienceBrowser,
|
|
771
|
+
browser: AsyncSentienceBrowser,
|
|
772
|
+
element_id: int,
|
|
773
|
+
text: str,
|
|
774
|
+
take_snapshot: bool = False,
|
|
775
|
+
delay_ms: float = 0,
|
|
557
776
|
) -> ActionResult:
|
|
558
777
|
"""
|
|
559
778
|
Type text into an element (async)
|
|
@@ -563,9 +782,16 @@ async def type_text_async(
|
|
|
563
782
|
element_id: Element ID from snapshot
|
|
564
783
|
text: Text to type
|
|
565
784
|
take_snapshot: Whether to take snapshot after action
|
|
785
|
+
delay_ms: Delay between keystrokes in milliseconds for human-like typing (default: 0)
|
|
566
786
|
|
|
567
787
|
Returns:
|
|
568
788
|
ActionResult
|
|
789
|
+
|
|
790
|
+
Example:
|
|
791
|
+
>>> # Type instantly (default behavior)
|
|
792
|
+
>>> await type_text_async(browser, element_id, "Hello World")
|
|
793
|
+
>>> # Type with human-like delay (~10ms between keystrokes)
|
|
794
|
+
>>> await type_text_async(browser, element_id, "Hello World", delay_ms=10)
|
|
569
795
|
"""
|
|
570
796
|
if not browser.page:
|
|
571
797
|
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
@@ -596,8 +822,8 @@ async def type_text_async(
|
|
|
596
822
|
error={"code": "focus_failed", "reason": "Element not found"},
|
|
597
823
|
)
|
|
598
824
|
|
|
599
|
-
# Type using Playwright keyboard
|
|
600
|
-
await browser.page.keyboard.type(text)
|
|
825
|
+
# Type using Playwright keyboard with optional delay between keystrokes
|
|
826
|
+
await browser.page.keyboard.type(text, delay=delay_ms)
|
|
601
827
|
|
|
602
828
|
duration_ms = int((time.time() - start_time) * 1000)
|
|
603
829
|
url_after = browser.page.url
|
|
@@ -663,6 +889,94 @@ async def press_async(
|
|
|
663
889
|
)
|
|
664
890
|
|
|
665
891
|
|
|
892
|
+
async def scroll_to_async(
|
|
893
|
+
browser: AsyncSentienceBrowser,
|
|
894
|
+
element_id: int,
|
|
895
|
+
behavior: str = "smooth",
|
|
896
|
+
block: str = "center",
|
|
897
|
+
take_snapshot: bool = False,
|
|
898
|
+
) -> ActionResult:
|
|
899
|
+
"""
|
|
900
|
+
Scroll an element into view (async)
|
|
901
|
+
|
|
902
|
+
Scrolls the page so that the specified element is visible in the viewport.
|
|
903
|
+
Uses the element registry to find the element and scrollIntoView() to scroll it.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
browser: AsyncSentienceBrowser instance
|
|
907
|
+
element_id: Element ID from snapshot to scroll into view
|
|
908
|
+
behavior: Scroll behavior - 'smooth', 'instant', or 'auto' (default: 'smooth')
|
|
909
|
+
block: Vertical alignment - 'start', 'center', 'end', or 'nearest' (default: 'center')
|
|
910
|
+
take_snapshot: Whether to take snapshot after action
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
ActionResult
|
|
914
|
+
|
|
915
|
+
Example:
|
|
916
|
+
>>> snap = await snapshot_async(browser)
|
|
917
|
+
>>> button = find(snap, 'role=button[name="Submit"]')
|
|
918
|
+
>>> if button:
|
|
919
|
+
>>> # Scroll element into view with smooth animation
|
|
920
|
+
>>> await scroll_to_async(browser, button.id)
|
|
921
|
+
>>> # Scroll instantly to top of viewport
|
|
922
|
+
>>> await scroll_to_async(browser, button.id, behavior='instant', block='start')
|
|
923
|
+
"""
|
|
924
|
+
if not browser.page:
|
|
925
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
926
|
+
|
|
927
|
+
start_time = time.time()
|
|
928
|
+
url_before = browser.page.url
|
|
929
|
+
|
|
930
|
+
# Scroll element into view using the element registry
|
|
931
|
+
scrolled = await browser.page.evaluate(
|
|
932
|
+
"""
|
|
933
|
+
(args) => {
|
|
934
|
+
const el = window.sentience_registry[args.id];
|
|
935
|
+
if (el && el.scrollIntoView) {
|
|
936
|
+
el.scrollIntoView({
|
|
937
|
+
behavior: args.behavior,
|
|
938
|
+
block: args.block,
|
|
939
|
+
inline: 'nearest'
|
|
940
|
+
});
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
""",
|
|
946
|
+
{"id": element_id, "behavior": behavior, "block": block},
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
if not scrolled:
|
|
950
|
+
return ActionResult(
|
|
951
|
+
success=False,
|
|
952
|
+
duration_ms=int((time.time() - start_time) * 1000),
|
|
953
|
+
outcome="error",
|
|
954
|
+
error={"code": "scroll_failed", "reason": "Element not found or not scrollable"},
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
# Wait a bit for scroll to complete (especially for smooth scrolling)
|
|
958
|
+
wait_time = 500 if behavior == "smooth" else 100
|
|
959
|
+
await browser.page.wait_for_timeout(wait_time)
|
|
960
|
+
|
|
961
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
962
|
+
url_after = browser.page.url
|
|
963
|
+
url_changed = url_before != url_after
|
|
964
|
+
|
|
965
|
+
outcome = "navigated" if url_changed else "dom_updated"
|
|
966
|
+
|
|
967
|
+
snapshot_after: Snapshot | None = None
|
|
968
|
+
if take_snapshot:
|
|
969
|
+
snapshot_after = await snapshot_async(browser)
|
|
970
|
+
|
|
971
|
+
return ActionResult(
|
|
972
|
+
success=True,
|
|
973
|
+
duration_ms=duration_ms,
|
|
974
|
+
outcome=outcome,
|
|
975
|
+
url_changed=url_changed,
|
|
976
|
+
snapshot_after=snapshot_after,
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
|
|
666
980
|
async def _highlight_rect_async(
|
|
667
981
|
browser: AsyncSentienceBrowser, rect: dict[str, float], duration_sec: float = 2.0
|
|
668
982
|
) -> None:
|
|
@@ -724,6 +1038,7 @@ async def click_rect_async(
|
|
|
724
1038
|
highlight: bool = True,
|
|
725
1039
|
highlight_duration: float = 2.0,
|
|
726
1040
|
take_snapshot: bool = False,
|
|
1041
|
+
cursor_policy: CursorPolicy | None = None,
|
|
727
1042
|
) -> ActionResult:
|
|
728
1043
|
"""
|
|
729
1044
|
Click at the center of a rectangle (async)
|
|
@@ -770,6 +1085,7 @@ async def click_rect_async(
|
|
|
770
1085
|
# Calculate center of rectangle
|
|
771
1086
|
center_x = x + w / 2
|
|
772
1087
|
center_y = y + h / 2
|
|
1088
|
+
cursor_meta: dict | None = None
|
|
773
1089
|
|
|
774
1090
|
# Show highlight before clicking
|
|
775
1091
|
if highlight:
|
|
@@ -778,7 +1094,35 @@ async def click_rect_async(
|
|
|
778
1094
|
|
|
779
1095
|
# Use Playwright's native mouse click
|
|
780
1096
|
try:
|
|
1097
|
+
if cursor_policy is not None and cursor_policy.mode == "human":
|
|
1098
|
+
pos = getattr(browser, "_sentience_cursor_pos", None)
|
|
1099
|
+
if not isinstance(pos, tuple) or len(pos) != 2:
|
|
1100
|
+
try:
|
|
1101
|
+
vp = browser.page.viewport_size or {}
|
|
1102
|
+
pos = (float(vp.get("width", 0)) / 2.0, float(vp.get("height", 0)) / 2.0)
|
|
1103
|
+
except Exception:
|
|
1104
|
+
pos = (0.0, 0.0)
|
|
1105
|
+
|
|
1106
|
+
cursor_meta = build_human_cursor_path(
|
|
1107
|
+
start=(float(pos[0]), float(pos[1])),
|
|
1108
|
+
target=(float(center_x), float(center_y)),
|
|
1109
|
+
policy=cursor_policy,
|
|
1110
|
+
)
|
|
1111
|
+
pts = cursor_meta.get("path", [])
|
|
1112
|
+
duration_ms_move = int(cursor_meta.get("duration_ms") or 0)
|
|
1113
|
+
per_step_s = (
|
|
1114
|
+
(duration_ms_move / max(1, len(pts))) / 1000.0 if duration_ms_move > 0 else 0.0
|
|
1115
|
+
)
|
|
1116
|
+
for p in pts:
|
|
1117
|
+
await browser.page.mouse.move(float(p["x"]), float(p["y"]))
|
|
1118
|
+
if per_step_s > 0:
|
|
1119
|
+
await asyncio.sleep(per_step_s)
|
|
1120
|
+
pause_ms = int(cursor_meta.get("pause_before_click_ms") or 0)
|
|
1121
|
+
if pause_ms > 0:
|
|
1122
|
+
await asyncio.sleep(pause_ms / 1000.0)
|
|
1123
|
+
|
|
781
1124
|
await browser.page.mouse.click(center_x, center_y)
|
|
1125
|
+
setattr(browser, "_sentience_cursor_pos", (float(center_x), float(center_y)))
|
|
782
1126
|
success = True
|
|
783
1127
|
except Exception as e:
|
|
784
1128
|
success = False
|
|
@@ -811,6 +1155,7 @@ async def click_rect_async(
|
|
|
811
1155
|
outcome=outcome,
|
|
812
1156
|
url_changed=url_changed,
|
|
813
1157
|
snapshot_after=snapshot_after,
|
|
1158
|
+
cursor=cursor_meta,
|
|
814
1159
|
error=(
|
|
815
1160
|
None
|
|
816
1161
|
if success
|
sentience/agent.py
CHANGED
|
@@ -355,6 +355,7 @@ class SentienceAgent(BaseAgent):
|
|
|
355
355
|
url_changed=result_dict.get("url_changed"),
|
|
356
356
|
error=result_dict.get("error"),
|
|
357
357
|
message=result_dict.get("message"),
|
|
358
|
+
cursor=result_dict.get("cursor"),
|
|
358
359
|
)
|
|
359
360
|
|
|
360
361
|
# Emit action execution trace event if tracer is enabled
|
|
@@ -391,6 +392,7 @@ class SentienceAgent(BaseAgent):
|
|
|
391
392
|
"post_url": post_url,
|
|
392
393
|
"elements": elements_data, # Add element data for overlay
|
|
393
394
|
"target_element_id": result.element_id, # Highlight target in red
|
|
395
|
+
"cursor": result.cursor,
|
|
394
396
|
},
|
|
395
397
|
step_id=step_id,
|
|
396
398
|
)
|
|
@@ -445,6 +447,8 @@ class SentienceAgent(BaseAgent):
|
|
|
445
447
|
),
|
|
446
448
|
"duration_ms": duration_ms,
|
|
447
449
|
}
|
|
450
|
+
if result.cursor is not None:
|
|
451
|
+
exec_data["cursor"] = result.cursor
|
|
448
452
|
|
|
449
453
|
# Add optional exec fields
|
|
450
454
|
if result.element_id is not None:
|