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.
- sentience/__init__.py +120 -6
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +217 -0
- sentience/actions.py +758 -30
- sentience/agent.py +806 -293
- sentience/agent_config.py +3 -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 +89 -1141
- 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/base_agent.py +95 -0
- sentience/browser.py +678 -39
- sentience/browser_evaluator.py +299 -0
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +507 -42
- sentience/constants.py +6 -0
- sentience/conversational_agent.py +77 -43
- sentience/cursor_policy.py +142 -0
- sentience/element_filter.py +136 -0
- sentience/expect.py +98 -2
- sentience/extension/background.js +56 -185
- sentience/extension/content.js +150 -287
- sentience/extension/injected_api.js +1088 -1368
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +275 -433
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +47 -47
- sentience/failure_artifacts.py +241 -0
- sentience/formatting.py +9 -53
- sentience/inspector.py +183 -1
- 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_interaction_handler.py +191 -0
- sentience/llm_provider.py +765 -66
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +595 -3
- sentience/ordinal.py +280 -0
- sentience/overlay.py +109 -2
- sentience/protocols.py +228 -0
- sentience/query.py +67 -5
- sentience/read.py +95 -3
- sentience/recorder.py +223 -3
- sentience/schemas/trace_v1.json +128 -9
- sentience/screenshot.py +48 -2
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +599 -55
- sentience/snapshot_diff.py +126 -0
- sentience/text_search.py +120 -5
- sentience/trace_event_builder.py +148 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/index_schema.py +95 -7
- sentience/trace_indexing/indexer.py +105 -48
- sentience/tracer_factory.py +120 -9
- sentience/tracing.py +172 -8
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/{utils.py → utils/element.py} +3 -42
- sentience/utils/formatting.py +59 -0
- sentience/verification.py +618 -0
- sentience/visual_agent.py +2058 -0
- sentience/wait.py +68 -2
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/extension/test-content.js +0 -4
- sentienceapi-0.90.16.dist-info/RECORD +0 -50
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
|
Binary file
|
sentience/extension/release.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
"url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/
|
|
3
|
-
"assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/
|
|
4
|
-
"upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/
|
|
5
|
-
"html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v2.0
|
|
6
|
-
"id":
|
|
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": "
|
|
9
|
-
"id":
|
|
10
|
-
"node_id": "
|
|
11
|
-
"avatar_url": "https://avatars.githubusercontent.com/
|
|
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/
|
|
14
|
-
"html_url": "https://github.com/
|
|
15
|
-
"followers_url": "https://api.github.com/users/
|
|
16
|
-
"following_url": "https://api.github.com/users/
|
|
17
|
-
"gists_url": "https://api.github.com/users/
|
|
18
|
-
"starred_url": "https://api.github.com/users/
|
|
19
|
-
"subscriptions_url": "https://api.github.com/users/
|
|
20
|
-
"organizations_url": "https://api.github.com/users/
|
|
21
|
-
"repos_url": "https://api.github.com/users/
|
|
22
|
-
"events_url": "https://api.github.com/users/
|
|
23
|
-
"received_events_url": "https://api.github.com/users/
|
|
24
|
-
"type": "
|
|
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": "
|
|
29
|
-
"tag_name": "v2.0
|
|
28
|
+
"node_id": "RE_kwDOQshiJ84Qju9i",
|
|
29
|
+
"tag_name": "v2.7.0",
|
|
30
30
|
"target_commitish": "main",
|
|
31
|
-
"name": "Release v2.0
|
|
31
|
+
"name": "Release v2.7.0",
|
|
32
32
|
"draft": false,
|
|
33
33
|
"immutable": false,
|
|
34
34
|
"prerelease": false,
|
|
35
|
-
"created_at": "
|
|
36
|
-
"updated_at": "
|
|
37
|
-
"published_at": "
|
|
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/
|
|
41
|
-
"id":
|
|
42
|
-
"node_id": "
|
|
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":
|
|
69
|
-
"digest": "sha256:
|
|
70
|
-
"download_count":
|
|
71
|
-
"created_at": "
|
|
72
|
-
"updated_at": "
|
|
73
|
-
"browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.0
|
|
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/
|
|
77
|
-
"id":
|
|
78
|
-
"node_id": "
|
|
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":
|
|
105
|
-
"digest": "sha256:
|
|
104
|
+
"size": 80692,
|
|
105
|
+
"digest": "sha256:83de4c4c54f401fc6404c27ef01a9205cf8e3566e81f62a7bc08f7103a2b0cc5",
|
|
106
106
|
"download_count": 0,
|
|
107
|
-
"created_at": "
|
|
108
|
-
"updated_at": "
|
|
109
|
-
"browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.0
|
|
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
|
|
113
|
-
"zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v2.0
|
|
114
|
-
"body": "
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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,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"]
|