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
|
@@ -13,6 +13,7 @@ class TraceFileInfo:
|
|
|
13
13
|
path: str
|
|
14
14
|
size_bytes: int
|
|
15
15
|
sha256: str
|
|
16
|
+
line_count: int | None = None # Number of lines in the trace file
|
|
16
17
|
|
|
17
18
|
def to_dict(self) -> dict:
|
|
18
19
|
return asdict(self)
|
|
@@ -28,6 +29,12 @@ class TraceSummary:
|
|
|
28
29
|
step_count: int
|
|
29
30
|
error_count: int
|
|
30
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
|
+
)
|
|
31
38
|
|
|
32
39
|
def to_dict(self) -> dict:
|
|
33
40
|
return asdict(self)
|
|
@@ -78,17 +85,18 @@ class StepIndex:
|
|
|
78
85
|
step_index: int
|
|
79
86
|
step_id: str
|
|
80
87
|
goal: str | None
|
|
81
|
-
status: Literal["
|
|
88
|
+
status: Literal["success", "failure", "partial", "unknown"]
|
|
82
89
|
ts_start: str
|
|
83
90
|
ts_end: str
|
|
84
91
|
offset_start: int
|
|
85
92
|
offset_end: int
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
92
100
|
|
|
93
101
|
def to_dict(self) -> dict:
|
|
94
102
|
result = asdict(self)
|
|
@@ -109,3 +117,83 @@ class TraceIndex:
|
|
|
109
117
|
def to_dict(self) -> dict:
|
|
110
118
|
"""Convert to dictionary for JSON serialization."""
|
|
111
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
|
+
}
|
|
@@ -7,8 +7,9 @@ import json
|
|
|
7
7
|
import os
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, Optional
|
|
11
11
|
|
|
12
|
+
from ..canonicalization import canonicalize_element
|
|
12
13
|
from .index_schema import (
|
|
13
14
|
ActionInfo,
|
|
14
15
|
SnapshotInfo,
|
|
@@ -20,30 +21,6 @@ from .index_schema import (
|
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def _normalize_text(text: str | None, max_len: int = 80) -> str:
|
|
24
|
-
"""Normalize text for digest: trim, collapse whitespace, lowercase, cap length."""
|
|
25
|
-
if not text:
|
|
26
|
-
return ""
|
|
27
|
-
# Trim and collapse whitespace
|
|
28
|
-
normalized = " ".join(text.split())
|
|
29
|
-
# Lowercase
|
|
30
|
-
normalized = normalized.lower()
|
|
31
|
-
# Cap length
|
|
32
|
-
if len(normalized) > max_len:
|
|
33
|
-
normalized = normalized[:max_len]
|
|
34
|
-
return normalized
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _round_bbox(bbox: dict[str, float], precision: int = 2) -> dict[str, int]:
|
|
38
|
-
"""Round bbox coordinates to reduce noise (default: 2px precision)."""
|
|
39
|
-
return {
|
|
40
|
-
"x": round(bbox.get("x", 0) / precision) * precision,
|
|
41
|
-
"y": round(bbox.get("y", 0) / precision) * precision,
|
|
42
|
-
"width": round(bbox.get("width", 0) / precision) * precision,
|
|
43
|
-
"height": round(bbox.get("height", 0) / precision) * precision,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
24
|
def _compute_snapshot_digest(snapshot_data: dict[str, Any]) -> str:
|
|
48
25
|
"""
|
|
49
26
|
Compute stable digest of snapshot for diffing.
|
|
@@ -55,18 +32,8 @@ def _compute_snapshot_digest(snapshot_data: dict[str, Any]) -> str:
|
|
|
55
32
|
viewport = snapshot_data.get("viewport", {})
|
|
56
33
|
elements = snapshot_data.get("elements", [])
|
|
57
34
|
|
|
58
|
-
# Canonicalize elements
|
|
59
|
-
canonical_elements = []
|
|
60
|
-
for elem in elements:
|
|
61
|
-
canonical_elem = {
|
|
62
|
-
"id": elem.get("id"),
|
|
63
|
-
"role": elem.get("role", ""),
|
|
64
|
-
"text_norm": _normalize_text(elem.get("text")),
|
|
65
|
-
"bbox": _round_bbox(elem.get("bbox", {"x": 0, "y": 0, "width": 0, "height": 0})),
|
|
66
|
-
"is_primary": elem.get("is_primary", False),
|
|
67
|
-
"is_clickable": elem.get("is_clickable", False),
|
|
68
|
-
}
|
|
69
|
-
canonical_elements.append(canonical_elem)
|
|
35
|
+
# Canonicalize elements using shared helper
|
|
36
|
+
canonical_elements = [canonicalize_element(elem) for elem in elements]
|
|
70
37
|
|
|
71
38
|
# Sort by element id for determinism
|
|
72
39
|
canonical_elements.sort(key=lambda e: e.get("id", 0))
|
|
@@ -149,15 +116,21 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
149
116
|
event_count = 0
|
|
150
117
|
error_count = 0
|
|
151
118
|
final_url = None
|
|
119
|
+
run_end_status = None # Track status from run_end event
|
|
120
|
+
agent_name = None # Extract from run_start event
|
|
121
|
+
line_count = 0 # Track total line count
|
|
152
122
|
|
|
153
123
|
steps_by_id: dict[str, StepIndex] = {}
|
|
154
124
|
step_order: list[str] = [] # Track order of first appearance
|
|
155
125
|
|
|
156
|
-
# Stream through file, tracking byte offsets
|
|
126
|
+
# Stream through file, tracking byte offsets and line numbers
|
|
157
127
|
with open(trace_path, "rb") as f:
|
|
158
128
|
byte_offset = 0
|
|
129
|
+
line_number = 0 # Track line number for each event
|
|
159
130
|
|
|
160
131
|
for line_bytes in f:
|
|
132
|
+
line_number += 1
|
|
133
|
+
line_count += 1
|
|
161
134
|
line_len = len(line_bytes)
|
|
162
135
|
|
|
163
136
|
try:
|
|
@@ -182,6 +155,10 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
182
155
|
if event_type == "error":
|
|
183
156
|
error_count += 1
|
|
184
157
|
|
|
158
|
+
# Extract agent_name from run_start event
|
|
159
|
+
if event_type == "run_start":
|
|
160
|
+
agent_name = data.get("agent")
|
|
161
|
+
|
|
185
162
|
# Initialize step if first time seeing this step_id
|
|
186
163
|
if step_id not in steps_by_id:
|
|
187
164
|
step_order.append(step_id)
|
|
@@ -189,11 +166,12 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
189
166
|
step_index=len(step_order),
|
|
190
167
|
step_id=step_id,
|
|
191
168
|
goal=None,
|
|
192
|
-
status="
|
|
169
|
+
status="failure", # Default to failure (will be updated by step_end event)
|
|
193
170
|
ts_start=ts,
|
|
194
171
|
ts_end=ts,
|
|
195
172
|
offset_start=byte_offset,
|
|
196
173
|
offset_end=byte_offset + line_len,
|
|
174
|
+
line_number=line_number, # Track line number
|
|
197
175
|
url_before=None,
|
|
198
176
|
url_after=None,
|
|
199
177
|
snapshot_before=SnapshotInfo(),
|
|
@@ -207,6 +185,7 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
207
185
|
# Update step metadata
|
|
208
186
|
step.ts_end = ts
|
|
209
187
|
step.offset_end = byte_offset + line_len
|
|
188
|
+
step.line_number = line_number # Update line number on each event
|
|
210
189
|
step.counters.events += 1
|
|
211
190
|
|
|
212
191
|
# Handle specific event types
|
|
@@ -214,7 +193,8 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
214
193
|
step.goal = data.get("goal")
|
|
215
194
|
step.url_before = data.get("pre_url")
|
|
216
195
|
|
|
217
|
-
elif event_type == "snapshot":
|
|
196
|
+
elif event_type == "snapshot" or event_type == "snapshot_taken":
|
|
197
|
+
# Handle both "snapshot" (current) and "snapshot_taken" (schema) for backward compatibility
|
|
218
198
|
snapshot_id = data.get("snapshot_id")
|
|
219
199
|
url = data.get("url")
|
|
220
200
|
digest = _compute_snapshot_digest(data)
|
|
@@ -231,7 +211,8 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
231
211
|
step.counters.snapshots += 1
|
|
232
212
|
final_url = url
|
|
233
213
|
|
|
234
|
-
elif event_type == "action":
|
|
214
|
+
elif event_type == "action" or event_type == "action_executed":
|
|
215
|
+
# Handle both "action" (current) and "action_executed" (schema) for backward compatibility
|
|
235
216
|
step.action = ActionInfo(
|
|
236
217
|
type=data.get("type"),
|
|
237
218
|
target_element_id=data.get("target_element_id"),
|
|
@@ -240,18 +221,83 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
240
221
|
)
|
|
241
222
|
step.counters.actions += 1
|
|
242
223
|
|
|
243
|
-
elif event_type == "llm_response":
|
|
224
|
+
elif event_type == "llm_response" or event_type == "llm_called":
|
|
225
|
+
# Handle both "llm_response" (current) and "llm_called" (schema) for backward compatibility
|
|
244
226
|
step.counters.llm_calls += 1
|
|
245
227
|
|
|
246
228
|
elif event_type == "error":
|
|
247
|
-
step.status = "
|
|
229
|
+
step.status = "failure"
|
|
248
230
|
|
|
249
231
|
elif event_type == "step_end":
|
|
250
|
-
|
|
251
|
-
|
|
232
|
+
# Determine status from step_end event data
|
|
233
|
+
# Frontend expects: success, failure, or partial
|
|
234
|
+
# Logic: success = exec.success && verify.passed
|
|
235
|
+
# partial = exec.success && !verify.passed
|
|
236
|
+
# failure = !exec.success
|
|
237
|
+
exec_data = data.get("exec", {})
|
|
238
|
+
verify_data = data.get("verify", {})
|
|
239
|
+
|
|
240
|
+
exec_success = exec_data.get("success", False)
|
|
241
|
+
verify_passed = verify_data.get("passed", False)
|
|
242
|
+
|
|
243
|
+
if exec_success and verify_passed:
|
|
244
|
+
step.status = "success"
|
|
245
|
+
elif exec_success and not verify_passed:
|
|
246
|
+
step.status = "partial"
|
|
247
|
+
elif not exec_success:
|
|
248
|
+
step.status = "failure"
|
|
249
|
+
else:
|
|
250
|
+
# Fallback: if step_end exists but no exec/verify data, default to failure
|
|
251
|
+
step.status = "failure"
|
|
252
|
+
|
|
253
|
+
elif event_type == "run_end":
|
|
254
|
+
# Extract status from run_end event
|
|
255
|
+
run_end_status = data.get("status")
|
|
256
|
+
# Validate status value
|
|
257
|
+
if run_end_status not in ["success", "failure", "partial", "unknown"]:
|
|
258
|
+
run_end_status = None
|
|
252
259
|
|
|
253
260
|
byte_offset += line_len
|
|
254
261
|
|
|
262
|
+
# Use run_end status if available, otherwise infer from step statuses
|
|
263
|
+
if run_end_status is None:
|
|
264
|
+
step_statuses = [step.status for step in steps_by_id.values()]
|
|
265
|
+
if step_statuses:
|
|
266
|
+
# Infer overall status from step statuses
|
|
267
|
+
if all(s == "success" for s in step_statuses):
|
|
268
|
+
run_end_status = "success"
|
|
269
|
+
elif any(s == "failure" for s in step_statuses):
|
|
270
|
+
# If any failure and no successes, it's failure; otherwise partial
|
|
271
|
+
if any(s == "success" for s in step_statuses):
|
|
272
|
+
run_end_status = "partial"
|
|
273
|
+
else:
|
|
274
|
+
run_end_status = "failure"
|
|
275
|
+
elif any(s == "partial" for s in step_statuses):
|
|
276
|
+
run_end_status = "partial"
|
|
277
|
+
else:
|
|
278
|
+
run_end_status = "failure" # Default to failure instead of unknown
|
|
279
|
+
else:
|
|
280
|
+
run_end_status = "failure" # Default to failure instead of unknown
|
|
281
|
+
|
|
282
|
+
# Calculate duration
|
|
283
|
+
duration_ms = None
|
|
284
|
+
if first_ts and last_ts:
|
|
285
|
+
try:
|
|
286
|
+
start = datetime.fromisoformat(first_ts.replace("Z", "+00:00"))
|
|
287
|
+
end = datetime.fromisoformat(last_ts.replace("Z", "+00:00"))
|
|
288
|
+
duration_ms = int((end - start).total_seconds() * 1000)
|
|
289
|
+
except (ValueError, AttributeError):
|
|
290
|
+
duration_ms = None
|
|
291
|
+
|
|
292
|
+
# Aggregate counters
|
|
293
|
+
snapshot_count = sum(step.counters.snapshots for step in steps_by_id.values())
|
|
294
|
+
action_count = sum(step.counters.actions for step in steps_by_id.values())
|
|
295
|
+
counters = {
|
|
296
|
+
"snapshot_count": snapshot_count,
|
|
297
|
+
"action_count": action_count,
|
|
298
|
+
"error_count": error_count,
|
|
299
|
+
}
|
|
300
|
+
|
|
255
301
|
# Build summary
|
|
256
302
|
summary = TraceSummary(
|
|
257
303
|
first_ts=first_ts,
|
|
@@ -260,6 +306,10 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
260
306
|
step_count=len(steps_by_id),
|
|
261
307
|
error_count=error_count,
|
|
262
308
|
final_url=final_url,
|
|
309
|
+
status=run_end_status,
|
|
310
|
+
agent_name=agent_name,
|
|
311
|
+
duration_ms=duration_ms,
|
|
312
|
+
counters=counters,
|
|
263
313
|
)
|
|
264
314
|
|
|
265
315
|
# Build steps list in order
|
|
@@ -270,6 +320,7 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
270
320
|
path=str(trace_path),
|
|
271
321
|
size_bytes=os.path.getsize(trace_path),
|
|
272
322
|
sha256=_compute_file_sha256(str(trace_path)),
|
|
323
|
+
line_count=line_count,
|
|
273
324
|
)
|
|
274
325
|
|
|
275
326
|
# Build final index
|
|
@@ -285,13 +336,16 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
285
336
|
return index
|
|
286
337
|
|
|
287
338
|
|
|
288
|
-
def write_trace_index(
|
|
339
|
+
def write_trace_index(
|
|
340
|
+
trace_path: str, index_path: str | None = None, frontend_format: bool = False
|
|
341
|
+
) -> str:
|
|
289
342
|
"""
|
|
290
343
|
Build index and write to file.
|
|
291
344
|
|
|
292
345
|
Args:
|
|
293
346
|
trace_path: Path to trace JSONL file
|
|
294
347
|
index_path: Optional custom path for index file (default: trace_path with .index.json)
|
|
348
|
+
frontend_format: If True, write in frontend-compatible format (default: False)
|
|
295
349
|
|
|
296
350
|
Returns:
|
|
297
351
|
Path to written index file
|
|
@@ -301,8 +355,11 @@ def write_trace_index(trace_path: str, index_path: str | None = None) -> str:
|
|
|
301
355
|
|
|
302
356
|
index = build_trace_index(trace_path)
|
|
303
357
|
|
|
304
|
-
with open(index_path, "w") as f:
|
|
305
|
-
|
|
358
|
+
with open(index_path, "w", encoding="utf-8") as f:
|
|
359
|
+
if frontend_format:
|
|
360
|
+
json.dump(index.to_sentience_studio_dict(), f, indent=2)
|
|
361
|
+
else:
|
|
362
|
+
json.dump(index.to_dict(), f, indent=2)
|
|
306
363
|
|
|
307
364
|
return index_path
|
|
308
365
|
|
sentience/tracer_factory.py
CHANGED
|
@@ -7,16 +7,16 @@ Provides convenient factory function for creating tracers with cloud upload supp
|
|
|
7
7
|
import gzip
|
|
8
8
|
import os
|
|
9
9
|
import uuid
|
|
10
|
+
from collections.abc import Callable
|
|
10
11
|
from pathlib import Path
|
|
12
|
+
from typing import Any, Optional
|
|
11
13
|
|
|
12
14
|
import requests
|
|
13
15
|
|
|
14
16
|
from sentience.cloud_tracing import CloudTraceSink, SentienceLogger
|
|
17
|
+
from sentience.constants import SENTIENCE_API_URL
|
|
15
18
|
from sentience.tracing import JsonlTraceSink, Tracer
|
|
16
19
|
|
|
17
|
-
# Sentience API base URL (constant)
|
|
18
|
-
SENTIENCE_API_URL = "https://api.sentienceapi.com"
|
|
19
|
-
|
|
20
20
|
|
|
21
21
|
def create_tracer(
|
|
22
22
|
api_key: str | None = None,
|
|
@@ -24,6 +24,11 @@ def create_tracer(
|
|
|
24
24
|
api_url: str | None = None,
|
|
25
25
|
logger: SentienceLogger | None = None,
|
|
26
26
|
upload_trace: bool = False,
|
|
27
|
+
goal: str | None = None,
|
|
28
|
+
agent_type: str | None = None,
|
|
29
|
+
llm_model: str | None = None,
|
|
30
|
+
start_url: str | None = None,
|
|
31
|
+
screenshot_processor: Callable[[str], str] | None = None,
|
|
27
32
|
) -> Tracer:
|
|
28
33
|
"""
|
|
29
34
|
Create tracer with automatic tier detection.
|
|
@@ -42,15 +47,42 @@ def create_tracer(
|
|
|
42
47
|
upload_trace: Enable cloud trace upload (default: False). When True and api_key
|
|
43
48
|
is provided, traces will be uploaded to cloud. When False, traces
|
|
44
49
|
are saved locally only.
|
|
50
|
+
goal: User's goal/objective for this trace run. This will be displayed as the
|
|
51
|
+
trace name in the frontend. Should be descriptive and action-oriented.
|
|
52
|
+
Example: "Add wireless headphones to cart on Amazon"
|
|
53
|
+
agent_type: Type of agent running (e.g., "SentienceAgent", "CustomAgent")
|
|
54
|
+
llm_model: LLM model used (e.g., "gpt-4-turbo", "claude-3-5-sonnet")
|
|
55
|
+
start_url: Starting URL of the agent run (e.g., "https://amazon.com")
|
|
56
|
+
screenshot_processor: Optional function to process screenshots before upload.
|
|
57
|
+
Takes base64 string, returns processed base64 string.
|
|
58
|
+
Useful for PII redaction or custom image processing.
|
|
45
59
|
|
|
46
60
|
Returns:
|
|
47
61
|
Tracer configured with appropriate sink
|
|
48
62
|
|
|
49
63
|
Example:
|
|
50
|
-
>>> # Pro tier user
|
|
51
|
-
>>> tracer = create_tracer(
|
|
64
|
+
>>> # Pro tier user with goal
|
|
65
|
+
>>> tracer = create_tracer(
|
|
66
|
+
... api_key="sk_pro_xyz",
|
|
67
|
+
... run_id="demo",
|
|
68
|
+
... goal="Add headphones to cart",
|
|
69
|
+
... agent_type="SentienceAgent",
|
|
70
|
+
... llm_model="gpt-4-turbo",
|
|
71
|
+
... start_url="https://amazon.com"
|
|
72
|
+
... )
|
|
52
73
|
>>> # Returns: Tracer with CloudTraceSink
|
|
53
74
|
>>>
|
|
75
|
+
>>> # With screenshot processor for PII redaction
|
|
76
|
+
>>> def redact_pii(screenshot_base64: str) -> str:
|
|
77
|
+
... # Your custom redaction logic
|
|
78
|
+
... return redacted_screenshot
|
|
79
|
+
>>>
|
|
80
|
+
>>> tracer = create_tracer(
|
|
81
|
+
... api_key="sk_pro_xyz",
|
|
82
|
+
... screenshot_processor=redact_pii
|
|
83
|
+
... )
|
|
84
|
+
>>> # Screenshots will be processed before upload
|
|
85
|
+
>>>
|
|
54
86
|
>>> # Free tier user
|
|
55
87
|
>>> tracer = create_tracer(run_id="demo")
|
|
56
88
|
>>> # Returns: Tracer with JsonlTraceSink (local-only)
|
|
@@ -73,11 +105,28 @@ def create_tracer(
|
|
|
73
105
|
# 1. Try to initialize Cloud Sink (Pro/Enterprise tier) if upload enabled
|
|
74
106
|
if api_key and upload_trace:
|
|
75
107
|
try:
|
|
108
|
+
# Build metadata object for trace initialization
|
|
109
|
+
# Only include non-empty fields to avoid sending empty strings
|
|
110
|
+
metadata: dict[str, str] = {}
|
|
111
|
+
if goal and goal.strip():
|
|
112
|
+
metadata["goal"] = goal.strip()
|
|
113
|
+
if agent_type and agent_type.strip():
|
|
114
|
+
metadata["agent_type"] = agent_type.strip()
|
|
115
|
+
if llm_model and llm_model.strip():
|
|
116
|
+
metadata["llm_model"] = llm_model.strip()
|
|
117
|
+
if start_url and start_url.strip():
|
|
118
|
+
metadata["start_url"] = start_url.strip()
|
|
119
|
+
|
|
120
|
+
# Build request payload
|
|
121
|
+
payload: dict[str, Any] = {"run_id": run_id}
|
|
122
|
+
if metadata:
|
|
123
|
+
payload["metadata"] = metadata
|
|
124
|
+
|
|
76
125
|
# Request pre-signed upload URL from backend
|
|
77
126
|
response = requests.post(
|
|
78
127
|
f"{api_url}/v1/traces/init",
|
|
79
128
|
headers={"Authorization": f"Bearer {api_key}"},
|
|
80
|
-
json=
|
|
129
|
+
json=payload,
|
|
81
130
|
timeout=10,
|
|
82
131
|
)
|
|
83
132
|
|
|
@@ -96,16 +145,46 @@ def create_tracer(
|
|
|
96
145
|
api_url=api_url,
|
|
97
146
|
logger=logger,
|
|
98
147
|
),
|
|
148
|
+
screenshot_processor=screenshot_processor,
|
|
99
149
|
)
|
|
100
150
|
else:
|
|
101
151
|
print("⚠️ [Sentience] Cloud init response missing upload_url")
|
|
152
|
+
print(f" Response data: {data}")
|
|
102
153
|
print(" Falling back to local-only tracing")
|
|
103
154
|
|
|
104
155
|
elif response.status_code == 403:
|
|
105
156
|
print("⚠️ [Sentience] Cloud tracing requires Pro tier")
|
|
157
|
+
try:
|
|
158
|
+
error_data = response.json()
|
|
159
|
+
error_msg = error_data.get("error") or error_data.get("message", "")
|
|
160
|
+
if error_msg:
|
|
161
|
+
print(f" API Error: {error_msg}")
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
print(" Falling back to local-only tracing")
|
|
165
|
+
elif response.status_code == 401:
|
|
166
|
+
print("⚠️ [Sentience] Cloud init failed: HTTP 401 Unauthorized")
|
|
167
|
+
print(" API key is invalid or expired")
|
|
168
|
+
try:
|
|
169
|
+
error_data = response.json()
|
|
170
|
+
error_msg = error_data.get("error") or error_data.get("message", "")
|
|
171
|
+
if error_msg:
|
|
172
|
+
print(f" API Error: {error_msg}")
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
106
175
|
print(" Falling back to local-only tracing")
|
|
107
176
|
else:
|
|
108
177
|
print(f"⚠️ [Sentience] Cloud init failed: HTTP {response.status_code}")
|
|
178
|
+
try:
|
|
179
|
+
error_data = response.json()
|
|
180
|
+
error_msg = error_data.get("error") or error_data.get(
|
|
181
|
+
"message", "Unknown error"
|
|
182
|
+
)
|
|
183
|
+
print(f" Error: {error_msg}")
|
|
184
|
+
if "tier" in error_msg.lower() or "subscription" in error_msg.lower():
|
|
185
|
+
print(f" 💡 This may be a tier/subscription issue")
|
|
186
|
+
except Exception:
|
|
187
|
+
print(f" Response: {response.text[:200]}")
|
|
109
188
|
print(" Falling back to local-only tracing")
|
|
110
189
|
|
|
111
190
|
except requests.exceptions.Timeout:
|
|
@@ -125,7 +204,11 @@ def create_tracer(
|
|
|
125
204
|
local_path = traces_dir / f"{run_id}.jsonl"
|
|
126
205
|
print(f"💾 [Sentience] Local tracing: {local_path}")
|
|
127
206
|
|
|
128
|
-
return Tracer(
|
|
207
|
+
return Tracer(
|
|
208
|
+
run_id=run_id,
|
|
209
|
+
sink=JsonlTraceSink(str(local_path)),
|
|
210
|
+
screenshot_processor=screenshot_processor,
|
|
211
|
+
)
|
|
129
212
|
|
|
130
213
|
|
|
131
214
|
def _recover_orphaned_traces(api_key: str, api_url: str = SENTIENCE_API_URL) -> None:
|
|
@@ -149,10 +232,23 @@ def _recover_orphaned_traces(api_key: str, api_url: str = SENTIENCE_API_URL) ->
|
|
|
149
232
|
if not orphaned:
|
|
150
233
|
return
|
|
151
234
|
|
|
152
|
-
|
|
235
|
+
# Filter out test files (run_ids that start with "test-" or are clearly test data)
|
|
236
|
+
# These are likely from local testing and shouldn't be uploaded
|
|
237
|
+
test_patterns = ["test-", "test_", "test."]
|
|
238
|
+
valid_orphaned = [
|
|
239
|
+
f
|
|
240
|
+
for f in orphaned
|
|
241
|
+
if not any(f.stem.startswith(pattern) for pattern in test_patterns)
|
|
242
|
+
and not f.stem.startswith("test")
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
if not valid_orphaned:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
print(f"⚠️ [Sentience] Found {len(valid_orphaned)} un-uploaded trace(s) from previous runs")
|
|
153
249
|
print(" Attempting to upload now...")
|
|
154
250
|
|
|
155
|
-
for trace_file in
|
|
251
|
+
for trace_file in valid_orphaned:
|
|
156
252
|
try:
|
|
157
253
|
# Extract run_id from filename (format: {run_id}.jsonl)
|
|
158
254
|
run_id = trace_file.stem
|
|
@@ -166,6 +262,21 @@ def _recover_orphaned_traces(api_key: str, api_url: str = SENTIENCE_API_URL) ->
|
|
|
166
262
|
)
|
|
167
263
|
|
|
168
264
|
if response.status_code != 200:
|
|
265
|
+
# HTTP 409 means trace already exists (already uploaded)
|
|
266
|
+
# Treat as success and delete local file
|
|
267
|
+
if response.status_code == 409:
|
|
268
|
+
print(f"✅ Trace {run_id} already exists in cloud (skipping re-upload)")
|
|
269
|
+
# Delete local file since it's already in cloud
|
|
270
|
+
try:
|
|
271
|
+
os.remove(trace_file)
|
|
272
|
+
except Exception:
|
|
273
|
+
pass # Ignore cleanup errors
|
|
274
|
+
continue
|
|
275
|
+
# HTTP 422 typically means invalid run_id (e.g., test files)
|
|
276
|
+
# Skip silently for 422, but log other errors
|
|
277
|
+
if response.status_code == 422:
|
|
278
|
+
# Likely a test file or invalid run_id, skip silently
|
|
279
|
+
continue
|
|
169
280
|
print(f"❌ Failed to get upload URL for {run_id}: HTTP {response.status_code}")
|
|
170
281
|
continue
|
|
171
282
|
|