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.

Files changed (64) hide show
  1. sentience/__init__.py +107 -2
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +2 -0
  4. sentience/actions.py +354 -9
  5. sentience/agent.py +4 -0
  6. sentience/agent_runtime.py +840 -0
  7. sentience/asserts/__init__.py +70 -0
  8. sentience/asserts/expect.py +621 -0
  9. sentience/asserts/query.py +383 -0
  10. sentience/async_api.py +8 -1
  11. sentience/backends/__init__.py +137 -0
  12. sentience/backends/actions.py +372 -0
  13. sentience/backends/browser_use_adapter.py +241 -0
  14. sentience/backends/cdp_backend.py +393 -0
  15. sentience/backends/exceptions.py +211 -0
  16. sentience/backends/playwright_backend.py +194 -0
  17. sentience/backends/protocol.py +216 -0
  18. sentience/backends/sentience_context.py +469 -0
  19. sentience/backends/snapshot.py +483 -0
  20. sentience/browser.py +230 -74
  21. sentience/canonicalization.py +207 -0
  22. sentience/cloud_tracing.py +65 -24
  23. sentience/constants.py +6 -0
  24. sentience/cursor_policy.py +142 -0
  25. sentience/extension/content.js +35 -0
  26. sentience/extension/injected_api.js +310 -15
  27. sentience/extension/manifest.json +1 -1
  28. sentience/extension/pkg/sentience_core.d.ts +22 -22
  29. sentience/extension/pkg/sentience_core.js +192 -144
  30. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  31. sentience/extension/release.json +29 -29
  32. sentience/failure_artifacts.py +241 -0
  33. sentience/integrations/__init__.py +6 -0
  34. sentience/integrations/langchain/__init__.py +12 -0
  35. sentience/integrations/langchain/context.py +18 -0
  36. sentience/integrations/langchain/core.py +326 -0
  37. sentience/integrations/langchain/tools.py +180 -0
  38. sentience/integrations/models.py +46 -0
  39. sentience/integrations/pydanticai/__init__.py +15 -0
  40. sentience/integrations/pydanticai/deps.py +20 -0
  41. sentience/integrations/pydanticai/toolset.py +468 -0
  42. sentience/llm_provider.py +695 -18
  43. sentience/models.py +536 -3
  44. sentience/ordinal.py +280 -0
  45. sentience/query.py +66 -4
  46. sentience/schemas/trace_v1.json +27 -1
  47. sentience/snapshot.py +384 -93
  48. sentience/snapshot_diff.py +39 -54
  49. sentience/text_search.py +1 -0
  50. sentience/trace_event_builder.py +20 -1
  51. sentience/trace_indexing/indexer.py +3 -49
  52. sentience/tracer_factory.py +1 -3
  53. sentience/verification.py +618 -0
  54. sentience/visual_agent.py +3 -1
  55. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +198 -40
  56. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  57. sentience/utils.py +0 -296
  58. sentienceapi-0.92.2.dist-info/RECORD +0 -65
  59. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  60. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  61. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  62. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  63. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  64. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
@@ -2,10 +2,12 @@
2
2
  Snapshot comparison utilities for diff_status detection.
3
3
 
4
4
  Implements change detection logic for the Diff Overlay feature.
5
- """
6
5
 
7
- from typing import Literal
6
+ Uses shared canonicalization helpers from canonicalization.py to ensure
7
+ consistent comparison behavior with trace_indexing/indexer.py.
8
+ """
8
9
 
10
+ from .canonicalization import bbox_changed, content_changed
9
11
  from .models import Element, Snapshot
10
12
 
11
13
 
@@ -18,55 +20,30 @@ class SnapshotDiff:
18
20
  - REMOVED: Element existed in previous but not in current
19
21
  - MODIFIED: Element exists in both but has changed
20
22
  - MOVED: Element exists in both but position changed
21
- """
22
-
23
- @staticmethod
24
- def _has_bbox_changed(el1: Element, el2: Element, threshold: float = 5.0) -> bool:
25
- """
26
- Check if element's bounding box has changed significantly.
27
23
 
28
- Args:
29
- el1: First element
30
- el2: Second element
31
- threshold: Position change threshold in pixels (default: 5.0)
32
-
33
- Returns:
34
- True if position or size changed beyond threshold
35
- """
36
- return (
37
- abs(el1.bbox.x - el2.bbox.x) > threshold
38
- or abs(el1.bbox.y - el2.bbox.y) > threshold
39
- or abs(el1.bbox.width - el2.bbox.width) > threshold
40
- or abs(el1.bbox.height - el2.bbox.height) > threshold
41
- )
24
+ Uses canonicalized comparisons (normalized text, rounded bbox) to reduce
25
+ noise from insignificant changes like sub-pixel rendering differences
26
+ or whitespace variations.
27
+ """
42
28
 
