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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,28 @@ for match in result.results:

---

## 🔄 Async API

For asyncio contexts (FastAPI, async frameworks):

```python
from sentience.async_api import AsyncSentienceBrowser, snapshot_async, click_async, find

async def main():
async with AsyncSentienceBrowser() as browser:
await browser.goto("https://example.com")
snap = await snapshot_async(browser)
button = find(snap, "role=button")
if button:
await click_async(browser, button.id)

asyncio.run(main())
```

**See example:** `examples/async_api_demo.py`

---

## 📋 Reference

<details>
Expand Down
192 changes: 192 additions & 0 deletions examples/async_api_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""
Example: Using Async API for asyncio contexts

This example demonstrates how to use the Sentience SDK's async API
when working with asyncio, FastAPI, or other async frameworks.

To run this example:
python -m examples.async_api_demo

Or if sentience is installed:
python examples/async_api_demo.py
"""

import asyncio
import os

# Import async API functions
from sentience.async_api import (
AsyncSentienceBrowser,
click_async,
find,
press_async,
snapshot_async,
type_text_async,
)
from sentience.models import SnapshotOptions, Viewport


async def basic_async_example():
"""Basic async browser usage with context manager"""
api_key = os.environ.get("SENTIENCE_API_KEY")

# Use async context manager
async with AsyncSentienceBrowser(api_key=api_key, headless=False) as browser:
# Navigate to a page
await browser.goto("https://example.com")

# Take a snapshot (async)
snap = await snapshot_async(browser)
print(f"✅ Found {len(snap.elements)} elements on the page")

# Find an element
link = find(snap, "role=link")
if link:
print(f"Found link: {link.text} (id: {link.id})")

# Click it (async)
result = await click_async(browser, link.id)
print(f"Click result: success={result.success}, outcome={result.outcome}")


async def custom_viewport_example():
"""Example using custom viewport with Viewport class"""
# Use Viewport class for type safety
custom_viewport = Viewport(width=1920, height=1080)

async with AsyncSentienceBrowser(viewport=custom_viewport, headless=False) as browser:
await browser.goto("https://example.com")

# Verify viewport size
viewport_size = await browser.page.evaluate(
"() => ({ width: window.innerWidth, height: window.innerHeight })"
)
print(f"✅ Viewport: {viewport_size['width']}x{viewport_size['height']}")


async def snapshot_with_options_example():
"""Example using SnapshotOptions with async API"""
async with AsyncSentienceBrowser(headless=False) as browser:
await browser.goto("https://example.com")

# Take snapshot with options
options = SnapshotOptions(
limit=10,
screenshot=False,
show_overlay=False,
)
snap = await snapshot_async(browser, options)
print(f"✅ Snapshot with limit=10: {len(snap.elements)} elements")


async def actions_example():
"""Example of all async actions"""
async with AsyncSentienceBrowser(headless=False) as browser:
await browser.goto("https://example.com")

# Take snapshot
snap = await snapshot_async(browser)

# Find a textbox if available
textbox = find(snap, "role=textbox")
if textbox:
# Type text (async)
result = await type_text_async(browser, textbox.id, "Hello, World!")
print(f"✅ Typed text: success={result.success}")

# Press a key (async)
result = await press_async(browser, "Enter")
print(f"✅ Pressed Enter: success={result.success}")


async def from_existing_context_example():
"""Example using from_existing() with existing Playwright context"""
from playwright.async_api import async_playwright

async with async_playwright() as p:
# Create your own Playwright context
context = await p.chromium.launch_persistent_context("", headless=True)

try:
# Create SentienceBrowser from existing context
browser = await AsyncSentienceBrowser.from_existing(context)
await browser.goto("https://example.com")

# Use Sentience SDK functions
snap = await snapshot_async(browser)
print(f"✅ Using existing context: {len(snap.elements)} elements")
finally:
await context.close()


async def from_existing_page_example():
"""Example using from_page() with existing Playwright page"""
from playwright.async_api import async_playwright

async with async_playwright() as p:
browser_instance = await p.chromium.launch(headless=True)
context = await browser_instance.new_context()
page = await context.new_page()

try:
# Create SentienceBrowser from existing page
sentience_browser = await AsyncSentienceBrowser.from_page(page)
await sentience_browser.goto("https://example.com")

# Use Sentience SDK functions
snap = await snapshot_async(sentience_browser)
print(f"✅ Using existing page: {len(snap.elements)} elements")
finally:
await context.close()
await browser_instance.close()


async def multiple_browsers_example():
"""Example running multiple browsers concurrently"""

async def process_site(url: str):
async with AsyncSentienceBrowser(headless=True) as browser:
await browser.goto(url)
snap = await snapshot_async(browser)
return {"url": url, "elements": len(snap.elements)}

# Process multiple sites concurrently
urls = [
"https://example.com",
"https://httpbin.org/html",
]

results = await asyncio.gather(*[process_site(url) for url in urls])
for result in results:
print(f"✅ {result['url']}: {result['elements']} elements")


async def main():
"""Run all examples"""
print("=== Basic Async Example ===")
await basic_async_example()

print("\n=== Custom Viewport Example ===")
await custom_viewport_example()

print("\n=== Snapshot with Options Example ===")
await snapshot_with_options_example()

print("\n=== Actions Example ===")
await actions_example()

print("\n=== From Existing Context Example ===")
await from_existing_context_example()

print("\n=== From Existing Page Example ===")
await from_existing_page_example()

print("\n=== Multiple Browsers Concurrent Example ===")
await multiple_browsers_example()

print("\n✅ All async examples completed!")


if __name__ == "__main__":
# Run the async main function
asyncio.run(main())
Binary file modified screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions sentience/_extension_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
Shared extension loading logic for sync and async implementations
"""

from pathlib import Path


def find_extension_path() -> Path:
"""
Find Sentience extension directory (shared logic for sync and async).

Checks multiple locations:
1. sentience/extension/ (installed package)
2. ../sentience-chrome (development/monorepo)

Returns:
Path to extension directory

Raises:
FileNotFoundError: If extension not found in any location
"""
# 1. Try relative to this file (installed package structure)
# sentience/_extension_loader.py -> sentience/extension/
package_ext_path = Path(__file__).parent / "extension"

# 2. Try development root (if running from source repo)
# sentience/_extension_loader.py -> ../sentience-chrome
dev_ext_path = Path(__file__).parent.parent.parent / "sentience-chrome"

if package_ext_path.exists() and (package_ext_path / "manifest.json").exists():
return package_ext_path
elif dev_ext_path.exists() and (dev_ext_path / "manifest.json").exists():
return dev_ext_path
else:
raise FileNotFoundError(
f"Extension not found. Checked:\n"
f"1. {package_ext_path}\n"
f"2. {dev_ext_path}\n"
"Make sure the extension is built and 'sentience/extension' directory exists."
)
Loading