Implement the RichFilesystemAdapter that converts non-markdown files (PDF, DOCX, XLSX, PPTX, HTML, images, audio) to markdown using MarkItDown with Pandoc fallback, yielding NormalizedContent with rich structural metadata.
Requirements
- Convert non-markdown files to markdown using MarkItDown as primary conversion library (FR-4.1)
- Fall back to Pandoc subprocess for formats not supported by MarkItDown (FR-4.2)
- Emit
NormalizedContent with StructuralHints populated with MIME type, file size in bytes, creation timestamp, modification timestamp, and directory hierarchy (FR-4.3)
- Use
_watching.py FileSystemWatcher when configured for push-based ingestion (FR-4.4)
adapter_id must be f"filesystem_rich:{resolved_path}" — distinct from FilesystemAdapter's f"filesystem:{absolute_path}" (FR-4.5)
- Set
domain = Domain.NOTES (FR-4.6)
- Include original MIME type or file extension in metadata on each
NormalizedContent item (FR-4.7)
- Skip
.md files — those are handled by the existing FilesystemAdapter
- Add
markitdown to optional dependencies under rich-fs group in pyproject.toml
Design Guidance
File to create: src/context_library/adapters/filesystem_rich.py
Class signature:
class RichFilesystemAdapter(BaseAdapter):
def __init__(
self,
directory: Path,
poll_strategy: PollStrategy = PollStrategy.PULL,
extensions: set[str] | None = None, # None = all supported
) -> None: ...
@property
def adapter_id(self) -> str:
return f"filesystem_rich:{self._directory.resolve()}"
@property
def domain(self) -> Domain:
return Domain.NOTES
@property
def normalizer_version(self) -> str:
return "1.0.0"
def fetch(self, source_ref: str) -> Iterator[NormalizedContent]: ...
MarkItDown conversion:
from markitdown import MarkItDown
def _convert_with_markitdown(file_path: Path) -> str | None:
md = MarkItDown()
try:
result = md.convert(str(file_path))
return result.text_content
except Exception:
return None # signal fallback needed
Pandoc fallback:
import subprocess
def _convert_with_pandoc(file_path: Path) -> str | None:
try:
result = subprocess.run(
["pandoc", "-t", "markdown", str(file_path)],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
return result.stdout
return None
except (FileNotFoundError, subprocess.TimeoutExpired):
return None
StructuralHints population: The existing StructuralHints model has file_path, modified_at, file_size_bytes. To pass MIME type and creation timestamp, use the extra_metadata field (if added in Phase 1) or encode them in the file_path field comment. Recommended: use extra_metadata: dict on StructuralHints.
MIME type detection:
import mimetypes
mime_type, _ = mimetypes.guess_type(str(file_path))
pyproject.toml change:
[project.optional-dependencies]
rich-fs = ["markitdown", "watchdog"]
Supported formats (MarkItDown handles): PDF, DOCX, PPTX, XLSX, HTML, images (with alt text), audio (with transcription if configured).
Pandoc handles: LaTeX (.tex), EPUB, RST, ODT, and many others.
Directory walking: Use Path.rglob("*") and filter by suffix, skipping .md files.
Push mode: When poll_strategy == PollStrategy.PUSH, instantiate FileSystemWatcher and wire its callback to trigger re-ingestion of the changed file's source_ref.
Acceptance Criteria
Dependencies
Phase 1: Model Additions (PollStrategy + MessageMetadata), Phase 3: Shared Filesystem Watcher (_watching.py)
Parent Issue
Part of #83
Discussion
This work is detailed in discussion 84
Implement the RichFilesystemAdapter that converts non-markdown files (PDF, DOCX, XLSX, PPTX, HTML, images, audio) to markdown using MarkItDown with Pandoc fallback, yielding NormalizedContent with rich structural metadata.
Requirements
NormalizedContentwithStructuralHintspopulated with MIME type, file size in bytes, creation timestamp, modification timestamp, and directory hierarchy (FR-4.3)_watching.pyFileSystemWatcherwhen configured for push-based ingestion (FR-4.4)adapter_idmust bef"filesystem_rich:{resolved_path}"— distinct fromFilesystemAdapter'sf"filesystem:{absolute_path}"(FR-4.5)domain = Domain.NOTES(FR-4.6)NormalizedContentitem (FR-4.7).mdfiles — those are handled by the existingFilesystemAdaptermarkitdownto optional dependencies underrich-fsgroup inpyproject.tomlDesign Guidance
File to create:
src/context_library/adapters/filesystem_rich.pyClass signature:
MarkItDown conversion:
Pandoc fallback:
StructuralHints population: The existing
StructuralHintsmodel hasfile_path,modified_at,file_size_bytes. To pass MIME type and creation timestamp, use theextra_metadatafield (if added in Phase 1) or encode them in thefile_pathfield comment. Recommended: useextra_metadata: dictonStructuralHints.MIME type detection:
pyproject.toml change:
Supported formats (MarkItDown handles): PDF, DOCX, PPTX, XLSX, HTML, images (with alt text), audio (with transcription if configured).
Pandoc handles: LaTeX (.tex), EPUB, RST, ODT, and many others.
Directory walking: Use
Path.rglob("*")and filter by suffix, skipping.mdfiles.Push mode: When
poll_strategy == PollStrategy.PUSH, instantiateFileSystemWatcherand wire its callback to trigger re-ingestion of the changed file'ssource_ref.Acceptance Criteria
RichFilesystemAdapter(directory=path).adapter_idreturnsf"filesystem_rich:{path.resolve()}"FilesystemAdapterandRichFilesystemAdapterregistered on the sameDocumentStorehave distinctadapter_idvaluesfetch("")on a directory containing a.pdffile yields aNormalizedContentwith non-emptymarkdownfetch("")on a directory containing a.docxfile converts it to markdownfetch("")skips.mdfiles (those are handled by FilesystemAdapter)NormalizedContent.structural_hintsincludes MIME type of the original fileNormalizedContent.structural_hints.file_size_bytesis populatedNormalizedContent.structural_hints.modified_atis populated as ISO 8601domainproperty returnsDomain.NOTESpoll_strategy=PollStrategy.PUSH, aFileSystemWatcherinstance is created (not started untilstart()is called)RichFilesystemAdapteris importable fromcontext_library.adapters.filesystem_richDependencies
Phase 1: Model Additions (PollStrategy + MessageMetadata), Phase 3: Shared Filesystem Watcher (_watching.py)
Parent Issue
Part of #83
Discussion
This work is detailed in discussion 84