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.
- mcal_ai_crewai-0.2.0/.gitignore +57 -0
- mcal_ai_crewai-0.2.0/LICENSE +21 -0
- mcal_ai_crewai-0.2.0/PKG-INFO +198 -0
- mcal_ai_crewai-0.2.0/README.md +170 -0
- mcal_ai_crewai-0.2.0/pyproject.toml +50 -0
- mcal_ai_crewai-0.2.0/src/mcal_crewai/__init__.py +23 -0
- mcal_ai_crewai-0.2.0/src/mcal_crewai/storage.py +316 -0
- mcal_ai_crewai-0.2.0/tests/__init__.py +1 -0
- mcal_ai_crewai-0.2.0/tests/test_storage.py +310 -0
|
@@ -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
|