sentienceapi 0.95.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 +253 -0
- sentience/_extension_loader.py +195 -0
- sentience/action_executor.py +215 -0
- sentience/actions.py +1020 -0
- sentience/agent.py +1181 -0
- sentience/agent_config.py +46 -0
- sentience/agent_runtime.py +424 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +108 -0
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +343 -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 +427 -0
- sentience/base_agent.py +196 -0
- sentience/browser.py +1215 -0
- sentience/browser_evaluator.py +299 -0
- sentience/canonicalization.py +207 -0
- sentience/cli.py +130 -0
- sentience/cloud_tracing.py +807 -0
- sentience/constants.py +6 -0
- sentience/conversational_agent.py +543 -0
- sentience/element_filter.py +136 -0
- sentience/expect.py +188 -0
- sentience/extension/background.js +104 -0
- sentience/extension/content.js +161 -0
- sentience/extension/injected_api.js +914 -0
- sentience/extension/manifest.json +36 -0
- sentience/extension/pkg/sentience_core.d.ts +51 -0
- sentience/extension/pkg/sentience_core.js +323 -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/formatting.py +15 -0
- sentience/generator.py +202 -0
- sentience/inspector.py +367 -0
- sentience/llm_interaction_handler.py +191 -0
- sentience/llm_provider.py +875 -0
- sentience/llm_provider_utils.py +120 -0
- sentience/llm_response_builder.py +153 -0
- sentience/models.py +846 -0
- sentience/ordinal.py +280 -0
- sentience/overlay.py +222 -0
- sentience/protocols.py +228 -0
- sentience/query.py +303 -0
- sentience/read.py +188 -0
- sentience/recorder.py +589 -0
- sentience/schemas/trace_v1.json +335 -0
- sentience/screenshot.py +100 -0
- sentience/sentience_methods.py +86 -0
- sentience/snapshot.py +706 -0
- sentience/snapshot_diff.py +126 -0
- sentience/text_search.py +262 -0
- sentience/trace_event_builder.py +148 -0
- sentience/trace_file_manager.py +197 -0
- sentience/trace_indexing/__init__.py +27 -0
- sentience/trace_indexing/index_schema.py +199 -0
- sentience/trace_indexing/indexer.py +414 -0
- sentience/tracer_factory.py +322 -0
- sentience/tracing.py +449 -0
- sentience/utils/__init__.py +40 -0
- sentience/utils/browser.py +46 -0
- sentience/utils/element.py +257 -0
- sentience/utils/formatting.py +59 -0
- sentience/utils.py +296 -0
- sentience/verification.py +380 -0
- sentience/visual_agent.py +2058 -0
- sentience/wait.py +139 -0
- sentienceapi-0.95.0.dist-info/METADATA +984 -0
- sentienceapi-0.95.0.dist-info/RECORD +82 -0
- sentienceapi-0.95.0.dist-info/WHEEL +5 -0
- sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
- sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
- sentienceapi-0.95.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Playwright backend implementation for BrowserBackend protocol.
|
|
3
|
+
|
|
4
|
+
This wraps existing SentienceBrowser/AsyncSentienceBrowser to provide
|
|
5
|
+
a unified interface, enabling code that works with both browser-use
|
|
6
|
+
(CDPBackendV0) and native Playwright (PlaywrightBackend).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from sentience import SentienceBrowserAsync
|
|
10
|
+
from sentience.backends import PlaywrightBackend, snapshot_from_backend
|
|
11
|
+
|
|
12
|
+
browser = SentienceBrowserAsync()
|
|
13
|
+
await browser.start()
|
|
14
|
+
await browser.goto("https://example.com")
|
|
15
|
+
|
|
16
|
+
# Create backend from existing browser
|
|
17
|
+
backend = PlaywrightBackend(browser.page)
|
|
18
|
+
|
|
19
|
+
# Use backend-agnostic functions
|
|
20
|
+
snap = await snapshot_from_backend(backend)
|
|
21
|
+
await click(backend, element.bbox)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import base64
|
|
26
|
+
import time
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
28
|
+
|
|
29
|
+
from .protocol import BrowserBackend, LayoutMetrics, ViewportInfo
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from playwright.async_api import Page as AsyncPage
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PlaywrightBackend:
|
|
36
|
+
"""
|
|
37
|
+
Playwright-based implementation of BrowserBackend.
|
|
38
|
+
|
|
39
|
+
Wraps a Playwright async Page to provide the standard backend interface.
|
|
40
|
+
This enables using backend-agnostic actions with existing SentienceBrowser code.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, page: "AsyncPage") -> None:
|
|
44
|
+
"""
|
|
45
|
+
Initialize Playwright backend.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
page: Playwright async Page object
|
|
49
|
+
"""
|
|
50
|
+
self._page = page
|
|
51
|
+
self._cached_viewport: ViewportInfo | None = None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def page(self) -> "AsyncPage":
|
|
55
|
+
"""Access the underlying Playwright page."""
|
|
56
|
+
return self._page
|
|
57
|
+
|
|
58
|
+
async def refresh_page_info(self) -> ViewportInfo:
|
|
59
|
+
"""Cache viewport + scroll offsets; cheap & safe to call often."""
|
|
60
|
+
result = await self._page.evaluate(
|
|
61
|
+
"""
|
|
62
|
+
(() => ({
|
|
63
|
+
width: window.innerWidth,
|
|
64
|
+
height: window.innerHeight,
|
|
65
|
+
scroll_x: window.scrollX,
|
|
66
|
+
scroll_y: window.scrollY,
|
|
67
|
+
content_width: document.documentElement.scrollWidth,
|
|
68
|
+
content_height: document.documentElement.scrollHeight
|
|
69
|
+
}))()
|
|
70
|
+
"""
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self._cached_viewport = ViewportInfo(
|
|
74
|
+
width=result.get("width", 0),
|
|
75
|
+
height=result.get("height", 0),
|
|
76
|
+
scroll_x=result.get("scroll_x", 0),
|
|
77
|
+
scroll_y=result.get("scroll_y", 0),
|
|
78
|
+
content_width=result.get("content_width"),
|
|
79
|
+
content_height=result.get("content_height"),
|
|
80
|
+
)
|
|
81
|
+
return self._cached_viewport
|
|
82
|
+
|
|
83
|
+
async def eval(self, expression: str) -> Any:
|
|
84
|
+
"""Evaluate JavaScript expression in page context."""
|
|
85
|
+
return await self._page.evaluate(expression)
|
|
86
|
+
|
|
87
|
+
async def call(
|
|
88
|
+
self,
|
|
89
|
+
function_declaration: str,
|
|
90
|
+
args: list[Any] | None = None,
|
|
91
|
+
) -> Any:
|
|
92
|
+
"""Call JavaScript function with arguments."""
|
|
93
|
+
if args:
|
|
94
|
+
return await self._page.evaluate(function_declaration, *args)
|
|
95
|
+
return await self._page.evaluate(f"({function_declaration})()")
|
|
96
|
+
|
|
97
|
+
async def get_layout_metrics(self) -> LayoutMetrics:
|
|
98
|
+
"""Get page layout metrics."""
|
|
99
|
+
# Playwright doesn't expose CDP directly in the same way,
|
|
100
|
+
# so we approximate using JavaScript
|
|
101
|
+
result = await self._page.evaluate(
|
|
102
|
+
"""
|
|
103
|
+
(() => ({
|
|
104
|
+
viewport_x: window.scrollX,
|
|
105
|
+
viewport_y: window.scrollY,
|
|
106
|
+
viewport_width: window.innerWidth,
|
|
107
|
+
viewport_height: window.innerHeight,
|
|
108
|
+
content_width: document.documentElement.scrollWidth,
|
|
109
|
+
content_height: document.documentElement.scrollHeight,
|
|
110
|
+
device_scale_factor: window.devicePixelRatio || 1
|
|
111
|
+
}))()
|
|
112
|
+
"""
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return LayoutMetrics(
|
|
116
|
+
viewport_x=result.get("viewport_x", 0),
|
|
117
|
+
viewport_y=result.get("viewport_y", 0),
|
|
118
|
+
viewport_width=result.get("viewport_width", 0),
|
|
119
|
+
viewport_height=result.get("viewport_height", 0),
|
|
120
|
+
content_width=result.get("content_width", 0),
|
|
121
|
+
content_height=result.get("content_height", 0),
|
|
122
|
+
device_scale_factor=result.get("device_scale_factor", 1.0),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def screenshot_png(self) -> bytes:
|
|
126
|
+
"""Capture viewport screenshot as PNG bytes."""
|
|
127
|
+
return await self._page.screenshot(type="png")
|
|
128
|
+
|
|
129
|
+
async def mouse_move(self, x: float, y: float) -> None:
|
|
130
|
+
"""Move mouse to viewport coordinates."""
|
|
131
|
+
await self._page.mouse.move(x, y)
|
|
132
|
+
|
|
133
|
+
async def mouse_click(
|
|
134
|
+
self,
|
|
135
|
+
x: float,
|
|
136
|
+
y: float,
|
|
137
|
+
button: Literal["left", "right", "middle"] = "left",
|
|
138
|
+
click_count: int = 1,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Click at viewport coordinates."""
|
|
141
|
+
await self._page.mouse.click(x, y, button=button, click_count=click_count)
|
|
142
|
+
|
|
143
|
+
async def wheel(
|
|
144
|
+
self,
|
|
145
|
+
delta_y: float,
|
|
146
|
+
x: float | None = None,
|
|
147
|
+
y: float | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Scroll using mouse wheel."""
|
|
150
|
+
# Get viewport center if coordinates not provided
|
|
151
|
+
if x is None or y is None:
|
|
152
|
+
if self._cached_viewport is None:
|
|
153
|
+
await self.refresh_page_info()
|
|
154
|
+
assert self._cached_viewport is not None
|
|
155
|
+
x = x if x is not None else self._cached_viewport.width / 2
|
|
156
|
+
y = y if y is not None else self._cached_viewport.height / 2
|
|
157
|
+
|
|
158
|
+
await self._page.mouse.wheel(0, delta_y)
|
|
159
|
+
|
|
160
|
+
async def type_text(self, text: str) -> None:
|
|
161
|
+
"""Type text using keyboard input."""
|
|
162
|
+
await self._page.keyboard.type(text)
|
|
163
|
+
|
|
164
|
+
async def wait_ready_state(
|
|
165
|
+
self,
|
|
166
|
+
state: Literal["interactive", "complete"] = "interactive",
|
|
167
|
+
timeout_ms: int = 15000,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Wait for document.readyState to reach target state."""
|
|
170
|
+
acceptable_states = {"complete"} if state == "complete" else {"interactive", "complete"}
|
|
171
|
+
|
|
172
|
+
start = time.monotonic()
|
|
173
|
+
timeout_sec = timeout_ms / 1000.0
|
|
174
|
+
|
|
175
|
+
while True:
|
|
176
|
+
elapsed = time.monotonic() - start
|
|
177
|
+
if elapsed >= timeout_sec:
|
|
178
|
+
raise TimeoutError(
|
|
179
|
+
f"Timed out waiting for document.readyState='{state}' " f"after {timeout_ms}ms"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
current_state = await self._page.evaluate("document.readyState")
|
|
183
|
+
if current_state in acceptable_states:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
await asyncio.sleep(0.1)
|
|
187
|
+
|
|
188
|
+
async def get_url(self) -> str:
|
|
189
|
+
"""Get current page URL."""
|
|
190
|
+
return self._page.url
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# Verify protocol compliance at import time
|
|
194
|
+
assert isinstance(PlaywrightBackend.__new__(PlaywrightBackend), BrowserBackend)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
v0 BrowserBackend Protocol - Minimal interface for browser-use integration.
|
|
3
|
+
|
|
4
|
+
This protocol defines the minimal interface required to:
|
|
5
|
+
- Take Sentience snapshots (DOM/geometry via extension)
|
|
6
|
+
- Compute viewport-coord clicks
|
|
7
|
+
- Scroll + re-snapshot + click
|
|
8
|
+
- Stabilize after action
|
|
9
|
+
|
|
10
|
+
No navigation API required (browser-use already handles navigation).
|
|
11
|
+
|
|
12
|
+
Design principle: Keep it so small that nothing can break.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Any, Literal, Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ViewportInfo(BaseModel):
|
|
21
|
+
"""Viewport and scroll position information."""
|
|
22
|
+
|
|
23
|
+
width: int
|
|
24
|
+
height: int
|
|
25
|
+
scroll_x: float = 0.0
|
|
26
|
+
scroll_y: float = 0.0
|
|
27
|
+
content_width: float | None = None
|
|
28
|
+
content_height: float | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LayoutMetrics(BaseModel):
|
|
32
|
+
"""Page layout metrics from CDP Page.getLayoutMetrics."""
|
|
33
|
+
|
|
34
|
+
# Viewport dimensions
|
|
35
|
+
viewport_x: float = 0.0
|
|
36
|
+
viewport_y: float = 0.0
|
|
37
|
+
viewport_width: float = 0.0
|
|
38
|
+
viewport_height: float = 0.0
|
|
39
|
+
|
|
40
|
+
# Content dimensions (scrollable area)
|
|
41
|
+
content_width: float = 0.0
|
|
42
|
+
content_height: float = 0.0
|
|
43
|
+
|
|
44
|
+
# Device scale factor
|
|
45
|
+
device_scale_factor: float = 1.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class BrowserBackend(Protocol):
|
|
50
|
+
"""
|
|
51
|
+
Minimal backend protocol for v0 proof-of-concept.
|
|
52
|
+
|
|
53
|
+
This is enough to:
|
|
54
|
+
- Take Sentience snapshots (DOM/geometry via extension)
|
|
55
|
+
- Execute JavaScript for element interaction
|
|
56
|
+
- Perform mouse operations (move, click, scroll)
|
|
57
|
+
- Wait for page stability
|
|
58
|
+
|
|
59
|
+
Implementers:
|
|
60
|
+
- CDPBackendV0: For browser-use integration via CDP
|
|
61
|
+
- PlaywrightBackend: Wrapper around existing SentienceBrowser (future)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
async def refresh_page_info(self) -> ViewportInfo:
|
|
65
|
+
"""
|
|
66
|
+
Cache viewport + scroll offsets + url; cheap & safe to call often.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
ViewportInfo with current viewport state
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
async def eval(self, expression: str) -> Any:
|
|
74
|
+
"""
|
|
75
|
+
Evaluate JavaScript expression in page context.
|
|
76
|
+
|
|
77
|
+
Uses CDP Runtime.evaluate with returnByValue=True.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
expression: JavaScript expression to evaluate
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Result value (JSON-serializable)
|
|
84
|
+
"""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
async def call(
|
|
88
|
+
self,
|
|
89
|
+
function_declaration: str,
|
|
90
|
+
args: list[Any] | None = None,
|
|
91
|
+
) -> Any:
|
|
92
|
+
"""
|
|
93
|
+
Call a JavaScript function with arguments.
|
|
94
|
+
|
|
95
|
+
Uses CDP Runtime.callFunctionOn for safe argument passing.
|
|
96
|
+
Safer than eval() for passing complex arguments.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
function_declaration: JavaScript function body, e.g., "(x, y) => x + y"
|
|
100
|
+
args: Arguments to pass to the function
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Result value (JSON-serializable)
|
|
104
|
+
"""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
async def get_layout_metrics(self) -> LayoutMetrics:
|
|
108
|
+
"""
|
|
109
|
+
Get page layout metrics.
|
|
110
|
+
|
|
111
|
+
Uses CDP Page.getLayoutMetrics to get viewport and content dimensions.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
LayoutMetrics with viewport and content size info
|
|
115
|
+
"""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
async def screenshot_png(self) -> bytes:
|
|
119
|
+
"""
|
|
120
|
+
Capture viewport screenshot as PNG bytes.
|
|
121
|
+
|
|
122
|
+
Uses CDP Page.captureScreenshot.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
PNG image bytes
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
async def mouse_move(self, x: float, y: float) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Move mouse to viewport coordinates.
|
|
132
|
+
|
|
133
|
+
Uses CDP Input.dispatchMouseEvent with type="mouseMoved".
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
x: X coordinate in viewport
|
|
137
|
+
y: Y coordinate in viewport
|
|
138
|
+
"""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
async def mouse_click(
|
|
142
|
+
self,
|
|
143
|
+
x: float,
|
|
144
|
+
y: float,
|
|
145
|
+
button: Literal["left", "right", "middle"] = "left",
|
|
146
|
+
click_count: int = 1,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Click at viewport coordinates.
|
|
150
|
+
|
|
151
|
+
Uses CDP Input.dispatchMouseEvent with mousePressed + mouseReleased.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
x: X coordinate in viewport
|
|
155
|
+
y: Y coordinate in viewport
|
|
156
|
+
button: Mouse button to click
|
|
157
|
+
click_count: Number of clicks (1 for single, 2 for double)
|
|
158
|
+
"""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
async def wheel(
|
|
162
|
+
self,
|
|
163
|
+
delta_y: float,
|
|
164
|
+
x: float | None = None,
|
|
165
|
+
y: float | None = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Scroll using mouse wheel.
|
|
169
|
+
|
|
170
|
+
Uses CDP Input.dispatchMouseEvent with type="mouseWheel".
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
delta_y: Scroll amount (positive = down, negative = up)
|
|
174
|
+
x: X coordinate for scroll (default: viewport center)
|
|
175
|
+
y: Y coordinate for scroll (default: viewport center)
|
|
176
|
+
"""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
async def type_text(self, text: str) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Type text using keyboard input.
|
|
182
|
+
|
|
183
|
+
Uses CDP Input.dispatchKeyEvent for each character.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
text: Text to type
|
|
187
|
+
"""
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
async def wait_ready_state(
|
|
191
|
+
self,
|
|
192
|
+
state: Literal["interactive", "complete"] = "interactive",
|
|
193
|
+
timeout_ms: int = 15000,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Wait for document.readyState to reach target state.
|
|
197
|
+
|
|
198
|
+
Uses polling instead of CDP events (no leak from unregistered listeners).
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
state: Target state ("interactive" or "complete")
|
|
202
|
+
timeout_ms: Maximum time to wait in milliseconds
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
TimeoutError: If state not reached within timeout
|
|
206
|
+
"""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
async def get_url(self) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Get current page URL.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Current page URL (window.location.href)
|
|
215
|
+
"""
|
|
216
|
+
...
|