Skip to content

Commit dd4bcff

Browse files
committed
upload screenshots along with traces
1 parent fc7e7ff commit dd4bcff

File tree

9 files changed

+698
-123
lines changed

9 files changed

+698
-123
lines changed

sentience/agent.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import TYPE_CHECKING, Any, Optional
1010

1111
from .actions import click, click_async, press, press_async, type_text, type_text_async
12+
from .agent_config import AgentConfig
1213
from .base_agent import BaseAgent, BaseAgentAsync
1314
from .browser import AsyncSentienceBrowser, SentienceBrowser
1415
from .llm_provider import LLMProvider, LLMResponse
@@ -25,7 +26,6 @@
2526
from .snapshot import snapshot, snapshot_async
2627

2728
if TYPE_CHECKING:
28-
from .agent_config import AgentConfig
2929
from .tracing import Tracer
3030

3131

@@ -78,7 +78,10 @@ def __init__(
7878
self.default_snapshot_limit = default_snapshot_limit
7979
self.verbose = verbose
8080
self.tracer = tracer
81-
self.config = config
81+
self.config = config or AgentConfig()
82+
83+
# Screenshot sequence counter
84+
self._screenshot_sequence = 0
8285

8386
# Execution history
8487
self.history: list[dict[str, Any]] = []
@@ -150,12 +153,42 @@ def act( # noqa: C901
150153
if snap_opts.goal is None:
151154
snap_opts.goal = goal
152155

156+
# Apply AgentConfig screenshot settings if not overridden by snapshot_options
157+
if snapshot_options is None and self.config:
158+
if self.config.capture_screenshots:
159+
# Create ScreenshotConfig from AgentConfig
160+
snap_opts.screenshot = ScreenshotConfig(
161+
format=self.config.screenshot_format,
162+
quality=(
163+
self.config.screenshot_quality
164+
if self.config.screenshot_format == "jpeg"
165+
else None
166+
),
167+
)
168+
else:
169+
snap_opts.screenshot = False
170+
153171
# Call snapshot with options object (matches TypeScript API)
154172
snap = snapshot(self.browser, snap_opts)
155173

156174
if snap.status != "success":
157175
raise RuntimeError(f"Snapshot failed: {snap.error}")
158176

177+
# Store screenshot if captured
178+
if snap.screenshot and self.tracer:
179+
self._screenshot_sequence += 1
180+
seq = self._screenshot_sequence
181+
182+
# Store screenshot in CloudTraceSink if available
183+
if hasattr(self.tracer.sink, "store_screenshot"):
184+
self.tracer.sink.store_screenshot(
185+
sequence=seq,
186+
screenshot_data=snap.screenshot,
187+
format=snap.screenshot_format
188+
or (self.config.screenshot_format if self.config else "jpeg"),
189+
step_id=step_id,
190+
)
191+
159192
# Apply element filtering based on goal
160193
filtered_elements = self.filter_elements(snap, goal)
161194

@@ -721,7 +754,10 @@ def __init__(
721754
self.default_snapshot_limit = default_snapshot_limit
722755
self.verbose = verbose
723756
self.tracer = tracer
724-
self.config = config
757+
self.config = config or AgentConfig()
758+
759+
# Screenshot sequence counter
760+
self._screenshot_sequence = 0
725761

726762
# Execution history
727763
self.history: list[dict[str, Any]] = []
@@ -790,12 +826,42 @@ async def act( # noqa: C901
790826
if snap_opts.goal is None:
791827
snap_opts.goal = goal
792828

829+
# Apply AgentConfig screenshot settings if not overridden by snapshot_options
830+
if snapshot_options is None and self.config:
831+
if self.config.capture_screenshots:
832+
# Create ScreenshotConfig from AgentConfig
833+
snap_opts.screenshot = ScreenshotConfig(
834+
format=self.config.screenshot_format,
835+
quality=(
836+
self.config.screenshot_quality
837+
if self.config.screenshot_format == "jpeg"
838+
else None
839+
),
840+
)
841+
else:
842+
snap_opts.screenshot = False
843+
793844
# Call snapshot with options object (matches TypeScript API)
794845
snap = await snapshot_async(self.browser, snap_opts)
795846

796847
if snap.status != "success":
797848
raise RuntimeError(f"Snapshot failed: {snap.error}")
798849

850+
# Store screenshot if captured
851+
if snap.screenshot and self.tracer:
852+
self._screenshot_sequence += 1
853+
seq = self._screenshot_sequence
854+
855+
# Store screenshot in CloudTraceSink if available
856+
if hasattr(self.tracer.sink, "store_screenshot"):
857+
self.tracer.sink.store_screenshot(
858+
sequence=seq,
859+
screenshot_data=snap.screenshot,
860+
format=snap.screenshot_format
861+
or (self.config.screenshot_format if self.config else "jpeg"),
862+
step_id=step_id,
863+
)
864+
799865
# Apply element filtering based on goal
800866
filtered_elements = self.filter_elements(snap, goal)
801867

sentience/cloud_tracing.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44
Implements "Local Write, Batch Upload" pattern for enterprise cloud tracing.
55
"""
66

7+
import base64
78
import gzip
89
import json
910
import os
1011
import threading
1112
from collections.abc import Callable
13+
from concurrent.futures import ThreadPoolExecutor, as_completed
1214
from pathlib import Path
1315
from typing import Any, Protocol
1416

1517
import requests
1618

19+
from sentience.models import ScreenshotMetadata
1720
from sentience.tracing import TraceSink
1821

1922

@@ -103,10 +106,17 @@ def __init__(
103106
self._closed = False
104107
self._upload_successful = False
105108

106-
# File size tracking (NEW)
109+
# File size tracking
107110
self.trace_file_size_bytes = 0
108111
self.screenshot_total_size_bytes = 0
109112

113+
# Screenshot storage directory
114+
self._screenshot_dir = cache_dir / f"{run_id}_screenshots"
115+
self._screenshot_dir.mkdir(exist_ok=True)
116+
117+
# Screenshot metadata tracking (sequence -> ScreenshotMetadata)
118+
self._screenshot_metadata: dict[int, ScreenshotMetadata] = {}
119+
110120
def emit(self, event: dict[str, Any]) -> None:
111121
"""
112122
Write event to local persistent file (Fast, non-blocking).
@@ -213,18 +223,21 @@ def _do_upload(self, on_progress: Callable[[int, int], None] | None = None) -> N
213223
if on_progress:
214224
on_progress(compressed_size, compressed_size)
215225

226+
# Upload screenshots after trace upload succeeds
227+
if self._screenshot_metadata:
228+
print(
229+
f"📸 [Sentience] Uploading {len(self._screenshot_metadata)} screenshots..."
230+
)
231+
self._upload_screenshots(on_progress)
232+
216233
# Upload trace index file
217234
self._upload_index()
218235

219236
# Call /v1/traces/complete to report file sizes
220237
self._complete_trace()
221238

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
239+
# Delete files only on successful upload
240+
self._cleanup_files()
228241
else:
229242
self._upload_successful = False
230243
print(f"❌ [Sentience] Upload failed: HTTP {response.status_code}")
@@ -353,6 +366,7 @@ def _complete_trace(self) -> None:
353366
"stats": {
354367
"trace_file_size_bytes": self.trace_file_size_bytes,
355368
"screenshot_total_size_bytes": self.screenshot_total_size_bytes,
369+
"screenshot_count": len(self._screenshot_metadata),
356370
},
357371
},
358372
timeout=10,

sentience/extension/background.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,13 @@ async function handleScreenshotCapture(_tabId, options = {}) {
144144
async function handleSnapshotProcessing(rawData, options = {}) {
145145
const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs
146146
const startTime = performance.now();
147-
147+
148148
try {
149149
// Safety check: limit element count to prevent hangs
150150
if (!Array.isArray(rawData)) {
151151
throw new Error('rawData must be an array');
152152
}
153-
153+
154154
if (rawData.length > MAX_ELEMENTS) {
155155
console.warn(`[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.`);
156156
rawData = rawData.slice(0, MAX_ELEMENTS);
@@ -186,7 +186,7 @@ async function handleSnapshotProcessing(rawData, options = {}) {
186186
// Add timeout protection (18 seconds - less than content.js timeout)
187187
analyzedElements = await Promise.race([
188188
wasmPromise,
189-
new Promise((_, reject) =>
189+
new Promise((_, reject) =>
190190
setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000)
191191
)
192192
]);

sentience/extension/content.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function handleSnapshotRequest(data) {
9292
if (responded) return; // Already responded via timeout
9393
responded = true;
9494
clearTimeout(timeoutId);
95-
95+
9696
const duration = performance.now() - startTime;
9797

9898
// Handle Chrome extension errors (e.g., background script crashed)

0 commit comments

Comments
 (0)