Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "sentienceapi"
version = "0.90.15"
version = "0.90.16"
description = "Python SDK for Sentience AI Agent Browser Automation"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion sentience/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
)
from .wait import wait_for

__version__ = "0.90.15"
__version__ = "0.90.16"

__all__ = [
# Core SDK
Expand Down
102 changes: 101 additions & 1 deletion sentience/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(
storage_state: str | Path | StorageState | dict | None = None,
record_video_dir: str | Path | None = None,
record_video_size: dict[str, int] | None = None,
viewport: dict[str, int] | None = None,
):
"""
Initialize Sentience browser
Expand Down Expand Up @@ -67,6 +68,11 @@ def __init__(
Examples: {"width": 1280, "height": 800} (default)
{"width": 1920, "height": 1080} (1080p)
If None, defaults to 1280x800.
viewport: Optional viewport size as dict with 'width' and 'height' keys.
Examples: {"width": 1280, "height": 800} (default)
{"width": 1920, "height": 1080} (Full HD)
{"width": 375, "height": 667} (iPhone)
If None, defaults to 1280x800.
"""
self.api_key = api_key
# Only set api_url if api_key is provided, otherwise None (free tier)
Expand Down Expand Up @@ -94,6 +100,9 @@ def __init__(
self.record_video_dir = record_video_dir
self.record_video_size = record_video_size or {"width": 1280, "height": 800}

# Viewport configuration
self.viewport = viewport or {"width": 1280, "height": 800}

self.playwright: Playwright | None = None
self.context: BrowserContext | None = None
self.page: Page | None = None
Expand Down Expand Up @@ -211,7 +220,7 @@ def start(self) -> None:
"user_data_dir": user_data_dir,
"headless": False, # IMPORTANT: See note above
"args": args,
"viewport": {"width": 1280, "height": 800},
"viewport": self.viewport,
# Remove "HeadlessChrome" from User Agent automatically
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
}
Expand Down Expand Up @@ -480,6 +489,97 @@ def close(self, output_path: str | Path | None = None) -> str | None:

return final_path

@classmethod
def from_existing(
cls,
context: BrowserContext,
api_key: str | None = None,
api_url: str | None = None,
) -> "SentienceBrowser":
"""
Create SentienceBrowser from an existing Playwright BrowserContext.

This allows you to use Sentience SDK with a browser context you've already created,
giving you more control over browser initialization.

Args:
context: Existing Playwright BrowserContext
api_key: Optional API key for server-side processing
api_url: Optional API URL (defaults to https://api.sentienceapi.com if api_key provided)

Returns:
SentienceBrowser instance configured to use the existing context

Example:
from playwright.sync_api import sync_playwright
from sentience import SentienceBrowser, snapshot

with sync_playwright() as p:
context = p.chromium.launch_persistent_context(...)
browser = SentienceBrowser.from_existing(context)
browser.page.goto("https://example.com")
snap = snapshot(browser)
"""
instance = cls(api_key=api_key, api_url=api_url)
instance.context = context
instance.page = context.pages[0] if context.pages else context.new_page()

# Apply stealth if available
if STEALTH_AVAILABLE:
stealth_sync(instance.page)

# Wait for extension to be ready (if extension is loaded)
time.sleep(0.5)

return instance

@classmethod
def from_page(
cls,
page: Page,
api_key: str | None = None,
api_url: str | None = None,
) -> "SentienceBrowser":
"""
Create SentienceBrowser from an existing Playwright Page.

This allows you to use Sentience SDK with a page you've already created,
giving you more control over browser initialization.

Args:
page: Existing Playwright Page
api_key: Optional API key for server-side processing
api_url: Optional API URL (defaults to https://api.sentienceapi.com if api_key provided)

Returns:
SentienceBrowser instance configured to use the existing page

Example:
from playwright.sync_api import sync_playwright
from sentience import SentienceBrowser, snapshot

with sync_playwright() as p:
browser_instance = p.chromium.launch()
context = browser_instance.new_context()
page = context.new_page()
page.goto("https://example.com")

browser = SentienceBrowser.from_page(page)
snap = snapshot(browser)
"""
instance = cls(api_key=api_key, api_url=api_url)
instance.page = page
instance.context = page.context

# Apply stealth if available
if STEALTH_AVAILABLE:
stealth_sync(instance.page)

# Wait for extension to be ready (if extension is loaded)
time.sleep(0.5)

return instance

def __enter__(self):
"""Context manager entry"""
self.start()
Expand Down
6 changes: 3 additions & 3 deletions sentience/extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ async function handleScreenshotCapture(_tabId, options = {}) {
async function handleSnapshotProcessing(rawData, options = {}) {
const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs
const startTime = performance.now();

try {
// Safety check: limit element count to prevent hangs
if (!Array.isArray(rawData)) {
throw new Error('rawData must be an array');
}

if (rawData.length > MAX_ELEMENTS) {
console.warn(`[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.`);
rawData = rawData.slice(0, MAX_ELEMENTS);
Expand Down Expand Up @@ -186,7 +186,7 @@ async function handleSnapshotProcessing(rawData, options = {}) {
// Add timeout protection (18 seconds - less than content.js timeout)
analyzedElements = await Promise.race([
wasmPromise,
new Promise((_, reject) =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000)
)
]);
Expand Down
2 changes: 1 addition & 1 deletion sentience/extension/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function handleSnapshotRequest(data) {
if (responded) return; // Already responded via timeout
responded = true;
clearTimeout(timeoutId);

const duration = performance.now() - startTime;

// Handle Chrome extension errors (e.g., background script crashed)
Expand Down
Loading