sentienceapi 0.90.16__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 (90) hide show
  1. sentience/__init__.py +120 -6
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +217 -0
  4. sentience/actions.py +758 -30
  5. sentience/agent.py +806 -293
  6. sentience/agent_config.py +3 -0
  7. sentience/agent_runtime.py +840 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +89 -1141
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +372 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +483 -0
  21. sentience/base_agent.py +95 -0
  22. sentience/browser.py +678 -39
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cloud_tracing.py +507 -42
  26. sentience/constants.py +6 -0
  27. sentience/conversational_agent.py +77 -43
  28. sentience/cursor_policy.py +142 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +98 -2
  31. sentience/extension/background.js +56 -185
  32. sentience/extension/content.js +150 -287
  33. sentience/extension/injected_api.js +1088 -1368
  34. sentience/extension/manifest.json +1 -1
  35. sentience/extension/pkg/sentience_core.d.ts +22 -22
  36. sentience/extension/pkg/sentience_core.js +275 -433
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/release.json +47 -47
  39. sentience/failure_artifacts.py +241 -0
  40. sentience/formatting.py +9 -53
  41. sentience/inspector.py +183 -1
  42. sentience/integrations/__init__.py +6 -0
  43. sentience/integrations/langchain/__init__.py +12 -0
  44. sentience/integrations/langchain/context.py +18 -0
  45. sentience/integrations/langchain/core.py +326 -0
  46. sentience/integrations/langchain/tools.py +180 -0
  47. sentience/integrations/models.py +46 -0
  48. sentience/integrations/pydanticai/__init__.py +15 -0
  49. sentience/integrations/pydanticai/deps.py +20 -0
  50. sentience/integrations/pydanticai/toolset.py +468 -0
  51. sentience/llm_interaction_handler.py +191 -0
  52. sentience/llm_provider.py +765 -66
  53. sentience/llm_provider_utils.py +120 -0
  54. sentience/llm_response_builder.py +153 -0
  55. sentience/models.py +595 -3
  56. sentience/ordinal.py +280 -0
  57. sentience/overlay.py +109 -2
  58. sentience/protocols.py +228 -0
  59. sentience/query.py +67 -5
  60. sentience/read.py +95 -3
  61. sentience/recorder.py +223 -3
  62. sentience/schemas/trace_v1.json +128 -9
  63. sentience/screenshot.py +48 -2
  64. sentience/sentience_methods.py +86 -0
  65. sentience/snapshot.py +599 -55
  66. sentience/snapshot_diff.py +126 -0
  67. sentience/text_search.py +120 -5
  68. sentience/trace_event_builder.py +148 -0
  69. sentience/trace_file_manager.py +197 -0
  70. sentience/trace_indexing/index_schema.py +95 -7
  71. sentience/trace_indexing/indexer.py +105 -48
  72. sentience/tracer_factory.py +120 -9
  73. sentience/tracing.py +172 -8
  74. sentience/utils/__init__.py +40 -0
  75. sentience/utils/browser.py +46 -0
  76. sentience/{utils.py → utils/element.py} +3 -42
  77. sentience/utils/formatting.py +59 -0
  78. sentience/verification.py +618 -0
  79. sentience/visual_agent.py +2058 -0
  80. sentience/wait.py +68 -2
  81. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
  82. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  83. sentience/extension/test-content.js +0 -4
  84. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  85. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  86. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  87. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  88. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  89. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  90. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
@@ -1,45 +1,45 @@
1
1
  {
2
- "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/273122615",
3
- "assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/273122615/assets",
4
- "upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/273122615/assets{?name,label}",
5
- "html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v2.0.7",
6
- "id": 273122615,
2
+ "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/277802850",
3
+ "assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/277802850/assets",
4
+ "upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/277802850/assets{?name,label}",
5
+ "html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v2.7.0",
6
+ "id": 277802850,
7
7
  "author": {
8
- "login": "github-actions[bot]",
9
- "id": 41898282,
10
- "node_id": "MDM6Qm90NDE4OTgyODI=",
11
- "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
8
+ "login": "rcholic",
9
+ "id": 135060,
10
+ "node_id": "MDQ6VXNlcjEzNTA2MA==",
11
+ "avatar_url": "https://avatars.githubusercontent.com/u/135060?v=4",
12
12
  "gravatar_id": "",
13
- "url": "https://api.github.com/users/github-actions%5Bbot%5D",
14
- "html_url": "https://github.com/apps/github-actions",
15
- "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
16
- "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
17
- "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
18
- "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
19
- "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
20
- "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
21
- "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
22
- "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
23
- "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
24
- "type": "Bot",
13
+ "url": "https://api.github.com/users/rcholic",
14
+ "html_url": "https://github.com/rcholic",
15
+ "followers_url": "https://api.github.com/users/rcholic/followers",
16
+ "following_url": "https://api.github.com/users/rcholic/following{/other_user}",
17
+ "gists_url": "https://api.github.com/users/rcholic/gists{/gist_id}",
18
+ "starred_url": "https://api.github.com/users/rcholic/starred{/owner}{/repo}",
19
+ "subscriptions_url": "https://api.github.com/users/rcholic/subscriptions",
20
+ "organizations_url": "https://api.github.com/users/rcholic/orgs",
21
+ "repos_url": "https://api.github.com/users/rcholic/repos",
22
+ "events_url": "https://api.github.com/users/rcholic/events{/privacy}",
23
+ "received_events_url": "https://api.github.com/users/rcholic/received_events",
24
+ "type": "User",
25
25
  "user_view_type": "public",
26
26
  "site_admin": false
27
27
  },
28
- "node_id": "RE_kwDOQshiJ84QR4U3",
29
- "tag_name": "v2.0.7",
28
+ "node_id": "RE_kwDOQshiJ84Qju9i",
29
+ "tag_name": "v2.7.0",
30
30
  "target_commitish": "main",
31
- "name": "Release v2.0.7",
31
+ "name": "Release v2.7.0",
32
32
  "draft": false,
33
33
  "immutable": false,
34
34
  "prerelease": false,
35
- "created_at": "2025-12-29T03:56:13Z",
36
- "updated_at": "2025-12-29T03:57:09Z",
37
- "published_at": "2025-12-29T03:57:08Z",
35
+ "created_at": "2026-01-19T05:08:35Z",
36
+ "updated_at": "2026-01-19T05:09:41Z",
37
+ "published_at": "2026-01-19T05:09:31Z",
38
38
  "assets": [
39
39
  {
40
- "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/333966751",
41
- "id": 333966751,
42
- "node_id": "RA_kwDOQshiJ84T5-2f",
40
+ "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/342599449",
41
+ "id": 342599449,
42
+ "node_id": "RA_kwDOQshiJ84Ua6cZ",
43
43
  "name": "extension-files.tar.gz",
44
44
  "label": "",
45
45
  "uploader": {
@@ -65,17 +65,17 @@
65
65
  },
66
66
  "content_type": "application/gzip",
67
67
  "state": "uploaded",
68
- "size": 78091,
69
- "digest": "sha256:e281f8b755b61da4b8015d6172064aa9a337c14133ceceff4ab29199ee53307e",
70
- "download_count": 5,
71
- "created_at": "2025-12-29T03:57:09Z",
72
- "updated_at": "2025-12-29T03:57:09Z",
73
- "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.0.7/extension-files.tar.gz"
68
+ "size": 79222,
69
+ "digest": "sha256:e0cad96e20e539d62a4777f1c5baedb1c8bd02cd26a8e38f7c1b9c804325f068",
70
+ "download_count": 0,
71
+ "created_at": "2026-01-19T05:09:41Z",
72
+ "updated_at": "2026-01-19T05:09:41Z",
73
+ "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.7.0/extension-files.tar.gz"
74
74
  },
75
75
  {
76
- "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/333966752",
77
- "id": 333966752,
78
- "node_id": "RA_kwDOQshiJ84T5-2g",
76
+ "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/342599448",
77
+ "id": 342599448,
78
+ "node_id": "RA_kwDOQshiJ84Ua6cY",
79
79
  "name": "extension-package.zip",
80
80
  "label": "",
81
81
  "uploader": {
@@ -101,15 +101,15 @@
101
101
  },
102
102
  "content_type": "application/zip",
103
103
  "state": "uploaded",
104
- "size": 80179,
105
- "digest": "sha256:a025edeb8b6d05bfb25c57f913b68507060653ecbdf616000a46df4cb8dec377",
104
+ "size": 80692,
105
+ "digest": "sha256:83de4c4c54f401fc6404c27ef01a9205cf8e3566e81f62a7bc08f7103a2b0cc5",
106
106
  "download_count": 0,
107
- "created_at": "2025-12-29T03:57:09Z",
108
- "updated_at": "2025-12-29T03:57:09Z",
109
- "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.0.7/extension-package.zip"
107
+ "created_at": "2026-01-19T05:09:41Z",
108
+ "updated_at": "2026-01-19T05:09:41Z",
109
+ "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.7.0/extension-package.zip"
110
110
  }
111
111
  ],
112
- "tarball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/tarball/v2.0.7",
113
- "zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v2.0.7",
114
- "body": "**Full Changelog**: https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/compare/v2.0.6...v2.0.7"
112
+ "tarball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/tarball/v2.7.0",
113
+ "zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v2.7.0",
114
+ "body": ""
115
115
  }
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import tempfile
6
+ import time
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Literal
11
+
12
+
13
+ @dataclass
14
+ class FailureArtifactsOptions:
15
+ buffer_seconds: float = 15.0
16
+ capture_on_action: bool = True
17
+ fps: float = 0.0
18
+ persist_mode: Literal["onFail", "always"] = "onFail"
19
+ output_dir: str = ".sentience/artifacts"
20
+ on_before_persist: Callable[[RedactionContext], RedactionResult] | None = None
21
+ redact_snapshot_values: bool = True
22
+
23
+
24
+ @dataclass
25
+ class RedactionContext:
26
+ run_id: str
27
+ reason: str | None
28
+ status: Literal["failure", "success"]
29
+ snapshot: Any | None
30
+ diagnostics: Any | None
31
+ frame_paths: list[str]
32
+ metadata: dict[str, Any]
33
+
34
+
35
+ @dataclass
36
+ class RedactionResult:
37
+ snapshot: Any | None = None
38
+ diagnostics: Any | None = None
39
+ frame_paths: list[str] | None = None
40
+ drop_frames: bool = False
41
+
42
+
43
+ @dataclass
44
+ class _FrameRecord:
45
+ ts: float
46
+ file_name: str
47
+ path: Path
48
+
49
+
50
+ class FailureArtifactBuffer:
51
+ """
52
+ Ring buffer of screenshots with minimal persistence on failure.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ *,
58
+ run_id: str,
59
+ options: FailureArtifactsOptions,
60
+ time_fn: Callable[[], float] = time.time,
61
+ ) -> None:
62
+ self.run_id = run_id
63
+ self.options = options
64
+ self._time_fn = time_fn
65
+ self._temp_dir = Path(tempfile.mkdtemp(prefix="sentience-artifacts-"))
66
+ self._frames_dir = self._temp_dir / "frames"
67
+ self._frames_dir.mkdir(parents=True, exist_ok=True)
68
+ self._frames: list[_FrameRecord] = []
69
+ self._steps: list[dict] = []
70
+ self._persisted = False
71
+
72
+ @property
73
+ def temp_dir(self) -> Path:
74
+ return self._temp_dir
75
+
76
+ def record_step(
77
+ self,
78
+ *,
79
+ action: str,
80
+ step_id: str | None,
81
+ step_index: int | None,
82
+ url: str | None,
83
+ ) -> None:
84
+ self._steps.append(
85
+ {
86
+ "ts": self._time_fn(),
87
+ "action": action,
88
+ "step_id": step_id,
89
+ "step_index": step_index,
90
+ "url": url,
91
+ }
92
+ )
93
+
94
+ def add_frame(self, image_bytes: bytes, *, fmt: str = "png") -> None:
95
+ ts = self._time_fn()
96
+ file_name = f"frame_{int(ts * 1000)}.{fmt}"
97
+ path = self._frames_dir / file_name
98
+ path.write_bytes(image_bytes)
99
+ self._frames.append(_FrameRecord(ts=ts, file_name=file_name, path=path))
100
+ self._prune()
101
+
102
+ def frame_count(self) -> int:
103
+ return len(self._frames)
104
+
105
+ def _prune(self) -> None:
106
+ cutoff = self._time_fn() - max(0.0, self.options.buffer_seconds)
107
+ keep: list[_FrameRecord] = []
108
+ for frame in self._frames:
109
+ if frame.ts >= cutoff:
110
+ keep.append(frame)
111
+ else:
112
+ try:
113
+ frame.path.unlink(missing_ok=True)
114
+ except Exception:
115
+ pass
116
+ self._frames = keep
117
+
118
+ def _write_json_atomic(self, path: Path, data: Any) -> None:
119
+ tmp_path = path.with_suffix(path.suffix + ".tmp")
120
+ tmp_path.write_text(json.dumps(data, indent=2))
121
+ tmp_path.replace(path)
122
+
123
+ def _redact_snapshot_defaults(self, payload: Any) -> Any:
124
+ if not isinstance(payload, dict):
125
+ return payload
126
+ elements = payload.get("elements")
127
+ if not isinstance(elements, list):
128
+ return payload
129
+ redacted = []
130
+ for el in elements:
131
+ if not isinstance(el, dict):
132
+ redacted.append(el)
133
+ continue
134
+ input_type = (el.get("input_type") or "").lower()
135
+ if input_type in {"password", "email", "tel"} and "value" in el:
136
+ el = dict(el)
137
+ el["value"] = None
138
+ el["value_redacted"] = True
139
+ redacted.append(el)
140
+ payload = dict(payload)
141
+ payload["elements"] = redacted
142
+ return payload
143
+
144
+ def persist(
145
+ self,
146
+ *,
147
+ reason: str | None,
148
+ status: Literal["failure", "success"],
149
+ snapshot: Any | None = None,
150
+ diagnostics: Any | None = None,
151
+ metadata: dict[str, Any] | None = None,
152
+ ) -> Path | None:
153
+ if self._persisted:
154
+ return None
155
+
156
+ output_dir = Path(self.options.output_dir)
157
+ output_dir.mkdir(parents=True, exist_ok=True)
158
+ ts = int(self._time_fn() * 1000)
159
+ run_dir = output_dir / f"{self.run_id}-{ts}"
160
+ frames_out = run_dir / "frames"
161
+ frames_out.mkdir(parents=True, exist_ok=True)
162
+
163
+ snapshot_payload = None
164
+ if snapshot is not None:
165
+ if hasattr(snapshot, "model_dump"):
166
+ snapshot_payload = snapshot.model_dump()
167
+ else:
168
+ snapshot_payload = snapshot
169
+ if self.options.redact_snapshot_values:
170
+ snapshot_payload = self._redact_snapshot_defaults(snapshot_payload)
171
+
172
+ diagnostics_payload = None
173
+ if diagnostics is not None:
174
+ if hasattr(diagnostics, "model_dump"):
175
+ diagnostics_payload = diagnostics.model_dump()
176
+ else:
177
+ diagnostics_payload = diagnostics
178
+
179
+ frame_paths = [str(frame.path) for frame in self._frames]
180
+ drop_frames = False
181
+
182
+ if self.options.on_before_persist is not None:
183
+ try:
184
+ result = self.options.on_before_persist(
185
+ RedactionContext(
186
+ run_id=self.run_id,
187
+ reason=reason,
188
+ status=status,
189
+ snapshot=snapshot_payload,
190
+ diagnostics=diagnostics_payload,
191
+ frame_paths=frame_paths,
192
+ metadata=metadata or {},
193
+ )
194
+ )
195
+ if result.snapshot is not None:
196
+ snapshot_payload = result.snapshot
197
+ if result.diagnostics is not None:
198
+ diagnostics_payload = result.diagnostics
199
+ if result.frame_paths is not None:
200
+ frame_paths = result.frame_paths
201
+ drop_frames = result.drop_frames
202
+ except Exception:
203
+ drop_frames = True
204
+
205
+ if not drop_frames:
206
+ for frame_path in frame_paths:
207
+ src = Path(frame_path)
208
+ if not src.exists():
209
+ continue
210
+ shutil.copy2(src, frames_out / src.name)
211
+
212
+ self._write_json_atomic(run_dir / "steps.json", self._steps)
213
+ if snapshot_payload is not None:
214
+ self._write_json_atomic(run_dir / "snapshot.json", snapshot_payload)
215
+ if diagnostics_payload is not None:
216
+ self._write_json_atomic(run_dir / "diagnostics.json", diagnostics_payload)
217
+
218
+ manifest = {
219
+ "run_id": self.run_id,
220
+ "created_at_ms": ts,
221
+ "status": status,
222
+ "reason": reason,
223
+ "buffer_seconds": self.options.buffer_seconds,
224
+ "frame_count": 0 if drop_frames else len(frame_paths),
225
+ "frames": (
226
+ [] if drop_frames else [{"file": Path(p).name, "ts": None} for p in frame_paths]
227
+ ),
228
+ "snapshot": "snapshot.json" if snapshot_payload is not None else None,
229
+ "diagnostics": "diagnostics.json" if diagnostics_payload is not None else None,
230
+ "metadata": metadata or {},
231
+ "frames_redacted": not drop_frames and self.options.on_before_persist is not None,
232
+ "frames_dropped": drop_frames,
233
+ }
234
+ self._write_json_atomic(run_dir / "manifest.json", manifest)
235
+
236
+ self._persisted = True
237
+ return run_dir
238
+
239
+ def cleanup(self) -> None:
240
+ if self._temp_dir.exists():
241
+ shutil.rmtree(self._temp_dir, ignore_errors=True)
sentience/formatting.py CHANGED
@@ -1,59 +1,15 @@
1
1
  """
