Skip to main content

agent-pdf-workspace

agent-pdf-workspace converts local PDFs into persistent, agent-oriented workspaces. One PDF becomes one workspace; several PDFs become one collection that is searched as a unit. Text, layout, tables, OCR, metadata, exact/regex/full-text search, rendering, and integrity checks run locally. The package contains no LLM, vision model, or embedding dependency and performs no network requests while processing documents.

An external agent can inspect queued image crops with its own vision capability and return short, schema-validated descriptions. PDF content is always treated as untrusted data.

Install

Python 3.11–3.14 is supported.

uv tool install .
# or, for development from this checkout
uv sync
uv run pdfws --version

LiteParse supplies native PDF parsing and page rendering with its OCR disabled. RapidOCR and ONNX Runtime perform OCR through Python packages installed from PyPI. RapidOCR's default PP-OCRv6 models are included in its wheel, cover English, German, and other Latin-script languages, and require no runtime model download. No Tesseract executable, Tesseract language pack, GitHub access, or separate system OCR installation is used.

In auto mode the package first inspects native extraction and sends only image-dominated or text-empty complex pages through local OCR; both layers retain their provenance when merged.

Quick start

pdfws ingest report.pdf --target ./report-workspace --ocr auto --language deu+eng --json
pdfws inspect ./report-workspace --json
pdfws search ./report-workspace "operating income" --json
pdfws read ./report-workspace --page 12 --json
pdfws visuals next ./report-workspace --json
pdfws verify ./report-workspace --json
pdfws export okf ./report-workspace --target ./report-okf --json

For several PDFs, pass more than one source or add them to a collection over time:

pdfws ingest q1.pdf q2.pdf q3.pdf --target ./library --ocr auto --json
pdfws collection add ./library q4.pdf --json
pdfws collection list ./library --json
pdfws search ./library "operating income" --json

Temporary parser sessions now default to ./report-workspace/cache/tmp/pdfws-worker-*/, so parser workers do not use %TEMP%, %TMP%, or /tmp. Select another owned cache root with an absolute path when ingesting, rendering, or submitting a visual audit:

pdfws ingest report.pdf --target ./report-workspace --cache-dir /absolute/cache --json
pdfws render ./report-workspace --page 12 --bbox 72,90,520,420 --cache-dir /absolute/cache --json

The Python API accepts the same override as Workspace.ingest(..., cache_directory=...) and Workspace.open(..., cache_directory=...). An already-open instance can switch subsequent worker sessions with workspace.set_cache_directory(...). A new external directory receives the .agent-pdf-workspace-cache-v1 ownership marker. Reusing an existing directory without that marker, or passing a relative path, is refused.

The bundled OpenCode integration additionally exports set_cache_directory(workspace, cache_directory). It validates the path and remembers it for later render and audit calls in the current OpenCode session.

For encrypted input, provide the password through standard input so it is not exposed in process arguments or persisted:

printf '%s\n' "$PDF_PASSWORD" | pdfws ingest protected.pdf --target ./workspace --password-stdin

Visual descriptions

pdfws visuals next returns either a describe_region task or an annotated audit_page task. For a region, have the calling agent inspect asset_path and submit JSON like:

{
  "visual_id": "visual-p0001-…",
  "crop_sha256": "…",
  "kind": "chart",
  "decorative": false,
  "short_description": "Revenue rises through Q3 and dips slightly in Q4.",
  "text_in_visual": "| Quarter | Revenue (EUR m) |\n|---|---:|\n| Q1 | 12 |\n| Q2 | 15 |",
  "agent_id": "document-agent",
  "model_id": "vision-model"
}
pdfws visuals submit ./workspace --task-id TASK --input description.json --json

For charts and plots, text_in_visual should be a Markdown table with one row per visible data point and explicit series/category, value, and unit columns. Preserve signs, decimals, years, and label/value associations. Mark estimates as estimates instead of inventing precision. To correct an already submitted description, recheck the crop and existing JSON and pass --replace; a formatting-only revision must not recompute or change verified values.

Page audits can add bboxes missed by deterministic raster/vector detection; every added region then becomes its own description task. Accepted descriptions are persisted as both readable Markdown and a versioned JSON sidecar under visuals/descriptions/.

Python API

from agent_pdf_workspace import Workspace

