zep-crewai 0.1.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zep_crewai/__init__.py +42 -18
- zep_crewai/graph_storage.py +147 -0
- zep_crewai/tools.py +318 -0
- zep_crewai/user_storage.py +208 -0
- zep_crewai/utils.py +139 -0
- zep_crewai-1.0.0.dist-info/METADATA +404 -0
- zep_crewai-1.0.0.dist-info/RECORD +10 -0
- zep_crewai-0.1.0.dist-info/METADATA +0 -186
- zep_crewai-0.1.0.dist-info/RECORD +0 -6
- {zep_crewai-0.1.0.dist-info → zep_crewai-1.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
"""
|
2
|
+
Zep User Storage for CrewAI.
|
3
|
+
|
4
|
+
This module provides user-specific storage that integrates Zep's user graph
|
5
|
+
and thread capabilities with CrewAI's memory system.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Any, Literal
|
10
|
+
|
11
|
+
from crewai.memory.storage.interface import Storage
|
12
|
+
from zep_cloud.client import Zep
|
13
|
+
from zep_cloud.types import Message, SearchFilters
|
14
|
+
|
15
|
+
from .utils import search_graph_and_compose_context
|
16
|
+
|
17
|
+
|
18
|
+
class ZepUserStorage(Storage):
|
19
|
+
"""
|
20
|
+
Storage implementation for Zep's user-specific graphs and threads.
|
21
|
+
|
22
|
+
This class provides persistent storage and retrieval of user-specific memories
|
23
|
+
and conversations using Zep's user graph and thread capabilities.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
client: Zep,
|
29
|
+
user_id: str,
|
30
|
+
thread_id: str,
|
31
|
+
search_filters: SearchFilters | None = None,
|
32
|
+
facts_limit: int = 20,
|
33
|
+
entity_limit: int = 5,
|
34
|
+
mode: Literal["summary", "raw_messages"] = "summary",
|
35
|
+
**kwargs: Any,
|
36
|
+
) -> None:
|
37
|
+
"""
|
38
|
+
Initialize ZepUserStorage with a Zep client instance.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
client: An initialized Zep instance (sync client)
|
42
|
+
user_id: User ID identifying a created Zep user (required)
|
43
|
+
thread_id: Thread ID for conversation context (required)
|
44
|
+
search_filters: Optional filters for search operations
|
45
|
+
facts_limit: Maximum number of facts (edges) to retrieve for context
|
46
|
+
entity_limit: Maximum number of entities (nodes) to retrieve for context
|
47
|
+
mode: Mode for thread context retrieval ("summary" or "raw_messages")
|
48
|
+
**kwargs: Additional configuration options
|
49
|
+
"""
|
50
|
+
if not isinstance(client, Zep):
|
51
|
+
raise TypeError("client must be an instance of Zep")
|
52
|
+
|
53
|
+
if not user_id:
|
54
|
+
raise ValueError("user_id is required")
|
55
|
+
|
56
|
+
if not thread_id:
|
57
|
+
raise ValueError("thread_id is required")
|
58
|
+
|
59
|
+
self._client = client
|
60
|
+
self._user_id = user_id
|
61
|
+
self._thread_id = thread_id
|
62
|
+
self._search_filters = search_filters
|
63
|
+
self._facts_limit = facts_limit
|
64
|
+
self._entity_limit = entity_limit
|
65
|
+
self._mode = mode
|
66
|
+
self._config = kwargs
|
67
|
+
|
68
|
+
self._logger = logging.getLogger(__name__)
|
69
|
+
|
70
|
+
def save(self, value: Any, metadata: dict[str, Any] | None = None) -> None:
|
71
|
+
"""
|
72
|
+
Save data to the user's graph or thread.
|
73
|
+
|
74
|
+
Routes storage based on metadata.type:
|
75
|
+
- "message": Store as thread message (requires thread_id)
|
76
|
+
- "json": Store as JSON data in user graph
|
77
|
+
- "text": Store as text data in user graph (default)
|
78
|
+
|
79
|
+
Args:
|
80
|
+
value: The content to store
|
81
|
+
metadata: Metadata including type, role, name, etc.
|
82
|
+
"""
|
83
|
+
metadata = metadata or {}
|
84
|
+
content_str = str(value)
|
85
|
+
content_type = metadata.get("type", "text")
|
86
|
+
|
87
|
+
# Validate content type
|
88
|
+
if content_type not in ["message", "json", "text"]:
|
89
|
+
content_type = "text"
|
90
|
+
|
91
|
+
try:
|
92
|
+
if content_type == "message":
|
93
|
+
# Store as thread message
|
94
|
+
role = metadata.get("role", "user")
|
95
|
+
name = metadata.get("name")
|
96
|
+
|
97
|
+
message = Message(
|
98
|
+
role=role,
|
99
|
+
name=name,
|
100
|
+
content=content_str,
|
101
|
+
)
|
102
|
+
|
103
|
+
self._client.thread.add_messages(thread_id=self._thread_id, messages=[message])
|
104
|
+
|
105
|
+
self._logger.debug(
|
106
|
+
f"Saved message to thread {self._thread_id} from {name or role}: {content_str[:100]}..."
|
107
|
+
)
|
108
|
+
|
109
|
+
else:
|
110
|
+
# Store in user graph
|
111
|
+
self._client.graph.add(
|
112
|
+
user_id=self._user_id,
|
113
|
+
data=content_str,
|
114
|
+
type=content_type,
|
115
|
+
)
|
116
|
+
|
117
|
+
self._logger.debug(
|
118
|
+
f"Saved {content_type} data to user graph {self._user_id}: {content_str[:100]}..."
|
119
|
+
)
|
120
|
+
|
121
|
+
except Exception as e:
|
122
|
+
self._logger.error(f"Error saving to Zep user storage: {e}")
|
123
|
+
raise
|
124
|
+
|
125
|
+
def search(
|
126
|
+
self, query: str, limit: int = 10, score_threshold: float = 0.0
|
127
|
+
) -> dict[str, Any] | list[Any]:
|
128
|
+
"""
|
129
|
+
Search the user's graph and return composed context.
|
130
|
+
|
131
|
+
Performs parallel searches across edges, nodes, and episodes in the user graph,
|
132
|
+
then returns composed context string.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
query: Search query string from the agent
|
136
|
+
limit: Maximum number of results per scope
|
137
|
+
score_threshold: Minimum relevance score (not used in Zep, kept for interface compatibility)
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
List with context results from user storage
|
141
|
+
"""
|
142
|
+
try:
|
143
|
+
# Use the shared utility function for graph search and context composition
|
144
|
+
context = search_graph_and_compose_context(
|
145
|
+
client=self._client,
|
146
|
+
query=query,
|
147
|
+
user_id=self._user_id,
|
148
|
+
facts_limit=self._facts_limit,
|
149
|
+
entity_limit=self._entity_limit,
|
150
|
+
episodes_limit=limit,
|
151
|
+
search_filters=self._search_filters,
|
152
|
+
)
|
153
|
+
|
154
|
+
if context:
|
155
|
+
self._logger.info(f"Composed context for query: {query}")
|
156
|
+
return [
|
157
|
+
{
|
158
|
+
"memory": context,
|
159
|
+
"type": "user_graph_context",
|
160
|
+
"source": "user_graph",
|
161
|
+
"query": query,
|
162
|
+
}
|
163
|
+
]
|
164
|
+
|
165
|
+
self._logger.info(f"No results found for query: {query}")
|
166
|
+
return []
|
167
|
+
|
168
|
+
except Exception as e:
|
169
|
+
self._logger.error(f"Error searching user graph: {e}")
|
170
|
+
return []
|
171
|
+
|
172
|
+
def get_context(self) -> str | None:
|
173
|
+
"""
|
174
|
+
Get context from the thread using get_user_context.
|
175
|
+
|
176
|
+
Returns:
|
177
|
+
The context string if available, None otherwise.
|
178
|
+
"""
|
179
|
+
if not self._thread_id:
|
180
|
+
return None
|
181
|
+
|
182
|
+
try:
|
183
|
+
context = self._client.thread.get_user_context(
|
184
|
+
thread_id=self._thread_id, mode=self._mode
|
185
|
+
)
|
186
|
+
|
187
|
+
# Return the context string if available
|
188
|
+
if context and hasattr(context, "context"):
|
189
|
+
return context.context
|
190
|
+
return None
|
191
|
+
|
192
|
+
except Exception as e:
|
193
|
+
self._logger.error(f"Error getting context from thread: {e}")
|
194
|
+
return None
|
195
|
+
|
196
|
+
def reset(self) -> None:
|
197
|
+
"""Reset is not implemented for user storage as it should persist."""
|
198
|
+
pass
|
199
|
+
|
200
|
+
@property
|
201
|
+
def user_id(self) -> str:
|
202
|
+
"""Get the user ID."""
|
203
|
+
return self._user_id
|
204
|
+
|
205
|
+
@property
|
206
|
+
def thread_id(self) -> str:
|
207
|
+
"""Get the thread ID."""
|
208
|
+
return self._thread_id
|
zep_crewai/utils.py
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for Zep CrewAI integration.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
7
|
+
|
8
|
+
from zep_cloud.client import Zep
|
9
|
+
from zep_cloud.graph.utils import compose_context_string
|
10
|
+
from zep_cloud.types import SearchFilters
|
11
|
+
|
12
|
+
|
13
|
+
def search_graph_and_compose_context(
|
14
|
+
client: Zep,
|
15
|
+
query: str,
|
16
|
+
graph_id: str | None = None,
|
17
|
+
user_id: str | None = None,
|
18
|
+
facts_limit: int = 20,
|
19
|
+
entity_limit: int = 5,
|
20
|
+
episodes_limit: int = 10,
|
21
|
+
search_filters: SearchFilters | None = None,
|
22
|
+
) -> str | None:
|
23
|
+
"""
|
24
|
+
Perform parallel graph searches and compose context string.
|
25
|
+
|
26
|
+
Searches for edges, nodes, and episodes in parallel, then uses
|
27
|
+
compose_context_string to format the results.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
client: Zep client instance
|
31
|
+
query: Search query string
|
32
|
+
graph_id: Graph ID for generic graph search
|
33
|
+
user_id: User ID for user graph search
|
34
|
+
facts_limit: Maximum number of facts (edges) to retrieve
|
35
|
+
entity_limit: Maximum number of entities (nodes) to retrieve
|
36
|
+
episodes_limit: Maximum number of episodes to retrieve
|
37
|
+
search_filters: Optional search filters
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Composed context string or None if no results
|
41
|
+
"""
|
42
|
+
logger = logging.getLogger(__name__)
|
43
|
+
|
44
|
+
if not graph_id and not user_id:
|
45
|
+
raise ValueError("Either graph_id or user_id must be provided")
|
46
|
+
|
47
|
+
# Truncate query if too long
|
48
|
+
truncated_query = query[:400] if len(query) > 400 else query
|
49
|
+
|
50
|
+
edges = []
|
51
|
+
nodes = []
|
52
|
+
episodes = []
|
53
|
+
|
54
|
+
# Execute searches in parallel
|
55
|
+
try:
|
56
|
+
with ThreadPoolExecutor(max_workers=3) as executor:
|
57
|
+
# Search for facts (edges)
|
58
|
+
if graph_id:
|
59
|
+
future_edges = executor.submit(
|
60
|
+
client.graph.search,
|
61
|
+
graph_id=graph_id,
|
62
|
+
query=truncated_query,
|
63
|
+
limit=facts_limit,
|
64
|
+
scope="edges",
|
65
|
+
search_filters=search_filters,
|
66
|
+
)
|
67
|
+
else:
|
68
|
+
future_edges = executor.submit(
|
69
|
+
client.graph.search,
|
70
|
+
user_id=user_id,
|
71
|
+
query=truncated_query,
|
72
|
+
limit=facts_limit,
|
73
|
+
scope="edges",
|
74
|
+
search_filters=search_filters,
|
75
|
+
)
|
76
|
+
|
77
|
+
# Search for entities (nodes)
|
78
|
+
if graph_id:
|
79
|
+
future_nodes = executor.submit(
|
80
|
+
client.graph.search,
|
81
|
+
graph_id=graph_id,
|
82
|
+
query=truncated_query,
|
83
|
+
limit=entity_limit,
|
84
|
+
scope="nodes",
|
85
|
+
search_filters=search_filters,
|
86
|
+
)
|
87
|
+
else:
|
88
|
+
future_nodes = executor.submit(
|
89
|
+
client.graph.search,
|
90
|
+
user_id=user_id,
|
91
|
+
query=truncated_query,
|
92
|
+
limit=entity_limit,
|
93
|
+
scope="nodes",
|
94
|
+
search_filters=search_filters,
|
95
|
+
)
|
96
|
+
|
97
|
+
# Search for episodes
|
98
|
+
if graph_id:
|
99
|
+
future_episodes = executor.submit(
|
100
|
+
client.graph.search,
|
101
|
+
graph_id=graph_id,
|
102
|
+
query=truncated_query,
|
103
|
+
limit=episodes_limit,
|
104
|
+
scope="episodes",
|
105
|
+
search_filters=search_filters,
|
106
|
+
)
|
107
|
+
else:
|
108
|
+
future_episodes = executor.submit(
|
109
|
+
client.graph.search,
|
110
|
+
user_id=user_id,
|
111
|
+
query=truncated_query,
|
112
|
+
limit=episodes_limit,
|
113
|
+
scope="episodes",
|
114
|
+
search_filters=search_filters,
|
115
|
+
)
|
116
|
+
|
117
|
+
edge_results = future_edges.result()
|
118
|
+
node_results = future_nodes.result()
|
119
|
+
episode_results = future_episodes.result()
|
120
|
+
|
121
|
+
if edge_results and edge_results.edges:
|
122
|
+
edges = edge_results.edges
|
123
|
+
|
124
|
+
if node_results and node_results.nodes:
|
125
|
+
nodes = node_results.nodes
|
126
|
+
|
127
|
+
if episode_results and episode_results.episodes:
|
128
|
+
episodes = episode_results.episodes
|
129
|
+
|
130
|
+
except Exception as e:
|
131
|
+
logger.error(f"Failed to search graph: {e}")
|
132
|
+
return None
|
133
|
+
|
134
|
+
# Compose context string from all results
|
135
|
+
if edges or nodes or episodes:
|
136
|
+
context = compose_context_string(edges=edges, nodes=nodes, episodes=episodes)
|
137
|
+
return context
|
138
|
+
|
139
|
+
return None
|