massgen 0.1.3__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.

Files changed (90) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/api_params_handler/_chat_completions_api_params_handler.py +4 -0
  3. massgen/api_params_handler/_claude_api_params_handler.py +4 -0
  4. massgen/api_params_handler/_gemini_api_params_handler.py +4 -0
  5. massgen/api_params_handler/_response_api_params_handler.py +4 -0
  6. massgen/backend/base_with_custom_tool_and_mcp.py +25 -5
  7. massgen/backend/docs/permissions_and_context_files.md +2 -2
  8. massgen/backend/response.py +2 -0
  9. massgen/chat_agent.py +340 -20
  10. massgen/cli.py +326 -19
  11. massgen/configs/README.md +92 -41
  12. massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
  13. massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
  14. massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
  15. massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
  16. massgen/configs/memory/single_agent_compression_test.yaml +64 -0
  17. massgen/configs/tools/custom_tools/crawl4ai_example.yaml +55 -0
  18. massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_multi.yaml +61 -0
  19. massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_single.yaml +29 -0
  20. massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_multi.yaml +51 -0
  21. massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_single.yaml +33 -0
  22. massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_multi.yaml +55 -0
  23. massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_single.yaml +33 -0
  24. massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_multi.yaml +47 -0
  25. massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_single.yaml +29 -0
  26. massgen/configs/tools/custom_tools/multimodal_tools/understand_audio.yaml +1 -1
  27. massgen/configs/tools/custom_tools/multimodal_tools/understand_file.yaml +1 -1
  28. massgen/configs/tools/custom_tools/multimodal_tools/understand_image.yaml +1 -1
  29. massgen/configs/tools/custom_tools/multimodal_tools/understand_video.yaml +1 -1
  30. massgen/configs/tools/custom_tools/multimodal_tools/youtube_video_analysis.yaml +1 -1
  31. massgen/filesystem_manager/_filesystem_manager.py +1 -0
  32. massgen/filesystem_manager/_path_permission_manager.py +148 -0
  33. massgen/memory/README.md +277 -0
  34. massgen/memory/__init__.py +26 -0
  35. massgen/memory/_base.py +193 -0
  36. massgen/memory/_compression.py +237 -0
  37. massgen/memory/_context_monitor.py +211 -0
  38. massgen/memory/_conversation.py +255 -0
  39. massgen/memory/_fact_extraction_prompts.py +333 -0
  40. massgen/memory/_mem0_adapters.py +257 -0
  41. massgen/memory/_persistent.py +687 -0
  42. massgen/memory/docker-compose.qdrant.yml +36 -0
  43. massgen/memory/docs/DESIGN.md +388 -0
  44. massgen/memory/docs/QUICKSTART.md +409 -0
  45. massgen/memory/docs/SUMMARY.md +319 -0
  46. massgen/memory/docs/agent_use_memory.md +408 -0
  47. massgen/memory/docs/orchestrator_use_memory.md +586 -0
  48. massgen/memory/examples.py +237 -0
  49. massgen/message_templates.py +160 -12
  50. massgen/orchestrator.py +223 -7
  51. massgen/tests/memory/test_agent_compression.py +174 -0
  52. massgen/{configs/tools → tests}/memory/test_context_window_management.py +30 -30
  53. massgen/tests/memory/test_force_compression.py +154 -0
  54. massgen/tests/memory/test_simple_compression.py +147 -0
  55. massgen/tests/test_agent_memory.py +534 -0
  56. massgen/tests/test_binary_file_blocking.py +274 -0
  57. massgen/tests/test_case_studies.md +12 -12
  58. massgen/tests/test_conversation_memory.py +382 -0
  59. massgen/tests/test_multimodal_size_limits.py +407 -0
  60. massgen/tests/test_orchestrator_memory.py +620 -0
  61. massgen/tests/test_persistent_memory.py +435 -0
  62. massgen/token_manager/token_manager.py +6 -0
  63. massgen/tool/_manager.py +7 -2
  64. massgen/tool/_multimodal_tools/image_to_image_generation.py +293 -0
  65. massgen/tool/_multimodal_tools/text_to_file_generation.py +455 -0
  66. massgen/tool/_multimodal_tools/text_to_image_generation.py +222 -0
  67. massgen/tool/_multimodal_tools/text_to_speech_continue_generation.py +226 -0
  68. massgen/tool/_multimodal_tools/text_to_speech_transcription_generation.py +217 -0
  69. massgen/tool/_multimodal_tools/text_to_video_generation.py +223 -0
  70. massgen/tool/_multimodal_tools/understand_audio.py +19 -1
  71. massgen/tool/_multimodal_tools/understand_file.py +6 -1
  72. massgen/tool/_multimodal_tools/understand_image.py +112 -8
  73. massgen/tool/_multimodal_tools/understand_video.py +32 -5
  74. massgen/tool/_web_tools/crawl4ai_tool.py +718 -0
  75. massgen/tool/docs/multimodal_tools.md +589 -0
  76. massgen/tools/__init__.py +8 -0
  77. massgen/tools/_planning_mcp_server.py +520 -0
  78. massgen/tools/planning_dataclasses.py +434 -0
  79. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/METADATA +142 -82
  80. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/RECORD +84 -41
  81. massgen/configs/tools/custom_tools/crawl4ai_mcp_example.yaml +0 -67
  82. massgen/configs/tools/custom_tools/crawl4ai_multi_agent_example.yaml +0 -68
  83. massgen/configs/tools/memory/README.md +0 -199
  84. massgen/configs/tools/memory/gpt5mini_gemini_context_window_management.yaml +0 -131
  85. massgen/configs/tools/memory/gpt5mini_gemini_no_persistent_memory.yaml +0 -133
  86. massgen/configs/tools/multimodal/gpt5mini_gpt5nano_documentation_evolution.yaml +0 -97
  87. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/WHEEL +0 -0
  88. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/entry_points.txt +0 -0
  89. {massgen-0.1.3.dist-info → massgen-0.1.5.dist-info}/licenses/LICENSE +0 -0
  90. {massgen-0.1.3.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 ""