Files
stack/tests/llm/test_pdfs.py
kert 6324960f28
Some checks failed
CI / lint (push) Successful in 38s
CI / notebooks-smoke (push) Successful in 1m38s
Deploy / notebooks (push) Has been skipped
CI / test (push) Failing after 2m22s
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / zotero (push) Successful in 13s
Infra CI / docs (push) Successful in 21s
Infra CI / notebooks (push) Successful in 51s
Deploy / report (push) Has been cancelled
Infra CI / mc (push) Has been cancelled
Infra CI / api (push) Has been cancelled
Infra CI / llm (push) Has been cancelled
feat(llm): the document viewer renders Word (.docx) and plain-text attachments in-page, not just PDFs
.docx: vendored docx-preview 0.3.5 (+ JSZip 3.10.1, both served from
/ui/vendor/) renders the file in the browser as pages (sections split
on page breaks, headers/footers/footnotes, embedded images), with the
same toolbar: fit-to-width / zoom (CSS zoom, transform fallback),
page input with hash tracking, arrow keys, and find-in-document over
the rendered DOM with highlighted, steppable matches seeded from the
cited snippet. .txt renders as a text pane with the same find. Legacy
.doc stays download-only (no comment attachment on file is one).

/pdf/<key> now says how each file renders (renderer: pdf | docx |
text | null); the file endpoint serves PDFs inline and everything else
as a download so the viewer fetches it as a blob. Chat and search
links read 'PDF' / 'Word' / 'file' by format.
2026-09-24 19:02:57 -04:00

136 lines
5.4 KiB
Python

"""llm.pdfs — attachments on file for the viewer: comment downloads, bib
attachments, Zotero storage PDFs; name-only resolution inside the roots."""
from __future__ import annotations
import sqlite3
from pathlib import Path
from bib.item import Source
from bib.store import Store
from llm.pdfs import PdfFile, pdf_files, resolve
def _zotero(
tmp_path: Path, parent_key: str, att_key: str, name: str
) -> tuple[Path, Path]:
db = tmp_path / "zotero.sqlite"
con = sqlite3.connect(db)
con.executescript(
"CREATE TABLE items (itemID INTEGER PRIMARY KEY, key TEXT);"
"CREATE TABLE itemAttachments (itemID INTEGER, parentItemID INTEGER, path TEXT);"
"CREATE TABLE deletedItems (itemID INTEGER);"
)
con.execute("INSERT INTO items VALUES (1, ?)", (parent_key,))
con.execute("INSERT INTO items VALUES (2, ?)", (att_key,))
con.execute("INSERT INTO items VALUES (3, 'GONE1234')")
con.execute("INSERT INTO itemAttachments VALUES (2, 1, ?)", (f"storage:{name}",))
con.execute("INSERT INTO itemAttachments VALUES (3, 1, 'storage:deleted.pdf')")
con.execute("INSERT INTO deletedItems VALUES (3)")
con.commit()
con.close()
storage = tmp_path / "zstorage"
(storage / att_key).mkdir(parents=True)
(storage / att_key / name).write_bytes(b"%PDF-1.4 z")
(storage / "GONE1234").mkdir()
(storage / "GONE1234" / "deleted.pdf").write_bytes(b"%PDF-1.4 d")
return db, storage
def test_all_three_sources_in_order_and_deduped(tmp_path):
store = Store(":memory:", storage_dir=tmp_path / "bibstorage")
key = store.upsert(Source(title="Paper", url="https://doi.org/10.1/x"))
pdf = tmp_path / "paper.pdf"
pdf.write_bytes(b"%PDF-1.4 b")
store.attach_file(key, pdf)
(tmp_path / "notes.docx").write_bytes(b"PK docx")
store.attach_file(key, tmp_path / "notes.docx")
zdb, zstore = _zotero(
tmp_path, key, "ATT00001", "paper.pdf"
) # same name as the bib copy → deduped
files = pdf_files(
store,
key,
zotero_sqlite=zdb,
zotero_storage=zstore,
comments_root=tmp_path / "comments",
)
assert [(f.name, f.source, f.renderer) for f in files] == [
("paper.pdf", "bib", "pdf"),
("notes.docx", "bib", "docx"),
]
assert all(f.renderable for f in files)
assert files[1].media_type.startswith("application/vnd.openxmlformats")
assert all(isinstance(f, PdfFile) and f.size > 0 for f in files)
store.close()
def test_zotero_only_item_and_deleted_attachments_skipped(tmp_path):
store = Store(":memory:", storage_dir=tmp_path / "bibstorage")
key = store.upsert(Source(title="Zot only", url="https://example.org/z"))
zdb, zstore = _zotero(tmp_path, key, "ATT00002", "Cassidy - 2025 - RFI.pdf")
files = pdf_files(store, key, zotero_sqlite=zdb, zotero_storage=zstore)
assert [(f.name, f.source) for f in files] == [
("Cassidy - 2025 - RFI.pdf", "zotero")
]
assert pdf_files(store, "NOPE1234", zotero_sqlite=zdb, zotero_storage=zstore) == []
assert (
pdf_files(
store, key, zotero_sqlite=tmp_path / "missing.sqlite", zotero_storage=zstore
)
== []
)
store.close()
def test_comment_attachments_from_the_state_tree(tmp_path):
store = Store(":memory:", storage_dir=tmp_path / "bibstorage")
key = store.upsert(
Source(title="c", url="https://www.regulations.gov/comment/CMS-2026-2377-40314")
)
d = tmp_path / "comments" / "CMS-2026-2377" / "CMS-2026-2377-40314"
d.mkdir(parents=True)
(d / "attachment_1.pdf").write_bytes(b"%PDF-1.4 a1")
(d / "attachment_2.docx").write_bytes(b"PK")
(d / "combined.md").write_text("# not an attachment")
(d / "attachment_1.png").write_bytes(b"png")
files = pdf_files(store, key, comments_root=tmp_path / "comments")
(d / "attachment_3.doc").write_bytes(b"\xd0\xcf")
(d / "attachment_4.txt").write_text("plain")
files = pdf_files(store, key, comments_root=tmp_path / "comments")
assert [(f.name, f.source, f.renderer) for f in files] == [
("attachment_1.pdf", "comment", "pdf"),
("attachment_2.docx", "comment", "docx"),
("attachment_3.doc", "comment", None),
("attachment_4.txt", "comment", "text"),
]
assert [f.renderable for f in files] == [True, True, False, True]
# a non-comment url, or a comment with no downloaded dir, yields nothing
other = store.upsert(Source(title="p", url="https://doi.org/10.1/y"))
assert pdf_files(store, other, comments_root=tmp_path / "comments") == []
gone = store.upsert(
Source(
title="c2", url="https://www.regulations.gov/comment/CMS-2026-2377-99999"
)
)
assert pdf_files(store, gone, comments_root=tmp_path / "comments") == []
store.close()
def test_resolve_by_name_inside_roots_only(tmp_path):
inside = tmp_path / "root" / "K" / "a.pdf"
inside.parent.mkdir(parents=True)
inside.write_bytes(b"%PDF")
outside = tmp_path / "elsewhere" / "b.pdf"
outside.parent.mkdir()
outside.write_bytes(b"%PDF")
files = [PdfFile("a.pdf", inside, 4, "bib"), PdfFile("b.pdf", outside, 4, "bib")]
roots = (tmp_path / "root",)
assert resolve(files, "", roots=roots).name == "a.pdf"
assert resolve(files, "a.pdf", roots=roots).name == "a.pdf"
assert (
resolve(files, "b.pdf", roots=roots) is None
) # listed, but outside every root
assert resolve(files, "../a.pdf", roots=roots) is None
assert resolve([], "", roots=roots) is None