zep-livekit 0.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.
- zep_livekit/__init__.py +11 -0
- zep_livekit/agent.py +432 -0
- zep_livekit/exceptions.py +15 -0
- zep_livekit-0.1.0.dist-info/METADATA +330 -0
- zep_livekit-0.1.0.dist-info/RECORD +6 -0
- zep_livekit-0.1.0.dist-info/WHEEL +4 -0
zep_livekit/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zep Memory integration for LiveKit.
|
|
3
|
+
|
|
4
|
+
This module provides a memory-enabled agent that integrates Zep with LiveKit's voice AI framework.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .agent import ZepGraphAgent, ZepUserAgent
|
|
8
|
+
from .exceptions import ZepLiveKitError
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = ["ZepUserAgent", "ZepGraphAgent", "ZepLiveKitError"]
|
zep_livekit/agent.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zep user memory integration for LiveKit agents.
|
|
3
|
+
|
|
4
|
+
This module provides the ZepUserAgent class that integrates Zep's memory capabilities
|
|
5
|
+
with LiveKit's voice AI agent framework
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from livekit import agents
|
|
13
|
+
from livekit.agents.llm.chat_context import ChatContext, ChatMessage
|
|
14
|
+
from zep_cloud import SearchFilters
|
|
15
|
+
from zep_cloud.client import AsyncZep
|
|
16
|
+
from zep_cloud.graph.utils import compose_context_string
|
|
17
|
+
from zep_cloud.types import Message, Reranker
|
|
18
|
+
|
|
19
|
+
from .exceptions import AgentConfigurationError
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ZepUserAgent(agents.Agent):
|
|
25
|
+
"""
|
|
26
|
+
LiveKit agent with Zep memory capabilities.
|
|
27
|
+
|
|
28
|
+
A drop-in replacement for LiveKit's Agent that adds persistent memory:
|
|
29
|
+
- Stores user and assistant messages in Zep threads
|
|
30
|
+
- Retrieves relevant context and injects it for personalized responses
|
|
31
|
+
- Accepts all standard LiveKit Agent parameters
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
zep_client: Initialized AsyncZep client for memory operations
|
|
35
|
+
user_id: User identifier for memory isolation and personalization
|
|
36
|
+
thread_id: Thread identifier for conversation continuity
|
|
37
|
+
user_message_name: Optional name to set on user messages in Zep
|
|
38
|
+
assistant_message_name: Optional name to set on assistant messages in Zep
|
|
39
|
+
**kwargs: All other LiveKit Agent parameters (chat_ctx, tools, stt, llm, tts, etc.)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
zep_client: AsyncZep,
|
|
46
|
+
user_id: str,
|
|
47
|
+
thread_id: str,
|
|
48
|
+
context_mode: Literal["basic", "summary"] | None = None,
|
|
49
|
+
user_message_name: str | None = None,
|
|
50
|
+
assistant_message_name: str | None = None,
|
|
51
|
+
**kwargs: Any,
|
|
52
|
+
) -> None:
|
|
53
|
+
if not user_id:
|
|
54
|
+
raise AgentConfigurationError("user_id must be a non-empty string")
|
|
55
|
+
if not thread_id:
|
|
56
|
+
raise AgentConfigurationError("thread_id must be a non-empty string")
|
|
57
|
+
|
|
58
|
+
# Initialize base Agent with all parameters passed through
|
|
59
|
+
super().__init__(**kwargs)
|
|
60
|
+
|
|
61
|
+
self._zep_client = zep_client
|
|
62
|
+
self._user_id = user_id
|
|
63
|
+
self._thread_id = thread_id
|
|
64
|
+
self._context_mode = context_mode or "basic"
|
|
65
|
+
self._user_message_name = user_message_name
|
|
66
|
+
self._assistant_message_name = assistant_message_name
|
|
67
|
+
|
|
68
|
+
async def on_enter(self) -> None:
|
|
69
|
+
"""Called when the agent enters a conversation."""
|
|
70
|
+
await super().on_enter()
|
|
71
|
+
|
|
72
|
+
# Hook into session events to capture assistant messages
|
|
73
|
+
if hasattr(self, "session"):
|
|
74
|
+
self._setup_session_handlers()
|
|
75
|
+
|
|
76
|
+
def _setup_session_handlers(self) -> None:
|
|
77
|
+
"""Set up event handlers on the session to capture assistant responses."""
|
|
78
|
+
|
|
79
|
+
@self.session.on("conversation_item_added")
|
|
80
|
+
def on_conversation_item_added(event: Any) -> None:
|
|
81
|
+
"""Handle conversation item addition events to capture assistant responses."""
|
|
82
|
+
# Schedule async storage to avoid blocking event processing
|
|
83
|
+
asyncio.create_task(self._handle_conversation_item(event))
|
|
84
|
+
|
|
85
|
+
async def _handle_conversation_item(self, event: Any) -> None:
|
|
86
|
+
"""Handle conversation item from session event."""
|
|
87
|
+
try:
|
|
88
|
+
# Extract conversation item from event
|
|
89
|
+
if not hasattr(event, "item"):
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
item = event.item
|
|
93
|
+
|
|
94
|
+
# Validate item has required message attributes
|
|
95
|
+
if not (hasattr(item, "role") and hasattr(item, "content")):
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
role = item.role
|
|
99
|
+
content = item.content
|
|
100
|
+
|
|
101
|
+
# Only store assistant messages (user messages handled in on_user_turn_completed)
|
|
102
|
+
if role == "assistant":
|
|
103
|
+
content_text = self._extract_text_content(content)
|
|
104
|
+
if content_text.strip():
|
|
105
|
+
await self._store_assistant_message(content_text.strip(), item)
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Failed to handle conversation item: {e}")
|
|
109
|
+
|
|
110
|
+
def _extract_text_content(self, content: Any) -> str:
|
|
111
|
+
"""Extract text content from various LiveKit content formats."""
|
|
112
|
+
if isinstance(content, str):
|
|
113
|
+
return content
|
|
114
|
+
|
|
115
|
+
if isinstance(content, list):
|
|
116
|
+
text_parts = []
|
|
117
|
+
for item in content:
|
|
118
|
+
if hasattr(item, "text"):
|
|
119
|
+
text_parts.append(item.text)
|
|
120
|
+
elif isinstance(item, str):
|
|
121
|
+
text_parts.append(item)
|
|
122
|
+
return " ".join(text_parts)
|
|
123
|
+
|
|
124
|
+
return str(content)
|
|
125
|
+
|
|
126
|
+
async def _store_assistant_message(self, content_text: str, item: Any) -> None:
|
|
127
|
+
"""Store assistant message in Zep thread memory."""
|
|
128
|
+
try:
|
|
129
|
+
# Use custom assistant name if provided, otherwise fallback to item name
|
|
130
|
+
message_name = self._assistant_message_name or getattr(item, "name", None)
|
|
131
|
+
|
|
132
|
+
zep_message = Message(content=content_text, role="assistant", name=message_name)
|
|
133
|
+
|
|
134
|
+
await self._zep_client.thread.add_messages(
|
|
135
|
+
thread_id=self._thread_id, messages=[zep_message]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning(f"Failed to store assistant response: {e}")
|
|
140
|
+
|
|
141
|
+
async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Handle user turn completion - store message and inject memory context.
|
|
144
|
+
|
|
145
|
+
1. Store user message in Zep
|
|
146
|
+
2. Retrieve relevant context from Zep
|
|
147
|
+
3. Inject context into conversation
|
|
148
|
+
"""
|
|
149
|
+
await super().on_user_turn_completed(turn_ctx, new_message)
|
|
150
|
+
|
|
151
|
+
user_text = new_message.text_content
|
|
152
|
+
if not user_text or not user_text.strip():
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
zep_message = Message(
|
|
157
|
+
content=user_text.strip(), role="user", name=self._user_message_name
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
await self._zep_client.thread.add_messages(
|
|
161
|
+
thread_id=self._thread_id, messages=[zep_message]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning(f"Failed to store user message in Zep: {e}")
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
memory_result = await self._zep_client.thread.get_user_context(
|
|
169
|
+
thread_id=self._thread_id, mode=self._context_mode
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if memory_result and memory_result.context:
|
|
173
|
+
context = memory_result.context
|
|
174
|
+
|
|
175
|
+
turn_ctx.add_message(role="system", content=f"Relevant user context:\n{context}")
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.warning(f"Failed to retrieve context from Zep: {e}")
|
|
179
|
+
|
|
180
|
+
async def on_exit(self) -> None:
|
|
181
|
+
"""Called when the agent exits a conversation."""
|
|
182
|
+
await super().on_exit()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class ZepGraphAgent(agents.Agent):
|
|
186
|
+
"""
|
|
187
|
+
LiveKit agent with Zep graph memory capabilities.
|
|
188
|
+
|
|
189
|
+
A drop-in replacement for LiveKit's Agent that adds persistent knowledge storage:
|
|
190
|
+
- Stores user and assistant messages in Zep graph
|
|
191
|
+
- Performs hybrid search to retrieve relevant context from edges, nodes, and episodes
|
|
192
|
+
- Uses smart context composition for comprehensive knowledge retrieval
|
|
193
|
+
- Optional user name prefixing for message attribution
|
|
194
|
+
|
|
195
|
+
User Identification:
|
|
196
|
+
- If user_name is provided, messages are stored as "[UserName]: message" and "[Assistant]: response"
|
|
197
|
+
- If user_name is None, messages are stored without prefixes
|
|
198
|
+
- Designed for per-user agent instances (typical deployment pattern)
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
zep_client: Initialized AsyncZep client for memory operations
|
|
202
|
+
graph_id: Graph identifier for knowledge storage
|
|
203
|
+
user_name: Optional user name for message prefixing (e.g., "Alice", "Bob")
|
|
204
|
+
facts_limit: Maximum number of facts/edges to retrieve (default: 20)
|
|
205
|
+
entity_limit: Maximum number of entities/nodes to retrieve (default: 5)
|
|
206
|
+
episode_limit: Maximum number of episodes to retrieve (default: 3)
|
|
207
|
+
search_filters: Optional filters for graph search
|
|
208
|
+
reranker: Optional reranker for search results
|
|
209
|
+
**kwargs: All other LiveKit Agent parameters
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
zep_client: AsyncZep,
|
|
216
|
+
graph_id: str,
|
|
217
|
+
user_name: str | None = None,
|
|
218
|
+
facts_limit: int = 15,
|
|
219
|
+
entity_limit: int = 5,
|
|
220
|
+
episode_limit: int = 2,
|
|
221
|
+
search_filters: SearchFilters | None = None,
|
|
222
|
+
reranker: Reranker | None = "rrf",
|
|
223
|
+
**kwargs: Any,
|
|
224
|
+
) -> None:
|
|
225
|
+
if not graph_id:
|
|
226
|
+
raise AgentConfigurationError("graph_id must be a non-empty string")
|
|
227
|
+
|
|
228
|
+
# Initialize base Agent with all parameters passed through
|
|
229
|
+
super().__init__(**kwargs)
|
|
230
|
+
|
|
231
|
+
self._zep_client = zep_client
|
|
232
|
+
self._graph_id = graph_id
|
|
233
|
+
self._user_name = user_name
|
|
234
|
+
self._facts_limit = facts_limit
|
|
235
|
+
self._entity_limit = entity_limit
|
|
236
|
+
self._episode_limit = episode_limit
|
|
237
|
+
self._search_filters = search_filters
|
|
238
|
+
self._reranker = reranker
|
|
239
|
+
|
|
240
|
+
async def on_enter(self) -> None:
|
|
241
|
+
"""Called when the agent enters a conversation."""
|
|
242
|
+
await super().on_enter()
|
|
243
|
+
|
|
244
|
+
# Hook into session events to capture assistant messages
|
|
245
|
+
if hasattr(self, "session"):
|
|
246
|
+
self._setup_session_handlers()
|
|
247
|
+
|
|
248
|
+
def _setup_session_handlers(self) -> None:
|
|
249
|
+
"""Set up event handlers on the session to capture assistant responses."""
|
|
250
|
+
|
|
251
|
+
@self.session.on("conversation_item_added")
|
|
252
|
+
def on_conversation_item_added(event: Any) -> None:
|
|
253
|
+
"""Handle conversation item addition events to capture assistant responses."""
|
|
254
|
+
# Schedule async storage to avoid blocking event processing
|
|
255
|
+
asyncio.create_task(self._handle_conversation_item(event))
|
|
256
|
+
|
|
257
|
+
async def _handle_conversation_item(self, event: Any) -> None:
|
|
258
|
+
"""Handle conversation item from session event."""
|
|
259
|
+
try:
|
|
260
|
+
# Extract conversation item from event
|
|
261
|
+
if not hasattr(event, "item"):
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
item = event.item
|
|
265
|
+
|
|
266
|
+
# Validate item has required message attributes
|
|
267
|
+
if not (hasattr(item, "role") and hasattr(item, "content")):
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
role = item.role
|
|
271
|
+
content = item.content
|
|
272
|
+
|
|
273
|
+
# Only store assistant messages (user messages handled in on_user_turn_completed)
|
|
274
|
+
if role == "assistant":
|
|
275
|
+
content_text = self._extract_text_content(content)
|
|
276
|
+
if content_text.strip():
|
|
277
|
+
await self._store_assistant_message(content_text.strip(), item)
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.error(f"Failed to handle conversation item: {e}")
|
|
281
|
+
|
|
282
|
+
def _extract_text_content(self, content: Any) -> str:
|
|
283
|
+
"""Extract text content from various LiveKit content formats."""
|
|
284
|
+
if isinstance(content, str):
|
|
285
|
+
return content
|
|
286
|
+
|
|
287
|
+
if isinstance(content, list):
|
|
288
|
+
text_parts = []
|
|
289
|
+
for item in content:
|
|
290
|
+
if hasattr(item, "text"):
|
|
291
|
+
text_parts.append(item.text)
|
|
292
|
+
elif isinstance(item, str):
|
|
293
|
+
text_parts.append(item)
|
|
294
|
+
return " ".join(text_parts)
|
|
295
|
+
|
|
296
|
+
return str(content)
|
|
297
|
+
|
|
298
|
+
async def _store_assistant_message(self, content_text: str, item: Any) -> None:
|
|
299
|
+
"""Store assistant message in Zep graph."""
|
|
300
|
+
try:
|
|
301
|
+
# Prefix assistant messages for consistency when user has a name
|
|
302
|
+
if self._user_name:
|
|
303
|
+
message_data = f"[Assistant]: {content_text}"
|
|
304
|
+
else:
|
|
305
|
+
message_data = content_text
|
|
306
|
+
|
|
307
|
+
await self._zep_client.graph.add(
|
|
308
|
+
graph_id=self._graph_id, type="message", data=message_data
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.warning(f"Failed to store assistant response: {e}")
|
|
313
|
+
|
|
314
|
+
async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None:
|
|
315
|
+
"""
|
|
316
|
+
Handle user turn completion - store message and inject memory context.
|
|
317
|
+
|
|
318
|
+
1. Store user message in Zep graph
|
|
319
|
+
2. Perform hybrid search to retrieve relevant context
|
|
320
|
+
3. Inject context into conversation using smart composition
|
|
321
|
+
"""
|
|
322
|
+
await super().on_user_turn_completed(turn_ctx, new_message)
|
|
323
|
+
|
|
324
|
+
user_text = new_message.text_content
|
|
325
|
+
if not user_text or not user_text.strip():
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
# Step 1: Store user message in Zep graph with user identification
|
|
329
|
+
try:
|
|
330
|
+
# Prefix message with user name if provided
|
|
331
|
+
message_data = user_text.strip()
|
|
332
|
+
if self._user_name:
|
|
333
|
+
message_data = f"[{self._user_name}]: {message_data}"
|
|
334
|
+
|
|
335
|
+
await self._zep_client.graph.add(
|
|
336
|
+
graph_id=self._graph_id, type="message", data=message_data
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.warning(f"Failed to store user message in Zep graph: {e}")
|
|
341
|
+
|
|
342
|
+
# Step 2: Retrieve relevant context using hybrid search
|
|
343
|
+
try:
|
|
344
|
+
context = await self._retrieve_graph_context(user_text[:400]) # Limit query length
|
|
345
|
+
|
|
346
|
+
if context:
|
|
347
|
+
# Step 3: Inject context as system message
|
|
348
|
+
turn_ctx.add_message(
|
|
349
|
+
role="system", content=f"Relevant knowledge from memory:\n{context}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.warning(f"Failed to retrieve context from Zep graph: {e}")
|
|
354
|
+
|
|
355
|
+
async def _retrieve_graph_context(self, query: str) -> str | None:
|
|
356
|
+
"""
|
|
357
|
+
Retrieve and compose context from graph using hybrid search.
|
|
358
|
+
|
|
359
|
+
- Search for edges (facts), nodes (entities) and episodes concurrently
|
|
360
|
+
- Compose a context string using the graph utilities
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
# Perform parallel searches like in autogen
|
|
364
|
+
search_functions = []
|
|
365
|
+
|
|
366
|
+
if self._facts_limit:
|
|
367
|
+
# Search for facts/relationships (edges)
|
|
368
|
+
search_functions.append(
|
|
369
|
+
self._zep_client.graph.search(
|
|
370
|
+
graph_id=self._graph_id,
|
|
371
|
+
query=query,
|
|
372
|
+
limit=self._facts_limit,
|
|
373
|
+
search_filters=self._search_filters,
|
|
374
|
+
reranker=self._reranker,
|
|
375
|
+
scope="edges",
|
|
376
|
+
),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if self._entity_limit:
|
|
380
|
+
# Search for entities (nodes)
|
|
381
|
+
search_functions.append(
|
|
382
|
+
self._zep_client.graph.search(
|
|
383
|
+
graph_id=self._graph_id,
|
|
384
|
+
query=query,
|
|
385
|
+
limit=self._entity_limit,
|
|
386
|
+
search_filters=self._search_filters,
|
|
387
|
+
reranker=self._reranker,
|
|
388
|
+
scope="nodes",
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if self._episode_limit:
|
|
393
|
+
# Search for episodes
|
|
394
|
+
search_functions.append(
|
|
395
|
+
self._zep_client.graph.search(
|
|
396
|
+
graph_id=self._graph_id,
|
|
397
|
+
query=query,
|
|
398
|
+
limit=self._episode_limit,
|
|
399
|
+
search_filters=self._search_filters,
|
|
400
|
+
reranker=self._reranker,
|
|
401
|
+
scope="episodes",
|
|
402
|
+
),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
results = await asyncio.gather(*search_functions)
|
|
406
|
+
|
|
407
|
+
edges = []
|
|
408
|
+
nodes = []
|
|
409
|
+
episodes = []
|
|
410
|
+
|
|
411
|
+
# Collect all results
|
|
412
|
+
for result in results:
|
|
413
|
+
if result.edges:
|
|
414
|
+
edges.extend(result.edges)
|
|
415
|
+
if result.nodes:
|
|
416
|
+
nodes.extend(result.nodes)
|
|
417
|
+
if result.episodes:
|
|
418
|
+
episodes.extend(result.episodes)
|
|
419
|
+
|
|
420
|
+
if not edges and not nodes and not episodes:
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
context = compose_context_string(edges, nodes, episodes)
|
|
424
|
+
return context
|
|
425
|
+
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.error(f"Error retrieving graph context: {e}")
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
async def on_exit(self) -> None:
|
|
431
|
+
"""Called when the agent exits a conversation."""
|
|
432
|
+
await super().on_exit()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LiveKit-specific exceptions for Zep integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ZepLiveKitError(Exception):
|
|
7
|
+
"""Base exception for Zep LiveKit integration errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentConfigurationError(ZepLiveKitError):
|
|
13
|
+
"""Raised when agent configuration is invalid."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zep-livekit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LiveKit integration for Zep
|
|
5
|
+
Project-URL: Homepage, https://github.com/getzep/zep
|
|
6
|
+
Project-URL: Documentation, https://help.getzep.com/integrations/livekit
|
|
7
|
+
Project-URL: Repository, https://github.com/getzep/zep
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/getzep/zep/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/getzep/zep/blob/main/integrations/python/zep_livekit/CHANGELOG.md
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: livekit-agents[openai,silero]>=1.0.0
|
|
12
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
13
|
+
Requires-Dist: zep-cloud>=3.4.3
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Zep LiveKit Integration
|
|
23
|
+
|
|
24
|
+
Add persistent memory to your LiveKit voice agents with [Zep's](https://www.getzep.com) memory capabilities. This integration provides both conversational memory for user sessions and shared knowledge graphs for cross-session information storage.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### Install Dependencies
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install zep-livekit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Environment Setup
|
|
35
|
+
|
|
36
|
+
Configure your environment with the required API keys and LiveKit connection details:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Required API keys
|
|
40
|
+
export OPENAI_API_KEY="your-openai-api-key"
|
|
41
|
+
export ZEP_API_KEY="your-zep-cloud-api-key"
|
|
42
|
+
|
|
43
|
+
# LiveKit configuration
|
|
44
|
+
export LIVEKIT_URL="your-livekit-url"
|
|
45
|
+
export LIVEKIT_API_KEY="your-livekit-api-key"
|
|
46
|
+
export LIVEKIT_API_SECRET="your-livekit-api-secret"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Memory Architecture
|
|
50
|
+
|
|
51
|
+
Zep uses a unified temporal knowledge graph where all conversation data contributes to a single, dynamic graph structure. The LiveKit integration provides two complementary approaches to interact with this unified memory:
|
|
52
|
+
|
|
53
|
+
### Thread-Based Memory Access (ZepUserAgent)
|
|
54
|
+
- **Purpose**: Structured conversation history and contextual retrieval
|
|
55
|
+
- **Storage**: Messages stored in threads that automatically contribute to the user's unified graph
|
|
56
|
+
- **Retrieval**: Context blocks assembled with temporal information from the graph
|
|
57
|
+
- **Use Case**: Personal assistants, customer support, tutoring sessions
|
|
58
|
+
|
|
59
|
+
### Direct Graph Memory Access (ZepGraphAgent)
|
|
60
|
+
- **Purpose**: Direct interaction with the knowledge graph for shared information
|
|
61
|
+
- **Storage**: Information stored directly as facts, entities, and relationships in the graph
|
|
62
|
+
- **Retrieval**: Semantic search across the entire temporal knowledge graph
|
|
63
|
+
- **Use Case**: Knowledge bases, collaborative assistants, information systems
|
|
64
|
+
|
|
65
|
+
Both approaches work with the same underlying temporal knowledge graph - threads automatically enrich the graph with entities, relationships, and facts, while direct graph access allows for explicit knowledge management.
|
|
66
|
+
|
|
67
|
+
## Thread-Based Memory Access
|
|
68
|
+
|
|
69
|
+
Using structured conversation threads that automatically contribute to your unified graph:
|
|
70
|
+
|
|
71
|
+
### Basic Setup
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import os
|
|
75
|
+
from livekit import agents
|
|
76
|
+
from livekit.plugins import openai, silero
|
|
77
|
+
from zep_cloud.client import AsyncZep
|
|
78
|
+
from zep_livekit import ZepUserAgent
|
|
79
|
+
|
|
80
|
+
async def entrypoint(ctx: agents.JobContext):
|
|
81
|
+
# Initialize Zep client
|
|
82
|
+
zep_client = AsyncZep(api_key=os.getenv("ZEP_API_KEY"))
|
|
83
|
+
|
|
84
|
+
# Create user and thread
|
|
85
|
+
user_id = "user_123"
|
|
86
|
+
thread_id = f"conversation_{user_id}"
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
await zep_client.user.get(user_id=user_id)
|
|
90
|
+
except:
|
|
91
|
+
await zep_client.user.add(user_id=user_id, first_name="Alice")
|
|
92
|
+
|
|
93
|
+
await zep_client.thread.create(thread_id=thread_id, user_id=user_id)
|
|
94
|
+
|
|
95
|
+
# Connect to room
|
|
96
|
+
await ctx.connect()
|
|
97
|
+
|
|
98
|
+
# Create session with providers
|
|
99
|
+
session = agents.AgentSession(
|
|
100
|
+
stt=openai.STT(),
|
|
101
|
+
llm=openai.LLM(model="gpt-4o-mini"),
|
|
102
|
+
tts=openai.TTS(),
|
|
103
|
+
vad=silero.VAD.load(),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Create memory-enabled agent
|
|
107
|
+
agent = ZepUserAgent(
|
|
108
|
+
zep_client=zep_client,
|
|
109
|
+
user_id=user_id,
|
|
110
|
+
thread_id=thread_id,
|
|
111
|
+
instructions="You are a helpful assistant with persistent memory."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Start conversation with memory
|
|
115
|
+
await session.start(agent=agent, room=ctx.room)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Advanced Configuration
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Enhanced user agent with message attribution
|
|
122
|
+
agent = ZepUserAgent(
|
|
123
|
+
zep_client=zep_client,
|
|
124
|
+
user_id="user_123",
|
|
125
|
+
thread_id="conversation_456",
|
|
126
|
+
context_mode="summary", # or "basic"
|
|
127
|
+
user_message_name="Alice", # Name for user messages in Zep
|
|
128
|
+
assistant_message_name="Assistant", # Name for assistant messages
|
|
129
|
+
instructions="You remember our previous conversations and preferences."
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Direct Graph Memory Access
|
|
134
|
+
|
|
135
|
+
For explicit control over what gets stored as facts, entities, and relationships in your unified graph:
|
|
136
|
+
|
|
137
|
+
### Basic Setup
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from zep_livekit import ZepGraphAgent
|
|
141
|
+
|
|
142
|
+
async def entrypoint(ctx: agents.JobContext):
|
|
143
|
+
# Initialize Zep client
|
|
144
|
+
zep_client = AsyncZep(api_key=os.getenv("ZEP_API_KEY"))
|
|
145
|
+
|
|
146
|
+
# Create or get knowledge graph
|
|
147
|
+
graph_id = "company_knowledge_base"
|
|
148
|
+
try:
|
|
149
|
+
await zep_client.graph.get(graph_id)
|
|
150
|
+
except:
|
|
151
|
+
await zep_client.graph.create(
|
|
152
|
+
graph_id=graph_id,
|
|
153
|
+
name="Company Knowledge Base",
|
|
154
|
+
description="Shared knowledge across all conversations"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Connect to room
|
|
158
|
+
await ctx.connect()
|
|
159
|
+
|
|
160
|
+
# Create session
|
|
161
|
+
session = agents.AgentSession(
|
|
162
|
+
stt=openai.STT(),
|
|
163
|
+
llm=openai.LLM(model="gpt-4o-mini"),
|
|
164
|
+
tts=openai.TTS(),
|
|
165
|
+
vad=silero.VAD.load(),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Create knowledge-enabled agent
|
|
169
|
+
agent = ZepGraphAgent(
|
|
170
|
+
zep_client=zep_client,
|
|
171
|
+
graph_id=graph_id,
|
|
172
|
+
user_name="Alice", # Optional: for message attribution
|
|
173
|
+
facts_limit=15, # Max facts to retrieve
|
|
174
|
+
entity_limit=5, # Max entities to retrieve
|
|
175
|
+
episode_limit=3, # Max episodes to retrieve
|
|
176
|
+
instructions="""
|
|
177
|
+
You have access to a shared knowledge graph.
|
|
178
|
+
Store important facts for future reference and
|
|
179
|
+
search your knowledge to answer questions accurately.
|
|
180
|
+
"""
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
await session.start(agent=agent, room=ctx.room)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
## Querying Your Unified Graph
|
|
188
|
+
|
|
189
|
+
### Thread-Based Context Retrieval
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# Get conversation context assembled from the unified graph
|
|
193
|
+
memory_result = await zep_client.thread.get_user_context(
|
|
194
|
+
thread_id="conversation_123",
|
|
195
|
+
mode="basic" # or "summary"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if memory_result and memory_result.context:
|
|
199
|
+
print(f"Context from unified graph: {memory_result.context}")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Direct Graph Search
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# Search directly across the temporal knowledge graph
|
|
206
|
+
search_results = await zep_client.graph.search(
|
|
207
|
+
graph_id="company_knowledge_base",
|
|
208
|
+
query="Python programming best practices",
|
|
209
|
+
limit=10,
|
|
210
|
+
scope="edges" # facts, or "nodes" (entities), "episodes"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Use Zep's utility to compose context
|
|
214
|
+
from zep_cloud.graph.utils import compose_context_string
|
|
215
|
+
context = compose_context_string(
|
|
216
|
+
search_results.edges,
|
|
217
|
+
search_results.nodes,
|
|
218
|
+
search_results.episodes
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Agent Comparison
|
|
223
|
+
|
|
224
|
+
| Agent Type | Best For | Memory Access Method | Use Cases |
|
|
225
|
+
|------------|----------|-------------------|-----------|
|
|
226
|
+
| **ZepUserAgent** | Personal assistants | Thread-based access to unified graph | Conversation continuity, customer support, tutoring |
|
|
227
|
+
| **ZepGraphAgent** | Knowledge systems | Direct graph access to unified graph | Shared information, collaborative assistants, knowledge bases |
|
|
228
|
+
|
|
229
|
+
### When to Use Each
|
|
230
|
+
|
|
231
|
+
**Use ZepUserAgent when:**
|
|
232
|
+
- Building personal assistants with structured conversation flow
|
|
233
|
+
- Need conversation history and context retrieval across sessions
|
|
234
|
+
- Want automatic thread-to-graph ingestion without manual management
|
|
235
|
+
- Prefer working with conversation-based memory access patterns
|
|
236
|
+
|
|
237
|
+
**Use ZepGraphAgent when:**
|
|
238
|
+
- Building knowledge management systems with explicit fact storage
|
|
239
|
+
- Need direct semantic search across the temporal knowledge graph
|
|
240
|
+
- Want to manually control what information gets stored as facts
|
|
241
|
+
- Building systems where information should be immediately searchable across entities
|
|
242
|
+
|
|
243
|
+
## Complete Examples
|
|
244
|
+
|
|
245
|
+
### Personal Assistant
|
|
246
|
+
```bash
|
|
247
|
+
# examples/voice_assistant.py
|
|
248
|
+
python examples/voice_assistant.py
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Knowledge Assistant
|
|
252
|
+
```bash
|
|
253
|
+
# examples/graph_voice_assistant.py
|
|
254
|
+
python examples/graph_voice_assistant.py
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## API Reference
|
|
258
|
+
|
|
259
|
+
### ZepUserAgent
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
class ZepUserAgent(agents.Agent):
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
*,
|
|
266
|
+
zep_client: AsyncZep,
|
|
267
|
+
user_id: str,
|
|
268
|
+
thread_id: str,
|
|
269
|
+
context_mode: Literal["basic", "summary"] = "basic",
|
|
270
|
+
user_message_name: str | None = None,
|
|
271
|
+
assistant_message_name: str | None = None,
|
|
272
|
+
**kwargs: Any # All LiveKit Agent parameters
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### ZepGraphAgent
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
class ZepGraphAgent(agents.Agent):
|
|
280
|
+
def __init__(
|
|
281
|
+
self,
|
|
282
|
+
*,
|
|
283
|
+
zep_client: AsyncZep,
|
|
284
|
+
graph_id: str,
|
|
285
|
+
user_name: str | None = None,
|
|
286
|
+
facts_limit: int = 15,
|
|
287
|
+
entity_limit: int = 5,
|
|
288
|
+
episode_limit: int = 2,
|
|
289
|
+
search_filters: SearchFilters | None = None,
|
|
290
|
+
reranker: Reranker | None = "rrf",
|
|
291
|
+
**kwargs: Any # All LiveKit Agent parameters
|
|
292
|
+
)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
## Development
|
|
297
|
+
|
|
298
|
+
### Setup Development Environment
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
git clone https://github.com/getzep/zep
|
|
302
|
+
cd zep/integrations/python/zep_livekit
|
|
303
|
+
make install
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Development Workflow
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
make format # Format code with ruff
|
|
310
|
+
make lint # Run linting checks
|
|
311
|
+
make type-check # Run MyPy type checking
|
|
312
|
+
make test # Run test suite
|
|
313
|
+
make pre-commit # Full pre-commit workflow
|
|
314
|
+
make ci # Strict CI-style checks
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Support
|
|
318
|
+
|
|
319
|
+
### Zep Resources
|
|
320
|
+
- 📖 [Zep Documentation](https://help.getzep.com)
|
|
321
|
+
- 💬 [Zep Discord Community](https://discord.gg/W8xaHrqWVc)
|
|
322
|
+
- 🐛 [GitHub Issues](https://github.com/getzep/zep/issues)
|
|
323
|
+
- 📧 [Email Support](mailto:support@getzep.com)
|
|
324
|
+
|
|
325
|
+
### LiveKit Resources
|
|
326
|
+
- 📖 [LiveKit Documentation](https://docs.livekit.io)
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
Built with ❤️ by the [Zep](https://www.getzep.com) team.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
zep_livekit/__init__.py,sha256=AnfAn284Ov_OOJThgXyWZ8JF8FojrSgy8B_Ijp95tMo,318
|
|
2
|
+
zep_livekit/agent.py,sha256=tcxCT7i7pl61N2sGRwR1zpVJOdXg7T6fIOlE0ZwjgRM,16269
|
|
3
|
+
zep_livekit/exceptions.py,sha256=W1JAf3gTrRX-R8ozyEDDPN3SadlYujoEcBQmXVWKbjk,278
|
|
4
|
+
zep_livekit-0.1.0.dist-info/METADATA,sha256=ck3rWQT8zuUzCNXOqJ_VQK_vdcIQL1cPlrpDlnn57zs,10129
|
|
5
|
+
zep_livekit-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
zep_livekit-0.1.0.dist-info/RECORD,,
|