Protocol B append: failed-turn retry (L3–4), git source tokens (L5), image documents (L6 ingest), chat image questions (L6 chat side). TODO.md items now live in .agents/phases/todo/ and the file is cleared. LLM-Generated: true
4.3 KiB
4.3 KiB
Task 02 — Image ingest: model fields, walk filter, binary index path, prune guard
Phase: 122_image_documents · Source: TODO.md:6 — "Images uploaded as part of documents or as standalone images should be read, summarized, and retrieved like any other document."
Objective
When BOR_IMAGES=true, standalone image files in ANY ingest path (direct upload, uploaded archive, git/local source walk) become Document rows — bytes persisted to image_dir, is_image/image_path set, content = the vision description (task 03) — and an images-off sync never prunes existing image docs.
Work
app/models.py—Document(L102): add, with house docstrings (thesummaryL140 /created_at_manualprecedent):is_image: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false"), nullable=False)— True iff the doc's content is a vision description of an image (phase 122); the image bytes live atimage_path.image_path: Mapped[str | None] = mapped_column(Text, default=None)— absolute path of the persistent copy insettings.image_dir; NULL for text docs.
alembic/versions/— new revision: both columns (is_imageNOT NULL server_default 'false';image_pathnullable) + downgrade.app/rag/importer.py:iter_importable_files(L235) / the walk inimport_sources(L279): whenllm.settings.images, accept a file iff its extension matchesimport_extension_setOR the image frozenset (task 01) — pass the image set in (the function takes explicit extension sets; the image set is NOT merged intoimport_extension_set)._index_file(L435): image branch FIRST (before theread_textat L453) — if the path's extension is in the image set:data = full_path.read_bytes(),digest = sha256(data), and on new/changed: copydatatoimage_dir/<doc-id or uuid4>.<ext>(dir created withmkdir(parents=True, exist_ok=True)), setis_image=True+image_pathon theDocumentrow,content= the description (task 03'sdescribe_image— this task wires the call; the function lands in task 03, so for THIS task storecontent = ""placeholder ONLY if task 03 is not yet merged — the phases run task-ordered, so in practice task 03's function exists; wire it directly and let task 03 implement it. If implementing strictly per task: this task storescontentvia a_describe_or_skiphook that task 03 fills — keep the seam single and commented).- A CHANGED image (hash differs) deletes the stale
image_pathcopy before replacing it. - The unchanged/hash path (L470+) works unmodified for images (byte digest); the phase-118 summary-backfill branch (L480) re-describes an image doc whose
summaryis NULL on the next sync (same fail-soft).
- A CHANGED image (hash differs) deletes the stale
_prune(L649): the prune guard (LOCKED derived decision) — whensettings.imagesis FALSE, skip everyis_imagedoc (invisible to the walk ≠ deleted); toggle TRUE → normal prune + delete theimage_pathcopy of each pruned image doc (also on the normal prune path when the file is gone).ImportSummary(L100): newimages_failed: int = 0counter + its slot informat_counts/log(L135–151) — task 03 increments it; add it now so the log shape is stable.
- ASSUMPTION (A4 re-stated): only standalone image FILES are ingested — no archive-of-documents extraction, no PDF/DOCX embedded-image pulls (the archive unpacker already places image members on disk; the walk now just accepts them).
Testing & Quality
- Unit:
tests/unit/test_image_documents.py(task 06) — the walk accepts.pngonly whenimages=True(off → ignored, the byte-identical default), the binary branch (digest over bytes, copy made, fields set,read_textnever called for an image), the changed-image stale-copy delete, the prune guard (off → image doc survives; on + file gone → pruned + copy deleted),images_failedin the log line. - Integration:
tests/integration/test_docs_api.py(task 06) — theimport_sourcesend-to-end cases. - Coverage: >90% on the touched modules.
Completion Criteria
alembic upgrade headapplies; a seeded image walk withimages=Truecreates the doc row +image_dircopy;images=Falseignores the file entirely.- A sync with
images=Falseleaves a pre-existing image doc untouched (prune guard). uv run pytestgreen;uv run ruff check . && uv run pyrightclean.