sentienceapi 0.90.17__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 (50) hide show
  1. sentience/__init__.py +153 -0
  2. sentience/_extension_loader.py +40 -0
  3. sentience/actions.py +837 -0
  4. sentience/agent.py +1246 -0
  5. sentience/agent_config.py +43 -0
  6. sentience/async_api.py +101 -0
  7. sentience/base_agent.py +194 -0
  8. sentience/browser.py +1037 -0
  9. sentience/cli.py +130 -0
  10. sentience/cloud_tracing.py +382 -0
  11. sentience/conversational_agent.py +509 -0
  12. sentience/expect.py +188 -0
  13. sentience/extension/background.js +233 -0
  14. sentience/extension/content.js +298 -0
  15. sentience/extension/injected_api.js +1473 -0
  16. sentience/extension/manifest.json +36 -0
  17. sentience/extension/pkg/sentience_core.d.ts +51 -0
  18. sentience/extension/pkg/sentience_core.js +529 -0
  19. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  20. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  21. sentience/extension/release.json +115 -0
  22. sentience/extension/test-content.js +4 -0
  23. sentience/formatting.py +59 -0
  24. sentience/generator.py +202 -0
  25. sentience/inspector.py +365 -0
  26. sentience/llm_provider.py +637 -0
  27. sentience/models.py +412 -0
  28. sentience/overlay.py +222 -0
  29. sentience/query.py +303 -0
  30. sentience/read.py +185 -0
  31. sentience/recorder.py +589 -0
  32. sentience/schemas/trace_v1.json +216 -0
  33. sentience/screenshot.py +100 -0
  34. sentience/snapshot.py +516 -0
  35. sentience/text_search.py +290 -0
  36. sentience/trace_indexing/__init__.py +27 -0
  37. sentience/trace_indexing/index_schema.py +111 -0
  38. sentience/trace_indexing/indexer.py +357 -0
  39. sentience/tracer_factory.py +211 -0
  40. sentience/tracing.py +285 -0
  41. sentience/utils.py +296 -0
  42. sentience/wait.py +137 -0
  43. sentienceapi-0.90.17.dist-info/METADATA +917 -0
  44. sentienceapi-0.90.17.dist-info/RECORD +50 -0
  45. sentienceapi-0.90.17.dist-info/WHEEL +5 -0
  46. sentienceapi-0.90.17.dist-info/entry_points.txt +2 -0
  47. sentienceapi-0.90.17.dist-info/licenses/LICENSE +24 -0
  48. sentienceapi-0.90.17.dist-info/licenses/LICENSE-APACHE +201 -0
  49. sentienceapi-0.90.17.dist-info/licenses/LICENSE-MIT +21 -0
  50. sentienceapi-0.90.17.dist-info/top_level.txt +1 -0
