feat(attachments): rasterize PDF to page images for vision models without native PDF

A vision-capable model that can't ingest PDF natively now gets the PDF
rendered to one image per page instead of extracted text; falls back to
text extraction when rendering yields nothing.

- core/pdf.py: rasterize_pdf via pypdfium2 render + Pillow PNG (page-capped
  at 10, never raises)
- session._wire_content_part: pdf + !supports_pdf + supports_vision ->
  rasterized image parts; else text extraction
- trajectory.resolve_attachment_parts: a placeholder can now expand to a
  list of parts (1->N); the resolve_attachments callback return type widened
  to dict[str, Any] across the provider protocol + 4 providers
- pyproject: pillow dependency
- tests: rasterize_pdf, vision-rasterize gate path, 1->N materialization
This commit is contained in:
Patrick Buckley
2026-06-15 16:13:58 -07:00
parent addb8d0be8
commit 471d94b27f
11 changed files with 250 additions and 21 deletions
+11 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from turnstone.core.pdf import extract_pdf_text
from turnstone.core.pdf import extract_pdf_text, rasterize_pdf
def _minimal_pdf(text: str = "Hello PDF") -> bytes:
@@ -38,3 +38,13 @@ class TestExtractPdfText:
def test_empty_returns_empty(self) -> None:
assert extract_pdf_text(b"") == ""
class TestRasterizePdf:
def test_renders_pages_to_png(self) -> None:
pages = rasterize_pdf(_minimal_pdf("Hello PDF"))
assert len(pages) == 1
assert pages[0][:8] == b"\x89PNG\r\n\x1a\n"
def test_garbage_returns_empty_no_raise(self) -> None:
assert rasterize_pdf(b"not a pdf at all") == []