mcal-ai-crewai 0.2.0__tar.gz

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.
@@ -0,0 +1,57 @@
1
+ aws_config.env
2
+ *.pem
3
+ instance_info.txt
4
+ .env
5
+
6
+ # Deployment artifacts
7
+ *.tar.gz
8
+ *.zip
9
+
10
+ # Backup folders
11
+ backup_feb2_2026/
12
+ backup_*/
13
+
14
+ # Python cache
15
+ __pycache__/
16
+ *.pyc
17
+ *.pyo
18
+ .pytest_cache/
19
+
20
+ # Virtual environment
21
+ .venv/
22
+ venv/
23
+
24
+ # Logs
25
+ logs/
26
+
27
+ # IDE
28
+ .idea/
29
+ .vscode/
30
+
31
+ # OS files
32
+ .DS_Store
33
+
34
+ # Build artifacts
35
+ *.egg-info/
36
+ dist/
37
+ build/
38
+ .coverage
39
+ htmlcov/
40
+
41
+ # Test data and outputs
42
+ mcal_data/
43
+ results/*.json
44
+ experiments/__pycache__/
45
+
46
+ # Local development folders (archived)
47
+ docs/
48
+ data/
49
+ experiments/
50
+
51
+ # Debug output files
52
+ debug_output.txt
53
+ validation_output.txt
54
+ *.log
55
+
56
+ # Temporary PR body
57
+ .github/pr_body.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shiva
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcal-ai-crewai
3
+ Version: 0.2.0
4
+ Summary: CrewAI integration for MCAL - Goal-aware memory for AI agent crews
5
+ Project-URL: Homepage, https://github.com/Shivakoreddi/mcal-ai
6
+ Project-URL: Documentation, https://github.com/Shivakoreddi/mcal-ai/docs
7
+ Project-URL: Repository, https://github.com/Shivakoreddi/mcal-ai
8
+ Author: MCAL Team
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,crewai,goal-tracking,mcal,memory
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: crewai>=0.100.0
22
+ Requires-Dist: mcal-ai>=0.1.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
25
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
26
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # mcal-crewai
30
+
31
+ Goal-aware memory integration for CrewAI agent crews.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install mcal-crewai
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### Using MCALStorage (Mem0-style)
42
+
43
+ MCAL provides a storage backend that integrates directly with CrewAI's memory system:
44
+
45
+ ```python
46
+ from crewai import Crew, Agent, Task, Process
47
+ from crewai.memory.short_term.short_term_memory import ShortTermMemory
48
+ from crewai.memory.long_term.long_term_memory import LongTermMemory
49
+ from crewai.memory.entity.entity_memory import EntityMemory
50
+ from mcal_crewai import MCALStorage
51
+
52
+ # Create MCAL-backed memories
53
+ short_term = ShortTermMemory(
54
+ storage=MCALStorage(type="short_term", user_id="john")
55
+ )
56
+ long_term = LongTermMemory(
57
+ storage=MCALStorage(type="long_term", user_id="john")
58
+ )
59
+ entity_memory = EntityMemory(
60
+ storage=MCALStorage(type="entities", user_id="john")
61
+ )
62
+
63
+ # Use with CrewAI
64
+ crew = Crew(
65
+ agents=[agent],
66
+ tasks=[task],
67
+ memory=True,
68
+ short_term_memory=short_term,
69
+ long_term_memory=long_term,
70
+ entity_memory=entity_memory,
71
+ )
72
+ ```
73
+
74
+ ### Using External Memory
75
+
76
+ For cross-session persistence with goal awareness:
77
+
78
+ ```python
79
+ from crewai.memory.external.external_memory import ExternalMemory
80
+ from mcal_crewai import MCALStorage
81
+
82
+ external = ExternalMemory(
83
+ embedder_config={
84
+ "provider": "mcal",
85
+ "config": {
86
+ "user_id": "john",
87
+ "llm_provider": "anthropic",
88
+ "enable_goal_tracking": True,
89
+ }
90
+ }
91
+ )
92
+
93
+ crew = Crew(
94
+ agents=[...],
95
+ tasks=[...],
96
+ external_memory=external,
97
+ process=Process.sequential,
98
+ )
99
+ ```
100
+
101
+ ## Features
102
+
103
+ ### Goal-Aware Memory
104
+ Unlike basic memory systems, MCAL tracks user goals and priorities:
105
+
106
+ ```python
107
+ storage = MCALStorage(
108
+ type="long_term",
109
+ user_id="project_manager",
110
+ config={
111
+ "enable_goal_tracking": True,
112
+ "extract_priorities": True,
113
+ }
114
+ )
115
+ ```
116
+
117
+ ### Context Preservation
118
+ MCAL maintains reasoning context across agent handoffs:
119
+
120
+ ```python
121
+ # Agent 1 saves with context
122
+ await storage.save(
123
+ "Research findings on market trends",
124
+ metadata={
125
+ "agent": "researcher",
126
+ "goal": "market_analysis",
127
+ "confidence": 0.95
128
+ }
129
+ )
130
+
131
+ # Agent 2 retrieves with goal awareness
132
+ results = await storage.search(
133
+ "What do we know about market trends?",
134
+ limit=5,
135
+ score_threshold=0.7
136
+ )
137
+ ```
138
+
139
+ ### TTL Support
140
+ Automatic expiration for short-term memories:
141
+
142
+ ```python
143
+ storage = MCALStorage(
144
+ type="short_term",
145
+ user_id="session_user",
146
+ default_ttl=3600, # 1 hour
147
+ )
148
+ ```
149
+
150
+ ## Configuration
151
+
152
+ | Parameter | Type | Default | Description |
153
+ |-----------|------|---------|-------------|
154
+ | `type` | str | required | Memory type: "short_term", "long_term", "entities", "external" |
155
+ | `user_id` | str | "default" | User identifier for memory isolation |
156
+ | `llm_provider` | str | "anthropic" | LLM for goal extraction |
157
+ | `embedding_provider` | str | "openai" | Embedding model provider |
158
+ | `default_ttl` | int | None | Default TTL in seconds |
159
+ | `enable_goal_tracking` | bool | True | Enable goal extraction |
160
+
161
+ ## API Reference
162
+
163
+ ### MCALStorage
164
+
165
+ ```python
166
+ class MCALStorage(Storage):
167
+ """MCAL storage backend for CrewAI memory."""
168
+
169
+ def save(self, value: Any, metadata: dict) -> None:
170
+ """Save value with goal-aware processing."""
171
+
172
+ def search(
173
+ self,
174
+ query: str,
175
+ limit: int = 5,
176
+ score_threshold: float = 0.6
177
+ ) -> list:
178
+ """Search with goal-aware relevance."""
179
+
180
+ def reset(self) -> None:
181
+ """Clear all stored memories."""
182
+ ```
183
+
184
+ ## Comparison with Mem0
185
+
186
+ | Feature | Mem0 | MCAL |
187
+ |---------|------|------|
188
+ | Basic Memory | ✓ | ✓ |
189
+ | Goal Tracking | ✗ | ✓ |
190
+ | Priority Extraction | ✗ | ✓ |
191
+ | Context Preservation | ✗ | ✓ |
192
+ | TTL Support | ✗ | ✓ |
193
+ | Local Storage | ✓ | ✓ |
194
+ | Cloud API | ✓ | Coming |
195
+
196
+ ## License
197
+
198
+ MIT License
@@ -0,0 +1,170 @@
1
+ # mcal-crewai
2
+
3
+ Goal-aware memory integration for CrewAI agent crews.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install mcal-crewai
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Using MCALStorage (Mem0-style)
14
+
15
+ MCAL provides a storage backend that integrates directly with CrewAI's memory system:
16
+
17
+ ```python
18
+ from crewai import Crew, Agent, Task, Process
19
+ from crewai.memory.short_term.short_term_memory import ShortTermMemory
20
+ from crewai.memory.long_term.long_term_memory import LongTermMemory
21
+ from crewai.memory.entity.entity_memory import EntityMemory
22
+ from mcal_crewai import MCALStorage
23
+
24
+ # Create MCAL-backed memories
25
+ short_term = ShortTermMemory(
26
+ storage=MCALStorage(type="short_term", user_id="john")
27
+ )
28
+ long_term = LongTermMemory(
29
+ storage=MCALStorage(type="long_term", user_id="john")
30
+ )
31
+ entity_memory = EntityMemory(
32
+ storage=MCALStorage(type="entities", user_id="john")
33
+ )
34
+
35
+ # Use with CrewAI
36
+ crew = Crew(
37
+ agents=[agent],
38
+ tasks=[task],
39
+ memory=True,
40
+ short_term_memory=short_term,
41
+ long_term_memory=long_term,
42
+ entity_memory=entity_memory,
43
+ )
44
+ ```
45
+
46
+ ### Using External Memory
47
+
48
+ For cross-session persistence with goal awareness:
49
+
50
+ ```python
51
+ from crewai.memory.external.external_memory import ExternalMemory
52
+ from mcal_crewai import MCALStorage
53
+
54
+ external = ExternalMemory(
55
+ embedder_config={
56
+ "provider": "mcal",
57
+ "config": {
58
+ "user_id": "john",
59
+ "llm_provider": "anthropic",
60
+ "enable_goal_tracking": True,
61
+ }
62
+ }
63
+ )
64
+
65
+ crew = Crew(
66
+ agents=[...],
67
+ tasks=[...],
68
+ external_memory=external,
69
+ process=Process.sequential,
70
+ )
71
+ ```
72
+
73
+ ## Features
74
+
75
+ ### Goal-Aware Memory
76
+ Unlike basic memory systems, MCAL tracks user goals and priorities:
77
+
78
+ ```python
79
+ storage = MCALStorage(
80
+ type="long_term",
81
+ user_id="project_manager",
82
+ config={
83
+ "enable_goal_tracking": True,
84
+ "extract_priorities": True,
85
+ }
86
+ )
87
+ ```
88
+
89
+ ### Context Preservation
90
+ MCAL maintains reasoning context across agent handoffs:
91
+
92
+ ```python
93
+ # Agent 1 saves with context
94
+ await storage.save(
95
+ "Research findings on market trends",
96
+ metadata={
97
+ "agent": "researcher",
98
+ "goal": "market_analysis",
99
+ "confidence": 0.95
100
+ }
101
+ )
102
+
103
+ # Agent 2 retrieves with goal awareness
104
+ results = await storage.search(
105
+ "What do we know about market trends?",
106
+ limit=5,
107
+ score_threshold=0.7
108
+ )
109
+ ```
110
+
111
+ ### TTL Support
112
+ Automatic expiration for short-term memories:
113
+
114
+ ```python
115
+ storage = MCALStorage(
116
+ type="short_term",
117
+ user_id="session_user",
118
+ default_ttl=3600, # 1 hour
119
+ )
120
+ ```
121
+
122
+ ## Configuration
123
+
124
+ | Parameter | Type | Default | Description |
125
+ |-----------|------|---------|-------------|
126
+ | `type` | str | required | Memory type: "short_term", "long_term", "entities", "external" |
127
+ | `user_id` | str | "default" | User identifier for memory isolation |
128
+ | `llm_provider` | str | "anthropic" | LLM for goal extraction |
129
+ | `embedding_provider` | str | "openai" | Embedding model provider |
130
+ | `default_ttl` | int | None | Default TTL in seconds |
131
+ | `enable_goal_tracking` | bool | True | Enable goal extraction |
132
+
133
+ ## API Reference
134
+
135
+ ### MCALStorage
136
+
137
+ ```python
138
+ class MCALStorage(Storage):
139
+ """MCAL storage backend for CrewAI memory."""
140
+
141
+ def save(self, value: Any, metadata: dict) -> None:
142
+ """Save value with goal-aware processing."""
143
+
144
+ def search(
145
+ self,
146
+ query: str,
147
+ limit: int = 5,
148
+ score_threshold: float = 0.6
149
+ ) -> list:
150
+ """Search with goal-aware relevance."""
151
+
152
+ def reset(self) -> None:
153
+ """Clear all stored memories."""
154
+ ```
155
+
156
+ ## Comparison with Mem0
157
+
158
+ | Feature | Mem0 | MCAL |
159
+ |---------|------|------|
160
+ | Basic Memory | ✓ | ✓ |
161
+ | Goal Tracking | ✗ | ✓ |
162
+ | Priority Extraction | ✗ | ✓ |
163
+ | Context Preservation | ✗ | ✓ |
164
+ | TTL Support | ✗ | ✓ |
165
+ | Local Storage | ✓ | ✓ |
166
+ | Cloud API | ✓ | Coming |
167
+
168
+ ## License
169
+
170
+ MIT License
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcal-ai-crewai"
7
+ version = "0.2.0"
8
+ description = "CrewAI integration for MCAL - Goal-aware memory for AI agent crews"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "MCAL Team" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ ]
25
+ keywords = ["mcal", "crewai", "memory", "agents", "ai", "goal-tracking"]
26
+
27
+ dependencies = [
28
+ "mcal-ai>=0.1.0",
29
+ "crewai>=0.100.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7.0.0",
35
+ "pytest-asyncio>=0.21.0",
36
+ "pytest-cov>=4.0.0",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/Shivakoreddi/mcal-ai"
41
+ Documentation = "https://github.com/Shivakoreddi/mcal-ai/docs"
42
+ Repository = "https://github.com/Shivakoreddi/mcal-ai"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/mcal_crewai"]
46
+
47
+ [tool.pytest.ini_options]
48
+ asyncio_mode = "auto"
49
+ testpaths = ["tests"]
50
+ addopts = "-v --tb=short"
@@ -0,0 +1,23 @@
1
+ """
2
+ MCAL CrewAI Integration
3
+
4
+ Goal-aware memory for CrewAI agent crews.
5
+
6
+ Usage:
7
+ from mcal_crewai import MCALStorage
8
+
9
+ storage = MCALStorage(type="short_term", user_id="john")
10
+
11
+ # Use with CrewAI memory
12
+ from crewai.memory.short_term.short_term_memory import ShortTermMemory
13
+ memory = ShortTermMemory(storage=storage)
14
+ """
15
+
16
+ from mcal_crewai.storage import MCALStorage
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ __all__ = [
21
+ "MCALStorage",
22
+ "__version__",
23
+ ]
@@ -0,0 +1,316 @@
1
+ """
2
+ MCAL Storage Backend for CrewAI
3
+
4
+ Implements CrewAI's Storage interface with goal-aware memory.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ import time
11
+ from collections.abc import Iterable
12
+ from typing import Any, Optional
13
+
14
+ # Lazy import for MCAL to avoid circular dependencies
15
+ _mcal_instance: Optional[Any] = None
16
+ _mcal_lock = threading.Lock()
17
+
18
+
19
+ def _get_mcal(
20
+ llm_provider: str = "anthropic",
21
+ embedding_provider: str = "openai",
22
+ storage_path: Optional[str] = None,
23
+ **kwargs
24
+ ) -> Any:
25
+ """Get or create MCAL instance (lazy singleton)."""
26
+ global _mcal_instance
27
+
28
+ if _mcal_instance is None:
29
+ with _mcal_lock:
30
+ if _mcal_instance is None:
31
+ from mcal import MCAL
32
+ _mcal_instance = MCAL(
33
+ llm_provider=llm_provider,
34
+ embedding_provider=embedding_provider,
35
+ storage_path=storage_path,
36
+ **kwargs
37
+ )
38
+ return _mcal_instance
39
+
40
+
41
+ class MCALStorage:
42
+ """
43
+ MCAL storage backend for CrewAI memory.
44
+
45
+ This class implements CrewAI's Storage interface, providing
46
+ goal-aware memory with context preservation for agent crews.
47
+
48
+ Compatible with:
49
+ - ShortTermMemory
50
+ - LongTermMemory
51
+ - EntityMemory
52
+ - ExternalMemory
53
+
54
+ Usage:
55
+ from mcal_crewai import MCALStorage
56
+ from crewai.memory.short_term.short_term_memory import ShortTermMemory
57
+
58
+ storage = MCALStorage(type="short_term", user_id="john")
59
+ memory = ShortTermMemory(storage=storage)
60
+
61
+ Args:
62
+ type: Memory type - "short_term", "long_term", "entities", "external"
63
+ crew: Optional CrewAI Crew instance
64
+ config: Configuration dictionary with MCAL options
65
+ user_id: User identifier for memory isolation
66
+ default_ttl: Default TTL in seconds for memory items
67
+ enable_goal_tracking: Whether to extract goals from content
68
+ """
69
+
70
+ SUPPORTED_TYPES = {"short_term", "long_term", "entities", "external"}
71
+
72
+ def __init__(
73
+ self,
74
+ type: str,
75
+ crew: Any = None,
76
+ config: Optional[dict] = None,
77
+ user_id: str = "default",
78
+ default_ttl: Optional[int] = None,
79
+ enable_goal_tracking: bool = True,
80
+ **kwargs
81
+ ):
82
+ # Validate type
83
+ if type not in self.SUPPORTED_TYPES:
84
+ raise ValueError(
85
+ f"Invalid type '{type}'. Must be one of: {', '.join(self.SUPPORTED_TYPES)}"
86
+ )
87
+
88
+ self.memory_type = type
89
+ self.crew = crew
90
+ self.config = config or {}
91
+ # user_id priority: explicit param > config > default
92
+ self.user_id = user_id if user_id != "default" else self.config.get("user_id", user_id)
93
+ self.default_ttl = default_ttl
94
+ self.enable_goal_tracking = enable_goal_tracking
95
+
96
+ # Extract config values
97
+ self.llm_provider = self.config.get("llm_provider", "anthropic")
98
+ self.embedding_provider = self.config.get("embedding_provider", "openai")
99
+ self.storage_path = self.config.get("storage_path")
100
+
101
+ # TTL tracking (lazy expiration)
102
+ self._ttl: dict[str, int] = {} # key -> ttl_seconds
103
+ self._expires_at: dict[str, float] = {} # key -> expiration_timestamp
104
+
105
+ # Thread safety
106
+ self._lock = threading.RLock()
107
+
108
+ # Internal storage (in-memory for now, will use MCAL graph later)
109
+ self._data: dict[str, dict[str, Any]] = {}
110
+
111
+ # MCAL instance (lazy loaded)
112
+ self._mcal: Optional[Any] = None
113
+
114
+ @property
115
+ def mcal(self) -> Any:
116
+ """Lazy load MCAL instance."""
117
+ if self._mcal is None:
118
+ self._mcal = _get_mcal(
119
+ llm_provider=self.llm_provider,
120
+ embedding_provider=self.embedding_provider,
121
+ storage_path=self.storage_path,
122
+ )
123
+ return self._mcal
124
+
125
+ def _generate_key(self, value: Any, metadata: dict) -> str:
126
+ """Generate a unique key for storage."""
127
+ import hashlib
128
+ content = str(value) + str(metadata)
129
+ return hashlib.sha256(content.encode()).hexdigest()[:16]
130
+
131
+ def _is_expired(self, key: str) -> bool:
132
+ """Check if a key has expired."""
133
+ if key not in self._expires_at:
134
+ return False
135
+ return time.time() > self._expires_at[key]
136
+
137
+ def _set_ttl(self, key: str, ttl: Optional[int] = None) -> None:
138
+ """Set TTL for a key."""
139
+ ttl_value = ttl or self.default_ttl
140
+ if ttl_value is not None:
141
+ self._ttl[key] = ttl_value
142
+ self._expires_at[key] = time.time() + ttl_value
143
+
144
+ def _cleanup_expired(self) -> None:
145
+ """Remove expired entries (lazy cleanup)."""
146
+ expired_keys = [
147
+ key for key in self._expires_at
148
+ if time.time() > self._expires_at[key]
149
+ ]
150
+ for key in expired_keys:
151
+ self._data.pop(key, None)
152
+ self._ttl.pop(key, None)
153
+ self._expires_at.pop(key, None)
154
+
155
+ def _extract_last_content(
156
+ self,
157
+ messages: Iterable[dict[str, Any]],
158
+ role: str
159
+ ) -> str:
160
+ """Extract last message content for a given role."""
161
+ return next(
162
+ (
163
+ m.get("content", "")
164
+ for m in reversed(list(messages))
165
+ if m.get("role") == role
166
+ ),
167
+ "",
168
+ )
169
+
170
+ def _get_agent_name(self) -> str:
171
+ """Get current agent name from crew context."""
172
+ if self.crew and hasattr(self.crew, "_current_agent"):
173
+ agent = self.crew._current_agent
174
+ if agent and hasattr(agent, "role"):
175
+ return str(agent.role)
176
+ return self.config.get("agent_id", "default_agent")
177
+
178
+ def save(self, value: Any, metadata: dict[str, Any]) -> None:
179
+ """
180
+ Save a value to MCAL storage.
181
+
182
+ Args:
183
+ value: The content to save (string, dict, or conversation)
184
+ metadata: Additional metadata (agent, task, etc.)
185
+ """
186
+ with self._lock:
187
+ # Generate key
188
+ key = self._generate_key(value, metadata)
189
+
190
+ # Process value based on type
191
+ if isinstance(value, dict) and "messages" in value:
192
+ # Conversation format
193
+ messages = value.get("messages", [])
194
+ content = self._extract_last_content(messages, "assistant")
195
+ if not content:
196
+ content = self._extract_last_content(messages, "user")
197
+ elif isinstance(value, str):
198
+ content = value
199
+ else:
200
+ content = str(value)
201
+
202
+ # Build storage entry
203
+ entry = {
204
+ "content": content,
205
+ "raw_value": value,
206
+ "metadata": {
207
+ "type": self.memory_type,
208
+ "user_id": self.user_id,
209
+ "agent": self._get_agent_name(),
210
+ "timestamp": time.time(),
211
+ **metadata,
212
+ },
213
+ }
214
+
215
+ # Extract goals if enabled
216
+ if self.enable_goal_tracking and content:
217
+ entry["metadata"]["goal_tracked"] = True
218
+ # TODO: Use MCAL's goal extraction when integrated
219
+ # goals = self.mcal.extract_goals(content)
220
+ # entry["metadata"]["goals"] = goals
221
+
222
+ # Store with TTL
223
+ self._data[key] = entry
224
+
225
+ # Set TTL if configured
226
+ ttl = metadata.get("ttl") or self.default_ttl
227
+ if ttl:
228
+ self._set_ttl(key, ttl)
229
+
230
+ # Short-term memory gets default TTL if not set
231
+ if self.memory_type == "short_term" and key not in self._ttl:
232
+ self._set_ttl(key, 3600) # 1 hour default for short-term
233
+
234
+ def search(
235
+ self,
236
+ query: str,
237
+ limit: int = 5,
238
+ score_threshold: float = 0.6
239
+ ) -> list[Any]:
240
+ """
241
+ Search storage for relevant content.
242
+
243
+ Args:
244
+ query: Search query
245
+ limit: Maximum results to return
246
+ score_threshold: Minimum relevance score (0-1)
247
+
248
+ Returns:
249
+ List of matching results with 'content' key
250
+ """
251
+ with self._lock:
252
+ # Cleanup expired entries
253
+ self._cleanup_expired()
254
+
255
+ # Simple keyword-based search for now
256
+ # TODO: Use MCAL's semantic search when integrated
257
+ results = []
258
+ query_lower = query.lower()
259
+ query_words = set(query_lower.split())
260
+
261
+ for key, entry in self._data.items():
262
+ # Skip expired
263
+ if self._is_expired(key):
264
+ continue
265
+
266
+ content = entry.get("content", "")
267
+ content_lower = content.lower()
268
+
269
+ # Calculate simple relevance score
270
+ content_words = set(content_lower.split())
271
+ overlap = query_words & content_words
272
+
273
+ if overlap:
274
+ score = len(overlap) / max(len(query_words), 1)
275
+
276
+ if score >= score_threshold:
277
+ results.append({
278
+ "content": content,
279
+ "memory": content, # Compatibility with Mem0 format
280
+ "score": score,
281
+ "metadata": entry.get("metadata", {}),
282
+ })
283
+
284
+ # Sort by score and limit
285
+ results.sort(key=lambda x: x["score"], reverse=True)
286
+ return results[:limit]
287
+
288
+ def reset(self) -> None:
289
+ """Clear all stored memories for this type."""
290
+ with self._lock:
291
+ self._data.clear()
292
+ self._ttl.clear()
293
+ self._expires_at.clear()
294
+
295
+ def get_all(self) -> list[dict[str, Any]]:
296
+ """Get all non-expired entries."""
297
+ with self._lock:
298
+ self._cleanup_expired()
299
+ return [
300
+ entry for key, entry in self._data.items()
301
+ if not self._is_expired(key)
302
+ ]
303
+
304
+ def delete(self, key: str) -> bool:
305
+ """Delete a specific entry by key."""
306
+ with self._lock:
307
+ if key in self._data:
308
+ del self._data[key]
309
+ self._ttl.pop(key, None)
310
+ self._expires_at.pop(key, None)
311
+ return True
312
+ return False
313
+
314
+
315
+ # Alias for backward compatibility
316
+ MCALMemoryStorage = MCALStorage
@@ -0,0 +1 @@
1
+ """Tests for mcal-crewai package."""
@@ -0,0 +1,310 @@
1
+ """
2
+ Tests for MCALStorage - CrewAI Storage backend.
3
+ """
4
+
5
+ import pytest
6
+ import time
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ from mcal_crewai import MCALStorage
10
+
11
+
12
+ class TestMCALStorageInit:
13
+ """Test MCALStorage initialization."""
14
+
15
+ def test_init_with_valid_type(self):
16
+ """Test initialization with valid memory types."""
17
+ for memory_type in ["short_term", "long_term", "entities", "external"]:
18
+ storage = MCALStorage(type=memory_type)
19
+ assert storage.memory_type == memory_type
20
+
21
+ def test_init_with_invalid_type(self):
22
+ """Test initialization with invalid type raises error."""
23
+ with pytest.raises(ValueError, match="Invalid type"):
24
+ MCALStorage(type="invalid_type")
25
+
26
+ def test_init_with_user_id(self):
27
+ """Test initialization with user_id."""
28
+ storage = MCALStorage(type="short_term", user_id="test_user")
29
+ assert storage.user_id == "test_user"
30
+
31
+ def test_init_with_config_user_id(self):
32
+ """Test user_id from config."""
33
+ storage = MCALStorage(
34
+ type="short_term",
35
+ config={"user_id": "config_user"}
36
+ )
37
+ assert storage.user_id == "config_user"
38
+
39
+ def test_init_with_default_ttl(self):
40
+ """Test initialization with default TTL."""
41
+ storage = MCALStorage(type="short_term", default_ttl=3600)
42
+ assert storage.default_ttl == 3600
43
+
44
+ def test_init_goal_tracking_enabled(self):
45
+ """Test goal tracking is enabled by default."""
46
+ storage = MCALStorage(type="short_term")
47
+ assert storage.enable_goal_tracking is True
48
+
49
+ def test_init_goal_tracking_disabled(self):
50
+ """Test goal tracking can be disabled."""
51
+ storage = MCALStorage(type="short_term", enable_goal_tracking=False)
52
+ assert storage.enable_goal_tracking is False
53
+
54
+
55
+ class TestMCALStorageSave:
56
+ """Test MCALStorage save operations."""
57
+
58
+ def test_save_string_value(self):
59
+ """Test saving a simple string."""
60
+ storage = MCALStorage(type="long_term", user_id="test")
61
+ storage.save("Test content", {"agent": "researcher"})
62
+
63
+ results = storage.get_all()
64
+ assert len(results) == 1
65
+ assert results[0]["content"] == "Test content"
66
+
67
+ def test_save_with_metadata(self):
68
+ """Test metadata is preserved."""
69
+ storage = MCALStorage(type="long_term", user_id="test")
70
+ storage.save("Content", {"agent": "writer", "task": "summarize"})
71
+
72
+ results = storage.get_all()
73
+ assert results[0]["metadata"]["agent"] == "writer"
74
+ assert results[0]["metadata"]["task"] == "summarize"
75
+
76
+ def test_save_conversation_format(self):
77
+ """Test saving conversation format (messages list)."""
78
+ storage = MCALStorage(type="short_term", user_id="test")
79
+
80
+ conversation = {
81
+ "messages": [
82
+ {"role": "user", "content": "What is the weather?"},
83
+ {"role": "assistant", "content": "It's sunny today."}
84
+ ]
85
+ }
86
+ storage.save(conversation, {})
87
+
88
+ results = storage.get_all()
89
+ assert len(results) == 1
90
+ assert "sunny" in results[0]["content"]
91
+
92
+ def test_save_with_explicit_ttl(self):
93
+ """Test saving with explicit TTL in metadata."""
94
+ storage = MCALStorage(type="short_term", user_id="test")
95
+ storage.save("Temporary", {"ttl": 60})
96
+
97
+ # Check TTL was set
98
+ assert len(storage._ttl) == 1
99
+ assert len(storage._expires_at) == 1
100
+
101
+
102
+ class TestMCALStorageSearch:
103
+ """Test MCALStorage search operations."""
104
+
105
+ def test_search_finds_matching_content(self):
106
+ """Test search finds relevant content."""
107
+ storage = MCALStorage(type="long_term", user_id="test")
108
+ storage.save("Python is a programming language", {})
109
+ storage.save("JavaScript runs in browsers", {})
110
+
111
+ results = storage.search("Python programming")
112
+ assert len(results) >= 1
113
+ assert "Python" in results[0]["content"]
114
+
115
+ def test_search_respects_limit(self):
116
+ """Test search respects limit parameter."""
117
+ storage = MCALStorage(type="long_term", user_id="test")
118
+ for i in range(10):
119
+ storage.save(f"Document {i} about Python", {})
120
+
121
+ results = storage.search("Python", limit=3)
122
+ assert len(results) <= 3
123
+
124
+ def test_search_returns_empty_for_no_match(self):
125
+ """Test search returns empty for no matches."""
126
+ storage = MCALStorage(type="long_term", user_id="test")
127
+ storage.save("Python is great", {})
128
+
129
+ results = storage.search("completely unrelated xyz")
130
+ assert len(results) == 0
131
+
132
+ def test_search_includes_score(self):
133
+ """Test search results include relevance score."""
134
+ storage = MCALStorage(type="long_term", user_id="test")
135
+ storage.save("Python programming tutorial", {})
136
+
137
+ results = storage.search("Python programming")
138
+ assert len(results) >= 1
139
+ assert "score" in results[0]
140
+ assert 0 <= results[0]["score"] <= 1
141
+
142
+ def test_search_skips_expired_content(self):
143
+ """Test search skips expired content."""
144
+ storage = MCALStorage(type="short_term", user_id="test")
145
+
146
+ # Save with very short TTL
147
+ storage.save("Expired content about cats", {"ttl": 1})
148
+ storage.save("Valid content about dogs", {})
149
+
150
+ # Wait for expiration
151
+ time.sleep(1.1)
152
+
153
+ results = storage.search("cats dogs")
154
+ # Should only find dogs, not cats
155
+ for result in results:
156
+ assert "cats" not in result["content"].lower()
157
+
158
+
159
+ class TestMCALStorageTTL:
160
+ """Test TTL (Time-To-Live) functionality."""
161
+
162
+ def test_ttl_expiration(self):
163
+ """Test items expire after TTL."""
164
+ storage = MCALStorage(type="short_term", default_ttl=1)
165
+ storage.save("Temporary data", {})
166
+
167
+ # Should exist initially
168
+ assert len(storage.get_all()) == 1
169
+
170
+ # Wait for expiration
171
+ time.sleep(1.1)
172
+
173
+ # Should be gone
174
+ assert len(storage.get_all()) == 0
175
+
176
+ def test_no_ttl_means_no_expiration(self):
177
+ """Test items without TTL don't expire."""
178
+ storage = MCALStorage(type="long_term") # No default_ttl
179
+ storage.save("Permanent data", {})
180
+
181
+ # Should persist
182
+ assert len(storage.get_all()) == 1
183
+
184
+ # Clear TTL tracking to ensure no expiration
185
+ storage._expires_at.clear()
186
+
187
+ time.sleep(0.1)
188
+ assert len(storage.get_all()) == 1
189
+
190
+ def test_short_term_gets_default_ttl(self):
191
+ """Test short_term memory gets default TTL."""
192
+ storage = MCALStorage(type="short_term")
193
+ storage.save("Short term data", {})
194
+
195
+ # Should have TTL set (1 hour default)
196
+ assert len(storage._ttl) == 1
197
+
198
+
199
+ class TestMCALStorageReset:
200
+ """Test reset functionality."""
201
+
202
+ def test_reset_clears_all_data(self):
203
+ """Test reset clears all stored data."""
204
+ storage = MCALStorage(type="long_term", user_id="test")
205
+ storage.save("Data 1", {})
206
+ storage.save("Data 2", {})
207
+
208
+ assert len(storage.get_all()) == 2
209
+
210
+ storage.reset()
211
+
212
+ assert len(storage.get_all()) == 0
213
+
214
+ def test_reset_clears_ttl_tracking(self):
215
+ """Test reset clears TTL tracking."""
216
+ storage = MCALStorage(type="short_term", default_ttl=3600)
217
+ storage.save("Data", {})
218
+
219
+ assert len(storage._ttl) == 1
220
+ assert len(storage._expires_at) == 1
221
+
222
+ storage.reset()
223
+
224
+ assert len(storage._ttl) == 0
225
+ assert len(storage._expires_at) == 0
226
+
227
+
228
+ class TestMCALStorageDelete:
229
+ """Test delete functionality."""
230
+
231
+ def test_delete_existing_key(self):
232
+ """Test deleting an existing key."""
233
+ storage = MCALStorage(type="long_term")
234
+ storage.save("Test data", {})
235
+
236
+ keys = list(storage._data.keys())
237
+ assert len(keys) == 1
238
+
239
+ result = storage.delete(keys[0])
240
+ assert result is True
241
+ assert len(storage.get_all()) == 0
242
+
243
+ def test_delete_nonexistent_key(self):
244
+ """Test deleting a nonexistent key."""
245
+ storage = MCALStorage(type="long_term")
246
+
247
+ result = storage.delete("nonexistent_key")
248
+ assert result is False
249
+
250
+
251
+ class TestMCALStorageThreadSafety:
252
+ """Test thread safety."""
253
+
254
+ def test_storage_has_lock(self):
255
+ """Test storage has RLock for thread safety."""
256
+ storage = MCALStorage(type="long_term")
257
+ assert hasattr(storage, "_lock")
258
+ assert isinstance(storage._lock, type(storage._lock))
259
+
260
+
261
+ class TestMCALStorageMemoryFormat:
262
+ """Test compatibility with CrewAI memory format."""
263
+
264
+ def test_search_returns_memory_key(self):
265
+ """Test search results have 'memory' key for Mem0 compatibility."""
266
+ storage = MCALStorage(type="long_term")
267
+ storage.save("Test content for memory", {})
268
+
269
+ results = storage.search("memory")
270
+ assert len(results) >= 1
271
+ assert "memory" in results[0]
272
+ assert results[0]["memory"] == results[0]["content"]
273
+
274
+
275
+ class TestMCALStorageMetadataExtraction:
276
+ """Test metadata extraction."""
277
+
278
+ def test_metadata_includes_type(self):
279
+ """Test metadata includes memory type."""
280
+ storage = MCALStorage(type="entities")
281
+ storage.save("Entity data", {})
282
+
283
+ results = storage.get_all()
284
+ assert results[0]["metadata"]["type"] == "entities"
285
+
286
+ def test_metadata_includes_user_id(self):
287
+ """Test metadata includes user_id."""
288
+ storage = MCALStorage(type="long_term", user_id="john")
289
+ storage.save("User data", {})
290
+
291
+ results = storage.get_all()
292
+ assert results[0]["metadata"]["user_id"] == "john"
293
+
294
+ def test_metadata_includes_timestamp(self):
295
+ """Test metadata includes timestamp."""
296
+ storage = MCALStorage(type="long_term")
297
+ storage.save("Timestamped data", {})
298
+
299
+ results = storage.get_all()
300
+ assert "timestamp" in results[0]["metadata"]
301
+ assert results[0]["metadata"]["timestamp"] > 0
302
+
303
+
304
+ class TestMCALStorageBackwardCompatibility:
305
+ """Test backward compatibility."""
306
+
307
+ def test_alias_exists(self):
308
+ """Test MCALMemoryStorage alias exists."""
309
+ from mcal_crewai.storage import MCALMemoryStorage
310
+ assert MCALMemoryStorage is MCALStorage