sentience/cli.py ADDED
@@ -0,0 +1,130 @@
1
+ """
2
+ CLI commands for Sentience SDK
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from .browser import SentienceBrowser
9
+ from .generator import ScriptGenerator
10
+ from .inspector import inspect
11
+ from .recorder import Trace, record
12
+
13
+
14
+ def cmd_inspect(args):
15
+ """Start inspector mode"""
16
+ browser = SentienceBrowser(headless=False)
17
+ try:
18
+ browser.start()
19
+ print("āœ… Inspector started. Hover elements to see info, click to see full details.")
20
+ print("Press Ctrl+C to stop.")
21
+
22
+ with inspect(browser):
23
+ # Keep running until interrupted
24
+ import time
25
+
26
+ try:
27
+ while True:
28
+ time.sleep(1)
29
+ except KeyboardInterrupt:
30
+ print("\nšŸ‘‹ Inspector stopped.")
31
+ finally:
32
+ browser.close()
33
+
34
+
35
+ def cmd_record(args):
36
+ """Start recording mode"""
37
+ browser = SentienceBrowser(headless=False)
38
+ try:
39
+ browser.start()
40
+
41
+ # Navigate to start URL if provided
42
+ if args.url:
43
+ browser.page.goto(args.url)
44
+ browser.page.wait_for_load_state("networkidle")
45
+
46
+ print("āœ… Recording started. Perform actions in the browser.")
47
+ print("Press Ctrl+C to stop and save trace.")
48
+
49
+ with record(browser, capture_snapshots=args.snapshots) as rec:
50
+ # Add mask patterns if provided
51
+ for pattern in args.mask or []:
52
+ rec.add_mask_pattern(pattern)
53
+
54
+ # Keep running until interrupted
55
+ import time
56
+
57
+ try:
58
+ while True:
59
+ time.sleep(1)
60
+ except KeyboardInterrupt:
61
+ print("\nšŸ’¾ Saving trace...")
62
+ output = args.output or "trace.json"
63
+ rec.save(output)
64
+ print(f"āœ… Trace saved to {output}")
65
+ finally:
66
+ browser.close()
67
+
68
+
69
+ def cmd_gen(args):
70
+ """Generate script from trace"""
71
+ # Load trace
72
+ trace = Trace.load(args.trace)
73
+
74
+ # Generate script
75
+ generator = ScriptGenerator(trace)
76
+
77
+ if args.lang == "py":
78
+ output = args.output or "generated.py"
79
+ generator.save_python(output)
80
+ elif args.lang == "ts":
81
+ output = args.output or "generated.ts"
82
+ generator.save_typescript(output)
83
+ else:
84
+ print(f"āŒ Unsupported language: {args.lang}")
85
+ sys.exit(1)
86
+
87
+ print(f"āœ… Generated {args.lang.upper()} script: {output}")
88
+
89
+
90
+ def main():
91
+ """Main CLI entry point"""
92
+ parser = argparse.ArgumentParser(description="Sentience SDK CLI")
93
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
94
+
95
+ # Inspect command
96
+ inspect_parser = subparsers.add_parser("inspect", help="Start inspector mode")
97
+ inspect_parser.set_defaults(func=cmd_inspect)
98
+
99
+ # Record command
100
+ record_parser = subparsers.add_parser("record", help="Start recording mode")
101
+ record_parser.add_argument("--url", help="Start URL")
102
+ record_parser.add_argument("--output", "-o", help="Output trace file", default="trace.json")
103
+ record_parser.add_argument(
104
+ "--snapshots", action="store_true", help="Capture snapshots at each step"
105
+ )
106
+ record_parser.add_argument(
107
+ "--mask",
108
+ action="append",
109
+ help="Pattern to mask in recorded text (e.g., password)",
110
+ )
111
+ record_parser.set_defaults(func=cmd_record)
112
+
113
+ # Generate command
114
+ gen_parser = subparsers.add_parser("gen", help="Generate script from trace")
115
+ gen_parser.add_argument("trace", help="Trace JSON file")
116
+ gen_parser.add_argument("--lang", choices=["py", "ts"], default="py", help="Output language")
117
+ gen_parser.add_argument("--output", "-o", help="Output script file")
118
+ gen_parser.set_defaults(func=cmd_gen)
119
+
120
+ args = parser.parse_args()
121
+
122
+ if not args.command:
123
+ parser.print_help()
124
+ sys.exit(1)
125
+
126
+ args.func(args)
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
@@ -0,0 +1,382 @@
1
+ """
2
+ Cloud trace sink with pre-signed URL upload.
3
+
4
+ Implements "Local Write, Batch Upload" pattern for enterprise cloud tracing.
5
+ """
6
+
7
+ import gzip
8
+ import json
9
+ import os
10
+ import threading
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import Any, Protocol
14
+
15
+ import requests
16
+
17
+ from sentience.tracing import TraceSink
18
+
19
+
20
+ class SentienceLogger(Protocol):
21
+ """Protocol for optional logger interface."""
22
+
23
+ def info(self, message: str) -> None:
24
+ """Log info message."""
25
+ ...
26
+
27
+ def warning(self, message: str) -> None:
28
+ """Log warning message."""
29
+ ...
30
+
31
+ def error(self, message: str) -> None:
32
+ """Log error message."""
33
+ ...
34
+
35
+
36
+ class CloudTraceSink(TraceSink):
37
+ """
38
+ Enterprise Cloud Sink: "Local Write, Batch Upload" pattern.
39
+
40
+ Architecture:
41
+ 1. **Local Buffer**: Writes to persistent cache directory (zero latency, non-blocking)
42
+ 2. **Pre-signed URL**: Uses secure pre-signed PUT URL from backend API
43
+ 3. **Batch Upload**: Uploads complete file on close() or at intervals
44
+ 4. **Zero Credential Exposure**: Never embeds DigitalOcean credentials in SDK
45
+ 5. **Crash Recovery**: Traces survive process crashes (stored in ~/.sentience/traces/pending/)
46
+
47
+ This design ensures:
48
+ - Fast agent performance (microseconds per emit, not milliseconds)
49
+ - Security (credentials stay on backend)
50
+ - Reliability (network issues don't crash the agent)
51
+ - Data durability (traces survive crashes and can be recovered)
52
+
53
+ Tiered Access:
54
+ - Free Tier: Falls back to JsonlTraceSink (local-only)
55
+ - Pro/Enterprise: Uploads to cloud via pre-signed URLs
56
+
57
+ Example:
58
+ >>> from sentience.cloud_tracing import CloudTraceSink
59
+ >>> from sentience.tracing import Tracer
60
+ >>> # Get upload URL from API
61
+ >>> upload_url = "https://sentience.nyc3.digitaloceanspaces.com/..."
62
+ >>> sink = CloudTraceSink(upload_url, run_id="demo")
63
+ >>> tracer = Tracer(run_id="demo", sink=sink)
64
+ >>> tracer.emit_run_start("SentienceAgent")
65
+ >>> tracer.close() # Uploads to cloud
66
+ >>> # Or non-blocking:
67
+ >>> tracer.close(blocking=False) # Returns immediately
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ upload_url: str,
73
+ run_id: str,
74
+ api_key: str | None = None,
75
+ api_url: str | None = None,
76
+ logger: SentienceLogger | None = None,
77
+ ):
78
+ """
79
+ Initialize cloud trace sink.
80
+
81
+ Args:
82
+ upload_url: Pre-signed PUT URL from Sentience API
83
+ (e.g., "https://sentience.nyc3.digitaloceanspaces.com/...")
84
+ run_id: Unique identifier for this agent run (used for persistent cache)
85
+ api_key: Sentience API key for calling /v1/traces/complete
86
+ api_url: Sentience API base URL (default: https://api.sentienceapi.com)
87
+ logger: Optional logger instance for logging file sizes and errors
88
+ """
89
+ self.upload_url = upload_url
90
+ self.run_id = run_id
91
+ self.api_key = api_key
92
+ self.api_url = api_url or "https://api.sentienceapi.com"
93
+ self.logger = logger
94
+
95
+ # Use persistent cache directory instead of temp file
96
+ # This ensures traces survive process crashes
97
+ cache_dir = Path.home() / ".sentience" / "traces" / "pending"
98
+ cache_dir.mkdir(parents=True, exist_ok=True)
99
+
100
+ # Persistent file (survives process crash)
101
+ self._path = cache_dir / f"{run_id}.jsonl"
102
+ self._trace_file = open(self._path, "w", encoding="utf-8")
103
+ self._closed = False
104
+ self._upload_successful = False
105
+
106
+ # File size tracking (NEW)
107
+ self.trace_file_size_bytes = 0
108
+ self.screenshot_total_size_bytes = 0
109
+
110
+ def emit(self, event: dict[str, Any]) -> None:
111
+ """
112
+ Write event to local persistent file (Fast, non-blocking).
113
+
114
+ Performance: ~10 microseconds per write vs ~50ms for HTTP request
115
+
116
+ Args:
117
+ event: Event dictionary from TraceEvent.to_dict()
118
+ """
119
+ if self._closed:
120
+ raise RuntimeError("CloudTraceSink is closed")
121
+
122
+ json_str = json.dumps(event, ensure_ascii=False)
123
+ self._trace_file.write(json_str + "\n")
124
+ self._trace_file.flush() # Ensure written to disk
125
+
126
+ def close(
127
+ self,
128
+ blocking: bool = True,
129
+ on_progress: Callable[[int, int], None] | None = None,
130
+ ) -> None:
131
+ """
132
+ Upload buffered trace to cloud via pre-signed URL.
133
+
134
+ Args:
135
+ blocking: If False, returns immediately and uploads in background thread
136
+ on_progress: Optional callback(uploaded_bytes, total_bytes) for progress updates
137
+
138
+ This is the only network call - happens once at the end.
139
+ """
140
+ if self._closed:
141
+ return
142
+
143
+ self._closed = True
144
+
145
+ # Close file first
146
+ self._trace_file.close()
147
+
148
+ # Generate index after closing file
149
+ self._generate_index()
150
+
151
+ if not blocking:
152
+ # Fire-and-forget background upload
153
+ thread = threading.Thread(
154
+ target=self._do_upload,
155
+ args=(on_progress,),
156
+ daemon=True,
157
+ )
158
+ thread.start()
159
+ return # Return immediately
160
+
161
+ # Blocking mode
162
+ self._do_upload(on_progress)
163
+
164
+ def _do_upload(self, on_progress: Callable[[int, int], None] | None = None) -> None:
165
+ """
166
+ Internal upload method with progress tracking.
167
+
168
+ Args:
169
+ on_progress: Optional callback(uploaded_bytes, total_bytes) for progress updates
170
+ """
171
+ try:
172
+ # Read and compress
173
+ with open(self._path, "rb") as f:
174
+ trace_data = f.read()
175
+
176
+ compressed_data = gzip.compress(trace_data)
177
+ compressed_size = len(compressed_data)
178
+
179
+ # Measure trace file size (NEW)
180
+ self.trace_file_size_bytes = compressed_size
181
+
182
+ # Log file sizes if logger is provided (NEW)
183
+ if self.logger:
184
+ self.logger.info(
185
+ f"Trace file size: {self.trace_file_size_bytes / 1024 / 1024:.2f} MB"
186
+ )
187
+ self.logger.info(
188
+ f"Screenshot total: {self.screenshot_total_size_bytes / 1024 / 1024:.2f} MB"
189
+ )
190
+
191
+ # Report progress: start
192
+ if on_progress:
193
+ on_progress(0, compressed_size)
194
+
195
+ # Upload to DigitalOcean Spaces via pre-signed URL
196
+ print(f"šŸ“¤ [Sentience] Uploading trace to cloud ({compressed_size} bytes)...")
197
+
198
+ response = requests.put(
199
+ self.upload_url,
200
+ data=compressed_data,
201
+ headers={
202
+ "Content-Type": "application/x-gzip",
203
+ "Content-Encoding": "gzip",
204
+ },
205
+ timeout=60, # 1 minute timeout for large files
206
+ )
207
+
208
+ if response.status_code == 200:
209
+ self._upload_successful = True
210
+ print("āœ… [Sentience] Trace uploaded successfully")
211
+
212
+ # Report progress: complete
213
+ if on_progress:
214
+ on_progress(compressed_size, compressed_size)
215
+
216
+ # Upload trace index file
217
+ self._upload_index()
218
+
219
+ # Call /v1/traces/complete to report file sizes
220
+ self._complete_trace()
221
+
222
+ # Delete file only on successful upload
223
+ if os.path.exists(self._path):
224
+ try:
225
+ os.remove(self._path)
226
+ except Exception:
227
+ pass # Ignore cleanup errors
228
+ else:
229
+ self._upload_successful = False
230
+ print(f"āŒ [Sentience] Upload failed: HTTP {response.status_code}")
231
+ print(f" Response: {response.text}")
232
+ print(f" Local trace preserved at: {self._path}")
233
+
234
+ except Exception as e:
235
+ self._upload_successful = False
236
+ print(f"āŒ [Sentience] Error uploading trace: {e}")
237
+ print(f" Local trace preserved at: {self._path}")
238
+ # Don't raise - preserve trace locally even if upload fails
239
+
240
+ def _generate_index(self) -> None:
241
+ """Generate trace index file (automatic on close)."""
242
+ try:
243
+ from .trace_indexing import write_trace_index
244
+
245
+ write_trace_index(str(self._path))
246
+ except Exception as e:
247
+ # Non-fatal: log but don't crash
248
+ print(f"āš ļø Failed to generate trace index: {e}")
249
+
250
+ def _upload_index(self) -> None:
251
+ """
252
+ Upload trace index file to cloud storage.
253
+
254
+ Called after successful trace upload to provide fast timeline rendering.
255
+ The index file enables O(1) step lookups without parsing the entire trace.
256
+ """
257
+ # Construct index file path (same as trace file with .index.json extension)
258
+ index_path = Path(str(self._path).replace(".jsonl", ".index.json"))
259
+
260
+ if not index_path.exists():
261
+ if self.logger:
262
+ self.logger.warning("Index file not found, skipping index upload")
263
+ return
264
+
265
+ try:
266
+ # Request index upload URL from API
267
+ if not self.api_key:
268
+ # No API key - skip index upload
269
+ if self.logger:
270
+ self.logger.info("No API key provided, skipping index upload")
271
+ return
272
+
273
+ response = requests.post(
274
+ f"{self.api_url}/v1/traces/index_upload",
275
+ headers={"Authorization": f"Bearer {self.api_key}"},
276
+ json={"run_id": self.run_id},
277
+ timeout=10,
278
+ )
279
+
280
+ if response.status_code != 200:
281
+ if self.logger:
282
+ self.logger.warning(
283
+ f"Failed to get index upload URL: HTTP {response.status_code}"
284
+ )
285
+ return
286
+
287
+ upload_data = response.json()
288
+ index_upload_url = upload_data.get("upload_url")
289
+
290
+ if not index_upload_url:
291
+ if self.logger:
292
+ self.logger.warning("No upload URL in index upload response")
293
+ return
294
+
295
+ # Read and compress index file
296
+ with open(index_path, "rb") as f:
297
+ index_data = f.read()
298
+
299
+ compressed_index = gzip.compress(index_data)
300
+ index_size = len(compressed_index)
301
+
302
+ if self.logger:
303
+ self.logger.info(f"Index file size: {index_size / 1024:.2f} KB")
304
+
305
+ print(f"šŸ“¤ [Sentience] Uploading trace index ({index_size} bytes)...")
306
+
307
+ # Upload index to cloud storage
308
+ index_response = requests.put(
309
+ index_upload_url,
310
+ data=compressed_index,
311
+ headers={
312
+ "Content-Type": "application/json",
313
+ "Content-Encoding": "gzip",
314
+ },
315
+ timeout=30,
316
+ )
317
+
318
+ if index_response.status_code == 200:
319
+ print("āœ… [Sentience] Trace index uploaded successfully")
320
+
321
+ # Delete local index file after successful upload
322
+ try:
323
+ os.remove(index_path)
324
+ except Exception:
325
+ pass # Ignore cleanup errors
326
+ else:
327
+ if self.logger:
328
+ self.logger.warning(f"Index upload failed: HTTP {index_response.status_code}")
329
+ print(f"āš ļø [Sentience] Index upload failed: HTTP {index_response.status_code}")
330
+
331
+ except Exception as e:
332
+ # Non-fatal: log but don't crash
333
+ if self.logger:
334
+ self.logger.warning(f"Error uploading trace index: {e}")
335
+ print(f"āš ļø [Sentience] Error uploading trace index: {e}")
336
+
337
+ def _complete_trace(self) -> None:
338
+ """
339
+ Call /v1/traces/complete to report file sizes to gateway.
340
+
341
+ This is a best-effort call - failures are logged but don't affect upload success.
342
+ """
343
+ if not self.api_key:
344
+ # No API key - skip complete call
345
+ return
346
+
347
+ try:
348
+ response = requests.post(
349
+ f"{self.api_url}/v1/traces/complete",
350
+ headers={"Authorization": f"Bearer {self.api_key}"},
351
+ json={
352
+ "run_id": self.run_id,
353
+ "stats": {
354
+ "trace_file_size_bytes": self.trace_file_size_bytes,
355
+ "screenshot_total_size_bytes": self.screenshot_total_size_bytes,
356
+ },
357
+ },
358
+ timeout=10,
359
+ )
360
+
361
+ if response.status_code == 200:
362
+ if self.logger:
363
+ self.logger.info("Trace completion reported to gateway")
364
+ else:
365
+ if self.logger:
366
+ self.logger.warning(
367
+ f"Failed to report trace completion: HTTP {response.status_code}"
368
+ )
369
+
370
+ except Exception as e:
371
+ # Best-effort - log but don't fail
372
+ if self.logger:
373
+ self.logger.warning(f"Error reporting trace completion: {e}")
374
+
375
+ def __enter__(self):
376
+ """Context manager support."""
377
+ return self
378
+
379
+ def __exit__(self, exc_type, exc_val, exc_tb):
380
+ """Context manager cleanup."""
381
+ self.close()
382
+ return False