# How the MiroFish Ontology Generator Defines and Manages Entity and Edge Types in the Graph

> Learn how the MiroFish ontology generator defines and manages graph entity and edge types by converting simulation documents into a structured knowledge-graph schema using LLM generated JSON and Zep-compatible Python classes.

- Repository: [BaiFu/mirofish](https://github.com/666ghj/mirofish)
- Tags: deep-dive
- Published: 2026-02-23

---

**The MiroFish ontology generator converts raw simulation documents into a structured knowledge-graph schema by prompting an LLM to produce JSON definitions for entity and edge types, validating those definitions against strict capacity limits, injecting mandatory fallback types, and finally emitting Zep-compatible Python classes.**

The ontology generation service sits at the heart of the MiroFish platform, transforming unstructured text into a formal graph schema that drives simulations. This article examines how the `OntologyGenerator` class in [`backend/app/services/ontology_generator.py`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/ontology_generator.py) defines entity and edge types, enforces structural constraints, and manages type hierarchies through a three-stage pipeline.

## Ontology Generation Pipeline

The generation workflow follows a deterministic sequence from prompt construction to executable code output.

### Stage 1: Prompt Construction

The process begins with the `ONTOLOGY_SYSTEM_PROMPT` constant defined at lines 11-55 of [`backend/app/services/ontology_generator.py`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/ontology_generator.py). This deterministic prompt instructs the LLM to return a JSON object containing three top-level keys: `entity_types`, `edge_types`, and `analysis_summary`.

The prompt enforces strict schema rules:
- Exactly 10 entity types must be defined
- Two fallback entity types (`Person` and `Organization`) must be included
- Descriptions are capped at 100 characters
- Each entity requires `name`, `description`, optional `attributes`, and optional `examples`

### Stage 2: LLM Validation and Post-Processing

The `generate()` method (lines 57-85) orchestrates the LLM call via `LLMClient.chat_json`, passing the constructed prompt along with the user-provided `document_texts`, `simulation_requirement`, and `additional_context`.

Raw LLM output flows into `_validate_and_process()` (lines 87-104), which performs critical normalization:
- Guarantees required top-level keys exist in the JSON
- Initializes missing `attributes` and `examples` fields with empty lists
- Truncates descriptions exceeding length limits
- Enforces hard limits of **10 entity types** and **10 edge types** via truncation

**Fallback Injection:** If the mandatory `Person` or `Organization` types are missing, the generator injects them with predefined attributes—`full_name` and `role` for Person, `org_name` and `org_type` for Organization. When capacity is reached, the system evicts the least-important custom types to accommodate these fallbacks.

### Stage 3: Python Code Generation

The `generate_python_code()` method (lines 106-149) transforms the validated ontology JSON into executable Python modules. This stage creates:
- **Entity Classes:** Zep-compatible `EntityModel` subclasses for each entity type (lines 71-89)
- **Edge Classes:** `EdgeModel` subclasses defining relationships with `source_targets` constraints (lines 94-117)
- **Lookup Dictionaries:** `ENTITY_TYPES`, `EDGE_TYPES`, and `EDGE_SOURCE_TARGETS` mappings for runtime type resolution (lines 123-149)

The generated code imports base classes from `zep_cloud.external_clients.ontology`, ensuring compatibility with the Zep graph database API.

## Entity Type Management Strategy

The ontology generator implements a rigorous entity type governance model to ensure graph consistency.

### Structural Requirements

Each entity type definition must conform to a strict schema:
- **Name:** Unique identifier string
- **Description:** Maximum 100-character explanation of the entity's semantic meaning
- **Attributes:** Optional list of objects containing `name`, `type`, and `description`
- **Examples:** Optional list of string instances illustrating the entity type

### Mandatory Fallback Types

The system guarantees two universal entity types exist in every ontology:
- **Person:** Automatically injected with attributes `full_name` (string) and `role` (string) if missing from LLM output
- **Organization:** Automatically injected with attributes `org_name` (string) and `org_type` (string) if missing

These fallbacks ensure that simulations always have basic actor types available for relationship modeling.

### Capacity Enforcement

The `MAX_ENTITY_TYPES = 10` constant creates a hard ceiling on ontology complexity. When fallback injection would exceed this limit, the `_validate_and_process()` method truncates the existing entity list, removing the least-important custom types (those at the end of the array) before appending the mandatory fallbacks.

## Edge Type Management Strategy

Edge types follow parallel governance rules with relationship-specific constraints.

### Relationship Structure

Each edge type requires:
- **Name:** Unique relationship identifier
- **Description:** Semantic explanation of the relationship (max 100 characters)
- **Source Targets:** List of valid source-target entity pairs (`[{source: "EntityA", target: "EntityB"}]`) defining which entity types may connect via this edge
- **Attributes:** Optional relationship properties (name, type, description)

### Validation and Limits

The `_validate_and_process()` method applies identical normalization to edge types as it does to entities: missing fields receive default empty values, descriptions undergo truncation, and the `MAX_EDGE_TYPES = 10` limit is strictly enforced through list truncation.

### Schema Constraints

The `source_targets` field provides the critical constraint that prevents invalid graph connections. During code generation, these constraints translate into the `EDGE_SOURCE_TARGETS` dictionary, which the simulation engine consults before instantiating relationships.

## Integration with Zep Graph Database

The generated ontology achieves runtime utility through tight integration with the Zep cloud platform. The `generate_python_code()` method imports `EntityModel`, `EntityText`, and `EdgeModel` from `zep_cloud.external_clients.ontology`, creating subclasses that Zep's graph API recognizes natively.

This integration allows the [`graph_builder.py`](https://github.com/666ghj/mirofish/blob/main/graph_builder.py) service (located at [`backend/app/services/graph_builder.py`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/graph_builder.py)) to import the generated ontology module and instantiate nodes and edges that comply with the defined schema, ensuring type safety throughout the simulation lifecycle.

## Practical Implementation Examples

### Generating an Ontology from Simulation Documents

```python
from backend.app.services.ontology_generator import OntologyGenerator

# Sample simulation inputs

documents = [
    "The protest was led by student groups at University X...",
    "CEO of TechCorp announced a new product..."
]
simulation_req = "Model the spread of information about the new product and the protest."
additional = "Focus on interactions between media outlets and public figures."

# Initialize generator and produce ontology JSON

generator = OntologyGenerator()
ontology_json = generator.generate(
    document_texts=documents,
    simulation_requirement=simulation_req,
    additional_context=additional
)

# Access defined types

print(ontology_json["entity_types"])
print(ontology_json["edge_types"])

```

The `generate()` method constructs the user message (lines 28-53 in the source), invokes `LLMClient.chat_json`, and normalizes output through `_validate_and_process()`.

### Converting Ontology to Executable Python Code

```python

# Generate Zep-compatible Python module

python_code = generator.generate_python_code(ontology_json)

# Persist for simulation engine consumption

with open("custom_ontology.py", "w", encoding="utf-8") as f:
    f.write(python_code)

# Runtime usage in graph builder

# from custom_ontology import Person, Organization, ENTITY_TYPES, EDGE_TYPES

```

This code generation loop produces entity class definitions (lines 71-89), edge class definitions (lines 94-117), and lookup dictionaries (`ENTITY_TYPES`, `EDGE_TYPES`, `EDGE_SOURCE_TARGETS` at lines 123-149).

## Key Source Files and Architecture

| File | Role | Location |
|------|------|----------|
| [`backend/app/services/ontology_generator.py`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/ontology_generator.py) | Core service implementing prompt construction, LLM orchestration, validation with fallback injection, and Python code generation. | [ontology_generator.py](https://github.com/666ghj/mirofish/blob/main/backend/app/services/ontology_generator.py) |
| [`backend/app/utils/llm_client.py`](https://github.com/666ghj/mirofish/blob/main/backend/app/utils/llm_client.py) | Utility wrapper handling LLM provider communication; provides `chat_json` method used by the ontology generator. | [llm_client.py](https://github.com/666ghj/mirofish/blob/main/backend/app/utils/llm_client.py) |
| [`backend/app/services/graph_builder.py`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/graph_builder.py) | Consumer of generated ontology code; instantiates Zep-compatible nodes and edges during simulation execution. | [graph_builder.py](https://github.com/666ghj/mirofish/blob/main/backend/app/services/graph_builder.py) |

These components form the end-to-end pipeline that transforms unstructured simulation requirements into type-safe graph schemas for the MiroFish platform.

## Summary

- The **OntologyGenerator** service converts raw documents into structured graph schemas through a three-stage pipeline: prompt construction, LLM validation with post-processing, and Python code generation.
- **Entity types** are strictly governed by a maximum limit of 10 types, with mandatory fallback injection for `Person` and `Organization` entities that include predefined attributes like `full_name` and `org_name`.
- **Edge types** support relationship constraints through `source_targets` definitions that specify valid source-target entity pairs, enforcing schema integrity at the type level.
- The system generates **Zep-compatible Python classes** (`EntityModel` and `EdgeModel` subclasses) along with lookup dictionaries (`ENTITY_TYPES`, `EDGE_TYPES`, `EDGE_SOURCE_TARGETS`) for runtime graph construction.
- All validation logic resides in [`backend/app/services/ontology_generator.py`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/ontology_generator.py), specifically within the `_validate_and_process()` method that enforces capacity limits and injects fallback types.

## Frequently Asked Questions

### How does the ontology generator ensure that essential entity types are always present?

The generator enforces the presence of `Person` and `Organization` entity types through mandatory fallback injection. If the LLM output omits these types, the `_validate_and_process()` method automatically appends them with predefined attributes—`full_name` and `role` for Person, `org_name` and `org_type` for Organization. If the entity list has reached the `MAX_ENTITY_TYPES = 10` limit, the generator evicts the least-important custom types (those at the end of the list) to make room for these mandatory fallbacks.

### What constraints exist on the number of entity and edge types in the ontology?

The system imposes strict capacity limits to maintain graph performance and schema clarity. The `MAX_ENTITY_TYPES` constant is set to 10, and `MAX_EDGE_TYPES` is similarly capped at 10. During the `_validate_and_process()` phase, any entity or edge types exceeding these limits are truncated from the end of their respective lists. This ensures that the final ontology JSON and generated Python code never contain more than 10 entity types and 10 edge types, regardless of the LLM's initial output.

### How are relationship constraints between entity types enforced in the generated code?

Relationship constraints are enforced through the `source_targets` field in edge type definitions. Each edge type specifies a list of valid source-target pairs (e.g., `[{"source": "Person", "target": "Organization"}]`) that define which entity types may connect via that relationship. During code generation, these constraints populate the `EDGE_SOURCE_TARGETS` dictionary, which the simulation engine consults at runtime. The generated edge classes inherit from Zep's `EdgeModel` and maintain these constraints, ensuring that the graph builder can only create valid connections between compatible entity types.

### What is the role of the `generate_python_code()` method in the ontology pipeline?

The `generate_python_code()` method serves as the final transformation stage, converting validated ontology JSON into executable Python modules that the simulation engine can import directly. This method generates three critical components: (1) `EntityModel` subclasses for each entity type with their defined attributes, (2) `EdgeModel` subclasses for each relationship type with source-target constraints, and (3) lookup dictionaries (`ENTITY_TYPES`, `EDGE_TYPES`, `EDGE_SOURCE_TARGETS`) that map type names to their corresponding classes. By importing `EntityModel`, `EntityText`, and `EdgeModel` from `zep_cloud.external_clients.ontology`, the generated code ensures full compatibility with the Zep graph database API, enabling the [`graph_builder.py`](https://github.com/666ghj/mirofish/blob/main/graph_builder.py) service to instantiate type-safe nodes and edges during simulation execution.