Skip to content

Per-chunk text "breadcrumb" / metadata hook — chunking_func is silently bypassed on /documents/scan & /documents/upload #3603

Description

@geraldtscharre

Summary

There is no working way — through the REST API — to prepend per-chunk metadata (e.g. source filename, path, document as-of date) to the chunk text that the extraction LLM sees. The one documented customization point for per-chunk text, chunking_func, is silently bypassed on the /documents/scan and /documents/upload paths, with no error and no log line. This makes per-chunk provenance/temporal grounding impossible for API-driven ingestion.

The problem (silent bypass)

chunking_func is described in lightrag/lightrag.py as a "Legacy chunking-function customization point," and its docstring is explicit:

"If process_options explicitly contains a chunking selector char (F/R/V/P), the dispatcher routes to a chunker that follows the new file-chunker contract … This chunking_func is NOT called in that case — it is a legacy escape hatch and is intentionally bypassed when the user opted into a specific strategy."

Meanwhile, in lightrag/api/routers/document_routes.py, the scan/upload enqueue path does:

api_process_options = process_options or PROCESS_OPTION_CHUNK_FIXED

So both endpoints always name a strategy (F by default) → chunking_func is never invoked. The only way to reach the no-selector path where chunking_func actually runs is the SDK (ainsert) with an empty/non-chunking process_options — not the HTTP API most deployments use.

Net effect: a user who supplies a chunking_func that, say, prepends a SOURCE / PATH / DATE breadcrumb to every chunk gets 0 of their chunks augmented, silently. The chunks are created, entities extracted, the graph builds — and the customization simply never ran.

Why this matters (use case)

The extraction LLM sees only chunk text, never chunk metadata (file_path, headings, created_at). For multi-document corpora this loses information that matters at extraction time, not query time:

  • Temporal grounding: a document's as-of date (often only present in the filename or a header line) lands in chunk 1 and is lost on chunks 2..N. Entities extracted from later chunks get no date grounding, so the graph can't self-document when a fact was attested.
  • Source provenance: which document a chunk came from — useful for any multi-doc corpus, essential when the same entity is mentioned across many documents with different roles/statuses.
  • Section / table disambiguation: a chunk cut mid-table or mid-section loses the heading that scopes it (e.g. a membership roster whose column headers landed in a different chunk).

A per-chunk prefix ("breadcrumb") carrying SOURCE / PATH / DATE closes all three, because it puts the context into the text the extractor reads. ENABLE_CONTENT_HEADINGS helps at query time but not at extraction time, so it is not a substitute.

What I tried

Supplying a custom chunking_func that prepends the breadcrumb. It works on the SDK ainsert path (no selector → legacy path → chunking_func runs), but is a complete no-op when ingesting via POST /documents/scan or /documents/upload, because those force PROCESS_OPTION_CHUNK_FIXED. There is no process_options value I can send through the API to opt out of all four strategies and reach the legacy path.

Proposed solution

The cleanest general fix is a strategy-agnostic per-chunk transform hook, decoupled from "how to split":

LightRAG(
    ...,
    chunking_func=...,                 # how to split (existing)
    chunk_transform_func=my_transform,  # NEW: decorate every chunk after splitting
)
# my_transform(chunk: dict, doc_meta: dict) -> dict
#   receives one chunk {"content": ..., ...} + {"file_path":..., "doc_date":..., ...}
#   and may rewrite chunk["content"] (e.g. prepend a breadcrumb).
#   runs AFTER the strategy chunker (F/R/V/P) on every chunk, on every ingest path.

Why a new hook rather than extending chunking_func:

As a smaller interim, the F-branch dispatch in lightrag/pipeline.py could delegate to self.chunking_func when it is a non-default callable (identity-gated: is not chunking_by_token_size), via the legacy 6-arg signature — this restores chunking_func on the scan default path without a new API. We have this working locally (with regression tests). It doesn't help R/V/P, but it unblocks the scan default immediately. I'm happy to open a PR for either.

At minimum, the docstring / docs should call out that chunking_func is a no-op on the HTTP scan/upload paths, so users don't spend time wiring a customization that can't run there.

Minimal reproduction

calls = {"n": 0}
def my_chunking_func(tokenizer, content, split_by_character, split_by_character_only, overlap, size):
    calls["n"] += 1
    chunks = chunking_by_token_size(tokenizer, content, split_by_character, split_by_character_only, overlap, size)
    for c in chunks:
        c["content"] = "SOURCE: my-doc\n---\n" + c["content"]
    return chunks

rag = LightRAG(working_dir=..., chunking_func=my_chunking_func, ...)

# SDK path: works — chunks carry "SOURCE: my-doc"
await rag.ainsert(open("doc.md").read())

# HTTP path: my_chunking_func is NEVER called; chunks have no breadcrumb
# POST /documents/scan  →  process_options defaults to PROCESS_OPTION_CHUNK_FIXED
#                         → F-branch bypasses chunking_func → 0 augmented chunks

Environment

  • LightRAG main (as of Aug 2026).
  • Python 3.11, Postgres + Neo4j backends.
  • Ingestion via /documents/scan (a supervisor polling inputs/<workspace>/).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions