Skip to content

feat(core): ingest docx, pptx, and csv into document sidecar notes - #1529

Open
phernandez wants to merge 10 commits into
mainfrom
feat/1512-document-ingestion-office
Open

feat(core): ingest docx, pptx, and csv into document sidecar notes#1529
phernandez wants to merge 10 commits into
mainfrom
feat/1512-document-ingestion-office

Conversation

@phernandez

@phernandez phernandez commented Sep 8, 2026

Copy link
Copy Markdown
Member

Closes #1512.

Why

Word, PowerPoint, and CSV files now get the same sidecar Markdown note a PDF gets (report.docx.md next to report.docx), through the existing parser-neutral document contract. PDF stays on pdf-inspector; the evaluation in #1512 shows why markitdown's PDF path is not a replacement (5.5s vs 0.36s on an 87-page paper, glued words, no page markers).

What Changed

  • Contract: ExtractedDocument + DocumentExtractor protocol in raw_document.py; RawDocumentRuntime dispatches on the indexed media type. RawPdfDocumentRuntime and build_raw_document_artifacts keep their signatures and delegate, so Cloud's wiring is untouched. Tags and document.kind come from the extractor instead of being hardcoded to pdf.
  • Bounded worker: the killable, byte-capped child-process mechanics move from the pdf-inspector adapter into bounded_process.py; the rlimit helpers move into worker_limits.py. Both workers use them.
  • markitdown (docx, pptx): markitdown_extractor.py / markitdown_worker.py call DocxConverter / PptxConverter directly. The sniffing MarkItDown.convert() loop is never used, so magika, the URL converters, zip recursion, and the plain-text / ASCII-decode failures seen on real files are out of the picture. Picture links are dropped (alt text kept when it is descriptive), so no dangling ![image.png](Picture2.jpg) or data URIs land in notes. Slides fill the page diagnostics for pptx.
  • CSV: stdlib preview (header, first 200 rows, row count) with strict UTF-8. A guessed charset is how markitdown produced mojibake on a valid UTF-8 export.
  • Local runtime: local_runtime.py resolves the indexed source through the API, uses the file checksum as the generation marker, writes the sidecar and document-ingestion-runs/<run-id>.md, indexes both, no-ops on an identical raw projection, and refuses to overwrite a hand-written or enriched note at the sidecar path.
  • CLI: bm import document <path> [--project]. PDF works through the same command, which closes the local bm import pdf gap from Parse PDFs into searchable LiteParse sidecar notes #1006.
  • Packaging: basic-memory[documents] = markitdown[docx,pptx] (also in the dev group). New install weight is roughly lxml, Pillow, mammoth, python-pptx; onnxruntime and numpy are already present via fastembed.

Implementation Details

The local CLI forces local routing because the source and sidecar live in the local project directory. Raw sidecars are checked against their originating run before reuse or replacement; edited or enriched content is refused. This prevents an unchanged import from recording user annotations as generated content and authorizing their loss on a later refresh.

AcceptedDocumentNote carries separate physical-file and normalized-projection checksums. Filesystem checks retain the actual LF/CRLF bytes. Run provenance hashes the complete persisted post-index text with normalized native line endings, including the indexed permalink. It does not parse/reassemble YAML, so authored comments and formatting remain significant. The physical checksum is also read after indexing, consistently across creation, reuse and refresh. CSV previews flatten CRLF, CR and LF, support large fields through a raised parser ceiling, enforce per-import UTF-8 field byte limits on headers and all rows, and report parser errors as CsvExtractionError. Local source reads and generation rechecks allocate at most the reader ceiling plus one byte (25 MiB by default); extractors retain their stricter format-specific limits.

Generated document entities now derive their initial UUID from the source identity during indexing, so run-ledger references resolve to the actual sidecar. Existing entities retain their IDs. The local writer refuses imports when that derived identity is already indexed at another sidecar path, or an existing sidecar belongs to a different source entity. This preserves canonical bytes when a source is moved alone or deleted and recreated; automatic identity migration is outside this PR.

