Customizing Graph Structure

Zep enables the use of rich, domain-specific data structures in graphs through Entity Types and Edge Types, replacing generic graph nodes and edges with detailed models.

Zep classifies newly created nodes/edges as one of the default or custom types or leaves them unclassified. For example, a node representing a preference is classified as a Preference node, and attributes specific to that type are automatically populated. You may restrict graph queries to nodes/edges of a specific type, such as Preference.

The default entity types are applied to all graphs by default, but you may define additional custom types as needed.

Each node/edge is classified as a single type only. Multiple classifications are not supported.

Default Entity Types

Definition

The default entity types are:

  • User: A human that is part of the current chat thread.
  • Preference: One of the User’s preferences.
  • Procedure: A multi-step instruction informing the agent how to behave (e.g. ‘When the user asks for code, respond only with code snippets followed by a bullet point explanation’)

Default entity types only apply to user graphs (not group graphs). All nodes in any user graph will be classified into one of these types or none.

Adding Data

When we add data to the graph, default entity types are automatically created:

1from zep_cloud.types import Message
2
3message = {"role": "John Doe", "role_type": "user", "content": "I really like pop music, and I don't like metal"}
4
5client.memory.add(session_id=session_id, messages=[Message(**message)])

Searching

When searching nodes in the graph, you may provide a list of types to filter the search by. The provided types are ORed together. Search results will only include nodes that satisfy one of the provided types:

1from zep_cloud.types import SearchFilters
2
3search_results = client.graph.search(
4 user_id=user_id,
5 query="the user's music preferences",
6 scope="nodes",
7 search_filters=SearchFilters(
8 node_labels=["Preference"]
9 )
10)
11for i, node in enumerate(search_results.nodes):
12 preference = node.attributes
13 print(f"Preference {i+1}:{preference}")
Preference 1: {'category': 'Music', 'description': 'Pop Music is a genre of music characterized by its catchy melodies and widespread appeal.', 'labels': ['Entity', 'Preference']}
Preference 2: {'category': 'Music', 'description': 'Metal Music is a genre of music characterized by its heavy sound and complex compositions.', 'labels': ['Entity', 'Preference']}

Custom Entity Types

Definition

In addition to the default entity types, you may specify your own custom entity types. You need to provide a description of the type and a description for each of the fields. The syntax for this is different for each language.

You may not create more than 10 custom EntityModels per project. Each model may have up to 10 fields.

When creating custom entity types, you may not use the following attribute names (including in Go struct tags), as they conflict with default node attributes: uuid, name, group_id, name_embedding, summary, and created_at.

1from pydantic import Field
2from zep_cloud.external_clients.ontology import EntityModel, EntityText, EntityInt
3
4class ApartmentComplex(EntityModel):
5 """
6 Represents an apartment complex.
7 """
8 complex_name: EntityText = Field(
9 description="The name of the apartment complex",
10 default=None
11 )
12 price_of_rent: EntityInt = Field(
13 description="The price of rent for the apartment complex",
14 default=None
15 )
16
17class Restaurant(EntityModel):
18 """
19 Represents a restaurant.
20 """
21 restaurant_name: EntityText = Field(
22 description="The name of the restaurant",
23 default=None
24 )

Setting Entity Types

You can then set these as the custom entity types for your current Zep project:

1client.graph.set_entity_types(
2 entities={
3 "ApartmentComplex": ApartmentComplex,
4 "Restaurant": Restaurant
5 }
6)

Adding Data

Now, when you add data to the graph, new nodes are classified into exactly one of the overall set of entity types or none:

1from zep_cloud.types import Message
2import json
3
4messages = [
5 {"role": "John Doe", "role_type": "user", "content": "The last apartment complex I lived in was Oasis"},
6 {"role": "John Doe", "role_type": "user", "content": "There were really great restaurants near Oasis, such as The Biscuit"}
7]
8
9apartment_complexes = [
10 {"name": "Oasis", "description": "An apartment complex", "price_of_rent": 1050},
11 {"name": "Sanctuary", "description": "An apartment complex", "price_of_rent": 1100},
12 {"name": "Harbor View", "description": "An apartment complex", "price_of_rent": 1250},
13 {"name": "Greenwood", "description": "An apartment complex", "price_of_rent": 950},
14 {"name": "Skyline", "description": "An apartment complex", "price_of_rent": 1350}
15]
16
17for apartment_complex in apartment_complexes:
18 client.graph.add(
19 user_id=user_id,
20 type="json",
21 data=json.dumps(apartment_complex)
22 )
23
24client.memory.add(session_id=session_id, messages=[Message(**m) for m in messages])

