noesium 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.
- noesium/core/__init__.py +4 -0
- noesium/core/agent/__init__.py +14 -0
- noesium/core/agent/base.py +227 -0
- noesium/core/consts.py +6 -0
- noesium/core/goalith/conflict/conflict.py +104 -0
- noesium/core/goalith/conflict/detector.py +53 -0
- noesium/core/goalith/decomposer/__init__.py +6 -0
- noesium/core/goalith/decomposer/base.py +46 -0
- noesium/core/goalith/decomposer/callable_decomposer.py +65 -0
- noesium/core/goalith/decomposer/llm_decomposer.py +326 -0
- noesium/core/goalith/decomposer/prompts.py +140 -0
- noesium/core/goalith/decomposer/simple_decomposer.py +61 -0
- noesium/core/goalith/errors.py +22 -0
- noesium/core/goalith/goalgraph/graph.py +526 -0
- noesium/core/goalith/goalgraph/node.py +179 -0
- noesium/core/goalith/replanner/base.py +31 -0
- noesium/core/goalith/replanner/replanner.py +36 -0
- noesium/core/goalith/service.py +26 -0
- noesium/core/llm/__init__.py +154 -0
- noesium/core/llm/base.py +152 -0
- noesium/core/llm/litellm.py +528 -0
- noesium/core/llm/llamacpp.py +487 -0
- noesium/core/llm/message.py +184 -0
- noesium/core/llm/ollama.py +459 -0
- noesium/core/llm/openai.py +520 -0
- noesium/core/llm/openrouter.py +89 -0
- noesium/core/llm/prompt.py +551 -0
- noesium/core/memory/__init__.py +11 -0
- noesium/core/memory/base.py +464 -0
- noesium/core/memory/memu/__init__.py +24 -0
- noesium/core/memory/memu/config/__init__.py +26 -0
- noesium/core/memory/memu/config/activity/config.py +46 -0
- noesium/core/memory/memu/config/event/config.py +46 -0
- noesium/core/memory/memu/config/markdown_config.py +241 -0
- noesium/core/memory/memu/config/profile/config.py +48 -0
- noesium/core/memory/memu/llm_adapter.py +129 -0
- noesium/core/memory/memu/memory/__init__.py +31 -0
- noesium/core/memory/memu/memory/actions/__init__.py +40 -0
- noesium/core/memory/memu/memory/actions/add_activity_memory.py +299 -0
- noesium/core/memory/memu/memory/actions/base_action.py +342 -0
- noesium/core/memory/memu/memory/actions/cluster_memories.py +262 -0
- noesium/core/memory/memu/memory/actions/generate_suggestions.py +198 -0
- noesium/core/memory/memu/memory/actions/get_available_categories.py +66 -0
- noesium/core/memory/memu/memory/actions/link_related_memories.py +515 -0
- noesium/core/memory/memu/memory/actions/run_theory_of_mind.py +254 -0
- noesium/core/memory/memu/memory/actions/update_memory_with_suggestions.py +514 -0
- noesium/core/memory/memu/memory/embeddings.py +130 -0
- noesium/core/memory/memu/memory/file_manager.py +306 -0
- noesium/core/memory/memu/memory/memory_agent.py +578 -0
- noesium/core/memory/memu/memory/recall_agent.py +376 -0
- noesium/core/memory/memu/memory_store.py +628 -0
- noesium/core/memory/models.py +149 -0
- noesium/core/msgbus/__init__.py +12 -0
- noesium/core/msgbus/base.py +395 -0
- noesium/core/orchestrix/__init__.py +0 -0
- noesium/core/py.typed +0 -0
- noesium/core/routing/__init__.py +20 -0
- noesium/core/routing/base.py +66 -0
- noesium/core/routing/router.py +241 -0
- noesium/core/routing/strategies/__init__.py +9 -0
- noesium/core/routing/strategies/dynamic_complexity.py +361 -0
- noesium/core/routing/strategies/self_assessment.py +147 -0
- noesium/core/routing/types.py +38 -0
- noesium/core/toolify/__init__.py +39 -0
- noesium/core/toolify/base.py +360 -0
- noesium/core/toolify/config.py +138 -0
- noesium/core/toolify/mcp_integration.py +275 -0
- noesium/core/toolify/registry.py +214 -0
- noesium/core/toolify/toolkits/__init__.py +1 -0
- noesium/core/tracing/__init__.py +37 -0
- noesium/core/tracing/langgraph_hooks.py +308 -0
- noesium/core/tracing/opik_tracing.py +144 -0
- noesium/core/tracing/token_tracker.py +166 -0
- noesium/core/utils/__init__.py +10 -0
- noesium/core/utils/logging.py +172 -0
- noesium/core/utils/statistics.py +12 -0
- noesium/core/utils/typing.py +17 -0
- noesium/core/vector_store/__init__.py +79 -0
- noesium/core/vector_store/base.py +94 -0
- noesium/core/vector_store/pgvector.py +304 -0
- noesium/core/vector_store/weaviate.py +383 -0
- noesium-0.1.0.dist-info/METADATA +525 -0
- noesium-0.1.0.dist-info/RECORD +86 -0
- noesium-0.1.0.dist-info/WHEEL +5 -0
- noesium-0.1.0.dist-info/licenses/LICENSE +21 -0
- noesium-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RecallAgent for MemU Memory System
|
|
3
|
+
|
|
4
|
+
A simple workflow for intelligent memory retrieval based on markdown configurations.
|
|
5
|
+
Handles context=all (full content) and context=rag (search with limitations) based on config.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import math
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List
|
|
13
|
+
|
|
14
|
+
from ..config.markdown_config import get_config_manager
|
|
15
|
+
from .file_manager import MemoryFileManager
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RecallAgent:
|
|
21
|
+
"""
|
|
22
|
+
Enhanced workflow for intelligent memory retrieval with three distinct methods.
|
|
23
|
+
Automatically scans {agent_id}/{user_id}/{category}.md files in memory directory.
|
|
24
|
+
|
|
25
|
+
Three core retrieval methods:
|
|
26
|
+
1. retrieve_default_category: Get content from ['profile', 'event'] categories
|
|
27
|
+
2. retrieve_relevant_category: Get top-k similar category names (excluding profile/event/activity)
|
|
28
|
+
3. retrieve_relevant_memories: Get top-k memories using embedding search
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, memory_dir: str = "memu/server/memory", agent_id: str = None, user_id: str = None):
|
|
32
|
+
"""
|
|
33
|
+
Initialize Recall Agent
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
memory_dir: Directory where memory files are stored
|
|
37
|
+
agent_id: Agent identifier
|
|
38
|
+
user_id: User identifier
|
|
39
|
+
"""
|
|
40
|
+
self.memory_dir = Path(memory_dir)
|
|
41
|
+
|
|
42
|
+
# Initialize config manager
|
|
43
|
+
self.config_manager = get_config_manager()
|
|
44
|
+
self.memory_types = self.config_manager.get_file_types_mapping()
|
|
45
|
+
|
|
46
|
+
# Initialize file-based storage manager
|
|
47
|
+
self.storage_manager = MemoryFileManager(memory_dir, agent_id=agent_id, user_id=user_id)
|
|
48
|
+
|
|
49
|
+
# Initialize embedding client for semantic search
|
|
50
|
+
try:
|
|
51
|
+
self.embedding_client = get_default_embedding_client()
|
|
52
|
+
self.semantic_search_enabled = True
|
|
53
|
+
logger.info("Semantic search enabled")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.warning(f"Failed to initialize embedding client: {e}. Semantic search disabled.")
|
|
56
|
+
self.embedding_client = None
|
|
57
|
+
self.semantic_search_enabled = False
|
|
58
|
+
|
|
59
|
+
# Default categories for core retrieval
|
|
60
|
+
self.default_categories = ["profile", "event"]
|
|
61
|
+
|
|
62
|
+
logger.info(f"Recall Agent initialized with memory directory: {self.memory_dir}")
|
|
63
|
+
|
|
64
|
+
def retrieve_default_category(self, agent_id: str = None, user_id: str = None) -> Dict[str, Any]:
|
|
65
|
+
"""
|
|
66
|
+
Method 1: Retrieve Default Category
|
|
67
|
+
Get complete content from ['profile', 'event'] categories
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
agent_id: Agent identifier
|
|
71
|
+
user_id: User identifier
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dict containing default category content
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
results = []
|
|
78
|
+
|
|
79
|
+
all_categories = self.storage_manager.list_memory_files()
|
|
80
|
+
existing_defaults = [cat for cat in self.default_categories if cat in all_categories]
|
|
81
|
+
|
|
82
|
+
for category in self.default_categories:
|
|
83
|
+
content = self.storage_manager.read_memory_file(category)
|
|
84
|
+
if content:
|
|
85
|
+
results.append(
|
|
86
|
+
{
|
|
87
|
+
"category": category,
|
|
88
|
+
"content": content,
|
|
89
|
+
"content_type": "default_category",
|
|
90
|
+
"length": len(content),
|
|
91
|
+
"lines": len(content.split("\n")),
|
|
92
|
+
"file_exists": category in all_categories,
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
logger.debug(f"No content found for {agent_id}:{user_id}:{category}")
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"success": True,
|
|
100
|
+
"method": "retrieve_default_category",
|
|
101
|
+
"requested_categories": self.default_categories,
|
|
102
|
+
"existing_categories": existing_defaults,
|
|
103
|
+
"all_categories_found": all_categories,
|
|
104
|
+
"results": results,
|
|
105
|
+
"total_items": len(results),
|
|
106
|
+
"message": f"Retrieved {len(results)} default categories for {agent_id}:{user_id} (found {len(existing_defaults)}/{len(self.default_categories)} requested files)",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error in retrieve_default_category for {agent_id}:{user_id}: {e}")
|
|
111
|
+
return {
|
|
112
|
+
"success": False,
|
|
113
|
+
"error": str(e),
|
|
114
|
+
"method": "retrieve_default_category",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
def retrieve_relevant_category(self, agent_id: str, user_id: str, query: str, top_k: int = 5) -> Dict[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Method 2: Retrieve Relevant Category
|
|
120
|
+
Retrieve relevant contents from top-k similar category names (excluding profile, event, and activity)
|
|
121
|
+
Scans actual {agent_id}/{user_id}/{category}.md files in memory directory
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
agent_id: Agent identifier
|
|
125
|
+
user_id: User identifier
|
|
126
|
+
query: Search query for category relevance
|
|
127
|
+
top_k: Number of top categories to return
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dict containing relevant category content
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
all_categories = self.storage_manager.list_memory_files("all")
|
|
134
|
+
excluded_categories = self.default_categories + ["activity"]
|
|
135
|
+
relevant_categories = [cat for cat in all_categories if cat not in excluded_categories]
|
|
136
|
+
|
|
137
|
+
if not relevant_categories:
|
|
138
|
+
return {
|
|
139
|
+
"success": True,
|
|
140
|
+
"method": "retrieve_relevant_category",
|
|
141
|
+
"query": query,
|
|
142
|
+
"results": [],
|
|
143
|
+
"total_items": 0,
|
|
144
|
+
"message": "No categories available (excluding profile/event/activity)",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Calculate category relevance scores
|
|
148
|
+
category_scores = []
|
|
149
|
+
query_lower = query.lower()
|
|
150
|
+
query_words = set(query_lower.split())
|
|
151
|
+
|
|
152
|
+
for category in relevant_categories:
|
|
153
|
+
# Check if category has content for this character
|
|
154
|
+
content = self.storage_manager.read_memory_file(category)
|
|
155
|
+
if not content:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
# Semantic search for content relevance
|
|
159
|
+
content_relevance = 0.0
|
|
160
|
+
if self.semantic_search_enabled and self.embedding_client:
|
|
161
|
+
try:
|
|
162
|
+
# Generate embeddings for query and content
|
|
163
|
+
query_embedding = self.embedding_client.embed(query)
|
|
164
|
+
content_embedding = self.embedding_client.embed(
|
|
165
|
+
content[:1000]
|
|
166
|
+
) # Limit content length for embedding
|
|
167
|
+
|
|
168
|
+
# Calculate semantic similarity
|
|
169
|
+
semantic_similarity = self._cosine_similarity(query_embedding, content_embedding)
|
|
170
|
+
content_relevance = semantic_similarity
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.warning(f"Semantic search failed for {category}: {e}")
|
|
173
|
+
# Fallback to simple keyword matching
|
|
174
|
+
content_lower = content.lower()
|
|
175
|
+
content_relevance = (
|
|
176
|
+
sum(1 for word in query_words if word in content_lower) / len(query_words)
|
|
177
|
+
if query_words
|
|
178
|
+
else 0
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
# Fallback to simple keyword matching when semantic search is not available
|
|
182
|
+
content_lower = content.lower()
|
|
183
|
+
content_relevance = (
|
|
184
|
+
sum(1 for word in query_words if word in content_lower) / len(query_words) if query_words else 0
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Use semantic score directly
|
|
188
|
+
combined_score = content_relevance
|
|
189
|
+
|
|
190
|
+
if combined_score > 0:
|
|
191
|
+
category_scores.append(
|
|
192
|
+
{
|
|
193
|
+
"category": category,
|
|
194
|
+
"content": content,
|
|
195
|
+
"score": combined_score,
|
|
196
|
+
"content_relevance": content_relevance,
|
|
197
|
+
"semantic_search_used": self.semantic_search_enabled and self.embedding_client is not None,
|
|
198
|
+
"length": len(content),
|
|
199
|
+
"lines": len(content.split("\n")),
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Sort by score and get top-k
|
|
204
|
+
category_scores.sort(key=lambda x: x["score"], reverse=True)
|
|
205
|
+
top_categories = category_scores[:top_k]
|
|
206
|
+
|
|
207
|
+
# Format results
|
|
208
|
+
results = []
|
|
209
|
+
for item in top_categories:
|
|
210
|
+
results.append(
|
|
211
|
+
{
|
|
212
|
+
"category": item["category"],
|
|
213
|
+
"content": item["content"],
|
|
214
|
+
"content_type": "relevant_category",
|
|
215
|
+
"relevance_score": item["score"],
|
|
216
|
+
"content_relevance": item["content_relevance"],
|
|
217
|
+
"semantic_search_used": item["semantic_search_used"],
|
|
218
|
+
"length": item["length"],
|
|
219
|
+
"lines": item["lines"],
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
"success": True,
|
|
225
|
+
"method": "retrieve_relevant_category",
|
|
226
|
+
"query": query,
|
|
227
|
+
"top_k": top_k,
|
|
228
|
+
"all_categories_found": all_categories,
|
|
229
|
+
"excluded_categories": excluded_categories,
|
|
230
|
+
"available_categories": relevant_categories,
|
|
231
|
+
"semantic_search_enabled": self.semantic_search_enabled,
|
|
232
|
+
"results": results,
|
|
233
|
+
"total_items": len(results),
|
|
234
|
+
"message": f"Retrieved top {len(results)} relevant categories for query '{query}' using semantic search from {len(all_categories)} total categories",
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Error in retrieve_relevant_category for {agent_id}:{user_id}: {e}")
|
|
239
|
+
return {
|
|
240
|
+
"success": False,
|
|
241
|
+
"error": str(e),
|
|
242
|
+
"method": "retrieve_relevant_category",
|
|
243
|
+
"query": query,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
def retrieve_relevant_memories(self, agent_id: str, user_id: str, query: str, top_k: int = 10) -> Dict[str, Any]:
|
|
247
|
+
"""
|
|
248
|
+
Method 3: Retrieve Relevant Memories
|
|
249
|
+
Retrieve top-k memories using embedding search across all categories
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
agent_id: Agent identifier
|
|
253
|
+
user_id: User identifier
|
|
254
|
+
query: Search query for memory retrieval
|
|
255
|
+
top_k: Number of top memories to return
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Dict containing relevant memories
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
if not self.semantic_search_enabled:
|
|
262
|
+
return {
|
|
263
|
+
"success": False,
|
|
264
|
+
"error": "Semantic search not available - embedding client not initialized",
|
|
265
|
+
"method": "retrieve_relevant_memories",
|
|
266
|
+
"query": query,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Generate query embedding
|
|
270
|
+
query_embedding = self.embedding_client.embed(query)
|
|
271
|
+
|
|
272
|
+
results = []
|
|
273
|
+
|
|
274
|
+
# Get embeddings directory from storage manager
|
|
275
|
+
embeddings_dir = self.storage_manager.get_char_embeddings_dir()
|
|
276
|
+
|
|
277
|
+
if not embeddings_dir.exists():
|
|
278
|
+
return {
|
|
279
|
+
"success": True,
|
|
280
|
+
"method": "retrieve_relevant_memories",
|
|
281
|
+
"query": query,
|
|
282
|
+
"results": [],
|
|
283
|
+
"total_items": 0,
|
|
284
|
+
"message": f"No embeddings found for {agent_id}:{user_id} in {embeddings_dir}",
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# Search through all embedding files for this character
|
|
288
|
+
for embeddings_file in embeddings_dir.glob("*_embeddings.json"):
|
|
289
|
+
category = embeddings_file.stem.replace("_embeddings", "")
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
with open(embeddings_file, "r", encoding="utf-8") as f:
|
|
293
|
+
embeddings_data = json.load(f)
|
|
294
|
+
|
|
295
|
+
# Search through stored embeddings
|
|
296
|
+
for emb_data in embeddings_data.get("embeddings", []):
|
|
297
|
+
similarity = self._cosine_similarity(query_embedding, emb_data["embedding"])
|
|
298
|
+
|
|
299
|
+
if similarity > 0.1: # Minimum threshold for semantic similarity
|
|
300
|
+
results.append(
|
|
301
|
+
{
|
|
302
|
+
"content": emb_data["text"],
|
|
303
|
+
"content_type": "relevant_memory",
|
|
304
|
+
"semantic_score": similarity,
|
|
305
|
+
"category": category,
|
|
306
|
+
"item_id": emb_data.get("item_id", ""),
|
|
307
|
+
"memory_id": emb_data.get("memory_id", ""),
|
|
308
|
+
"line_number": emb_data.get("line_number", 0),
|
|
309
|
+
"length": len(emb_data["text"]),
|
|
310
|
+
"metadata": emb_data.get("metadata", {}),
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.warning(f"Failed to process embeddings for {category}: {e}")
|
|
316
|
+
|
|
317
|
+
# Sort by semantic score and get top-k
|
|
318
|
+
results.sort(key=lambda x: x["semantic_score"], reverse=True)
|
|
319
|
+
top_memories = results[:top_k]
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"success": True,
|
|
323
|
+
"method": "retrieve_relevant_memories",
|
|
324
|
+
"query": query,
|
|
325
|
+
"top_k": top_k,
|
|
326
|
+
"semantic_search_enabled": self.semantic_search_enabled,
|
|
327
|
+
"results": top_memories,
|
|
328
|
+
"total_items": len(top_memories),
|
|
329
|
+
"total_candidates": len(results),
|
|
330
|
+
"message": f"Retrieved top {len(top_memories)} memories from {len(results)} candidates",
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
except Exception as e:
|
|
334
|
+
logger.error(f"Error in retrieve_relevant_memories for {agent_id}:{user_id}: {e}")
|
|
335
|
+
return {
|
|
336
|
+
"success": False,
|
|
337
|
+
"error": str(e),
|
|
338
|
+
"method": "retrieve_relevant_memories",
|
|
339
|
+
"query": query,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
|
|
343
|
+
"""Calculate cosine similarity between two vectors"""
|
|
344
|
+
try:
|
|
345
|
+
if len(vec1) != len(vec2):
|
|
346
|
+
return 0.0
|
|
347
|
+
|
|
348
|
+
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
|
349
|
+
magnitude1 = math.sqrt(sum(a * a for a in vec1))
|
|
350
|
+
magnitude2 = math.sqrt(sum(a * a for a in vec2))
|
|
351
|
+
|
|
352
|
+
if magnitude1 == 0 or magnitude2 == 0:
|
|
353
|
+
return 0.0
|
|
354
|
+
|
|
355
|
+
return dot_product / (magnitude1 * magnitude2)
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.warning(f"Cosine similarity calculation failed: {e}")
|
|
359
|
+
return 0.0
|
|
360
|
+
|
|
361
|
+
def get_status(self) -> Dict[str, Any]:
|
|
362
|
+
"""Get status information about the recall agent"""
|
|
363
|
+
return {
|
|
364
|
+
"agent_name": "recall_agent",
|
|
365
|
+
"agent_type": "enhanced_retrieval",
|
|
366
|
+
"memory_types": list(self.memory_types.keys()),
|
|
367
|
+
"memory_dir": str(self.memory_dir),
|
|
368
|
+
"semantic_search_enabled": self.semantic_search_enabled,
|
|
369
|
+
"default_categories": self.default_categories,
|
|
370
|
+
"config_source": "markdown_config.py",
|
|
371
|
+
"retrieval_methods": [
|
|
372
|
+
"retrieve_default_category(agent_id, user_id)",
|
|
373
|
+
"retrieve_relevant_category(agent_id, user_id, query, top_k) - excludes profile/event/activity",
|
|
374
|
+
"retrieve_relevant_memories(agent_id, user_id, query, top_k)",
|
|
375
|
+
],
|
|
376
|
+
}
|