Add controls inventory and truncate long data: URIs in dumps.
CI / automation-js (vitest + real Chromium) (pull_request) Successful in 1m13s
CI / Lint + tests (pull_request) Successful in 10m24s

Agents hitting Outline-style SPAs need visible vs hidden control lists,
and inlined base64 images were blowing up AI prompts and network copy.
This commit is contained in:
2026-08-09 19:13:54 -04:00
parent f45833ff07
commit ee520fc391
12 changed files with 297 additions and 12 deletions
+38 -5
View File
@@ -94,15 +94,35 @@ return extractMarkdown(el);
"""
def _build_inventory_script(selector: Optional[str]) -> str:
dom_js = _read_js("dom.js")
sel_json = json.dumps(selector or "")
return f"""
(() => {{
{dom_js}
let el = null;
const sel = {sel_json};
if (sel) {{ try {{ el = document.querySelector(sel); }} catch (_) {{ el = null; }} }}
if (!el) el = document.body;
return inventoryInterestingControls(el);
}})()
"""
def _build_prompt_script(
meta: dict[str, Any], markdown: str, store: dict[str, Any], max_chars: Optional[int] = None
meta: dict[str, Any],
markdown: str,
store: dict[str, Any],
max_chars: Optional[int] = None,
controls: Optional[list[Any]] = None,
) -> str:
prompt_js = _read_js("prompt.js")
max_chars_js = json.dumps(max_chars) if max_chars is not None else "undefined"
controls_js = json.dumps(controls if controls is not None else [])
return f"""
(() => {{
{prompt_js}
return buildAIPrompt({json.dumps(meta)}, {json.dumps(markdown)}, {json.dumps(store)}, {max_chars_js});
return buildAIPrompt({json.dumps(meta)}, {json.dumps(markdown)}, {json.dumps(store)}, {max_chars_js}, {controls_js});
}})()
"""
@@ -196,13 +216,19 @@ class ExtractorSession(_CaptureMixin):
"markdown": markdown or "",
}
def inventory_controls(self, selector: Optional[str] = None) -> list[Any]:
result = self.page.evaluate(_build_inventory_script(selector))
return list(result) if result else []
def build_ai_prompt(self, selector: Optional[str] = None, max_chars: Optional[int] = None) -> str:
extracted = self.extract_markdown(selector)
controls = self.inventory_controls(selector)
meta = {k: extracted[k] for k in ("url", "title", "ts", "selector")}
script = _build_prompt_script(meta, extracted["markdown"], self.get_store(), max_chars)
script = _build_prompt_script(
meta, extracted["markdown"], self.get_store(), max_chars, controls
)
return self.page.evaluate(script)
class AsyncExtractorSession(_CaptureMixin):
"""Async API. Use with `playwright.async_api` or `camoufox.AsyncCamoufox`.
@@ -236,8 +262,15 @@ class AsyncExtractorSession(_CaptureMixin):
"markdown": markdown or "",
}
async def inventory_controls(self, selector: Optional[str] = None) -> list[Any]:
result = await self.page.evaluate(_build_inventory_script(selector))
return list(result) if result else []
async def build_ai_prompt(self, selector: Optional[str] = None, max_chars: Optional[int] = None) -> str:
extracted = await self.extract_markdown(selector)
controls = await self.inventory_controls(selector)
meta = {k: extracted[k] for k in ("url", "title", "ts", "selector")}
script = _build_prompt_script(meta, extracted["markdown"], self.get_store(), max_chars)
script = _build_prompt_script(
meta, extracted["markdown"], self.get_store(), max_chars, controls
)
return await self.page.evaluate(script)
+4
View File
@@ -12,6 +12,10 @@
<main id="main-content">
<h2>Section</h2>
<p>Hello <strong>world</strong>, visit <a href="/docs">docs</a>.</p>
<p>Big inline art: <a href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxISEhUQEhIWFhUVFRUVFRUVFRUVFRUWFxUXFhUYHSggGBolGxUVITEhJSkrLi4uFx8zODMtNygtLisBCgoKDg0OGxAQGy0lHyUtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAAEAAQMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAADBAECBQYAB//EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAGfAP/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAQUCf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Bf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Bf//Z">tiny-jpeg</a>.</p>
<button type="button" aria-label="Document options" id="doc-opts-visible"></button>
<button type="button" aria-label="Document options" id="doc-opts-hidden" style="display:none"></button>
<button type="button" aria-label="Upload file" id="upload-btn">Upload file</button>
<ul>
<li>Alpha</li>
<li>Beta</li>
+55
View File
@@ -14,7 +14,62 @@ def test_shared_js_files_exist_and_match_package():
assert (CORE / "prompt.js").is_file()
# package-side copies/symlinks must resolve to the same source
assert "function extractMarkdown" in _read_js("dom.js")
assert "function truncateDataUri" in _read_js("dom.js")
assert "function inventoryInterestingControls" in _read_js("dom.js")
assert "function buildAIPrompt" in _read_js("prompt.js")
assert "function truncateDataUri" in _read_js("prompt.js")
def test_build_ai_prompt_truncates_data_uris_in_network(page):
page.goto("about:blank")
meta = {
"url": "https://example.test/page",
"title": "T",
"ts": 1_700_000_000_000,
"selector": "body",
}
long_data = "data:image/jpeg;base64," + ("A" * 500)
store = {
"console": [],
"errors": [],
"network": [{
"type": "fetch", "method": "GET", "url": long_data,
"status": 200, "ts": 3, "duration": 12, "error": None,
}],
}
out = page.evaluate(_build_prompt_script(meta, "# Hello", store))
assert "bytes truncated" in out
assert ("A" * 100) not in out
def test_build_ai_prompt_includes_interesting_controls(page):
page.goto("about:blank")
meta = {"url": "https://example.test/page", "title": "T", "ts": 1, "selector": "body"}
controls = [
{
"tag": "button",
"role": "",
"ariaLabel": "Document options",
"text": "",
"visible": True,
"selector": "#vis",
},
{
"tag": "button",
"role": "",
"ariaLabel": "Document options",
"text": "",
"visible": False,
"selector": "#hid",
},
]
out = page.evaluate(
_build_prompt_script(meta, "# Hello", {"console": [], "errors": [], "network": []}, None, controls)
)
assert "## Interesting Controls" in out
assert "[visible]" in out
assert "### Hidden" in out
assert "Document options" in out
def test_build_ai_prompt_script_formats_errors_and_failures(page):
+15
View File
@@ -31,6 +31,8 @@ def test_extract_markdown_body_and_selector(page, fixture_url):
assert "**world**" in full["markdown"]
assert "[docs](/docs)" in full["markdown"]
assert "- Alpha" in full["markdown"]
assert "bytes truncated" in full["markdown"]
assert "4AAQSkZJRgABAQAAAQABAAD" not in full["markdown"]
scoped = session.extract_markdown("#main-content")
assert scoped["selector"] == "#main-content"
@@ -64,10 +66,23 @@ def test_build_ai_prompt_has_sections(page, fixture_url):
assert prompt.startswith("# Page Context")
assert "## Page Content" in prompt
assert "Hello **world**" in prompt
assert "## Interesting Controls" in prompt
assert "[visible]" in prompt
assert "Document options" in prompt
assert "JavaScript Errors" in prompt or "Console Errors" in prompt
assert "_Extracted by Context Extractor_" in prompt
def test_inventory_controls_visible_and_hidden(page, fixture_url):
session = ExtractorSession(page)
page.goto(fixture_url, wait_until="domcontentloaded")
controls = session.inventory_controls("#main-content")
docs = [c for c in controls if c.get("ariaLabel") == "Document options"]
assert len(docs) == 2
assert any(c.get("visible") for c in docs)
assert any(not c.get("visible") for c in docs)
def test_clear_store(page, fixture_url):
session = ExtractorSession(page)
page.goto(fixture_url, wait_until="domcontentloaded")