sentienceapi 0.95.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 +253 -0
- sentience/_extension_loader.py +195 -0
- sentience/action_executor.py +215 -0
- sentience/actions.py +1020 -0
- sentience/agent.py +1181 -0
- sentience/agent_config.py +46 -0
- sentience/agent_runtime.py +424 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +108 -0
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +343 -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 +427 -0
- sentience/base_agent.py +196 -0
- sentience/browser.py +1215 -0
- sentience/browser_evaluator.py +299 -0
- sentience/canonicalization.py +207 -0
- sentience/cli.py +130 -0
- sentience/cloud_tracing.py +807 -0
- sentience/constants.py +6 -0
- sentience/conversational_agent.py +543 -0
- sentience/element_filter.py +136 -0
- sentience/expect.py +188 -0
- sentience/extension/background.js +104 -0
- sentience/extension/content.js +161 -0
- sentience/extension/injected_api.js +914 -0
- sentience/extension/manifest.json +36 -0
- sentience/extension/pkg/sentience_core.d.ts +51 -0
- sentience/extension/pkg/sentience_core.js +323 -0
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
- sentience/extension/release.json +115 -0
- sentience/formatting.py +15 -0
- sentience/generator.py +202 -0
- sentience/inspector.py +367 -0
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +875 -0
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +846 -0
- sentience/ordinal.py +280 -0
- sentience/overlay.py +222 -0
- sentience/protocols.py +228 -0
- sentience/query.py +303 -0
- sentience/read.py +188 -0
- sentience/recorder.py +589 -0
- sentience/schemas/trace_v1.json +335 -0
- sentience/screenshot.py +100 -0
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +706 -0
- sentience/snapshot_diff.py +126 -0
- sentience/text_search.py +262 -0
- sentience/trace_event_builder.py +148 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/__init__.py +27 -0
- sentience/trace_indexing/index_schema.py +199 -0
- sentience/trace_indexing/indexer.py +414 -0
- sentience/tracer_factory.py +322 -0
- sentience/tracing.py +449 -0
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/utils/element.py +257 -0
- sentience/utils/formatting.py +59 -0
- sentience/utils.py +296 -0
- sentience/verification.py +380 -0
- sentience/visual_agent.py +2058 -0
- sentience/wait.py +139 -0
- sentienceapi-0.95.0.dist-info/METADATA +984 -0
- sentienceapi-0.95.0.dist-info/RECORD +82 -0
- sentienceapi-0.95.0.dist-info/WHEEL +5 -0
- sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
- sentienceapi-0.95.0.dist-info/top_level.txt +1 -0
sentience/tracing.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace event writer for Sentience agents.
|
|
3
|
+
|
|
4
|
+
Provides abstract interface and JSONL implementation for emitting trace events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from .models import TraceStats
|
|
16
|
+
from .trace_file_manager import TraceFileManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TraceEvent:
|
|
21
|
+
"""
|
|
22
|
+
Trace event data structure.
|
|
23
|
+
|
|
24
|
+
Represents a single event in the agent execution trace.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
v: int # Schema version
|
|
28
|
+
type: str # Event type
|
|
29
|
+
ts: str # ISO 8601 timestamp
|
|
30
|
+
run_id: str # UUID for the run
|
|
31
|
+
seq: int # Sequence number
|
|
32
|
+
data: dict[str, Any] # Event payload
|
|
33
|
+
step_id: str | None = None # UUID for the step (if step-scoped)
|
|
34
|
+
ts_ms: int | None = None # Unix timestamp in milliseconds
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict[str, Any]:
|
|
37
|
+
"""Convert to dictionary for JSON serialization."""
|
|
38
|
+
result = {
|
|
39
|
+
"v": self.v,
|
|
40
|
+
"type": self.type,
|
|
41
|
+
"ts": self.ts,
|
|
42
|
+
"run_id": self.run_id,
|
|
43
|
+
"seq": self.seq,
|
|
44
|
+
"data": self.data,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if self.step_id is not None:
|
|
48
|
+
result["step_id"] = self.step_id
|
|
49
|
+
|
|
50
|
+
if self.ts_ms is not None:
|
|
51
|
+
result["ts_ms"] = self.ts_ms
|
|
52
|
+
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TraceSink(ABC):
|
|
57
|
+
"""
|
|
58
|
+
Abstract interface for trace event sink.
|
|
59
|
+
|
|
60
|
+
Implementations can write to files, databases, or remote services.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def emit(self, event: dict[str, Any]) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Emit a trace event.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
event: Event dictionary (from TraceEvent.to_dict())
|
|
70
|
+
"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
"""Close the sink and flush any buffered data."""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class JsonlTraceSink(TraceSink):
|
|
80
|
+
"""
|
|
81
|
+
JSONL file sink for trace events.
|
|
82
|
+
|
|
83
|
+
Writes one JSON object per line to a file.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, path: str | Path):
|
|
87
|
+
"""
|
|
88
|
+
Initialize JSONL sink.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
path: File path to write traces to
|
|
92
|
+
"""
|
|
93
|
+
self.path = Path(path)
|
|
94
|
+
TraceFileManager.ensure_directory(self.path)
|
|
95
|
+
|
|
96
|
+
# Open file in append mode with line buffering
|
|
97
|
+
self._file = open(self.path, "a", encoding="utf-8", buffering=1)
|
|
98
|
+
|
|
99
|
+
def emit(self, event: dict[str, Any]) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Emit event as JSONL line.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
event: Event dictionary
|
|
105
|
+
"""
|
|
106
|
+
TraceFileManager.write_event(self._file, event)
|
|
107
|
+
|
|
108
|
+
def close(self) -> None:
|
|
109
|
+
"""Close the file and generate index."""
|
|
110
|
+
if hasattr(self, "_file") and not self._file.closed:
|
|
111
|
+
self._file.close()
|
|
112
|
+
|
|
113
|
+
# Generate index after closing file
|
|
114
|
+
self._generate_index()
|
|
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
|
+
|
|
137
|
+
def _generate_index(self) -> None:
|
|
138
|
+
"""Generate trace index file (automatic on close)."""
|
|
139
|
+
try:
|
|
140
|
+
from .trace_indexing import write_trace_index
|
|
141
|
+
|
|
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)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
# Non-fatal: log but don't crash
|
|
148
|
+
print(f"⚠️ Failed to generate trace index: {e}")
|
|
149
|
+
|
|
150
|
+
def __enter__(self):
|
|
151
|
+
"""Context manager support."""
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
155
|
+
"""Context manager cleanup."""
|
|
156
|
+
self.close()
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class Tracer:
|
|
162
|
+
"""
|
|
163
|
+
Trace event builder and emitter.
|
|
164
|
+
|
|
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
|
+
... )
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
run_id: str
|
|
195
|
+
sink: TraceSink
|
|
196
|
+
screenshot_processor: Callable[[str], str] | None = None
|
|
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)
|
|
208
|
+
|
|
209
|
+
def emit(
|
|
210
|
+
self,
|
|
211
|
+
event_type: str,
|
|
212
|
+
data: dict[str, Any],
|
|
213
|
+
step_id: str | None = None,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""
|
|
216
|
+
Emit a trace event.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
event_type: Type of event (e.g., 'run_start', 'step_end')
|
|
220
|
+
data: Event-specific payload
|
|
221
|
+
step_id: Step UUID (if step-scoped event)
|
|
222
|
+
"""
|
|
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"])
|
|
230
|
+
|
|
231
|
+
# Generate timestamps
|
|
232
|
+
ts_ms = int(time.time() * 1000)
|
|
233
|
+
ts = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())
|
|
234
|
+
|
|
235
|
+
event = TraceEvent(
|
|
236
|
+
v=1,
|
|
237
|
+
type=event_type,
|
|
238
|
+
ts=ts,
|
|
239
|
+
ts_ms=ts_ms,
|
|
240
|
+
run_id=self.run_id,
|
|
241
|
+
seq=self.seq,
|
|
242
|
+
step_id=step_id,
|
|
243
|
+
data=data,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self.sink.emit(event.to_dict())
|
|
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
|
+
|
|
258
|
+
def emit_run_start(
|
|
259
|
+
self,
|
|
260
|
+
agent: str,
|
|
261
|
+
llm_model: str | None = None,
|
|
262
|
+
config: dict[str, Any] | None = None,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""
|
|
265
|
+
Emit run_start event.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
agent: Agent name (e.g., 'SentienceAgent')
|
|
269
|
+
llm_model: LLM model name
|
|
270
|
+
config: Agent configuration
|
|
271
|
+
"""
|
|
272
|
+
# Track start time
|
|
273
|
+
self.started_at = datetime.utcnow()
|
|
274
|
+
|
|
275
|
+
data: dict[str, Any] = {"agent": agent}
|
|
276
|
+
if llm_model is not None:
|
|
277
|
+
data["llm_model"] = llm_model
|
|
278
|
+
if config is not None:
|
|
279
|
+
data["config"] = config
|
|
280
|
+
|
|
281
|
+
self.emit("run_start", data)
|
|
282
|
+
|
|
283
|
+
def emit_step_start(
|
|
284
|
+
self,
|
|
285
|
+
step_id: str,
|
|
286
|
+
step_index: int,
|
|
287
|
+
goal: str,
|
|
288
|
+
attempt: int = 0,
|
|
289
|
+
pre_url: str | None = None,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""
|
|
292
|
+
Emit step_start event.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
step_id: Step UUID
|
|
296
|
+
step_index: Step number (1-indexed)
|
|
297
|
+
goal: Step goal description
|
|
298
|
+
attempt: Attempt number (0-indexed)
|
|
299
|
+
pre_url: URL before step
|
|
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
|
+
|
|
305
|
+
data = {
|
|
306
|
+
"step_id": step_id,
|
|
307
|
+
"step_index": step_index,
|
|
308
|
+
"goal": goal,
|
|
309
|
+
"attempt": attempt,
|
|
310
|
+
}
|
|
311
|
+
if pre_url is not None:
|
|
312
|
+
data["pre_url"] = pre_url
|
|
313
|
+
|
|
314
|
+
self.emit("step_start", data, step_id=step_id)
|
|
315
|
+
|
|
316
|
+
def emit_run_end(self, steps: int, status: str | None = None) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Emit run_end event.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
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
|
|
324
|
+
"""
|
|
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})
|
|
339
|
+
|
|
340
|
+
def emit_error(
|
|
341
|
+
self,
|
|
342
|
+
step_id: str,
|
|
343
|
+
error: str,
|
|
344
|
+
attempt: int = 0,
|
|
345
|
+
) -> None:
|
|
346
|
+
"""
|
|
347
|
+
Emit error event.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
step_id: Step UUID
|
|
351
|
+
error: Error message
|
|
352
|
+
attempt: Attempt number when error occurred
|
|
353
|
+
"""
|
|
354
|
+
data = {
|
|
355
|
+
"step_id": step_id,
|
|
356
|
+
"error": error,
|
|
357
|
+
"attempt": attempt,
|
|
358
|
+
}
|
|
359
|
+
self.emit("error", data, step_id=step_id)
|
|
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
|
+
|
|
417
|
+
def close(self, **kwargs) -> None:
|
|
418
|
+
"""
|
|
419
|
+
Close the underlying sink.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
**kwargs: Passed through to sink.close() (e.g., blocking=True for CloudTraceSink)
|
|
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
|
+
|
|
430
|
+
# Check if sink.close() accepts kwargs (CloudTraceSink does, JsonlTraceSink doesn't)
|
|
431
|
+
import inspect
|
|
432
|
+
|
|
433
|
+
sig = inspect.signature(self.sink.close)
|
|
434
|
+
if any(
|
|
435
|
+
p.kind in (inspect.Parameter.VAR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
436
|
+
for p in sig.parameters.values()
|
|
437
|
+
):
|
|
438
|
+
self.sink.close(**kwargs)
|
|
439
|
+
else:
|
|
440
|
+
self.sink.close()
|
|
441
|
+
|
|
442
|
+
def __enter__(self):
|
|
443
|
+
"""Context manager support."""
|
|
444
|
+
return self
|
|
445
|
+
|
|
446
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
447
|
+
"""Context manager cleanup."""
|
|
448
|
+
self.close()
|
|
449
|
+
return False
|
|
@@ -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}")
|