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
sentience/tracing.py
CHANGED
|
@@ -4,12 +4,16 @@ Trace event writer for Sentience agents.
|
|
|
4
4
|
Provides abstract interface and JSONL implementation for emitting trace events.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import json
|
|
8
7
|
import time
|
|
9
8
|
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import Callable
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from .models import TraceStats
|
|
16
|
+
from .trace_file_manager import TraceFileManager
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
@dataclass
|
|
@@ -87,7 +91,7 @@ class JsonlTraceSink(TraceSink):
|
|
|
87
91
|
path: File path to write traces to
|
|
88
92
|
"""
|
|
89
93
|
self.path = Path(path)
|
|
90
|
-
self.path
|
|
94
|
+
TraceFileManager.ensure_directory(self.path)
|
|
91
95
|
|
|
92
96
|
# Open file in append mode with line buffering
|
|
93
97
|
self._file = open(self.path, "a", encoding="utf-8", buffering=1)
|
|
@@ -99,8 +103,7 @@ class JsonlTraceSink(TraceSink):
|
|
|
99
103
|
Args:
|
|
100
104
|
event: Event dictionary
|
|
101
105
|
"""
|
|
102
|
-
|
|
103
|
-
self._file.write(json_str + "\n")
|
|
106
|
+
TraceFileManager.write_event(self._file, event)
|
|
104
107
|
|
|
105
108
|
def close(self) -> None:
|
|
106
109
|
"""Close the file and generate index."""
|
|
@@ -110,12 +113,36 @@ class JsonlTraceSink(TraceSink):
|
|
|
110
113
|
# Generate index after closing file
|
|
111
114
|
self._generate_index()
|
|
112
115
|
|
|
116
|
+
def get_stats(self) -> TraceStats:
|
|
117
|
+
"""
|
|
118
|
+
Extract execution statistics from trace file (for local traces).
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
TraceStats with execution statistics
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
# Read trace file to extract stats
|
|
125
|
+
events = TraceFileManager.read_events(self.path)
|
|
126
|
+
return TraceFileManager.extract_stats(events)
|
|
127
|
+
except Exception:
|
|
128
|
+
return TraceStats(
|
|
129
|
+
total_steps=0,
|
|
130
|
+
total_events=0,
|
|
131
|
+
duration_ms=None,
|
|
132
|
+
final_status="unknown",
|
|
133
|
+
started_at=None,
|
|
134
|
+
ended_at=None,
|
|
135
|
+
)
|
|
136
|
+
|
|
113
137
|
def _generate_index(self) -> None:
|
|
114
138
|
"""Generate trace index file (automatic on close)."""
|
|
115
139
|
try:
|
|
116
140
|
from .trace_indexing import write_trace_index
|
|
117
141
|
|
|
118
|
-
|
|
142
|
+
# Use frontend format to ensure 'step' field is present (1-based)
|
|
143
|
+
# Frontend derives sequence from step.step - 1, so step must be valid
|
|
144
|
+
index_path = Path(self.path).with_suffix(".index.json")
|
|
145
|
+
write_trace_index(str(self.path), str(index_path), frontend_format=True)
|
|
119
146
|
except Exception as e:
|
|
120
147
|
# Non-fatal: log but don't crash
|
|
121
148
|
print(f"⚠️ Failed to generate trace index: {e}")
|
|
@@ -136,11 +163,48 @@ class Tracer:
|
|
|
136
163
|
Trace event builder and emitter.
|
|
137
164
|
|
|
138
165
|
Manages sequence numbers and provides convenient methods for emitting events.
|
|
166
|
+
Tracks execution statistics and final status for trace completion.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
run_id: Unique identifier for this trace run
|
|
170
|
+
sink: TraceSink implementation for writing events
|
|
171
|
+
screenshot_processor: Optional function to process screenshots before emission.
|
|
172
|
+
Takes base64 string, returns processed base64 string.
|
|
173
|
+
Useful for PII redaction or custom image processing.
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
>>> from sentience import Tracer, JsonlTraceSink
|
|
177
|
+
>>>
|
|
178
|
+
>>> # Basic usage
|
|
179
|
+
>>> sink = JsonlTraceSink("trace.jsonl")
|
|
180
|
+
>>> tracer = Tracer(run_id="abc123", sink=sink)
|
|
181
|
+
>>>
|
|
182
|
+
>>> # With screenshot processor for PII redaction
|
|
183
|
+
>>> def redact_pii(screenshot_base64: str) -> str:
|
|
184
|
+
... # Your custom redaction logic
|
|
185
|
+
... return redacted_screenshot
|
|
186
|
+
>>>
|
|
187
|
+
>>> tracer = Tracer(
|
|
188
|
+
... run_id="abc123",
|
|
189
|
+
... sink=sink,
|
|
190
|
+
... screenshot_processor=redact_pii
|
|
191
|
+
... )
|
|
139
192
|
"""
|
|
140
193
|
|
|
141
194
|
run_id: str
|
|
142
195
|
sink: TraceSink
|
|
196
|
+
screenshot_processor: Callable[[str], str] | None = None
|
|
143
197
|
seq: int = field(default=0, init=False)
|
|
198
|
+
# Stats tracking
|
|
199
|
+
total_steps: int = field(default=0, init=False)
|
|
200
|
+
total_events: int = field(default=0, init=False)
|
|
201
|
+
started_at: datetime | None = field(default=None, init=False)
|
|
202
|
+
ended_at: datetime | None = field(default=None, init=False)
|
|
203
|
+
final_status: str = field(default="unknown", init=False)
|
|
204
|
+
# Track step outcomes for automatic status inference
|
|
205
|
+
_step_successes: int = field(default=0, init=False)
|
|
206
|
+
_step_failures: int = field(default=0, init=False)
|
|
207
|
+
_has_errors: bool = field(default=False, init=False)
|
|
144
208
|
|
|
145
209
|
def emit(
|
|
146
210
|
self,
|
|
@@ -157,6 +221,12 @@ class Tracer:
|
|
|
157
221
|
step_id: Step UUID (if step-scoped event)
|
|
158
222
|
"""
|
|
159
223
|
self.seq += 1
|
|
224
|
+
self.total_events += 1
|
|
225
|
+
|
|
226
|
+
# Apply screenshot processor if configured and screenshot is present
|
|
227
|
+
if self.screenshot_processor and "screenshot_base64" in data:
|
|
228
|
+
data = data.copy() # Don't modify the original dict
|
|
229
|
+
data["screenshot_base64"] = self.screenshot_processor(data["screenshot_base64"])
|
|
160
230
|
|
|
161
231
|
# Generate timestamps
|
|
162
232
|
ts_ms = int(time.time() * 1000)
|
|
@@ -175,6 +245,16 @@ class Tracer:
|
|
|
175
245
|
|
|
176
246
|
self.sink.emit(event.to_dict())
|
|
177
247
|
|
|
248
|
+
# Track step outcomes for automatic status inference
|
|
249
|
+
if event_type == "step_end":
|
|
250
|
+
success = data.get("success", False)
|
|
251
|
+
if success:
|
|
252
|
+
self._step_successes += 1
|
|
253
|
+
else:
|
|
254
|
+
self._step_failures += 1
|
|
255
|
+
elif event_type == "error":
|
|
256
|
+
self._has_errors = True
|
|
257
|
+
|
|
178
258
|
def emit_run_start(
|
|
179
259
|
self,
|
|
180
260
|
agent: str,
|
|
@@ -189,6 +269,9 @@ class Tracer:
|
|
|
189
269
|
llm_model: LLM model name
|
|
190
270
|
config: Agent configuration
|
|
191
271
|
"""
|
|
272
|
+
# Track start time
|
|
273
|
+
self.started_at = datetime.utcnow()
|
|
274
|
+
|
|
192
275
|
data: dict[str, Any] = {"agent": agent}
|
|
193
276
|
if llm_model is not None:
|
|
194
277
|
data["llm_model"] = llm_model
|
|
@@ -215,6 +298,10 @@ class Tracer:
|
|
|
215
298
|
attempt: Attempt number (0-indexed)
|
|
216
299
|
pre_url: URL before step
|
|
217
300
|
"""
|
|
301
|
+
# Track step count (only count first attempt of each step)
|
|
302
|
+
if attempt == 0:
|
|
303
|
+
self.total_steps = max(self.total_steps, step_index)
|
|
304
|
+
|
|
218
305
|
data = {
|
|
219
306
|
"step_id": step_id,
|
|
220
307
|
"step_index": step_index,
|
|
@@ -226,14 +313,29 @@ class Tracer:
|
|
|
226
313
|
|
|
227
314
|
self.emit("step_start", data, step_id=step_id)
|
|
228
315
|
|
|
229
|
-
def emit_run_end(self, steps: int) -> None:
|
|
316
|
+
def emit_run_end(self, steps: int, status: str | None = None) -> None:
|
|
230
317
|
"""
|
|
231
318
|
Emit run_end event.
|
|
232
319
|
|
|
233
320
|
Args:
|
|
234
321
|
steps: Total number of steps executed
|
|
322
|
+
status: Optional final status ("success", "failure", "partial", "unknown")
|
|
323
|
+
If not provided, infers from tracked outcomes or uses self.final_status
|
|
235
324
|
"""
|
|
236
|
-
|
|
325
|
+
# Track end time
|
|
326
|
+
self.ended_at = datetime.utcnow()
|
|
327
|
+
|
|
328
|
+
# Auto-infer status if not provided and not explicitly set
|
|
329
|
+
if status is None and self.final_status == "unknown":
|
|
330
|
+
self._infer_final_status()
|
|
331
|
+
|
|
332
|
+
# Use provided status or fallback to self.final_status
|
|
333
|
+
final_status = status if status is not None else self.final_status
|
|
334
|
+
|
|
335
|
+
# Ensure total_steps is at least the provided steps value
|
|
336
|
+
self.total_steps = max(self.total_steps, steps)
|
|
337
|
+
|
|
338
|
+
self.emit("run_end", {"steps": steps, "status": final_status})
|
|
237
339
|
|
|
238
340
|
def emit_error(
|
|
239
341
|
self,
|
|
@@ -256,6 +358,62 @@ class Tracer:
|
|
|
256
358
|
}
|
|
257
359
|
self.emit("error", data, step_id=step_id)
|
|
258
360
|
|
|
361
|
+
def set_final_status(self, status: str) -> None:
|
|
362
|
+
"""
|
|
363
|
+
Set the final status of the trace run.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
status: Final status ("success", "failure", "partial", "unknown")
|
|
367
|
+
"""
|
|
368
|
+
if status not in ("success", "failure", "partial", "unknown"):
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"Invalid status: {status}. Must be one of: success, failure, partial, unknown"
|
|
371
|
+
)
|
|
372
|
+
self.final_status = status
|
|
373
|
+
|
|
374
|
+
def get_stats(self) -> TraceStats:
|
|
375
|
+
"""
|
|
376
|
+
Get execution statistics for trace completion.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
TraceStats with execution statistics
|
|
380
|
+
"""
|
|
381
|
+
duration_ms: int | None = None
|
|
382
|
+
if self.started_at and self.ended_at:
|
|
383
|
+
delta = self.ended_at - self.started_at
|
|
384
|
+
duration_ms = int(delta.total_seconds() * 1000)
|
|
385
|
+
|
|
386
|
+
return TraceStats(
|
|
387
|
+
total_steps=self.total_steps,
|
|
388
|
+
total_events=self.total_events,
|
|
389
|
+
duration_ms=duration_ms,
|
|
390
|
+
final_status=self.final_status,
|
|
391
|
+
started_at=self.started_at.isoformat() + "Z" if self.started_at else None,
|
|
392
|
+
ended_at=self.ended_at.isoformat() + "Z" if self.ended_at else None,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def _infer_final_status(self) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Automatically infer final_status from tracked step outcomes if not explicitly set.
|
|
398
|
+
|
|
399
|
+
This is called automatically in close() if final_status is still "unknown".
|
|
400
|
+
"""
|
|
401
|
+
if self.final_status != "unknown":
|
|
402
|
+
# Status already set explicitly, don't override
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
# Infer from tracked outcomes
|
|
406
|
+
if self._has_errors:
|
|
407
|
+
# Has errors - check if there were successful steps too
|
|
408
|
+
if self._step_successes > 0:
|
|
409
|
+
self.final_status = "partial"
|
|
410
|
+
else:
|
|
411
|
+
self.final_status = "failure"
|
|
412
|
+
elif self._step_successes > 0:
|
|
413
|
+
# Has successful steps and no errors
|
|
414
|
+
self.final_status = "success"
|
|
415
|
+
# Otherwise stays "unknown" (no steps executed or no clear outcome)
|
|
416
|
+
|
|
259
417
|
def close(self, **kwargs) -> None:
|
|
260
418
|
"""
|
|
261
419
|
Close the underlying sink.
|
|
@@ -263,6 +421,12 @@ class Tracer:
|
|
|
263
421
|
Args:
|
|
264
422
|
**kwargs: Passed through to sink.close() (e.g., blocking=True for CloudTraceSink)
|
|
265
423
|
"""
|
|
424
|
+
# Auto-infer final_status if not explicitly set and we have step outcomes
|
|
425
|
+
if self.final_status == "unknown" and (
|
|
426
|
+
self._step_successes > 0 or self._step_failures > 0 or self._has_errors
|
|
427
|
+
):
|
|
428
|
+
self._infer_final_status()
|
|
429
|
+
|
|
266
430
|
# Check if sink.close() accepts kwargs (CloudTraceSink does, JsonlTraceSink doesn't)
|
|
267
431
|
import inspect
|
|
268
432
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for Sentience SDK.
|
|
3
|
+
|
|
4
|
+
This module re-exports all utility functions from submodules for backward compatibility.
|
|
5
|
+
Users can continue using:
|
|
6
|
+
from sentience.utils import compute_snapshot_digests, canonical_snapshot_strict
|
|
7
|
+
from sentience import canonical_snapshot_strict, format_snapshot_for_llm
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Re-export all functions from submodules for backward compatibility
|
|
11
|
+
from .browser import save_storage_state
|
|
12
|
+
from .element import (
|
|
13
|
+
BBox,
|
|
14
|
+
ElementFingerprint,
|
|
15
|
+
canonical_snapshot_loose,
|
|
16
|
+
canonical_snapshot_strict,
|
|
17
|
+
compute_snapshot_digests,
|
|
18
|
+
extract_element_fingerprint,
|
|
19
|
+
normalize_bbox,
|
|
20
|
+
normalize_text_strict,
|
|
21
|
+
sha256_digest,
|
|
22
|
+
)
|
|
23
|
+
from .formatting import format_snapshot_for_llm
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Browser utilities
|
|
27
|
+
"save_storage_state",
|
|
28
|
+
# Element/digest utilities
|
|
29
|
+
"BBox",
|
|
30
|
+
"ElementFingerprint",
|
|
31
|
+
"canonical_snapshot_loose",
|
|
32
|
+
"canonical_snapshot_strict",
|
|
33
|
+
"compute_snapshot_digests",
|
|
34
|
+
"extract_element_fingerprint",
|
|
35
|
+
"normalize_bbox",
|
|
36
|
+
"normalize_text_strict",
|
|
37
|
+
"sha256_digest",
|
|
38
|
+
# Formatting utilities
|
|
39
|
+
"format_snapshot_for_llm",
|
|
40
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Browser-related utilities for Sentience SDK.
|
|
3
|
+
|
|
4
|
+
Provides functions for managing browser storage state (cookies, localStorage).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from playwright.sync_api import BrowserContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def save_storage_state(context: BrowserContext, file_path: str | Path) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Save current browser storage state (cookies + localStorage) to a file.
|
|
16
|
+
|
|
17
|
+
This is useful for capturing a logged-in session to reuse later.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
context: Playwright BrowserContext
|
|
21
|
+
file_path: Path to save the storage state JSON file
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
```python
|
|
25
|
+
from sentience import SentienceBrowser, save_storage_state
|
|
26
|
+
|
|
27
|
+
browser = SentienceBrowser()
|
|
28
|
+
browser.start()
|
|
29
|
+
|
|
30
|
+
# User logs in manually or via agent
|
|
31
|
+
browser.goto("https://example.com")
|
|
32
|
+
# ... login happens ...
|
|
33
|
+
|
|
34
|
+
# Save session for later
|
|
35
|
+
save_storage_state(browser.context, "auth.json")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
IOError: If file cannot be written
|
|
40
|
+
"""
|
|
41
|
+
storage_state = context.storage_state()
|
|
42
|
+
file_path_obj = Path(file_path)
|
|
43
|
+
file_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
with open(file_path_obj, "w") as f:
|
|
45
|
+
json.dump(storage_state, f, indent=2)
|
|
46
|
+
print(f"✅ [Sentience] Saved storage state to {file_path_obj}")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Element manipulation and digest utilities for Sentience SDK.
|
|
3
3
|
|
|
4
|
-
Provides functions to compute stable digests of snapshots for
|
|
4
|
+
Provides functions to compute stable digests of snapshots for deterministic diff.
|
|
5
5
|
Two digest strategies:
|
|
6
6
|
- strict: includes structure + normalized text
|
|
7
7
|
- loose: structure only (no text) - detects layout changes vs content changes
|
|
@@ -11,10 +11,7 @@ import hashlib
|
|
|
11
11
|
import json
|
|
12
12
|
import re
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
-
from
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
|
-
from playwright.sync_api import BrowserContext
|
|
14
|
+
from typing import Any, Optional
|
|
18
15
|
|
|
19
16
|
|
|
20
17
|
@dataclass
|
|
@@ -258,39 +255,3 @@ def compute_snapshot_digests(elements: list[dict[str, Any]]) -> dict[str, str]:
|
|
|
258
255
|
"strict": sha256_digest(canonical_strict),
|
|
259
256
|
"loose": sha256_digest(canonical_loose),
|
|
260
257
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def save_storage_state(context: BrowserContext, file_path: str | Path) -> None:
|
|
264
|
-
"""
|
|
265
|
-
Save current browser storage state (cookies + localStorage) to a file.
|
|
266
|
-
|
|
267
|
-
This is useful for capturing a logged-in session to reuse later.
|
|
268
|
-
|
|
269
|
-
Args:
|
|
270
|
-
context: Playwright BrowserContext
|
|
271
|
-
file_path: Path to save the storage state JSON file
|
|
272
|
-
|
|
273
|
-
Example:
|
|
274
|
-
```python
|
|
275
|
-
from sentience import SentienceBrowser, save_storage_state
|
|
276
|
-
|
|
277
|
-
browser = SentienceBrowser()
|
|
278
|
-
browser.start()
|
|
279
|
-
|
|
280
|
-
# User logs in manually or via agent
|
|
281
|
-
browser.goto("https://example.com")
|
|
282
|
-
# ... login happens ...
|
|
283
|
-
|
|
284
|
-
# Save session for later
|
|
285
|
-
save_storage_state(browser.context, "auth.json")
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
Raises:
|
|
289
|
-
IOError: If file cannot be written
|
|
290
|
-
"""
|
|
291
|
-
storage_state = context.storage_state()
|
|
292
|
-
file_path_obj = Path(file_path)
|
|
293
|
-
file_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
294
|
-
with open(file_path_obj, "w") as f:
|
|
295
|
-
json.dump(storage_state, f, indent=2)
|
|
296
|
-
print(f"✅ [Sentience] Saved storage state to {file_path_obj}")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Snapshot formatting utilities for LLM prompts.
|
|
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.
|
|
20
|
+
|
|
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] + "..."
|
|
52
|
+
|
|
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
|
+
)
|
|
58
|
+
|
|
59
|
+
return "\n".join(lines)
|