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
sentience/snapshot.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Snapshot functionality - calls window.sentience.snapshot() or server-side API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .browser import SentienceBrowser
|
|
13
|
+
from .models import Snapshot, SnapshotOptions
|
|
14
|
+
|
|
15
|
+
# Maximum payload size for API requests (10MB server limit)
|
|
16
|
+
MAX_PAYLOAD_BYTES = 10 * 1024 * 1024
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _save_trace_to_file(raw_elements: list[dict[str, Any]], trace_path: str | None = None) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Save raw_elements to a JSON file for benchmarking/training
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
raw_elements: Raw elements data from snapshot
|
|
25
|
+
trace_path: Path to save trace file. If None, uses "trace_{timestamp}.json"
|
|
26
|
+
"""
|
|
27
|
+
# Default filename if none provided
|
|
28
|
+
filename = trace_path or f"trace_{int(time.time())}.json"
|
|
29
|
+
|
|
30
|
+
# Ensure directory exists
|
|
31
|
+
directory = os.path.dirname(filename)
|
|
32
|
+
if directory:
|
|
33
|
+
os.makedirs(directory, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
# Save the raw elements to JSON
|
|
36
|
+
with open(filename, "w") as f:
|
|
37
|
+
json.dump(raw_elements, f, indent=2)
|
|
38
|
+
|
|
39
|
+
print(f"[SDK] Trace saved to: {filename}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def snapshot(
|
|
43
|
+
browser: SentienceBrowser,
|
|
44
|
+
screenshot: bool | None = None,
|
|
45
|
+
limit: int | None = None,
|
|
46
|
+
filter: dict[str, Any] | None = None,
|
|
47
|
+
use_api: bool | None = None,
|
|
48
|
+
save_trace: bool = False,
|
|
49
|
+
trace_path: str | None = None,
|
|
50
|
+
show_overlay: bool = False,
|
|
51
|
+
) -> Snapshot:
|
|
52
|
+
"""
|
|
53
|
+
Take a snapshot of the current page
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
browser: SentienceBrowser instance
|
|
57
|
+
screenshot: Whether to capture screenshot (bool or dict with format/quality)
|
|
58
|
+
limit: Limit number of elements returned
|
|
59
|
+
filter: Filter options (min_area, allowed_roles, min_z_index)
|
|
60
|
+
use_api: Force use of server-side API if True, local extension if False.
|
|
61
|
+
If None, uses API if api_key is set, otherwise uses local extension.
|
|
62
|
+
save_trace: Whether to save raw_elements to JSON for benchmarking/training
|
|
63
|
+
trace_path: Path to save trace file. If None, uses "trace_{timestamp}.json"
|
|
64
|
+
show_overlay: Show visual overlay highlighting elements in browser
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Snapshot object
|
|
68
|
+
"""
|
|
69
|
+
# Build SnapshotOptions from individual parameters
|
|
70
|
+
options = SnapshotOptions(
|
|
71
|
+
screenshot=screenshot if screenshot is not None else False,
|
|
72
|
+
limit=limit if limit is not None else 50,
|
|
73
|
+
filter=filter,
|
|
74
|
+
use_api=use_api,
|
|
75
|
+
save_trace=save_trace,
|
|
76
|
+
trace_path=trace_path,
|
|
77
|
+
show_overlay=show_overlay,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Determine if we should use server-side API
|
|
81
|
+
should_use_api = (
|
|
82
|
+
options.use_api if options.use_api is not None else (browser.api_key is not None)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if should_use_api and browser.api_key:
|
|
86
|
+
# Use server-side API (Pro/Enterprise tier)
|
|
87
|
+
return _snapshot_via_api(browser, options)
|
|
88
|
+
else:
|
|
89
|
+
# Use local extension (Free tier)
|
|
90
|
+
return _snapshot_via_extension(browser, options)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _snapshot_via_extension(
|
|
94
|
+
browser: SentienceBrowser,
|
|
95
|
+
options: SnapshotOptions,
|
|
96
|
+
) -> Snapshot:
|
|
97
|
+
"""Take snapshot using local extension (Free tier)"""
|
|
98
|
+
if not browser.page:
|
|
99
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
100
|
+
|
|
101
|
+
# CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
|
|
102
|
+
# The new architecture loads injected_api.js asynchronously, so window.sentience
|
|
103
|
+
# may not be immediately available after page load
|
|
104
|
+
try:
|
|
105
|
+
browser.page.wait_for_function(
|
|
106
|
+
"typeof window.sentience !== 'undefined'",
|
|
107
|
+
timeout=5000, # 5 second timeout
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
# Gather diagnostics if wait fails
|
|
111
|
+
try:
|
|
112
|
+
diag = browser.page.evaluate(
|
|
113
|
+
"""() => ({
|
|
114
|
+
sentience_defined: typeof window.sentience !== 'undefined',
|
|
115
|
+
extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set',
|
|
116
|
+
url: window.location.href
|
|
117
|
+
})"""
|
|
118
|
+
)
|
|
119
|
+
except Exception:
|
|
120
|
+
diag = {"error": "Could not gather diagnostics"}
|
|
121
|
+
|
|
122
|
+
raise RuntimeError(
|
|
123
|
+
f"Sentience extension failed to inject window.sentience API. "
|
|
124
|
+
f"Is the extension loaded? Diagnostics: {diag}"
|
|
125
|
+
) from e
|
|
126
|
+
|
|
127
|
+
# Build options dict for extension API (exclude save_trace/trace_path)
|
|
128
|
+
ext_options: dict[str, Any] = {}
|
|
129
|
+
if options.screenshot is not False:
|
|
130
|
+
ext_options["screenshot"] = options.screenshot
|
|
131
|
+
if options.limit != 50:
|
|
132
|
+
ext_options["limit"] = options.limit
|
|
133
|
+
if options.filter is not None:
|
|
134
|
+
ext_options["filter"] = (
|
|
135
|
+
options.filter.model_dump() if hasattr(options.filter, "model_dump") else options.filter
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Call extension API
|
|
139
|
+
result = browser.page.evaluate(
|
|
140
|
+
"""
|
|
141
|
+
(options) => {
|
|
142
|
+
return window.sentience.snapshot(options);
|
|
143
|
+
}
|
|
144
|
+
""",
|
|
145
|
+
ext_options,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Save trace if requested
|
|
149
|
+
if options.save_trace:
|
|
150
|
+
_save_trace_to_file(result.get("raw_elements", []), options.trace_path)
|
|
151
|
+
|
|
152
|
+
# Show visual overlay if requested
|
|
153
|
+
if options.show_overlay:
|
|
154
|
+
raw_elements = result.get("raw_elements", [])
|
|
155
|
+
if raw_elements:
|
|
156
|
+
browser.page.evaluate(
|
|
157
|
+
"""
|
|
158
|
+
(elements) => {
|
|
159
|
+
if (window.sentience && window.sentience.showOverlay) {
|
|
160
|
+
window.sentience.showOverlay(elements, null);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
""",
|
|
164
|
+
raw_elements,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Validate and parse with Pydantic
|
|
168
|
+
snapshot_obj = Snapshot(**result)
|
|
169
|
+
return snapshot_obj
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _snapshot_via_api(
|
|
173
|
+
browser: SentienceBrowser,
|
|
174
|
+
options: SnapshotOptions,
|
|
175
|
+
) -> Snapshot:
|
|
176
|
+
"""Take snapshot using server-side API (Pro/Enterprise tier)"""
|
|
177
|
+
if not browser.page:
|
|
178
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
179
|
+
|
|
180
|
+
if not browser.api_key:
|
|
181
|
+
raise ValueError("API key required for server-side processing")
|
|
182
|
+
|
|
183
|
+
if not browser.api_url:
|
|
184
|
+
raise ValueError("API URL required for server-side processing")
|
|
185
|
+
|
|
186
|
+
# CRITICAL: Wait for extension injection to complete (CSP-resistant architecture)
|
|
187
|
+
# Even for API mode, we need the extension to collect raw data locally
|
|
188
|
+
try:
|
|
189
|
+
browser.page.wait_for_function("typeof window.sentience !== 'undefined'", timeout=5000)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
"Sentience extension failed to inject. Cannot collect raw data for API processing."
|
|
193
|
+
) from e
|
|
194
|
+
|
|
195
|
+
# Step 1: Get raw data from local extension (always happens locally)
|
|
196
|
+
raw_options: dict[str, Any] = {}
|
|
197
|
+
if options.screenshot is not False:
|
|
198
|
+
raw_options["screenshot"] = options.screenshot
|
|
199
|
+
|
|
200
|
+
raw_result = browser.page.evaluate(
|
|
201
|
+
"""
|
|
202
|
+
(options) => {
|
|
203
|
+
return window.sentience.snapshot(options);
|
|
204
|
+
}
|
|
205
|
+
""",
|
|
206
|
+
raw_options,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Save trace if requested (save raw data before API processing)
|
|
210
|
+
if options.save_trace:
|
|
211
|
+
_save_trace_to_file(raw_result.get("raw_elements", []), options.trace_path)
|
|
212
|
+
|
|
213
|
+
# Step 2: Send to server for smart ranking/filtering
|
|
214
|
+
# Use raw_elements (raw data) instead of elements (processed data)
|
|
215
|
+
# Server validates API key and applies proprietary ranking logic
|
|
216
|
+
payload = {
|
|
217
|
+
"raw_elements": raw_result.get("raw_elements", []), # Raw data needed for server processing
|
|
218
|
+
"url": raw_result.get("url", ""),
|
|
219
|
+
"viewport": raw_result.get("viewport"),
|
|
220
|
+
"goal": options.goal, # Optional goal/task description
|
|
221
|
+
"options": {
|
|
222
|
+
"limit": options.limit,
|
|
223
|
+
"filter": options.filter.model_dump() if options.filter else None,
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Check payload size before sending (server has 10MB limit)
|
|
228
|
+
payload_json = json.dumps(payload)
|
|
229
|
+
payload_size = len(payload_json.encode("utf-8"))
|
|
230
|
+
if payload_size > MAX_PAYLOAD_BYTES:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"Payload size ({payload_size / 1024 / 1024:.2f}MB) exceeds server limit "
|
|
233
|
+
f"({MAX_PAYLOAD_BYTES / 1024 / 1024:.0f}MB). "
|
|
234
|
+
f"Try reducing the number of elements on the page or filtering elements."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
headers = {
|
|
238
|
+
"Authorization": f"Bearer {browser.api_key}",
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
response = requests.post(
|
|
244
|
+
f"{browser.api_url}/v1/snapshot",
|
|
245
|
+
data=payload_json, # Reuse already-serialized JSON
|
|
246
|
+
headers=headers,
|
|
247
|
+
timeout=30,
|
|
248
|
+
)
|
|
249
|
+
response.raise_for_status()
|
|
250
|
+
|
|
251
|
+
api_result = response.json()
|
|
252
|
+
|
|
253
|
+
# Merge API result with local data (screenshot, etc.)
|
|
254
|
+
snapshot_data = {
|
|
255
|
+
"status": api_result.get("status", "success"),
|
|
256
|
+
"timestamp": api_result.get("timestamp"),
|
|
257
|
+
"url": api_result.get("url", raw_result.get("url", "")),
|
|
258
|
+
"viewport": api_result.get("viewport", raw_result.get("viewport")),
|
|
259
|
+
"elements": api_result.get("elements", []),
|
|
260
|
+
"screenshot": raw_result.get("screenshot"), # Keep local screenshot
|
|
261
|
+
"screenshot_format": raw_result.get("screenshot_format"),
|
|
262
|
+
"error": api_result.get("error"),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Show visual overlay if requested (use API-ranked elements)
|
|
266
|
+
if options.show_overlay:
|
|
267
|
+
elements = api_result.get("elements", [])
|
|
268
|
+
if elements:
|
|
269
|
+
browser.page.evaluate(
|
|
270
|
+
"""
|
|
271
|
+
(elements) => {
|
|
272
|
+
if (window.sentience && window.sentience.showOverlay) {
|
|
273
|
+
window.sentience.showOverlay(elements, null);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
""",
|
|
277
|
+
elements,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return Snapshot(**snapshot_data)
|
|
281
|
+
except requests.exceptions.RequestException as e:
|
|
282
|
+
raise RuntimeError(f"API request failed: {e}")
|
sentience/text_search.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Text search utilities - find text and get pixel coordinates
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .browser import SentienceBrowser
|
|
6
|
+
from .models import TextRectSearchResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def find_text_rect(
|
|
10
|
+
browser: SentienceBrowser,
|
|
11
|
+
text: str,
|
|
12
|
+
case_sensitive: bool = False,
|
|
13
|
+
whole_word: bool = False,
|
|
14
|
+
max_results: int = 10,
|
|
15
|
+
) -> TextRectSearchResult:
|
|
16
|
+
"""
|
|
17
|
+
Find all occurrences of text on the page and get their exact pixel coordinates.
|
|
18
|
+
|
|
19
|
+
This function searches for text in all visible text nodes on the page and returns
|
|
20
|
+
the bounding rectangles for each match. Useful for:
|
|
21
|
+
- Finding specific UI elements by their text content
|
|
22
|
+
- Locating buttons, links, or labels without element IDs
|
|
23
|
+
- Getting exact coordinates for click automation
|
|
24
|
+
- Highlighting search results visually
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
browser: SentienceBrowser instance
|
|
28
|
+
text: Text to search for (required)
|
|
29
|
+
case_sensitive: If True, search is case-sensitive (default: False)
|
|
30
|
+
whole_word: If True, only match whole words surrounded by whitespace (default: False)
|
|
31
|
+
max_results: Maximum number of matches to return (default: 10, max: 100)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
TextRectSearchResult with:
|
|
35
|
+
- status: "success" or "error"
|
|
36
|
+
- query: The search text
|
|
37
|
+
- case_sensitive: Whether search was case-sensitive
|
|
38
|
+
- whole_word: Whether whole-word matching was used
|
|
39
|
+
- matches: Number of matches found
|
|
40
|
+
- results: List of TextMatch objects, each containing:
|
|
41
|
+
- text: The matched text
|
|
42
|
+
- rect: Absolute rectangle (with scroll offset)
|
|
43
|
+
- viewport_rect: Viewport-relative rectangle
|
|
44
|
+
- context: Surrounding text (before/after)
|
|
45
|
+
- in_viewport: Whether visible in current viewport
|
|
46
|
+
- viewport: Current viewport dimensions and scroll position
|
|
47
|
+
- error: Error message if status is "error"
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
# Find "Sign In" button
|
|
51
|
+
result = find_text_rect(browser, "Sign In")
|
|
52
|
+
if result.status == "success" and result.results:
|
|
53
|
+
first_match = result.results[0]
|
|
54
|
+
print(f"Found at: ({first_match.rect.x}, {first_match.rect.y})")
|
|
55
|
+
print(f"Size: {first_match.rect.width}x{first_match.rect.height}")
|
|
56
|
+
print(f"In viewport: {first_match.in_viewport}")
|
|
57
|
+
|
|
58
|
+
# Case-sensitive search
|
|
59
|
+
result = find_text_rect(browser, "LOGIN", case_sensitive=True)
|
|
60
|
+
|
|
61
|
+
# Whole word only
|
|
62
|
+
result = find_text_rect(browser, "log", whole_word=True) # Won't match "login"
|
|
63
|
+
|
|
64
|
+
# Find all matches and click the first visible one
|
|
65
|
+
result = find_text_rect(browser, "Buy Now", max_results=5)
|
|
66
|
+
if result.status == "success" and result.results:
|
|
67
|
+
for match in result.results:
|
|
68
|
+
if match.in_viewport:
|
|
69
|
+
# Use click_rect from actions module
|
|
70
|
+
from sentience import click_rect
|
|
71
|
+
click_result = click_rect(browser, {
|
|
72
|
+
"x": match.rect.x,
|
|
73
|
+
"y": match.rect.y,
|
|
74
|
+
"w": match.rect.width,
|
|
75
|
+
"h": match.rect.height
|
|
76
|
+
})
|
|
77
|
+
break
|
|
78
|
+
"""
|
|
79
|
+
if not browser.page:
|
|
80
|
+
raise RuntimeError("Browser not started. Call browser.start() first.")
|
|
81
|
+
|
|
82
|
+
if not text or not text.strip():
|
|
83
|
+
return TextRectSearchResult(
|
|
84
|
+
status="error",
|
|
85
|
+
error="Text parameter is required and cannot be empty",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Limit max_results to prevent performance issues
|
|
89
|
+
max_results = min(max_results, 100)
|
|
90
|
+
|
|
91
|
+
# Call the extension's findTextRect method
|
|
92
|
+
result_dict = browser.page.evaluate(
|
|
93
|
+
"""
|
|
94
|
+
(options) => {
|
|
95
|
+
return window.sentience.findTextRect(options);
|
|
96
|
+
}
|
|
97
|
+
""",
|
|
98
|
+
{
|
|
99
|
+
"text": text,
|
|
100
|
+
"caseSensitive": case_sensitive,
|
|
101
|
+
"wholeWord": whole_word,
|
|
102
|
+
"maxResults": max_results,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Parse and validate with Pydantic
|
|
107
|
+
return TextRectSearchResult(**result_dict)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace indexing module for Sentience SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .indexer import build_trace_index, write_trace_index, read_step_events
|
|
6
|
+
from .index_schema import (
|
|
7
|
+
TraceIndex,
|
|
8
|
+
StepIndex,
|
|
9
|
+
TraceSummary,
|
|
10
|
+
TraceFileInfo,
|
|
11
|
+
SnapshotInfo,
|
|
12
|
+
ActionInfo,
|
|
13
|
+
StepCounters,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"build_trace_index",
|
|
18
|
+
"write_trace_index",
|
|
19
|
+
"read_step_events",
|
|
20
|
+
"TraceIndex",
|
|
21
|
+
"StepIndex",
|
|
22
|
+
"TraceSummary",
|
|
23
|
+
"TraceFileInfo",
|
|
24
|
+
"SnapshotInfo",
|
|
25
|
+
"ActionInfo",
|
|
26
|
+
"StepCounters",
|
|
27
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions for trace index schema using concrete classes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field, asdict
|
|
6
|
+
from typing import Optional, List, Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class TraceFileInfo:
|
|
11
|
+
"""Metadata about the trace file."""
|
|
12
|
+
|
|
13
|
+
path: str
|
|
14
|
+
size_bytes: int
|
|
15
|
+
sha256: str
|
|
16
|
+
|
|
17
|
+
def to_dict(self) -> dict:
|
|
18
|
+
return asdict(self)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class TraceSummary:
|
|
23
|
+
"""High-level summary of the trace."""
|
|
24
|
+
|
|
25
|
+
first_ts: str
|
|
26
|
+
last_ts: str
|
|
27
|
+
event_count: int
|
|
28
|
+
step_count: int
|
|
29
|
+
error_count: int
|
|
30
|
+
final_url: Optional[str]
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return asdict(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class SnapshotInfo:
|
|
38
|
+
"""Snapshot metadata for index."""
|
|
39
|
+
|
|
40
|
+
snapshot_id: Optional[str] = None
|
|
41
|
+
digest: Optional[str] = None
|
|
42
|
+
url: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ActionInfo:
|
|
50
|
+
"""Action metadata for index."""
|
|
51
|
+
|
|
52
|
+
type: Optional[str] = None
|
|
53
|
+
target_element_id: Optional[int] = None
|
|
54
|
+
args_digest: Optional[str] = None
|
|
55
|
+
success: Optional[bool] = None
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return asdict(self)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class StepCounters:
|
|
63
|
+
"""Event counters per step."""
|
|
64
|
+
|
|
65
|
+
events: int = 0
|
|
66
|
+
snapshots: int = 0
|
|
67
|
+
actions: int = 0
|
|
68
|
+
llm_calls: int = 0
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict:
|
|
71
|
+
return asdict(self)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class StepIndex:
|
|
76
|
+
"""Index entry for a single step."""
|
|
77
|
+
|
|
78
|
+
step_index: int
|
|
79
|
+
step_id: str
|
|
80
|
+
goal: Optional[str]
|
|
81
|
+
status: Literal["ok", "error", "partial"]
|
|
82
|
+
ts_start: str
|
|
83
|
+
ts_end: str
|
|
84
|
+
offset_start: int
|
|
85
|
+
offset_end: int
|
|
86
|
+
url_before: Optional[str]
|
|
87
|
+
url_after: Optional[str]
|
|
88
|
+
snapshot_before: SnapshotInfo
|
|
89
|
+
snapshot_after: SnapshotInfo
|
|
90
|
+
action: ActionInfo
|
|
91
|
+
counters: StepCounters
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict:
|
|
94
|
+
result = asdict(self)
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class TraceIndex:
|
|
100
|
+
"""Complete trace index schema."""
|
|
101
|
+
|
|
102
|
+
version: int
|
|
103
|
+
run_id: str
|
|
104
|
+
created_at: str
|
|
105
|
+
trace_file: TraceFileInfo
|
|
106
|
+
summary: TraceSummary
|
|
107
|
+
steps: List[StepIndex] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> dict:
|
|
110
|
+
"""Convert to dictionary for JSON serialization."""
|
|
111
|
+
return asdict(self)
|