Searching/Retrieving

Now that a graph with custom entity types has been created, you may filter node search results by entity type. In this case, you are able to get a structured answer (an ApartmentComplex object) to an open ended query (the apartment complex the user previously resided in) where the answer required fusing together the chat history and the JSON data:

1from zep_cloud.types import SearchFilters
2
3search_results = client.graph.search(
4 user_id=user_id,
5 query="The apartment complex the user previously resided in",
6 scope="nodes",
7 limit=1,
8 search_filters=SearchFilters(
9 node_labels=["ApartmentComplex"]
10 )
11)
12previous_apartment_complex = ApartmentComplex(**search_results.nodes[0].attributes)
13print(f"{previous_apartment_complex}")
complex_name='Oasis' price_of_rent=1050

The search filter ORs together the provided types, so search results will only include nodes that satisfy one of the provided types.

You can also retrieve all nodes of a specific type:

1nodes = client.graph.node.get_by_user_id(user_id)
2for node in nodes:
3 if "ApartmentComplex" in node.labels:
4 apartment_complex = ApartmentComplex(**node.attributes)
5 print(f"{apartment_complex}")
complex_name='Oasis' price_of_rent=1050
complex_name='Sanctuary' price_of_rent=1100
complex_name='Greenwood' price_of_rent=950
complex_name='Skyline' price_of_rent=1350
complex_name='Harbor View' price_of_rent=1250

Important Notes/Tips

Some notes regarding custom entity types:

  • The set_entity_types method overwrites any previously defined custom entity types, so the set of custom entity types is always the list of types provided in the last set_entity_types method call
  • The overall set of entity types for a project includes both the custom entity types you set and the default entity types
  • You can overwrite the default entity types by providing custom entity types with the same names
  • Changing the custom entity types will not update previously created nodes. The classification and attributes of existing nodes will stay the same. The only thing that can change existing classifications or attributes is adding data that provides new information.
  • When creating custom entity types, avoid using the following attribute names (including in Go struct tags), as they conflict with default node attributes: uuid, name, group_id, name_embedding, summary, and created_at
  • Tip: Design custom entity types to represent entities/nouns as opposed to relationships/verbs. Your type might be represented in the graph as an edge more often than as a node
  • Tip: If you have overlapping entity types (e.g. ‘Hobby’ and ‘Hiking’), you can prioritize one type over another by mentioning which to prioritize in the entity type descriptions

Custom Edge Types

Definition

In addition to custom entity types, you may specify your own custom edge types to represent types of facts/relationships as opposed to types of entities. Each custom edge type requires a description and a definition for each field. The syntax for defining edge types is different for each language.

You may not create more than 10 custom edge types per project. Each model may have up to 10 fields.

When creating custom edge types, you may not use the following attribute names (including in Go struct tags), as they conflict with default edge attributes: uuid, name, group_id, name_embedding, summary, and created_at.

1from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel
2from pydantic import Field
3
4class Restaurant(EntityModel):
5 """A restaurant entity"""
6 restaurant_name: EntityText = Field(
7 description="The restaurant's name",
8 default=None
9 )
10 location: EntityText = Field(
11 description="The restaurant's location",
12 default=None
13 )
14
15class NavigatesToRestaurant(EdgeModel):
16 """The fact that the user navigates to or requests navigation to a restaurant"""
17 reason_for_visit: EntityText = Field(
18 description="The reason for the visit to the restaurant if one is given",
19 default=None
20 )
21
22class RequestsFoodItem(EdgeModel):
23 """The fact that the user requests a food item to be ordered or delivered"""
24 food_item: EntityText = Field(
25 description="The food item ordered",
26 default=None
27 )

