17 KiB
Unified Document Model (UDM)
Konzept & Zielsetzung
Das Unified Document Model definiert eine generische, formatunabhängige Baumstruktur, in die jeder Dokumenttyp (PDF, DOCX, PPTX, XLSX, HTML, ZIP) durch einen Extractor überführt wird. Dadurch können AI-Workflows, Nodes und Tools mit einem einzigen Objektmodell arbeiten – unabhängig vom Quellformat.
Designprinzipien
- 3-Ebenen-Garantie: Jedes Dokument hat exakt drei Verschachtelungsebenen (ausgenommen ZIP als Meta-Container).
- Einheitliche Blattknoten: Alle atomaren Inhalte sind
ContentBlock-Objekte mit identischer Attributstruktur. - Generische Traversierung: Workflow-Nodes (Loop, Filter, Transform, Map) arbeiten formatunabhängig über dieselbe Baumstruktur.
- Keine formatspezifischen Zwischenschichten: Konzepte wie "Paragraph", "Row" oder "Cell" werden in den
ContentBlockabsorbiert, nicht als eigene Ebenen modelliert.
Architekturübersicht
┌─────────────────────────────────────────────────────┐
│ Level 1 — Document │
│ ┌───────────────────────────────────────────────┐ │
│ │ Level 2 — StructuralNode │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Level 3 — ContentBlock (Blattknoten) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Ebenen im Detail
| Ebene | Typ | Beschreibung |
|---|---|---|
| Level 1 | Document |
Wurzelknoten. Repräsentiert ein einzelnes Quelldokument. |
| Level 2 | StructuralNode |
Strukturelle Gliederungseinheit: Seite, Abschnitt, Slide oder Sheet. |
| Level 3 | ContentBlock |
Atomarer Inhalt: Text, Bild, Tabelle, Code, Medien, Link oder Formel. |
Datenmodell
Document (Level 1)
Der Wurzelknoten pro Quelldatei.
interface Document {
id: string;
role: "document";
source_type: "pdf" | "docx" | "pptx" | "xlsx" | "html";
source_path: string;
metadata: Metadata;
children: StructuralNode[];
}
| Feld | Typ | Beschreibung |
|---|---|---|
id |
string |
Eindeutige ID (UUID) |
role |
"document" |
Immer "document" auf Level 1 |
source_type |
string |
Originalformat der Quelldatei |
source_path |
string |
Pfad der Originaldatei (relativ zum Workspace oder Archiv) |
metadata |
Metadata |
Dokument-Metadaten |
children |
StructuralNode[] |
Liste der strukturellen Einheiten |
StructuralNode (Level 2)
Die Gliederungseinheit innerhalb eines Dokuments.
interface StructuralNode {
id: string;
role: "page" | "section" | "slide" | "sheet";
index: number;
label: string | null;
metadata: Metadata;
children: ContentBlock[];
}
| Feld | Typ | Beschreibung |
|---|---|---|
id |
string |
Eindeutige ID |
role |
string |
Art der Struktureinheit (formatabhängig, aber aus fester Menge) |
index |
number |
0-basierte Position innerhalb des Dokuments |
label |
string? |
Optionaler Name (Sheet-Name, Abschnittsüberschrift, Slide-Titel) |
metadata |
Metadata |
Zusätzliche Informationen zur Struktureinheit |
children |
ContentBlock[] |
Liste der atomaren Inhalte |
Rollen-Zuordnung pro Format
| Quellformat | role |
Entspricht im Original |
|---|---|---|
page |
Seite | |
| DOCX | section |
Abschnitt (Heading-basierte Gliederung) |
| PPTX | slide |
Folie |
| XLSX | sheet |
Tabellenblatt |
| HTML | section |
Semantischer Bereich (<header>, <main>, <nav>, <footer>, <aside>) |
ContentBlock (Level 3)
Der atomare Inhaltsknoten. Alle Formate erzeugen identisch strukturierte ContentBlocks.
interface ContentBlock {
id: string;
content_type: "text" | "image" | "table" | "code" | "media" | "link" | "formula";
raw: string;
mime_type: string | null;
language: string | null;
attributes: Record<string, any>;
position: Position;
metadata: Metadata;
}
| Feld | Typ | Beschreibung |
|---|---|---|
id |
string |
Eindeutige ID |
content_type |
string |
Art des Inhalts (aus fester Menge) |
raw |
string |
Rohinhalt: Plaintext, Base64-kodierte Binärdaten oder JSON-serialisierte Struktur |
mime_type |
string? |
MIME-Type (text/plain, image/png, text/html, application/json, …) |
language |
string? |
Programmiersprache bei code-Blocks (python, sql, javascript, …) |
attributes |
Record |
Zusätzliche Eigenschaften (Styling, Alt-Text, Grösse, …) |
position |
Position |
Lokalisierung innerhalb der Struktureinheit |
metadata |
Metadata |
Block-spezifische Metadaten |
Content-Typen im Detail
content_type |
raw enthält |
mime_type Beispiel |
Typische attributes |
|---|---|---|---|
text |
Plaintext-Inhalt | text/plain |
{ style, heading_level, list_type, bold, italic } |
image |
Base64-kodierte Bilddaten | image/png, image/jpeg |
{ width, height, alt_text, caption } |
table |
JSON-Matrix { headers: [...], rows: [[...], ...] } |
application/json |
{ row_count, col_count, has_header, name } |
code |
Quellcode als Text | text/plain |
{ language, line_count, executable } |
media |
Base64-kodierte Daten oder URI | audio/mp3, video/mp4 |
{ duration, format, embedded } |
link |
URL als Text | text/uri-list |
{ display_text, target, rel } |
formula |
Formelausdruck (LaTeX, Excel-Syntax) | text/plain |
{ notation, result_value, result_type } |
Position
Lokalisiert einen ContentBlock innerhalb seiner Struktureinheit.
interface Position {
index: number;
page: number | null;
row: number | null;
col: number | null;
bbox: BoundingBox | null;
}
interface BoundingBox {
x: number;
y: number;
width: number;
height: number;
unit: "px" | "pt" | "mm";
}
| Feld | Typ | Beschreibung |
|---|---|---|
index |
number |
0-basierte Reihenfolge innerhalb der Struktureinheit |
page |
number? |
Seitennummer (relevant für PDF) |
row |
number? |
Zeilenposition (relevant für tabellarische Daten) |
col |
number? |
Spaltenposition (relevant für tabellarische Daten) |
bbox |
BoundingBox? |
Bounding Box für visuell positionierte Inhalte (PDF, PPTX) |
Metadata
Einheitliches Metadaten-Objekt, verwendbar auf allen drei Ebenen.
interface Metadata {
title: string | null;
author: string | null;
created_at: string | null; // ISO 8601
modified_at: string | null; // ISO 8601
source_path: string;
tags: string[];
custom: Record<string, any>;
}
Archive (Sonderfall ZIP)
ZIP-Dateien fungieren als Meta-Container und erzeugen eine zusätzliche Wrapper-Ebene. Jede enthaltene Datei wird als eigenständiges Document extrahiert.
interface Archive {
id: string;
role: "archive";
source_type: "zip" | "tar" | "gz";
source_path: string;
metadata: Metadata;
children: (Archive | Document)[];
}
Die 3-Ebenen-Garantie gilt pro Dokument innerhalb des Archivs. Das Archiv selbst ist eine Hülle.
Archive (ZIP)
├── Document (PDF)
│ ├── StructuralNode (page 0)
│ │ ├── ContentBlock (text)
│ │ └── ContentBlock (image)
│ └── StructuralNode (page 1)
│ └── ContentBlock (text)
├── Document (DOCX)
│ └── StructuralNode (section 0)
│ ├── ContentBlock (text)
│ └── ContentBlock (table)
└── Archive (nested ZIP)
└── Document (…)
Format-Mapping-Referenz
Übersicht, wie jedes Quellformat in das 3-Ebenen-Modell abgebildet wird.
PDF → UDM
PDF-Datei
├── Document (source_type: "pdf")
│ ├── StructuralNode (role: "page", index: 0)
│ │ ├── ContentBlock (text) ← Textblöcke der Seite
│ │ ├── ContentBlock (image) ← Eingebettete Bilder
│ │ └── ContentBlock (table) ← Erkannte Tabellen
│ ├── StructuralNode (role: "page", index: 1)
│ │ └── ...
- Jede PDF-Seite wird ein
StructuralNode(page). - Text wird als zusammenhängende Blöcke extrahiert, nicht zeilenweise.
- Bilder werden als Base64 in
rawgespeichert. - Tabellen werden als JSON-Matrix serialisiert.
bboxinPositionenthält die visuelle Position auf der Seite.
DOCX → UDM
DOCX-Datei
├── Document (source_type: "docx")
│ ├── StructuralNode (role: "section", label: "Einleitung")
│ │ ├── ContentBlock (text) ← Absätze
│ │ ├── ContentBlock (image) ← Inline-Bilder
│ │ └── ContentBlock (table) ← Word-Tabellen
│ ├── StructuralNode (role: "section", label: "Methodik")
│ │ └── ...
- Sections werden anhand von Heading-Ebenen gegliedert (H1 → neue Section).
- Absätze (Paragraphs) werden direkt zu
ContentBlock(text)– keine Zwischenebene. - Styling-Informationen (bold, italic, heading_level) landen in
attributes. - Tabellen werden als JSON-Matrix in
rawserialisiert.
PPTX → UDM
PPTX-Datei
├── Document (source_type: "pptx")
│ ├── StructuralNode (role: "slide", index: 0, label: "Titelfolie")
│ │ ├── ContentBlock (text) ← Textboxen
│ │ ├── ContentBlock (image) ← Bilder/Grafiken
│ │ └── ContentBlock (table) ← Slide-Tabellen
│ ├── StructuralNode (role: "slide", index: 1)
│ │ └── ...
- Jede Folie wird ein
StructuralNode(slide). - Textboxen werden zu
ContentBlock(text)mitbboxfür die Position. - Speaker Notes landen als
ContentBlock(text)mitattributes.note: true.
XLSX → UDM
XLSX-Datei
├── Document (source_type: "xlsx")
│ ├── StructuralNode (role: "sheet", index: 0, label: "Umsatz 2024")
│ │ ├── ContentBlock (table) ← Tabellenbereich A
│ │ └── ContentBlock (table) ← Tabellenbereich B (falls disjunkt)
│ ├── StructuralNode (role: "sheet", index: 1, label: "Kosten")
│ │ └── ContentBlock (table)
- Jedes Sheet wird ein
StructuralNode(sheet). - Das gesamte Daten-Grid eines Sheets wird als ein
ContentBlock(table)serialisiert. - Bei mehreren disjunkten Tabellenbereichen im selben Sheet → mehrere ContentBlocks.
- Formeln werden als
ContentBlock(formula)extrahiert, wenn gewünscht.
HTML → UDM
HTML-Datei
├── Document (source_type: "html")
│ ├── StructuralNode (role: "section", label: "header")
│ │ └── ContentBlock (text)
│ ├── StructuralNode (role: "section", label: "nav")
│ │ └── ContentBlock (link)
│ ├── StructuralNode (role: "section", label: "main")
│ │ ├── ContentBlock (text)
│ │ ├── ContentBlock (image)
│ │ └── ContentBlock (table)
│ ├── StructuralNode (role: "section", label: "footer")
│ │ └── ContentBlock (text)
- Semantische HTML5-Elemente (
<header>,<main>,<nav>,<footer>,<aside>) werden zu Sections. - Falls keine semantischen Elemente vorhanden: gesamter
<body>als eine Section. - HTML-Inhalte werden in Plaintext konvertiert, nicht als HTML-Markup gespeichert.
Workflow-Integration
Generische Traversierung
Da alle Formate dieselbe Struktur haben, funktioniert ein einzelner rekursiver Walker für alle Dokumenttypen:
def walk_content_blocks(document: Document) -> Iterator[ContentBlock]:
"""Iteriert über alle ContentBlocks eines Dokuments, formatunabhängig."""
for structural_node in document.children:
for block in structural_node.children:
yield block
Filter-Node
def filter_by_type(document: Document, content_type: str) -> list[ContentBlock]:
"""Filtert alle ContentBlocks nach Typ (z.B. 'image', 'table')."""
return [
block for block in walk_content_blocks(document)
if block.content_type == content_type
]
Loop-Node
def process_all_documents(archive: Archive):
"""Verarbeitet alle Dokumente in einem Archiv mit identischer Logik."""
for document in archive.children:
if isinstance(document, Archive):
process_all_documents(document) # rekursiv für verschachtelte ZIPs
else:
for block in walk_content_blocks(document):
# Identische Verarbeitung, egal ob PDF, DOCX, PPTX, ...
transform(block)
Map-Node
def map_blocks(document: Document, fn: Callable[[ContentBlock], T]) -> list[T]:
"""Wendet eine Funktion auf jeden ContentBlock an."""
return [fn(block) for block in walk_content_blocks(document)]
Beispiel: Alle Bilder aus beliebigem Dokument extrahieren
images = filter_by_type(document, "image")
for img in images:
save_image(
data=base64_decode(img.raw),
filename=f"{img.id}.{img.mime_type.split('/')[1]}",
alt_text=img.attributes.get("alt_text", "")
)
Beispiel: Alle Tabellen als CSV exportieren
tables = filter_by_type(document, "table")
for table in tables:
data = json.loads(table.raw)
write_csv(
headers=data["headers"],
rows=data["rows"],
filename=f"{table.id}.csv"
)
JSON-Beispiel
Vollständiges Beispiel eines extrahierten PDF-Dokuments:
{
"id": "doc-a1b2c3",
"role": "document",
"source_type": "pdf",
"source_path": "reports/quarterly-report-q3.pdf",
"metadata": {
"title": "Quarterly Report Q3 2025",
"author": "Finance Team",
"created_at": "2025-10-01T08:00:00Z",
"modified_at": "2025-10-15T14:30:00Z",
"source_path": "reports/quarterly-report-q3.pdf",
"tags": ["finance", "quarterly"],
"custom": {}
},
"children": [
{
"id": "sn-page-0",
"role": "page",
"index": 0,
"label": null,
"metadata": {
"title": null,
"author": null,
"created_at": null,
"modified_at": null,
"source_path": "reports/quarterly-report-q3.pdf#page=1",
"tags": [],
"custom": {}
},
"children": [
{
"id": "cb-001",
"content_type": "text",
"raw": "Quarterly Report Q3 2025\n\nThis report summarizes the financial performance...",
"mime_type": "text/plain",
"language": null,
"attributes": {
"heading_level": 1,
"style": "title"
},
"position": {
"index": 0,
"page": 1,
"row": null,
"col": null,
"bbox": { "x": 50, "y": 30, "width": 500, "height": 40, "unit": "pt" }
},
"metadata": {
"title": null,
"author": null,
"created_at": null,
"modified_at": null,
"source_path": "reports/quarterly-report-q3.pdf#page=1",
"tags": [],
"custom": {}
}
},
{
"id": "cb-002",
"content_type": "table",
"raw": "{\"headers\":[\"Metric\",\"Q2\",\"Q3\",\"Delta\"],\"rows\":[[\"Revenue\",\"1.2M\",\"1.5M\",\"+25%\"],[\"Costs\",\"800K\",\"850K\",\"+6%\"]]}",
"mime_type": "application/json",
"language": null,
"attributes": {
"row_count": 2,
"col_count": 4,
"has_header": true,
"name": "Financial Overview"
},
"position": {
"index": 1,
"page": 1,
"row": null,
"col": null,
"bbox": { "x": 50, "y": 200, "width": 500, "height": 120, "unit": "pt" }
},
"metadata": {
"title": null,
"author": null,
"created_at": null,
"modified_at": null,
"source_path": "reports/quarterly-report-q3.pdf#page=1",
"tags": [],
"custom": {}
}
}
]
}
]
}
Zusammenfassung
| Eigenschaft | Wert |
|---|---|
| Ebenen | Exakt 3 pro Dokument (Archive als optionaler Wrapper) |
| Struktureinheiten | page, section, slide, sheet |
| Content-Typen | text, image, table, code, media, link, formula |
| Formate | PDF, DOCX, PPTX, XLSX, HTML (erweiterbar) |
| Traversierung | Ein generischer Walker für alle Formate |
| Serialisierung | JSON-kompatibel, sofort einsetzbar in Workflow-Engines |