Download src/knowledge_parsing/contracts.py from DataEyond/Agentic-Service-Data-Eyond-Catalog: direct link, hf CLI and curl.
- Browser
- Download file 17.3 kB
-
https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/main/src/knowledge_parsing/contracts.py
- Command line
-
hf download hf://spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/src/knowledge_parsing/contracts.py
-
curl -L -o contracts.py https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/main/src/knowledge_parsing/contracts.py
17.3 kB
| """Data shapes handed between pipeline stages β the parsing/extraction seam. | |
| Extraction consumes `ParsedDocument`, never a file path and never the parser's | |
| API. That is what keeps the parser swappable: a Tesseract or Azure Document | |
| Intelligence path emits the same artifact, and the extraction half never learns | |
| which one ran. | |
| Design note β these fields were derived from real MinerU output, not designed on | |
| paper. Verified against actual `content_list.json` output and cross-checked | |
| against MinerU's source for both the `pipeline` and `vlm` backends. | |
| Two things worth knowing before reviewing: | |
| 1. `bbox` and `text_level` are emitted by **both** backends β verified in | |
| MinerU's source, where `pipeline` and `vlm` run identical logic: | |
| elif para_type == BlockType.TITLE: | |
| title_level = get_title_level(para_block) | |
| if title_level != 0: | |
| para_content['text_level'] = title_level | |
| So heading structure is read from the document when MinerU detects titles, | |
| and only derived from numbering patterns as a fallback. | |
| 2. Heading availability is **document-dependent, not backend-dependent**. A | |
| document with explicit numbered headings (e.g. the BUMA standard: `2.` / | |
| `2.1.` / `2.1.1.`) yields a clean hierarchy. A document of mid-chapter pages | |
| with no headings yields none β which is why `section_no`, `heading` and | |
| `heading_path` stay Optional rather than required. | |
| β οΈ `Chunk.text` must stay VERBATIM from the source document. The extraction-side | |
| guardrail locates LLM-quoted spans literally inside this text; if the text is | |
| ever cleaned up (lines rejoined, whitespace normalised), the lookup fails and | |
| the field is silently set to null instead of raising. The failure looks like a | |
| bad LLM, but the cause would be here. | |
| """ | |
| from __future__ import annotations | |
| from typing import Literal | |
| from pydantic import BaseModel, Field, computed_field | |
| ChunkKind = Literal["text", "table", "chart", "equation", "image"] | |
| AssetKind = Literal["figure", "chart", "diagram", "table"] | |
| # 0.4.0 β ASSET POINTERS. A figure is no longer its own chunk: its markdown | |
| # reference is folded into the prose chunk at its reading-order position | |
| # and the figure travels as an `Asset` on that chunk. Tables stay their | |
| # own chunk (their linearised text earns its place as evidence) but gain | |
| # an `Asset` and a two-way pointer. Adds `Chunk.assets` and | |
| # `Chunk.referenced_by`; `Chunk.images` is kept as a DEPRECATED alias. | |
| # | |
| # A MINOR bump, not a patch: chunk boundaries change, so a 0.3.x consumer | |
| # reading a 0.4.0 artifact would silently see a different document shape. | |
| # `adapter.check_schema_version` rejects a newer minor, which is the | |
| # designed behaviour β parsing and extraction ship together here. | |
| # 0.3.3 β adds the computed 1-based `page_no` (S6b). Derived from `page_idx`. | |
| # 0.3.2 β adds the optional `source_title`. | |
| # 0.3.1 β adds the optional `normaliser_version`. | |
| # 0.3.x were all patch bumps: purely additive, so every 0.3.0 consumer reads them | |
| # unchanged and the new fields are null on anything written earlier. | |
| SCHEMA_VERSION = "0.4.0" | |
| class Asset(BaseModel): | |
| """A figure, chart, diagram or table referenced by a chunk. Added 0.4.0. | |
| **Why this exists.** Before 0.4.0 an image became its own chunk with an EMPTY | |
| `text`, which made it unreachable by construction: no text means no term | |
| mentions, no mentions means the chunk joins no cluster, and a chunk in no | |
| cluster is never selected as evidence. Measured across three documents: 13 | |
| figures, every one described by a vision model we paid for, and **zero of | |
| them appearing in any cluster's evidence.** Meanwhile the prose that said | |
| "Figure 1.1" had its `` reference stripped, so nothing connected the | |
| sentence to the picture. | |
| The asset carries both halves of the provenance split, and the distinction is | |
| load-bearing: | |
| - `caption` is **verbatim document text** β printed on the page, therefore | |
| quotable as evidence and span-checkable like any other transcription. | |
| - `description` is **model-written** β a claim *about* the picture. It is | |
| never span-checkable and must never be quoted as evidence, which is why it | |
| lives here and not in `Chunk.text`. | |
| """ | |
| # Content-addressed: sha256 of the asset bytes, first 16 hex chars. | |
| # | |
| # NOT the parser's own id. Mistral names images per PAGE (`img-0.jpeg`), which | |
| # already collides in practice β the Open Pit textbook yields 7 distinct names | |
| # for 8 images, so a flat store keyed on the parser's name silently loses one. | |
| # Content addressing also makes a re-parse idempotent and de-duplicates a | |
| # figure that appears in two documents. | |
| asset_id: str | |
| kind: AssetKind | |
| # Where the bytes live. `null` for a table (it has no bytes of its own) and | |
| # for a figure whose persistence failed β writing assets is a never-throw | |
| # seam, so a storage outage degrades to "no key" rather than failing a parse. | |
| storage_key: str | None = None | |
| page_idx: int = 0 | |
| # type: ignore[prop-decorator] | |
| def page_no(self) -> int: | |
| """1-based page number, for a human and for a review UI. Derived from | |
| `page_idx` so the two cannot drift.""" | |
| return self.page_idx + 1 | |
| # `[x0, y0, x1, y1]` of the asset itself, so a UI can highlight the figure. | |
| # Distinct from the host chunk's own `bbox`, which stays where the TEXT is. | |
| bbox: list[int] | None = None | |
| # Verbatim, from the document's own caption block. Quotable as evidence. | |
| caption: str | None = None | |
| # β οΈ MODEL-WRITTEN. See the class docstring. Never evidence. | |
| description: str | None = None | |
| # Which model/feature produced `description`, so a future swap is traceable | |
| # rather than silently changing what the field means. | |
| description_source: str | None = None | |
| class Chunk(BaseModel): | |
| """One unit of a parsed document, ready for the extraction stage.""" | |
| # Provenance key. Format is part of the interface: `f"{doc_id}::{ordinal:04d}"`, | |
| # separator `::`, agreed 2026-08-24. Every candidate entry points back here, so | |
| # the two halves must mint and read the same shape or provenance breaks silently. | |
| chunk_id: str | |
| doc_id: str | |
| kind: ChunkKind | |
| # Content. VERBATIM β never reflowed or normalised. See note above. | |
| # | |
| # Does NOT include the section's heading line; that lives in `heading`. | |
| # Consumers that need both compose them β the extraction half already builds | |
| # its span-check haystack as `heading + "\n" + text` and treats a heading | |
| # that names the term as a mention at offset 0. | |
| text: str | |
| # Location in the source document. | |
| # | |
| # Named `page_idx` deliberately: these are 0-BASED, exactly as the parser | |
| # reports them, with no conversion β so the artifact always matches the raw | |
| # parser output kept alongside it. The 1-based number a human reads is | |
| # `page_no` below: a review queue is about to see these, and an off-by-one | |
| # there is invisible until an expert opens the wrong page, so the conversion | |
| # is done once, here, rather than left to each consumer (S6b, 2026-09-02). | |
| page_idx: int | |
| page_idxs: list[int] = Field(default_factory=list) | |
| # type: ignore[prop-decorator] | |
| def page_no(self) -> int: | |
| """1-based page number, for humans and the review UI. | |
| Derived from `page_idx`, never stored, so it cannot drift from it β and | |
| neither normaliser has to remember to set it. `page_idx` stays 0-based | |
| and raw; this is the single number the reviewer sees. | |
| """ | |
| return self.page_idx + 1 | |
| # Structural context. All Optional β many documents carry no headings. | |
| section_no: str | None = None # e.g. "2.1.3", when the document is numbered | |
| # This chunk's own section title, VERBATIM as the document writes it β | |
| # including wording a reader may be tempted to "fix". The BUMA standard says | |
| # "Physical of Availability (PA)", not "Physical Availability"; that exact | |
| # string must reach the extraction model, or it gets silently normalised and | |
| # the discrepancy never reaches the expert. | |
| heading: str | None = None | |
| # Running headers of every page this chunk spans, in page order. A list | |
| # rather than a single value: a chunk crossing a chapter boundary would | |
| # otherwise silently keep only the first page's chapter. | |
| chapters: list[str] = Field(default_factory=list) | |
| # Breadcrumb of enclosing headings, outermost first, built from MinerU's | |
| # `text_level` hierarchy. Example from the BUMA standard: | |
| # ["2. PENJELASAN PARAMETER", "2.1. Production Parameter", "2.1.1. Production"] | |
| heading_path: list[str] = Field(default_factory=list) | |
| # Flags for cheap filtering downstream | |
| has_formula: bool = False | |
| is_tabular: bool = False | |
| # Trace back to the source: item indices in MinerU's content_list.json | |
| source_items: list[int] = Field(default_factory=list) | |
| # Position on the page of the first source item, as MinerU reports it. | |
| # Carried through for a curation UI that highlights where on the page a | |
| # definition came from. Nothing in the pipeline reasons about it β ordering | |
| # uses item sequence, never coordinates. | |
| bbox: list[int] | None = None | |
| # Figures, charts, diagrams and tables this chunk contains or points at. | |
| # Added 0.4.0 β see the `Asset` docstring for why images stopped being chunks. | |
| # | |
| # A prose chunk that holds a figure carries it here AND carries the figure's | |
| # `` reference inline in `text` at its reading-order | |
| # position, so the model reads the reference exactly where the document put it. | |
| assets: list[Asset] = Field(default_factory=list) | |
| # chunk_ids of OTHER chunks that point at this one. Populated on table chunks, | |
| # which stay standalone (their linearised text earns its place as evidence β | |
| # 74.6% of the Komatsu manual's top-3 evidence is tabular) while still being | |
| # reachable from the prose that introduces them. | |
| referenced_by: list[str] = Field(default_factory=list) | |
| # β οΈ DEPRECATED 0.4.0 β superseded by `assets`. Still populated for one | |
| # version so a 0.3.x reader degrades rather than breaking; it holds the bare | |
| # parser-local filenames, which are NOT unique across a document (see | |
| # `Asset.asset_id`). Read `assets` instead. | |
| images: list[str] = Field(default_factory=list) | |
| # Source markup, kept verbatim beside the rendered prose in `text`. | |
| # | |
| # `text` carries a readable rendering because the term filter is an NER | |
| # model reading prose: MinerU writes formulas character-spaced | |
| # ("P u r c h a s i n g ~ c o s t s") and tables as HTML, and neither | |
| # produces a single mention. Measured on the same document and gold set, | |
| # only the parse differing: raw markup in `text` scored recall 0.7561 against | |
| # 0.8537 for plain text; rendering it back recovered 0.8293. | |
| # | |
| # The markup is not discarded β the formula branch needs exactly this form. | |
| latex: list[str] = Field(default_factory=list) | |
| table_html: str | None = None | |
| # β οΈ MODEL-WRITTEN, NOT FROM THE DOCUMENT. MinerU's `vlm` backend (and | |
| # `hybrid` at effort=high) describes images and charts; that description | |
| # arrives in the item's `content` field. | |
| # | |
| # It is kept OUT of `text` deliberately. `text` is the haystack the | |
| # extraction guardrail searches to prove a quote is verbatim; putting | |
| # generated prose there would let a hallucinated sentence pass the very | |
| # check that exists to catch hallucination. A caption printed in the | |
| # document is verbatim and belongs in `text`; a description invented about | |
| # the picture does not. | |
| # | |
| # Any consumer using this must treat it as a model claim about the image, | |
| # never as evidence β it cannot be span-checked against anything. | |
| generated_description: str | None = None | |
| class ParsedDocument(BaseModel): | |
| """The artifact itself β one parsed document, self-describing. | |
| The chunk list alone is not enough to hand across the seam: an artifact that | |
| travels to the extraction half must carry its own provenance. Without | |
| `parser_name` / `parser_version` / `parser_backend`, a MinerU upgrade and a | |
| prompt change are indistinguishable when extraction results shift. | |
| `content_hash` is the hash of the SOURCE FILE, so re-parsing the same | |
| document is detectable and a changed document forces a new artifact version. | |
| **Metadata is declared before `chunks`, and that ordering is deliberate.** | |
| Pydantic serialises in declaration order, so it decides what a person sees on | |
| opening the file. With `chunks` first, checking which parser or which | |
| normaliser produced an artifact means scrolling past every chunk to the end β | |
| thousands of lines in a real document. That is not hypothetical fussiness: | |
| verifying `parser_config` and `normaliser_version` is now a routine step when | |
| an artifact is handed between the two halves, and burying them is how a stale | |
| artifact went unnoticed for a day. | |
| JSON objects are unordered by specification and every consumer reads by key, | |
| so this changes nothing for code β only for the person reading it. | |
| """ | |
| doc_id: str | |
| # The document's name as a HUMAN reads it. Today this is the file stem and | |
| # therefore equal to `doc_id` β the point is that it is a separate field, so | |
| # the two can diverge without anything having to change here. | |
| # | |
| # `doc_id` is on its way to becoming an identifier rather than a label (see | |
| # the parsing README): every `term_id` / `formula_id` / `rule_id` downstream | |
| # is a digest of it, so it must stop depending on what someone typed into a | |
| # Save dialog. Once it does, `Provenance` β which is what a reviewing expert | |
| # actually reads β would otherwise show them a hash instead of the standard's | |
| # name. This field is what travels alongside it. | |
| # | |
| # Added ahead of that switch because it is needed whichever identifier wins, | |
| # and adding it now costs nothing and breaks nothing. | |
| source_title: str | None = None | |
| # Identity of the source | |
| source_path: str | |
| content_hash: str # sha256 of the source file | |
| n_pages: int | |
| # Which parser produced this, and how | |
| parser_name: str = "mineru" | |
| parser_version: str | None = None # e.g. "3.4.4" | |
| parser_backend: str | None = None # "pipeline" | "vlm" | "hybrid", as MinerU recorded it | |
| parser_config: str | None = None # fingerprint of the settings that affect output | |
| # Fingerprint of the code that turned MinerU's output into these chunks β | |
| # `normalize.py`, `render.py`, `checks.py`, `contracts.py`. OPTIONAL and | |
| # purely informational: nothing downstream has to read it, and an artifact | |
| # written before this field existed simply carries null. | |
| # | |
| # It closes a real gap. `parser_config` fingerprints MinerU's settings, and | |
| # `schema_version` versions the artifact's SHAPE β neither moves when a | |
| # normalisation bug is fixed, because the shape is unchanged. So a stale | |
| # artifact and a correct one declared identical metadata, and the only | |
| # observable difference was the character count. That cost a day on | |
| # 2026-08-26 (see `normalize.normaliser_version`). | |
| normaliser_version: str | None = None | |
| # Version of THIS ARTIFACT for this document β bumped when the document is | |
| # re-parsed (new source content, new parser version, or changed settings). | |
| # Distinct from `schema_version`, which versions the contract itself. | |
| version: int = 1 | |
| schema_version: str = SCHEMA_VERSION | |
| created_at: str | None = None | |
| # Where the untouched MinerU output for this document lives | |
| raw_output_dir: str | None = None | |
| # LAST on purpose β see the class docstring. Everything above answers "what | |
| # is this file?", and a reader should not have to scroll past the payload to | |
| # find out. | |
| chunks: list[Chunk] | |
| # --- Seam for the downstream stages (declared, not yet used) --- | |
| # Written here so the handoff shape is visible from the start. The extraction | |
| # half owns the final form of these two β see `KNOWLEDGE_PIPELINE_TODO.md` Β§5. | |
| class Mention(BaseModel): | |
| """One occurrence of a term inside a chunk.""" | |
| term: str | |
| chunk_id: str | |
| page_idx: int | |
| class TermRecord(BaseModel): | |
| """Extracted knowledge for one term (one cluster of mentions).""" | |
| term: str | |
| full_name: str | None = None | |
| # What the document literally says, before any normalisation β e.g. | |
| # "Physical of Availability (PA)" where `full_name` may read "Physical | |
| # Availability". Span-checked against `Chunk.text` like every other field, | |
| # so the discrepancy surfaces to the expert instead of being quietly fixed. | |
| source_wording: str | None = None | |
| definition: str | None = None | |
| formula_latex: str | None = None | |
| subdomain_tags: list[str] = Field(default_factory=list) | |
| mention_count: int = 0 | |
| provenance: dict = Field(default_factory=dict) | |
| extraction_status: str = "ok" | |