Architecture
Ontographia separates what the graph schema allows (ontology) from what the user wants to find (Intent JSON). The Rust core owns parsing, validation, AST construction, and emission. Language bindings are thin wrappers; LLM / agent layers sit outside the core and must never emit raw Cypher.
End-to-end pipeline
flowchart LR
subgraph ingest ["Ingest (adapters)"]
O["Ontology file\n(TTL, YAML, …)"]
A["OntologyAdapter"]
COM["CanonicalOntology (COM)"]
O --> A --> COM
end
subgraph llm ["App / agent layer (out of core)"]
S["intent_json_schema()"]
L["LLM or hand-authored Intent JSON"]
COM --> S --> L
end
subgraph core ["ontographia-core"]
V["validate_intent()"]
B["build_ast()"]
AST["QueryAst"]
E["Emitter (cypher25 / cypher5 / gql)"]
L --> V --> B --> AST --> E
end
E --> OUT["{ query, params }"]
At runtime the public entry point is Engine::build():
pub fn build(&self, intent: Intent, dialect: Dialect) -> Result<EmittedQuery> {
let validated = validate_intent(&self.ontology, intent)?;
let ast = build_ast(&self.ontology, &validated)?;
emit_query(dialect, &ast, &validated.params)
}
Source: crates/ontographia-core/src/engine.rs
Design principles
| Principle | Implication |
|---|---|
| Ontology is source of truth | Valid class, relationship, and property names come only from COM after adapter parsing. |
| LLMs emit Intent, not Cypher | Schema-constrained JSON is validated before any query text is produced. |
| Parameter binding | Filter literals are extracted into params (param_0, param_1, …) and referenced as $param_N in emitted Cypher. |
| Determinism | Same ontology + Intent + dialect → same { query, params }. |
| Single implementation | Extend ontographia-core / ontographia-adapters; do not duplicate logic in bindings. |
Crate layout
| Crate | Responsibility |
|---|---|
ontographia-adapters |
Format detection, OntologyAdapter implementations, load_ontology() |
ontographia-core |
COM, Intent types, validation, QueryAst, emitters, Engine |
ontographia-schema |
COM → expected Neo4j graph schema, constraint DDL, offline catalog diff |
ontographia-ffi |
Stable C ABI for non-Rust callers |
bindings/python |
PyO3 wrapper around Engine |
bindings/go |
cgo wrapper over ontographia-ffi |
Stage 1 — Ontology ingestion
Adapter registry
AdapterRegistry::load() probes adapters in a fixed order (native YAML → LinkML → OBO → JSON-LD → Turtle/OWL → SHACL → SKOS) using detect() heuristics on bytes and optional path hints.
Each adapter implements:
pub trait OntologyAdapter {
fn name(&self) -> &'static str;
fn detect(source: &[u8], path_hint: Option<&str>) -> bool;
fn parse(source: &[u8]) -> Result<CanonicalOntology>
where
Self: Sized;
fn supported_extensions() -> &'static [&'static str];
}
Source: crates/ontographia-adapters/src/registry.rs
Adapters must map their source format into COM. There is no alternate internal representation.
Canonical Ontology Model (COM)
COM is the shared intermediate representation for all formats. JSON Schema: schemas/com.schema.json.
| COM section | Purpose |
|---|---|
classes |
Node labels, optional IRI, super_classes for inheritance |
relationships |
Edge types with optional from_class / to_class domain/range |
properties |
Datatype properties per owner class |
constraints |
SHACL-style hints (min/max count, pattern, …) |
namespaces |
Prefix → IRI map from source ontology |
source |
Provenance (format, uri, version) |
CanonicalOntology exposes resolution helpers used during validation: resolve_class, resolve_relationship, resolve_property (with inheritance via ancestor_classes), and is_subclass_of.
Stage 2 — Intent JSON schema (LLM boundary)
Engine::intent_json_schema() generates a JSON Schema for the Intent struct and injects ontology-specific enums for class and relationship fields so constrained decoding / tool schemas cannot invent schema names.
pub fn intent_json_schema(ontology: &CanonicalOntology) -> serde_json::Value {
let mut schema = serde_json::to_value(schemars::schema_for!(crate::intent::Intent))
.expect("schema serialization");
enrich_with_ontology(&mut schema, ontology);
schema
}
Source: crates/ontographia-core/src/schema_gen.rs
Intent extraction (LLM prompts, retries, repair) lives in the application layer — the optional Python extra ontographia[llm], skills/ontographia-cypher-builder/SKILL.md, thin demos under examples/llm/, and Neo4j walkthrough. The core only defines the shape and validates instances.
Intent structure
| Field | Role |
|---|---|
start |
{ class, alias } — anchor node of the pattern |
traverse |
Ordered relationship hops (relationship, direction, to, optional min_hops / max_hops) |
filter |
Property predicates on bound aliases |
return |
Projections (property, whole node, or aggregate) |
order_by, limit, skip |
Result shaping |
optional |
When true, OPTIONAL MATCH instead of MATCH |
Types: crates/ontographia-core/src/intent.rs
Stage 3 — Validation
validate_intent() checks Intent against COM before AST construction:
- Classes —
start.classand eachtraverse.to.classmust exist in COM. - Relationships — each
traverse.relationshipmust exist;from_class/to_classmust align with the path (subclass-aware). - Aliases — unique across the pattern; filter/return aliases must refer to nodes in the pattern.
- Properties — filter and return properties must exist on the resolved class (inheritance-aware).
- Return — at least one
returnitem required.
On success, filter literal values are copied into ValidatedIntent.params as param_0, param_1, … — never embedded in query strings.
Implementation: crates/ontographia-core/src/validate.rs
Stage 4 — QueryAst
build_ast() maps validated Intent to a dialect-neutral QueryAst:
MatchClause— linear node/relationship chain (onePatternNodetoday)FilterNode— references$param_Nby name, not inline valuesReturnNode— property, node, or aggregate expressionsorder_by,limit,skip
The AST is intentionally close to Cypher/GQL structure but not a string template. Emitters own syntax details (FILTER vs WHERE, CYPHER 25 prefix, etc.).
Types: crates/ontographia-core/src/ast/mod.rs
Stage 5 — Emission
emit_query(dialect, ast, params) dispatches to a QueryEmitter implementation:
| Dialect | Emitter | Notes |
|---|---|---|
cypher25 (default) |
Cypher25Emitter |
CYPHER 25 prefix, FILTER clause — Neo4j 2025.06+ |
cypher5 |
Cypher5Emitter |
Legacy WHERE for older Neo4j |
gql |
GqlEmitter |
GQL-oriented prototype |
Output is always EmittedQuery { query, params }. Filter values appear only in params; the query string contains $param_N placeholders.
Implementation: crates/ontographia-core/src/emit/
Bindings
Bindings call the same Engine API; they do not fork validation or emission. Usage guides:
- Rust — direct
EngineAPI and CLI - Python — PyO3
Engine.load()/build() - Go — C FFI via
ontographia-ffi
Graph schema governance (ontographia-schema)
Ontographia core validates Intent against COM; it does not verify that a live Neo4j database matches the ontology. That boundary is handled by ontographia-schema:
COM → GraphSchema::from_com()
├─ emit_cypher25_constraints() → CREATE CONSTRAINT DDL
└─ diff(schema, GraphSnapshot) → missing/extra labels & rel types
| API | Purpose |
|---|---|
GraphSchema::from_com |
Derive expected node labels, relationship types, and unique properties from COM |
emit_cypher25_constraints |
Generate CREATE CONSTRAINT … IF NOT EXISTS for properties marked unique: true in the ontology |
GraphSnapshot |
Offline JSON catalog (labels, relationship_types) — e.g. examples/neo4j/catalog.snapshot.json |
diff |
Report schema drift between COM-derived expectations and a snapshot |
CLI example:
ontographia schema examples/manufacturing.native.yaml
ontographia schema examples/manufacturing.native.yaml --snapshot examples/neo4j/catalog.snapshot.json
Out of scope (v1): Neo4j live introspection, data seed generation, automatic migration execution. Seed data remains in examples/neo4j/seed.cypher; generated constraints are tested for semantic equivalence.
Mark business-key properties with unique: true in native ontology YAML (see examples/manufacturing.native.yaml).
Roles and guarantees (ontology ↔ Neo4j ↔ executable queries): ontology-graph-alignment.md
What is intentionally outside the core
The core must stay LLM-agnostic and driver-agnostic: same Engine::build for hand-authored Intent, mock fixtures, and live models. Prompt shaping, schema subsetting, execution feedback, and Neo4j sessions are agent / app concerns — they may call intent_json_schema() / build(), but they must not live inside emitters or validation. That keeps bindings thin, lets apps swap LLM backends, and ensures Cypher always comes from deterministic compilation (never from an LLM rewrite). Packaged helpers: ontographia[llm]; thin demos: examples/llm/.
| Concern | Where it lives |
|---|---|
| LLM API calls / Intent extraction | Optional Python extra ontographia.llm (pip install "ontographia[llm]"), agent skills |
| Ontology subset for Intent prompts | ontographia.llm.subset (issue #45) |
| Neo4j driver / execution | examples/run_neo4j_demo.py, run_llm_e2e.py, user applications (not part of the llm extra) |
| Execution → Intent refinement | ontographia.llm + --refine-on-exec (issue #46) |
| Intent-layer evaluation harnesses | examples/llm/eval/ — evaluation.md |
| RDF reasoning / OWL entailment | Out of scope — adapters extract asserted schema only |
| Ontology ↔ Neo4j catalog alignment | ontographia-schema (offline diff + constraint DDL) |
| Graph data seeding | examples/neo4j/seed.cypher |
Extension points
| Change | Steps |
|---|---|
| New ontology format | Implement OntologyAdapter → map to COM → register in registry.rs → add example + integration test |
| New Intent capability | Extend Intent + validation + build_ast + all emitters |
| New dialect | Add QueryEmitter + Dialect variant |
| New language binding | Prefer ontographia-ffi C ABI or direct Rust crate dependency |
| Graph schema / constraints | Extend ontographia-schema (from_com, emit, diff) |
Related docs
- Related work (Text2Cypher / adaptive decoding): related-work.md
- Evaluation baselines (Track A): evaluation.md
- Neo4j walkthrough: end-to-end-neo4j.md
- Ontology ↔ graph alignment: ontology-graph-alignment.md
- Agent routing: AGENTS.md
- COM / native ontology JSON Schemas: repository-layout.md