2
2
  Snapshot formatting utilities for LLM prompts.
3
3
 
4
- Provides functions to convert Sentience snapshots into text format suitable
5
- for LLM consumption.
6
- """
7
-
8
- from typing import List
9
-
10
- from .models import Snapshot
11
-
12
-
13
- def format_snapshot_for_llm(snap: Snapshot, limit: int = 50) -> str:
14
- """
15
- Convert snapshot elements to text format for LLM consumption.
16
-
17
- This is the canonical way Sentience formats DOM state for LLMs.
18
- The format includes element ID, role, text preview, visual cues,
19
- position, and importance score.
4
+ DEPRECATED: This module is maintained for backward compatibility only.
5
+ New code should import from sentience.utils.formatting or sentience directly:
20
6
 
21
- Args:
22
- snap: Snapshot object with elements
23
- limit: Maximum number of elements to include (default: 50)
24
-
25
- Returns:
26
- Formatted string with one element per line
27
-
28
- Example:
29
- >>> snap = snapshot(browser)
30
- >>> formatted = format_snapshot_for_llm(snap, limit=10)
31
- >>> print(formatted)
32
- [1] <button> "Sign In" {PRIMARY,CLICKABLE} @ (100,50) (Imp:10)
33
- [2] <input> "Email address" @ (100,100) (Imp:8)
34
- ...
35
- """
36
- lines: list[str] = []
37
-
38
- for el in snap.elements[:limit]:
39
- # Build visual cues string
40
- cues = []
41
- if getattr(el.visual_cues, "is_primary", False):
42
- cues.append("PRIMARY")
43
- if getattr(el.visual_cues, "is_clickable", False):
44
- cues.append("CLICKABLE")
45
-
46
- cues_str = f" {{{','.join(cues)}}}" if cues else ""
47
-
48
- # Format text preview (truncate to 50 chars)
49
- text_preview = el.text or ""
50
- if len(text_preview) > 50:
51
- text_preview = text_preview[:50] + "..."
7
+ from sentience.utils.formatting import format_snapshot_for_llm
8
+ # or
9
+ from sentience import format_snapshot_for_llm
10
+ """
52
11
 
