Skip to content

Commit 451d9a7

Browse files
fix: Add missing get_context method to ZepUserStorage
- Implement get_context() method that was missing but required by tests - Update tests to work with required thread_id parameter - Fix mock patches to point to utils module after ThreadPoolExecutor was moved - Update test expectations to match new search behavior using compose_context_string 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e2e0f6b commit 451d9a7

2 files changed

Lines changed: 104 additions & 110 deletions

File tree

integrations/python/zep_crewai/src/zep_crewai/user_storage.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,31 @@ def search(
165165
self._logger.info(f"No results found for query: {query}")
166166
return []
167167

168+
def get_context(self) -> str | None:
169+
"""
170+
Get context from the thread using get_user_context.
171+
172+
Returns:
173+
The context string if available, None otherwise.
174+
"""
175+
if not self._thread_id:
176+
return None
177+
178+
try:
179+
context = self._client.thread.get_user_context(
180+
thread_id=self._thread_id,
181+
mode=self._mode
182+
)
183+
184+
# Return the context string if available
185+
if context and hasattr(context, 'context'):
186+
return context.context
187+
return None
188+
189+
except Exception as e:
190+
self._logger.error(f"Error getting context from thread: {e}")
191+
return None
192+
168193
def reset(self) -> None:
169194
"""Reset is not implemented for user storage as it should persist."""
170195
pass

integrations/python/zep_crewai/tests/test_user_storage.py

Lines changed: 79 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,16 @@ def test_initialization_success(self):
3535
assert storage._entity_limit == 10
3636
assert storage._mode == "summary" # Should default to summary
3737

38-
def test_initialization_without_thread(self):
39-
"""Test initialization without thread_id."""
38+
def test_initialization_without_thread_raises_error(self):
39+
"""Test initialization without thread_id raises TypeError."""
4040
from zep_cloud.client import Zep
4141

4242
mock_client = MagicMock(spec=Zep)
43-
storage = ZepUserStorage(
44-
client=mock_client,
45-
user_id="test-user"
46-
)
47-
48-
assert storage._client is mock_client
49-
assert storage.user_id == "test-user"
50-
assert storage.thread_id is None
43+
with pytest.raises(TypeError, match="missing 1 required positional argument: 'thread_id'"):
44+
storage = ZepUserStorage(
45+
client=mock_client,
46+
user_id="test-user"
47+
)
5148

5249
def test_initialization_requires_user_id(self):
5350
"""Test that user_id is required."""
@@ -56,12 +53,21 @@ def test_initialization_requires_user_id(self):
5653
mock_client = MagicMock(spec=Zep)
5754

5855
with pytest.raises(ValueError, match="user_id is required"):
59-
ZepUserStorage(client=mock_client, user_id="")
56+
ZepUserStorage(client=mock_client, user_id="", thread_id="test-thread")
57+
58+
def test_initialization_requires_thread_id(self):
59+
"""Test that thread_id is required and non-empty."""
60+
from zep_cloud.client import Zep
61+
62+
mock_client = MagicMock(spec=Zep)
63+
64+
with pytest.raises(ValueError, match="thread_id is required"):
65+
ZepUserStorage(client=mock_client, user_id="test-user", thread_id="")
6066

6167
def test_initialization_requires_zep_client(self):
6268
"""Test that client must be Zep instance."""
6369
with pytest.raises(TypeError, match="client must be an instance of Zep"):
64-
ZepUserStorage(client="not_a_client", user_id="test-user")
70+
ZepUserStorage(client="not_a_client", user_id="test-user", thread_id="test-thread")
6571

6672
def test_save_message_with_thread(self):
6773
"""Test saving message when thread_id is set."""
@@ -96,32 +102,6 @@ def test_save_message_with_thread(self):
96102
assert message.role == "assistant"
97103
assert message.name == "Helper"
98104

99-
def test_save_message_without_thread(self):
100-
"""Test saving message when no thread_id falls back to graph."""
101-
from zep_cloud.client import Zep
102-
103-
mock_client = MagicMock(spec=Zep)
104-
mock_client.graph = MagicMock()
105-
mock_client.graph.add = MagicMock()
106-
107-
storage = ZepUserStorage(
108-
client=mock_client,
109-
user_id="test-user"
110-
# No thread_id
111-
)
112-
113-
# Save message (should go to graph as text)
114-
storage.save(
115-
"Message without thread",
116-
metadata={"type": "message", "role": "user"}
117-
)
118-
119-
# Should save to graph as text
120-
mock_client.graph.add.assert_called_once_with(
121-
user_id="test-user",
122-
data="Message without thread",
123-
type="text" # Converted from message to text
124-
)
125105

126106
def test_save_json_data(self):
127107
"""Test saving JSON data to user graph."""
@@ -131,7 +111,7 @@ def test_save_json_data(self):
131111
mock_client.graph = MagicMock()
132112
mock_client.graph.add = MagicMock()
133113

134-
storage = ZepUserStorage(client=mock_client, user_id="test-user")
114+
storage = ZepUserStorage(client=mock_client, user_id="test-user", thread_id="test-thread")
135115

136116
# Save JSON data
137117
json_data = '{"preference": "dark_mode", "timezone": "PST"}'
@@ -152,7 +132,7 @@ def test_save_text_data(self):
152132
mock_client.graph = MagicMock()
153133
mock_client.graph.add = MagicMock()
154134

155-
storage = ZepUserStorage(client=mock_client, user_id="test-user")
135+
storage = ZepUserStorage(client=mock_client, user_id="test-user", thread_id="test-thread")
156136

157137
# Save text data
158138
storage.save("User prefers morning meetings", metadata={"type": "text"})
@@ -164,8 +144,9 @@ def test_save_text_data(self):
164144
type="text"
165145
)
166146

167-
@patch('zep_crewai.user_storage.ThreadPoolExecutor')
168-
def test_search_with_thread_context(self, mock_executor):
147+
@patch('zep_crewai.utils.compose_context_string')
148+
@patch('zep_crewai.utils.ThreadPoolExecutor')
149+
def test_search_with_thread_context(self, mock_executor, mock_compose):
169150
"""Test search includes thread context when available."""
170151
from zep_cloud.client import Zep
171152
from zep_cloud.types import GraphSearchResults, EntityEdge
@@ -174,36 +155,37 @@ def test_search_with_thread_context(self, mock_executor):
174155
mock_client.thread = MagicMock()
175156
mock_client.graph = MagicMock()
176157

177-
# Mock thread context
178-
mock_context = MagicMock()
179-
mock_context.context = "User's conversation context"
180-
mock_client.thread.get_user_context.return_value = mock_context
181-
182158
# Setup mock executor
183159
mock_executor_instance = MagicMock()
184160
mock_executor.return_value.__enter__.return_value = mock_executor_instance
185161

186-
# Mock futures
187-
future_thread = MagicMock()
188-
future_thread.result.return_value = {
189-
"memory": "User's conversation context",
190-
"type": "thread_context",
191-
"source": "thread"
192-
}
162+
# Create proper GraphSearchResults mocks
163+
edge_results = MagicMock(spec=GraphSearchResults)
164+
edge_results.edges = [MagicMock(fact="User likes Python")]
165+
166+
node_results = MagicMock(spec=GraphSearchResults)
167+
node_results.nodes = []
193168

169+
episode_results = MagicMock(spec=GraphSearchResults)
170+
episode_results.episodes = []
171+
172+
# Mock futures
194173
future_edges = MagicMock()
195-
future_edges.result.return_value = []
174+
future_edges.result.return_value = edge_results
196175

197176
future_nodes = MagicMock()
198-
future_nodes.result.return_value = []
177+
future_nodes.result.return_value = node_results
199178

200179
future_episodes = MagicMock()
201-
future_episodes.result.return_value = []
180+
future_episodes.result.return_value = episode_results
202181

203182
mock_executor_instance.submit.side_effect = [
204-
future_thread, future_edges, future_nodes, future_episodes
183+
future_edges, future_nodes, future_episodes
205184
]
206185

