1
1
from datetime import datetime
2
2
from typing import List
3
3
4
- from zep_cloud import EntityEdge , EntityNode
4
+ from zep_cloud import EntityEdge , EntityNode , Episode
5
5
6
6
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
7
7
8
+
9
+ def parse_iso_datetime (iso_string : str ) -> datetime :
10
+ """Parse ISO datetime string, handling Z suffix for UTC."""
11
+ try :
12
+ return datetime .fromisoformat (iso_string )
13
+ except ValueError :
14
+ # Handle Z suffix for Python 3.9 compatibility
15
+ if iso_string .endswith ('Z' ):
16
+ return datetime .fromisoformat (iso_string [:- 1 ] + '+00:00' )
17
+ raise
18
+
8
19
TEMPLATE_STRING = """
9
- FACTS and ENTITIES represent relevant context to the current conversation.
20
+ FACTS and ENTITIES{episodes_header} represent relevant context to the current conversation.
10
21
11
22
# These are the most relevant facts and their valid date ranges
12
23
# format: FACT (Date range: from - to)
13
24
<FACTS>
14
- %s
25
+ {facts}
15
26
</FACTS>
16
27
17
28
# These are the most relevant entities
18
- # ENTITY_NAME: entity summary
29
+ # Name: ENTITY_NAME
30
+ # Label: entity_label (if present)
31
+ # Attributes: (if present)
32
+ # attr_name: attr_value
33
+ # Summary: entity summary
19
34
<ENTITIES>
20
- %s
35
+ {entities}
21
36
</ENTITIES>
37
+ {episodes_section}
22
38
"""
23
39
24
40
@@ -36,35 +52,93 @@ def format_edge_date_range(edge: EntityEdge) -> str:
36
52
invalid_at = "present"
37
53
38
54
if edge .valid_at is not None :
39
- valid_at = datetime . fromisoformat (edge .valid_at ).strftime (DATE_FORMAT )
55
+ valid_at = parse_iso_datetime (edge .valid_at ).strftime (DATE_FORMAT )
40
56
if edge .invalid_at is not None :
41
- invalid_at = datetime . fromisoformat (edge .invalid_at ).strftime (DATE_FORMAT )
57
+ invalid_at = parse_iso_datetime (edge .invalid_at ).strftime (DATE_FORMAT )
42
58
43
59
return f"{ valid_at } - { invalid_at } "
44
60
45
61
46
- def compose_context_string (edges : List [EntityEdge ], nodes : List [EntityNode ]) -> str :
62
+ def compose_context_string (edges : List [EntityEdge ], nodes : List [EntityNode ], episodes : List [ Episode ] ) -> str :
47
63
"""
48
- Compose a search context from entity edges and nodes .
64
+ Compose a search context from entity edges, nodes, and episodes .
49
65
50
66
Args:
51
67
edges: List of entity edges.
52
68
nodes: List of entity nodes.
69
+ episodes: List of episodes.
53
70
54
71
Returns:
55
- A formatted string containing facts and entities .
72
+ A formatted string containing facts, entities, and episodes .
56
73
"""
57
74
facts = []
58
75
for edge in edges :
59
- fact = f" - { edge .fact } ({ format_edge_date_range (edge )} )"
76
+ fact = f" - { edge .fact } (Date range: { format_edge_date_range (edge )} )"
60
77
facts .append (fact )
61
78
62
79
entities = []
63
80
for node in nodes :
64
- entity = f" - { node .name } : { node .summary } "
81
+ entity_parts = [f"Name: { node .name } " ]
82
+
83
+ if hasattr (node , 'labels' ) and node .labels :
84
+ labels = list (node .labels ) # Create a copy to avoid modifying original
85
+ if 'Entity' in labels :
86
+ labels .remove ('Entity' )
87
+ if labels : # Only add label if there are remaining labels after removing 'Entity'
88
+ entity_parts .append (f"Label: { labels [0 ]} " )
89
+
90
+ if hasattr (node , 'attributes' ) and node .attributes :
91
+ # Filter out 'labels' from attributes as it's redundant with the Label field
92
+ filtered_attributes = {k : v for k , v in node .attributes .items () if k != 'labels' }
93
+ if filtered_attributes : # Only add attributes section if there are non-label attributes
94
+ entity_parts .append ("Attributes:" )
95
+ for attr_name , attr_value in filtered_attributes .items ():
96
+ entity_parts .append (f" { attr_name } : { attr_value } " )
97
+
98
+ if node .summary :
99
+ entity_parts .append (f"Summary: { node .summary } " )
100
+
101
+ entity = "\n " .join (entity_parts )
65
102
entities .append (entity )
66
103
67
- facts_str = "\n " .join (facts )
68
- entities_str = "\n " .join (entities )
104
+ # Format episodes
105
+ episodes_list = []
106
+ if episodes :
107
+ for episode in episodes :
108
+ role_prefix = ""
109
+ if hasattr (episode , 'role' ) and episode .role :
110
+ if hasattr (episode , 'role_type' ) and episode .role_type :
111
+ role_prefix = f"{ episode .role } ({ episode .role_type } ): "
112
+ else :
113
+ role_prefix = f"{ episode .role } : "
114
+ elif hasattr (episode , 'role_type' ) and episode .role_type :
115
+ role_prefix = f"({ episode .role_type } ): "
116
+
117
+ # Format timestamp
118
+ episode_created_at = episode .created_at
119
+ if hasattr (episode , 'provided_created_at' ) and episode .provided_created_at :
120
+ episode_created_at = episode .provided_created_at
121
+
122
+ # Parse timestamp if it's a string (ISO format)
123
+ if isinstance (episode_created_at , str ):
124
+ timestamp = parse_iso_datetime (episode_created_at ).strftime (DATE_FORMAT )
125
+ else :
126
+ timestamp = episode_created_at .strftime (DATE_FORMAT )
127
+
128
+ episode_str = f" - { role_prefix } { episode .content } ({ timestamp } )"
129
+ episodes_list .append (episode_str )
69
130
70
- return TEMPLATE_STRING % (facts_str , entities_str )
131
+ facts_str = "\n " .join (facts ) if facts else ""
132
+ entities_str = "\n " .join (entities ) if entities else ""
133
+ episodes_str = "\n " .join (episodes_list ) if episodes_list else ""
134
+
135
+ # Determine if episodes section should be included
136
+ episodes_header = ", and EPISODES" if episodes else ""
137
+ episodes_section = f"\n # These are the most relevant episodes\n <EPISODES>\n { episodes_str } \n </EPISODES>" if episodes else ""
138
+
139
+ return TEMPLATE_STRING .format (
140
+ episodes_header = episodes_header ,
141
+ facts = facts_str ,
142
+ entities = entities_str ,
143
+ episodes_section = episodes_section
144
+ )
0 commit comments