53
- # Build element line: [ID] <role> "text" {cues} @ (x,y) (Imp:score)
54
- lines.append(
55
- f'[{el.id}] <{el.role}> "{text_preview}"{cues_str} '
56
- f"@ ({int(el.bbox.x)},{int(el.bbox.y)}) (Imp:{el.importance})"
57
- )
12
+ # Re-export from new location for backward compatibility
13
+ from .utils.formatting import format_snapshot_for_llm
58
14
 
59
- return "\n".join(lines)
15
+ __all__ = ["format_snapshot_for_llm"]
sentience/inspector.py CHANGED
@@ -1,8 +1,10 @@
1
+ from typing import Optional
2
+
1
3
  """
2
4
  Inspector tool - helps developers see what the agent "sees"
3
5
  """
4
6
 
5
- from .browser import SentienceBrowser
7
+ from .browser import AsyncSentienceBrowser, SentienceBrowser
6
8
 
7
9
 
8
10
  class Inspector:
@@ -183,3 +185,183 @@ def inspect(browser: SentienceBrowser) -> Inspector:
183
185
  Inspector instance
184
186
  """
185
187
  return Inspector(browser)
188
+
189
+
190
+ class InspectorAsync:
191
+ """Inspector for debugging - shows element info on hover/click (async)"""
192
+
193
+ def __init__(self, browser: AsyncSentienceBrowser):
194
+ self.browser = browser
195
+ self._active = False
196
+ self._last_element_id: int | None = None
197
+
198
+ async def start(self) -> None:
199
+ """Start inspection mode - prints element info on mouse move/click (async)"""
200
+ if not self.browser.page:
201
+ raise RuntimeError("Browser not started. Call await browser.start() first.")
202
+
203
+ self._active = True
204
+
205
+ # Inject inspector script into page
206
+ await self.browser.page.evaluate(
207
+ """
208
+ (() => {
209
+ // Remove existing inspector if any
210
+ if (window.__sentience_inspector_active) {
211
+ return;
212
+ }
213
+
214
+ window.__sentience_inspector_active = true;
215
+ window.__sentience_last_element_id = null;
216
+
217
+ // Get element at point
218
+ function getElementAtPoint(x, y) {
219
+ const el = document.elementFromPoint(x, y);
220
+ if (!el) return null;
221
+
222
+ // Find element in registry
223
+ if (window.sentience_registry) {
224
+ for (let i = 0; i < window.sentience_registry.length; i++) {
225
+ if (window.sentience_registry[i] === el) {
226
+ return i;
227
+ }
228
+ }
229
+ }
230
+ return null;
231
+ }
232
+
233
+ // Mouse move handler
234
+ function handleMouseMove(e) {
235
+ if (!window.__sentience_inspector_active) return;
236
+
237
+ const elementId = getElementAtPoint(e.clientX, e.clientY);
238
+ if (elementId === null || elementId === window.__sentience_last_element_id) {
239
+ return;
240
+ }
241
+
242
+ window.__sentience_last_element_id = elementId;
243
+
244
+ // Get element info from snapshot if available
245
+ if (window.sentience && window.sentience_registry) {
246
+ const el = window.sentience_registry[elementId];
247
+ if (el) {
248
+ const rect = el.getBoundingClientRect();
249
+ const text = el.getAttribute('aria-label') ||
250
+ el.value ||
251
+ el.placeholder ||
252
+ el.alt ||
253
+ (el.innerText || '').substring(0, 50);
254
+
255
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
256
+
257
+ console.log(`[Sentience Inspector] Element #${elementId}: role=${role}, text="${text}", bbox=(${Math.round(rect.x)}, ${Math.round(rect.y)}, ${Math.round(rect.width)}, ${Math.round(rect.height)})`);
258
+ }
259
+ }
260
+ }
261
+
262
+ // Click handler
263
+ function handleClick(e) {
264
+ if (!window.__sentience_inspector_active) return;
265
+
266
+ e.preventDefault();
267
+ e.stopPropagation();
268
+
269
+ const elementId = getElementAtPoint(e.clientX, e.clientY);
270
+ if (elementId === null) return;
271
+
272
+ // Get full element info
273
+ if (window.sentience && window.sentience_registry) {
274
+ const el = window.sentience_registry[elementId];
275
+ if (el) {
276
+ const rect = el.getBoundingClientRect();
277
+ const info = {
278
+ id: elementId,
279
+ tag: el.tagName.toLowerCase(),
280
+ role: el.getAttribute('role') || 'generic',
281
+ text: el.getAttribute('aria-label') ||
282
+ el.value ||
283
+ el.placeholder ||
284
+ el.alt ||
285
+ (el.innerText || '').substring(0, 100),
286
+ bbox: {
287
+ x: Math.round(rect.x),
288
+ y: Math.round(rect.y),
289
+ width: Math.round(rect.width),
290
+ height: Math.round(rect.height)
291
+ },
292
+ attributes: {
293
+ id: el.id || null,
294
+ class: el.className || null,
295
+ name: el.name || null,
296
+ type: el.type || null
297
+ }
298
+ };
299
+
300
+ console.log('[Sentience Inspector] Clicked element:', JSON.stringify(info, null, 2));
301
+
302
+ // Also try to get from snapshot if available
303
+ window.sentience.snapshot({ limit: 100 }).then(snap => {
304
+ const element = snap.elements.find(el => el.id === elementId);
305
+ if (element) {
306
+ console.log('[Sentience Inspector] Snapshot element:', JSON.stringify(element, null, 2));
307
+ }
308
+ }).catch(() => {});
309
+ }
310
+ }
311
+ }
312
+
313
+ // Add event listeners
314
+ document.addEventListener('mousemove', handleMouseMove, true);
315
+ document.addEventListener('click', handleClick, true);
316
+
317
+ // Store cleanup function
318
+ window.__sentience_inspector_cleanup = () => {
319
+ document.removeEventListener('mousemove', handleMouseMove, true);
320
+ document.removeEventListener('click', handleClick, true);
321
+ window.__sentience_inspector_active = false;
322
+ };
323
+
324
+ console.log('[Sentience Inspector] ✅ Inspection mode active. Hover elements to see info, click to see full details.');
325
+ })();
326
+ """
327
+ )
328
+
329
+ async def stop(self) -> None:
330
+ """Stop inspection mode (async)"""
331
+ if not self.browser.page:
332
+ return
333
+
334
+ self._active = False
335
+
336
+ # Cleanup inspector
337
+ await self.browser.page.evaluate(
338
+ """
339
+ () => {
340
+ if (window.__sentience_inspector_cleanup) {
341
+ window.__sentience_inspector_cleanup();
342
+ }
343
+ }
344
+ """
345
+ )
346
+
347
+ async def __aenter__(self):
348
+ """Context manager entry"""
349
+ await self.start()
350
+ return self
351
+
352
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
353
+ """Context manager exit"""
354
+ await self.stop()
355
+
356
+
357
+ def inspect_async(browser: AsyncSentienceBrowser) -> InspectorAsync:
358
+ """
359
+ Create an inspector instance (async)
360
+
361
+ Args:
362
+ browser: AsyncSentienceBrowser instance
363
+
364
+ Returns:
365
+ InspectorAsync instance
366
+ """
367
+ return InspectorAsync(browser)
@@ -0,0 +1,6 @@
1
+ """
2
+ Integrations package (internal).
3
+
4
+ This package is intended for framework integrations (e.g., PydanticAI, LangChain/LangGraph).
5
+ Public APIs should be introduced deliberately once the integration surface is stable.
6
+ """
@@ -0,0 +1,12 @@
1
+ """
2
+ LangChain / LangGraph integration helpers (optional).
3
+
4
+ This package is designed so the base SDK can be imported without LangChain installed.
5
+ All LangChain imports are done lazily inside tool-builder functions.
6
+ """
7
+
8
+ from .context import SentienceLangChainContext
9
+ from .core import SentienceLangChainCore
10
+ from .tools import build_sentience_langchain_tools
11
+
12
+ __all__ = ["SentienceLangChainContext", "SentienceLangChainCore", "build_sentience_langchain_tools"]