Repository navigation
feat(knowledge): the batch document extractor rejects .csv, .json and .html that the upload path ingests #16785
Description
Activity
- addedenhancementNew feature or requestNew feature or requestarea: document-ingestWave 5 · cluster K — Document ingest & OCRWave 5 · cluster K — Document ingest & OCR
on Sep 16, 2026 Closing with evidence — all four acceptance criteria verified against the actual merged code on
origin/mainat928573e3bb(merge of #17077, PR #16790), not the PR body's claims.AC1 —
.csv/.json/.htmlhandled byextract_from_file, reusing the GUI path's treatment rather than decoding all three as plain text
autobot-backend/utils/document_extractors.py:94-111—SUPPORTED_FORMATS["text"]now includes.csv(line 99, decoded as text like.txt/.md), plus new"json": [".json"](line 110) and"html": [".html"](line 111) groups. Dispatch inextract_from_file(lines 302-305):.json→extract_from_json(line 332),.html→extract_from_html(line 354).extract_from_json(:332-350):json.dumps(json.loads(raw.decode("utf-8")), indent=2)— same call shape as the GUI path.extract_from_html(:354-371):from api.knowledge import _sanitize_html_contentimported at call time, thencontent, _title = _sanitize_html_content(raw.decode("utf-8", errors="replace"))— calls the real shared sanitiser, not a reimplementation.
Compare to the GUI path,
api/knowledge.py:_extract_file_content(:1075-1109):.csvdecoded as text (:1096-1097),.html→_sanitize_html_content(:1101-1103),.json→json.dumps(json.loads(...), indent=2)with the sameJSONDecodeErrorfallback (:1105-1109). Met.AC2 —
get_supported_extensions()/is_supported_format()report the three formats
document_extractors.py:470-485and:487-502both iterate/querySUPPORTED_FORMATS, which now includes the.csv/.json/.htmlentries above — no separate list to fall out of sync. Confirmed directly bytest_get_supported_extensions_reports_the_three_new_formatsandtest_is_supported_format_reports_the_three_new_formatsinutils/document_extractors_16785_test.py. Met.AC3 — a test asserts identical output through both routes for the same bytes
autobot-backend/utils/document_extractors_16785_test.pyimportsfrom api.knowledge import _extract_file_contentandfrom utils.document_extractors import DocumentExtractor, then runs both on the same byte string in each of:test_csv_no_longer_raises_and_matches_upload_path—extractor_text == upload_text == content.decode("utf-8")test_json_reserialised_with_indent_matches_upload_path— asserts equality and the exact indented JSON stringtest_invalid_json_falls_back_to_raw_text_matching_upload_path— malformed JSON, both routes fall back identicallytest_html_sanitised_via_the_shared_sanitiser_matches_upload_path— both routes produce the same sanitised text
Met.
AC4 — HTML path is not a second sanitiser; genuine negative control
extract_from_htmlimports and callsapi.knowledge._sanitize_html_contentdirectly (document_extractors.py:363,371) rather than reimplementing tag-stripping. The negative control istest_html_sanitised_via_the_shared_sanitiser_matches_upload_path: input<script>alert(1)</script><p>Hello</p>, asserts"alert(1)" not in extractor_textand"Hello" in extractor_text— the test's own docstring notes this specifically catches "a local reimplementation that merely stripped tags" leavingalert(1)content in. This proves the shared sanitiser's actual script-removal behavior runs through the batch path, not just a superficially similar one. Met.All four ACs are met by code on
origin/main, not just PR narrative. Closing.AC verification — 4 of 4 delivered, now ticked
Verified against
git show origin/main:at2b88c1c2e5.AC1 — handled with the GUI path's treatment, not decoded as plain text.
autobot-backend/utils/document_extractors.py::97-99puts.csvin the"text"group with the comment "#16785: .csv joins this group — the GUI upload path (api/knowledge.py …)", while:110-111give.jsonand.htmltheir own groups rather than folding them into text. The dispatch docstring at:258-260states each treatment:.jsonre-serialised withindent=2,.htmlsanitised viaapi.knowledge._sanitize_html_content.AC2 — discovery agrees.
:101-102: "Listed here soget_supported_extensions()andis_supported_format()— which drive directory discovery — agree with …", and asserted bydocument_extractors_16785_test.py:95and:102.AC3 — both routes, same bytes, asserted to agree.
autobot-backend/utils/document_extractors_16785_test.py:9-11states the method: "Every test here drives both routes on identical bytes and asserts they agree, since that agreement is …" — the criterion's own reasoning. By test name::26csv matches the upload path ·:39json re-serialised matches it ·:53invalid json falls back to raw text and still matches ·:65html sanitised via the shared sanitiser matches ·:82extract_from_filedispatches all three.:53is worth calling out: the agreement is asserted on the malformed input too, which is where two independent decoders would most easily diverge.AC4 — not a second sanitiser.
:354-371extract_from_html, docstring: "Reusesapi.knowledge._sanitize_html_contentrather than a second sanitiser. Imported at call time: this module is reusable by any component, andapi.knowledge(a FastAPI router module) is not a …" — then:363from api.knowledge import _sanitize_html_contentand:371calls it. The criterion verbatim, with the import-time reasoning recorded.Part of a sweep over the 93
v0.9.0closures closedCOMPLETEDwith a checklist and nothing ticked (#17361). Drawn in a seeded random sample.
Problem
The owner's ruling is that every format named in the ingestion review has to be handled. #16775 covers one direction — the GUI upload rejecting office/ODF formats the batch path parses. This is the other direction.
DocumentExtractor.SUPPORTED_FORMATS(utils/document_extractors.py:93) covers pdf, docx/doc, text (.txt .md .rst .markdown .text) and office (.xlsx .ppt .pptx .odt .ods .odp .odg). It has no entry for.csv,.jsonor.html, andextract_from_fileraisesValueError: Unsupported file typefor them (:285).The GUI upload path handles all three, each deliberately rather than by decoding:
.csvas text,.jsonre-serialised withjson.dumps(..., indent=2)so the structure survives embedding,.htmlthrough_sanitize_html_content(api/knowledge.py:1095-1108).So a CSV dropped in the upload form is ingested, and the same CSV discovered by a connector or a directory walk is rejected.
get_supported_extensions()also drives directory discovery, which is the same class of silent-smaller-ingest theofficeentry's comment describes at:97-101.Acceptance criteria
.csv,.jsonand.htmlare handled byDocumentExtractor.extract_from_file, reusing the GUI path's treatment rather than decoding all three as plain text — JSON keeps its structure, HTML is sanitised, and the sanitiser is the existing one.get_supported_extensions()andis_supported_format()report them, so directory discovery stops skipping them.api/knowledge.pycalls.Related
Umbrella #16772. Counterpart of #16775 (same divergence, opposite direction) and #16784 (a third route that decodes these bytes as text).