workspace = Workspace.ingest(
    "report.pdf",
    "report-workspace",
    ocr="auto",
    ocr_languages=("deu", "eng"),
)
hits = workspace.search("cash flow", mode="fts")
page = workspace.read_page(hits[0].page)
crop_path = workspace.render_region(hits[0].page, (72, 90, 520, 420))
okf_bundle = workspace.export_okf("report-okf")

Search hits include a stable hit ID, page, page-coordinate bounding box, kind, and snippet. Custom ingestion limits supplied with IngestConfig are persisted without secrets and remain effective when the workspace is reopened.

Several PDFs in one workspace

A collection holds any number of PDFs under one root. Every member stays a complete, separately verifiable schema-1 workspace below members/<document_id>/; the collection adds a shared cross-document search index, one manifest, and one agent entry point.

from agent_pdf_workspace import Collection

collection = Collection.ingest(["q1.pdf", "q2.pdf"], "./library", ocr="auto")
collection.add_pdf("q3.pdf", "q4.pdf")
collection.add("q5.pdf", "./old-report-workspace")   # ingests PDFs, adopts workspaces
document_id = collection.members()[0].document_id

hits = collection.search("cash flow")                  # every PDF; hits carry document_id
page = collection.read_page(12, document_id=document_id)
collection.export_okf("./library-okf")
collection.verify()

document_id is <filename-slug>-<source_sha256[:12]>, so adding the same PDF file twice is idempotent and the member directory name always equals the document ID. The same bytes under a different file name form a separate member sharing the hash suffix, and adding the same file with different extraction settings is refused.

An existing single-PDF workspace joins a collection without re-ingestion. collection add detects a workspace directory and adopts it; collection attach is the explicit form. Both copy by default and only relocate with --move:

pdfws collection add ./library new.pdf ./report-workspace --json
pdfws collection attach ./library ./report-workspace --json
pdfws collection attach ./library ./report-workspace --move --json

The workspace is verified before and after adoption; a workspace that fails integrity verification is refused and nothing is written.

inspect, search, read, render, verify, visuals *, and export okf detect a collection path automatically and accept --document ID. read and render require it once a collection holds more than one PDF; search and visuals span every document without it.

pdfws read ./library --document q1-1a2b3c4d5e6f --page 12 --json
pdfws visuals next ./library --json     # returns a task tagged with its document_id

Visual task assets stay member-relative: resolve a task crop as COLLECTION/members/<document_id>/<asset_path>.

pdfws collection remove ./library --document ID detaches a member into removed/<ID>, where it remains a valid standalone workspace. Data is deleted only with the explicit --delete flag.

The main public contracts are Pydantic models exported from agent_pdf_workspace. Versioned JSON Schemas and the bundled agent skill are included in the wheel; export the latter with pdfws skill export TARGET.

OpenCode installation

OpenCode calls its configurable home OPENCODE_CONFIG_DIR. The installer reads that variable first and otherwise asks the installed CLI via opencode debug paths; it does not assume that ~/.config/opencode is the active directory.

pdfws opencode path --json
pdfws opencode install --json

The install command writes the skill to <config>/skills/agent-pdf-workspace and native tools to <config>/tools/agent_pdf_workspace.ts. Use --config-dir DIR for an explicit directory or --force to replace only those two package-owned artifacts. The OpenCode tools invoke pdfws without a shell and expose bounded inspect, search, page-read, region-render, and visual-task operations. Each of those accepts an optional document argument so it can address one PDF inside a collection, and collection_add(collection, sources) plus collection_list(collection) manage membership. It also exposes export_okf(workspace, target) for portable OKF 0.2 output.

OKF 0.2 export

Version 0.2.0 exports any integrity-valid schema-1 workspace to the official Open Knowledge Format 0.2:

pdfws export okf ./report-workspace --target ./report-okf --json

Version 0.3.0 exports a whole collection the same way. The bundle root links to documents/index.md and holds one documents/<document-id>/ subtree — document.md, pages/, tables/, visuals/, and references/ — per PDF:

pdfws export okf ./library --target ./library-okf --json

The target must be a new directory outside the workspace. Existing paths and symbolic links are refused. The exporter verifies manifest-sha256.txt first, copies only the known original PDF and referenced visual crops, and never modifies the source workspace.

