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.

Files changed (61) hide show
  1. sentience/__init__.py +14 -5
  2. sentience/action_executor.py +215 -0
  3. sentience/actions.py +408 -25
  4. sentience/agent.py +802 -293
  5. sentience/agent_config.py +3 -0
  6. sentience/async_api.py +83 -1142
  7. sentience/base_agent.py +95 -0
  8. sentience/browser.py +484 -1
  9. sentience/browser_evaluator.py +299 -0
  10. sentience/cloud_tracing.py +457 -33
  11. sentience/conversational_agent.py +77 -43
  12. sentience/element_filter.py +136 -0
  13. sentience/expect.py +98 -2
  14. sentience/extension/background.js +56 -185
  15. sentience/extension/content.js +117 -289
  16. sentience/extension/injected_api.js +799 -1374
  17. sentience/extension/manifest.json +1 -1
  18. sentience/extension/pkg/sentience_core.js +190 -396
  19. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  20. sentience/extension/release.json +47 -47
  21. sentience/formatting.py +9 -53
  22. sentience/inspector.py +183 -1
  23. sentience/llm_interaction_handler.py +191 -0
  24. sentience/llm_provider.py +74 -52
  25. sentience/llm_provider_utils.py +120 -0
  26. sentience/llm_response_builder.py +153 -0
  27. sentience/models.py +60 -1
  28. sentience/overlay.py +109 -2
  29. sentience/protocols.py +228 -0
  30. sentience/query.py +1 -1
  31. sentience/read.py +95 -3
  32. sentience/recorder.py +223 -3
  33. sentience/schemas/trace_v1.json +102 -9
  34. sentience/screenshot.py +48 -2
  35. sentience/sentience_methods.py +86 -0
  36. sentience/snapshot.py +291 -38
  37. sentience/snapshot_diff.py +141 -0
  38. sentience/text_search.py +119 -5
  39. sentience/trace_event_builder.py +129 -0
  40. sentience/trace_file_manager.py +197 -0
  41. sentience/trace_indexing/index_schema.py +95 -7
  42. sentience/trace_indexing/indexer.py +117 -14
  43. sentience/tracer_factory.py +119 -6
  44. sentience/tracing.py +172 -8
  45. sentience/utils/__init__.py +40 -0
  46. sentience/utils/browser.py +46 -0
  47. sentience/utils/element.py +257 -0
  48. sentience/utils/formatting.py +59 -0
  49. sentience/utils.py +1 -1
  50. sentience/visual_agent.py +2056 -0
  51. sentience/wait.py +68 -2
  52. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/METADATA +2 -1
  53. sentienceapi-0.92.2.dist-info/RECORD +65 -0
  54. sentience/extension/test-content.js +0 -4
  55. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  56. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/WHEEL +0 -0
  57. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/entry_points.txt +0 -0
  58. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE +0 -0
  59. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE-APACHE +0 -0
  60. {sentienceapi-0.90.16.dist-info → sentienceapi-0.92.2.dist-info}/licenses/LICENSE-MIT +0 -0
  61. {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, Dict, List
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": elem.get("is_primary", False),
67
- "is_clickable": elem.get("is_clickable", False),
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="partial",
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 = "error"
275
+ step.status = "failure"
248
276
 
249
277
  elif event_type == "step_end":
250
- if step.status != "error":
251
- step.status = "ok"
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(trace_path: str, index_path: str | None = None) -> str:
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
- json.dump(index.to_dict(), f, indent=2)
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
 
@@ -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(api_key="sk_pro_xyz", run_id="demo")
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={"run_id": run_id},
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(run_id=run_id, sink=JsonlTraceSink(str(local_path)))
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
- print(f"⚠️ [Sentience] Found {len(orphaned)} un-uploaded trace(s) from previous runs")
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 orphaned:
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