Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions containers/kr0ki-docling/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# kr0ki-docling — SPIKE (not wired into kr0ki-server)

Throwaway-grade groundwork for the Docling sidecar (option C in issue #97): document → structure (Docling) → knowledge graph
(docling-graph, local Qwen3.8) → PROV-O. **What exists:** the extraction template and its tests. **What does not:** the HTTP
wrapper, the Containerfile, the docling-mcp entrypoint, the PROV-O mapping, kr0ki-server integration.

## Findings (2026-10-07, all measured; details in `docs/evaluations/EVAL-graalvm-vs-jvm-kroki.md` §Related measurements)

- Docling reads PPTX, including speaker notes (own `notes` layer); 67 slides in 0.9 s, 440 MiB peak. The 43-page PDF takes 730 s and
3.0 GiB on CPU-only torch; fewer threads cost time, not memory. docling-mcp needs `DOCLING_MCP_CONVERSION_MODE=local` and only
returns a document key from its `conversion` tools (no content-returning tool), so the product path should be a stateless HTTP wrapper.
- **docling-graph's "0 edges" cause:** `GraphConverter._create_edges_pass` returns an empty list at once when the template's
ROOT class has `is_entity=False`. The model output was fine. Same output: 36 nodes / 0 edges vs 37 nodes / 67 edges once the root is an
entity. Live (Qwen3.8, dense contract): 28 nodes / 53 edges. Pinned by `tests/test_template_edges.py`.
- LiteLLM needs the model as `openai/<name>` for an OpenAI-compatible server. Install CPU-only torch to keep the venv small
(1.6 GB) and keep Docling off the GPU that the 27B model fills.

## Test

```bash
# venv with docling + docling-graph (CPU torch): see the install line in the eval doc
python -m unittest discover -s tests
```
Empty file.
Empty file.
64 changes: 64 additions & 0 deletions containers/kr0ki-docling/kr0ki_docling/templates/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""The small core vocabulary for ad hoc document knowledge (docling-graph extraction template).

Deliberately generic: no domain classes. `Entity`/`Concept` are things the document talks about, a `Claim` is something it says
about them. Provenance is not modelled here: docling-graph records source chunk, page and bbox on every node, and kr0ki maps
that to PROV-O. Per the docling-graph template rules, validators normalize and never reject, and only identity fields are required.
"""
from typing import Any, List, Optional

from pydantic import BaseModel, ConfigDict, Field


def edge(label: str, reference: bool = False, **kwargs: Any) -> Any:
"""Declare a field as a graph edge (docling-graph reads `edge_label` from json_schema_extra).

`reference=True` marks an identity-only link to an entity described in full elsewhere (`graph_reference`): the parent's own
fill call supplies it, which is what keeps edges from coming back empty on a small local model.
"""
extra = dict(kwargs.pop("json_schema_extra", {}) or {})
extra["edge_label"] = label
if reference:
extra["graph_reference"] = True
return Field(json_schema_extra=extra, **kwargs)


class Concept(BaseModel):
"""An idea, topic, term or category the document discusses (e.g. "ownership", "garbage collection")."""

model_config = ConfigDict(graph_id_fields=["name"])
name: str = Field(description="Short canonical name of the concept, as the document names it.")
definition: Optional[str] = Field(default=None, description="One sentence definition, only if the document gives one.")
broader: List["Concept"] = edge("BROADER", reference=True, default_factory=list, description="More general concepts this one is a kind of.")


class Entity(BaseModel):
"""A named thing: a person, organisation, product, system, tool, standard, place or document."""

model_config = ConfigDict(graph_id_fields=["name"])
name: str = Field(description="The name exactly as written in the document.")
kind: Optional[str] = Field(default=None, description="Free-text type, e.g. 'language', 'person', 'standard'.")
related_to: List["Entity"] = edge("RELATED_TO", reference=True, default_factory=list, description="Other entities this one is explicitly related to.")


class Claim(BaseModel):
"""A single assertion the document makes, stated in one sentence and kept close to the source wording."""

model_config = ConfigDict(graph_id_fields=["statement"])
statement: str = Field(description="The assertion as one self-contained sentence.")
mentions: List[Entity] = edge("MENTIONS", reference=True, default_factory=list, description="Entities the claim is about.")
about: List[Concept] = edge("ABOUT", reference=True, default_factory=list, description="Concepts the claim is about.")


class DocumentKnowledge(BaseModel):
"""Root: everything worth keeping from the document as entities, concepts and claims.

The root MUST be an entity (the default). docling-graph starts its edge walk at the root and returns at once when the root is
a component (`is_entity=False`), so every edge below it is silently dropped: that gave "N nodes, 0 edges" with a model output
that did contain the relations (verified 2026-10-07: same output, 36 nodes / 0 edges vs 37 nodes / 67 edges).
"""

model_config = ConfigDict(graph_id_fields=["title"])
title: str = Field(description="The document's title; if it has none, a short descriptive title taken from its first heading.")
claims: List[Claim] = edge("HAS_CLAIM", default_factory=list, description="The assertions the document makes.")
entities: List[Entity] = edge("HAS_ENTITY", default_factory=list, description="Named things the document mentions.")
concepts: List[Concept] = edge("HAS_CONCEPT", default_factory=list, description="Ideas and topics the document discusses.")
309 changes: 309 additions & 0 deletions containers/kr0ki-docling/tests/fixtures/assembled_root.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
{
"title": "Considering Rust",
"claims": [
{
"statement": "Rust is much faster.",
"mentions": [
{
"name": "Rust"
}
],
"about": [
{
"name": "performance"
}
]
},
{
"statement": "Rust has much lower memory use",
"mentions": [
{
"name": "Rust"
}
],
"about": [
{
"name": "memory use"
}
]
},
{
"statement": "Rust supports multi-threading.",
"mentions": [
{
"name": "Rust"
}
],
"about": [
{
"name": "multi-threading"
}
]
},
{
"statement": "Rust has algebraic data types",
"mentions": [
{
"name": "Rust"
}
],
"about": [
{
"name": "algebraic data types"
}
]
},
{
"statement": "Rust has pattern matching",
"mentions": [
{
"name": "Rust"
}
],
"about": [
{
"name": "pattern matching"
}
]
},
{
"statement": "Rust has comprehensive static typing",
"mentions": [
{
"name": "Rust"
}
],
"about": [
{
"name": "static typing"
}
]
},
{
"statement": "Rust has many fewer runtime crashes",
"mentions": [
{
"name": "Rust"
}
],
"about": [
{
"name": "runtime crashes"
}
]
}
],
"entities": [
{
"name": "Jon Gjengset",
"kind": "person",
"related_to": [
{
"name": "MIT",
"kind": "organisation"
},
{
"name": "Parallel and Distributed Operating Systems group",
"kind": "organisation"
},
{
"name": "Noria",
"kind": "product"
},
{
"name": "Rust",
"kind": "language"
},
{
"name": "C",
"kind": "language"
},
{
"name": "C++",
"kind": "language"
},
{
"name": "Go",
"kind": "language"
},
{
"name": "Python",
"kind": "language"
},
{
"name": "Java",
"kind": "language"
}
]
},
{
"name": "MIT",
"kind": "organisation",
"related_to": [
{
"name": "Parallel and Distributed Operating Systems group",
"kind": "organisation"
}
]
},
{
"name": "Parallel and Distributed Operating Systems group",
"kind": "research group",
"related_to": [
{
"name": "MIT",
"kind": "university"
},
{
"name": "Jon Gjengset",
"kind": "person"
}
]
},
{
"name": "Noria",
"kind": "database",
"related_to": [
{
"name": "Rust"
}
]
},
{
"name": "Rust",
"kind": "language",
"related_to": [
{
"name": "Mozilla"
},
{
"name": "Java"
},
{
"name": "C++"
},
{
"name": "Python"
},
{
"name": "Go"
},
{
"name": "C"
}
]
},
{
"name": "Rust std",
"kind": "standard library",
"related_to": [
{
"name": "Rust",
"kind": "language"
}
]
},
{
"name": "C",
"kind": "language",
"related_to": []
},
{
"name": "C++",
"kind": "language",
"related_to": []
},
{
"name": "Go",
"kind": "language",
"related_to": []
},
{
"name": "Python",
"kind": "language",
"related_to": []
},
{
"name": "Java",
"kind": "language",
"related_to": []
},
{
"name": "Mozilla",
"kind": "organisation",
"related_to": []
}
],
"concepts": [
{
"name": "systems programming",
"definition": null,
"broader": []
},
{
"name": "fearless concurrency",
"definition": null,
"broader": []
},
{
"name": "compiled language",
"definition": "A language that is compiled to machine code rather than bytecode.",
"broader": []
},
{
"name": "machine code",
"definition": null,
"broader": []
},
{
"name": "strong, static typing",
"definition": null,
"broader": []
},
{
"name": "imperative",
"definition": null,
"broader": []
},
{
"name": "functional aspects",
"definition": null,
"broader": []
},
{
"name": "garbage collection",
"definition": null,
"broader": []
},
{
"name": "runtime",
"definition": null,
"broader": []
},
{
"name": "type system",
"definition": null,
"broader": []
},
{
"name": "multi-threading",
"definition": null,
"broader": []
},
{
"name": "algebraic data types",
"definition": null,
"broader": []
},
{
"name": "pattern matching",
"definition": null,
"broader": []
},
{
"name": "runtime crashes",
"definition": null,
"broader": []
}
]
}
Loading
Loading