OKF_BUNDLE/
├── index.md                         # declares okf_version: "0.2"
├── document.md                      # type: PDF Document
├── pages/
│   ├── index.md                     # reserved OKF index; no concept frontmatter
│   └── page-0001.md                 # type: PDF Page
├── tables/
│   ├── index.md
│   └── <table-id>.md                # type: PDF Table
├── visuals/
│   ├── index.md
│   └── <visual-id>.md               # type: PDF Visual
└── references/
    ├── original.pdf                 # byte-identical source
    └── visuals/<sha256>.png          # referenced crops

Every non-reserved Markdown file contains parseable YAML frontmatter with a non-empty type. Page, table, and visual concepts retain PDF-specific IDs, page numbers, bounding boxes, extraction provenance, hashes, and the existing untrusted-content warnings as producer extensions. Submitted visual descriptions are included as stable concepts; visuals still awaiting an agent description remain explicit draft concepts. The OKF bundle is a separate export and does not change persistent workspace schema 1.0.

Workspace format

The source PDF is copied unchanged to source/original.pdf. Human-readable Markdown provides navigation while JSON sidecars preserve coordinates and provenance. Tables are also emitted as CSV. SQLite FTS and rendered query crops live under cache/ and are regenerable.

Processing flow

agent-pdf-workspace creates a persistent, page-oriented workspace rather than separate per-page PDF files:

flowchart LR
    A["Source PDF"] --> B["Copy unchanged<br/>source/original.pdf"]
    B --> C["pypdf inventory<br/>pages, metadata, attachments, active content"]
    B --> D["LiteParse<br/>native extraction and page rendering<br/>OCR disabled"]
    D --> K["RapidOCR plus ONNX Runtime<br/>selected local OCR<br/>models bundled in PyPI wheel"]
    C --> E["pdfplumber geometry<br/>tables, regions, page previews"]
    D --> E
    K --> E
    E --> F["Per-page Markdown and JSON"]
    E --> G["Table MD, JSON, and CSV"]
    E --> H["Visual PNG assets and tasks"]
    F --> I["SQLite full-text index"]
    G --> I
    H --> I
    I --> J["Integrity manifest and reusable workspace"]

Workspace and cache versions

The persistent workspace schema is 1.0 in every package version to date. Package releases keep the same persistent workspace schema while extending runtime and export behavior:

Package versions Persistent workspace Temporary parser-worker location
0.1.00.1.1 Schema 1.0; regenerable search and render data below WORKSPACE_ROOT/cache/ OS temporary directory selected by Python (%TEMP%, %TMP%, or /tmp)
0.1.2 Schema 1.0; existing workspaces open without conversion; OCR moves from LiteParse/Tesseract to Python-packaged RapidOCR/ONNX Runtime WORKSPACE_ROOT/cache/tmp/ by default, or an absolute owned cache selected through Python, CLI, or OpenCode
0.2.0 Schema 1.0; adds a separate, conformant OKF 0.2 export through Python, CLI, and OpenCode Same workspace-local or explicitly selected owned cache behavior as 0.1.2
0.2.1 Schema 1.0; closes SQLite search handles before atomic index replacement for Windows compatibility Same workspace-local or explicitly selected owned cache behavior as 0.1.2
0.3.0 Schema 1.0 unchanged per PDF; adds an additive collection layer (collection.json schema 1.0) that holds many such workspaces below members/ Same workspace-local or explicitly selected owned cache behavior as 0.1.2

Old pdfws-worker-* directories contain stage-specific result.json scratch without a reliable source-PDF identity. They are not reusable document caches and are never imported as authoritative workspace data. A complete workspace from 0.1.0 or 0.1.1, however, remains directly usable because its schema did not change.

Persistent directory structure

One target directory represents one source PDF. Files below cache/ are regenerable; the other listed records are canonical unless noted otherwise:

