sentienceapi 0.90.16__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.

Files changed (61) hide show
  1. sentience/__init__.py +14 -5
  2. sentience/action_executor.py +215 -0
  3. sentience/actions.py +408 -25
  4. sentience/agent.py +802 -293
  5. sentience/agent_config.py +3 -0
  6. sentience/async_api.py +83 -1142
  7. sentience/base_agent.py +95 -0
  8. sentience/browser.py +484 -1
  9. sentience/browser_evaluator.py +299 -0
  10. sentience/cloud_tracing.py +457 -33
  11. sentience/conversational_agent.py +77 -43
  12. sentience/element_filter.py +136 -0
  13. sentience/expect.py +98 -2
  14. sentience/extension/background.js +56 -185
  15. sentience/extension/content.js +117 -289
  16. sentience/extension/injected_api.js +799 -1374
  17. sentience/extension/manifest.json +1 -1
  18. sentience/extension/pkg/sentience_core.js +190 -396
  19. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  20. sentience/extension/release.json +47 -47
  21. sentience/formatting.py +9 -53
  22. sentience/inspector.py +183 -1
  23. sentience/llm_interaction_handler.py +191 -0
  24. sentience/llm_provider.py +74 -52
  25. sentience/llm_provider_utils.py +120 -0
  26. sentience/llm_response_builder.py +153 -0
  27. sentience/models.py +60 -1
  28. sentience/overlay.py +109 -2
  29. sentience/protocols.py +228 -0
  30. sentience/query.py +1 -1
  31. sentience/read.py +95 -3
  32. sentience/recorder.py +223 -3
  33. sentience/schemas/trace_v1.json +102 -9
  34. sentience/screenshot.py +48 -2
  35. sentience/sentience_methods.py +86 -0
  36. sentience/snapshot.py +291 -38
  37. sentience/snapshot_diff.py +141 -0
  38. sentience/text_search.py +119 -5
  39. sentience/trace_event_builder.py +129 -0
  40. sentience/trace_file_manager.py +197 -0
  41. sentience/trace_indexing/index_schema.py +95 -7
  42. sentience/trace_indexing/indexer.py +117 -14
  43. sentience/tracer_factory.py +119 -6
  44. sentience/tracing.py +172 -8
  45. sentience/utils/__init__.py +40 -0
  46. sentience/utils/browser.py +46 -0
  47. sentience/utils/element.py +257 -0
  48. sentience/utils/formatting.py +59 -0
  49. sentience/utils.py +1 -1
  50. sentience/visual_agent.py +2056 -0
  51. sentience/wait.py +68 -2
  52. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/METADATA +2 -1
  53. sentienceapi-0.92.2.dist-info/RECORD +65 -0
  54. sentience/extension/test-content.js +0 -4
  55. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  56. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/WHEEL +0 -0
  57. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/entry_points.txt +0 -0
  58. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE +0 -0
  59. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE-APACHE +0 -0
  60. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE-MIT +0 -0
  61. {sentienceapi-0.90.16.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 .snapshot import snapshot
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 = browser.page.evaluate(
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.evaluate(
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.evaluate(
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
+ )