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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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
@@ -0,0 +1,10 @@
1
+ """
2
+ MCAL AutoGen Integration
3
+
4
+ Goal-aware memory for Microsoft AutoGen multi-agent systems.
5
+ """
6
+
7
+ from mcal_autogen.memory import MCALMemory
8
+
9
+ __version__ = "0.1.0"
10
+ __all__ = ["MCALMemory"]
@@ -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"]