Testing

  • Final provenance follow-up: 138 focused tests passed; 3 real indexing/identity integration cases passed on SQLite and Postgres; just fast-check passed. YAML-comment preservation and the post-index physical checksum failed before the fix. The SQLite integration additionally proves real creation/reuse/refresh.

  • Final ownership follow-up: 137 document-ingestion/CLI/identity tests passed on SQLite; all 3 real identity integration cases passed on Postgres; just fast-check passed.

  • just fast-check: passed, including typecheck.

  • uv run pytest tests/markdown tests/document_ingestion tests/cli/test_import_document.py test-int/test_local_document_identity.py --no-cov -q: 463 passed.

  • BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest test-int/test_local_document_identity.py --no-cov -q: 1 passed; proves the ledger ID resolves through the real indexing/API path.

  • uv run pytest tests/document_ingestion tests/cli/test_import_document.py --no-cov -q: 132 passed on the final checksum follow-up, including LF/CRLF creation, reuse and refresh; annotation preservation; real mdformat reuse; per-import CSV field bounds; and bounded local source reads.

  • just doctor: passed.

  • just package-check: passed.

  • A coverage-enabled local rerun hit a Pydantic BasicMemoryConfig fixture initialization error; the same focused suite passes without coverage. Hosted CI covers the supported platform matrix.

  • Original implementation validation: 114 tests across tests/document_ingestion and tests/cli/test_import_document.py, 100% line coverage on every touched module in document_ingestion/, including the real markitdown worker on generated docx/pptx and the CLI wiring.

  • Every unit-test directory, the top-level unit files, test-int (excluding benchmark/slow/live), and plugins pass when run per directory.

  • End to end in an isolated home: docx, pptx, csv, and an 87-page PDF each produced an indexed sidecar and run note, found by bm tool search-notes; a second run reported both notes as already current.

Risks / Follow-ups

  • Maintainer-approved follow-up: #1530 tracks concurrent sidecar edits/creation and stale-import acceptance. Current checksum and generation checks are preflight checks, not atomic compare-and-swap. These two races are deferred; ordinary source-change refresh remains enabled.

  • A missing or mismatched originating run note now requires explicit recovery (for example, moving the sidecar aside before importing). Rewriting that checksum from current content cannot distinguish an interrupted import from user edits, so reuse refuses instead of silently accepting ambiguous provenance.

  • The run note records the complete accepted post-index text checksum with normalized native line endings. It may differ from the physical file checksum for CRLF files; YAML representation and the indexed permalink are included, not discarded.

  • docx form-style layout tables lose structure in markitdown's output; xlsx is deferred until there is a header strategy for title rows.

  • Only tested locally on macOS; Windows falls back to the parent's wall-clock deadline as the only worker ceiling, same as pdf-inspector.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VPRRyMREwCHSLnY3TCBr7s

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T17:40:39.124412Z 134abc8 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Closes #1512. Documents other than PDF now get the same sidecar
Markdown note a PDF gets (`report.docx.md` next to `report.docx`),
through the existing parser-neutral document contract.

- Add `ExtractedDocument` and a `DocumentExtractor` protocol; the new
  `RawDocumentRuntime` dispatches on the indexed media type. The PDF
  runtime and `build_raw_document_artifacts` keep their signatures and
  delegate, so Cloud's wiring is unchanged.
- Extract the killable, byte-capped child-process mechanics from the
  pdf-inspector adapter into `bounded_process.py` and the rlimit
  helpers into `worker_limits.py`; both workers use them.
- Add a markitdown worker that calls `DocxConverter` / `PptxConverter`
  directly. The sniffing `MarkItDown.convert()` loop is never used, so
  magika, URL converters, zip recursion, and the plain-text and
  ASCII-decode failures seen on real files are out of the picture.
  Picture links are dropped, keeping alt text only when descriptive.
