Plan: KB pluggable parser framework + bundled parsers (#39)¶
KB ingest used to be MIME-allowlist gated (text + a tiny set of binary readers). Issue #39 turns it into a pluggable parser framework where bundled parsers (PDF / HTML / DOCX / JSON / CSV / Excel / image-VLM / slides-VLM) sit alongside custom in-house parsers operators register via
kb.parsers. Decisions captured below come from the/grill-mewalks on 2026-06-05/06. Per CLAUDE.md workflow: every parser shipped through/tdd.
0 · 進度總覽¶
| 階段 | 內容 | 狀態 |
|---|---|---|
| P1 | Framework — IParser ABC + IParserInput lazy adapter + ParserRegistry |
✅ 8096c63 |
| P2 | PDF / HTML / DOCX wrappers — bundled LlamaIndexReader-backed IParsers |
✅ 97ea784 |
| P3 | get_parser_registry(settings) factory + kb.parsers: list[str] config |
✅ 034185e |
| P4 | IParser.parse(on_progress=…) + ParserRegistry.all_matching |
✅ a4c0662 |
| P5 | Schema — SourceDoc.status_detail + DocChunk.parser_id |
✅ 756bfd8 |
| P6 | Ingestor rewrite — store-all + all-matching parsers + parser_id + status_detail | ✅ 83f3a62 |
| P7 | JSON parser + DispatchSplitter JSON branch (JSONNodeParser) |
✅ 58d5637 |
| P8 | CSV/TSV (PagedCSVReader) + Excel (pandas paged) parsers |
✅ ae22da6 |
| P9 | kb.vlm_llm + kb-vlm preset + kb.parsers_disabled + IVlm/VlmDescriber |
✅ ec44eac + 91ee005 |
| P10/P11 | VlmImageParser + PdfParser v2 (selective per-page VLM) + PptxParser (soffice) |
✅ fa30f3e |
| P12 | FE — status_detail beside the doc-table status chip |
✅ 17fde92 |
每階段完成定義:uv run ruff check && uv run ruff format --check && uv run ty check 全清、
uv run pytest 全綠,commit 完成,本表打勾。
1 · 鎖定的設計(grilling)¶
Q1-Q5 框架¶
- Q1 — 機制:
IParserABC +ParserRegistry(feedback_abc_over_protocol) - Q2 — 輸出:
Iterator[Document] | list[Document](LlamaIndex Document) - Q3 — 輸入:
IParserInputlazy 適配器,as_bytes/as_path/as_stream - Q4 — matches:每個 parser 自帶
matches(*, filename, mime, source) -> bool, 可選 peek 進source內容。mime 來自 libmagic,extension 是可靠訊號 (JSON/CSV 常被 sniff 成 text/plain;xlsx/pptx 常只看得到 zip container) - Q5 — VLM LLM:
kb.vlm_llm: {preset: kb-vlm}(parallel tokb.retrieval_llm); bundledkb-vlmpreset =ollama_chat/qwen2.5vl:7b
Q6-Q7 註冊¶
- Q6:既有 reader 遷成
IParser,reader_for刪除 - Q7:Bundled parsers 在
factories.get_parser_registry(settings)寫死; custom 走kb.parsers: ["my.pkg.MyParser", ...]dotted path,註冊在 HEAD
Q8-Q10 ingest model¶
- Q8a — store 範圍:全都存(pipeline mode)。未知類型也存,未來 plugin
parser 可以 backfill。Legacy chunker mode (
pipeline=None) 仍 text-only - Q8b — 多 parser:所有
matches(...)=True的 parser 都跑,各產一個 chunk packet。Dispatch 為 parsers-first:inline text/code packet(parser_id="") 只是無 parser 認領時的 fallback - Q8c — chunk 歸屬:
DocChunk.parser_id: str= parser class name - Q9a — splitter 還跑:parser 出 Document →
DispatchSplitter再切 - Q9b — no-match 行為:
status=ready, chunks=0,SourceDoc 保留 - Q10 — parser exception:
status=error,status_detail帶錯誤摘要(240 字截斷)
Q11-Q16 操作面¶
- Q11 — 進度回報:
SourceDoc.status_detail+IParser.parse(on_progress=...); FE doc 表在 status chip 旁直接顯示(1.5s polling 帶動) - Q12 — model 換 / soffice 補裝:不自動 reindex,operator 手動
- Q15 — sandbox tool packages:不共享,完全分開的概念
- Q16 — VLM 測試:Fake
IVlm注入(tests/kb/parsers/test_vision.py)
Chunking 哲學(user 拍板)¶
「原檔永遠保存 + chunk 以 specstar Ref 連回 SourceDoc,其餘粒度選擇都是可調
hyperparameter。」 Parser 出 whole-file(或 whole-page / whole-row 等自然單位)
Document,粒度歸 splitter 管(DispatchSplitter:md → MarkdownNodeParser +
breadcrumb;code → tree-sitter CodeSplitter;json → JSONNodeParser;其他 →
SentenceSplitter 256/32)。詳見 memory chunking-hyperparams-not-design。
2 · 各 parser 落地形狀(research-corrected)¶
研究結論(LlamaIndex docs / TabRAG / ColPali / 多篇實戰文,2026-06):
| Parser | 形狀 | 根據 |
|---|---|---|
JsonParser |
.json 整檔一 Document、.jsonl 一行一 Document;DispatchSplitter JSON 分支走 JSONNodeParser(array → 一 element 一 node;leaf 帶祖先 key path)。壞 JSON raise → status=error |
SentenceSplitter 會切斷 record、拆散 key-value |
CsvParser |
PagedCSVReader:一列一 Document,col: value 行(欄名跟著每列);.tsv 同 reader delimiter="\t" |
裸 v1, v2 列是反模式 — embedding 失去欄位語意;token 窗切碎列 |
ExcelParser |
pandas(openpyxl)自製 paged 形狀,sheet_name=None 全 sheet,多 sheet 時補 sheet: <name> 行 |
同上;PandasExcelReader 丟 header(反模式)且無 Paged 版 |
VlmImageParser |
png/jpg/webp → 一張圖一次 VLM call → 一個 Markdown Document。無 VLM → matches False(存而不 index) | VLM-to-text 是 doc-heavy RAG 共識;CLIP 弱(60% vs 95%);gif/svg 不收 |
PdfParser v2 |
逐頁 Document + 選擇性 VLM:pypdf 抽文字層;文字稀疏(<50 字)或頁帶圖 → pypdfium2 轉 200DPI PNG → VLM。無 VLM → 純文字層。對應 user 的兩種 PDF(paper / slide export) | pypdf 是文字型 PDF 共識地板;選擇性 VLM 省 call;pypdfium2 = pip wheel、BSD/Apache(避開 PyMuPDF AGPL) |
PptxParser |
soffice headless 轉 PDF → 走 pdf_pages_to_documents 同一條路(轉出的 PDF 保留逐字文字層 + 可 rasterize)→ 一 slide 一 Document。soffice 沒裝 → RuntimeError(status=error + 清楚訊息) |
純 Python 沒有 pptx rasteriser;混合式(文字層逐字 + VLM 只管視覺)是 SOTA |
VLM 層:IVlm ABC(streaming-only,collect drain stream)+ LitellmVlm
(multimodal content parts、base64 data URI)+ VlmDescriber(分層 prompt:
逐字 OCR → 結構描述 → 表格轉 markdown,kb/prompts/vlm_describe.md)。
3 · Docling 升級路(預留的接點)¶
最終會需要 Docling(layout model、table structure、掃描檔)。接法已備好:
- 寫
DoclingParser(IParser)— Docling 原生輸出 markdown,丟回 pipeline 後DispatchSplitter自動走 markdown 分支(heading breadcrumb 白賺) - config 一行替換(
kb.parsers_disabled就是為此而生 — all-matching 語意下 custom parser 不會 shadow bundled,要顯式關):
- 不用動任何框架程式
4 · 未來選項(這輪不做)¶
- Splitter 升級(hyperparameter 級,動機出現再開):
SentenceWindowNodeParser(句子級檢索精度)、HierarchicalNodeParser+ AutoMergingRetriever(長文件)、SemanticSplitterNodeParser(語意斷點,切時要 embedding)、MarkdownElementNodeParser(文件內嵌表格) - ColPali / 多向量 page embedding:VLM-to-text 的升級路,需要 multi-vector store(Qdrant/Vespa),等 vector 層換代再議
parser_id的 FE 顯示:等 chunk-level debug view 存在才有地方放 badge- VLM cost / quota 監測:操作面,另案
- 已知固有成本(接受):百萬列 CSV = 百萬 chunk/embedding;JSON 單一巨型 record = 一個超大 node(embedding 模型自行截斷)
5 · 依賴 / 注意¶
- 新依賴:
pypdfium2(PDF 轉圖,pip wheel)、openpyxl(Excel)。 系統依賴:soffice(只有 pptx 需要;沒裝 → 該 doc status=error,裝完 reindex) - 全部 commit 在
demobranch,不 push(perfeedback_no_push) - ABC + 介面/實作分檔(per
feedback_abc_over_protocol) - VLM 必須 streaming(per
feedback_always_stream_llm)—IVlm.stream是唯一 primitive - 既有 plan
docs/plan-llamaindex-ingest.mdP1/P2 是這份 plan 的基礎