186+
# Mock compose_context_string to return context
187+
mock_compose.return_value = "Context: User likes Python"
188+
207189
storage = ZepUserStorage(
208190
client=mock_client,
209191
user_id="test-user",
@@ -213,14 +195,15 @@ def test_search_with_thread_context(self, mock_executor):
213195
# Perform search
214196
results = storage.search("test query", limit=5)
215197

216-
# Verify results include thread context
198+
# Verify results include context
217199
assert isinstance(results, list)
218-
assert len(results) >= 1
219-
assert results[0]["type"] == "thread_context"
220-
assert results[0]["memory"] == "User's conversation context"
200+
assert len(results) == 1
201+
assert results[0]["type"] == "user_graph_context"
202+
assert results[0]["context"] == "Context: User likes Python"
221203

222-
@patch('zep_crewai.user_storage.ThreadPoolExecutor')
223-
def test_search_user_graph(self, mock_executor):
204+
@patch('zep_crewai.utils.compose_context_string')
205+
@patch('zep_crewai.utils.ThreadPoolExecutor')
206+
def test_search_user_graph(self, mock_executor, mock_compose):
224207
"""Test search searches user graph correctly."""
225208
from zep_cloud.client import Zep
226209
from zep_cloud.types import EntityEdge, EntityNode, Episode, GraphSearchResults
@@ -249,47 +232,51 @@ def test_search_user_graph(self, mock_executor):
249232
mock_executor_instance = MagicMock()
250233
mock_executor.return_value.__enter__.return_value = mock_executor_instance
251234

252-
future_thread = MagicMock()
253-
future_thread.result.return_value = None
235+
# Create proper GraphSearchResults mocks
236+
edge_results = MagicMock(spec=GraphSearchResults)
237+
edge_results.edges = [mock_edge]
254238

239+
node_results = MagicMock(spec=GraphSearchResults)
240+
node_results.nodes = [mock_node]
241+
242+
episode_results = MagicMock(spec=GraphSearchResults)
243+
episode_results.episodes = []
244+
245+
# Mock futures
255246
future_edges = MagicMock()
256-
future_edges.result.return_value = [{
257-
"memory": mock_edge.fact,
258-
"type": "edge",
259-
"name": mock_edge.name,
260-
"attributes": mock_edge.attributes,
261-
"created_at": mock_edge.created_at,
262-
"valid_at": mock_edge.valid_at,
263-
"invalid_at": mock_edge.invalid_at,
264-
}]
247+
future_edges.result.return_value = edge_results
265248

266249
future_nodes = MagicMock()
267-
future_nodes.result.return_value = [{
268-
"memory": f"{mock_node.name}: {mock_node.summary}",
269-
"type": "node",
270-
"name": mock_node.name,
271-
"attributes": mock_node.attributes,
272-
"created_at": mock_node.created_at,
273-
}]
250+
future_nodes.result.return_value = node_results
274251

275252
future_episodes = MagicMock()
276-
future_episodes.result.return_value = []
253+
future_episodes.result.return_value = episode_results
277254

278255
mock_executor_instance.submit.side_effect = [
279-
future_thread, future_edges, future_nodes, future_episodes
256+
future_edges, future_nodes, future_episodes
280257
]
281258

282-
storage = ZepUserStorage(client=mock_client, user_id="test-user")
259+
# Mock compose_context_string to return formatted context
260+
mock_compose.return_value = "Context: User prefers Python. UserPreference: Programming language preference"
261+
262+
storage = ZepUserStorage(client=mock_client, user_id="test-user", thread_id="test-thread")
283263

284264
# Perform search
285265
results = storage.search("preferences", limit=5)
286266

287267
# Verify results
288-
assert len(results) == 2
289-
assert results[0]["type"] == "edge"
290-
assert "Python" in results[0]["memory"]
291-
assert results[1]["type"] == "node"
292-
assert "UserPreference" in results[1]["memory"]
268+
# Verify compose_context_string was called with correct arguments
269+
mock_compose.assert_called_once()
270+
call_args = mock_compose.call_args
271+
assert call_args[1]['edges'] == [mock_edge]
272+
assert call_args[1]['nodes'] == [mock_node]
273+
assert call_args[1]['episodes'] == []
274+
275+
# Verify results
276+
assert len(results) == 1
277+
assert results[0]["type"] == "user_graph_context"
278+
assert "User prefers Python" in results[0]["context"]
279+
assert "UserPreference" in results[0]["context"]
293280

294281
def test_get_context_with_thread(self):
295282
"""Test get_context retrieves context using thread.get_user_context."""
@@ -351,25 +338,6 @@ def test_get_context_with_raw_messages_mode(self):
351338

352339
assert context == "User: Hello\nAssistant: Hi there!\nUser: How are you?"
353340

354-
def test_get_context_without_thread_id(self):
355-
"""Test get_context returns None when no thread_id."""
356-
from zep_cloud.client import Zep
357-
358-
mock_client = MagicMock(spec=Zep)
359-
mock_client.thread = MagicMock()
360-
361-
storage = ZepUserStorage(
362-
client=mock_client,
363-
user_id="test-user"
364-
# No thread_id
365-
)
366-
367-
# Get context should return None
368-
context = storage.get_context()
369-
370-
# Verify no client methods were called
371-
assert context is None
372-
mock_client.thread.get_user_context.assert_not_called()
373341

374342
def test_get_context_with_empty_response(self):
375343
"""Test get_context handles empty context response."""
@@ -417,6 +385,7 @@ def test_search_with_filters(self):
417385
storage = ZepUserStorage(
418386
client=mock_client,
419387
user_id="test-user",
388+
thread_id="test-thread",
420389
search_filters=search_filters
421390
)
422391

@@ -434,7 +403,7 @@ def test_reset_does_nothing(self):
434403
from zep_cloud.client import Zep
435404

436405
mock_client = MagicMock(spec=Zep)
437-
storage = ZepUserStorage(client=mock_client, user_id="test-user")
406+
storage = ZepUserStorage(client=mock_client, user_id="test-user", thread_id="test-thread")
438407

439408
# Should not raise exception
440409
storage.reset()

0 commit comments

Comments
 (0)