Guide 07 — Attachment Text Extraction¶
symfonic.agent.attachments ships three public helpers for converting Attachment
objects (or raw provider content-block dicts) into plain text strings.
When to extract vs. when to send natively¶
| Scenario | Recommended approach |
|---|---|
| Targeting a text-only model (legacy Ollama, custom backend) | Extract — native blocks will be ignored or cause an error |
| Anthropic / OpenAI vision model, visual content matters (charts, diagrams, screenshots) | Send natively via attachments=[...] |
| Anthropic / OpenAI vision model, only the text content matters (contracts, invoices) | Extract — reduces per-turn token cost, text survives in HMS memory |
| Multi-turn conversations with attachments | Extract — image tokens repeat every turn; extracted text can be cached |
Installation¶
Install only the extras you need:
# PDF support (pdfplumber primary, pypdf fallback)
pip install "symfonic-core[attachments-pdf]"
# DOCX support
pip install "symfonic-core[attachments-docx]"
# Excel / XLSX support
pip install "symfonic-core[attachments-xlsx]"
# Image OCR (requires tesseract binary — see below)
pip install "symfonic-core[attachments-ocr]"
# All four at once
pip install "symfonic-core[attachments]"
For OCR, you also need the tesseract binary on PATH:
# macOS
brew install tesseract
# Ubuntu / Debian
apt-get install tesseract-ocr
# Windows
# Download installer from https://tesseract-ocr.github.io/tessdoc/Installation.html
API reference¶
extract_text(att, *, max_pages=None, page_markers=True) -> str¶
Extract plain text from a single attachment.
att—Attachmentinstance or a raw Anthropic / OpenAI content-block dict.max_pages— limit pages read from PDFs/DOCX.None= all pages. Must be a positive integer;0and negative values raiseValueError(Python slicing would otherwise silently drop the last page withmax_pages=-1).page_markers— insert--- page N ---separators between pages (defaultTrue). No marker is emitted before page 1.
Raises UnsupportedAttachmentError, ExtractionDependencyMissingError, or
ExtractionFailedError (see error taxonomy below).
extract_texts(atts, *, max_pages=None, page_markers=True, on_error="warn") -> list[str]¶
Batch extraction over a sequence of attachments.
on_errorcontrols per-item failure handling:"raise"— stop on first failure (re-raises the original exception)."skip"— silently omit failing items. No log output."warn"(default) — emit alogging.WARNINGvia thesymfonic.agent.attachments.extractorslogger and omit the failing item. Intercept in tests withcaplog.at_level(logging.WARNING)or a customlogging.Handler; not withwarnings.catch_warnings.
The returned list length may be less than len(atts) when on_error is
"skip" or "warn" and any item fails. Positional correspondence between
the input and output is NOT preserved. If you need a per-item failure
signal, use "warn" (and read caplog) or "raise".
is_extractable(att) -> bool¶
Returns True if an extractor is registered for this attachment's
(kind, media_type) combination AND its optional Python dependency is
importable (for image OCR, also requires the tesseract binary on PATH).
Cheap predicate: does not decode or validate the base64 payload. A
malformed data field still returns True when the combo is extractable —
the failure only surfaces when you actually call extract_text. Never
raises. Safe to use for runtime routing decisions.
Error taxonomy¶
AttachmentExtractionError # base
├── UnsupportedAttachmentError # no extractor for this kind/media_type,
│ # or source_type="url" (not fetched)
├── ExtractionDependencyMissingError # extractor exists but pip extra missing
│ # message names the extra to install
└── ExtractionFailedError # dep present, file corrupt / protected
# __cause__ = underlying library exception
End-to-end examples¶
Cost-sensitive path: extract before the agent call¶
from symfonic.agent.attachments import extract_text, is_extractable
from symfonic.agent import Attachment, SymfonicAgent
pdf = Attachment(
kind="document",
source_type="base64",
data=pdf_b64,
media_type="application/pdf",
filename="contract.pdf",
)
# Route based on what's available at runtime
if is_extractable(pdf):
text = extract_text(pdf, max_pages=20)
await agent.run(f"{user_question}\n\nContract:\n{text}", scope=scope)
else:
# Fall back to native block (provider must support documents)
await agent.run(user_question, attachments=[pdf], scope=scope)
Hybrid mixed-media path¶
from symfonic.agent.attachments import extract_texts, is_extractable, UnsupportedAttachmentError
from symfonic.agent import Attachment, SymfonicAgent
attachments: list[Attachment] = [pdf_att, chart_image_att, logo_image_att]
# Extract text from all extractable attachments; keep visual ones native
extracted_texts: list[str] = []
native_blocks: list[Attachment] = []
for att in attachments:
if is_extractable(att):
extracted_texts.append(extract_text(att))
else:
native_blocks.append(att)
# Fold extracted text into the query string
combined_query = user_question
if extracted_texts:
combined_query += "\n\n" + "\n\n---\n\n".join(extracted_texts)
await agent.run(
combined_query,
attachments=native_blocks or None,
scope=scope,
)
Supported formats (v1)¶
kind |
media_type |
Library | Extra |
|---|---|---|---|
document |
application/pdf |
pdfplumber (pypdf fallback) | attachments-pdf |
document |
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
python-docx | attachments-docx |
document |
text/plain, text/markdown, text/csv |
stdlib | always |
document |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
openpyxl | attachments-xlsx |
image |
image/png, image/jpeg, image/webp, image/gif |
pytesseract + Pillow | attachments-ocr |
Non-goals (v1)¶
- URL fetching —
source_type="url"always raisesUnsupportedAttachmentError. Download the file yourself before callingextract_text. - Metadata extraction (author, EXIF, creation date) — future
extract_metadata()helper. - PDF password unlocking — raises
ExtractionFailedErrorwith a clear message. - Streaming extraction — returns a
str, not an iterator. - Auto-chunking for embedder token limits — caller responsibility.