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.

Files changed (82) hide show
  1. sentience/__init__.py +253 -0
  2. sentience/_extension_loader.py +195 -0
  3. sentience/action_executor.py +215 -0
  4. sentience/actions.py +1020 -0
  5. sentience/agent.py +1181 -0
  6. sentience/agent_config.py +46 -0
  7. sentience/agent_runtime.py +424 -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 +108 -0
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +343 -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 +427 -0
  21. sentience/base_agent.py +196 -0
  22. sentience/browser.py +1215 -0
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cli.py +130 -0
  26. sentience/cloud_tracing.py +807 -0
  27. sentience/constants.py +6 -0
  28. sentience/conversational_agent.py +543 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +188 -0
  31. sentience/extension/background.js +104 -0
  32. sentience/extension/content.js +161 -0
  33. sentience/extension/injected_api.js +914 -0
  34. sentience/extension/manifest.json +36 -0
  35. sentience/extension/pkg/sentience_core.d.ts +51 -0
  36. sentience/extension/pkg/sentience_core.js +323 -0
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  39. sentience/extension/release.json +115 -0
  40. sentience/formatting.py +15 -0
  41. sentience/generator.py +202 -0
  42. sentience/inspector.py +367 -0
  43. sentience/llm_interaction_handler.py +191 -0
  44. sentience/llm_provider.py +875 -0
  45. sentience/llm_provider_utils.py +120 -0
  46. sentience/llm_response_builder.py +153 -0
  47. sentience/models.py +846 -0
  48. sentience/ordinal.py +280 -0
  49. sentience/overlay.py +222 -0
  50. sentience/protocols.py +228 -0
  51. sentience/query.py +303 -0
  52. sentience/read.py +188 -0
  53. sentience/recorder.py +589 -0
  54. sentience/schemas/trace_v1.json +335 -0
  55. sentience/screenshot.py +100 -0
  56. sentience/sentience_methods.py +86 -0
  57. sentience/snapshot.py +706 -0
  58. sentience/snapshot_diff.py +126 -0
  59. sentience/text_search.py +262 -0
  60. sentience/trace_event_builder.py +148 -0
  61. sentience/trace_file_manager.py +197 -0
  62. sentience/trace_indexing/__init__.py +27 -0
  63. sentience/trace_indexing/index_schema.py +199 -0
  64. sentience/trace_indexing/indexer.py +414 -0
  65. sentience/tracer_factory.py +322 -0
  66. sentience/tracing.py +449 -0
  67. sentience/utils/__init__.py +40 -0
  68. sentience/utils/browser.py +46 -0
  69. sentience/utils/element.py +257 -0
  70. sentience/utils/formatting.py +59 -0
  71. sentience/utils.py +296 -0
  72. sentience/verification.py +380 -0
  73. sentience/visual_agent.py +2058 -0
  74. sentience/wait.py +139 -0
  75. sentienceapi-0.95.0.dist-info/METADATA +984 -0
  76. sentienceapi-0.95.0.dist-info/RECORD +82 -0
  77. sentienceapi-0.95.0.dist-info/WHEEL +5 -0
  78. sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
  79. sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
  80. sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
  81. sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
  82. 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}")