sentienceapi 0.90.11__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.
Files changed (46) hide show
  1. sentience/__init__.py +153 -0
  2. sentience/actions.py +439 -0
  3. sentience/agent.py +687 -0
  4. sentience/agent_config.py +43 -0
  5. sentience/base_agent.py +101 -0
  6. sentience/browser.py +409 -0
  7. sentience/cli.py +130 -0
  8. sentience/cloud_tracing.py +292 -0
  9. sentience/conversational_agent.py +509 -0
  10. sentience/expect.py +92 -0
  11. sentience/extension/background.js +233 -0
  12. sentience/extension/content.js +298 -0
  13. sentience/extension/injected_api.js +1473 -0
  14. sentience/extension/manifest.json +36 -0
  15. sentience/extension/pkg/sentience_core.d.ts +51 -0
  16. sentience/extension/pkg/sentience_core.js +529 -0
  17. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  18. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  19. sentience/extension/release.json +115 -0
  20. sentience/extension/test-content.js +4 -0
  21. sentience/formatting.py +59 -0
  22. sentience/generator.py +202 -0
  23. sentience/inspector.py +185 -0
  24. sentience/llm_provider.py +431 -0
  25. sentience/models.py +406 -0
  26. sentience/overlay.py +115 -0
  27. sentience/query.py +303 -0
  28. sentience/read.py +96 -0
  29. sentience/recorder.py +369 -0
  30. sentience/schemas/trace_v1.json +216 -0
  31. sentience/screenshot.py +54 -0
  32. sentience/snapshot.py +282 -0
  33. sentience/text_search.py +150 -0
  34. sentience/trace_indexing/__init__.py +27 -0
  35. sentience/trace_indexing/index_schema.py +111 -0
  36. sentience/trace_indexing/indexer.py +363 -0
  37. sentience/tracer_factory.py +211 -0
  38. sentience/tracing.py +285 -0
  39. sentience/utils.py +296 -0
  40. sentience/wait.py +73 -0
  41. sentienceapi-0.90.11.dist-info/METADATA +878 -0
  42. sentienceapi-0.90.11.dist-info/RECORD +46 -0
  43. sentienceapi-0.90.11.dist-info/WHEEL +5 -0
  44. sentienceapi-0.90.11.dist-info/entry_points.txt +2 -0
  45. sentienceapi-0.90.11.dist-info/licenses/LICENSE.md +43 -0
  46. sentienceapi-0.90.11.dist-info/top_level.txt +1 -0
@@ -0,0 +1,292 @@
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
+ # Call /v1/traces/complete to report file sizes (NEW)
217
+ self._complete_trace()
218
+
219
+ # Delete file only on successful upload
220
+ if os.path.exists(self._path):
221
+ try:
222
+ os.remove(self._path)
223
+ except Exception:
224
+ pass # Ignore cleanup errors
225
+ else:
226
+ self._upload_successful = False
227
+ print(f"❌ [Sentience] Upload failed: HTTP {response.status_code}")
228
+ print(f" Response: {response.text}")
229
+ print(f" Local trace preserved at: {self._path}")
230
+
231
+ except Exception as e:
232
+ self._upload_successful = False
233
+ print(f"❌ [Sentience] Error uploading trace: {e}")
234
+ print(f" Local trace preserved at: {self._path}")
235
+ # Don't raise - preserve trace locally even if upload fails
236
+
237
+ def _generate_index(self) -> None:
238
+ """Generate trace index file (automatic on close)."""
239
+ try:
240
+ from .trace_indexing import write_trace_index
241
+
242
+ write_trace_index(str(self._path))
243
+ except Exception as e:
244
+ # Non-fatal: log but don't crash
245
+ print(f"⚠️ Failed to generate trace index: {e}")
246
+
247
+ def _complete_trace(self) -> None:
248
+ """
249
+ Call /v1/traces/complete to report file sizes to gateway.
250
+
251
+ This is a best-effort call - failures are logged but don't affect upload success.
252
+ """
253
+ if not self.api_key:
254
+ # No API key - skip complete call
255
+ return
256
+
257
+ try:
258
+ response = requests.post(
259
+ f"{self.api_url}/v1/traces/complete",
260
+ headers={"Authorization": f"Bearer {self.api_key}"},
261
+ json={
262
+ "run_id": self.run_id,
263
+ "stats": {
264
+ "trace_file_size_bytes": self.trace_file_size_bytes,
265
+ "screenshot_total_size_bytes": self.screenshot_total_size_bytes,
266
+ },
267
+ },
268
+ timeout=10,
269
+ )
270
+
271
+ if response.status_code == 200:
272
+ if self.logger:
273
+ self.logger.info("Trace completion reported to gateway")
274
+ else:
275
+ if self.logger:
276
+ self.logger.warning(
277
+ f"Failed to report trace completion: HTTP {response.status_code}"
278
+ )
279
+
280
+ except Exception as e:
281
+ # Best-effort - log but don't fail
282
+ if self.logger:
283
+ self.logger.warning(f"Error reporting trace completion: {e}")
284
+
285
+ def __enter__(self):
286
+ """Context manager support."""
287
+ return self
288
+
289
+ def __exit__(self, exc_type, exc_val, exc_tb):
290
+ """Context manager cleanup."""
291
+ self.close()
292
+ return False