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

> Discover how the ZepEntityReader enriches graph nodes. Learn how it retrieves incident edges, categorizes them, and uses UUID lookups to populate related_edges and related_nodes for deeper insights.

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

---

**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`](https://github.com/666ghj/mirofish/blob/main/backend/app/services/zep_entity_reader.py) at lines 252-255, the code creates a dictionary mapping UUIDs to node objects:

```python
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`](https://github.com/666ghj/mirofish/blob/main/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`:

```python
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:

```python
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:

```python
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`](https://github.com/666ghj/mirofish/blob/main/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.

```python
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`](https://github.com/666ghj/mirofish/blob/main/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.