sentienceapi 0.92.2__py3-none-any.whl → 0.98.0__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 +107 -2
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +2 -0
- sentience/actions.py +354 -9
- sentience/agent.py +4 -0
- sentience/agent_runtime.py +840 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +8 -1
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +372 -0
- sentience/backends/browser_use_adapter.py +241 -0
- sentience/backends/cdp_backend.py +393 -0
- sentience/backends/exceptions.py +211 -0
- sentience/backends/playwright_backend.py +194 -0
- sentience/backends/protocol.py +216 -0
- sentience/backends/sentience_context.py +469 -0
- sentience/backends/snapshot.py +483 -0
- sentience/browser.py +230 -74
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +65 -24
- sentience/constants.py +6 -0
- sentience/cursor_policy.py +142 -0
- sentience/extension/content.js +35 -0
- sentience/extension/injected_api.js +310 -15
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +192 -144
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +29 -29
- sentience/failure_artifacts.py +241 -0
- sentience/integrations/__init__.py +6 -0
- sentience/integrations/langchain/__init__.py +12 -0
- sentience/integrations/langchain/context.py +18 -0
- sentience/integrations/langchain/core.py +326 -0
- sentience/integrations/langchain/tools.py +180 -0
- sentience/integrations/models.py +46 -0
- sentience/integrations/pydanticai/__init__.py +15 -0
- sentience/integrations/pydanticai/deps.py +20 -0
- sentience/integrations/pydanticai/toolset.py +468 -0
- sentience/llm_provider.py +695 -18
- sentience/models.py +536 -3
- sentience/ordinal.py +280 -0
- sentience/query.py +66 -4
- sentience/schemas/trace_v1.json +27 -1
- sentience/snapshot.py +384 -93
- sentience/snapshot_diff.py +39 -54
- sentience/text_search.py +1 -0
- sentience/trace_event_builder.py +20 -1
- sentience/trace_indexing/indexer.py +3 -49
- sentience/tracer_factory.py +1 -3
- sentience/verification.py +618 -0
- sentience/visual_agent.py +3 -1
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +198 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/utils.py +0 -296
- sentienceapi-0.92.2.dist-info/RECORD +0 -65
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sentienceapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.98.0
|
|
4
4
|
Summary: Python SDK for Sentience AI Agent Browser Automation
|
|
5
5
|
Author: Sentience Team
|
|
6
6
|
License: MIT OR Apache-2.0
|
|
@@ -26,6 +26,20 @@ Requires-Dist: requests>=2.31.0
|
|
|
26
26
|
Requires-Dist: httpx>=0.25.0
|
|
27
27
|
Requires-Dist: playwright-stealth>=1.0.6
|
|
28
28
|
Requires-Dist: markdownify>=0.11.6
|
|
29
|
+
Provides-Extra: browser-use
|
|
30
|
+
Requires-Dist: browser-use>=0.1.40; extra == "browser-use"
|
|
31
|
+
Provides-Extra: pydanticai
|
|
32
|
+
Requires-Dist: pydantic-ai; extra == "pydanticai"
|
|
33
|
+
Provides-Extra: langchain
|
|
34
|
+
Requires-Dist: langchain; extra == "langchain"
|
|
35
|
+
Requires-Dist: langgraph; extra == "langchain"
|
|
36
|
+
Provides-Extra: vision-local
|
|
37
|
+
Requires-Dist: pillow>=10.0.0; extra == "vision-local"
|
|
38
|
+
Requires-Dist: torch>=2.2.0; extra == "vision-local"
|
|
39
|
+
Requires-Dist: transformers>=4.46.0; extra == "vision-local"
|
|
40
|
+
Provides-Extra: mlx-vlm
|
|
41
|
+
Requires-Dist: pillow>=10.0.0; extra == "mlx-vlm"
|
|
42
|
+
Requires-Dist: mlx-vlm>=0.1.0; extra == "mlx-vlm"
|
|
29
43
|
Provides-Extra: dev
|
|
30
44
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
45
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
@@ -33,7 +47,7 @@ Dynamic: license-file
|
|
|
33
47
|
|
|
34
48
|
# Sentience Python SDK
|
|
35
49
|
|
|
36
|
-
**Semantic
|
|
50
|
+
**Semantic snapshots and Jest-style assertions for reliable AI web agents with time-travel traces**
|
|
37
51
|
|
|
38
52
|
## 📦 Installation
|
|
39
53
|
|
|
@@ -55,6 +69,106 @@ pip install transformers torch # For local LLMs
|
|
|
55
69
|
pip install -e .
|
|
56
70
|
```
|
|
57
71
|
|
|
72
|
+
## Jest for AI Web Agent
|
|
73
|
+
|
|
74
|
+
### Semantic snapshots and assertions that let agents act, verify, and know when they're done.
|
|
75
|
+
|
|
76
|
+
Use `AgentRuntime` to add Jest-style assertions to your agent loops. Verify browser state, check task completion, and get clear feedback on what's working:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import asyncio
|
|
80
|
+
from sentience import AsyncSentienceBrowser, AgentRuntime
|
|
81
|
+
from sentience.verification import (
|
|
82
|
+
url_contains,
|
|
83
|
+
exists,
|
|
84
|
+
all_of,
|
|
85
|
+
is_enabled,
|
|
86
|
+
is_checked,
|
|
87
|
+
value_equals,
|
|
88
|
+
)
|
|
89
|
+
from sentience.tracing import Tracer, JsonlTraceSink
|
|
90
|
+
|
|
91
|
+
async def main():
|
|
92
|
+
# Create tracer
|
|
93
|
+
tracer = Tracer(run_id="my-run", sink=JsonlTraceSink("trace.jsonl"))
|
|
94
|
+
|
|
95
|
+
# Create browser and runtime
|
|
96
|
+
async with AsyncSentienceBrowser() as browser:
|
|
97
|
+
page = await browser.new_page()
|
|
98
|
+
runtime = await AgentRuntime.from_sentience_browser(
|
|
99
|
+
browser=browser,
|
|
100
|
+
page=page,
|
|
101
|
+
tracer=tracer
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Navigate and take snapshot
|
|
105
|
+
await page.goto("https://example.com")
|
|
106
|
+
runtime.begin_step("Verify page loaded")
|
|
107
|
+
await runtime.snapshot()
|
|
108
|
+
|
|
109
|
+
# v1: deterministic assertions (Jest-style)
|
|
110
|
+
runtime.assert_(url_contains("example.com"), label="on_correct_domain")
|
|
111
|
+
runtime.assert_(exists("role=heading"), label="has_heading")
|
|
112
|
+
runtime.assert_(all_of([
|
|
113
|
+
exists("role=button"),
|
|
114
|
+
exists("role=link")
|
|
115
|
+
]), label="has_interactive_elements")
|
|
116
|
+
|
|
117
|
+
# v1: state-aware assertions (when Gateway refinement is enabled)
|
|
118
|
+
runtime.assert_(is_enabled("role=button"), label="button_enabled")
|
|
119
|
+
runtime.assert_(is_checked("role=checkbox name~'subscribe'"), label="subscribe_checked_if_present")
|
|
120
|
+
runtime.assert_(value_equals("role=textbox name~'email'", "user@example.com"), label="email_value_if_present")
|
|
121
|
+
|
|
122
|
+
# v2: retry loop with snapshot confidence gating + exhaustion
|
|
123
|
+
ok = await runtime.check(
|
|
124
|
+
exists("role=heading"),
|
|
125
|
+
label="heading_eventually_visible",
|
|
126
|
+
required=True,
|
|
127
|
+
).eventually(timeout_s=10.0, poll_s=0.25, min_confidence=0.7, max_snapshot_attempts=3)
|
|
128
|
+
print("eventually() result:", ok)
|
|
129
|
+
|
|
130
|
+
# Check task completion
|
|
131
|
+
if runtime.assert_done(exists("text~'Example'"), label="task_complete"):
|
|
132
|
+
print("✅ Task completed!")
|
|
133
|
+
|
|
134
|
+
print(f"Task done: {runtime.is_task_done}")
|
|
135
|
+
|
|
136
|
+
asyncio.run(main())
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Failure Artifact Buffer (Phase 1)
|
|
140
|
+
|
|
141
|
+
Capture a short ring buffer of screenshots and persist them when a required assertion fails.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from sentience.failure_artifacts import FailureArtifactsOptions
|
|
145
|
+
|
|
146
|
+
await runtime.enable_failure_artifacts(
|
|
147
|
+
FailureArtifactsOptions(buffer_seconds=15, capture_on_action=True, fps=0.0)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# After each action, record it (best-effort).
|
|
151
|
+
await runtime.record_action("CLICK")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Redaction callback (Phase 3)
|
|
155
|
+
|
|
156
|
+
Provide a user-defined callback to redact snapshots and decide whether to persist frames. The SDK does not implement image/video redaction.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from sentience.failure_artifacts import FailureArtifactsOptions, RedactionContext, RedactionResult
|
|
160
|
+
|
|
161
|
+
def redact(ctx: RedactionContext) -> RedactionResult:
|
|
162
|
+
# Example: drop frames entirely, keep JSON only.
|
|
163
|
+
return RedactionResult(drop_frames=True)
|
|
164
|
+
|
|
165
|
+
await runtime.enable_failure_artifacts(
|
|
166
|
+
FailureArtifactsOptions(on_before_persist=redact)
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**See examples:** [`examples/asserts/`](examples/asserts/)
|
|
171
|
+
|
|
58
172
|
## 🚀 Quick Start: Choose Your Abstraction Level
|
|
59
173
|
|
|
60
174
|
Sentience SDK offers **three abstraction levels** - use what fits your needs:
|
|
@@ -135,56 +249,66 @@ with SentienceBrowser(headless=False) as browser:
|
|
|
135
249
|
|
|
136
250
|
---
|
|
137
251
|
|
|
138
|
-
|
|
139
|
-
|
|
252
|
+
## 🆕 What's New (2026-01-06)
|
|
253
|
+
|
|
254
|
+
### Human-like Typing
|
|
255
|
+
Add realistic delays between keystrokes to mimic human typing:
|
|
256
|
+
```python
|
|
257
|
+
from sentience import type_text
|
|
140
258
|
|
|
141
|
-
|
|
259
|
+
# Type instantly (default)
|
|
260
|
+
type_text(browser, element_id, "Hello World")
|
|
142
261
|
|
|
262
|
+
# Type with human-like delay (~10ms between keystrokes)
|
|
263
|
+
type_text(browser, element_id, "Hello World", delay_ms=10)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Scroll to Element
|
|
267
|
+
Scroll elements into view with smooth animation:
|
|
143
268
|
```python
|
|
144
|
-
from sentience import
|
|
145
|
-
import time
|
|
269
|
+
from sentience import snapshot, find, scroll_to
|
|
146
270
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
browser.goto("https://www.amazon.com/gp/bestsellers/", wait_until="domcontentloaded")
|
|
150
|
-
time.sleep(2) # Wait for dynamic content
|
|
271
|
+
snap = snapshot(browser)
|
|
272
|
+
button = find(snap, 'role=button text~"Submit"')
|
|
151
273
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
print(f"Found {len(snap.elements)} elements")
|
|
274
|
+
# Scroll element into view with smooth animation
|
|
275
|
+
scroll_to(browser, button.id)
|
|
155
276
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if el.role == "link"
|
|
160
|
-
and el.visual_cues.is_clickable
|
|
161
|
-
and el.in_viewport
|
|
162
|
-
and not el.is_occluded
|
|
163
|
-
and el.bbox.y < 600 # First row
|
|
164
|
-
]
|
|
277
|
+
# Scroll instantly to top of viewport
|
|
278
|
+
scroll_to(browser, button.id, behavior='instant', block='start')
|
|
279
|
+
```
|
|
165
280
|
|
|
166
|
-
|
|
167
|
-
# Sort by position (left to right, top to bottom)
|
|
168
|
-
products.sort(key=lambda e: (e.bbox.y, e.bbox.x))
|
|
169
|
-
first_product = products[0]
|
|
281
|
+
---
|
|
170
282
|
|
|
171
|
-
|
|
172
|
-
|
|
283
|
+
<details>
|
|
284
|
+
<summary><h2>💼 Real-World Example: Assertion-driven navigation</h2></summary>
|
|
173
285
|
|
|
174
|
-
|
|
175
|
-
browser.page.wait_for_load_state("networkidle")
|
|
176
|
-
time.sleep(2)
|
|
286
|
+
This example shows how to use **assertions + `.eventually()`** to make an agent loop resilient:
|
|
177
287
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
288
|
+
```python
|
|
289
|
+
import asyncio
|
|
290
|
+
import os
|
|
291
|
+
from sentience import AsyncSentienceBrowser, AgentRuntime
|
|
292
|
+
from sentience.tracing import Tracer, JsonlTraceSink
|
|
293
|
+
from sentience.verification import url_contains, exists
|
|
181
294
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
295
|
+
async def main():
|
|
296
|
+
tracer = Tracer(run_id="verified-run", sink=JsonlTraceSink("trace_verified.jsonl"))
|
|
297
|
+
async with AsyncSentienceBrowser(headless=True) as browser:
|
|
298
|
+
page = await browser.new_page()
|
|
299
|
+
runtime = await AgentRuntime.from_sentience_browser(browser=browser, page=page, tracer=tracer)
|
|
300
|
+
runtime.sentience_api_key = os.getenv("SENTIENCE_API_KEY") # optional, enables Gateway diagnostics
|
|
301
|
+
|
|
302
|
+
await page.goto("https://example.com")
|
|
303
|
+
runtime.begin_step("Verify we're on the right page")
|
|
186
304
|
|
|
187
|
-
|
|
305
|
+
await runtime.check(url_contains("example.com"), label="on_domain", required=True).eventually(
|
|
306
|
+
timeout_s=10.0, poll_s=0.25, min_confidence=0.7, max_snapshot_attempts=3
|
|
307
|
+
)
|
|
308
|
+
runtime.assert_(exists("role=heading"), label="heading_present")
|
|
309
|
+
|
|
310
|
+
asyncio.run(main())
|
|
311
|
+
```
|
|
188
312
|
|
|
189
313
|
</details>
|
|
190
314
|
|
|
@@ -832,6 +956,40 @@ with browser:
|
|
|
832
956
|
|
|
833
957
|
</details>
|
|
834
958
|
|
|
959
|
+
<details>
|
|
960
|
+
<summary><h3>🔍 Agent Runtime Verification</h3></summary>
|
|
961
|
+
|
|
962
|
+
`AgentRuntime` provides assertion predicates for runtime verification in agent loops, enabling programmatic verification of browser state during execution.
|
|
963
|
+
|
|
964
|
+
```python
|
|
965
|
+
from sentience import (
|
|
966
|
+
AgentRuntime, SentienceBrowser,
|
|
967
|
+
url_contains, exists, all_of
|
|
968
|
+
)
|
|
969
|
+
from sentience.tracer_factory import create_tracer
|
|
970
|
+
|
|
971
|
+
browser = SentienceBrowser()
|
|
972
|
+
browser.start()
|
|
973
|
+
tracer = create_tracer(run_id="my-run", upload_trace=False)
|
|
974
|
+
runtime = AgentRuntime(browser, browser.page, tracer)
|
|
975
|
+
|
|
976
|
+
# Navigate and take snapshot
|
|
977
|
+
browser.page.goto("https://example.com")
|
|
978
|
+
runtime.begin_step("Verify page")
|
|
979
|
+
runtime.snapshot()
|
|
980
|
+
|
|
981
|
+
# Run assertions
|
|
982
|
+
runtime.assert_(url_contains("example.com"), "on_correct_domain")
|
|
983
|
+
runtime.assert_(exists("role=heading"), "has_heading")
|
|
984
|
+
runtime.assert_done(exists("text~'Example'"), "task_complete")
|
|
985
|
+
|
|
986
|
+
print(f"Task done: {runtime.is_task_done}")
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**See example:** [`examples/agent_runtime_verification.py`](examples/agent_runtime_verification.py)
|
|
990
|
+
|
|
991
|
+
</details>
|
|
992
|
+
|
|
835
993
|
<details>
|
|
836
994
|
<summary><h3>🧰 Snapshot Utilities</h3></summary>
|
|
837
995
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
sentience/__init__.py,sha256=ZzolJfPqb2vnvBwrv7G7MgYJqrB8RuQ1VZsYCK4mgGY,6607
|
|
2
|
+
sentience/_extension_loader.py,sha256=z99Kvwh0bbFEbH04-LJ52t6kIpK9_hh1HG_QBdRg7e4,6118
|
|
3
|
+
sentience/action_executor.py,sha256=8ESWrFPvRDs5pp7Yvih7wkb_-8aY3kL0Ssr0KHRp-ZE,8226
|
|
4
|
+
sentience/actions.py,sha256=UUpmCKxws9tU_ppgITu4-nqUI0piFEkqGV9s_afrf-A,38382
|
|
5
|
+
sentience/agent.py,sha256=xcZ6gRucZRN0La3v5Y6jJRH2js8tgKx4qdwjcgfYVxs,49158
|
|
6
|
+
sentience/agent_config.py,sha256=n6HohW5j4VK3kT23xW7x2Vfz7HvEheb9g4AZ3VWfPpg,1588
|
|
7
|
+
sentience/agent_runtime.py,sha256=ak5CU6X-ig_mvwyt6lNV10XMh7lizlz0qeC97_xJ3Y8,30535
|
|
8
|
+
sentience/async_api.py,sha256=Wh2fE8SQ_9wpCPUyqpMubPbmuzzf83H_ncw21QUnTS8,4077
|
|
9
|
+
sentience/base_agent.py,sha256=5yVXe0S72I7GV4NPYySTvHfUAFvHBD4VjsNKSTo8MFo,5786
|
|
10
|
+
sentience/browser.py,sha256=B6UprJYyIcK4UFdxF8b8BhKwcuivgpwujcqgLK0qDBk,52937
|
|
11
|
+
sentience/browser_evaluator.py,sha256=CRiMsutM68dZWHnMIrFzpVzrkUwxVsXpjBplSIWCkso,9772
|
|
12
|
+
sentience/canonicalization.py,sha256=C7pkxaSgnCI0j_8oZ66yTQu6DGBCTM233IavjhSZgjA,6416
|
|
13
|
+
sentience/cli.py,sha256=R95DgCSJmezuymAdfL6fzUdB1HpCF3iMWcLO73KJ8eY,3851
|
|
14
|
+
sentience/cloud_tracing.py,sha256=ghhgDh9gqF1igEtmlAIFkRsSM7iVQanNhmOsJYhnDCk,32542
|
|
15
|
+
sentience/constants.py,sha256=sGiG_-gnchIW0maA0K9cVaq1-TAkwV4GyEW3kZ_6bhE,110
|
|
16
|
+
sentience/conversational_agent.py,sha256=v3FXczfNZdizQA7ejfgEbhGq_zaOboGb3__UaW7Alpo,18551
|
|
17
|
+
sentience/cursor_policy.py,sha256=36RsH387Y0EMj70_hMNLS6NdVbes6FEIcyB4hWtsypk,4137
|
|
18
|
+
sentience/element_filter.py,sha256=aVFrbTDkH-gtMZt07QfBZH3Oq8_KzwQMz17PD1uQ1h0,4021
|
|
19
|
+
sentience/expect.py,sha256=JIhHJ_UGiPvKWbX5SOmX1irOZlko5N5hhhX6d2Ymem0,6177
|
|
20
|
+
sentience/failure_artifacts.py,sha256=aMbAAZt06MJtCwxJSUw8U9REhUSWXR_j1OcVtzcg66g,7938
|
|
21
|
+
sentience/formatting.py,sha256=rptVpktftZZa5YJu4H2-K5dL8pzdIaPO9ONlEGSWnQw,483
|
|
22
|
+
sentience/generator.py,sha256=Wj--yn6bVo02nABBzrAIKPMmU8TRQm3JHAOER9XXT4Y,8035
|
|
23
|
+
sentience/inspector.py,sha256=D4q_GqXUCV3nUJv8uDv2novDbgnkboHOc9ahGeSYw9Q,14562
|
|
24
|
+
sentience/llm_interaction_handler.py,sha256=elvNtmGwIvoRcdra_fvijSONWzhztgdDmuPFJF2Whkw,7218
|
|
25
|
+
sentience/llm_provider.py,sha256=NZjJOPgbIhTSEzVceCA45-ZTleNzt3R4wTwH3_5_xyA,45685
|
|
26
|
+
sentience/llm_provider_utils.py,sha256=Hkr30MzZ5AvROJxEZHW97FINklzU9xoV8lITUQRoFls,3854
|
|
27
|
+
sentience/llm_response_builder.py,sha256=9lKJcfvD2EY7XcJtTYWTLYkxMVv_6nvlKTcGCO1nB2Q,4935
|
|
28
|
+
sentience/models.py,sha256=Je9Hk_9AOo1L0gFNFgIMT_a4DjxQqST1XPqt4raqq1c,38358
|
|
29
|
+
sentience/ordinal.py,sha256=E6XKLiGPhwavsWOghWwsG00JxOupF5bJnwJ9ctF3rP0,9048
|
|
30
|
+
sentience/overlay.py,sha256=awphkU0ZvkQrxSwQz7S8rvc3gQIqrDhztcQzzjEnR2c,7738
|
|
31
|
+
sentience/protocols.py,sha256=jXAa-RTxdIlMgISxTlIC8P79JqoD4i5T-cjbtgLjuUk,5667
|
|
32
|
+
sentience/query.py,sha256=Dx_OU8gm1BS-Vl9Ks3StLeuGCuZe99D_abzo8kVkNGU,12869
|
|
33
|
+
sentience/read.py,sha256=W0VSh9aBaIOMfUBcmjFubN1Ax1TmpCF1E74SLensy-g,7109
|
|
34
|
+
sentience/recorder.py,sha256=6cJVmjTT7HpRaet6KvdpTFAPVwFmD30eXHtbAKdwoUw,20636
|
|
35
|
+
sentience/screenshot.py,sha256=BJTPsIeE8irP1bhj4Vti2ohsZn5RkkGFTaZJOB855Pk,3147
|
|
36
|
+
sentience/sentience_methods.py,sha256=NmWmf_xfDt8t6RJs5Gf317EDBDrLcOUmHjDhrGpr4ws,2552
|
|
37
|
+
sentience/snapshot.py,sha256=1awUnAmxumo7us_-lYGkHHJh3aqqNzyGTlGPVIcJ6kU,29593
|
|
38
|
+
sentience/snapshot_diff.py,sha256=eh0PAl4PGsfAK8H0JopyQ-WwtPpV24BYV5t_sBqBs18,4716
|
|
39
|
+
sentience/text_search.py,sha256=MIKNn9PG-P4-L_0SzBB5F6O9KM9dzEKB4vRa4wPqoBw,10597
|
|
40
|
+
sentience/trace_event_builder.py,sha256=oj8Wb7mbWwoKRwzRivyVWiBXjy3G2SRMiYX0EaNUmBg,5025
|
|
41
|
+
sentience/trace_file_manager.py,sha256=JxvCx-Hzfyy0wP0DHo9JPnrmFNGtLvdCrPfk0mGiqmE,6311
|
|
42
|
+
sentience/tracer_factory.py,sha256=89gkYF5dTFw44_Db_FKm4vcrSaYhwzhr1jRk13MdnWc,12873
|
|
43
|
+
sentience/tracing.py,sha256=48J-vmtfhXjrJ4UB3i_oHbBmcKs5UsJ8ixfK3MrMt_Q,13885
|
|
44
|
+
sentience/verification.py,sha256=MADOKXSj4MjAmpD8DF6VUyW9UBBv-R2GJF9M-B8s8JM,18571
|
|
45
|
+
sentience/visual_agent.py,sha256=jdBC2YAckJWM2gE2bCW_6-3GXHsmY-CF7sryqyWuWgc,83054
|
|
46
|
+
sentience/wait.py,sha256=sx-87AN1TO_f8UbNP9np8zA0t27zQzQg2c2TltOn2h0,4373
|
|
47
|
+
sentience/asserts/__init__.py,sha256=73TN2aAGrgOkz_wakG4hcd9Cy69MF_3fRWiSFvFh-0o,1898
|
|
48
|
+
sentience/asserts/expect.py,sha256=IXf9yUSVDVFhSwfDGZqKb0raRyvBRkL4PtiWVI27lf4,20831
|
|
49
|
+
sentience/asserts/query.py,sha256=DPnHxEcY8-PenSqd-9J_Kmert3j5ri8xIqYigCwAlvM,11752
|
|
50
|
+
sentience/backends/__init__.py,sha256=HMEqcdp3mYfXYJwO4am4ndlCiRYIVCXcWOJR-OvU2Ic,4040
|
|
51
|
+
sentience/backends/actions.py,sha256=HTJy3xqSH6aZgSMVED5cW5hkCdiNNs_01fp1qo8Jawg,11637
|
|
52
|
+
sentience/backends/browser_use_adapter.py,sha256=bFb1PkPoZws4Ykq08a_ChK2Ms-Xobg94xCmEUmeGJDc,8066
|
|
53
|
+
sentience/backends/cdp_backend.py,sha256=sZJuOpSEhKTfGftbHMglja6AhuXUYlv_-eZS4pi9OY8,12674
|
|
54
|
+
sentience/backends/exceptions.py,sha256=vbvVn62HflMLPOc3YT8SngmjxPTLNL4VoE0eBLDO9cw,6687
|
|
55
|
+
sentience/backends/playwright_backend.py,sha256=XWBxFDGhIWJ6StzKdWNAxDP5aqTW7dzo74JN_qUfUOQ,6707
|
|
56
|
+
sentience/backends/protocol.py,sha256=sL11XDFUnxB-87RdWpN2266Z4WYxv74m1DH-Vsen3-k,5599
|
|
57
|
+
sentience/backends/sentience_context.py,sha256=0fAEpuK-YvD_tlqI0as-sIOXOoIoqFFY14rS1vTAT_w,16689
|
|
58
|
+
sentience/backends/snapshot.py,sha256=YAxaYH3dwaikrMswbhWGsncnBdWUV7lRY2n-oPCfKJo,15476
|
|
59
|
+
sentience/extension/background.js,sha256=Om-Wsu-DP0gjKILWjysW7le_BOCKm5YugUuuTMuic14,3754
|
|
60
|
+
sentience/extension/content.js,sha256=zdVveMeX_2bMupuzehSeAR5xtFzhMRC78hUn0O1oXmA,11249
|
|
61
|
+
sentience/extension/injected_api.js,sha256=pb6CGee5e1r9-DwEMWLsugSH7FpGOL-fxwV1nYpTY-w,66861
|
|
62
|
+
sentience/extension/manifest.json,sha256=FFaImjuIKdON9238WF5YypoPLJn_iWD6BaAt5dRIdvo,897
|
|
63
|
+
sentience/extension/release.json,sha256=w7c7qnB9HfqCgCoONOXkeiw1wSVVOpU484VPKczrQdc,6141
|
|
64
|
+
sentience/extension/pkg/sentience_core.d.ts,sha256=E_rJXQknWuU_8m5Nmt7buXy2J20bvMjBzIir31NBi8U,2011
|
|
65
|
+
sentience/extension/pkg/sentience_core.js,sha256=cUkeYs5FwGkFQ6l5zVWeEdSecFcSd4lAc-JC79DK13U,14623
|
|
66
|
+
sentience/extension/pkg/sentience_core_bg.wasm,sha256=drEsNRJ_s1OlU5CfJV6t_6fIHQggVQ2tl_9iPXmiVM4,112142
|
|
67
|
+
sentience/extension/pkg/sentience_core_bg.wasm.d.ts,sha256=O3c3HaUqmB6Aob6Pt0n3GEtTxM4VGeaClaA_-z2m2J4,517
|
|
68
|
+
sentience/integrations/__init__.py,sha256=TbJiIGTqpv9Bq9Kmq1img5aMlpEbfXPNnWxm5AK5H0w,221
|
|
69
|
+
sentience/integrations/models.py,sha256=TAcZhyvyisUcsTIihWPV1KCG2ylN2TuiwUKbvbkXDYk,1112
|
|
70
|
+
sentience/integrations/langchain/__init__.py,sha256=_8_fkan9_GXo-OLBUOFyUeISvtpza1w385sNQ8gK9zI,460
|
|
71
|
+
sentience/integrations/langchain/context.py,sha256=5iMQmANttMvnI8uuuEgUgfAPGqvgse0bYov4nvr8X-0,418
|
|
72
|
+
sentience/integrations/langchain/core.py,sha256=pFGJQqXkPOciC_CKymLGw0_E4_gmyPeu1tVXALbi8gw,11258
|
|
73
|
+
sentience/integrations/langchain/tools.py,sha256=kZ48jVl_VxSjUVVH7a9RYV2VvZu8ti72fi6NQ6zoYS4,7758
|
|
74
|
+
sentience/integrations/pydanticai/__init__.py,sha256=qeSnhoyAnyYQUC8ERk0FiAijPXHzf76Zpc5i5Zm64KU,464
|
|
75
|
+
sentience/integrations/pydanticai/deps.py,sha256=6uwsc1a2xVjMPV4grsymxLfgVmN25rDoQz4GhO7lFcw,460
|
|
76
|
+
sentience/integrations/pydanticai/toolset.py,sha256=J6uHHikFy5J2qjzh4BzlWL-MUVr_JBt4jb6CRhrxr0E,15378
|
|
77
|
+
sentience/schemas/trace_v1.json,sha256=qhMtb8PcuUwFSmuetkvKFcVOc7fcUpNvun372O6f12E,13487
|
|
78
|
+
sentience/trace_indexing/__init__.py,sha256=urjLuqqXCQE8pnwpYBqoMKnzZSqFJkM1SHmFve9RVE8,499
|
|
79
|
+
sentience/trace_indexing/index_schema.py,sha256=4ovMC_4t55DUkclq8NnBEEnKUZGfIieSkwMU_aBExKA,6262
|
|
80
|
+
sentience/trace_indexing/indexer.py,sha256=zQi8kDHisp5Pb6x42D7odM0Bu5fEL2LPpb74oHwyck4,13974
|
|
81
|
+
sentience/utils/__init__.py,sha256=h8vjeonoe9LF6TeeoJuJ9g6BdsCUP24lhmME7T9jhok,1125
|
|
82
|
+
sentience/utils/browser.py,sha256=pcDlAMgvmkjG13lNXe_LXiHmQs2QRQQBliTZiP7f1wA,1313
|
|
83
|
+
sentience/utils/element.py,sha256=S8AL9T-wqrbSJvJE-qr_Qv2FVk_gRiy6nx30hdRXy4E,6962
|
|
84
|
+
sentience/utils/formatting.py,sha256=IQ-3kLZu1k5ae4rlQ_AxnIkVPeLR9aa8J2NE-dj_eos,1829
|
|
85
|
+
sentienceapi-0.98.0.dist-info/licenses/LICENSE,sha256=jePeclQKwKdmz3jc0Oec6-jQuzwxIcGWPhMfxVII34Q,924
|
|
86
|
+
sentienceapi-0.98.0.dist-info/licenses/LICENSE-APACHE,sha256=YIflUygmGOc0hS_1f6EacrQUrH73G3j8VBQbDoqmY6A,11352
|
|
87
|
+
sentienceapi-0.98.0.dist-info/licenses/LICENSE-MIT,sha256=KiWwku3f8ikn6c8a6t0IdelVEu5GBLLr4tUKdGNNgJ8,1082
|
|
88
|
+
sentienceapi-0.98.0.dist-info/METADATA,sha256=HvsKbnvDI-aTxsD0rfW3od7AFH9Hv3vS3u1-tOS0VDg,32680
|
|
89
|
+
sentienceapi-0.98.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
90
|
+
sentienceapi-0.98.0.dist-info/entry_points.txt,sha256=HdW1BvgRJm3ZAbbqrwTvDWE2KbmVz-Ue0wllW-mLmvA,49
|
|
91
|
+
sentienceapi-0.98.0.dist-info/top_level.txt,sha256=A9IKao--8PsFFz5vDfBIXWHgN6oh3HkMQSiQWgUTUBQ,10
|
|
92
|
+
sentienceapi-0.98.0.dist-info/RECORD,,
|
sentience/utils.py
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Digest utilities for snapshot canonicalization and hashing.
|
|
3
|
-
|
|
4
|
-
Provides functions to compute stable digests of snapshots for determinism diff.
|
|
5
|
-
Two digest strategies:
|
|
6
|
-
- strict: includes structure + normalized text
|
|
7
|
-
- loose: structure only (no text) - detects layout changes vs content changes
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import hashlib
|
|
11
|
-
import json
|
|
12
|
-
import re
|
|
13
|
-
from dataclasses import dataclass
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any, Optional
|
|
16
|
-
|
|
17
|
-
from playwright.sync_api import BrowserContext
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclass
|
|
21
|
-
class BBox:
|
|
22
|
-
"""Bounding box with normalized coordinates."""
|
|
23
|
-
|
|
24
|
-
x: int
|
|
25
|
-
y: int
|
|
26
|
-
width: int
|
|
27
|
-
height: int
|
|
28
|
-
|
|
29
|
-
@classmethod
|
|
30
|
-
def from_dict(cls, bbox_dict: dict[str, Any]) -> "BBox":
|
|
31
|
-
"""Create BBox from dictionary."""
|
|
32
|
-
return cls(
|
|
33
|
-
x=int(bbox_dict.get("x", 0)),
|
|
34
|
-
y=int(bbox_dict.get("y", 0)),
|
|
35
|
-
width=int(bbox_dict.get("width", 0)),
|
|
36
|
-
height=int(bbox_dict.get("height", 0)),
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
def to_normalized(self, bucket_size: int = 2) -> list[int]:
|
|
40
|
-
"""
|
|
41
|
-
Normalize bbox to fixed-size buckets to ignore minor jitter.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
bucket_size: Pixel bucket size (default 2px)
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
List of [x, y, width, height] rounded to buckets
|
|
48
|
-
"""
|
|
49
|
-
return [
|
|
50
|
-
round(self.x / bucket_size) * bucket_size,
|
|
51
|
-
round(self.y / bucket_size) * bucket_size,
|
|
52
|
-
round(self.width / bucket_size) * bucket_size,
|
|
53
|
-
round(self.height / bucket_size) * bucket_size,
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@dataclass
|
|
58
|
-
class ElementFingerprint:
|
|
59
|
-
"""Normalized element data for digest computation."""
|
|
60
|
-
|
|
61
|
-
id: int
|
|
62
|
-
role: str
|
|
63
|
-
bbox: list[int] # Normalized
|
|
64
|
-
clickable: int # 0 or 1
|
|
65
|
-
primary: int # 0 or 1
|
|
66
|
-
text: str = "" # Empty for loose digest
|
|
67
|
-
|
|
68
|
-
def to_dict(self) -> dict[str, Any]:
|
|
69
|
-
"""Convert to dictionary for JSON serialization."""
|
|
70
|
-
data = {
|
|
71
|
-
"id": self.id,
|
|
72
|
-
"role": self.role,
|
|
73
|
-
"bbox": self.bbox,
|
|
74
|
-
"clickable": self.clickable,
|
|
75
|
-
"primary": self.primary,
|
|
76
|
-
}
|
|
77
|
-
if self.text: # Only include text if non-empty
|
|
78
|
-
data["text"] = self.text
|
|
79
|
-
return data
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def normalize_text_strict(text: str | None, max_length: int = 80) -> str:
|
|
83
|
-
"""
|
|
84
|
-
Normalize text for strict digest (structure + content).
|
|
85
|
-
|
|
86
|
-
Rules:
|
|
87
|
-
- Lowercase
|
|
88
|
-
- Trim and collapse whitespace
|
|
89
|
-
- Cap length at max_length
|
|
90
|
-
- Replace digit runs with '#'
|
|
91
|
-
- Normalize currency: $79.99 -> $#
|
|
92
|
-
- Normalize time patterns: 12:34 -> #:#
|
|
93
|
-
|
|
94
|
-
Args:
|
|
95
|
-
text: Input text
|
|
96
|
-
max_length: Maximum text length (default 80)
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
Normalized text string
|
|
100
|
-
"""
|
|
101
|
-
if not text:
|
|
102
|
-
return ""
|
|
103
|
-
|
|
104
|
-
# Lowercase and trim
|
|
105
|
-
text = text.strip().lower()
|
|
106
|
-
|
|
107
|
-
# Collapse whitespace
|
|
108
|
-
text = " ".join(text.split())
|
|
109
|
-
|
|
110
|
-
# Cap length
|
|
111
|
-
text = text[:max_length]
|
|
112
|
-
|
|
113
|
-
# Replace digit runs with #
|
|
114
|
-
text = re.sub(r"\d+", "#", text)
|
|
115
|
-
|
|
116
|
-
# Normalize currency
|
|
117
|
-
text = re.sub(r"\$\s*#", "$#", text)
|
|
118
|
-
|
|
119
|
-
# Normalize time patterns (HH:MM or similar)
|
|
120
|
-
text = re.sub(r"#:#", "#:#", text)
|
|
121
|
-
|
|
122
|
-
# Normalize date patterns (YYYY-MM-DD or similar)
|
|
123
|
-
text = re.sub(r"#-#-#", "#-#-#", text)
|
|
124
|
-
|
|
125
|
-
return text
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def normalize_bbox(bbox: dict[str, Any] | BBox, bucket_size: int = 2) -> list[int]:
|
|
129
|
-
"""
|
|
130
|
-
Round bbox to fixed-size buckets to ignore jitter.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
bbox: BBox object or dict with x, y, width, height
|
|
134
|
-
bucket_size: Pixel bucket size (default 2px)
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
List of [x, y, width, height] rounded to buckets
|
|
138
|
-
"""
|
|
139
|
-
if isinstance(bbox, BBox):
|
|
140
|
-
return bbox.to_normalized(bucket_size)
|
|
141
|
-
|
|
142
|
-
bbox_obj = BBox.from_dict(bbox)
|
|
143
|
-
return bbox_obj.to_normalized(bucket_size)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def extract_element_fingerprint(
|
|
147
|
-
element: dict[str, Any],
|
|
148
|
-
include_text: bool = True,
|
|
149
|
-
) -> ElementFingerprint:
|
|
150
|
-
"""
|
|
151
|
-
Extract normalized fingerprint from element dict.
|
|
152
|
-
|
|
153
|
-
Args:
|
|
154
|
-
element: Element dict from snapshot
|
|
155
|
-
include_text: Whether to include normalized text (False for loose digest)
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
ElementFingerprint with normalized data
|
|
159
|
-
"""
|
|
160
|
-
# Extract basic fields
|
|
161
|
-
element_id = element.get("id", 0)
|
|
162
|
-
role = element.get("role", "unknown")
|
|
163
|
-
|
|
164
|
-
# Extract and normalize bbox
|
|
165
|
-
bbox_data = element.get("bbox", {})
|
|
166
|
-
bbox_normalized = normalize_bbox(bbox_data)
|
|
167
|
-
|
|
168
|
-
# Extract visual cues
|
|
169
|
-
visual_cues = element.get("visual_cues", {})
|
|
170
|
-
clickable = 1 if visual_cues.get("is_clickable", False) else 0
|
|
171
|
-
primary = 1 if visual_cues.get("is_primary", False) else 0
|
|
172
|
-
|
|
173
|
-
# Extract and normalize text (if requested)
|
|
174
|
-
text = ""
|
|
175
|
-
if include_text:
|
|
176
|
-
raw_text = element.get("text", "")
|
|
177
|
-
text = normalize_text_strict(raw_text)
|
|
178
|
-
|
|
179
|
-
return ElementFingerprint(
|
|
180
|
-
id=element_id,
|
|
181
|
-
role=role,
|
|
182
|
-
bbox=bbox_normalized,
|
|
183
|
-
clickable=clickable,
|
|
184
|
-
primary=primary,
|
|
185
|
-
text=text,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def canonical_snapshot_strict(elements: list[dict[str, Any]]) -> str:
|
|
190
|
-
"""
|
|
191
|
-
Create strict snapshot digest (structure + normalized text).
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
elements: List of element dicts from snapshot
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
Canonical JSON string for hashing
|
|
198
|
-
"""
|
|
199
|
-
fingerprints = []
|
|
200
|
-
|
|
201
|
-
for element in sorted(elements, key=lambda e: e.get("id", 0)):
|
|
202
|
-
fingerprint = extract_element_fingerprint(element, include_text=True)
|
|
203
|
-
fingerprints.append(fingerprint.to_dict())
|
|
204
|
-
|
|
205
|
-
return json.dumps(fingerprints, sort_keys=True, ensure_ascii=False)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def canonical_snapshot_loose(elements: list[dict[str, Any]]) -> str:
|
|
209
|
-
"""
|
|
210
|
-
Create loose snapshot digest (structure only, no text).
|
|
211
|
-
|
|
212
|
-
This is more resistant to content churn (prices, ads, timestamps).
|
|
213
|
-
Use for detecting structural changes vs content changes.
|
|
214
|
-
|
|
215
|
-
Args:
|
|
216
|
-
elements: List of element dicts from snapshot
|
|
217
|
-
|
|
218
|
-
Returns:
|
|
219
|
-
Canonical JSON string for hashing
|
|
220
|
-
"""
|
|
221
|
-
fingerprints = []
|
|
222
|
-
|
|
223
|
-
for element in sorted(elements, key=lambda e: e.get("id", 0)):
|
|
224
|
-
fingerprint = extract_element_fingerprint(element, include_text=False)
|
|
225
|
-
fingerprints.append(fingerprint.to_dict())
|
|
226
|
-
|
|
227
|
-
return json.dumps(fingerprints, sort_keys=True, ensure_ascii=False)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def sha256_digest(canonical_str: str) -> str:
|
|
231
|
-
"""
|
|
232
|
-
Compute SHA256 hash with 'sha256:' prefix.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
canonical_str: Canonical string to hash
|
|
236
|
-
|
|
237
|
-
Returns:
|
|
238
|
-
Hash string with format: "sha256:<hex>"
|
|
239
|
-
"""
|
|
240
|
-
hash_obj = hashlib.sha256(canonical_str.encode("utf-8"))
|
|
241
|
-
return f"sha256:{hash_obj.hexdigest()}"
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def compute_snapshot_digests(elements: list[dict[str, Any]]) -> dict[str, str]:
|
|
245
|
-
"""
|
|
246
|
-
Compute both strict and loose digests for a snapshot.
|
|
247
|
-
|
|
248
|
-
Args:
|
|
249
|
-
elements: List of element dicts from snapshot
|
|
250
|
-
|
|
251
|
-
Returns:
|
|
252
|
-
Dict with 'strict' and 'loose' digest strings
|
|
253
|
-
"""
|
|
254
|
-
canonical_strict = canonical_snapshot_strict(elements)
|
|
255
|
-
canonical_loose = canonical_snapshot_loose(elements)
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
"strict": sha256_digest(canonical_strict),
|
|
259
|
-
"loose": sha256_digest(canonical_loose),
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def save_storage_state(context: BrowserContext, file_path: str | Path) -> None:
|
|
264
|
-
"""
|
|
265
|
-
Save current browser storage state (cookies + localStorage) to a file.
|
|
266
|
-
|
|
267
|
-
This is useful for capturing a logged-in session to reuse later.
|
|
268
|
-
|
|
269
|
-
Args:
|
|
270
|
-
context: Playwright BrowserContext
|
|
271
|
-
file_path: Path to save the storage state JSON file
|
|
272
|
-
|
|
273
|
-
Example:
|
|
274
|
-
```python
|
|
275
|
-
from sentience import SentienceBrowser, save_storage_state
|
|
276
|
-
|
|
277
|
-
browser = SentienceBrowser()
|
|
278
|
-
browser.start()
|
|
279
|
-
|
|
280
|
-
# User logs in manually or via agent
|
|
281
|
-
browser.goto("https://example.com")
|
|
282
|
-
# ... login happens ...
|
|
283
|
-
|
|
284
|
-
# Save session for later
|
|
285
|
-
save_storage_state(browser.context, "auth.json")
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
Raises:
|
|
289
|
-
IOError: If file cannot be written
|
|
290
|
-
"""
|
|
291
|
-
storage_state = context.storage_state()
|
|
292
|
-
file_path_obj = Path(file_path)
|
|
293
|
-
file_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
294
|
-
with open(file_path_obj, "w") as f:
|
|
295
|
-
json.dump(storage_state, f, indent=2)
|
|
296
|
-
print(f"✅ [Sentience] Saved storage state to {file_path_obj}")
|