massgen 0.1.4__py3-none-any.whl → 0.1.5__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.
Potentially problematic release.
This version of massgen might be problematic. Click here for more details.
- massgen/__init__.py +1 -1
- massgen/chat_agent.py +340 -20
- massgen/cli.py +326 -19
- massgen/configs/README.md +52 -10
- massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
- massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
- massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
- massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
- massgen/configs/memory/single_agent_compression_test.yaml +64 -0
- massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +98 -0
- massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +54 -0
- massgen/memory/README.md +277 -0
- massgen/memory/__init__.py +26 -0
- massgen/memory/_base.py +193 -0
- massgen/memory/_compression.py +237 -0
- massgen/memory/_context_monitor.py +211 -0
- massgen/memory/_conversation.py +255 -0
- massgen/memory/_fact_extraction_prompts.py +333 -0
- massgen/memory/_mem0_adapters.py +257 -0
- massgen/memory/_persistent.py +687 -0
- massgen/memory/docker-compose.qdrant.yml +36 -0
- massgen/memory/docs/DESIGN.md +388 -0
- massgen/memory/docs/QUICKSTART.md +409 -0
- massgen/memory/docs/SUMMARY.md +319 -0
- massgen/memory/docs/agent_use_memory.md +408 -0
- massgen/memory/docs/orchestrator_use_memory.md +586 -0
- massgen/memory/examples.py +237 -0
- massgen/orchestrator.py +207 -7
- massgen/tests/memory/test_agent_compression.py +174 -0
- massgen/tests/memory/test_context_window_management.py +286 -0
- massgen/tests/memory/test_force_compression.py +154 -0
- massgen/tests/memory/test_simple_compression.py +147 -0
- massgen/tests/test_agent_memory.py +534 -0
- massgen/tests/test_conversation_memory.py +382 -0
- massgen/tests/test_orchestrator_memory.py +620 -0
- massgen/tests/test_persistent_memory.py +435 -0
- massgen/token_manager/token_manager.py +6 -0
- massgen/tools/__init__.py +8 -0
- massgen/tools/_planning_mcp_server.py +520 -0
- massgen/tools/planning_dataclasses.py +434 -0
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/METADATA +109 -76
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/RECORD +46 -12
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/WHEEL +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Persistent memory implementation for MassGen using mem0.
|
|
4
|
+
|
|
5
|
+
This module provides long-term memory storage with semantic retrieval capabilities,
|
|
6
|
+
enabling agents to remember and recall information across multiple sessions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib import metadata
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
from pydantic import field_validator
|
|
13
|
+
|
|
14
|
+
from ._base import PersistentMemoryBase
|
|
15
|
+
from ._fact_extraction_prompts import get_fact_extraction_prompt
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from mem0.configs.base import MemoryConfig
|
|
19
|
+
from mem0.vector_stores.configs import VectorStoreConfig
|
|
20
|
+
else:
|
|
21
|
+
MemoryConfig = Any
|
|
22
|
+
VectorStoreConfig = Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _create_massgen_mem0_config_classes():
|
|
26
|
+
"""
|
|
27
|
+
Create custom config classes for MassGen mem0 integration.
|
|
28
|
+
|
|
29
|
+
This is necessary because mem0's default validation hardcodes provider names.
|
|
30
|
+
We override the validation to accept 'massgen' as a valid provider.
|
|
31
|
+
"""
|
|
32
|
+
from mem0.embeddings.configs import EmbedderConfig
|
|
33
|
+
from mem0.llms.configs import LlmConfig
|
|
34
|
+
|
|
35
|
+
class _MassGenLlmConfig(LlmConfig):
|
|
36
|
+
"""Custom LLM config that accepts MassGen backends."""
|
|
37
|
+
|
|
38
|
+
@field_validator("config")
|
|
39
|
+
@classmethod
|
|
40
|
+
def validate_config(cls, v: Any, values: Any) -> Any:
|
|
41
|
+
"""Validate LLM configuration with MassGen provider support."""
|
|
42
|
+
from mem0.utils.factory import LlmFactory
|
|
43
|
+
|
|
44
|
+
provider = values.data.get("provider")
|
|
45
|
+
if provider in LlmFactory.provider_to_class:
|
|
46
|
+
return v
|
|
47
|
+
# If provider is not in factory but config is valid, allow it
|
|
48
|
+
# This supports custom providers like 'massgen'
|
|
49
|
+
return v
|
|
50
|
+
|
|
51
|
+
class _MassGenEmbedderConfig(EmbedderConfig):
|
|
52
|
+
"""Custom embedder config that accepts MassGen backends."""
|
|
53
|
+
|
|
54
|
+
@field_validator("config")
|
|
55
|
+
@classmethod
|
|
56
|
+
def validate_config(cls, v: Any, values: Any) -> Any:
|
|
57
|
+
"""Validate embedder configuration with MassGen provider support."""
|
|
58
|
+
from mem0.utils.factory import EmbedderFactory
|
|
59
|
+
|
|
60
|
+
provider = values.data.get("provider")
|
|
61
|
+
if provider in EmbedderFactory.provider_to_class:
|
|
62
|
+
return v
|
|
63
|
+
# Allow custom providers
|
|
64
|
+
return v
|
|
65
|
+
|
|
66
|
+
return _MassGenLlmConfig, _MassGenEmbedderConfig
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PersistentMemory(PersistentMemoryBase):
|
|
70
|
+
"""
|
|
71
|
+
Long-term persistent memory using mem0 as the storage backend.
|
|
72
|
+
|
|
73
|
+
This memory system provides:
|
|
74
|
+
- Semantic search across historical conversations
|
|
75
|
+
- Automatic memory summarization and organization
|
|
76
|
+
- Persistent storage across sessions
|
|
77
|
+
- Metadata-based filtering (agent, user, session)
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> # Initialize with MassGen backends
|
|
81
|
+
>>> memory = PersistentMemory(
|
|
82
|
+
... agent_name="research_agent",
|
|
83
|
+
... llm_backend=my_llm_backend,
|
|
84
|
+
... embedding_backend=my_embedding_backend
|
|
85
|
+
... )
|
|
86
|
+
>>>
|
|
87
|
+
>>> # Record information
|
|
88
|
+
>>> await memory.record([
|
|
89
|
+
... {"role": "user", "content": "What is quantum computing?"},
|
|
90
|
+
... {"role": "assistant", "content": "Quantum computing uses..."}
|
|
91
|
+
... ])
|
|
92
|
+
>>>
|
|
93
|
+
>>> # Retrieve relevant memories
|
|
94
|
+
>>> relevant = await memory.retrieve("quantum computing concepts")
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
agent_name: Optional[str] = None,
|
|
100
|
+
user_name: Optional[str] = None,
|
|
101
|
+
session_name: Optional[str] = None,
|
|
102
|
+
llm_backend: Optional[Any] = None,
|
|
103
|
+
llm_config: Optional[Dict[str, Any]] = None,
|
|
104
|
+
embedding_backend: Optional[Any] = None,
|
|
105
|
+
embedding_config: Optional[Dict[str, Any]] = None,
|
|
106
|
+
vector_store_config: Optional[VectorStoreConfig] = None,
|
|
107
|
+
mem0_config: Optional[MemoryConfig] = None,
|
|
108
|
+
memory_type: Optional[str] = None,
|
|
109
|
+
qdrant_client: Optional[Any] = None,
|
|
110
|
+
**kwargs: Any,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Initialize persistent memory with mem0 backend.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
agent_name: Name/ID of the agent (used for memory filtering)
|
|
117
|
+
user_name: Name/ID of the user (used for memory filtering)
|
|
118
|
+
session_name: Name/ID of the session (used for memory filtering)
|
|
119
|
+
|
|
120
|
+
Note:
|
|
121
|
+
At least one of agent_name, user_name, or session_name is required.
|
|
122
|
+
These serve as metadata for organizing and filtering memories.
|
|
123
|
+
|
|
124
|
+
llm_backend: DEPRECATED. Use llm_config instead.
|
|
125
|
+
Legacy support: MassGen LLM backend object (uses MassGenLLMAdapter)
|
|
126
|
+
|
|
127
|
+
llm_config: RECOMMENDED. Configuration dict for mem0's native LLMs.
|
|
128
|
+
Supports mem0's built-in providers: openai, anthropic, groq, together, etc.
|
|
129
|
+
Example: {"provider": "openai", "model": "gpt-4.1-nano-2025-04-14", "api_key": "..."}
|
|
130
|
+
Default: {"provider": "openai", "model": "gpt-4.1-nano-2025-04-14"} if not specified
|
|
131
|
+
|
|
132
|
+
When to use each approach:
|
|
133
|
+
- Use llm_config (native mem0): For standard providers (OpenAI, Anthropic, etc.)
|
|
134
|
+
Simpler, no adapter overhead, no async complexity, direct mem0 integration
|
|
135
|
+
- Use llm_backend (custom): Only if you need a custom MassGen backend
|
|
136
|
+
that mem0 doesn't natively support (requires async adapter)
|
|
137
|
+
|
|
138
|
+
embedding_backend: DEPRECATED. Use embedding_config instead.
|
|
139
|
+
Legacy support: MassGen embedding backend object (uses MassGenEmbeddingAdapter)
|
|
140
|
+
|
|
141
|
+
embedding_config: RECOMMENDED. Configuration dict for mem0's native embedders.
|
|
142
|
+
Supports mem0's built-in providers: openai, together, azure_openai, gemini, etc.
|
|
143
|
+
Example: {"provider": "openai", "model": "text-embedding-3-small", "api_key": "..."}
|
|
144
|
+
|
|
145
|
+
When to use each approach:
|
|
146
|
+
- Use embedding_config (native mem0): For standard providers (OpenAI, Together, etc.)
|
|
147
|
+
Simpler, no adapter overhead, direct mem0 integration
|
|
148
|
+
- Use embedding_backend (custom): Only if you need a custom MassGen backend
|
|
149
|
+
that mem0 doesn't natively support
|
|
150
|
+
|
|
151
|
+
vector_store_config: mem0 vector store configuration
|
|
152
|
+
mem0_config: Full mem0 configuration (overrides individual configs)
|
|
153
|
+
memory_type: Type of memory storage (None for semantic, "procedural_memory" for procedural)
|
|
154
|
+
qdrant_client: Optional shared QdrantClient instance (for multi-agent concurrent access)
|
|
155
|
+
Note: Local file-based Qdrant doesn't support concurrent access.
|
|
156
|
+
Use qdrant_client from a Qdrant server for multi-agent scenarios.
|
|
157
|
+
**kwargs: Additional options (e.g., on_disk=True for persistence)
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ValueError: If neither mem0_config nor required backends are provided
|
|
161
|
+
ImportError: If mem0 library is not installed
|
|
162
|
+
"""
|
|
163
|
+
super().__init__()
|
|
164
|
+
|
|
165
|
+
# Import and configure mem0
|
|
166
|
+
try:
|
|
167
|
+
import mem0
|
|
168
|
+
from mem0.configs.llms.base import BaseLlmConfig
|
|
169
|
+
from mem0.utils.factory import EmbedderFactory, LlmFactory
|
|
170
|
+
from packaging import version
|
|
171
|
+
|
|
172
|
+
# Check mem0 version for compatibility
|
|
173
|
+
current_version = metadata.version("mem0ai")
|
|
174
|
+
is_legacy_version = version.parse(current_version) <= version.parse(
|
|
175
|
+
"0.1.115",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Register MassGen adapters with mem0's factory system
|
|
179
|
+
EmbedderFactory.provider_to_class["massgen"] = "massgen.memory._mem0_adapters.MassGenEmbeddingAdapter"
|
|
180
|
+
|
|
181
|
+
if is_legacy_version:
|
|
182
|
+
LlmFactory.provider_to_class["massgen"] = "massgen.memory._mem0_adapters.MassGenLLMAdapter"
|
|
183
|
+
else:
|
|
184
|
+
# Newer mem0 versions use tuple format
|
|
185
|
+
LlmFactory.provider_to_class["massgen"] = (
|
|
186
|
+
"massgen.memory._mem0_adapters.MassGenLLMAdapter",
|
|
187
|
+
BaseLlmConfig,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
except ImportError as e:
|
|
191
|
+
raise ImportError(
|
|
192
|
+
"mem0 library is required for persistent memory. " "Install it with: pip install mem0ai",
|
|
193
|
+
) from e
|
|
194
|
+
|
|
195
|
+
# Create custom config classes
|
|
196
|
+
_LlmConfig, _EmbedderConfig = _create_massgen_mem0_config_classes()
|
|
197
|
+
|
|
198
|
+
# Validate metadata requirements
|
|
199
|
+
if not any([agent_name, user_name, session_name]):
|
|
200
|
+
raise ValueError(
|
|
201
|
+
"At least one of agent_name, user_name, or session_name must be provided " "to organize memories.",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Store identifiers for memory operations
|
|
205
|
+
self.agent_id = agent_name
|
|
206
|
+
self.user_id = user_name
|
|
207
|
+
self.session_id = session_name
|
|
208
|
+
|
|
209
|
+
# Configure mem0 instance
|
|
210
|
+
if mem0_config is not None:
|
|
211
|
+
# Use provided mem0_config, optionally overriding components
|
|
212
|
+
|
|
213
|
+
# Handle LLM configuration (prefer llm_config over llm_backend)
|
|
214
|
+
if llm_config is not None:
|
|
215
|
+
# Use mem0's native LLM (RECOMMENDED)
|
|
216
|
+
from mem0.llms.configs import LlmConfig
|
|
217
|
+
|
|
218
|
+
mem0_config.llm = LlmConfig(**llm_config)
|
|
219
|
+
elif llm_backend is not None:
|
|
220
|
+
# Use custom MassGen backend via adapter (LEGACY)
|
|
221
|
+
mem0_config.llm = _LlmConfig(
|
|
222
|
+
provider="massgen",
|
|
223
|
+
config={"model": llm_backend},
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Handle embedder configuration (prefer embedding_config over embedding_backend)
|
|
227
|
+
if embedding_config is not None:
|
|
228
|
+
# Use mem0's native embedder (RECOMMENDED)
|
|
229
|
+
from mem0.embeddings.configs import EmbedderConfig
|
|
230
|
+
|
|
231
|
+
mem0_config.embedder = EmbedderConfig(**embedding_config)
|
|
232
|
+
elif embedding_backend is not None:
|
|
233
|
+
# Use custom MassGen backend via adapter (LEGACY)
|
|
234
|
+
mem0_config.embedder = _EmbedderConfig(
|
|
235
|
+
provider="massgen",
|
|
236
|
+
config={"model": embedding_backend},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if vector_store_config is not None:
|
|
240
|
+
mem0_config.vector_store = vector_store_config
|
|
241
|
+
|
|
242
|
+
# Add custom fact extraction prompt if not already set
|
|
243
|
+
if not hasattr(mem0_config, "custom_fact_extraction_prompt") or mem0_config.custom_fact_extraction_prompt is None:
|
|
244
|
+
mem0_config.custom_fact_extraction_prompt = get_fact_extraction_prompt("default")
|
|
245
|
+
|
|
246
|
+
else:
|
|
247
|
+
# Build mem0_config from scratch
|
|
248
|
+
|
|
249
|
+
# Require at least one LLM configuration
|
|
250
|
+
if llm_config is None and llm_backend is None:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
"Either llm_config or llm_backend is required when mem0_config is not provided.\n"
|
|
253
|
+
"RECOMMENDED: Use llm_config with mem0's native LLMs.\n"
|
|
254
|
+
"Example: llm_config={'provider': 'openai', 'model': 'gpt-4.1-nano-2025-04-14'}",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Require at least one embedding configuration
|
|
258
|
+
if embedding_config is None and embedding_backend is None:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"Either embedding_config or embedding_backend is required when mem0_config is not provided.\n"
|
|
261
|
+
"RECOMMENDED: Use embedding_config with mem0's native embedders.\n"
|
|
262
|
+
"Example: embedding_config={'provider': 'openai', 'model': 'text-embedding-3-small'}",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Configure LLM (prefer llm_config)
|
|
266
|
+
if llm_config is not None:
|
|
267
|
+
# Use mem0's native LLM (RECOMMENDED)
|
|
268
|
+
from mem0.llms.configs import LlmConfig
|
|
269
|
+
|
|
270
|
+
llm = LlmConfig(**llm_config)
|
|
271
|
+
else:
|
|
272
|
+
# Use custom MassGen backend via adapter (LEGACY)
|
|
273
|
+
llm = _LlmConfig(
|
|
274
|
+
provider="massgen",
|
|
275
|
+
config={"model": llm_backend},
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Configure embedder (prefer embedding_config)
|
|
279
|
+
if embedding_config is not None:
|
|
280
|
+
# Use mem0's native embedder (RECOMMENDED)
|
|
281
|
+
from mem0.embeddings.configs import EmbedderConfig
|
|
282
|
+
|
|
283
|
+
embedder = EmbedderConfig(**embedding_config)
|
|
284
|
+
else:
|
|
285
|
+
# Use custom MassGen backend via adapter (LEGACY)
|
|
286
|
+
embedder = _EmbedderConfig(
|
|
287
|
+
provider="massgen",
|
|
288
|
+
config={"model": embedding_backend},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Add custom fact extraction prompt for better memory quality
|
|
292
|
+
custom_prompt = get_fact_extraction_prompt("default")
|
|
293
|
+
|
|
294
|
+
mem0_config = mem0.configs.base.MemoryConfig(
|
|
295
|
+
llm=llm,
|
|
296
|
+
embedder=embedder,
|
|
297
|
+
custom_fact_extraction_prompt=custom_prompt,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Configure vector store
|
|
301
|
+
if vector_store_config is not None:
|
|
302
|
+
mem0_config.vector_store = vector_store_config
|
|
303
|
+
elif qdrant_client is not None:
|
|
304
|
+
# Use shared Qdrant client (for multi-agent scenarios)
|
|
305
|
+
# NOTE: Must be from a Qdrant server, not local file-based storage
|
|
306
|
+
mem0_config.vector_store = mem0.vector_stores.configs.VectorStoreConfig(
|
|
307
|
+
config={"client": qdrant_client},
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
# Default to Qdrant with disk persistence (single agent only)
|
|
311
|
+
# WARNING: File-based Qdrant doesn't support concurrent access
|
|
312
|
+
persist = kwargs.get("on_disk", True)
|
|
313
|
+
mem0_config.vector_store = mem0.vector_stores.configs.VectorStoreConfig(
|
|
314
|
+
config={"on_disk": persist},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Initialize async mem0 instance
|
|
318
|
+
self.mem0_memory = mem0.AsyncMemory(mem0_config)
|
|
319
|
+
self.default_memory_type = memory_type
|
|
320
|
+
|
|
321
|
+
async def save_to_memory(
|
|
322
|
+
self,
|
|
323
|
+
thinking: str,
|
|
324
|
+
content: List[str],
|
|
325
|
+
**kwargs: Any,
|
|
326
|
+
) -> Dict[str, Any]:
|
|
327
|
+
"""
|
|
328
|
+
Agent tool: Explicitly save important information to memory.
|
|
329
|
+
|
|
330
|
+
This method is exposed as a tool that agents can call to save information
|
|
331
|
+
they determine is important for future reference.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
thinking: Agent's reasoning about why this information matters
|
|
335
|
+
content: List of information items to save
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Dictionary with 'success' status and 'memory_ids' of saved items
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> result = await memory.save_to_memory(
|
|
342
|
+
... thinking="User mentioned their birthday",
|
|
343
|
+
... content=["User's birthday is March 15"]
|
|
344
|
+
... )
|
|
345
|
+
>>> print(result['success']) # True
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
# Combine thinking and content for better context
|
|
349
|
+
full_content = []
|
|
350
|
+
if thinking:
|
|
351
|
+
full_content.append(f"Context: {thinking}")
|
|
352
|
+
full_content.extend(content)
|
|
353
|
+
|
|
354
|
+
# Record to mem0
|
|
355
|
+
results = await self._mem0_add(
|
|
356
|
+
[
|
|
357
|
+
{
|
|
358
|
+
"role": "assistant",
|
|
359
|
+
"content": "\n".join(full_content),
|
|
360
|
+
"name": "memory_save",
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
**kwargs,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
"success": True,
|
|
368
|
+
"message": f"Successfully saved {len(content)} items to memory",
|
|
369
|
+
"memory_ids": results.get("results", []),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
return {
|
|
374
|
+
"success": False,
|
|
375
|
+
"message": f"Error saving to memory: {str(e)}",
|
|
376
|
+
"memory_ids": [],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async def recall_from_memory(
|
|
380
|
+
self,
|
|
381
|
+
keywords: List[str],
|
|
382
|
+
limit: int = 5,
|
|
383
|
+
**kwargs: Any,
|
|
384
|
+
) -> Dict[str, Any]:
|
|
385
|
+
"""
|
|
386
|
+
Agent tool: Retrieve memories based on keywords.
|
|
387
|
+
|
|
388
|
+
This method is exposed as a tool that agents can call to search their
|
|
389
|
+
long-term memory for relevant information.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
keywords: Keywords to search for (names, dates, topics, etc.)
|
|
393
|
+
limit: Maximum number of memories to retrieve per keyword
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Dictionary with 'success' status and 'memories' list
|
|
397
|
+
|
|
398
|
+
Example:
|
|
399
|
+
>>> result = await memory.recall_from_memory(
|
|
400
|
+
... keywords=["quantum computing", "algorithms"]
|
|
401
|
+
... )
|
|
402
|
+
>>> for memory in result['memories']:
|
|
403
|
+
... print(memory)
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
all_memories = []
|
|
407
|
+
|
|
408
|
+
for keyword in keywords:
|
|
409
|
+
search_result = await self.mem0_memory.search(
|
|
410
|
+
query=keyword,
|
|
411
|
+
agent_id=self.agent_id,
|
|
412
|
+
user_id=self.user_id,
|
|
413
|
+
run_id=self.session_id,
|
|
414
|
+
limit=limit,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if search_result and "results" in search_result:
|
|
418
|
+
memories = [item["memory"] for item in search_result["results"]]
|
|
419
|
+
all_memories.extend(memories)
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
"success": True,
|
|
423
|
+
"memories": all_memories,
|
|
424
|
+
"count": len(all_memories),
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
return {
|
|
429
|
+
"success": False,
|
|
430
|
+
"message": f"Error retrieving memories: {str(e)}",
|
|
431
|
+
"memories": [],
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async def record(
|
|
435
|
+
self,
|
|
436
|
+
messages: List[Dict[str, Any]],
|
|
437
|
+
memory_type: Optional[str] = None,
|
|
438
|
+
infer: bool = True,
|
|
439
|
+
**kwargs: Any,
|
|
440
|
+
) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Developer interface: Record conversation messages to persistent memory.
|
|
443
|
+
|
|
444
|
+
This is called automatically by the framework to save conversation history.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
messages: List of message dictionaries to record
|
|
448
|
+
memory_type: Type of memory ('semantic' or 'procedural')
|
|
449
|
+
infer: Whether to let mem0 infer key information
|
|
450
|
+
**kwargs: Additional mem0 recording options
|
|
451
|
+
"""
|
|
452
|
+
from ..logger_config import logger
|
|
453
|
+
|
|
454
|
+
if not messages:
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
# Filter out None values, system messages, and messages with None/empty content
|
|
458
|
+
valid_messages = []
|
|
459
|
+
for msg in messages:
|
|
460
|
+
if msg is None:
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
# Skip system messages (orchestrator prompts, not conversation content)
|
|
464
|
+
role = msg.get("role")
|
|
465
|
+
if role == "system":
|
|
466
|
+
logger.debug("⏭️ Skipping system message from memory recording (not conversation content)")
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
# Skip messages with None or empty content
|
|
470
|
+
content = msg.get("content")
|
|
471
|
+
if content is None or (isinstance(content, str) and not content.strip()):
|
|
472
|
+
logger.warning(f"⚠️ Skipping message with None/empty content: role={role}")
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
valid_messages.append(msg)
|
|
476
|
+
|
|
477
|
+
if not valid_messages:
|
|
478
|
+
logger.warning("⚠️ No valid messages to record (all were None or empty)")
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
# Convert to mem0 format
|
|
482
|
+
# Combine all messages into a single conversation context for mem0
|
|
483
|
+
# mem0's LLM will extract facts from this combined content
|
|
484
|
+
conversation_parts = []
|
|
485
|
+
for msg in valid_messages:
|
|
486
|
+
role = msg.get("role", "unknown")
|
|
487
|
+
content = msg.get("content", "")
|
|
488
|
+
# Format: "role: content" for each message
|
|
489
|
+
conversation_parts.append(f"{role}: {content}")
|
|
490
|
+
|
|
491
|
+
mem0_messages = [
|
|
492
|
+
{
|
|
493
|
+
"role": "assistant",
|
|
494
|
+
"content": "\n".join(conversation_parts),
|
|
495
|
+
"name": "conversation",
|
|
496
|
+
},
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
await self._mem0_add(
|
|
500
|
+
mem0_messages,
|
|
501
|
+
memory_type=memory_type,
|
|
502
|
+
infer=infer,
|
|
503
|
+
**kwargs,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
async def _mem0_add(
|
|
507
|
+
self,
|
|
508
|
+
messages: Union[str, List[Dict]],
|
|
509
|
+
memory_type: Optional[str] = None,
|
|
510
|
+
infer: bool = True,
|
|
511
|
+
**kwargs: Any,
|
|
512
|
+
) -> Dict:
|
|
513
|
+
"""
|
|
514
|
+
Internal helper to add memories to mem0.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
messages: String or message dictionaries to store
|
|
518
|
+
memory_type: Override default memory type
|
|
519
|
+
infer: Whether mem0 should infer structured information
|
|
520
|
+
**kwargs: Additional mem0 options
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
mem0 add operation result
|
|
524
|
+
"""
|
|
525
|
+
from ..logger_config import logger
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
# Logging - show what we're sending to mem0
|
|
529
|
+
logger.info(f"🔍 [_mem0_add] Recording to mem0 (agent={self.agent_id}, session={self.session_id}, turn={kwargs.get('metadata', {}).get('turn', 'N/A')})")
|
|
530
|
+
|
|
531
|
+
# Debug: Show message preview
|
|
532
|
+
if isinstance(messages, str):
|
|
533
|
+
preview = messages[:100] + "..." if len(messages) > 100 else messages
|
|
534
|
+
logger.debug(f" messages (string): {preview}")
|
|
535
|
+
elif isinstance(messages, list):
|
|
536
|
+
logger.debug(f" messages: {len(messages)} message(s)")
|
|
537
|
+
for i, msg in enumerate(messages[:1]): # Show first one
|
|
538
|
+
if msg is None:
|
|
539
|
+
logger.warning(f" ⚠️ Message [{i}] is None!")
|
|
540
|
+
continue
|
|
541
|
+
content = msg.get("content", "") if isinstance(msg, dict) else str(msg)
|
|
542
|
+
preview = content[:100] + "..." if len(content) > 100 else content
|
|
543
|
+
logger.debug(f" {msg.get('role', 'unknown') if isinstance(msg, dict) else 'str'}: {preview}")
|
|
544
|
+
|
|
545
|
+
# Call mem0
|
|
546
|
+
results = await self.mem0_memory.add(
|
|
547
|
+
messages=messages,
|
|
548
|
+
agent_id=self.agent_id,
|
|
549
|
+
user_id=self.user_id,
|
|
550
|
+
run_id=self.session_id,
|
|
551
|
+
memory_type=memory_type or self.default_memory_type,
|
|
552
|
+
infer=infer,
|
|
553
|
+
**kwargs,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Show results
|
|
557
|
+
if isinstance(results, dict):
|
|
558
|
+
result_count = len(results.get("results", []))
|
|
559
|
+
relation_count = len(results.get("relations", []))
|
|
560
|
+
logger.info(f" ✅ mem0 extracted {result_count} fact(s), {relation_count} relation(s)")
|
|
561
|
+
else:
|
|
562
|
+
logger.info(" ✅ mem0.add() completed")
|
|
563
|
+
|
|
564
|
+
return results
|
|
565
|
+
|
|
566
|
+
except Exception as e:
|
|
567
|
+
# Enhanced error logging
|
|
568
|
+
logger.error(f"❌ mem0.add() failed: {type(e).__name__}: {str(e)}")
|
|
569
|
+
logger.error(f" agent_id={self.agent_id}, user_id={self.user_id}, run_id={self.session_id}")
|
|
570
|
+
|
|
571
|
+
if "PointStruct" in str(e) or "vector" in str(e).lower():
|
|
572
|
+
logger.error(" 💡 Hint: This usually means embedding generation returned None")
|
|
573
|
+
logger.error(" Check: 1) API key is set, 2) Model name is correct, 3) API is accessible")
|
|
574
|
+
logger.error(" Debug: Run 'uv run python scripts/test_memory_setup.py' to isolate the issue")
|
|
575
|
+
|
|
576
|
+
raise
|
|
577
|
+
|
|
578
|
+
async def retrieve(
|
|
579
|
+
self,
|
|
580
|
+
query: Union[str, Dict[str, Any], List[Dict[str, Any]]],
|
|
581
|
+
limit: int = 5,
|
|
582
|
+
previous_winners: Optional[List[Dict[str, Any]]] = None,
|
|
583
|
+
**kwargs: Any,
|
|
584
|
+
) -> str:
|
|
585
|
+
"""
|
|
586
|
+
Developer interface: Retrieve relevant memories for a query.
|
|
587
|
+
|
|
588
|
+
This is called automatically by the framework to inject relevant
|
|
589
|
+
historical knowledge into the current conversation.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
query: Query string or message(s) to search for
|
|
593
|
+
limit: Maximum number of memories to retrieve per agent
|
|
594
|
+
previous_winners: List of previous winning agents with turns
|
|
595
|
+
Format: [{"agent_id": "agent_b", "turn": 1}, ...]
|
|
596
|
+
If provided, also searches winners' memories from their winning turns
|
|
597
|
+
**kwargs: Additional mem0 search options
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Formatted string of retrieved memories (own + previous winners')
|
|
601
|
+
"""
|
|
602
|
+
from ..logger_config import logger
|
|
603
|
+
|
|
604
|
+
logger.info(f"🔍 [retrieve] Searching memories (agent={self.agent_id}, limit={limit}, winners={len(previous_winners) if previous_winners else 0})")
|
|
605
|
+
logger.debug(f" Previous winners: {previous_winners}" if previous_winners else " No previous winners")
|
|
606
|
+
|
|
607
|
+
# Convert query to string format
|
|
608
|
+
query_strings = []
|
|
609
|
+
|
|
610
|
+
if isinstance(query, str):
|
|
611
|
+
query_strings = [query]
|
|
612
|
+
elif isinstance(query, dict):
|
|
613
|
+
# Single message dict
|
|
614
|
+
content = query.get("content", "")
|
|
615
|
+
if content:
|
|
616
|
+
query_strings = [str(content)]
|
|
617
|
+
elif isinstance(query, list):
|
|
618
|
+
# List of message dicts
|
|
619
|
+
for msg in query:
|
|
620
|
+
if isinstance(msg, dict):
|
|
621
|
+
content = msg.get("content", "")
|
|
622
|
+
if content:
|
|
623
|
+
query_strings.append(str(content))
|
|
624
|
+
|
|
625
|
+
if not query_strings:
|
|
626
|
+
logger.warning(" ⚠️ No valid query strings extracted")
|
|
627
|
+
return ""
|
|
628
|
+
|
|
629
|
+
logger.debug(f" Queries: {len(query_strings)} query string(s)")
|
|
630
|
+
|
|
631
|
+
# Search mem0 for each query string
|
|
632
|
+
all_results = []
|
|
633
|
+
|
|
634
|
+
# 1. Search own agent's memories first
|
|
635
|
+
logger.debug(f" 🔎 Searching own memories ({self.agent_id})...")
|
|
636
|
+
for query_str in query_strings:
|
|
637
|
+
search_result = await self.mem0_memory.search(
|
|
638
|
+
query=query_str,
|
|
639
|
+
agent_id=self.agent_id,
|
|
640
|
+
user_id=self.user_id,
|
|
641
|
+
run_id=self.session_id,
|
|
642
|
+
limit=limit,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
if search_result and "results" in search_result:
|
|
646
|
+
memories = [item["memory"] for item in search_result["results"]]
|
|
647
|
+
logger.debug(f" → Found {len(memories)} memory/memories")
|
|
648
|
+
all_results.extend(memories)
|
|
649
|
+
|
|
650
|
+
# 2. Search previous winning agents' memories (turn-filtered)
|
|
651
|
+
if previous_winners:
|
|
652
|
+
logger.debug(f" 🔎 Searching {len(previous_winners)} previous winner(s)...")
|
|
653
|
+
for winner in previous_winners:
|
|
654
|
+
winner_agent_id = winner.get("agent_id")
|
|
655
|
+
winner_turn = winner.get("turn")
|
|
656
|
+
|
|
657
|
+
# Skip if winner is self
|
|
658
|
+
if winner_agent_id == self.agent_id:
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
logger.debug(f" → Searching {winner_agent_id} (turn {winner_turn})...")
|
|
662
|
+
|
|
663
|
+
# Search each query string for this winner
|
|
664
|
+
for query_str in query_strings:
|
|
665
|
+
search_result = await self.mem0_memory.search(
|
|
666
|
+
query=query_str,
|
|
667
|
+
agent_id=winner_agent_id,
|
|
668
|
+
user_id=self.user_id,
|
|
669
|
+
run_id=self.session_id,
|
|
670
|
+
limit=limit,
|
|
671
|
+
metadata_filters={"turn": winner_turn} if winner_turn else None,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if search_result and "results" in search_result:
|
|
675
|
+
memories = [f"[From {winner_agent_id} Turn {winner_turn}] {item['memory']}" for item in search_result["results"]]
|
|
676
|
+
logger.debug(f" Found {len(memories)} memory/memories")
|
|
677
|
+
all_results.extend(memories)
|
|
678
|
+
|
|
679
|
+
# Format results as a readable string
|
|
680
|
+
logger.info(f" ✅ Total: {len(all_results)} memories retrieved")
|
|
681
|
+
if all_results:
|
|
682
|
+
# Show first 2 memories as preview
|
|
683
|
+
for i, mem in enumerate(all_results[:2]):
|
|
684
|
+
preview = mem[:100] + "..." if len(mem) > 100 else mem
|
|
685
|
+
logger.debug(f" [{i+1}] {preview}")
|
|
686
|
+
|
|
687
|
+
return "\n".join(all_results) if all_results else ""
|