sentienceapi 0.90.16__py3-none-any.whl → 0.92.2__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 +14 -5
- sentience/action_executor.py +215 -0
- sentience/actions.py +408 -25
- sentience/agent.py +802 -293
- sentience/agent_config.py +3 -0
- sentience/async_api.py +83 -1142
- sentience/base_agent.py +95 -0
- sentience/browser.py +484 -1
- sentience/browser_evaluator.py +299 -0
- sentience/cloud_tracing.py +457 -33
- sentience/conversational_agent.py +77 -43
- sentience/element_filter.py +136 -0
- sentience/expect.py +98 -2
- sentience/extension/background.js +56 -185
- sentience/extension/content.js +117 -289
- sentience/extension/injected_api.js +799 -1374
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.js +190 -396
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +47 -47
- sentience/formatting.py +9 -53
- sentience/inspector.py +183 -1
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +74 -52
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +60 -1
- sentience/overlay.py +109 -2
- sentience/protocols.py +228 -0
- sentience/query.py +1 -1
- sentience/read.py +95 -3
- sentience/recorder.py +223 -3
- sentience/schemas/trace_v1.json +102 -9
- sentience/screenshot.py +48 -2
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +291 -38
- sentience/snapshot_diff.py +141 -0
- sentience/text_search.py +119 -5
- sentience/trace_event_builder.py +129 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/index_schema.py +95 -7
- sentience/trace_indexing/indexer.py +117 -14
- sentience/tracer_factory.py +119 -6
- sentience/tracing.py +172 -8
- 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 +1 -1
- sentience/visual_agent.py +2056 -0
- sentience/wait.py +68 -2
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/METADATA +2 -1
- sentienceapi-0.92.2.dist-info/RECORD +65 -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.92.2.dist-info}/WHEEL +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ 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
12
|
from .index_schema import (
|
|
13
13
|
ActionInfo,
|
|
@@ -58,13 +58,26 @@ def _compute_snapshot_digest(snapshot_data: dict[str, Any]) -> str:
|
|
|
58
58
|
# Canonicalize elements
|
|
59
59
|
canonical_elements = []
|
|
60
60
|
for elem in elements:
|
|
61
|
+
# Extract is_primary and is_clickable from visual_cues if present
|
|
62
|
+
visual_cues = elem.get("visual_cues", {})
|
|
63
|
+
is_primary = (
|
|
64
|
+
visual_cues.get("is_primary", False)
|
|
65
|
+
if isinstance(visual_cues, dict)
|
|
66
|
+
else elem.get("is_primary", False)
|
|
67
|
+
)
|
|
68
|
+
is_clickable = (
|
|
69
|
+
visual_cues.get("is_clickable", False)
|
|
70
|
+
if isinstance(visual_cues, dict)
|
|
71
|
+
else elem.get("is_clickable", False)
|
|
72
|
+
)
|
|
73
|
+
|
|
61
74
|
canonical_elem = {
|
|
62
75
|
"id": elem.get("id"),
|
|
63
76
|
"role": elem.get("role", ""),
|
|
64
77
|
"text_norm": _normalize_text(elem.get("text")),
|
|
65
78
|
"bbox": _round_bbox(elem.get("bbox", {"x": 0, "y": 0, "width": 0, "height": 0})),
|
|
66
|
-
"is_primary":
|
|
67
|
-
"is_clickable":
|
|
79
|
+
"is_primary": is_primary,
|
|
80
|
+
"is_clickable": is_clickable,
|
|
68
81
|
}
|
|
69
82
|
canonical_elements.append(canonical_elem)
|
|
70
83
|
|
|
@@ -149,15 +162,21 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
149
162
|
event_count = 0
|
|
150
163
|
error_count = 0
|
|
151
164
|
final_url = None
|
|
165
|
+
run_end_status = None # Track status from run_end event
|
|
166
|
+
agent_name = None # Extract from run_start event
|
|
167
|
+
line_count = 0 # Track total line count
|
|
152
168
|
|
|
153
169
|
steps_by_id: dict[str, StepIndex] = {}
|
|
154
170
|
step_order: list[str] = [] # Track order of first appearance
|
|
155
171
|
|
|
156
|
-
# Stream through file, tracking byte offsets
|
|
172
|
+
# Stream through file, tracking byte offsets and line numbers
|
|
157
173
|
with open(trace_path, "rb") as f:
|
|
158
174
|
byte_offset = 0
|
|
175
|
+
line_number = 0 # Track line number for each event
|
|
159
176
|
|
|
160
177
|
for line_bytes in f:
|
|
178
|
+
line_number += 1
|
|
179
|
+
line_count += 1
|
|
161
180
|
line_len = len(line_bytes)
|
|
162
181
|
|
|
163
182
|
try:
|
|
@@ -182,6 +201,10 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
182
201
|
if event_type == "error":
|
|
183
202
|
error_count += 1
|
|
184
203
|
|
|
204
|
+
# Extract agent_name from run_start event
|
|
205
|
+
if event_type == "run_start":
|
|
206
|
+
agent_name = data.get("agent")
|
|
207
|
+
|
|
185
208
|
# Initialize step if first time seeing this step_id
|
|
186
209
|
if step_id not in steps_by_id:
|
|
187
210
|
step_order.append(step_id)
|
|
@@ -189,11 +212,12 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
189
212
|
step_index=len(step_order),
|
|
190
213
|
step_id=step_id,
|
|
191
214
|
goal=None,
|
|
192
|
-
status="
|
|
215
|
+
status="failure", # Default to failure (will be updated by step_end event)
|
|
193
216
|
ts_start=ts,
|
|
194
217
|
ts_end=ts,
|
|
195
218
|
offset_start=byte_offset,
|
|
196
219
|
offset_end=byte_offset + line_len,
|
|
220
|
+
line_number=line_number, # Track line number
|
|
197
221
|
url_before=None,
|
|
198
222
|
url_after=None,
|
|
199
223
|
snapshot_before=SnapshotInfo(),
|
|
@@ -207,6 +231,7 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
207
231
|
# Update step metadata
|
|
208
232
|
step.ts_end = ts
|
|
209
233
|
step.offset_end = byte_offset + line_len
|
|
234
|
+
step.line_number = line_number # Update line number on each event
|
|
210
235
|
step.counters.events += 1
|
|
211
236
|
|
|
212
237
|
# Handle specific event types
|
|
@@ -214,7 +239,8 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
214
239
|
step.goal = data.get("goal")
|
|
215
240
|
step.url_before = data.get("pre_url")
|
|
216
241
|
|
|
217
|
-
elif event_type == "snapshot":
|
|
242
|
+
elif event_type == "snapshot" or event_type == "snapshot_taken":
|
|
243
|
+
# Handle both "snapshot" (current) and "snapshot_taken" (schema) for backward compatibility
|
|
218
244
|
snapshot_id = data.get("snapshot_id")
|
|
219
245
|
url = data.get("url")
|
|
220
246
|
digest = _compute_snapshot_digest(data)
|
|
@@ -231,7 +257,8 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
231
257
|
step.counters.snapshots += 1
|
|
232
258
|
final_url = url
|
|
233
259
|
|
|
234
|
-
elif event_type == "action":
|
|
260
|
+
elif event_type == "action" or event_type == "action_executed":
|
|
261
|
+
# Handle both "action" (current) and "action_executed" (schema) for backward compatibility
|
|
235
262
|
step.action = ActionInfo(
|
|
236
263
|
type=data.get("type"),
|
|
237
264
|
target_element_id=data.get("target_element_id"),
|
|
@@ -240,18 +267,83 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
240
267
|
)
|
|
241
268
|
step.counters.actions += 1
|
|
242
269
|
|
|
243
|
-
elif event_type == "llm_response":
|
|
270
|
+
elif event_type == "llm_response" or event_type == "llm_called":
|
|
271
|
+
# Handle both "llm_response" (current) and "llm_called" (schema) for backward compatibility
|
|
244
272
|
step.counters.llm_calls += 1
|
|
245
273
|
|
|
246
274
|
elif event_type == "error":
|
|
247
|
-
step.status = "
|
|
275
|
+
step.status = "failure"
|
|
248
276
|
|
|
249
277
|
elif event_type == "step_end":
|
|
250
|
-
|
|
251
|
-
|
|
278
|
+
# Determine status from step_end event data
|
|
279
|
+
# Frontend expects: success, failure, or partial
|
|
280
|
+
# Logic: success = exec.success && verify.passed
|
|
281
|
+
# partial = exec.success && !verify.passed
|
|
282
|
+
# failure = !exec.success
|
|
283
|
+
exec_data = data.get("exec", {})
|
|
284
|
+
verify_data = data.get("verify", {})
|
|
285
|
+
|
|
286
|
+
exec_success = exec_data.get("success", False)
|
|
287
|
+
verify_passed = verify_data.get("passed", False)
|
|
288
|
+
|
|
289
|
+
if exec_success and verify_passed:
|
|
290
|
+
step.status = "success"
|
|
291
|
+
elif exec_success and not verify_passed:
|
|
292
|
+
step.status = "partial"
|
|
293
|
+
elif not exec_success:
|
|
294
|
+
step.status = "failure"
|
|
295
|
+
else:
|
|
296
|
+
# Fallback: if step_end exists but no exec/verify data, default to failure
|
|
297
|
+
step.status = "failure"
|
|
298
|
+
|
|
299
|
+
elif event_type == "run_end":
|
|
300
|
+
# Extract status from run_end event
|
|
301
|
+
run_end_status = data.get("status")
|
|
302
|
+
# Validate status value
|
|
303
|
+
if run_end_status not in ["success", "failure", "partial", "unknown"]:
|
|
304
|
+
run_end_status = None
|
|
252
305
|
|
|
253
306
|
byte_offset += line_len
|
|
254
307
|
|
|
308
|
+
# Use run_end status if available, otherwise infer from step statuses
|
|
309
|
+
if run_end_status is None:
|
|
310
|
+
step_statuses = [step.status for step in steps_by_id.values()]
|
|
311
|
+
if step_statuses:
|
|
312
|
+
# Infer overall status from step statuses
|
|
313
|
+
if all(s == "success" for s in step_statuses):
|
|
314
|
+
run_end_status = "success"
|
|
315
|
+
elif any(s == "failure" for s in step_statuses):
|
|
316
|
+
# If any failure and no successes, it's failure; otherwise partial
|
|
317
|
+
if any(s == "success" for s in step_statuses):
|
|
318
|
+
run_end_status = "partial"
|
|
319
|
+
else:
|
|
320
|
+
run_end_status = "failure"
|
|
321
|
+
elif any(s == "partial" for s in step_statuses):
|
|
322
|
+
run_end_status = "partial"
|
|
323
|
+
else:
|
|
324
|
+
run_end_status = "failure" # Default to failure instead of unknown
|
|
325
|
+
else:
|
|
326
|
+
run_end_status = "failure" # Default to failure instead of unknown
|
|
327
|
+
|
|
328
|
+
# Calculate duration
|
|
329
|
+
duration_ms = None
|
|
330
|
+
if first_ts and last_ts:
|
|
331
|
+
try:
|
|
332
|
+
start = datetime.fromisoformat(first_ts.replace("Z", "+00:00"))
|
|
333
|
+
end = datetime.fromisoformat(last_ts.replace("Z", "+00:00"))
|
|
334
|
+
duration_ms = int((end - start).total_seconds() * 1000)
|
|
335
|
+
except (ValueError, AttributeError):
|
|
336
|
+
duration_ms = None
|
|
337
|
+
|
|
338
|
+
# Aggregate counters
|
|
339
|
+
snapshot_count = sum(step.counters.snapshots for step in steps_by_id.values())
|
|
340
|
+
action_count = sum(step.counters.actions for step in steps_by_id.values())
|
|
341
|
+
counters = {
|
|
342
|
+
"snapshot_count": snapshot_count,
|
|
343
|
+
"action_count": action_count,
|
|
344
|
+
"error_count": error_count,
|
|
345
|
+
}
|
|
346
|
+
|
|
255
347
|
# Build summary
|
|
256
348
|
summary = TraceSummary(
|
|
257
349
|
first_ts=first_ts,
|
|
@@ -260,6 +352,10 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
260
352
|
step_count=len(steps_by_id),
|
|
261
353
|
error_count=error_count,
|
|
262
354
|
final_url=final_url,
|
|
355
|
+
status=run_end_status,
|
|
356
|
+
agent_name=agent_name,
|
|
357
|
+
duration_ms=duration_ms,
|
|
358
|
+
counters=counters,
|
|
263
359
|
)
|
|
264
360
|
|
|
265
361
|
# Build steps list in order
|
|
@@ -270,6 +366,7 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
270
366
|
path=str(trace_path),
|
|
271
367
|
size_bytes=os.path.getsize(trace_path),
|
|
272
368
|
sha256=_compute_file_sha256(str(trace_path)),
|
|
369
|
+
line_count=line_count,
|
|
273
370
|
)
|
|
274
371
|
|
|
275
372
|
# Build final index
|
|
@@ -285,13 +382,16 @@ def build_trace_index(trace_path: str) -> TraceIndex:
|
|
|
285
382
|
return index
|
|
286
383
|
|
|
287
384
|
|
|
288
|
-
def write_trace_index(
|
|
385
|
+
def write_trace_index(
|
|
386
|
+
trace_path: str, index_path: str | None = None, frontend_format: bool = False
|
|
387
|
+
) -> str:
|
|
289
388
|
"""
|
|
290
389
|
Build index and write to file.
|
|
291
390
|
|
|
292
391
|
Args:
|
|
293
392
|
trace_path: Path to trace JSONL file
|
|
294
393
|
index_path: Optional custom path for index file (default: trace_path with .index.json)
|
|
394
|
+
frontend_format: If True, write in frontend-compatible format (default: False)
|
|
295
395
|
|
|
296
396
|
Returns:
|
|
297
397
|
Path to written index file
|
|
@@ -301,8 +401,11 @@ def write_trace_index(trace_path: str, index_path: str | None = None) -> str:
|
|
|
301
401
|
|
|
302
402
|
index = build_trace_index(trace_path)
|
|
303
403
|
|
|
304
|
-
with open(index_path, "w") as f:
|
|
305
|
-
|
|
404
|
+
with open(index_path, "w", encoding="utf-8") as f:
|
|
405
|
+
if frontend_format:
|
|
406
|
+
json.dump(index.to_sentience_studio_dict(), f, indent=2)
|
|
407
|
+
else:
|
|
408
|
+
json.dump(index.to_dict(), f, indent=2)
|
|
306
409
|
|
|
307
410
|
return index_path
|
|
308
411
|
|
sentience/tracer_factory.py
CHANGED
|
@@ -7,7 +7,9 @@ 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
|
|
|
@@ -24,6 +26,11 @@ def create_tracer(
|
|
|
24
26
|
api_url: str | None = None,
|
|
25
27
|
logger: SentienceLogger | None = None,
|
|
26
28
|
upload_trace: bool = False,
|
|
29
|
+
goal: str | None = None,
|
|
30
|
+
agent_type: str | None = None,
|
|
31
|
+
llm_model: str | None = None,
|
|
32
|
+
start_url: str | None = None,
|
|
33
|
+
screenshot_processor: Callable[[str], str] | None = None,
|
|
27
34
|
) -> Tracer:
|
|
28
35
|
"""
|
|
29
36
|
Create tracer with automatic tier detection.
|
|
@@ -42,15 +49,42 @@ def create_tracer(
|
|
|
42
49
|
upload_trace: Enable cloud trace upload (default: False). When True and api_key
|
|
43
50
|
is provided, traces will be uploaded to cloud. When False, traces
|
|
44
51
|
are saved locally only.
|
|
52
|
+
goal: User's goal/objective for this trace run. This will be displayed as the
|
|
53
|
+
trace name in the frontend. Should be descriptive and action-oriented.
|
|
54
|
+
Example: "Add wireless headphones to cart on Amazon"
|
|
55
|
+
agent_type: Type of agent running (e.g., "SentienceAgent", "CustomAgent")
|
|
56
|
+
llm_model: LLM model used (e.g., "gpt-4-turbo", "claude-3-5-sonnet")
|
|
57
|
+
start_url: Starting URL of the agent run (e.g., "https://amazon.com")
|
|
58
|
+
screenshot_processor: Optional function to process screenshots before upload.
|
|
59
|
+
Takes base64 string, returns processed base64 string.
|
|
60
|
+
Useful for PII redaction or custom image processing.
|
|
45
61
|
|
|
46
62
|
Returns:
|
|
47
63
|
Tracer configured with appropriate sink
|
|
48
64
|
|
|
49
65
|
Example:
|
|
50
|
-
>>> # Pro tier user
|
|
51
|
-
>>> tracer = create_tracer(
|
|
66
|
+
>>> # Pro tier user with goal
|
|
67
|
+
>>> tracer = create_tracer(
|
|
68
|
+
... api_key="sk_pro_xyz",
|
|
69
|
+
... run_id="demo",
|
|
70
|
+
... goal="Add headphones to cart",
|
|
71
|
+
... agent_type="SentienceAgent",
|
|
72
|
+
... llm_model="gpt-4-turbo",
|
|
73
|
+
... start_url="https://amazon.com"
|
|
74
|
+
... )
|
|
52
75
|
>>> # Returns: Tracer with CloudTraceSink
|
|
53
76
|
>>>
|
|
77
|
+
>>> # With screenshot processor for PII redaction
|
|
78
|
+
>>> def redact_pii(screenshot_base64: str) -> str:
|
|
79
|
+
... # Your custom redaction logic
|
|
80
|
+
... return redacted_screenshot
|
|
81
|
+
>>>
|
|
82
|
+
>>> tracer = create_tracer(
|
|
83
|
+
... api_key="sk_pro_xyz",
|
|
84
|
+
... screenshot_processor=redact_pii
|
|
85
|
+
... )
|
|
86
|
+
>>> # Screenshots will be processed before upload
|
|
87
|
+
>>>
|
|
54
88
|
>>> # Free tier user
|
|
55
89
|
>>> tracer = create_tracer(run_id="demo")
|
|
56
90
|
>>> # Returns: Tracer with JsonlTraceSink (local-only)
|
|
@@ -73,11 +107,28 @@ def create_tracer(
|
|
|
73
107
|
# 1. Try to initialize Cloud Sink (Pro/Enterprise tier) if upload enabled
|
|
74
108
|
if api_key and upload_trace:
|
|
75
109
|
try:
|
|
110
|
+
# Build metadata object for trace initialization
|
|
111
|
+
# Only include non-empty fields to avoid sending empty strings
|
|
112
|
+
metadata: dict[str, str] = {}
|
|
113
|
+
if goal and goal.strip():
|
|
114
|
+
metadata["goal"] = goal.strip()
|
|
115
|
+
if agent_type and agent_type.strip():
|
|
116
|
+
metadata["agent_type"] = agent_type.strip()
|
|
117
|
+
if llm_model and llm_model.strip():
|
|
118
|
+
metadata["llm_model"] = llm_model.strip()
|
|
119
|
+
if start_url and start_url.strip():
|
|
120
|
+
metadata["start_url"] = start_url.strip()
|
|
121
|
+
|
|
122
|
+
# Build request payload
|
|
123
|
+
payload: dict[str, Any] = {"run_id": run_id}
|
|
124
|
+
if metadata:
|
|
125
|
+
payload["metadata"] = metadata
|
|
126
|
+
|
|
76
127
|
# Request pre-signed upload URL from backend
|
|
77
128
|
response = requests.post(
|
|
78
129
|
f"{api_url}/v1/traces/init",
|
|
79
130
|
headers={"Authorization": f"Bearer {api_key}"},
|
|
80
|
-
json=
|
|
131
|
+
json=payload,
|
|
81
132
|
timeout=10,
|
|
82
133
|
)
|
|
83
134
|
|
|
@@ -96,16 +147,46 @@ def create_tracer(
|
|
|
96
147
|
api_url=api_url,
|
|
97
148
|
logger=logger,
|
|
98
149
|
),
|
|
150
|
+
screenshot_processor=screenshot_processor,
|
|
99
151
|
)
|
|
100
152
|
else:
|
|
101
153
|
print("⚠️ [Sentience] Cloud init response missing upload_url")
|
|
154
|
+
print(f" Response data: {data}")
|
|
102
155
|
print(" Falling back to local-only tracing")
|
|
103
156
|
|
|
104
157
|
elif response.status_code == 403:
|
|
105
158
|
print("⚠️ [Sentience] Cloud tracing requires Pro tier")
|
|
159
|
+
try:
|
|
160
|
+
error_data = response.json()
|
|
161
|
+
error_msg = error_data.get("error") or error_data.get("message", "")
|
|
162
|
+
if error_msg:
|
|
163
|
+
print(f" API Error: {error_msg}")
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
print(" Falling back to local-only tracing")
|
|
167
|
+
elif response.status_code == 401:
|
|
168
|
+
print("⚠️ [Sentience] Cloud init failed: HTTP 401 Unauthorized")
|
|
169
|
+
print(" API key is invalid or expired")
|
|
170
|
+
try:
|
|
171
|
+
error_data = response.json()
|
|
172
|
+
error_msg = error_data.get("error") or error_data.get("message", "")
|
|
173
|
+
if error_msg:
|
|
174
|
+
print(f" API Error: {error_msg}")
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
106
177
|
print(" Falling back to local-only tracing")
|
|
107
178
|
else:
|
|
108
179
|
print(f"⚠️ [Sentience] Cloud init failed: HTTP {response.status_code}")
|
|
180
|
+
try:
|
|
181
|
+
error_data = response.json()
|
|
182
|
+
error_msg = error_data.get("error") or error_data.get(
|
|
183
|
+
"message", "Unknown error"
|
|
184
|
+
)
|
|
185
|
+
print(f" Error: {error_msg}")
|
|
186
|
+
if "tier" in error_msg.lower() or "subscription" in error_msg.lower():
|
|
187
|
+
print(f" 💡 This may be a tier/subscription issue")
|
|
188
|
+
except Exception:
|
|
189
|
+
print(f" Response: {response.text[:200]}")
|
|
109
190
|
print(" Falling back to local-only tracing")
|
|
110
191
|
|
|
111
192
|
except requests.exceptions.Timeout:
|
|
@@ -125,7 +206,11 @@ def create_tracer(
|
|
|
125
206
|
local_path = traces_dir / f"{run_id}.jsonl"
|
|
126
207
|
print(f"💾 [Sentience] Local tracing: {local_path}")
|
|
127
208
|
|
|
128
|
-
return Tracer(
|
|
209
|
+
return Tracer(
|
|
210
|
+
run_id=run_id,
|
|
211
|
+
sink=JsonlTraceSink(str(local_path)),
|
|
212
|
+
screenshot_processor=screenshot_processor,
|
|
213
|
+
)
|
|
129
214
|
|
|
130
215
|
|
|
131
216
|
def _recover_orphaned_traces(api_key: str, api_url: str = SENTIENCE_API_URL) -> None:
|
|
@@ -149,10 +234,23 @@ def _recover_orphaned_traces(api_key: str, api_url: str = SENTIENCE_API_URL) ->
|
|
|
149
234
|
if not orphaned:
|
|
150
235
|
return
|
|
151
236
|
|
|
152
|
-
|
|
237
|
+
# Filter out test files (run_ids that start with "test-" or are clearly test data)
|
|
238
|
+
# These are likely from local testing and shouldn't be uploaded
|
|
239
|
+
test_patterns = ["test-", "test_", "test."]
|
|
240
|
+
valid_orphaned = [
|
|
241
|
+
f
|
|
242
|
+
for f in orphaned
|
|
243
|
+
if not any(f.stem.startswith(pattern) for pattern in test_patterns)
|
|
244
|
+
and not f.stem.startswith("test")
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
if not valid_orphaned:
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
print(f"⚠️ [Sentience] Found {len(valid_orphaned)} un-uploaded trace(s) from previous runs")
|
|
153
251
|
print(" Attempting to upload now...")
|
|
154
252
|
|
|
155
|
-
for trace_file in
|
|
253
|
+
for trace_file in valid_orphaned:
|
|
156
254
|
try:
|
|
157
255
|
# Extract run_id from filename (format: {run_id}.jsonl)
|
|
158
256
|
run_id = trace_file.stem
|
|
@@ -166,6 +264,21 @@ def _recover_orphaned_traces(api_key: str, api_url: str = SENTIENCE_API_URL) ->
|
|
|
166
264
|
)
|
|
167
265
|
|
|
168
266
|
if response.status_code != 200:
|
|
267
|
+
# HTTP 409 means trace already exists (already uploaded)
|
|
268
|
+
# Treat as success and delete local file
|
|
269
|
+
if response.status_code == 409:
|
|
270
|
+
print(f"✅ Trace {run_id} already exists in cloud (skipping re-upload)")
|
|
271
|
+
# Delete local file since it's already in cloud
|
|
272
|
+
try:
|
|
273
|
+
os.remove(trace_file)
|
|
274
|
+
except Exception:
|
|
275
|
+
pass # Ignore cleanup errors
|
|
276
|
+
continue
|
|
277
|
+
# HTTP 422 typically means invalid run_id (e.g., test files)
|
|
278
|
+
# Skip silently for 422, but log other errors
|
|
279
|
+
if response.status_code == 422:
|
|
280
|
+
# Likely a test file or invalid run_id, skip silently
|
|
281
|
+
continue
|
|
169
282
|
print(f"❌ Failed to get upload URL for {run_id}: HTTP {response.status_code}")
|
|
170
283
|
continue
|
|
171
284
|
|