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
@@ -0,0 +1,197 @@
1
+ """
2
+ Trace file management utilities for consistent file operations.
3
+
4
+ This module provides helper functions for common trace file operations
5
+ shared between JsonlTraceSink and CloudTraceSink.
6
+ """
7
+
8
+ import json
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ from .models import TraceStats
14
+
15
+
16
+ class TraceFileManager:
17
+ """
18
+ Helper for common trace file operations.
19
+
20
+ Provides static methods for file operations shared across trace sinks.
21
+ """
22
+
23
+ @staticmethod
24
+ def write_event(file_handle: Any, event: dict[str, Any]) -> None:
25
+ """
26
+ Write a trace event to a file handle as JSONL.
27
+
28
+ Args:
29
+ file_handle: Open file handle (must be writable)
30
+ event: Event dictionary to write
31
+ """
32
+ json_str = json.dumps(event, ensure_ascii=False)
33
+ file_handle.write(json_str + "\n")
34
+ file_handle.flush() # Ensure written to disk
35
+
36
+ @staticmethod
37
+ def ensure_directory(path: Path) -> None:
38
+ """
39
+ Ensure the parent directory of a path exists.
40
+
41
+ Args:
42
+ path: File path whose parent directory should exist
43
+ """
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+
46
+ @staticmethod
47
+ def read_events(path: Path) -> list[dict[str, Any]]:
48
+ """
49
+ Read all events from a JSONL trace file.
50
+
51
+ Args:
52
+ path: Path to JSONL trace file
53
+
54
+ Returns:
55
+ List of event dictionaries
56
+
57
+ Raises:
58
+ FileNotFoundError: If file doesn't exist
59
+ json.JSONDecodeError: If file contains invalid JSON
60
+ """
61
+ events = []
62
+ with open(path, encoding="utf-8") as f:
63
+ for line in f:
64
+ line = line.strip()
65
+ if not line:
66
+ continue
67
+ try:
68
+ event = json.loads(line)
69
+ events.append(event)
70
+ except json.JSONDecodeError:
71
+ # Skip invalid lines but continue reading
72
+ continue
73
+ return events
74
+
75
+ @staticmethod
76
+ def extract_stats(
77
+ events: list[dict[str, Any]],
78
+ infer_status_func: None | (
79
+ Callable[[list[dict[str, Any]], dict[str, Any] | None], str]
80
+ ) = None,
81
+ ) -> TraceStats:
82
+ """
83
+ Extract execution statistics from trace events.
84
+
85
+ This is a common operation shared between JsonlTraceSink and CloudTraceSink.
86
+
87
+ Args:
88
+ events: List of trace event dictionaries
89
+ infer_status_func: Optional function to infer final_status from events.
90
+ If None, uses default inference logic.
91
+
92
+ Returns:
93
+ TraceStats with execution statistics
94
+ """
95
+ if not events:
96
+ return TraceStats(
97
+ total_steps=0,
98
+ total_events=0,
99
+ duration_ms=None,
100
+ final_status="unknown",
101
+ started_at=None,
102
+ ended_at=None,
103
+ )
104
+
105
+ # Find run_start and run_end events
106
+ run_start = next((e for e in events if e.get("type") == "run_start"), None)
107
+ run_end = next((e for e in events if e.get("type") == "run_end"), None)
108
+
109
+ # Extract timestamps
110
+ started_at: str | None = None
111
+ ended_at: str | None = None
112
+ if run_start:
113
+ started_at = run_start.get("ts")
114
+ if run_end:
115
+ ended_at = run_end.get("ts")
116
+
117
+ # Calculate duration
118
+ duration_ms: int | None = None
119
+ if started_at and ended_at:
120
+ try:
121
+ from datetime import datetime
122
+
123
+ start_dt = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
124
+ end_dt = datetime.fromisoformat(ended_at.replace("Z", "+00:00"))
125
+ delta = end_dt - start_dt
126
+ duration_ms = int(delta.total_seconds() * 1000)
127
+ except Exception:
128
+ pass
129
+
130
+ # Count steps (from step_start events, only first attempt)
131
+ step_indices = set()
132
+ for event in events:
133
+ if event.get("type") == "step_start":
134
+ step_index = event.get("data", {}).get("step_index")
135
+ if step_index is not None:
136
+ step_indices.add(step_index)
137
+ total_steps = len(step_indices) if step_indices else 0
138
+
139
+ # If run_end has steps count, use that (more accurate)
140
+ if run_end:
141
+ steps_from_end = run_end.get("data", {}).get("steps")
142
+ if steps_from_end is not None:
143
+ total_steps = max(total_steps, steps_from_end)
144
+
145
+ # Count total events
146
+ total_events = len(events)
147
+
148
+ # Infer final status
149
+ if infer_status_func:
150
+ final_status = infer_status_func(events, run_end)
151
+ else:
152
+ final_status = TraceFileManager._infer_final_status(events, run_end)
153
+
154
+ return TraceStats(
155
+ total_steps=total_steps,
156
+ total_events=total_events,
157
+ duration_ms=duration_ms,
158
+ final_status=final_status,
159
+ started_at=started_at,
160
+ ended_at=ended_at,
161
+ )
162
+
163
+ @staticmethod
164
+ def _infer_final_status(
165
+ events: list[dict[str, Any]],
166
+ run_end: dict[str, Any] | None,
167
+ ) -> str:
168
+ """
169
+ Infer final status from trace events.
170
+
171
+ Args:
172
+ events: List of trace event dictionaries
173
+ run_end: Optional run_end event dictionary
174
+
175
+ Returns:
176
+ Final status string: "success", "failure", "partial", or "unknown"
177
+ """
178
+ # Check for run_end event with status
179
+ if run_end:
180
+ status = run_end.get("data", {}).get("status")
181
+ if status in ("success", "failure", "partial", "unknown"):
182
+ return status
183
+
184
+ # Infer from error events
185
+ has_errors = any(e.get("type") == "error" for e in events)
186
+ if has_errors:
187
+ step_ends = [e for e in events if e.get("type") == "step_end"]
188
+ if step_ends:
189
+ return "partial"
190
+ else:
191
+ return "failure"
192
+ else:
193
+ step_ends = [e for e in events if e.get("type") == "step_end"]
194
+ if step_ends:
195
+ return "success"
196
+ else:
197
+ return "unknown"
@@ -0,0 +1,27 @@
1
+ """
2
+ Trace indexing module for Sentience SDK.
3
+ """
4
+
5
+ from .index_schema import (
6
+ ActionInfo,
7
+ SnapshotInfo,
8
+ StepCounters,
9
+ StepIndex,
10
+ TraceFileInfo,
11
+ TraceIndex,
12
+ TraceSummary,
13
+ )
14
+ from .indexer import build_trace_index, read_step_events, write_trace_index
15
+
16
+ __all__ = [
17
+ "build_trace_index",
18
+ "write_trace_index",
19
+ "read_step_events",
20
+ "TraceIndex",
21
+ "StepIndex",
22
+ "TraceSummary",
23
+ "TraceFileInfo",
24
+ "SnapshotInfo",
25
+ "ActionInfo",
26
+ "StepCounters",
27
+ ]
@@ -0,0 +1,199 @@
1
+ """
2
+ Type definitions for trace index schema using concrete classes.
3
+ """
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from typing import List, Literal, Optional
7
+
8
+
9
+ @dataclass
10
+ class TraceFileInfo:
11
+ """Metadata about the trace file."""
12
+
13
+ path: str
14
+ size_bytes: int
15
+ sha256: str
16
+ line_count: int | None = None # Number of lines in the trace file
17
+
18
+ def to_dict(self) -> dict:
19
+ return asdict(self)
20
+
21
+
22
+ @dataclass
23
+ class TraceSummary:
24
+ """High-level summary of the trace."""
25
+
26
+ first_ts: str
27
+ last_ts: str
28
+ event_count: int
29
+ step_count: int
30
+ error_count: int
31
+ final_url: str | None
32
+ status: Literal["success", "failure", "partial", "unknown"] | None = None
33
+ agent_name: str | None = None # Agent name from run_start event
34
+ duration_ms: int | None = None # Calculated duration in milliseconds
35
+ counters: dict[str, int] | None = (
36
+ None # Aggregated counters (snapshot_count, action_count, error_count)
37
+ )
38
+
39
+ def to_dict(self) -> dict:
40
+ return asdict(self)
41
+
42
+
43
+ @dataclass
44
+ class SnapshotInfo:
45
+ """Snapshot metadata for index."""
46
+
47
+ snapshot_id: str | None = None
48
+ digest: str | None = None
49
+ url: str | None = None
50
+
51
+ def to_dict(self) -> dict:
52
+ return asdict(self)
53
+
54
+
55
+ @dataclass
56
+ class ActionInfo:
57
+ """Action metadata for index."""
58
+
59
+ type: str | None = None
60
+ target_element_id: int | None = None
61
+ args_digest: str | None = None
62
+ success: bool | None = None
63
+
64
+ def to_dict(self) -> dict:
65
+ return asdict(self)
66
+
67
+
68
+ @dataclass
69
+ class StepCounters:
70
+ """Event counters per step."""
71
+
72
+ events: int = 0
73
+ snapshots: int = 0
74
+ actions: int = 0
75
+ llm_calls: int = 0
76
+
77
+ def to_dict(self) -> dict:
78
+ return asdict(self)
79
+
80
+
81
+ @dataclass
82
+ class StepIndex:
83
+ """Index entry for a single step."""
84
+
85
+ step_index: int
86
+ step_id: str
87
+ goal: str | None
88
+ status: Literal["success", "failure", "partial", "unknown"]
89
+ ts_start: str
90
+ ts_end: str
91
+ offset_start: int
92
+ offset_end: int
93
+ line_number: int | None = None # Line number for byte-range fetching
94
+ url_before: str | None = None
95
+ url_after: str | None = None
96
+ snapshot_before: SnapshotInfo = field(default_factory=SnapshotInfo)
97
+ snapshot_after: SnapshotInfo = field(default_factory=SnapshotInfo)
98
+ action: ActionInfo = field(default_factory=ActionInfo)
99
+ counters: StepCounters = field(default_factory=StepCounters)
100
+
101
+ def to_dict(self) -> dict:
102
+ result = asdict(self)
103
+ return result
104
+
105
+
106
+ @dataclass
107
+ class TraceIndex:
108
+ """Complete trace index schema."""
109
+
110
+ version: int
111
+ run_id: str
112
+ created_at: str
113
+ trace_file: TraceFileInfo
114
+ summary: TraceSummary
115
+ steps: list[StepIndex] = field(default_factory=list)
116
+
117
+ def to_dict(self) -> dict:
118
+ """Convert to dictionary for JSON serialization."""
119
+ return asdict(self)
120
+
121
+ def to_sentience_studio_dict(self) -> dict:
122
+ """
123
+ Convert to SS-compatible format.
124
+
125
+ Maps SDK field names to frontend expectations:
126
+ - created_at -> generated_at
127
+ - first_ts -> start_time
128
+ - last_ts -> end_time
129
+ - step_index (0-based) -> step (1-based)
130
+ - ts_start -> timestamp
131
+ - Filters out "unknown" status
132
+ """
133
+ from datetime import datetime
134
+
135
+ # Calculate duration if not already set
136
+ duration_ms = self.summary.duration_ms
137
+ if duration_ms is None and self.summary.first_ts and self.summary.last_ts:
138
+ try:
139
+ start = datetime.fromisoformat(self.summary.first_ts.replace("Z", "+00:00"))
140
+ end = datetime.fromisoformat(self.summary.last_ts.replace("Z", "+00:00"))
141
+ duration_ms = int((end - start).total_seconds() * 1000)
142
+ except (ValueError, AttributeError):
143
+ duration_ms = None
144
+
145
+ # Aggregate counters if not already set
146
+ counters = self.summary.counters
147
+ if counters is None:
148
+ snapshot_count = sum(step.counters.snapshots for step in self.steps)
149
+ action_count = sum(step.counters.actions for step in self.steps)
150
+ counters = {
151
+ "snapshot_count": snapshot_count,
152
+ "action_count": action_count,
153
+ "error_count": self.summary.error_count,
154
+ }
155
+
156
+ return {
157
+ "version": self.version,
158
+ "run_id": self.run_id,
159
+ "generated_at": self.created_at, # Renamed from created_at
160
+ "trace_file": {
161
+ "path": self.trace_file.path,
162
+ "size_bytes": self.trace_file.size_bytes,
163
+ "line_count": self.trace_file.line_count, # Added
164
+ },
165
+ "summary": {
166
+ "agent_name": self.summary.agent_name, # Added
167
+ "total_steps": self.summary.step_count, # Renamed from step_count
168
+ "status": (
169
+ self.summary.status if self.summary.status != "unknown" else None
170
+ ), # Filter out unknown
171
+ "start_time": self.summary.first_ts, # Renamed from first_ts
172
+ "end_time": self.summary.last_ts, # Renamed from last_ts
173
+ "duration_ms": duration_ms, # Added
174
+ "counters": counters, # Added
175
+ },
176
+ "steps": [
177
+ {
178
+ "step": s.step_index + 1, # Convert 0-based to 1-based
179
+ "byte_offset": s.offset_start,
180
+ "line_number": s.line_number, # Added
181
+ "timestamp": s.ts_start, # Use start time
182
+ "action": {
183
+ "type": s.action.type or "",
184
+ "goal": s.goal, # Move goal into action
185
+ "digest": s.action.args_digest,
186
+ },
187
+ "snapshot": (
188
+ {
189
+ "url": s.snapshot_after.url,
190
+ "digest": s.snapshot_after.digest,
191
+ }
192
+ if s.snapshot_after.url
193
+ else None
194
+ ),
195
+ "status": s.status if s.status != "unknown" else None, # Filter out unknown
196
+ }
197
+ for s in self.steps
198
+ ],
199
+ }