youclaw 4.6.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.
- youclaw/__init__.py +24 -0
- youclaw/bot.py +185 -0
- youclaw/cli.py +469 -0
- youclaw/commands.py +151 -0
- youclaw/config.py +170 -0
- youclaw/core_skills.py +210 -0
- youclaw/dashboard.py +1347 -0
- youclaw/discord_handler.py +187 -0
- youclaw/env_manager.py +61 -0
- youclaw/main.py +273 -0
- youclaw/memory_manager.py +440 -0
- youclaw/ollama_client.py +486 -0
- youclaw/personality_manager.py +42 -0
- youclaw/scheduler_manager.py +226 -0
- youclaw/search_client.py +66 -0
- youclaw/skills_manager.py +127 -0
- youclaw/telegram_handler.py +181 -0
- youclaw/vector_manager.py +94 -0
- youclaw-4.6.0.dist-info/LICENSE +21 -0
- youclaw-4.6.0.dist-info/METADATA +128 -0
- youclaw-4.6.0.dist-info/RECORD +24 -0
- youclaw-4.6.0.dist-info/WHEEL +5 -0
- youclaw-4.6.0.dist-info/entry_points.txt +2 -0
- youclaw-4.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YouClaw Memory Manager
|
|
3
|
+
Persistent conversation memory and context management using SQLite.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import aiosqlite
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import json
|
|
10
|
+
from typing import List, Dict, Optional
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from .config import config
|
|
13
|
+
from .vector_manager import VectorManager
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MemoryManager:
|
|
19
|
+
"""Manages persistent conversation memory across platforms"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.db_path = config.bot.database_path
|
|
23
|
+
self.max_context = config.bot.max_context_messages
|
|
24
|
+
self.db: Optional[aiosqlite.Connection] = None
|
|
25
|
+
self.vector_manager = VectorManager(self.db_path)
|
|
26
|
+
|
|
27
|
+
async def initialize(self):
|
|
28
|
+
"""Initialize the database and create tables"""
|
|
29
|
+
self.db = await aiosqlite.connect(self.db_path)
|
|
30
|
+
await self.vector_manager.initialize()
|
|
31
|
+
|
|
32
|
+
# Create conversations table
|
|
33
|
+
await self.db.execute("""
|
|
34
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
platform TEXT NOT NULL,
|
|
37
|
+
user_id TEXT NOT NULL,
|
|
38
|
+
channel_id TEXT,
|
|
39
|
+
role TEXT NOT NULL,
|
|
40
|
+
content TEXT NOT NULL,
|
|
41
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
42
|
+
metadata TEXT
|
|
43
|
+
)
|
|
44
|
+
""")
|
|
45
|
+
|
|
46
|
+
# Create user profile table
|
|
47
|
+
await self.db.execute("""
|
|
48
|
+
CREATE TABLE IF NOT EXISTS user_profiles (
|
|
49
|
+
platform TEXT NOT NULL,
|
|
50
|
+
user_id TEXT NOT NULL,
|
|
51
|
+
name TEXT,
|
|
52
|
+
interests TEXT,
|
|
53
|
+
onboarding_completed INTEGER DEFAULT 0,
|
|
54
|
+
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
55
|
+
PRIMARY KEY (platform, user_id)
|
|
56
|
+
)
|
|
57
|
+
""")
|
|
58
|
+
|
|
59
|
+
# Create user preferences table
|
|
60
|
+
await self.db.execute("""
|
|
61
|
+
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
62
|
+
platform TEXT NOT NULL,
|
|
63
|
+
user_id TEXT NOT NULL,
|
|
64
|
+
preference_key TEXT NOT NULL,
|
|
65
|
+
preference_value TEXT,
|
|
66
|
+
PRIMARY KEY (platform, user_id, preference_key)
|
|
67
|
+
)
|
|
68
|
+
""")
|
|
69
|
+
|
|
70
|
+
# Create global settings table
|
|
71
|
+
await self.db.execute("""
|
|
72
|
+
CREATE TABLE IF NOT EXISTS global_settings (
|
|
73
|
+
setting_key TEXT PRIMARY KEY,
|
|
74
|
+
setting_value TEXT,
|
|
75
|
+
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
76
|
+
)
|
|
77
|
+
""")
|
|
78
|
+
|
|
79
|
+
# Create users table for dashboard auth
|
|
80
|
+
await self.db.execute("""
|
|
81
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
username TEXT UNIQUE NOT NULL,
|
|
84
|
+
password_hash TEXT NOT NULL,
|
|
85
|
+
role TEXT DEFAULT 'user',
|
|
86
|
+
linked_platform TEXT,
|
|
87
|
+
linked_user_id TEXT,
|
|
88
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
89
|
+
)
|
|
90
|
+
""")
|
|
91
|
+
|
|
92
|
+
# Create user secrets table (for individual tokens/keys)
|
|
93
|
+
await self.db.execute("""
|
|
94
|
+
CREATE TABLE IF NOT EXISTS user_secrets (
|
|
95
|
+
platform TEXT NOT NULL,
|
|
96
|
+
user_id TEXT NOT NULL,
|
|
97
|
+
secret_key TEXT NOT NULL,
|
|
98
|
+
secret_value TEXT,
|
|
99
|
+
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
100
|
+
PRIMARY KEY (platform, user_id, secret_key)
|
|
101
|
+
)
|
|
102
|
+
""")
|
|
103
|
+
|
|
104
|
+
# Initialize default settings if they don't exist
|
|
105
|
+
defaults = [
|
|
106
|
+
('search_enabled', 'true'),
|
|
107
|
+
('personality_enabled', 'true'),
|
|
108
|
+
('onboarding_enabled', 'true'),
|
|
109
|
+
('discord_enabled', 'false'),
|
|
110
|
+
('telegram_enabled', 'true'),
|
|
111
|
+
('discord_token', ''),
|
|
112
|
+
('telegram_token', '')
|
|
113
|
+
]
|
|
114
|
+
for key, val in defaults:
|
|
115
|
+
await self.db.execute("INSERT OR IGNORE INTO global_settings (setting_key, setting_value) VALUES (?, ?)", (key, val))
|
|
116
|
+
|
|
117
|
+
# Create indexes for faster queries
|
|
118
|
+
await self.db.execute("""
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_user
|
|
120
|
+
ON conversations(platform, user_id, timestamp DESC)
|
|
121
|
+
""")
|
|
122
|
+
|
|
123
|
+
await self.db.commit()
|
|
124
|
+
logger.info(f"Memory manager initialized: {self.db_path}")
|
|
125
|
+
|
|
126
|
+
def _hash_password(self, password: str) -> str:
|
|
127
|
+
"""Securely hash a password for storage"""
|
|
128
|
+
import hashlib
|
|
129
|
+
return hashlib.sha256(password.encode()).hexdigest()
|
|
130
|
+
|
|
131
|
+
async def create_user(self, username: str, password: str, role: str = 'admin') -> bool:
|
|
132
|
+
"""Create a new dashboard user. Everyone is admin in this personal version."""
|
|
133
|
+
try:
|
|
134
|
+
pw_hash = self._hash_password(password)
|
|
135
|
+
await self.db.execute(
|
|
136
|
+
"INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)",
|
|
137
|
+
(username, pw_hash, role)
|
|
138
|
+
)
|
|
139
|
+
await self.db.commit()
|
|
140
|
+
return True
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Failed to create user: {e}")
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
async def verify_user(self, username: str, password: str) -> Optional[Dict]:
|
|
146
|
+
"""Verify user credentials and return user info with token"""
|
|
147
|
+
pw_hash = self._hash_password(password)
|
|
148
|
+
async with self.db.execute(
|
|
149
|
+
"SELECT id, username, role, linked_platform, linked_user_id FROM users WHERE username = ? AND password_hash = ?",
|
|
150
|
+
(username, pw_hash)
|
|
151
|
+
) as cursor:
|
|
152
|
+
row = await cursor.fetchone()
|
|
153
|
+
if row:
|
|
154
|
+
# Generate a session token based on credentials and a deployment-specific secret
|
|
155
|
+
import hashlib
|
|
156
|
+
token_base = f"{username}{pw_hash}{config.bot.prefix}"
|
|
157
|
+
token = hashlib.sha256(token_base.encode()).hexdigest()
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"id": row[0],
|
|
161
|
+
"username": row[1],
|
|
162
|
+
"role": row[2],
|
|
163
|
+
"linked_platform": row[3],
|
|
164
|
+
"linked_user_id": row[4],
|
|
165
|
+
"token": token
|
|
166
|
+
}
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
async def link_account(self, username: str, platform: str, user_id: str):
|
|
170
|
+
"""Link a dashboard user to a Telegram/Discord identity"""
|
|
171
|
+
await self.db.execute(
|
|
172
|
+
"UPDATE users SET linked_platform = ?, linked_user_id = ? WHERE username = ?",
|
|
173
|
+
(platform, user_id, username)
|
|
174
|
+
)
|
|
175
|
+
await self.db.commit()
|
|
176
|
+
logger.info(f"Linked dashboard user {username} to {platform}:{user_id}")
|
|
177
|
+
|
|
178
|
+
async def get_linked_identity(self, username: str) -> Optional[tuple]:
|
|
179
|
+
"""Get the platform:id linked to a username"""
|
|
180
|
+
async with self.db.execute(
|
|
181
|
+
"SELECT linked_platform, linked_user_id FROM users WHERE username = ?",
|
|
182
|
+
(username,)
|
|
183
|
+
) as cursor:
|
|
184
|
+
row = await cursor.fetchone()
|
|
185
|
+
if row and row[0] and row[1]:
|
|
186
|
+
return row[0], row[1]
|
|
187
|
+
return None, None
|
|
188
|
+
|
|
189
|
+
async def close(self):
|
|
190
|
+
"""Close the database connection"""
|
|
191
|
+
if self.db:
|
|
192
|
+
await self.db.close()
|
|
193
|
+
logger.info("Memory manager closed")
|
|
194
|
+
|
|
195
|
+
async def add_message(
|
|
196
|
+
self,
|
|
197
|
+
platform: str,
|
|
198
|
+
user_id: str,
|
|
199
|
+
role: str,
|
|
200
|
+
content: str,
|
|
201
|
+
channel_id: Optional[str] = None,
|
|
202
|
+
metadata: Optional[Dict] = None
|
|
203
|
+
):
|
|
204
|
+
"""
|
|
205
|
+
Add a message to conversation history.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
platform: Platform name (discord, telegram)
|
|
209
|
+
user_id: User identifier
|
|
210
|
+
role: Message role (user, assistant, system)
|
|
211
|
+
content: Message content
|
|
212
|
+
channel_id: Optional channel/chat identifier
|
|
213
|
+
metadata: Optional metadata dict
|
|
214
|
+
"""
|
|
215
|
+
metadata_json = json.dumps(metadata) if metadata else None
|
|
216
|
+
|
|
217
|
+
await self.db.execute("""
|
|
218
|
+
INSERT INTO conversations
|
|
219
|
+
(platform, user_id, channel_id, role, content, metadata)
|
|
220
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
221
|
+
""", (platform, user_id, channel_id, role, content, metadata_json))
|
|
222
|
+
|
|
223
|
+
await self.db.commit()
|
|
224
|
+
|
|
225
|
+
# Phase 1: Semantic Indexing
|
|
226
|
+
try:
|
|
227
|
+
# We need the last inserted ID
|
|
228
|
+
async with self.db.execute("SELECT last_insert_rowid()") as cursor:
|
|
229
|
+
message_id = (await cursor.fetchone())[0]
|
|
230
|
+
# Trigger embedding in background to not block the chat response
|
|
231
|
+
asyncio.create_task(self.vector_manager.save_embedding(message_id, content))
|
|
232
|
+
except Exception as ve:
|
|
233
|
+
logger.error(f"Failed to trigger semantic indexing: {ve}")
|
|
234
|
+
|
|
235
|
+
async def get_conversation_history(
|
|
236
|
+
self,
|
|
237
|
+
platform: str,
|
|
238
|
+
user_id: str,
|
|
239
|
+
channel_id: Optional[str] = None,
|
|
240
|
+
limit: Optional[int] = None
|
|
241
|
+
) -> List[Dict[str, str]]:
|
|
242
|
+
"""
|
|
243
|
+
Get conversation history for a user.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
platform: Platform name
|
|
247
|
+
user_id: User identifier
|
|
248
|
+
channel_id: Optional channel filter
|
|
249
|
+
limit: Max number of messages (defaults to max_context)
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of message dicts with 'role' and 'content'
|
|
253
|
+
"""
|
|
254
|
+
limit = limit or self.max_context
|
|
255
|
+
|
|
256
|
+
query = """
|
|
257
|
+
SELECT role, content FROM conversations
|
|
258
|
+
WHERE platform = ? AND user_id = ?
|
|
259
|
+
"""
|
|
260
|
+
params = [platform, user_id]
|
|
261
|
+
|
|
262
|
+
if channel_id:
|
|
263
|
+
query += " AND channel_id = ?"
|
|
264
|
+
params.append(channel_id)
|
|
265
|
+
|
|
266
|
+
query += " ORDER BY timestamp DESC LIMIT ?"
|
|
267
|
+
params.append(limit)
|
|
268
|
+
|
|
269
|
+
async with self.db.execute(query, params) as cursor:
|
|
270
|
+
rows = await cursor.fetchall()
|
|
271
|
+
|
|
272
|
+
# Reverse to get chronological order
|
|
273
|
+
messages = [
|
|
274
|
+
{"role": row[0], "content": row[1]}
|
|
275
|
+
for row in reversed(rows)
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
return messages
|
|
279
|
+
|
|
280
|
+
async def clear_conversation(
|
|
281
|
+
self,
|
|
282
|
+
platform: str,
|
|
283
|
+
user_id: str,
|
|
284
|
+
channel_id: Optional[str] = None
|
|
285
|
+
):
|
|
286
|
+
"""Clear conversation history for a user"""
|
|
287
|
+
query = "DELETE FROM conversations WHERE platform = ? AND user_id = ?"
|
|
288
|
+
params = [platform, user_id]
|
|
289
|
+
|
|
290
|
+
if channel_id:
|
|
291
|
+
query += " AND channel_id = ?"
|
|
292
|
+
params.append(channel_id)
|
|
293
|
+
|
|
294
|
+
await self.db.execute(query, params)
|
|
295
|
+
await self.db.commit()
|
|
296
|
+
logger.info(f"Cleared conversation for {platform}:{user_id}")
|
|
297
|
+
|
|
298
|
+
async def set_user_preference(
|
|
299
|
+
self,
|
|
300
|
+
platform: str,
|
|
301
|
+
user_id: str,
|
|
302
|
+
key: str,
|
|
303
|
+
value: str
|
|
304
|
+
):
|
|
305
|
+
"""Set a user preference"""
|
|
306
|
+
await self.db.execute("""
|
|
307
|
+
INSERT OR REPLACE INTO user_preferences
|
|
308
|
+
(platform, user_id, preference_key, preference_value)
|
|
309
|
+
VALUES (?, ?, ?, ?)
|
|
310
|
+
""", (platform, user_id, key, value))
|
|
311
|
+
|
|
312
|
+
await self.db.commit()
|
|
313
|
+
|
|
314
|
+
async def get_user_preference(
|
|
315
|
+
self,
|
|
316
|
+
platform: str,
|
|
317
|
+
user_id: str,
|
|
318
|
+
key: str,
|
|
319
|
+
default: Optional[str] = None
|
|
320
|
+
) -> Optional[str]:
|
|
321
|
+
"""Get a user preference"""
|
|
322
|
+
async with self.db.execute("""
|
|
323
|
+
SELECT preference_value FROM user_preferences
|
|
324
|
+
WHERE platform = ? AND user_id = ? AND preference_key = ?
|
|
325
|
+
""", (platform, user_id, key)) as cursor:
|
|
326
|
+
row = await cursor.fetchone()
|
|
327
|
+
return row[0] if row else default
|
|
328
|
+
|
|
329
|
+
async def get_user_profile(self, platform: str, user_id: str) -> Dict:
|
|
330
|
+
"""Get user profile information"""
|
|
331
|
+
async with self.db.execute("""
|
|
332
|
+
SELECT name, interests, onboarding_completed FROM user_profiles
|
|
333
|
+
WHERE platform = ? AND user_id = ?
|
|
334
|
+
""", (platform, user_id)) as cursor:
|
|
335
|
+
row = await cursor.fetchone()
|
|
336
|
+
if row:
|
|
337
|
+
return {
|
|
338
|
+
"name": row[0],
|
|
339
|
+
"interests": row[1],
|
|
340
|
+
"onboarding_completed": bool(row[2])
|
|
341
|
+
}
|
|
342
|
+
return {"name": None, "interests": None, "onboarding_completed": False}
|
|
343
|
+
|
|
344
|
+
async def update_user_profile(self, platform: str, user_id: str, **kwargs):
|
|
345
|
+
"""Update user profile information"""
|
|
346
|
+
fields = []
|
|
347
|
+
values = []
|
|
348
|
+
for key, value in kwargs.items():
|
|
349
|
+
if key in ['name', 'interests', 'onboarding_completed']:
|
|
350
|
+
if key == 'onboarding_completed':
|
|
351
|
+
value = 1 if value else 0
|
|
352
|
+
fields.append(f"{key} = ?")
|
|
353
|
+
values.append(value)
|
|
354
|
+
|
|
355
|
+
if not fields:
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
values.extend([platform, user_id])
|
|
359
|
+
|
|
360
|
+
# Try to update first
|
|
361
|
+
query = f"UPDATE user_profiles SET {', '.join(fields)}, last_updated = CURRENT_TIMESTAMP WHERE platform = ? AND user_id = ?"
|
|
362
|
+
cursor = await self.db.execute(query, values)
|
|
363
|
+
|
|
364
|
+
if cursor.rowcount == 0:
|
|
365
|
+
# If no rows updated, insert new profile
|
|
366
|
+
insert_fields = ['platform', 'user_id'] + list(kwargs.keys())
|
|
367
|
+
insert_placeholders = ['?'] * len(insert_fields)
|
|
368
|
+
insert_values = [platform, user_id] + [1 if k == 'onboarding_completed' and v else v for k, v in kwargs.items()]
|
|
369
|
+
|
|
370
|
+
await self.db.execute(f"""
|
|
371
|
+
INSERT INTO user_profiles ({', '.join(insert_fields)})
|
|
372
|
+
VALUES ({', '.join(insert_placeholders)})
|
|
373
|
+
""", insert_values)
|
|
374
|
+
|
|
375
|
+
await self.db.commit()
|
|
376
|
+
|
|
377
|
+
async def get_global_setting(self, key: str, default: str = None) -> str:
|
|
378
|
+
"""Get a global setting"""
|
|
379
|
+
async with self.db.execute("SELECT setting_value FROM global_settings WHERE setting_key = ?", (key,)) as cursor:
|
|
380
|
+
row = await cursor.fetchone()
|
|
381
|
+
return row[0] if row else default
|
|
382
|
+
|
|
383
|
+
async def set_global_setting(self, key: str, value: str):
|
|
384
|
+
"""Set a global setting"""
|
|
385
|
+
await self.db.execute("""
|
|
386
|
+
INSERT OR REPLACE INTO global_settings (setting_key, setting_value, last_updated)
|
|
387
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
388
|
+
""", (key, str(value)))
|
|
389
|
+
await self.db.commit()
|
|
390
|
+
|
|
391
|
+
async def get_user_secret(self, platform: str, user_id: str, key: str, default: str = None) -> str:
|
|
392
|
+
"""Get a user-specific secret (e.g., personal API key)"""
|
|
393
|
+
async with self.db.execute("""
|
|
394
|
+
SELECT secret_value FROM user_secrets
|
|
395
|
+
WHERE platform = ? AND user_id = ? AND secret_key = ?
|
|
396
|
+
""", (platform, user_id, key)) as cursor:
|
|
397
|
+
row = await cursor.fetchone()
|
|
398
|
+
return row[0] if row else default
|
|
399
|
+
|
|
400
|
+
async def set_user_secret(self, platform: str, user_id: str, key: str, value: str):
|
|
401
|
+
"""Set a user-specific secret"""
|
|
402
|
+
await self.db.execute("""
|
|
403
|
+
INSERT OR REPLACE INTO user_secrets (platform, user_id, secret_key, secret_value, last_updated)
|
|
404
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
405
|
+
""", (platform, user_id, key, str(value)))
|
|
406
|
+
await self.db.commit()
|
|
407
|
+
|
|
408
|
+
async def get_stats(self) -> Dict:
|
|
409
|
+
"""Get database statistics"""
|
|
410
|
+
async with self.db.execute("""
|
|
411
|
+
SELECT COUNT(*) FROM conversations
|
|
412
|
+
""") as cursor:
|
|
413
|
+
total_messages = (await cursor.fetchone())[0]
|
|
414
|
+
|
|
415
|
+
async with self.db.execute("""
|
|
416
|
+
SELECT COUNT(DISTINCT user_id) FROM conversations
|
|
417
|
+
""") as cursor:
|
|
418
|
+
unique_users = (await cursor.fetchone())[0]
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
"total_messages": total_messages,
|
|
422
|
+
"unique_users": unique_users,
|
|
423
|
+
"database_path": self.db_path
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async def get_semantic_context(self, query: str, limit: int = 5) -> str:
|
|
427
|
+
"""Get semantic context as a formatted string for LLM"""
|
|
428
|
+
results = await self.vector_manager.search_semantic(query, limit=limit)
|
|
429
|
+
if not results:
|
|
430
|
+
return ""
|
|
431
|
+
|
|
432
|
+
context_parts = ["### SEMANTIC MEMORY (PAST CONTEXT) ###"]
|
|
433
|
+
for res in results:
|
|
434
|
+
context_parts.append(f"[{res['timestamp']}] {res['role'].upper()}: {res['content']}")
|
|
435
|
+
|
|
436
|
+
return "\n".join(context_parts) + "\n"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# Global memory manager instance
|
|
440
|
+
memory_manager = MemoryManager()
|