From 7e73d84283893997f23bab33fc2d5977e88306e4 Mon Sep 17 00:00:00 2001 From: Joe Danziger <1423777+joedanz@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:02:41 -0400 Subject: [PATCH] feat(agents-box): bake PyMuPDF, Pillow and numpy into /opt/doctools Agents now run on a network allowlist that does not include PyPI, so tools an agent used to fetch at runtime (uv run --with pymupdf, ~60 runs 08-28..09-22) must be in the image. Pinned to what uv resolves under its 14-day cutoff and verified with a render + import test on Python 3.13.5. The build imports every library, so a bad pin fails the build. OCR stays tesseract; RapidOCR left out (+285 MB, unused since 09-09). Claude-Session: https://claude.ai/code/session_01DAcnnbhk6e7PpBhgJjTewZ --- CHANGELOG.md | 4 ++++ deploy/agents-box/Dockerfile | 11 ++++++++- tests/test_agents_box_doctools.py | 39 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/test_agents_box_doctools.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c93e81d..6fc49bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ explicitly under **Changed**, with what to do about it. ## [Unreleased] +### Added + +- **The agent image's document tools can now render PDF pages and handle images.** `/opt/doctools` gains PyMuPDF, Pillow and numpy, pinned. Agents on a network allowlist cannot reach PyPI, so tools they used to install on demand (`uv run --with pymupdf`) have to be in the image. OCR is still tesseract through `doc2text`. + ## [0.9.0] - 2026-10-06 ### Added diff --git a/deploy/agents-box/Dockerfile b/deploy/agents-box/Dockerfile index 19c1681..9d9d468 100644 --- a/deploy/agents-box/Dockerfile +++ b/deploy/agents-box/Dockerfile @@ -113,9 +113,18 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends poppler-utils tesseract-ocr tesseract-ocr-eng \ && rm -rf /var/lib/apt/lists/* \ && pdftotext -v >/dev/null 2>&1 && tesseract --version >/dev/null 2>&1 +# PyMuPDF (render a PDF page to an image, read a PDF's layout), Pillow and numpy +# (image handling). Baked in because the agents' network is an allowlist and PyPI +# is NOT on it: before this, an agent that needed them ran `uv run --with pymupdf …` +# ~60 times (2026-08-28..09-22) and fetched them at runtime. OCR stays tesseract +# (above); RapidOCR was left out on purpose (+285 MB, unused since 2026-09-09). +# Pins are what `uv` resolves under its 14-day exclude-newer cutoff, checked on +# 2026-10-20 with a render + import test on Python 3.13.5. RUN uv venv /opt/doctools --python 3.13 \ && uv pip install --python /opt/doctools/bin/python \ - "openpyxl==3.1.5" "xlrd==2.0.2" "python-docx==1.2.0" "pypdf==6.1.1" + "openpyxl==3.1.5" "xlrd==2.0.2" "python-docx==1.2.0" "pypdf==6.1.1" \ + "pymupdf==1.28.2" "pillow==13.0.0" "numpy==2.5.4" \ + && /opt/doctools/bin/python -c "import fitz, PIL, numpy, pypdf, docx, openpyxl, xlrd" COPY deploy/agents-box/scripts/doc2text /usr/local/bin/doc2text # openpyxl AGAIN, into hermes' own venv this time. hermes ships a bundled diff --git a/tests/test_agents_box_doctools.py b/tests/test_agents_box_doctools.py new file mode 100644 index 0000000..ab4b0f5 --- /dev/null +++ b/tests/test_agents_box_doctools.py @@ -0,0 +1,39 @@ +"""The agent image's document tools are baked in, pinned, and import-checked at build time. + +Agents run on a network allowlist that does not include PyPI, so a library the agent used to fetch with +`uv run --with pymupdf` (≈60 runs, 2026-08-28..09-22) must be in the image. The Dockerfile has no test run, +so this reads the text: every library is pinned with `==` and one RUN imports them all, so a bad pin fails the +build instead of the agent's first PDF. + +Run: uv run --extra dev python -m pytest tests/test_agents_box_doctools.py -q +""" +import re +from pathlib import Path + +DOCKERFILE = Path(__file__).resolve().parents[1] / "deploy" / "agents-box" / "Dockerfile" +BAKED = {"openpyxl": "openpyxl", "xlrd": "xlrd", "python-docx": "docx", "pypdf": "pypdf", + "pymupdf": "fitz", "pillow": "PIL", "numpy": "numpy"} + + +def doctools_run(): + df = DOCKERFILE.read_text() + start = df.index("RUN uv venv /opt/doctools") + return df[start:df.index("\n\n", start)].replace("\\\n", " ") + + +def test_every_document_library_is_pinned_exactly(): + run = doctools_run() + for pkg in BAKED: + assert re.search(rf'"{re.escape(pkg)}==\d[\w.]*"', run), f"{pkg} must be pinned with ==" + + +def test_the_build_imports_every_library_it_installs(): + run = doctools_run() + probe = run[run.index("/opt/doctools/bin/python -c"):] + for module in BAKED.values(): + assert re.search(rf"\b{module}\b", probe), f"the build-time import must cover {module}" + + +def test_ocr_is_tesseract_and_rapidocr_stays_out(): + df = DOCKERFILE.read_text() + assert "tesseract-ocr" in df and "rapidocr" not in df.lower().replace("rapidocr was left out", "")