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
91 changes: 71 additions & 20 deletions .github/workflows/sync-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # Fetch all history for proper branching

- name: Set up Python
uses: actions/setup-python@v5
Expand Down Expand Up @@ -62,34 +63,79 @@ jobs:
mkdir -p extension-temp
cd extension-temp

# Download each file from release
curl -L -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" \
# First, try to download the zip archive if available
ZIP_URL=$(curl -s -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" \
"https://api.github.com/repos/$REPO/releases/tags/$TAG" | \
jq -r '.assets[] | select(.name | endswith(".js") or endswith(".wasm") or endswith(".json") or endswith(".d.ts")) | .browser_download_url' | \
while read url; do
filename=$(basename "$url")
curl -L -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" "$url" -o "$filename"
done
jq -r '.assets[] | select(.name == "extension-package.zip") | .browser_download_url')

# Alternative: Download from release archive if available
# Or use the extension-package artifact
if [ -n "$ZIP_URL" ] && [ "$ZIP_URL" != "null" ]; then
echo "📦 Downloading extension-package.zip..."
curl -L -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" "$ZIP_URL" -o extension-package.zip
unzip -q extension-package.zip -d .
# Files should now be in extension-temp/extension-package/ or extension-temp/
if [ -d "extension-package" ]; then
mv extension-package/* . 2>/dev/null || true
rmdir extension-package 2>/dev/null || true
fi
else
echo "📁 Downloading individual files from release..."
# Download each file from release
curl -s -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" \
"https://api.github.com/repos/$REPO/releases/tags/$TAG" | \
jq -r '.assets[] | select(.name | endswith(".js") or endswith(".wasm") or endswith(".json") or endswith(".d.ts")) | .browser_download_url' | \
while read url; do
if [ -n "$url" ] && [ "$url" != "null" ]; then
filename=$(basename "$url")
echo " Downloading $filename..."
curl -L -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" "$url" -o "$filename"
fi
done
fi

# Verify files were downloaded
echo "📋 Downloaded files:"
ls -la

- name: Copy extension files
if: steps.release.outputs.skip != 'true'
run: |
# Create extension directory structure
mkdir -p sentience/extension/pkg

# Copy extension files
cp extension-temp/manifest.json sentience/extension/ 2>/dev/null || echo "manifest.json not found in release"
cp extension-temp/content.js sentience/extension/ 2>/dev/null || echo "content.js not found in release"
cp extension-temp/background.js sentience/extension/ 2>/dev/null || echo "background.js not found in release"
cp extension-temp/injected_api.js sentience/extension/ 2>/dev/null || echo "injected_api.js not found in release"
# Copy extension files (check both root and pkg subdirectory)
cp extension-temp/manifest.json sentience/extension/ 2>/dev/null || echo "⚠️ manifest.json not found in release"
cp extension-temp/content.js sentience/extension/ 2>/dev/null || echo "⚠️ content.js not found in release"
cp extension-temp/background.js sentience/extension/ 2>/dev/null || echo "⚠️ background.js not found in release"
cp extension-temp/injected_api.js sentience/extension/ 2>/dev/null || echo "⚠️ injected_api.js not found in release"

# Copy WASM files (check both root and pkg subdirectory)
if [ -f "extension-temp/pkg/sentience_core.js" ]; then
cp extension-temp/pkg/sentience_core.js sentience/extension/pkg/
elif [ -f "extension-temp/sentience_core.js" ]; then
cp extension-temp/sentience_core.js sentience/extension/pkg/
else
echo "⚠️ sentience_core.js not found"
fi

if [ -f "extension-temp/pkg/sentience_core_bg.wasm" ]; then
cp extension-temp/pkg/sentience_core_bg.wasm sentience/extension/pkg/
elif [ -f "extension-temp/sentience_core_bg.wasm" ]; then
cp extension-temp/sentience_core_bg.wasm sentience/extension/pkg/
else
echo "⚠️ sentience_core_bg.wasm not found"
fi

# Copy TypeScript definitions
if [ -d "extension-temp/pkg" ]; then
cp extension-temp/pkg/*.d.ts sentience/extension/pkg/ 2>/dev/null || echo "⚠️ Type definitions not found"
elif [ -d "extension-temp" ]; then
cp extension-temp/*.d.ts sentience/extension/pkg/ 2>/dev/null || echo "⚠️ Type definitions not found"
fi

# Copy WASM files
cp extension-temp/pkg/sentience_core.js sentience/extension/pkg/ 2>/dev/null || echo "sentience_core.js not found"
cp extension-temp/pkg/sentience_core_bg.wasm sentience/extension/pkg/ 2>/dev/null || echo "sentience_core_bg.wasm not found"
cp extension-temp/pkg/*.d.ts sentience/extension/pkg/ 2>/dev/null || echo "Type definitions not found"
# Verify copied files
echo "📋 Copied files:"
ls -la sentience/extension/
ls -la sentience/extension/pkg/ 2>/dev/null || echo "⚠️ pkg directory not created"

- name: Check for changes
if: steps.release.outputs.skip != 'true'
Expand All @@ -110,7 +156,9 @@ jobs:
if: steps.release.outputs.skip != 'true' && steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Use GITHUB_TOKEN (built-in) if repository allows PR creation, otherwise use PR_TOKEN (PAT)
# To use PAT: create secret named PR_TOKEN with a Personal Access Token that has 'repo' scope
token: ${{ secrets.PR_TOKEN || secrets.GITHUB_TOKEN }}
commit-message: "chore: sync extension files from sentience-chrome ${{ steps.release.outputs.tag }}"
title: "Sync Extension: ${{ steps.release.outputs.tag }}"
body: |
Expand All @@ -120,7 +168,10 @@ jobs:
- Extension manifest and scripts
- WASM binary and bindings

**Source:** [sentience-chrome release ${{ steps.release.outputs.tag }}](${{ secrets.SENTIENCE_CHROME_REPO }}/releases/tag/${{ steps.release.outputs.tag }})
**Source:** [sentience-chrome release ${{ steps.release.outputs.tag }}](https://github.com/${{ secrets.SENTIENCE_CHROME_REPO }}/releases/tag/${{ steps.release.outputs.tag }})
branch: sync-extension-${{ steps.release.outputs.tag }}
delete-branch: true
labels: |
automated
extension-sync

41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ with SentienceBrowser(headless=False) as browser:
- Pydantic models for type safety
- `snapshot.save(filepath)` - Save to JSON

### Content Reading & Screenshots
- `read(browser, format="text|markdown")` - Read page content as text or markdown
- Enhanced markdown conversion using `markdownify` (better than extension's lightweight conversion)
- Supports `enhance_markdown=True` to use improved conversion
- `screenshot(browser, format="png|jpeg", quality=80)` - Capture standalone screenshot
- Returns base64-encoded data URL
- Supports PNG and JPEG formats with quality control

### Day 4: Query Engine
- `query(snapshot, selector)` - Find elements matching selector
- `find(snapshot, selector)` - Find single best match
Expand Down Expand Up @@ -73,6 +81,39 @@ See `examples/` directory:
- `query_demo.py` - Query engine
- `wait_and_click.py` - Wait and actions

### Content Reading Example

```python
from sentience import SentienceBrowser, read

with SentienceBrowser() as browser:
browser.page.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")

# Read as enhanced markdown (better quality)
result = read(browser, format="markdown", enhance_markdown=True)
print(result["content"]) # High-quality markdown
```

### Screenshot Example

```python
from sentience import SentienceBrowser, screenshot
import base64

with SentienceBrowser() as browser:
browser.page.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")

# Capture PNG screenshot
data_url = screenshot(browser, format="png")

# Save to file
image_data = base64.b64decode(data_url.split(",")[1])
with open("screenshot.png", "wb") as f:
f.write(image_data)
```

## Testing

```bash
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
"jsonschema>=4.0.0",
"requests>=2.31.0", # For server-side API calls
"playwright-stealth>=1.0.6", # Bot evasion and stealth mode
"markdownify>=0.11.6", # Enhanced HTML to Markdown conversion
]

[project.urls]
Expand Down
4 changes: 4 additions & 0 deletions sentience/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from .inspector import Inspector, inspect
from .recorder import Recorder, Trace, TraceStep, record
from .generator import ScriptGenerator, generate
from .read import read
from .screenshot import screenshot

__version__ = "0.1.0"

Expand Down Expand Up @@ -39,5 +41,7 @@
"record",
"ScriptGenerator",
"generate",
"read",
"screenshot",
]

70 changes: 70 additions & 0 deletions sentience/read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Read page content - enhanced markdown conversion
"""

from typing import Optional, Literal
from .browser import SentienceBrowser


def read(
browser: SentienceBrowser,
format: Literal["text", "markdown"] = "text",
enhance_markdown: bool = True,
) -> dict:
"""
Read page content as text or markdown

Args:
browser: SentienceBrowser instance
format: Output format - "text" or "markdown"
enhance_markdown: If True and format="markdown", use markdownify for better conversion

Returns:
dict with:
- status: "success" or "error"
- url: Current page URL
- format: "text" or "markdown"
- content: Page content as string
- length: Content length in characters
- error: Error message if status is "error"
"""
if not browser.page:
raise RuntimeError("Browser not started. Call browser.start() first.")

# Get basic content from extension
result = browser.page.evaluate(
"""
(options) => {
return window.sentience.read(options);
}
""",
{"format": format},
)

# Enhance markdown if requested and format is markdown
if format == "markdown" and enhance_markdown and result.get("status") == "success":
try:
# Get full HTML from page
html_content = browser.page.evaluate("() => document.documentElement.outerHTML")

# Use markdownify for better conversion
from markdownify import markdownify as md
enhanced_markdown = md(
html_content,
heading_style="ATX", # Use # for headings
bullets="-", # Use - for lists
strip=['script', 'style', 'nav', 'footer', 'header', 'noscript'], # Strip unwanted tags
)
result["content"] = enhanced_markdown
result["length"] = len(enhanced_markdown)
except ImportError:
# Fallback to extension's lightweight conversion if markdownify not installed
# This shouldn't happen if dependencies are installed, but handle gracefully
pass
except Exception as e:
# If enhancement fails, use extension's result
# Don't overwrite result["error"] - keep extension's result
pass

return result

53 changes: 53 additions & 0 deletions sentience/screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Screenshot functionality - standalone screenshot capture
"""

from typing import Optional, Literal, Dict, Any
from .browser import SentienceBrowser


def screenshot(
browser: SentienceBrowser,
format: Literal["png", "jpeg"] = "png",
quality: Optional[int] = None,
) -> str:
"""
Capture screenshot of current page

Args:
browser: SentienceBrowser instance
format: Image format - "png" or "jpeg"
quality: JPEG quality (1-100), only used for JPEG format

Returns:
Base64-encoded screenshot data URL (e.g., "data:image/png;base64,...")

Raises:
RuntimeError: If browser not started
ValueError: If quality is invalid for JPEG
"""
if not browser.page:
raise RuntimeError("Browser not started. Call browser.start() first.")

if format == "jpeg" and quality is not None:
if not (1 <= quality <= 100):
raise ValueError("Quality must be between 1 and 100 for JPEG format")

# Use Playwright's screenshot with base64 encoding
screenshot_options: Dict[str, Any] = {
"type": format,
}

if format == "jpeg" and quality is not None:
screenshot_options["quality"] = quality

# Capture screenshot as base64
# Playwright returns bytes when encoding is not specified, so we encode manually
import base64
image_bytes = browser.page.screenshot(**screenshot_options)
base64_data = base64.b64encode(image_bytes).decode('utf-8')

# Return as data URL
mime_type = "image/png" if format == "png" else "image/jpeg"
return f"data:{mime_type};base64,{base64_data}"

64 changes: 64 additions & 0 deletions tests/test_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
Tests for read functionality
"""

from sentience import SentienceBrowser, read


def test_read_text():
"""Test reading page as text"""
with SentienceBrowser(headless=True) as browser:
browser.page.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")

result = read(browser, format="text")

assert result["status"] == "success"
assert result["format"] == "text"
assert "content" in result
assert "length" in result
assert len(result["content"]) > 0
assert result["url"] == "https://example.com"


def test_read_markdown():
"""Test reading page as markdown"""
with SentienceBrowser(headless=True) as browser:
browser.page.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")

result = read(browser, format="markdown")

assert result["status"] == "success"
assert result["format"] == "markdown"
assert "content" in result
assert "length" in result
assert len(result["content"]) > 0
assert result["url"] == "https://example.com"


def test_read_markdown_enhanced():
"""Test reading page as markdown with enhancement"""
with SentienceBrowser(headless=True) as browser:
browser.page.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")

# Test with enhancement (default)
result_enhanced = read(browser, format="markdown", enhance_markdown=True)

assert result_enhanced["status"] == "success"
assert result_enhanced["format"] == "markdown"
assert len(result_enhanced["content"]) > 0

# Test without enhancement
result_basic = read(browser, format="markdown", enhance_markdown=False)

assert result_basic["status"] == "success"
assert result_basic["format"] == "markdown"
assert len(result_basic["content"]) > 0

# Enhanced markdown should be different (and likely better formatted)
# Note: They might be similar for simple pages, but enhanced should handle more cases
assert isinstance(result_enhanced["content"], str)
assert isinstance(result_basic["content"], str)

Loading
Loading