sentienceapi 0.90.16__py3-none-any.whl → 0.98.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sentienceapi might be problematic. Click here for more details.

Files changed (90) hide show
  1. sentience/__init__.py +120 -6
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +217 -0
  4. sentience/actions.py +758 -30
  5. sentience/agent.py +806 -293
  6. sentience/agent_config.py +3 -0
  7. sentience/agent_runtime.py +840 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +89 -1141
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +372 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +483 -0
  21. sentience/base_agent.py +95 -0
  22. sentience/browser.py +678 -39
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cloud_tracing.py +507 -42
  26. sentience/constants.py +6 -0
  27. sentience/conversational_agent.py +77 -43
  28. sentience/cursor_policy.py +142 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +98 -2
  31. sentience/extension/background.js +56 -185
  32. sentience/extension/content.js +150 -287
  33. sentience/extension/injected_api.js +1088 -1368
  34. sentience/extension/manifest.json +1 -1
  35. sentience/extension/pkg/sentience_core.d.ts +22 -22
  36. sentience/extension/pkg/sentience_core.js +275 -433
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/release.json +47 -47
  39. sentience/failure_artifacts.py +241 -0
  40. sentience/formatting.py +9 -53
  41. sentience/inspector.py +183 -1
  42. sentience/integrations/__init__.py +6 -0
  43. sentience/integrations/langchain/__init__.py +12 -0
  44. sentience/integrations/langchain/context.py +18 -0
  45. sentience/integrations/langchain/core.py +326 -0
  46. sentience/integrations/langchain/tools.py +180 -0
  47. sentience/integrations/models.py +46 -0
  48. sentience/integrations/pydanticai/__init__.py +15 -0
  49. sentience/integrations/pydanticai/deps.py +20 -0
  50. sentience/integrations/pydanticai/toolset.py +468 -0
  51. sentience/llm_interaction_handler.py +191 -0
  52. sentience/llm_provider.py +765 -66
  53. sentience/llm_provider_utils.py +120 -0
  54. sentience/llm_response_builder.py +153 -0
  55. sentience/models.py +595 -3
  56. sentience/ordinal.py +280 -0
  57. sentience/overlay.py +109 -2
  58. sentience/protocols.py +228 -0
  59. sentience/query.py +67 -5
  60. sentience/read.py +95 -3
  61. sentience/recorder.py +223 -3
  62. sentience/schemas/trace_v1.json +128 -9
  63. sentience/screenshot.py +48 -2
  64. sentience/sentience_methods.py +86 -0
  65. sentience/snapshot.py +599 -55
  66. sentience/snapshot_diff.py +126 -0
  67. sentience/text_search.py +120 -5
  68. sentience/trace_event_builder.py +148 -0
  69. sentience/trace_file_manager.py +197 -0
  70. sentience/trace_indexing/index_schema.py +95 -7
  71. sentience/trace_indexing/indexer.py +105 -48
  72. sentience/tracer_factory.py +120 -9
  73. sentience/tracing.py +172 -8
  74. sentience/utils/__init__.py +40 -0
  75. sentience/utils/browser.py +46 -0
  76. sentience/{utils.py → utils/element.py} +3 -42
  77. sentience/utils/formatting.py +59 -0
  78. sentience/verification.py +618 -0
  79. sentience/visual_agent.py +2058 -0
  80. sentience/wait.py +68 -2
  81. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
  82. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  83. sentience/extension/test-content.js +0 -4
  84. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  85. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  86. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  87. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  88. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  89. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  90. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
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.parent.mkdir(parents=True, exist_ok=True)
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
- json_str = json.dumps(event, ensure_ascii=False)
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
- write_trace_index(str(self.path))
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
- self.emit("run_end", {"steps": steps})
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
- Digest utilities for snapshot canonicalization and hashing.
2
+ Element manipulation and digest utilities for Sentience SDK.
3
3
 
4
- Provides functions to compute stable digests of snapshots for determinism diff.
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 pathlib import Path
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)