Skip to content

Commit 34fe408

Browse files
committed
resolve merge conflicts
2 parents 0812d94 + fba9842 commit 34fe408

File tree

8 files changed

+286
-65
lines changed

8 files changed

+286
-65
lines changed

.github/workflows/sync-extension.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
uses: actions/checkout@v4
2626
with:
2727
token: ${{ secrets.GITHUB_TOKEN }}
28+
fetch-depth: 0 # Fetch all history for proper branching
2829

2930
- name: Set up Python
3031
uses: actions/setup-python@v5
@@ -62,7 +63,7 @@ jobs:
6263
mkdir -p extension-temp
6364
cd extension-temp
6465
65-
# Download individual files from release (reliable method)
66+
# Download individual files from release (reliable method - no zip)
6667
echo "📁 Downloading individual files from release..."
6768
curl -L -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" \
6869
"https://api.github.com/repos/$REPO/releases/tags/$TAG" | \
@@ -148,6 +149,11 @@ jobs:
148149
cp extension-temp/extension-package/pkg/*.d.ts sentience/extension/pkg/ 2>/dev/null || echo "⚠️ Type definitions not found"
149150
fi
150151
152+
# Verify copied files
153+
echo "📋 Copied files:"
154+
ls -la sentience/extension/
155+
ls -la sentience/extension/pkg/ 2>/dev/null || echo "⚠️ pkg directory not created"
156+
151157
- name: Check for changes
152158
if: steps.release.outputs.skip != 'true'
153159
id: changes
@@ -167,7 +173,9 @@ jobs:
167173
if: steps.release.outputs.skip != 'true' && steps.changes.outputs.changed == 'true'
168174
uses: peter-evans/create-pull-request@v5
169175
with:
170-
token: ${{ secrets.GITHUB_TOKEN }}
176+
# Use PR_TOKEN if available (for repos with org restrictions), otherwise use GITHUB_TOKEN
177+
# To use PAT: create secret named PR_TOKEN with a Personal Access Token that has 'repo' scope
178+
token: ${{ secrets.PR_TOKEN }}
171179
commit-message: "chore: sync extension files from sentience-chrome ${{ steps.release.outputs.tag }}"
172180
title: "Sync Extension: ${{ steps.release.outputs.tag }}"
173181
body: |
@@ -177,7 +185,10 @@ jobs:
177185
- Extension manifest and scripts
178186
- WASM binary and bindings
179187
180-
**Source:** [sentience-chrome release ${{ steps.release.outputs.tag }}](${{ secrets.SENTIENCE_CHROME_REPO }}/releases/tag/${{ steps.release.outputs.tag }})
188+
**Source:** [sentience-chrome release ${{ steps.release.outputs.tag }}](https://github.com/${{ secrets.SENTIENCE_CHROME_REPO }}/releases/tag/${{ steps.release.outputs.tag }})
181189
branch: sync-extension-${{ steps.release.outputs.tag }}
182190
delete-branch: true
191+
labels: |
192+
automated
193+
extension-sync
183194

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ with SentienceBrowser(headless=False) as browser:
4646
- Pydantic models for type safety
4747
- `snapshot.save(filepath)` - Save to JSON
4848

49+
### Content Reading & Screenshots
50+
- `read(browser, format="text|markdown")` - Read page content as text or markdown
51+
- Enhanced markdown conversion using `markdownify` (better than extension's lightweight conversion)
52+
- Supports `enhance_markdown=True` to use improved conversion
53+
- `screenshot(browser, format="png|jpeg", quality=80)` - Capture standalone screenshot
54+
- Returns base64-encoded data URL
55+
- Supports PNG and JPEG formats with quality control
56+
4957
### Day 4: Query Engine
5058
- `query(snapshot, selector)` - Find elements matching selector
5159
- `find(snapshot, selector)` - Find single best match
@@ -96,6 +104,39 @@ See `examples/` directory:
96104
- `wait_and_click.py` - Wait and actions
97105
- `read_markdown.py` - Reading page content and converting to markdown
98106

107+
### Content Reading Example
108+
109+
```python
110+
from sentience import SentienceBrowser, read
111+
112+
with SentienceBrowser() as browser:
113+
browser.page.goto("https://example.com")
114+
browser.page.wait_for_load_state("networkidle")
115+
116+
# Read as enhanced markdown (better quality)
117+
result = read(browser, format="markdown", enhance_markdown=True)
118+
print(result["content"]) # High-quality markdown
119+
```
120+
121+
### Screenshot Example
122+
123+
```python
124+
from sentience import SentienceBrowser, screenshot
125+
import base64
126+
127+
with SentienceBrowser() as browser:
128+
browser.page.goto("https://example.com")
129+
browser.page.wait_for_load_state("networkidle")
130+
131+
# Capture PNG screenshot
132+
data_url = screenshot(browser, format="png")
133+
134+
# Save to file
135+
image_data = base64.b64decode(data_url.split(",")[1])
136+
with open("screenshot.png", "wb") as f:
137+
f.write(image_data)
138+
```
139+
99140
## Testing
100141

101142
```bash

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies = [
2626
"jsonschema>=4.0.0",
2727
"requests>=2.31.0", # For server-side API calls
2828
"playwright-stealth>=1.0.6", # Bot evasion and stealth mode
29+
"markdownify>=0.11.6", # Enhanced HTML to Markdown conversion
2930
]
3031

3132
[project.urls]

sentience/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .recorder import Recorder, Trace, TraceStep, record
1414
from .generator import ScriptGenerator, generate
1515
from .read import read
16+
from .screenshot import screenshot
1617

1718
__version__ = "0.1.0"
1819

@@ -41,5 +42,6 @@
4142
"ScriptGenerator",
4243
"generate",
4344
"read",
45+
"screenshot",
4446
]
4547

sentience/read.py

Lines changed: 41 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88

99
def read(
1010
browser: SentienceBrowser,
11-
format: Literal["raw", "text", "markdown"] = "raw", # noqa: A002
11+
output_format: Literal["raw", "text", "markdown"] = "raw",
12+
enhance_markdown: bool = True,
1213
) -> dict:
1314
"""
1415
Read page content as raw HTML, text, or markdown
1516
1617
Args:
1718
browser: SentienceBrowser instance
18-
format: Output format - "raw" (default, returns HTML for Turndown/markdownify),
19-
"text" (plain text), or "markdown" (high-quality markdown via markdownify)
19+
output_format: Output format - "raw" (default, returns HTML for external processing),
20+
"text" (plain text), or "markdown" (lightweight or enhanced markdown).
21+
enhance_markdown: If True and output_format is "markdown", uses markdownify for better conversion.
22+
If False, uses the extension's lightweight markdown converter.
2023
2124
Returns:
2225
dict with:
@@ -33,20 +36,19 @@ def read(
3336
html_content = result["content"]
3437
3538
# Get high-quality markdown (uses markdownify internally)
36-
result = read(browser, format="markdown")
39+
result = read(browser, output_format="markdown")
3740
markdown = result["content"]
3841
3942
# Get plain text
40-
result = read(browser, format="text")
43+
result = read(browser, output_format="text")
4144
text = result["content"]
4245
"""
4346
if not browser.page:
4447
raise RuntimeError("Browser not started. Call browser.start() first.")
4548

46-
# For markdown format, get raw HTML first, then convert with markdownify
47-
if format == "markdown":
48-
# Get raw HTML from extension
49-
raw_result = browser.page.evaluate(
49+
if output_format == "markdown" and enhance_markdown:
50+
# Get raw HTML from the extension first
51+
raw_html_result = browser.page.evaluate(
5052
"""
5153
(options) => {
5254
return window.sentience.read(options);
@@ -55,57 +57,34 @@ def read(
5557
{"format": "raw"},
5658
)
5759

58-
if raw_result.get("status") != "success":
59-
return raw_result
60-
61-
# Convert to markdown using markdownify
62-
try:
63-
from markdownify import markdownify as md
64-
html_content = raw_result["content"]
65-
markdown_content = md(
66-
html_content,
67-
heading_style="ATX", # Use # for headings
68-
bullets="-", # Use - for lists
69-
strip=['script', 'style', 'nav', 'footer', 'header', 'noscript'], # Strip unwanted tags
70-
)
71-
72-
# Return result with markdown content
73-
return {
74-
"status": "success",
75-
"url": raw_result["url"],
76-
"format": "markdown",
77-
"content": markdown_content,
78-
"length": len(markdown_content),
79-
}
80-
except ImportError:
81-
# Fallback to extension's lightweight markdown if markdownify not installed
82-
result = browser.page.evaluate(
83-
"""
84-
(options) => {
85-
return window.sentience.read(options);
60+
if raw_html_result.get("status") == "success":
61+
html_content = raw_html_result["content"]
62+
try:
63+
# Use markdownify for enhanced markdown conversion
64+
from markdownify import markdownify, MarkdownifyError
65+
markdown_content = markdownify(html_content, heading_style="ATX", wrap=True)
66+
return {
67+
"status": "success",
68+
"url": raw_html_result["url"],
69+
"format": "markdown",
70+
"content": markdown_content,
71+
"length": len(markdown_content),
8672
}
87-
""",
88-
{"format": "markdown"},
89-
)
90-
return result
91-
except (ValueError, TypeError, AttributeError) as e:
92-
# If conversion fails, return error
93-
return {
94-
"status": "error",
95-
"url": raw_result.get("url", ""),
96-
"format": "markdown",
97-
"content": "",
98-
"length": 0,
99-
"error": f"Markdown conversion failed: {e}",
100-
}
101-
else:
102-
# For "raw" or "text", call extension directly
103-
result = browser.page.evaluate(
104-
"""
105-
(options) => {
106-
return window.sentience.read(options);
107-
}
108-
""",
109-
{"format": format},
110-
)
111-
return result
73+
except ImportError:
74+
print("Warning: 'markdownify' not installed. Install with 'pip install markdownify' for enhanced markdown. Falling back to extension's markdown.")
75+
except MarkdownifyError as e:
76+
print(f"Warning: markdownify failed ({e}), falling back to extension's markdown.")
77+
except Exception as e:
78+
print(f"Warning: An unexpected error occurred with markdownify ({e}), falling back to extension's markdown.")
79+
80+
# If not enhanced markdown, or fallback, call extension with requested format
81+
result = browser.page.evaluate(
82+
"""
83+
(options) => {
84+
return window.sentience.read(options);
85+
}
86+
""",
87+
{"format": output_format},
88+
)
89+
90+
return result

sentience/screenshot.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Screenshot functionality - standalone screenshot capture
3+
"""
4+
5+
from typing import Optional, Literal, Dict, Any
6+
from .browser import SentienceBrowser
7+
8+
9+
def screenshot(
10+
browser: SentienceBrowser,
11+
format: Literal["png", "jpeg"] = "png",
12+
quality: Optional[int] = None,
13+
) -> str:
14+
"""
15+
Capture screenshot of current page
16+
17+
Args:
18+
browser: SentienceBrowser instance
19+
format: Image format - "png" or "jpeg"
20+
quality: JPEG quality (1-100), only used for JPEG format
21+
22+
Returns:
23+
Base64-encoded screenshot data URL (e.g., "data:image/png;base64,...")
24+
25+
Raises:
26+
RuntimeError: If browser not started
27+
ValueError: If quality is invalid for JPEG
28+
"""
29+
if not browser.page:
30+
raise RuntimeError("Browser not started. Call browser.start() first.")
31+
32+
if format == "jpeg" and quality is not None:
33+
if not (1 <= quality <= 100):
34+
raise ValueError("Quality must be between 1 and 100 for JPEG format")
35+
36+
# Use Playwright's screenshot with base64 encoding
37+
screenshot_options: Dict[str, Any] = {
38+
"type": format,
39+
}
40+
41+
if format == "jpeg" and quality is not None:
42+
screenshot_options["quality"] = quality
43+
44+
# Capture screenshot as base64
45+
# Playwright returns bytes when encoding is not specified, so we encode manually
46+
import base64
47+
image_bytes = browser.page.screenshot(**screenshot_options)
48+
base64_data = base64.b64encode(image_bytes).decode('utf-8')
49+
50+
# Return as data URL
51+
mime_type = "image/png" if format == "png" else "image/jpeg"
52+
return f"data:{mime_type};base64,{base64_data}"
53+

tests/test_read.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
Tests for read functionality
3+
"""
4+
5+
from sentience import SentienceBrowser, read
6+
7+
8+
def test_read_text():
9+
"""Test reading page as text"""
10+
with SentienceBrowser(headless=True) as browser:
11+
browser.page.goto("https://example.com")
12+
browser.page.wait_for_load_state("networkidle")
13+
14+
result = read(browser, format="text")
15+
16+
assert result["status"] == "success"
17+
assert result["format"] == "text"
18+
assert "content" in result
19+
assert "length" in result
20+
assert len(result["content"]) > 0
21+
assert result["url"] == "https://example.com"
22+
23+
24+
def test_read_markdown():
25+
"""Test reading page as markdown"""
26+
with SentienceBrowser(headless=True) as browser:
27+
browser.page.goto("https://example.com")
28+
browser.page.wait_for_load_state("networkidle")
29+
30+
result = read(browser, format="markdown")
31+
32+
assert result["status"] == "success"
33+
assert result["format"] == "markdown"
34+
assert "content" in result
35+
assert "length" in result
36+
assert len(result["content"]) > 0
37+
assert result["url"] == "https://example.com"
38+
39+
40+
def test_read_markdown_enhanced():
41+
"""Test reading page as markdown with enhancement"""
42+
with SentienceBrowser(headless=True) as browser:
43+
browser.page.goto("https://example.com")
44+
browser.page.wait_for_load_state("networkidle")
45+
46+
# Test with enhancement (default)
47+
result_enhanced = read(browser, format="markdown", enhance_markdown=True)
48+
49+
assert result_enhanced["status"] == "success"
50+
assert result_enhanced["format"] == "markdown"
51+
assert len(result_enhanced["content"]) > 0
52+
53+
# Test without enhancement
54+
result_basic = read(browser, format="markdown", enhance_markdown=False)
55+
56+
assert result_basic["status"] == "success"
57+
assert result_basic["format"] == "markdown"
58+
assert len(result_basic["content"]) > 0
59+
60+
# Enhanced markdown should be different (and likely better formatted)
61+
# Note: They might be similar for simple pages, but enhanced should handle more cases
62+
assert isinstance(result_enhanced["content"], str)
63+
assert isinstance(result_basic["content"], str)
64+

0 commit comments

Comments
 (0)