43
29
  @staticmethod
44
- def _has_content_changed(el1: Element, el2: Element) -> bool:
45
- """
46
- Check if element's content has changed.
47
-
48
- Args:
49
- el1: First element
50
- el2: Second element
51
-
52
- Returns:
53
- True if text, role, or visual properties changed
54
- """
55
- # Compare text content
56
- if el1.text != el2.text:
57
- return True
58
-
59
- # Compare role
60
- if el1.role != el2.role:
61
- return True
62
-
63
- # Compare visual cues
64
- if el1.visual_cues.is_primary != el2.visual_cues.is_primary:
65
- return True
66
- if el1.visual_cues.is_clickable != el2.visual_cues.is_clickable:
67
- return True
68
-
69
- return False
30
+ def _element_to_dict(el: Element) -> dict:
31
+ """Convert Element model to dict for canonicalization helpers."""
32
+ return {
33
+ "id": el.id,
34
+ "role": el.role,
35
+ "text": el.text,
36
+ "bbox": {
37
+ "x": el.bbox.x,
38
+ "y": el.bbox.y,
39
+ "width": el.bbox.width,
40
+ "height": el.bbox.height,
41
+ },
42
+ "visual_cues": {
43
+ "is_primary": el.visual_cues.is_primary,
44
+ "is_clickable": el.visual_cues.is_clickable,
45
+ },
46
+ }
70
47
 
71
48
  @staticmethod
