Skip to content

Commit dbb0a0d

Browse files
feat: include episodes in context string and add tests (#281)
* chore: Fix fact ratings table * SDK regeneration * chore: Version bump * chore: Version bump * SDK regeneration * chore: Add support for user/graph ontology targets * SDK regeneration * chore: Add support for setting entity/edges on a list of users/graphs * SDK regeneration * chore: Bump version * SDK regeneration * feat: Update compose context string util to include episodes and display entity attributes * chore: Bump version * fix: tests --------- Co-authored-by: fern-api <115122769+fern-api[bot]@users.noreply.github.com>
1 parent 60001c1 commit dbb0a0d

File tree

5 files changed

+420
-16
lines changed

5 files changed

+420
-16
lines changed

.fernignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
src/zep_cloud/client.py
33
src/zep_cloud/graph/utils.py
44
src/zep_cloud/external_clients/
5+
tests/graph/
56
examples/
67
pyproject.toml
78
poetry.lock

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "zep-cloud"
33

44
[tool.poetry]
55
name = "zep-cloud"
6-
version = "3.2.0"
6+
version = "3.3.0"
77
description = ""
88
readme = "README.md"
99
authors = []

src/zep_cloud/graph/utils.py

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
11
from datetime import datetime
22
from typing import List
33

4-
from zep_cloud import EntityEdge, EntityNode
4+
from zep_cloud import EntityEdge, EntityNode, Episode
55

66
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
77

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+
819
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.
1021
1122
# These are the most relevant facts and their valid date ranges
1223
# format: FACT (Date range: from - to)
1324
<FACTS>
14-
%s
25+
{facts}
1526
</FACTS>
1627
1728
# 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
1934
<ENTITIES>
20-
%s
35+
{entities}
2136
</ENTITIES>
37+
{episodes_section}
2238
"""
2339

2440

@@ -36,35 +52,93 @@ def format_edge_date_range(edge: EntityEdge) -> str:
3652
invalid_at = "present"
3753

3854
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)
4056
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)
4258

4359
return f"{valid_at} - {invalid_at}"
4460

4561

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:
4763
"""
48-
Compose a search context from entity edges and nodes.
64+
Compose a search context from entity edges, nodes, and episodes.
4965
5066
Args:
5167
edges: List of entity edges.
5268
nodes: List of entity nodes.
69+
episodes: List of episodes.
5370
5471
Returns:
55-
A formatted string containing facts and entities.
72+
A formatted string containing facts, entities, and episodes.
5673
"""
5774
facts = []
5875
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)})"
6077
facts.append(fact)
6178

6279
entities = []
6380
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)
65102
entities.append(entity)
66103

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)
69130

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+
)

tests/graph/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)