Setting Edge Types

After defining your custom edge types, set them using the appropriate SDK method. This overwrites any previously defined custom edge types, so the set of edge types is always the list provided in the last set operation.

Note that you can require an edge type to have a source entity of a specific type and/or a target entity of a specific type. This ensures that an edge can only be classified as this edge type if it is between entities of the specified types. If a source/target entity type is not specified, then any entity is allowed for the source/target.

1from zep_cloud import EntityEdgeSourceTarget
2
3client.graph.set_entity_types(
4 entities={
5 "Restaurant": Restaurant,
6 },
7 edges={
8 "NAVIGATES_TO_RESTAURANT": (
9 NavigatesToRestaurant,
10 [
11 EntityEdgeSourceTarget(
12 source="User",
13 target="Restaurant"
14 )
15 ]
16 ),
17 "REQUESTS_FOOD_ITEM": (
18 RequestsFoodItem,
19 [
20 EntityEdgeSourceTarget(
21 source="User",
22 )
23 ]
24 ),
25 }
26)

Adding Data

When you add data to the graph, edges are classified into exactly one of the overall set of edge types or none.

1user_id = f"user-{uuid.uuid4()}"
2client.user.add(
3 user_id=user_id,
4 first_name="Paul",
5 last_name="Smith",
6 email="[email protected]"
7)
8
9session_id = f"session-{uuid.uuid4()}"
10client.memory.add_session(
11 session_id=session_id,
12 user_id=user_id
13)
14
15messages = [
16 Message(role="Paul", role_type="user", content="navigate to McDonald's"),
17 Message(role="AI Assistant", role_type="assistant", content="Ok navigating to McDonald's"),
18 Message(role="Paul", role_type="user", content="actually I changed my mind"),
19 Message(role="AI Assistant", role_type="assistant", content="Ok"),
20 Message(role="Paul", role_type="user", content="Navigate to Applebee's"),
21 Message(role="AI Assistant", role_type="assistant", content="Navigating to Applebee's"),
22 Message(role="Paul", role_type="user", content="Sounds good, but please order in a salad to-go ahead of time for me so that it is ready when we get there"),
23 Message(role="AI Assistant", role_type="assistant", content="Got it, ordering a salad for you"),
24]
25
26client.memory.add(
27 session_id=session_id,
28 messages=messages,
29 ignore_roles=["assistant"],
30)

Searching/Retrieving

You may filter edge search results by edge type. This enables you to retrieve only those edges that match a specific type.

1search_results_orders = client.graph.search(
2 user_id=user_id,
3 query="Order some food for me",
4 scope="edges",
5 search_filters={
6 "edge_types": ["REQUESTS_FOOD_ITEM"]
7 },
8 limit=1,
9)
10for edge in search_results_orders.edges:
11 print(f"Edge fact: {edge.fact}")
12 print(f"Edge type: {edge.name}")
13 print(f"Food item: {edge.attributes['food_item']}")
Edge fact: Paul ordered a salad to-go ahead of time so that it is ready when they get there
Edge type: REQUESTS_FOOD_ITEM
Food item: salad

Important Notes/Tips

Some notes regarding custom edge types:

  • The set_entity_types method overwrites any previously defined custom edge types, so the set of custom edge types is always the list of types provided in the last set_entity_types method call
  • There are no default edge types
  • Changing the custom edge types will not update previously created edges. The classification and attributes of existing edges will stay the same. The only thing that can change existing classifications or attributes is adding data that provides new information.
  • When creating custom edge types, avoid using the following attribute names (including in Go struct tags), as they conflict with default edge attributes: uuid, name, group_id, name_embedding, summary, and created_at
  • Tip: Design custom edge types to represent relationships/verbs as opposed to entities/nouns. Your type might be represented in the graph as an edge more often than as a node
  • Tip: If you have overlapping edge types (e.g. ‘NavigatesToRestaurant’ and ‘NavigatesToLocation’), you can prioritize one type over another by mentioning which to prioritize in the edge type descriptions