72
49
  def compute_diff_status(
@@ -76,6 +53,10 @@ class SnapshotDiff:
76
53
  """
77
54
  Compare current snapshot with previous and set diff_status on elements.
78
55
 
56
+ Uses canonicalized comparisons:
57
+ - Text is normalized (trimmed, collapsed whitespace, lowercased)
58
+ - Bbox is rounded to 2px grid to ignore sub-pixel differences
59
+
79
60
  Args:
80
61
  current: Current snapshot
81
62
  previous: Previous snapshot (None if this is the first snapshot)
@@ -110,19 +91,23 @@ class SnapshotDiff:
110
91
  # Element is new - mark as ADDED
111
92
  el_dict["diff_status"] = "ADDED"
112
93
  else:
113
- # Element existed before - check for changes
94
+ # Element existed before - check for changes using canonicalized comparisons
114
95
  prev_el = previous_by_id[el.id]
115
96
 
116
- bbox_changed = SnapshotDiff._has_bbox_changed(el, prev_el)
117
- content_changed = SnapshotDiff._has_content_changed(el, prev_el)
97
+ # Convert to dicts for canonicalization helpers
98
+ el_data = SnapshotDiff._element_to_dict(el)
99
+ prev_el_data = SnapshotDiff._element_to_dict(prev_el)
100
+
101
+ has_bbox_changed = bbox_changed(el_data["bbox"], prev_el_data["bbox"])
102
+ has_content_changed = content_changed(el_data, prev_el_data)
118
103
 
119
- if bbox_changed and content_changed:
104
+ if has_bbox_changed and has_content_changed:
120
105
  # Both position and content changed - mark as MODIFIED
121
106
  el_dict["diff_status"] = "MODIFIED"
122
- elif bbox_changed:
107
+ elif has_bbox_changed:
123
108
  # Only position changed - mark as MOVED
124
109
  el_dict["diff_status"] = "MOVED"
125
- elif content_changed:
110
+ elif has_content_changed:
126
111
  # Only content changed - mark as MODIFIED
127
112
  el_dict["diff_status"] = "MODIFIED"
128
113
  else:
sentience/text_search.py CHANGED
@@ -5,6 +5,7 @@ Text search utilities - find text and get pixel coordinates
5
5
  from .browser import AsyncSentienceBrowser, SentienceBrowser
6
6
  from .browser_evaluator import BrowserEvaluator
7
7
  from .models import TextRectSearchResult
8
+ from .sentience_methods import SentienceMethod
8
9
 
9
10
 
10
11
  def find_text_rect(
@@ -84,6 +84,7 @@ class TraceEventBuilder:
84
84
  exec_data: dict[str, Any],
85
85
  verify_data: dict[str, Any],
86
86
  pre_elements: list[dict[str, Any]] | None = None,
87
+ assertions: list[dict[str, Any]] | None = None,
87
88
  ) -> dict[str, Any]:
88
89
  """
89
90
  Build step_end trace event data.
@@ -100,6 +101,7 @@ class TraceEventBuilder:
100
101
  exec_data: Action execution data
101
102
  verify_data: Verification data
102
103
  pre_elements: Optional list of elements from pre-snapshot (with diff_status)
104
+ assertions: Optional list of assertion results from AgentRuntime
103
105
 
104
106
  Returns:
105
107
  Dictionary with step_end event data
@@ -113,6 +115,23 @@ class TraceEventBuilder:
113
115
  if pre_elements is not None:
114
116
  pre_data["elements"] = pre_elements
115
117
 
118
+ # Build verify data with assertions if provided
119
+ final_verify_data = verify_data.copy()
120
+ if assertions:
121
+ # Ensure signals dict exists
122
+ if "signals" not in final_verify_data:
123
+ final_verify_data["signals"] = {}
124
+
125
+ # Add assertions to signals
126
+ final_verify_data["signals"]["assertions"] = assertions
127
+
128
+ # Check for task completion (assertions marked as required that passed)
129
+ for a in assertions:
130
+ if a.get("passed") and a.get("required"):
131
+ final_verify_data["signals"]["task_done"] = True
132
+ final_verify_data["signals"]["task_done_label"] = a.get("label")
133
+ break
134
+
116
135
  return {
117
136
  "v": 1,
118
137
  "step_id": step_id,
@@ -125,5 +144,5 @@ class TraceEventBuilder:
125
144
  "post": {
126
145
  "url": post_url,
127
146
  },
128
- "verify": verify_data,
147
+ "verify": final_verify_data,
129
148
  }
@@ -9,6 +9,7 @@ from datetime import datetime, timezone
9
9
  from pathlib import Path
10
10
  from typing import Any, Optional
11
11
 
12
+ from ..canonicalization import canonicalize_element
12
13
  from .index_schema import (
13
14
  ActionInfo,
14
15
  SnapshotInfo,
@@ -20,30 +21,6 @@ from .index_schema import (
20
21
  )
21
22
 
22
23
 
23
- def _normalize_text(text: str | None, max_len: int = 80) -> str:
24
- """Normalize text for digest: trim, collapse whitespace, lowercase, cap length."""
25
- if not text:
26
- return ""
27
- # Trim and collapse whitespace
28
- normalized = " ".join(text.split())
29
- # Lowercase
30
- normalized = normalized.lower()
31
- # Cap length
32
- if len(normalized) > max_len:
33
- normalized = normalized[:max_len]
34
- return normalized
35
-
36
-
37
- def _round_bbox(bbox: dict[str, float], precision: int = 2) -> dict[str, int]:
38
- """Round bbox coordinates to reduce noise (default: 2px precision)."""
39
- return {
40
- "x": round(bbox.get("x", 0) / precision) * precision,
41
- "y": round(bbox.get("y", 0) / precision) * precision,
42
- "width": round(bbox.get("width", 0) / precision) * precision,
43
- "height": round(bbox.get("height", 0) / precision) * precision,
44
- }
45
-
46
-
47
24
  def _compute_snapshot_digest(snapshot_data: dict[str, Any]) -> str:
48
25
  """
49
26
  Compute stable digest of snapshot for diffing.
@@ -55,31 +32,8 @@ def _compute_snapshot_digest(snapshot_data: dict[str, Any]) -> str:
55
32
  viewport = snapshot_data.get("viewport", {})
56
33
  elements = snapshot_data.get("elements", [])
57
34
 
58
- # Canonicalize elements
59
- canonical_elements = []
60
- for elem in elements:
61
- # Extract is_primary and is_clickable from visual_cues if present
62
- visual_cues = elem.get("visual_cues", {})
63
- is_primary = (
64
- visual_cues.get("is_primary", False)
65
- if isinstance(visual_cues, dict)
66
- else elem.get("is_primary", False)
67
- )
68
- is_clickable = (
69
- visual_cues.get("is_clickable", False)
70
- if isinstance(visual_cues, dict)
71
- else elem.get("is_clickable", False)
72
- )
73
-
74
- canonical_elem = {
75
- "id": elem.get("id"),
76
- "role": elem.get("role", ""),
77
- "text_norm": _normalize_text(elem.get("text")),
78
- "bbox": _round_bbox(elem.get("bbox", {"x": 0, "y": 0, "width": 0, "height": 0})),
79
- "is_primary": is_primary,
80
- "is_clickable": is_clickable,
81
- }
82
- canonical_elements.append(canonical_elem)
35
+ # Canonicalize elements using shared helper
36
+ canonical_elements = [canonicalize_element(elem) for elem in elements]
83
37
 
84
38
  # Sort by element id for determinism
85
39
  canonical_elements.sort(key=lambda e: e.get("id", 0))
@@ -14,11 +14,9 @@ from typing import Any, Optional
14
14
  import requests
15
15
 
16
16
  from sentience.cloud_tracing import CloudTraceSink, SentienceLogger
17
+ from sentience.constants import SENTIENCE_API_URL
17
18
  from sentience.tracing import JsonlTraceSink, Tracer
18
19
 
19
- # Sentience API base URL (constant)
20
- SENTIENCE_API_URL = "https://api.sentienceapi.com"
21
-
22
20
 
23
21
  def create_tracer(
24
22
  api_key: str | None = None,