sentienceapi 0.90.12__py3-none-any.whl → 0.92.2__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 +14 -5
- sentience/_extension_loader.py +40 -0
- sentience/action_executor.py +215 -0
- sentience/actions.py +408 -25
- sentience/agent.py +804 -310
- sentience/agent_config.py +3 -0
- sentience/async_api.py +101 -0
- sentience/base_agent.py +95 -0
- sentience/browser.py +594 -25
- sentience/browser_evaluator.py +299 -0
- sentience/cloud_tracing.py +458 -36
- sentience/conversational_agent.py +79 -45
- sentience/element_filter.py +136 -0
- sentience/expect.py +98 -2
- sentience/extension/background.js +56 -185
- sentience/extension/content.js +117 -289
- sentience/extension/injected_api.js +799 -1374
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.js +190 -396
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +47 -47
- sentience/formatting.py +9 -53
- sentience/inspector.py +183 -1
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +256 -28
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +66 -1
- sentience/overlay.py +109 -2
- sentience/protocols.py +228 -0
- sentience/query.py +1 -1
- sentience/read.py +95 -3
- sentience/recorder.py +223 -3
- sentience/schemas/trace_v1.json +102 -9
- sentience/screenshot.py +48 -2
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +309 -64
- sentience/snapshot_diff.py +141 -0
- sentience/text_search.py +119 -5
- sentience/trace_event_builder.py +129 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/index_schema.py +95 -7
- sentience/trace_indexing/indexer.py +117 -14
- sentience/tracer_factory.py +119 -6
- sentience/tracing.py +172 -8
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/utils/element.py +257 -0
- sentience/utils/formatting.py +59 -0
- sentience/utils.py +1 -1
- sentience/visual_agent.py +2056 -0
- sentience/wait.py +70 -4
- {sentienceapi-0.90.12.dist-info → sentienceapi-0.92.2.dist-info}/METADATA +61 -22
- sentienceapi-0.92.2.dist-info/RECORD +65 -0
- sentienceapi-0.92.2.dist-info/licenses/LICENSE +24 -0
- sentienceapi-0.92.2.dist-info/licenses/LICENSE-APACHE +201 -0
- sentienceapi-0.92.2.dist-info/licenses/LICENSE-MIT +21 -0
- sentience/extension/test-content.js +0 -4
- sentienceapi-0.90.12.dist-info/RECORD +0 -46
- sentienceapi-0.90.12.dist-info/licenses/LICENSE.md +0 -43
- {sentienceapi-0.90.12.dist-info → sentienceapi-0.92.2.dist-info}/WHEEL +0 -0
- {sentienceapi-0.90.12.dist-info → sentienceapi-0.92.2.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.90.12.dist-info → sentienceapi-0.92.2.dist-info}/top_level.txt +0 -0
sentience/actions.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Actions v1 - click, type, press
|
|
3
5
|
"""
|
|
4
6
|
|
|
5
7
|
import time
|
|
6
8
|
|
|
7
|
-
from .browser import SentienceBrowser
|
|
9
|
+
from .browser import AsyncSentienceBrowser, SentienceBrowser
|
|
10
|
+
from .browser_evaluator import BrowserEvaluator
|
|
8
11
|
from .models import ActionResult, BBox, Snapshot
|
|
9
|
-
from .
|
|
12
|
+
from .sentience_methods import SentienceMethod
|
|
13
|
+
from .snapshot import snapshot, snapshot_async
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
def click( # noqa: C901
|
|
@@ -59,13 +63,8 @@ def click( # noqa: C901
|
|
|
59
63
|
else:
|
|
60
64
|
# Fallback to JS click if element not found in snapshot
|
|
61
65
|
try:
|
|
62
|
-
success =
|
|
63
|
-
|
|
64
|
-
(id) => {
|
|
65
|
-
return window.sentience.click(id);
|
|
66
|
-
}
|
|
67
|
-
""",
|
|
68
|
-
element_id,
|
|
66
|
+
success = BrowserEvaluator.invoke(
|
|
67
|
+
browser.page, SentienceMethod.CLICK, element_id
|
|
69
68
|
)
|
|
70
69
|
except Exception:
|
|
71
70
|
# Navigation might have destroyed context, assume success if URL changed
|
|
@@ -73,27 +72,13 @@ def click( # noqa: C901
|
|
|
73
72
|
except Exception:
|
|
74
73
|
# Fallback to JS click on error
|
|
75
74
|
try:
|
|
76
|
-
success = browser.page.
|
|
77
|
-
"""
|
|
78
|
-
(id) => {
|
|
79
|
-
return window.sentience.click(id);
|
|
80
|
-
}
|
|
81
|
-
""",
|
|
82
|
-
element_id,
|
|
83
|
-
)
|
|
75
|
+
success = BrowserEvaluator.invoke(browser.page, SentienceMethod.CLICK, element_id)
|
|
84
76
|
except Exception:
|
|
85
77
|
# Navigation might have destroyed context, assume success if URL changed
|
|
86
78
|
success = True
|
|
87
79
|
else:
|
|
88
80
|
# Legacy JS-based click
|
|
89
|
-
success = browser.page.
|
|
90
|
-
"""
|
|
91
|
-
(id) => {
|
|
92
|
-
return window.sentience.click(id);
|
|
93
|
-
}
|
|
94
|
-
""",
|
|
95
|
-
element_id,
|
|
96
|
-
)
|
|
81
|
+
success = BrowserEvaluator.invoke(browser.page, SentienceMethod.CLICK, element_id)
|
|
97
82
|
|
|
98
83
|
# Wait a bit for navigation/DOM updates
|
|
99
84
|
try:
|
|
@@ -437,3 +422,401 @@ def click_rect(
|
|
|
437
422
|
}
|
|
438
423
|
),
|
|
439
424
|
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# ========== Async Action Functions ==========
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
async def click_async(
|
|
431
|
+
browser: AsyncSentienceBrowser,
|
|
432
|
+
element_id: int,
|
|
433
|
+
use_mouse: bool = True,
|
|
434
|
+
take_snapshot: bool = False,
|
|
435
|
+
) -> ActionResult:
|
|
436
|
+
"""
|
|
437
|
+
Click an element by ID using hybrid approach (async)
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
browser: AsyncSentienceBrowser instance
|
|
441
|
+
element_id: Element ID from snapshot
|
|
442
|
+
use_mouse: If True, use Playwright's mouse.click() at element center
|
|
443
|
+
take_snapshot: Whether to take snapshot after action
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
ActionResult
|
|
447
|
+
"""
|
|
448
|
+
if not browser.page:
|
|
449
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
450
|
+
|
|
451
|
+
start_time = time.time()
|
|
452
|
+
url_before = browser.page.url
|
|
453
|
+
|
|
454
|
+
if use_mouse:
|
|
455
|
+
try:
|
|
456
|
+
snap = await snapshot_async(browser)
|
|
457
|
+
element = None
|
|
458
|
+
for el in snap.elements:
|
|
459
|
+
if el.id == element_id:
|
|
460
|
+
element = el
|
|
461
|
+
break
|
|
462
|
+
|
|
463
|
+
if element:
|
|
464
|
+
center_x = element.bbox.x + element.bbox.width / 2
|
|
465
|
+
center_y = element.bbox.y + element.bbox.height / 2
|
|
466
|
+
try:
|
|
467
|
+
await browser.page.mouse.click(center_x, center_y)
|
|
468
|
+
success = True
|
|
469
|
+
except Exception:
|
|
470
|
+
success = True
|
|
471
|
+
else:
|
|
472
|
+
try:
|
|
473
|
+
success = await browser.page.evaluate(
|
|
474
|
+
"""
|
|
475
|
+
(id) => {
|
|
476
|
+
return window.sentience.click(id);
|
|
477
|
+
}
|
|
478
|
+
""",
|
|
479
|
+
element_id,
|
|
480
|
+
)
|
|
481
|
+
except Exception:
|
|
482
|
+
success = True
|
|
483
|
+
except Exception:
|
|
484
|
+
try:
|
|
485
|
+
success = await browser.page.evaluate(
|
|
486
|
+
"""
|
|
487
|
+
(id) => {
|
|
488
|
+
return window.sentience.click(id);
|
|
489
|
+
}
|
|
490
|
+
""",
|
|
491
|
+
element_id,
|
|
492
|
+
)
|
|
493
|
+
except Exception:
|
|
494
|
+
success = True
|
|
495
|
+
else:
|
|
496
|
+
success = await browser.page.evaluate(
|
|
497
|
+
"""
|
|
498
|
+
(id) => {
|
|
499
|
+
return window.sentience.click(id);
|
|
500
|
+
}
|
|
501
|
+
""",
|
|
502
|
+
element_id,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Wait a bit for navigation/DOM updates
|
|
506
|
+
try:
|
|
507
|
+
await browser.page.wait_for_timeout(500)
|
|
508
|
+
except Exception:
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
512
|
+
|
|
513
|
+
# Check if URL changed
|
|
514
|
+
try:
|
|
515
|
+
url_after = browser.page.url
|
|
516
|
+
url_changed = url_before != url_after
|
|
517
|
+
except Exception:
|
|
518
|
+
url_after = url_before
|
|
519
|
+
url_changed = True
|
|
520
|
+
|
|
521
|
+
# Determine outcome
|
|
522
|
+
outcome: str | None = None
|
|
523
|
+
if url_changed:
|
|
524
|
+
outcome = "navigated"
|
|
525
|
+
elif success:
|
|
526
|
+
outcome = "dom_updated"
|
|
527
|
+
else:
|
|
528
|
+
outcome = "error"
|
|
529
|
+
|
|
530
|
+
# Optional snapshot after
|
|
531
|
+
snapshot_after: Snapshot | None = None
|
|
532
|
+
if take_snapshot:
|
|
533
|
+
try:
|
|
534
|
+
snapshot_after = await snapshot_async(browser)
|
|
535
|
+
except Exception:
|
|
536
|
+
pass
|
|
537
|
+
|
|
538
|
+
return ActionResult(
|
|
539
|
+
success=success,
|
|
540
|
+
duration_ms=duration_ms,
|
|
541
|
+
outcome=outcome,
|
|
542
|
+
url_changed=url_changed,
|
|
543
|
+
snapshot_after=snapshot_after,
|
|
544
|
+
error=(
|
|
545
|
+
None
|
|
546
|
+
if success
|
|
547
|
+
else {
|
|
548
|
+
"code": "click_failed",
|
|
549
|
+
"reason": "Element not found or not clickable",
|
|
550
|
+
}
|
|
551
|
+
),
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
async def type_text_async(
|
|
556
|
+
browser: AsyncSentienceBrowser, element_id: int, text: str, take_snapshot: bool = False
|
|
557
|
+
) -> ActionResult:
|
|
558
|
+
"""
|
|
559
|
+
Type text into an element (async)
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
browser: AsyncSentienceBrowser instance
|
|
563
|
+
element_id: Element ID from snapshot
|
|
564
|
+
text: Text to type
|
|
565
|
+
take_snapshot: Whether to take snapshot after action
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
ActionResult
|
|
569
|
+
"""
|
|
570
|
+
if not browser.page:
|
|
571
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
572
|
+
|
|
573
|
+
start_time = time.time()
|
|
574
|
+
url_before = browser.page.url
|
|
575
|
+
|
|
576
|
+
# Focus element first
|
|
577
|
+
focused = await browser.page.evaluate(
|
|
578
|
+
"""
|
|
579
|
+
(id) => {
|
|
580
|
+
const el = window.sentience_registry[id];
|
|
581
|
+
if (el) {
|
|
582
|
+
el.focus();
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
""",
|
|
588
|
+
element_id,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
if not focused:
|
|
592
|
+
return ActionResult(
|
|
593
|
+
success=False,
|
|
594
|
+
duration_ms=int((time.time() - start_time) * 1000),
|
|
595
|
+
outcome="error",
|
|
596
|
+
error={"code": "focus_failed", "reason": "Element not found"},
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Type using Playwright keyboard
|
|
600
|
+
await browser.page.keyboard.type(text)
|
|
601
|
+
|
|
602
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
603
|
+
url_after = browser.page.url
|
|
604
|
+
url_changed = url_before != url_after
|
|
605
|
+
|
|
606
|
+
outcome = "navigated" if url_changed else "dom_updated"
|
|
607
|
+
|
|
608
|
+
snapshot_after: Snapshot | None = None
|
|
609
|
+
if take_snapshot:
|
|
610
|
+
snapshot_after = await snapshot_async(browser)
|
|
611
|
+
|
|
612
|
+
return ActionResult(
|
|
613
|
+
success=True,
|
|
614
|
+
duration_ms=duration_ms,
|
|
615
|
+
outcome=outcome,
|
|
616
|
+
url_changed=url_changed,
|
|
617
|
+
snapshot_after=snapshot_after,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
async def press_async(
|
|
622
|
+
browser: AsyncSentienceBrowser, key: str, take_snapshot: bool = False
|
|
623
|
+
) -> ActionResult:
|
|
624
|
+
"""
|
|
625
|
+
Press a keyboard key (async)
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
browser: AsyncSentienceBrowser instance
|
|
629
|
+
key: Key to press (e.g., "Enter", "Escape", "Tab")
|
|
630
|
+
take_snapshot: Whether to take snapshot after action
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
ActionResult
|
|
634
|
+
"""
|
|
635
|
+
if not browser.page:
|
|
636
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
637
|
+
|
|
638
|
+
start_time = time.time()
|
|
639
|
+
url_before = browser.page.url
|
|
640
|
+
|
|
641
|
+
# Press key using Playwright
|
|
642
|
+
await browser.page.keyboard.press(key)
|
|
643
|
+
|
|
644
|
+
# Wait a bit for navigation/DOM updates
|
|
645
|
+
await browser.page.wait_for_timeout(500)
|
|
646
|
+
|
|
647
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
648
|
+
url_after = browser.page.url
|
|
649
|
+
url_changed = url_before != url_after
|
|
650
|
+
|
|
651
|
+
outcome = "navigated" if url_changed else "dom_updated"
|
|
652
|
+
|
|
653
|
+
snapshot_after: Snapshot | None = None
|
|
654
|
+
if take_snapshot:
|
|
655
|
+
snapshot_after = await snapshot_async(browser)
|
|
656
|
+
|
|
657
|
+
return ActionResult(
|
|
658
|
+
success=True,
|
|
659
|
+
duration_ms=duration_ms,
|
|
660
|
+
outcome=outcome,
|
|
661
|
+
url_changed=url_changed,
|
|
662
|
+
snapshot_after=snapshot_after,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
async def _highlight_rect_async(
|
|
667
|
+
browser: AsyncSentienceBrowser, rect: dict[str, float], duration_sec: float = 2.0
|
|
668
|
+
) -> None:
|
|
669
|
+
"""Highlight a rectangle with a red border overlay (async)"""
|
|
670
|
+
if not browser.page:
|
|
671
|
+
return
|
|
672
|
+
|
|
673
|
+
highlight_id = f"sentience_highlight_{int(time.time() * 1000)}"
|
|
674
|
+
|
|
675
|
+
args = {
|
|
676
|
+
"rect": {
|
|
677
|
+
"x": rect["x"],
|
|
678
|
+
"y": rect["y"],
|
|
679
|
+
"w": rect["w"],
|
|
680
|
+
"h": rect["h"],
|
|
681
|
+
},
|
|
682
|
+
"highlightId": highlight_id,
|
|
683
|
+
"durationSec": duration_sec,
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
await browser.page.evaluate(
|
|
687
|
+
"""
|
|
688
|
+
(args) => {
|
|
689
|
+
const { rect, highlightId, durationSec } = args;
|
|
690
|
+
const overlay = document.createElement('div');
|
|
691
|
+
overlay.id = highlightId;
|
|
692
|
+
overlay.style.position = 'fixed';
|
|
693
|
+
overlay.style.left = `${rect.x}px`;
|
|
694
|
+
overlay.style.top = `${rect.y}px`;
|
|
695
|
+
overlay.style.width = `${rect.w}px`;
|
|
696
|
+
overlay.style.height = `${rect.h}px`;
|
|
697
|
+
overlay.style.border = '3px solid red';
|
|
698
|
+
overlay.style.borderRadius = '2px';
|
|
699
|
+
overlay.style.boxSizing = 'border-box';
|
|
700
|
+
overlay.style.pointerEvents = 'none';
|
|
701
|
+
overlay.style.zIndex = '999999';
|
|
702
|
+
overlay.style.backgroundColor = 'rgba(255, 0, 0, 0.1)';
|
|
703
|
+
overlay.style.transition = 'opacity 0.3s ease-out';
|
|
704
|
+
|
|
705
|
+
document.body.appendChild(overlay);
|
|
706
|
+
|
|
707
|
+
setTimeout(() => {
|
|
708
|
+
overlay.style.opacity = '0';
|
|
709
|
+
setTimeout(() => {
|
|
710
|
+
if (overlay.parentNode) {
|
|
711
|
+
overlay.parentNode.removeChild(overlay);
|
|
712
|
+
}
|
|
713
|
+
}, 300);
|
|
714
|
+
}, durationSec * 1000);
|
|
715
|
+
}
|
|
716
|
+
""",
|
|
717
|
+
args,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
async def click_rect_async(
|
|
722
|
+
browser: AsyncSentienceBrowser,
|
|
723
|
+
rect: dict[str, float] | BBox,
|
|
724
|
+
highlight: bool = True,
|
|
725
|
+
highlight_duration: float = 2.0,
|
|
726
|
+
take_snapshot: bool = False,
|
|
727
|
+
) -> ActionResult:
|
|
728
|
+
"""
|
|
729
|
+
Click at the center of a rectangle (async)
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
browser: AsyncSentienceBrowser instance
|
|
733
|
+
rect: Dictionary with x, y, width (w), height (h) keys, or BBox object
|
|
734
|
+
highlight: Whether to show a red border highlight when clicking
|
|
735
|
+
highlight_duration: How long to show the highlight in seconds
|
|
736
|
+
take_snapshot: Whether to take snapshot after action
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
ActionResult
|
|
740
|
+
"""
|
|
741
|
+
if not browser.page:
|
|
742
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
743
|
+
|
|
744
|
+
# Handle BBox object or dict
|
|
745
|
+
if isinstance(rect, BBox):
|
|
746
|
+
x = rect.x
|
|
747
|
+
y = rect.y
|
|
748
|
+
w = rect.width
|
|
749
|
+
h = rect.height
|
|
750
|
+
else:
|
|
751
|
+
x = rect.get("x", 0)
|
|
752
|
+
y = rect.get("y", 0)
|
|
753
|
+
w = rect.get("w") or rect.get("width", 0)
|
|
754
|
+
h = rect.get("h") or rect.get("height", 0)
|
|
755
|
+
|
|
756
|
+
if w <= 0 or h <= 0:
|
|
757
|
+
return ActionResult(
|
|
758
|
+
success=False,
|
|
759
|
+
duration_ms=0,
|
|
760
|
+
outcome="error",
|
|
761
|
+
error={
|
|
762
|
+
"code": "invalid_rect",
|
|
763
|
+
"reason": "Rectangle width and height must be positive",
|
|
764
|
+
},
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
start_time = time.time()
|
|
768
|
+
url_before = browser.page.url
|
|
769
|
+
|
|
770
|
+
# Calculate center of rectangle
|
|
771
|
+
center_x = x + w / 2
|
|
772
|
+
center_y = y + h / 2
|
|
773
|
+
|
|
774
|
+
# Show highlight before clicking
|
|
775
|
+
if highlight:
|
|
776
|
+
await _highlight_rect_async(browser, {"x": x, "y": y, "w": w, "h": h}, highlight_duration)
|
|
777
|
+
await browser.page.wait_for_timeout(50)
|
|
778
|
+
|
|
779
|
+
# Use Playwright's native mouse click
|
|
780
|
+
try:
|
|
781
|
+
await browser.page.mouse.click(center_x, center_y)
|
|
782
|
+
success = True
|
|
783
|
+
except Exception as e:
|
|
784
|
+
success = False
|
|
785
|
+
error_msg = str(e)
|
|
786
|
+
|
|
787
|
+
# Wait a bit for navigation/DOM updates
|
|
788
|
+
await browser.page.wait_for_timeout(500)
|
|
789
|
+
|
|
790
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
791
|
+
url_after = browser.page.url
|
|
792
|
+
url_changed = url_before != url_after
|
|
793
|
+
|
|
794
|
+
# Determine outcome
|
|
795
|
+
outcome: str | None = None
|
|
796
|
+
if url_changed:
|
|
797
|
+
outcome = "navigated"
|
|
798
|
+
elif success:
|
|
799
|
+
outcome = "dom_updated"
|
|
800
|
+
else:
|
|
801
|
+
outcome = "error"
|
|
802
|
+
|
|
803
|
+
# Optional snapshot after
|
|
804
|
+
snapshot_after: Snapshot | None = None
|
|
805
|
+
if take_snapshot:
|
|
806
|
+
snapshot_after = await snapshot_async(browser)
|
|
807
|
+
|
|
808
|
+
return ActionResult(
|
|
809
|
+
success=success,
|
|
810
|
+
duration_ms=duration_ms,
|
|
811
|
+
outcome=outcome,
|
|
812
|
+
url_changed=url_changed,
|
|
813
|
+
snapshot_after=snapshot_after,
|
|
814
|
+
error=(
|
|
815
|
+
None
|
|
816
|
+
if success
|
|
817
|
+
else {
|
|
818
|
+
"code": "click_failed",
|
|
819
|
+
"reason": error_msg if not success else "Click failed",
|
|
820
|
+
}
|
|
821
|
+
),
|
|
822
|
+
)
|