How the Entity Reader Enriches Graph Nodes with Relationship Information in MiroFish

The ZepEntityReader service enriches graph nodes by retrieving all edges incident to each entity, categorizing them as incoming or outgoing, and resolving connected node details through a UUID lookup map to populate related_edges and related_nodes attributes.

The ZepEntityReader class in the MiroFish codebase (666ghj/mirofish) transforms raw Zep graph data into fully contextualized entity objects. By batching API calls and building efficient lookup structures, it minimizes network overhead while providing complete relationship context for downstream graph analysis. This enrichment process enables the application to traverse connections without additional round-trips to the Zep API.

Retrieving Raw Graph Data

The enrichment pipeline begins by fetching the complete dataset from the Zep graph API. The reader executes two primary fetch operations wrapped by _call_with_retry to ensure resilience against transient failures.

First, it retrieves all nodes using get_all_nodes(graph_id) and stores them in the all_nodes collection. Simultaneously, if enrichment is requested, it fetches all edges via get_all_edges(graph_id) and stores them as all_edges. This bulk retrieval strategy eliminates the need for individual API calls during the relationship mapping phase.

Building the Node Lookup Map

Before processing individual entities, the service constructs an in-memory index for O(1) node resolution. In backend/app/services/zep_entity_reader.py at lines 252-255, the code creates a dictionary mapping UUIDs to node objects:

node_map = {n["uuid"]: n for n in all_nodes}

This node_map serves as the backbone for relationship enrichment, allowing the service to instantly retrieve any neighbor node's metadata without additional API requests.

Filtering Meaningful Entities

Not all nodes warrant enrichment. The service iterates through all_nodes and discards entries carrying only default labels like "Entity" or "Node". Nodes possessing custom labels—such as "Person", "Organization", or "Student"—are instantiated as EntityNode objects and passed to the enrichment stage. This filtering ensures that only semantically meaningful entities receive relationship context.

Mapping Incoming and Outgoing Edges

The core enrichment logic resides in the edge processing loop at lines 291-311 of zep_entity_reader.py. When enrich_with_edges=True (the default), the method examines every edge in all_edges to identify connections to the current node:

  • Outgoing edges: When edge["source_node_uuid"] == node["uuid"], the edge is recorded with "direction": "outgoing" and the target UUID is captured.
  • Incoming edges: When edge["target_node_uuid"] == node["uuid"], the edge is marked with "direction": "incoming" and the source UUID is stored.

Each edge record includes the edge_name, fact (relationship description), and the connected node's UUID. These records populate the entity.related_edges list, while all connected UUIDs are collected into a related_node_uuids set for the next phase.

Resolving Connected Node Details

After identifying all relationships, the service resolves the actual node objects for every UUID in related_node_uuids. As implemented in lines 313-326, the code dereferences each UUID against the pre-built node_map:

for uuid in related_node_uuids:
    if uuid in node_map:
        entity.related_nodes.append({
            "uuid": node_map[uuid]["uuid"],
            "name": node_map[uuid]["name"],
            "labels": node_map[uuid]["labels"],
            "summary": node_map[uuid].get("summary", "")
        })

This population of entity.related_nodes provides immediate access to neighbor metadata including names, labels, and summaries, completing the enrichment cycle.

Working with Enriched Entities

The filter_defined_entities method returns a FilteredEntities object containing fully populated EntityNode instances. The following example demonstrates bulk enrichment of all custom entities in a graph:

from backend.app.services.zep_entity_reader import ZepEntityReader

reader = ZepEntityReader()
filtered = reader.filter_defined_entities(
    graph_id="my-graph-id",
    enrich_with_edges=True
)

print(f"Found {filtered.filtered_count} enriched entities")
for entity in filtered.entities[:3]:
    print(f"\nEntity: {entity.name} ({entity.get_entity_type()})")
    print(f"Edges: {len(entity.related_edges)}")
    for edge in entity.related_edges:
        direction = edge['direction']
        target = edge.get('target_node_uuid') or edge.get('source_node_uuid')
        print(f"  - {direction} {edge['edge_name']}{target}")

To retrieve entities of a specific type with their relationships:

students = reader.get_entities_by_type(
    graph_id="my-graph-id",
    entity_type="Student",
    enrich_with_edges=True
)

Single-Entity Context Retrieval

For targeted lookups, the get_entity_with_context method follows the same enrichment pattern for individual UUIDs. Located at lines 371-398 in zep_entity_reader.py, this helper retrieves the specific node, fetches its incident edges via get_node_edges, and resolves neighbors using the full node list. The method returns a single EntityNode populated with related_edges and related_nodes, making it ideal for detail views or relationship exploration of specific entities.

entity = reader.get_entity_with_context(
    graph_id="my-graph-id",
    entity_uuid="123e4567-e89b-12d3-a456-426614174000"
)

if entity:
    print(f"{entity.name} connects to {len(entity.related_nodes)} nodes via {len(entity.related_edges)} edges")

Summary

  • Bulk retrieval: The entity reader fetches all nodes and edges once via get_all_nodes and get_all_edges, wrapped in _call_with_retry for fault tolerance.
  • Lookup optimization: A UUID-to-node dictionary (node_map) built at lines 252-255 enables constant-time neighbor resolution without additional API calls.
  • Directional edge mapping: The service categorizes relationships as incoming or outgoing while capturing edge names, facts, and connected UUIDs in related_edges.
  • Complete context: Connected node metadata (name, labels, summary) is dereferenced from node_map and stored in related_nodes at lines 313-326.
  • Flexible access: Both bulk filtering (filter_defined_entities) and single-entity retrieval (get_entity_with_context) support the enrichment workflow.

Frequently Asked Questions

What is the difference between filter_defined_entities and get_entity_with_context?

The filter_defined_entities method processes all nodes in a graph to return a FilteredEntities collection containing every entity with custom labels, making it suitable for bulk analysis or graph construction. In contrast, get_entity_with_context targets a specific UUID, retrieves only that node's edges via get_node_edges, and returns a single enriched EntityNode optimized for detail views.

How does the entity reader handle API failures during data retrieval?

All Zep API calls are wrapped by the _call_with_retry helper method, which implements exponential backoff and retry logic to handle transient network failures. This ensures that temporary service interruptions do not break the enrichment pipeline when fetching nodes, edges, or individual entity contexts.

What data structure stores the enriched relationship information?

Each enriched entity is represented as an EntityNode object containing two key attributes: related_edges (a list of dictionaries with direction, edge_name, fact, and connected UUIDs) and related_nodes (a list of neighbor metadata including uuid, name, labels, and summary). These structures are populated in backend/app/services/zep_entity_reader.py at lines 291-326.

Why does the reader filter out nodes with default labels?

The service specifically excludes nodes carrying only generic labels like "Entity" or "Node" to focus computational resources on semantically meaningful entities. This filtering ensures that related_edges and related_nodes contain relevant business or domain objects (such as "Person" or "Organization") rather than untyped graph structures.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →