sentienceapi 0.90.9__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 +153 -0
- sentience/actions.py +439 -0
- sentience/agent.py +687 -0
- sentience/agent_config.py +43 -0
- sentience/base_agent.py +101 -0
- sentience/browser.py +409 -0
- sentience/cli.py +130 -0
- sentience/cloud_tracing.py +292 -0
- sentience/conversational_agent.py +509 -0
- sentience/expect.py +92 -0
- sentience/extension/background.js +233 -0
- sentience/extension/content.js +298 -0
- sentience/extension/injected_api.js +1473 -0
- sentience/extension/manifest.json +36 -0
- sentience/extension/pkg/sentience_core.d.ts +51 -0
- sentience/extension/pkg/sentience_core.js +529 -0
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
- sentience/extension/release.json +115 -0
- sentience/extension/test-content.js +4 -0
- sentience/formatting.py +59 -0
- sentience/generator.py +202 -0
- sentience/inspector.py +185 -0
- sentience/llm_provider.py +431 -0
- sentience/models.py +406 -0
- sentience/overlay.py +115 -0
- sentience/query.py +303 -0
- sentience/read.py +96 -0
- sentience/recorder.py +369 -0
- sentience/schemas/trace_v1.json +216 -0
- sentience/screenshot.py +54 -0
- sentience/snapshot.py +282 -0
- sentience/text_search.py +107 -0
- sentience/trace_indexing/__init__.py +27 -0
- sentience/trace_indexing/index_schema.py +111 -0
- sentience/trace_indexing/indexer.py +363 -0
- sentience/tracer_factory.py +211 -0
- sentience/tracing.py +285 -0
- sentience/utils.py +296 -0
- sentience/wait.py +73 -0
- sentienceapi-0.90.9.dist-info/METADATA +878 -0
- sentienceapi-0.90.9.dist-info/RECORD +46 -0
- sentienceapi-0.90.9.dist-info/WHEEL +5 -0
- sentienceapi-0.90.9.dist-info/entry_points.txt +2 -0
- sentienceapi-0.90.9.dist-info/licenses/LICENSE.md +43 -0
- sentienceapi-0.90.9.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
|