WORKSPACE_ROOT/
├── index.md
├── document.md
├── pages/
│   ├── index.md
│   └── page-0001.md
├── layout/pages/
│   └── page-0001.json
├── tables/
│   └── <table-id>.{md,json,csv}
├── visuals/
│   ├── index.md
│   ├── assets/<sha256>.png
│   └── descriptions/<visual-id>.{md,json}
├── tasks/
│   ├── visuals.jsonl
│   └── audits/<task-id>.json
├── source/original.pdf
├── metadata/
│   ├── workspace.json
│   └── diagnostics.jsonl
├── cache/                                      # regenerable and integrity-excluded
│   ├── .agent-pdf-workspace-cache-v1
│   ├── search.sqlite
│   ├── renders/page-<n>-<key>.png
│   ├── audit-staging/<sha256>.png
│   ├── rejected-agent-input/
│   └── tmp/
│       └── pdfws-worker-<random>/result.json   # temporary; removed after each worker
├── manifest-sha256.txt
├── log.md                                      # runtime log; integrity-excluded
└── .pdfws.lock                                 # temporary writer lock

Page splitting produces pages/page-0001.md and layout/pages/page-0001.json, not page-0001.pdf. The original PDF remains byte-for-byte available at source/original.pdf.

Collection directory structure

A collection wraps unmodified workspaces; it never rewrites a member tree except through the normal workspace API:

COLLECTION_ROOT/
├── index.md                                # overview of every document
├── collection.json                         # collection manifest, schema 1.0
├── members/
│   └── <document-id>/                      # a complete PDF workspace as listed above
├── removed/<document-id>/                  # detached members, no longer part of the collection
├── cache/                                  # regenerable and integrity-excluded
│   ├── .agent-pdf-workspace-cache-v1
│   ├── search.sqlite                       # cross-document index
│   ├── search-state.json                   # per-member index fingerprints
│   └── tmp/
├── manifest-sha256.txt                     # covers index.md and collection.json only
└── .pdfws-collection.lock                  # temporary collection writer lock

Integrity is split: the collection manifest covers only the collection's own files, and every member proves itself through its own manifest-sha256.txt. pdfws verify COLLECTION reports both and is valid only when all parts are valid.

When --cache-dir or cache_directory= selects an external cache, only the temporary worker branch moves:

<ABSOLUTE_CACHE_ROOT>/
├── .agent-pdf-workspace-cache-v1
└── tmp/
    └── pdfws-worker-<random>/result.json       # temporary

Canonical output, search.sqlite, and rendered query crops remain below the selected PDF workspace. Worker sessions contain bounded intermediate JSON and are deleted on normal completion or timeout; they are not importable document workspaces.

OpenCode integration creates files only when explicitly installed:

<OpenCode config directory>/
├── skills/agent-pdf-workspace/
│   ├── SKILL.md
│   └── agents/openai.yaml
└── tools/agent_pdf_workspace.ts

The persistent workspace layout is optimized for lossless PDF exploration and is not itself an OKF bundle. Use pdfws export okf to produce the separate, conformant OKF 0.2 tree described above. ODT/ODF export remains outside the workspace v1 contract.

The reproducible local comparison and its limitations are documented in benchmarks/parsebench/results/2026-07-15-test-suite.md.

See docs/format-v1.md for the format contract and SECURITY.md for the trust model.

Development

uv sync --python 3.12
uv run pytest --cov=agent_pdf_workspace --cov-fail-under=80
uv run ruff check .
uv run ruff format --check .
uv run mypy src/agent_pdf_workspace
uv run python -m build

No public PyPI release or remote repository is created automatically.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_pdf_workspace-0.3.0.tar.gz (249.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_pdf_workspace-0.3.0-py3-none-any.whl (85.3 kB view details)

Uploaded Python 3

File details

Details for the file agent_pdf_workspace-0.3.0.tar.gz.

File metadata

  • Download URL: agent_pdf_workspace-0.3.0.tar.gz
  • Upload date:
  • Size: 249.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.7

File hashes

Hashes for agent_pdf_workspace-0.3.0.tar.gz
Algorithm Hash digest
SHA256 4596eca4550c83ffdcc02370b3944bf21e60518b9411aef0d46cf5ade6a36890
MD5 fc4a1045d00981cb50dee231e1a5b102
BLAKE2b-256 9680f07d247ab1f709ad885b2d67c2f0cf5882f58087c963c4b1d51f619fd54d

See more details on using hashes here.

File details

Details for the file agent_pdf_workspace-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_pdf_workspace-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6e75839cfac2abfd1bc9aed4e5665914a82811b9ad665021d562dae248c82140
MD5 1715e5e8013a8fc5b1bb011558df33cc
BLAKE2b-256 1ad7e4a3112ba6e8adad7609f35da14cbdb036729607de56f96764d98df7a156

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.1

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page