mcal-ai-autogen 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcal_ai_autogen-0.2.0.dist-info/METADATA +246 -0
- mcal_ai_autogen-0.2.0.dist-info/RECORD +8 -0
- mcal_ai_autogen-0.2.0.dist-info/WHEEL +5 -0
- mcal_ai_autogen-0.2.0.dist-info/licenses/LICENSE +21 -0
- mcal_ai_autogen-0.2.0.dist-info/top_level.txt +1 -0
- mcal_autogen/__init__.py +10 -0
- mcal_autogen/_compat.py +67 -0
- mcal_autogen/memory.py +493 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcal-ai-autogen
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Microsoft AutoGen integration for MCAL - Goal-aware memory for multi-agent systems
|
|
5
|
+
Author: MCAL Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Shivakoreddi/mcal-ai
|
|
8
|
+
Project-URL: Documentation, https://github.com/Shivakoreddi/mcal-ai/tree/main/packages/mcal-autogen
|
|
9
|
+
Project-URL: Repository, https://github.com/Shivakoreddi/mcal-ai
|
|
10
|
+
Keywords: mcal,autogen,memory,llm,agents,goal-aware,multi-agent
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcal-ai>=0.1.0
|
|
23
|
+
Provides-Extra: autogen
|
|
24
|
+
Requires-Dist: autogen-core>=0.4.0; extra == "autogen"
|
|
25
|
+
Requires-Dist: autogen-agentchat>=0.4.0; extra == "autogen"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: mcal-autogen[autogen,dev]; extra == "all"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# mcal-autogen
|
|
35
|
+
|
|
36
|
+
Microsoft AutoGen integration for MCAL (Multi-turn Conversation Abstraction Layer), bringing goal-aware memory to AutoGen agents.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install mcal-autogen
|
|
42
|
+
|
|
43
|
+
# With AutoGen dependencies
|
|
44
|
+
pip install mcal-autogen[autogen]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
51
|
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
52
|
+
from mcal import MCAL
|
|
53
|
+
from mcal_autogen import MCALMemory
|
|
54
|
+
|
|
55
|
+
# Initialize MCAL with your project goal
|
|
56
|
+
mcal = MCAL(goal="Help users build data pipelines")
|
|
57
|
+
|
|
58
|
+
# Create MCAL-backed memory
|
|
59
|
+
memory = MCALMemory(mcal, user_id="user_123")
|
|
60
|
+
|
|
61
|
+
# Create an agent with MCAL memory
|
|
62
|
+
model_client = OpenAIChatCompletionClient(model="gpt-4")
|
|
63
|
+
agent = AssistantAgent(
|
|
64
|
+
name="data_engineer",
|
|
65
|
+
model_client=model_client,
|
|
66
|
+
memory=[memory],
|
|
67
|
+
system_message="You are a helpful data engineering assistant.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Use the agent - MCAL automatically tracks context and decisions
|
|
71
|
+
result = await agent.run(task="How should I set up my ETL pipeline?")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
### Goal-Aware Memory
|
|
77
|
+
|
|
78
|
+
MCAL's unique value is understanding your project's goals and maintaining context across conversations:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# Initialize with a clear goal
|
|
82
|
+
mcal = MCAL(goal="Build a real-time fraud detection system")
|
|
83
|
+
memory = MCALMemory(mcal)
|
|
84
|
+
|
|
85
|
+
# Add relevant context
|
|
86
|
+
from autogen_core.memory import MemoryContent
|
|
87
|
+
await memory.add(MemoryContent(
|
|
88
|
+
content="We decided to use Kafka for streaming",
|
|
89
|
+
mime_type="text/plain",
|
|
90
|
+
metadata={"category": "architecture", "decision": True}
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
# Query returns goal-relevant results
|
|
94
|
+
results = await memory.query("What messaging system should I use?")
|
|
95
|
+
# Returns Kafka decision with goal-relevance scoring
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Decision Tracking
|
|
99
|
+
|
|
100
|
+
Track architectural and project decisions automatically:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
memory = MCALMemory(
|
|
104
|
+
mcal,
|
|
105
|
+
enable_goal_tracking=True, # Extract goals from content
|
|
106
|
+
include_decisions=True, # Include decisions in search
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Decisions are automatically tracked
|
|
110
|
+
await memory.add(MemoryContent(
|
|
111
|
+
content="After evaluating options, we chose PostgreSQL for its JSON support",
|
|
112
|
+
mime_type="text/plain"
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
# Query finds relevant decisions
|
|
116
|
+
results = await memory.query("database selection")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### User Isolation
|
|
120
|
+
|
|
121
|
+
Support multi-tenant scenarios with user isolation:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# Create separate memories for different users
|
|
125
|
+
user1_memory = MCALMemory(mcal, user_id="alice")
|
|
126
|
+
user2_memory = MCALMemory(mcal, user_id="bob")
|
|
127
|
+
|
|
128
|
+
# Each user has isolated memory
|
|
129
|
+
await user1_memory.add(MemoryContent(content="Alice prefers Python"))
|
|
130
|
+
await user2_memory.add(MemoryContent(content="Bob prefers Rust"))
|
|
131
|
+
|
|
132
|
+
# Queries only return user-specific results
|
|
133
|
+
results = await user1_memory.query("language preference")
|
|
134
|
+
# Only returns Alice's preference
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### TTL Support
|
|
138
|
+
|
|
139
|
+
Configure time-to-live for memory entries:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
memory = MCALMemory(mcal, default_ttl_minutes=60) # 1 hour default
|
|
143
|
+
|
|
144
|
+
# Or per-entry TTL via metadata
|
|
145
|
+
await memory.add(MemoryContent(
|
|
146
|
+
content="Temporary context",
|
|
147
|
+
mime_type="text/plain",
|
|
148
|
+
metadata={"ttl_minutes": 15} # 15 minute TTL
|
|
149
|
+
))
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Integration with AutoGen Features
|
|
153
|
+
|
|
154
|
+
### With AssistantAgent
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
158
|
+
|
|
159
|
+
agent = AssistantAgent(
|
|
160
|
+
name="assistant",
|
|
161
|
+
model_client=model_client,
|
|
162
|
+
memory=[memory], # MCAL memory integrates seamlessly
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### With Teams
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from autogen_agentchat.teams import RoundRobinGroupChat
|
|
170
|
+
|
|
171
|
+
# Share MCAL memory across team members
|
|
172
|
+
shared_memory = MCALMemory(mcal, user_id="team_alpha")
|
|
173
|
+
|
|
174
|
+
coder = AssistantAgent("coder", model_client=model_client, memory=[shared_memory])
|
|
175
|
+
reviewer = AssistantAgent("reviewer", model_client=model_client, memory=[shared_memory])
|
|
176
|
+
|
|
177
|
+
team = RoundRobinGroupChat([coder, reviewer])
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Context Window Management
|
|
181
|
+
|
|
182
|
+
MCAL automatically manages context relevance:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
memory = MCALMemory(
|
|
186
|
+
mcal,
|
|
187
|
+
max_results=10, # Limit results per query
|
|
188
|
+
score_threshold=0.5, # Minimum relevance score
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# update_context adds relevant memories to the agent's context
|
|
192
|
+
result = await memory.update_context(model_context)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## API Reference
|
|
196
|
+
|
|
197
|
+
### MCALMemory
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
class MCALMemory(Memory):
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
mcal: MCAL,
|
|
204
|
+
user_id: str = "default",
|
|
205
|
+
name: str = "mcal_memory",
|
|
206
|
+
max_results: int = 10,
|
|
207
|
+
score_threshold: float = 0.0,
|
|
208
|
+
default_ttl_minutes: Optional[float] = None,
|
|
209
|
+
enable_goal_tracking: bool = True,
|
|
210
|
+
include_decisions: bool = True,
|
|
211
|
+
):
|
|
212
|
+
"""
|
|
213
|
+
Initialize MCAL-backed memory for AutoGen.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
mcal: Initialized MCAL instance
|
|
217
|
+
user_id: User identifier for memory isolation
|
|
218
|
+
name: Memory instance name
|
|
219
|
+
max_results: Maximum results to return from queries
|
|
220
|
+
score_threshold: Minimum relevance score (0-1)
|
|
221
|
+
default_ttl_minutes: Default TTL in minutes
|
|
222
|
+
enable_goal_tracking: Extract goals from content
|
|
223
|
+
include_decisions: Include decisions in search results
|
|
224
|
+
"""
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Key Methods
|
|
228
|
+
|
|
229
|
+
| Method | Description |
|
|
230
|
+
|--------|-------------|
|
|
231
|
+
| `add(content)` | Add content to memory |
|
|
232
|
+
| `query(query)` | Search for relevant memories |
|
|
233
|
+
| `update_context(model_context)` | Update agent context with memories |
|
|
234
|
+
| `clear()` | Clear all memory entries |
|
|
235
|
+
| `close()` | Cleanup resources |
|
|
236
|
+
|
|
237
|
+
## Requirements
|
|
238
|
+
|
|
239
|
+
- Python >= 3.10
|
|
240
|
+
- mcal >= 0.1.0
|
|
241
|
+
- autogen-core >= 0.4.0 (optional)
|
|
242
|
+
- autogen-agentchat >= 0.4.0 (optional)
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT License
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mcal_ai_autogen-0.2.0.dist-info/licenses/LICENSE,sha256=zdp5kxDzb-kYvBiEZ_h1Hi96z-o6e5oXoXFx2IIefCs,1062
|
|
2
|
+
mcal_autogen/__init__.py,sha256=kz9Y4F2Ji_FQAPGChDyQKo8I78plamx059yb30Zk0AY,187
|
|
3
|
+
mcal_autogen/_compat.py,sha256=8U8sj3F6hvfBnKjSpyjCZEDHnzpxc3J9tHx93LPfwbU,1868
|
|
4
|
+
mcal_autogen/memory.py,sha256=7su6wQ7-5rdxAhFSufw8at5c5mBEpOpKeeiVvN7R_SE,16959
|
|
5
|
+
mcal_ai_autogen-0.2.0.dist-info/METADATA,sha256=7Y4QEm70NSr05p7XDQJlLI_QSaw1md9vOHCYt9QHgoA,6944
|
|
6
|
+
mcal_ai_autogen-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
mcal_ai_autogen-0.2.0.dist-info/top_level.txt,sha256=VEv2d_KPDVrYHd9_NIyrcdqusUwOA63aJIMUF0gqvQw,13
|
|
8
|
+
mcal_ai_autogen-0.2.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
mcal_autogen
|
mcal_autogen/__init__.py
ADDED
mcal_autogen/_compat.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compatibility layer for AutoGen imports.
|
|
3
|
+
|
|
4
|
+
Provides graceful handling when autogen packages are not installed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
# Check if AutoGen is available
|
|
10
|
+
AUTOGEN_AVAILABLE = False
|
|
11
|
+
AUTOGEN_CORE_AVAILABLE = False
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from autogen_core import CancellationToken
|
|
15
|
+
from autogen_core.memory import (
|
|
16
|
+
Memory,
|
|
17
|
+
MemoryContent,
|
|
18
|
+
MemoryQueryResult,
|
|
19
|
+
UpdateContextResult,
|
|
20
|
+
MemoryMimeType,
|
|
21
|
+
)
|
|
22
|
+
from autogen_core.model_context import ChatCompletionContext
|
|
23
|
+
from autogen_core.models import SystemMessage
|
|
24
|
+
AUTOGEN_CORE_AVAILABLE = True
|
|
25
|
+
AUTOGEN_AVAILABLE = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
# Define minimal stubs for type checking
|
|
28
|
+
Memory = object # type: ignore
|
|
29
|
+
MemoryContent = Any # type: ignore
|
|
30
|
+
MemoryQueryResult = Any # type: ignore
|
|
31
|
+
UpdateContextResult = Any # type: ignore
|
|
32
|
+
MemoryMimeType = Any # type: ignore
|
|
33
|
+
CancellationToken = Any # type: ignore
|
|
34
|
+
ChatCompletionContext = Any # type: ignore
|
|
35
|
+
SystemMessage = Any # type: ignore
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_autogen() -> None:
|
|
39
|
+
"""Raise ImportError if AutoGen is not available."""
|
|
40
|
+
if not AUTOGEN_AVAILABLE:
|
|
41
|
+
raise ImportError(
|
|
42
|
+
"AutoGen packages not found. Install with: pip install mcal-autogen[autogen]"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_autogen_core() -> None:
|
|
47
|
+
"""Raise ImportError if autogen-core is not available."""
|
|
48
|
+
if not AUTOGEN_CORE_AVAILABLE:
|
|
49
|
+
raise ImportError(
|
|
50
|
+
"autogen-core not found. Install with: pip install autogen-core>=0.4.0"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"AUTOGEN_AVAILABLE",
|
|
56
|
+
"AUTOGEN_CORE_AVAILABLE",
|
|
57
|
+
"check_autogen",
|
|
58
|
+
"check_autogen_core",
|
|
59
|
+
"Memory",
|
|
60
|
+
"MemoryContent",
|
|
61
|
+
"MemoryQueryResult",
|
|
62
|
+
"UpdateContextResult",
|
|
63
|
+
"MemoryMimeType",
|
|
64
|
+
"CancellationToken",
|
|
65
|
+
"ChatCompletionContext",
|
|
66
|
+
"SystemMessage",
|
|
67
|
+
]
|
mcal_autogen/memory.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCAL Memory Backend for Microsoft AutoGen
|
|
3
|
+
|
|
4
|
+
Implements AutoGen's Memory interface with goal-aware memory capabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from mcal_autogen._compat import (
|
|
15
|
+
AUTOGEN_CORE_AVAILABLE,
|
|
16
|
+
check_autogen_core,
|
|
17
|
+
Memory,
|
|
18
|
+
MemoryContent,
|
|
19
|
+
MemoryQueryResult,
|
|
20
|
+
UpdateContextResult,
|
|
21
|
+
MemoryMimeType,
|
|
22
|
+
CancellationToken,
|
|
23
|
+
ChatCompletionContext,
|
|
24
|
+
SystemMessage,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from mcal import MCAL
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MCALMemory(Memory if AUTOGEN_CORE_AVAILABLE else object):
|
|
32
|
+
"""
|
|
33
|
+
MCAL memory backend for Microsoft AutoGen.
|
|
34
|
+
|
|
35
|
+
This class implements AutoGen's Memory interface, providing
|
|
36
|
+
goal-aware memory with context preservation for multi-agent systems.
|
|
37
|
+
|
|
38
|
+
Key Features:
|
|
39
|
+
- Goal-aware search that prioritizes project-relevant memories
|
|
40
|
+
- Decision tracking for architectural choices
|
|
41
|
+
- User isolation for multi-tenant scenarios
|
|
42
|
+
- TTL support with automatic expiration
|
|
43
|
+
- Thread-safe operations
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
from mcal import MCAL
|
|
47
|
+
from mcal_autogen import MCALMemory
|
|
48
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
49
|
+
|
|
50
|
+
mcal = MCAL(goal="Build fraud detection system")
|
|
51
|
+
memory = MCALMemory(mcal, user_id="user_123")
|
|
52
|
+
|
|
53
|
+
agent = AssistantAgent(
|
|
54
|
+
name="analyst",
|
|
55
|
+
model_client=model_client,
|
|
56
|
+
memory=[memory],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
mcal: Initialized MCAL instance
|
|
61
|
+
user_id: User identifier for memory isolation
|
|
62
|
+
name: Memory instance name (default: "mcal_memory")
|
|
63
|
+
max_results: Maximum results to return from queries (default: 10)
|
|
64
|
+
score_threshold: Minimum relevance score 0-1 (default: 0.0)
|
|
65
|
+
default_ttl_minutes: Default TTL in minutes (None = no expiration)
|
|
66
|
+
enable_goal_tracking: Whether to extract goals from content
|
|
67
|
+
include_decisions: Whether to include decisions in search results
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
mcal: "MCAL",
|
|
73
|
+
user_id: str = "default",
|
|
74
|
+
name: str = "mcal_memory",
|
|
75
|
+
max_results: int = 10,
|
|
76
|
+
score_threshold: float = 0.0,
|
|
77
|
+
default_ttl_minutes: Optional[float] = None,
|
|
78
|
+
enable_goal_tracking: bool = True,
|
|
79
|
+
include_decisions: bool = True,
|
|
80
|
+
):
|
|
81
|
+
check_autogen_core()
|
|
82
|
+
|
|
83
|
+
self._mcal = mcal
|
|
84
|
+
self._user_id = user_id
|
|
85
|
+
self._name = name
|
|
86
|
+
self._max_results = max_results
|
|
87
|
+
self._score_threshold = score_threshold
|
|
88
|
+
self._default_ttl_minutes = default_ttl_minutes
|
|
89
|
+
self._enable_goal_tracking = enable_goal_tracking
|
|
90
|
+
self._include_decisions = include_decisions
|
|
91
|
+
|
|
92
|
+
# Thread safety
|
|
93
|
+
self._lock = threading.RLock()
|
|
94
|
+
|
|
95
|
+
# In-memory storage for MemoryContent items
|
|
96
|
+
# Maps content_id -> MemoryContent
|
|
97
|
+
self._items: Dict[str, MemoryContent] = {}
|
|
98
|
+
self._created_at: Dict[str, datetime] = {}
|
|
99
|
+
self._expires_at: Dict[str, Optional[float]] = {} # Unix timestamp
|
|
100
|
+
|
|
101
|
+
# Counter for generating unique IDs
|
|
102
|
+
self._id_counter = 0
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def name(self) -> str:
|
|
106
|
+
"""Get the memory instance identifier."""
|
|
107
|
+
return self._name
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def user_id(self) -> str:
|
|
111
|
+
"""Get the user identifier."""
|
|
112
|
+
return self._user_id
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def mcal(self) -> "MCAL":
|
|
116
|
+
"""Get the underlying MCAL instance."""
|
|
117
|
+
return self._mcal
|
|
118
|
+
|
|
119
|
+
def _generate_id(self) -> str:
|
|
120
|
+
"""Generate a unique content ID."""
|
|
121
|
+
with self._lock:
|
|
122
|
+
self._id_counter += 1
|
|
123
|
+
return f"mem_{self._user_id}_{self._id_counter}_{int(time.time() * 1000)}"
|
|
124
|
+
|
|
125
|
+
def _is_expired(self, content_id: str) -> bool:
|
|
126
|
+
"""Check if a memory entry has expired."""
|
|
127
|
+
expires = self._expires_at.get(content_id)
|
|
128
|
+
if expires is None:
|
|
129
|
+
return False
|
|
130
|
+
return time.time() > expires
|
|
131
|
+
|
|
132
|
+
def _cleanup_expired(self) -> int:
|
|
133
|
+
"""Remove expired entries. Returns count of removed items."""
|
|
134
|
+
removed = 0
|
|
135
|
+
with self._lock:
|
|
136
|
+
expired_ids = [
|
|
137
|
+
cid for cid in list(self._items.keys())
|
|
138
|
+
if self._is_expired(cid)
|
|
139
|
+
]
|
|
140
|
+
for cid in expired_ids:
|
|
141
|
+
del self._items[cid]
|
|
142
|
+
self._created_at.pop(cid, None)
|
|
143
|
+
self._expires_at.pop(cid, None)
|
|
144
|
+
removed += 1
|
|
145
|
+
return removed
|
|
146
|
+
|
|
147
|
+
def _extract_text(self, content: MemoryContent | str) -> str:
|
|
148
|
+
"""Extract searchable text from content."""
|
|
149
|
+
if isinstance(content, str):
|
|
150
|
+
return content
|
|
151
|
+
|
|
152
|
+
if hasattr(content, 'content'):
|
|
153
|
+
val = content.content
|
|
154
|
+
if isinstance(val, str):
|
|
155
|
+
return val
|
|
156
|
+
elif isinstance(val, dict):
|
|
157
|
+
# Try common keys
|
|
158
|
+
for key in ['text', 'content', 'message', 'data']:
|
|
159
|
+
if key in val:
|
|
160
|
+
return str(val[key])
|
|
161
|
+
# Fallback to JSON-like string
|
|
162
|
+
import json
|
|
163
|
+
try:
|
|
164
|
+
return json.dumps(val)
|
|
165
|
+
except (TypeError, ValueError):
|
|
166
|
+
return str(val)
|
|
167
|
+
elif isinstance(val, bytes):
|
|
168
|
+
try:
|
|
169
|
+
return val.decode('utf-8')
|
|
170
|
+
except UnicodeDecodeError:
|
|
171
|
+
return str(val)
|
|
172
|
+
else:
|
|
173
|
+
return str(val)
|
|
174
|
+
|
|
175
|
+
return str(content)
|
|
176
|
+
|
|
177
|
+
async def add(
|
|
178
|
+
self,
|
|
179
|
+
content: MemoryContent,
|
|
180
|
+
cancellation_token: Optional[CancellationToken] = None,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Add new content to memory.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
content: Memory content to store
|
|
187
|
+
cancellation_token: Optional token to cancel operation
|
|
188
|
+
"""
|
|
189
|
+
# Check cancellation
|
|
190
|
+
if cancellation_token and hasattr(cancellation_token, 'cancelled'):
|
|
191
|
+
if getattr(cancellation_token, 'cancelled', False):
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
content_id = self._generate_id()
|
|
195
|
+
now = datetime.now(timezone.utc)
|
|
196
|
+
|
|
197
|
+
# Calculate expiration
|
|
198
|
+
expires_at: Optional[float] = None
|
|
199
|
+
ttl_minutes = self._default_ttl_minutes
|
|
200
|
+
|
|
201
|
+
# Check for TTL in metadata
|
|
202
|
+
if content.metadata and 'ttl_minutes' in content.metadata:
|
|
203
|
+
ttl_minutes = content.metadata['ttl_minutes']
|
|
204
|
+
|
|
205
|
+
if ttl_minutes is not None:
|
|
206
|
+
expires_at = time.time() + (ttl_minutes * 60)
|
|
207
|
+
|
|
208
|
+
# Store the content
|
|
209
|
+
with self._lock:
|
|
210
|
+
self._items[content_id] = content
|
|
211
|
+
self._created_at[content_id] = now
|
|
212
|
+
self._expires_at[content_id] = expires_at
|
|
213
|
+
|
|
214
|
+
# Also store in MCAL for goal-aware search
|
|
215
|
+
text = self._extract_text(content)
|
|
216
|
+
if text:
|
|
217
|
+
metadata = dict(content.metadata) if content.metadata else {}
|
|
218
|
+
metadata['content_id'] = content_id
|
|
219
|
+
metadata['user_id'] = self._user_id
|
|
220
|
+
metadata['source'] = 'autogen_memory'
|
|
221
|
+
|
|
222
|
+
# Add to MCAL using add_memory or similar method
|
|
223
|
+
try:
|
|
224
|
+
# Try different MCAL methods
|
|
225
|
+
if hasattr(self._mcal, 'add_memory'):
|
|
226
|
+
self._mcal.add_memory(
|
|
227
|
+
text=text,
|
|
228
|
+
metadata=metadata,
|
|
229
|
+
user_id=self._user_id,
|
|
230
|
+
)
|
|
231
|
+
elif hasattr(self._mcal, 'store'):
|
|
232
|
+
self._mcal.store(
|
|
233
|
+
content=text,
|
|
234
|
+
metadata=metadata,
|
|
235
|
+
)
|
|
236
|
+
elif hasattr(self._mcal, 'add'):
|
|
237
|
+
self._mcal.add(text, metadata=metadata)
|
|
238
|
+
except Exception:
|
|
239
|
+
# Silently continue if MCAL storage fails
|
|
240
|
+
# The in-memory storage still works
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
async def query(
|
|
244
|
+
self,
|
|
245
|
+
query: str | MemoryContent,
|
|
246
|
+
cancellation_token: Optional[CancellationToken] = None,
|
|
247
|
+
**kwargs: Any,
|
|
248
|
+
) -> MemoryQueryResult:
|
|
249
|
+
"""
|
|
250
|
+
Query memory for relevant content.
|
|
251
|
+
|
|
252
|
+
Uses MCAL's goal-aware search to find the most relevant memories
|
|
253
|
+
based on the current project goal and query.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
query: Query string or MemoryContent
|
|
257
|
+
cancellation_token: Optional token to cancel operation
|
|
258
|
+
**kwargs: Additional query parameters
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
MemoryQueryResult containing matching memories
|
|
262
|
+
"""
|
|
263
|
+
# Check cancellation
|
|
264
|
+
if cancellation_token and hasattr(cancellation_token, 'cancelled'):
|
|
265
|
+
if getattr(cancellation_token, 'cancelled', False):
|
|
266
|
+
return MemoryQueryResult(results=[])
|
|
267
|
+
|
|
268
|
+
# Cleanup expired entries
|
|
269
|
+
self._cleanup_expired()
|
|
270
|
+
|
|
271
|
+
query_text = self._extract_text(query)
|
|
272
|
+
if not query_text:
|
|
273
|
+
return MemoryQueryResult(results=[])
|
|
274
|
+
|
|
275
|
+
# Get search parameters
|
|
276
|
+
max_results = kwargs.get('max_results', self._max_results)
|
|
277
|
+
score_threshold = kwargs.get('score_threshold', self._score_threshold)
|
|
278
|
+
|
|
279
|
+
results: List[MemoryContent] = []
|
|
280
|
+
|
|
281
|
+
# Try MCAL search first for goal-aware results
|
|
282
|
+
try:
|
|
283
|
+
mcal_results = []
|
|
284
|
+
if hasattr(self._mcal, 'search'):
|
|
285
|
+
mcal_results = self._mcal.search(
|
|
286
|
+
query=query_text,
|
|
287
|
+
user_id=self._user_id,
|
|
288
|
+
top_k=max_results,
|
|
289
|
+
include_decisions=self._include_decisions,
|
|
290
|
+
)
|
|
291
|
+
elif hasattr(self._mcal, 'query'):
|
|
292
|
+
mcal_results = self._mcal.query(
|
|
293
|
+
query_text,
|
|
294
|
+
top_k=max_results,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Convert MCAL results to MemoryContent
|
|
298
|
+
for item in mcal_results[:max_results]:
|
|
299
|
+
score = 0.0
|
|
300
|
+
text = ""
|
|
301
|
+
metadata: Dict[str, Any] = {}
|
|
302
|
+
|
|
303
|
+
if isinstance(item, dict):
|
|
304
|
+
text = item.get('content', item.get('text', str(item)))
|
|
305
|
+
score = item.get('score', item.get('relevance', 0.0))
|
|
306
|
+
metadata = item.get('metadata', {})
|
|
307
|
+
elif hasattr(item, 'content'):
|
|
308
|
+
text = str(item.content)
|
|
309
|
+
score = getattr(item, 'score', 0.0)
|
|
310
|
+
metadata = getattr(item, 'metadata', {})
|
|
311
|
+
else:
|
|
312
|
+
text = str(item)
|
|
313
|
+
|
|
314
|
+
# Apply score threshold
|
|
315
|
+
if score >= score_threshold:
|
|
316
|
+
metadata['score'] = score
|
|
317
|
+
results.append(MemoryContent(
|
|
318
|
+
content=text,
|
|
319
|
+
mime_type=MemoryMimeType.TEXT,
|
|
320
|
+
metadata=metadata,
|
|
321
|
+
))
|
|
322
|
+
except Exception:
|
|
323
|
+
# Fallback to in-memory search if MCAL fails
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
# If no MCAL results, fall back to simple in-memory search
|
|
327
|
+
if not results:
|
|
328
|
+
query_lower = query_text.lower()
|
|
329
|
+
with self._lock:
|
|
330
|
+
for content_id, content in self._items.items():
|
|
331
|
+
if self._is_expired(content_id):
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
text = self._extract_text(content)
|
|
335
|
+
# Simple text matching
|
|
336
|
+
if query_lower in text.lower():
|
|
337
|
+
metadata = dict(content.metadata) if content.metadata else {}
|
|
338
|
+
metadata['score'] = 0.5 # Default score for text match
|
|
339
|
+
results.append(MemoryContent(
|
|
340
|
+
content=text,
|
|
341
|
+
mime_type=content.mime_type,
|
|
342
|
+
metadata=metadata,
|
|
343
|
+
))
|
|
344
|
+
|
|
345
|
+
if len(results) >= max_results:
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
return MemoryQueryResult(results=results[:max_results])
|
|
349
|
+
|
|
350
|
+
async def update_context(
|
|
351
|
+
self,
|
|
352
|
+
model_context: ChatCompletionContext,
|
|
353
|
+
) -> UpdateContextResult:
|
|
354
|
+
"""
|
|
355
|
+
Update the model context with relevant memories.
|
|
356
|
+
|
|
357
|
+
This method retrieves the conversation history, uses the last
|
|
358
|
+
message as a query to find relevant memories, and adds them
|
|
359
|
+
to the context as a system message.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
model_context: The model context to update
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
UpdateContextResult containing memories added to context
|
|
366
|
+
"""
|
|
367
|
+
# Get messages from context
|
|
368
|
+
messages = await model_context.get_messages()
|
|
369
|
+
if not messages:
|
|
370
|
+
return UpdateContextResult(memories=MemoryQueryResult(results=[]))
|
|
371
|
+
|
|
372
|
+
# Use the last message as query
|
|
373
|
+
last_message = messages[-1]
|
|
374
|
+
query_text = (
|
|
375
|
+
last_message.content
|
|
376
|
+
if isinstance(last_message.content, str)
|
|
377
|
+
else str(last_message)
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Query memory
|
|
381
|
+
query_results = await self.query(query_text)
|
|
382
|
+
|
|
383
|
+
# Add relevant memories to context
|
|
384
|
+
if query_results.results:
|
|
385
|
+
memory_strings = [
|
|
386
|
+
f"{i}. {self._extract_text(memory)}"
|
|
387
|
+
for i, memory in enumerate(query_results.results, 1)
|
|
388
|
+
]
|
|
389
|
+
memory_context = (
|
|
390
|
+
"\nRelevant context from memory:\n" +
|
|
391
|
+
"\n".join(memory_strings)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Add as system message
|
|
395
|
+
await model_context.add_message(
|
|
396
|
+
SystemMessage(content=memory_context)
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
return UpdateContextResult(memories=query_results)
|
|
400
|
+
|
|
401
|
+
async def clear(self) -> None:
|
|
402
|
+
"""Clear all memory entries for this user."""
|
|
403
|
+
with self._lock:
|
|
404
|
+
# Filter to only clear this user's entries
|
|
405
|
+
user_prefix = f"mem_{self._user_id}_"
|
|
406
|
+
to_remove = [
|
|
407
|
+
cid for cid in self._items.keys()
|
|
408
|
+
if cid.startswith(user_prefix) or self._user_id == "default"
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
for cid in to_remove:
|
|
412
|
+
self._items.pop(cid, None)
|
|
413
|
+
self._created_at.pop(cid, None)
|
|
414
|
+
self._expires_at.pop(cid, None)
|
|
415
|
+
|
|
416
|
+
# Also clear from MCAL if possible
|
|
417
|
+
try:
|
|
418
|
+
if hasattr(self._mcal, 'clear_user'):
|
|
419
|
+
self._mcal.clear_user(self._user_id)
|
|
420
|
+
elif hasattr(self._mcal, 'reset'):
|
|
421
|
+
# Only reset if default user (affects all)
|
|
422
|
+
if self._user_id == "default":
|
|
423
|
+
self._mcal.reset()
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
async def close(self) -> None:
|
|
428
|
+
"""Clean up resources."""
|
|
429
|
+
# Clear in-memory data
|
|
430
|
+
with self._lock:
|
|
431
|
+
self._items.clear()
|
|
432
|
+
self._created_at.clear()
|
|
433
|
+
self._expires_at.clear()
|
|
434
|
+
|
|
435
|
+
# -------------------------------------------------------------------------
|
|
436
|
+
# Additional helper methods
|
|
437
|
+
# -------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
def get_item_count(self) -> int:
|
|
440
|
+
"""Get the number of non-expired items in memory."""
|
|
441
|
+
self._cleanup_expired()
|
|
442
|
+
with self._lock:
|
|
443
|
+
return len(self._items)
|
|
444
|
+
|
|
445
|
+
def get_all_items(self) -> List[MemoryContent]:
|
|
446
|
+
"""Get all non-expired items (for debugging)."""
|
|
447
|
+
self._cleanup_expired()
|
|
448
|
+
with self._lock:
|
|
449
|
+
return list(self._items.values())
|
|
450
|
+
|
|
451
|
+
async def add_text(
|
|
452
|
+
self,
|
|
453
|
+
text: str,
|
|
454
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
455
|
+
) -> None:
|
|
456
|
+
"""
|
|
457
|
+
Convenience method to add plain text to memory.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
text: Text content to add
|
|
461
|
+
metadata: Optional metadata
|
|
462
|
+
"""
|
|
463
|
+
content = MemoryContent(
|
|
464
|
+
content=text,
|
|
465
|
+
mime_type=MemoryMimeType.TEXT,
|
|
466
|
+
metadata=metadata,
|
|
467
|
+
)
|
|
468
|
+
await self.add(content)
|
|
469
|
+
|
|
470
|
+
async def query_text(
|
|
471
|
+
self,
|
|
472
|
+
query: str,
|
|
473
|
+
max_results: Optional[int] = None,
|
|
474
|
+
) -> List[str]:
|
|
475
|
+
"""
|
|
476
|
+
Convenience method to query and return plain text results.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
query: Search query
|
|
480
|
+
max_results: Optional max results override
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
List of text strings from matching memories
|
|
484
|
+
"""
|
|
485
|
+
kwargs = {}
|
|
486
|
+
if max_results is not None:
|
|
487
|
+
kwargs['max_results'] = max_results
|
|
488
|
+
|
|
489
|
+
results = await self.query(query, **kwargs)
|
|
490
|
+
return [self._extract_text(r) for r in results.results]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
__all__ = ["MCALMemory"]
|