zep-crewai 0.1.0__py3-none-any.whl → 1.1.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.
@@ -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", "basic"] = "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 "basic")
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