- Render CSV with the stdlib as a bounded preview (header, first N rows,
  row count) with strict UTF-8 decoding instead of a charset guess.
- Add a local project-directory runtime (API-backed source resolver,
  checksum-as-generation reader, sidecar + run-note writer that refuses
  to overwrite hand-written or enriched notes) and `bm import document`.
- Ship markitdown[docx,pptx] behind a `basic-memory[documents]` extra.

Verified end to end in an isolated home: docx, pptx, csv, and an
87-page PDF each produced an indexed, searchable sidecar and a run
note; a second run reported both as already current.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPRRyMREwCHSLnY3TCBr7s
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez
phernandez force-pushed the feat/1512-document-ingestion-office branch from 680064d to 1e055ab Compare September 8, 2026 14:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 680064d40b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/cli/commands/import_document.py
Comment thread src/basic_memory/document_ingestion/local_runtime.py Outdated
Comment thread src/basic_memory/document_ingestion/csv_extractor.py Outdated
- `bm import document` pins local API routing. The runtime reads the
  source and writes the sidecar in the local project directory, so a
  cloud-configured project must not route its index calls to a server
  that cannot see those files.
- The local writer verifies an existing run note against the accepted
  sidecar checksum and rewrites it when the two disagree, so overlapping
  imports of the same source converge instead of freezing a mismatch.
- CSV cells escape backslashes before pipes so `a\|b` survives the
  Markdown table round trip.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPRRyMREwCHSLnY3TCBr7s
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58b425a6b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/document_ingestion/local_runtime.py Outdated
A raw sidecar whose source changed is rebuilt only while it is still the
projection an earlier run wrote. The writer looks up that run's note,
compares the recorded checksum with the note on disk (allowing only the
permalink the indexer adds), and refuses with a conflict when the bytes
differ, when the run note is missing, or when the note changes between
the check and the replacing write. Note content is canonical; a raw note
a person has annotated is never replaced silently.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPRRyMREwCHSLnY3TCBr7s
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ebd87bfc3c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/document_ingestion/local_runtime.py Outdated
Comment thread src/basic_memory/document_ingestion/csv_extractor.py Outdated
Comment thread src/basic_memory/document_ingestion/csv_extractor.py Outdated
Every checksum the local document writer records or compares is now the
checksum of the raw projection as its run wrote it: the sidecar
re-assembled without the permalink the indexer appends. An unchanged
re-import therefore leaves the run note alone, and a later source change
still recognizes the annotated sidecar as untouched and rebuilds it. This
also makes the comparison independent of the platform newline convention
the atomic writer uses, which is what failed the Windows unit job.

CSV cells flatten CRLF and lone CR as well as LF, so a quoted multiline
field cannot split a table row once note assembly normalizes carriage
returns. The reader raises the process-wide field size limit to the
configured `max_field_bytes` (never lowering it) and reports parser
failures as `CsvExtractionError` instead of a bare `_csv.Error`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPRRyMREwCHSLnY3TCBr7s
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@phernandez

Copy link
Copy Markdown
Member Author

I reproduced one remaining Windows failure on f904444 locally by making the atomic writer persist CRLF bytes: test_writer_creates_the_sidecar_and_run_note_and_indexes_both still expects document_db_checksum to match the physical file, but the new return value is the normalized projection checksum. I am preparing a small follow-up that carries physical and projection checksums separately in a frozen AcceptedDocumentNote, preserving the run normalization while retaining the file checksum contract. The regression will exercise LF and CRLF creation, reuse, and refresh on every host.

Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a8ad7192f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/document_ingestion/local_runtime.py Outdated
Comment thread src/basic_memory/document_ingestion/csv_extractor.py
Comment thread src/basic_memory/document_ingestion/local_runtime.py Outdated
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a865e179cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/document_ingestion/local_runtime.py Outdated
Comment thread src/basic_memory/document_ingestion/raw_document.py
Comment thread src/basic_memory/document_ingestion/local_runtime.py
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@phernandez

Copy link
Copy Markdown
Member Author

Heads-up for anyone else working this branch: I am taking the two open P1 threads (checksum-conditioned replacement primitive in local_runtime.py, and fencing the source generation through sidecar acceptance). Will push on top of af3ffea without rewriting history.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af3ffeabbd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/document_ingestion/local_runtime.py
Comment thread src/basic_memory/document_ingestion/local_runtime.py
Comment thread src/basic_memory/markdown/utils.py
@phernandez

Copy link
Copy Markdown
Member Author

@codex review current head af3ffea. The maintainer has explicitly deferred the two concurrent-write races to #1530; both threads and the PR scope now document that decision. Please review remaining in-scope behavior and confirm the current head. The deterministic document UUID fix is included.

Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review latest head 7245d5c. Source move/recreation now fails before writing when document ownership conflicts; real SQLite and Postgres regressions pass. Missing originating-run recovery intentionally remains fail-closed as documented and explained in the resolved thread. Concurrent acceptance races are maintainer-deferred to #1530. Please check remaining in-scope behavior.

@phernandez

Copy link
Copy Markdown
Member Author

Standing down on the two P1 threads: they were resolved as deferred to #1530 while I was implementing, and that is a maintainer scope call. A candidate implementation (advisory note lock plus source-generation fence under it, with interleaving tests) is parked on feat/1530-note-lock-fence for that issue; nothing from it is on this branch.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7245d5c988

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/document_ingestion/local_runtime.py Outdated
Comment thread src/basic_memory/document_ingestion/local_runtime.py Outdated
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

@codex review latest head 134abc8. Both post-index checksum and YAML-comment preservation findings are fixed using the complete persisted post-index text. Please check the remaining in-scope behavior. The maintainer-deferred concurrency races remain in #1530, and ambiguous/missing provenance still requires explicit recovery.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 134abc84c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/document_ingestion/local_runtime.py
@phernandez

Copy link
Copy Markdown
Member Author

@codex review current head 134abc8. Replied to the provenance-field finding with the existing schema invariants and regression evidence: independently altered output IDs/paths are rejected during parsing. Coordinated manual ledger rewriting is outside this release; please avoid duplicating the schema validator and identify a normal importer path if any in-scope issue remains. Concurrency remains deferred to #1530.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 134abc84c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

) from error
output = run.frontmatter.output
recorded_checksum = output.raw.checksum if output and output.raw else None
if recorded_checksum != document_markdown_checksum(markdown):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare physical bytes before refreshing a raw sidecar

When an editor changes only the sidecar's newline convention, FileService.read_file_content() performs universal-newline translation before this checksum, so CRLF and LF files compare equal. If the source later changes, the normal importer refresh path overwrites the sidecar without reporting an edit, silently discarding the user's physical byte representation even though Markdown bytes are canonical state. Fresh evidence beyond the resolved YAML-formatting case is the newline-normalized text read feeding this comparison; store or compare the physical checksum when authorizing replacement.

AGENTS.md reference: AGENTS.md:L156-L160

Useful? React with 👍 / 👎.

Comment on lines +141 to +142
header = next(reader, None)
if header is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip blank records before choosing the CSV header

When a valid CSV begins with a blank record, csv.reader returns [] here, so the renderer fixes the table width at zero and _table_row() slices every subsequent record to zero cells. For example, \nname,age\nAda,37\n produces a sidecar containing only empty rows and loses all previewed values; skip leading empty records or reject a zero-width header instead of rendering the remainder with width zero.

Useful? React with 👍 / 👎.

@phernandez phernandez added this to the v0.24.0 milestone Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(core): ingest docx, pptx, and csv into document sidecar notes with markitdown

1 participant