genxai-framework 0.1.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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +6 -0
- cli/commands/approval.py +85 -0
- cli/commands/audit.py +127 -0
- cli/commands/metrics.py +25 -0
- cli/commands/tool.py +389 -0
- cli/main.py +32 -0
- genxai/__init__.py +81 -0
- genxai/api/__init__.py +5 -0
- genxai/api/app.py +21 -0
- genxai/config/__init__.py +5 -0
- genxai/config/settings.py +37 -0
- genxai/connectors/__init__.py +19 -0
- genxai/connectors/base.py +122 -0
- genxai/connectors/kafka.py +92 -0
- genxai/connectors/postgres_cdc.py +95 -0
- genxai/connectors/registry.py +44 -0
- genxai/connectors/sqs.py +94 -0
- genxai/connectors/webhook.py +73 -0
- genxai/core/__init__.py +37 -0
- genxai/core/agent/__init__.py +32 -0
- genxai/core/agent/base.py +206 -0
- genxai/core/agent/config_io.py +59 -0
- genxai/core/agent/registry.py +98 -0
- genxai/core/agent/runtime.py +970 -0
- genxai/core/communication/__init__.py +6 -0
- genxai/core/communication/collaboration.py +44 -0
- genxai/core/communication/message_bus.py +192 -0
- genxai/core/communication/protocols.py +35 -0
- genxai/core/execution/__init__.py +22 -0
- genxai/core/execution/metadata.py +181 -0
- genxai/core/execution/queue.py +201 -0
- genxai/core/graph/__init__.py +30 -0
- genxai/core/graph/checkpoints.py +77 -0
- genxai/core/graph/edges.py +131 -0
- genxai/core/graph/engine.py +813 -0
- genxai/core/graph/executor.py +516 -0
- genxai/core/graph/nodes.py +161 -0
- genxai/core/graph/trigger_runner.py +40 -0
- genxai/core/memory/__init__.py +19 -0
- genxai/core/memory/base.py +72 -0
- genxai/core/memory/embedding.py +327 -0
- genxai/core/memory/episodic.py +448 -0
- genxai/core/memory/long_term.py +467 -0
- genxai/core/memory/manager.py +543 -0
- genxai/core/memory/persistence.py +297 -0
- genxai/core/memory/procedural.py +461 -0
- genxai/core/memory/semantic.py +526 -0
- genxai/core/memory/shared.py +62 -0
- genxai/core/memory/short_term.py +303 -0
- genxai/core/memory/vector_store.py +508 -0
- genxai/core/memory/working.py +211 -0
- genxai/core/state/__init__.py +6 -0
- genxai/core/state/manager.py +293 -0
- genxai/core/state/schema.py +115 -0
- genxai/llm/__init__.py +14 -0
- genxai/llm/base.py +150 -0
- genxai/llm/factory.py +329 -0
- genxai/llm/providers/__init__.py +1 -0
- genxai/llm/providers/anthropic.py +249 -0
- genxai/llm/providers/cohere.py +274 -0
- genxai/llm/providers/google.py +334 -0
- genxai/llm/providers/ollama.py +147 -0
- genxai/llm/providers/openai.py +257 -0
- genxai/llm/routing.py +83 -0
- genxai/observability/__init__.py +6 -0
- genxai/observability/logging.py +327 -0
- genxai/observability/metrics.py +494 -0
- genxai/observability/tracing.py +372 -0
- genxai/performance/__init__.py +39 -0
- genxai/performance/cache.py +256 -0
- genxai/performance/pooling.py +289 -0
- genxai/security/audit.py +304 -0
- genxai/security/auth.py +315 -0
- genxai/security/cost_control.py +528 -0
- genxai/security/default_policies.py +44 -0
- genxai/security/jwt.py +142 -0
- genxai/security/oauth.py +226 -0
- genxai/security/pii.py +366 -0
- genxai/security/policy_engine.py +82 -0
- genxai/security/rate_limit.py +341 -0
- genxai/security/rbac.py +247 -0
- genxai/security/validation.py +218 -0
- genxai/tools/__init__.py +21 -0
- genxai/tools/base.py +383 -0
- genxai/tools/builtin/__init__.py +131 -0
- genxai/tools/builtin/communication/__init__.py +15 -0
- genxai/tools/builtin/communication/email_sender.py +159 -0
- genxai/tools/builtin/communication/notification_manager.py +167 -0
- genxai/tools/builtin/communication/slack_notifier.py +118 -0
- genxai/tools/builtin/communication/sms_sender.py +118 -0
- genxai/tools/builtin/communication/webhook_caller.py +136 -0
- genxai/tools/builtin/computation/__init__.py +15 -0
- genxai/tools/builtin/computation/calculator.py +101 -0
- genxai/tools/builtin/computation/code_executor.py +183 -0
- genxai/tools/builtin/computation/data_validator.py +259 -0
- genxai/tools/builtin/computation/hash_generator.py +129 -0
- genxai/tools/builtin/computation/regex_matcher.py +201 -0
- genxai/tools/builtin/data/__init__.py +15 -0
- genxai/tools/builtin/data/csv_processor.py +213 -0
- genxai/tools/builtin/data/data_transformer.py +299 -0
- genxai/tools/builtin/data/json_processor.py +233 -0
- genxai/tools/builtin/data/text_analyzer.py +288 -0
- genxai/tools/builtin/data/xml_processor.py +175 -0
- genxai/tools/builtin/database/__init__.py +15 -0
- genxai/tools/builtin/database/database_inspector.py +157 -0
- genxai/tools/builtin/database/mongodb_query.py +196 -0
- genxai/tools/builtin/database/redis_cache.py +167 -0
- genxai/tools/builtin/database/sql_query.py +145 -0
- genxai/tools/builtin/database/vector_search.py +163 -0
- genxai/tools/builtin/file/__init__.py +17 -0
- genxai/tools/builtin/file/directory_scanner.py +214 -0
- genxai/tools/builtin/file/file_compressor.py +237 -0
- genxai/tools/builtin/file/file_reader.py +102 -0
- genxai/tools/builtin/file/file_writer.py +122 -0
- genxai/tools/builtin/file/image_processor.py +186 -0
- genxai/tools/builtin/file/pdf_parser.py +144 -0
- genxai/tools/builtin/test/__init__.py +15 -0
- genxai/tools/builtin/test/async_simulator.py +62 -0
- genxai/tools/builtin/test/data_transformer.py +99 -0
- genxai/tools/builtin/test/error_generator.py +82 -0
- genxai/tools/builtin/test/simple_math.py +94 -0
- genxai/tools/builtin/test/string_processor.py +72 -0
- genxai/tools/builtin/web/__init__.py +15 -0
- genxai/tools/builtin/web/api_caller.py +161 -0
- genxai/tools/builtin/web/html_parser.py +330 -0
- genxai/tools/builtin/web/http_client.py +187 -0
- genxai/tools/builtin/web/url_validator.py +162 -0
- genxai/tools/builtin/web/web_scraper.py +170 -0
- genxai/tools/custom/my_test_tool_2.py +9 -0
- genxai/tools/dynamic.py +105 -0
- genxai/tools/mcp_server.py +167 -0
- genxai/tools/persistence/__init__.py +6 -0
- genxai/tools/persistence/models.py +55 -0
- genxai/tools/persistence/service.py +322 -0
- genxai/tools/registry.py +227 -0
- genxai/tools/security/__init__.py +11 -0
- genxai/tools/security/limits.py +214 -0
- genxai/tools/security/policy.py +20 -0
- genxai/tools/security/sandbox.py +248 -0
- genxai/tools/templates.py +435 -0
- genxai/triggers/__init__.py +19 -0
- genxai/triggers/base.py +104 -0
- genxai/triggers/file_watcher.py +75 -0
- genxai/triggers/queue.py +68 -0
- genxai/triggers/registry.py +82 -0
- genxai/triggers/schedule.py +66 -0
- genxai/triggers/webhook.py +68 -0
- genxai/utils/__init__.py +1 -0
- genxai/utils/tokens.py +295 -0
- genxai_framework-0.1.0.dist-info/METADATA +495 -0
- genxai_framework-0.1.0.dist-info/RECORD +156 -0
- genxai_framework-0.1.0.dist-info/WHEEL +5 -0
- genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
- genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- genxai_framework-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Long-term memory implementation with Redis backend."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from genxai.core.memory.base import Memory, MemoryType, MemoryConfig
|
|
9
|
+
from genxai.core.memory.persistence import (
|
|
10
|
+
JsonMemoryStore,
|
|
11
|
+
MemoryPersistenceConfig,
|
|
12
|
+
SqliteMemoryStore,
|
|
13
|
+
create_memory_store,
|
|
14
|
+
)
|
|
15
|
+
from genxai.core.memory.vector_store import VectorStore
|
|
16
|
+
from genxai.core.memory.embedding import EmbeddingService
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LongTermMemory:
|
|
22
|
+
"""Long-term memory with persistent Redis storage.
|
|
23
|
+
|
|
24
|
+
This memory type stores important memories persistently and supports
|
|
25
|
+
TTL-based expiration and importance-based retention.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
config: Optional[MemoryConfig] = None,
|
|
31
|
+
redis_client: Optional[Any] = None,
|
|
32
|
+
key_prefix: str = "genxai:memory:long_term:",
|
|
33
|
+
vector_store: Optional[VectorStore] = None,
|
|
34
|
+
embedding_service: Optional[EmbeddingService] = None,
|
|
35
|
+
persistence: Optional[MemoryPersistenceConfig] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize long-term memory.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config: Memory configuration
|
|
41
|
+
redis_client: Redis client instance (optional, will use in-memory if not provided)
|
|
42
|
+
key_prefix: Prefix for Redis keys
|
|
43
|
+
"""
|
|
44
|
+
self.config = config or MemoryConfig()
|
|
45
|
+
self._redis = redis_client
|
|
46
|
+
self._key_prefix = key_prefix
|
|
47
|
+
self._vector_store = vector_store
|
|
48
|
+
self._embedding_service = embedding_service
|
|
49
|
+
self._persistence = persistence
|
|
50
|
+
if persistence:
|
|
51
|
+
self._store = create_memory_store(persistence)
|
|
52
|
+
else:
|
|
53
|
+
self._store = None
|
|
54
|
+
|
|
55
|
+
# Fallback to in-memory storage if Redis not available
|
|
56
|
+
self._in_memory_storage: Dict[str, Memory] = {}
|
|
57
|
+
self._use_redis = redis_client is not None
|
|
58
|
+
|
|
59
|
+
if self._use_redis:
|
|
60
|
+
logger.info("Initialized long-term memory with Redis backend")
|
|
61
|
+
else:
|
|
62
|
+
logger.warning(
|
|
63
|
+
"Redis client not provided. Using in-memory storage. "
|
|
64
|
+
"Memories will not persist across restarts."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if self._store and self._persistence and self._persistence.enabled:
|
|
68
|
+
self._load_from_disk()
|
|
69
|
+
|
|
70
|
+
def store(
|
|
71
|
+
self,
|
|
72
|
+
memory: Memory,
|
|
73
|
+
ttl: Optional[int] = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Store a memory with optional TTL.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
memory: Memory to store
|
|
79
|
+
ttl: Time-to-live in seconds (None for no expiration)
|
|
80
|
+
"""
|
|
81
|
+
key = self._make_key(memory.id)
|
|
82
|
+
|
|
83
|
+
# Serialize memory
|
|
84
|
+
data = self._serialize_memory(memory)
|
|
85
|
+
|
|
86
|
+
if self._use_redis:
|
|
87
|
+
try:
|
|
88
|
+
# Store in Redis
|
|
89
|
+
if ttl:
|
|
90
|
+
self._redis.setex(key, ttl, data)
|
|
91
|
+
else:
|
|
92
|
+
self._redis.set(key, data)
|
|
93
|
+
|
|
94
|
+
# Store metadata for querying
|
|
95
|
+
self._store_metadata(memory)
|
|
96
|
+
|
|
97
|
+
logger.debug(f"Stored memory {memory.id} in Redis (TTL: {ttl})")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Failed to store memory in Redis: {e}")
|
|
100
|
+
# Fallback to in-memory
|
|
101
|
+
self._in_memory_storage[memory.id] = memory
|
|
102
|
+
else:
|
|
103
|
+
# In-memory storage
|
|
104
|
+
self._in_memory_storage[memory.id] = memory
|
|
105
|
+
logger.debug(f"Stored memory {memory.id} in-memory")
|
|
106
|
+
|
|
107
|
+
self._persist()
|
|
108
|
+
|
|
109
|
+
def retrieve(self, memory_id: str) -> Optional[Memory]:
|
|
110
|
+
"""Retrieve a memory by ID.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
memory_id: ID of memory to retrieve
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Memory if found, None otherwise
|
|
117
|
+
"""
|
|
118
|
+
if self._use_redis:
|
|
119
|
+
try:
|
|
120
|
+
key = self._make_key(memory_id)
|
|
121
|
+
data = self._redis.get(key)
|
|
122
|
+
|
|
123
|
+
if data:
|
|
124
|
+
memory = self._deserialize_memory(data)
|
|
125
|
+
|
|
126
|
+
# Update access tracking
|
|
127
|
+
memory.access_count += 1
|
|
128
|
+
memory.last_accessed = datetime.now()
|
|
129
|
+
|
|
130
|
+
# Update in storage
|
|
131
|
+
self.store(memory)
|
|
132
|
+
|
|
133
|
+
logger.debug(f"Retrieved memory {memory_id} from Redis")
|
|
134
|
+
return memory
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"Failed to retrieve memory from Redis: {e}")
|
|
137
|
+
|
|
138
|
+
# Fallback to in-memory
|
|
139
|
+
if memory_id in self._in_memory_storage:
|
|
140
|
+
memory = self._in_memory_storage[memory_id]
|
|
141
|
+
memory.access_count += 1
|
|
142
|
+
memory.last_accessed = datetime.now()
|
|
143
|
+
logger.debug(f"Retrieved memory {memory_id} from in-memory storage")
|
|
144
|
+
return memory
|
|
145
|
+
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def retrieve_by_importance(
|
|
149
|
+
self,
|
|
150
|
+
threshold: float = 0.7,
|
|
151
|
+
limit: int = 10,
|
|
152
|
+
) -> List[Memory]:
|
|
153
|
+
"""Retrieve memories above an importance threshold.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
threshold: Minimum importance score
|
|
157
|
+
limit: Maximum number of memories
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of important memories
|
|
161
|
+
"""
|
|
162
|
+
if self._use_redis:
|
|
163
|
+
try:
|
|
164
|
+
# Query metadata index
|
|
165
|
+
pattern = f"{self._key_prefix}*"
|
|
166
|
+
keys = self._redis.keys(pattern)
|
|
167
|
+
|
|
168
|
+
memories = []
|
|
169
|
+
for key in keys:
|
|
170
|
+
data = self._redis.get(key)
|
|
171
|
+
if data:
|
|
172
|
+
memory = self._deserialize_memory(data)
|
|
173
|
+
if memory.importance >= threshold:
|
|
174
|
+
memories.append(memory)
|
|
175
|
+
|
|
176
|
+
# Sort by importance
|
|
177
|
+
memories.sort(key=lambda m: m.importance, reverse=True)
|
|
178
|
+
|
|
179
|
+
result = memories[:limit]
|
|
180
|
+
logger.debug(f"Retrieved {len(result)} important memories from Redis")
|
|
181
|
+
return result
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Failed to query Redis: {e}")
|
|
184
|
+
|
|
185
|
+
# Fallback to in-memory
|
|
186
|
+
memories = [
|
|
187
|
+
m for m in self._in_memory_storage.values()
|
|
188
|
+
if m.importance >= threshold
|
|
189
|
+
]
|
|
190
|
+
memories.sort(key=lambda m: m.importance, reverse=True)
|
|
191
|
+
result = memories[:limit]
|
|
192
|
+
logger.debug(f"Retrieved {len(result)} important memories from in-memory storage")
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
def retrieve_recent(
|
|
196
|
+
self,
|
|
197
|
+
days: int = 7,
|
|
198
|
+
limit: int = 10,
|
|
199
|
+
) -> List[Memory]:
|
|
200
|
+
"""Retrieve recent memories within a time window.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
days: Number of days to look back
|
|
204
|
+
limit: Maximum number of memories
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of recent memories
|
|
208
|
+
"""
|
|
209
|
+
cutoff = datetime.now() - timedelta(days=days)
|
|
210
|
+
|
|
211
|
+
if self._use_redis:
|
|
212
|
+
try:
|
|
213
|
+
pattern = f"{self._key_prefix}*"
|
|
214
|
+
keys = self._redis.keys(pattern)
|
|
215
|
+
|
|
216
|
+
memories = []
|
|
217
|
+
for key in keys:
|
|
218
|
+
data = self._redis.get(key)
|
|
219
|
+
if data:
|
|
220
|
+
memory = self._deserialize_memory(data)
|
|
221
|
+
if memory.timestamp >= cutoff:
|
|
222
|
+
memories.append(memory)
|
|
223
|
+
|
|
224
|
+
# Sort by timestamp (most recent first)
|
|
225
|
+
memories.sort(key=lambda m: m.timestamp, reverse=True)
|
|
226
|
+
|
|
227
|
+
result = memories[:limit]
|
|
228
|
+
logger.debug(f"Retrieved {len(result)} recent memories from Redis")
|
|
229
|
+
return result
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.error(f"Failed to query Redis: {e}")
|
|
232
|
+
|
|
233
|
+
# Fallback to in-memory
|
|
234
|
+
memories = [
|
|
235
|
+
m for m in self._in_memory_storage.values()
|
|
236
|
+
if m.timestamp >= cutoff
|
|
237
|
+
]
|
|
238
|
+
memories.sort(key=lambda m: m.timestamp, reverse=True)
|
|
239
|
+
result = memories[:limit]
|
|
240
|
+
logger.debug(f"Retrieved {len(result)} recent memories from in-memory storage")
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
def delete(self, memory_id: str) -> bool:
|
|
244
|
+
"""Delete a memory by ID.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
memory_id: ID of memory to delete
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if deleted, False if not found
|
|
251
|
+
"""
|
|
252
|
+
if self._use_redis:
|
|
253
|
+
try:
|
|
254
|
+
key = self._make_key(memory_id)
|
|
255
|
+
deleted = self._redis.delete(key)
|
|
256
|
+
|
|
257
|
+
if deleted:
|
|
258
|
+
self._delete_metadata(memory_id)
|
|
259
|
+
logger.debug(f"Deleted memory {memory_id} from Redis")
|
|
260
|
+
return True
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"Failed to delete memory from Redis: {e}")
|
|
263
|
+
|
|
264
|
+
# Fallback to in-memory
|
|
265
|
+
if memory_id in self._in_memory_storage:
|
|
266
|
+
del self._in_memory_storage[memory_id]
|
|
267
|
+
logger.debug(f"Deleted memory {memory_id} from in-memory storage")
|
|
268
|
+
self._persist()
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
def clear(self) -> None:
|
|
274
|
+
"""Clear all memories."""
|
|
275
|
+
if self._use_redis:
|
|
276
|
+
try:
|
|
277
|
+
pattern = f"{self._key_prefix}*"
|
|
278
|
+
keys = self._redis.keys(pattern)
|
|
279
|
+
|
|
280
|
+
if keys:
|
|
281
|
+
self._redis.delete(*keys)
|
|
282
|
+
|
|
283
|
+
logger.info(f"Cleared {len(keys)} memories from Redis")
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.error(f"Failed to clear Redis: {e}")
|
|
286
|
+
|
|
287
|
+
# Clear in-memory storage
|
|
288
|
+
count = len(self._in_memory_storage)
|
|
289
|
+
self._in_memory_storage.clear()
|
|
290
|
+
logger.info(f"Cleared {count} memories from in-memory storage")
|
|
291
|
+
|
|
292
|
+
self._persist()
|
|
293
|
+
|
|
294
|
+
def get_size(self) -> int:
|
|
295
|
+
"""Get current number of stored memories.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Number of memories
|
|
299
|
+
"""
|
|
300
|
+
if self._use_redis:
|
|
301
|
+
try:
|
|
302
|
+
pattern = f"{self._key_prefix}*"
|
|
303
|
+
keys = self._redis.keys(pattern)
|
|
304
|
+
return len(keys)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"Failed to get size from Redis: {e}")
|
|
307
|
+
|
|
308
|
+
return len(self._in_memory_storage)
|
|
309
|
+
|
|
310
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
311
|
+
"""Get memory statistics.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Statistics dictionary
|
|
315
|
+
"""
|
|
316
|
+
size = self.get_size()
|
|
317
|
+
|
|
318
|
+
if size == 0:
|
|
319
|
+
return {
|
|
320
|
+
"size": 0,
|
|
321
|
+
"backend": "redis" if self._use_redis else "in-memory",
|
|
322
|
+
"avg_importance": 0.0,
|
|
323
|
+
"vector_store": bool(self._vector_store),
|
|
324
|
+
"persistence": bool(self._persistence and self._persistence.enabled),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# Get sample of memories for stats
|
|
328
|
+
if self._use_redis:
|
|
329
|
+
try:
|
|
330
|
+
pattern = f"{self._key_prefix}*"
|
|
331
|
+
keys = list(self._redis.keys(pattern))[:100] # Sample
|
|
332
|
+
|
|
333
|
+
memories = []
|
|
334
|
+
for key in keys:
|
|
335
|
+
data = self._redis.get(key)
|
|
336
|
+
if data:
|
|
337
|
+
memories.append(self._deserialize_memory(data))
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.error(f"Failed to get stats from Redis: {e}")
|
|
340
|
+
memories = []
|
|
341
|
+
else:
|
|
342
|
+
memories = list(self._in_memory_storage.values())
|
|
343
|
+
|
|
344
|
+
if not memories:
|
|
345
|
+
return {
|
|
346
|
+
"size": size,
|
|
347
|
+
"backend": "redis" if self._use_redis else "in-memory",
|
|
348
|
+
"avg_importance": 0.0,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"size": size,
|
|
353
|
+
"backend": "redis" if self._use_redis else "in-memory",
|
|
354
|
+
"avg_importance": sum(m.importance for m in memories) / len(memories),
|
|
355
|
+
"oldest_memory": min(m.timestamp for m in memories).isoformat(),
|
|
356
|
+
"newest_memory": max(m.timestamp for m in memories).isoformat(),
|
|
357
|
+
"vector_store": bool(self._vector_store),
|
|
358
|
+
"persistence": bool(self._persistence and self._persistence.enabled),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async def store_with_embedding(self, memory: Memory, ttl: Optional[int] = None) -> None:
|
|
362
|
+
"""Store memory and push embedding to vector store if configured."""
|
|
363
|
+
self.store(memory, ttl)
|
|
364
|
+
if self._vector_store and self._embedding_service:
|
|
365
|
+
try:
|
|
366
|
+
embedding = await self._embedding_service.embed(str(memory.content))
|
|
367
|
+
await self._vector_store.store(memory, embedding)
|
|
368
|
+
except Exception as exc:
|
|
369
|
+
logger.error("Failed to store memory embedding: %s", exc)
|
|
370
|
+
|
|
371
|
+
async def search(
|
|
372
|
+
self,
|
|
373
|
+
query: str,
|
|
374
|
+
limit: int = 10,
|
|
375
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
376
|
+
) -> List[Tuple[Memory, float]]:
|
|
377
|
+
"""Search long-term memory using vector store if available."""
|
|
378
|
+
if not self._vector_store or not self._embedding_service:
|
|
379
|
+
logger.warning("Vector search not available")
|
|
380
|
+
return []
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
query_embedding = await self._embedding_service.embed(query)
|
|
384
|
+
return await self._vector_store.search(query_embedding, limit=limit, filters=filters)
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
logger.error("Failed to search long-term memory: %s", exc)
|
|
387
|
+
return []
|
|
388
|
+
|
|
389
|
+
def _persist(self) -> None:
|
|
390
|
+
if not self._store:
|
|
391
|
+
return
|
|
392
|
+
payload = [json.loads(self._serialize_memory(memory)) for memory in self._in_memory_storage.values()]
|
|
393
|
+
self._store.save_list("long_term_memory.json", payload)
|
|
394
|
+
|
|
395
|
+
def _load_from_disk(self) -> None:
|
|
396
|
+
if not self._store:
|
|
397
|
+
return
|
|
398
|
+
data = self._store.load_list("long_term_memory.json")
|
|
399
|
+
if not data:
|
|
400
|
+
return
|
|
401
|
+
self._in_memory_storage = {
|
|
402
|
+
item["id"]: self._deserialize_memory(json.dumps(item)) for item in data
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
def _make_key(self, memory_id: str) -> str:
|
|
406
|
+
"""Create Redis key for memory ID."""
|
|
407
|
+
return f"{self._key_prefix}{memory_id}"
|
|
408
|
+
|
|
409
|
+
def _serialize_memory(self, memory: Memory) -> str:
|
|
410
|
+
"""Serialize memory to JSON string."""
|
|
411
|
+
data = {
|
|
412
|
+
"id": memory.id,
|
|
413
|
+
"type": memory.type.value,
|
|
414
|
+
"content": memory.content,
|
|
415
|
+
"metadata": memory.metadata,
|
|
416
|
+
"timestamp": memory.timestamp.isoformat(),
|
|
417
|
+
"importance": memory.importance,
|
|
418
|
+
"access_count": memory.access_count,
|
|
419
|
+
"last_accessed": memory.last_accessed.isoformat(),
|
|
420
|
+
"tags": memory.tags,
|
|
421
|
+
}
|
|
422
|
+
return json.dumps(data)
|
|
423
|
+
|
|
424
|
+
def _deserialize_memory(self, data: str) -> Memory:
|
|
425
|
+
"""Deserialize memory from JSON string."""
|
|
426
|
+
obj = json.loads(data)
|
|
427
|
+
return Memory(
|
|
428
|
+
id=obj["id"],
|
|
429
|
+
type=MemoryType(obj["type"]),
|
|
430
|
+
content=obj["content"],
|
|
431
|
+
metadata=obj["metadata"],
|
|
432
|
+
timestamp=datetime.fromisoformat(obj["timestamp"]),
|
|
433
|
+
importance=obj["importance"],
|
|
434
|
+
access_count=obj["access_count"],
|
|
435
|
+
last_accessed=datetime.fromisoformat(obj["last_accessed"]),
|
|
436
|
+
tags=obj["tags"],
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def _store_metadata(self, memory: Memory) -> None:
|
|
440
|
+
"""Store memory metadata for querying (placeholder)."""
|
|
441
|
+
if not self._store:
|
|
442
|
+
return
|
|
443
|
+
if isinstance(self._store, SqliteMemoryStore):
|
|
444
|
+
self._store.store_long_term_metadata(
|
|
445
|
+
memory_id=memory.id,
|
|
446
|
+
memory_type=memory.type.value,
|
|
447
|
+
importance=memory.importance,
|
|
448
|
+
timestamp=memory.timestamp.isoformat(),
|
|
449
|
+
tags=memory.tags,
|
|
450
|
+
metadata=memory.metadata,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def _delete_metadata(self, memory_id: str) -> None:
|
|
454
|
+
"""Delete memory metadata (placeholder)."""
|
|
455
|
+
if not self._store:
|
|
456
|
+
return
|
|
457
|
+
if isinstance(self._store, SqliteMemoryStore):
|
|
458
|
+
self._store.delete_long_term_metadata(memory_id)
|
|
459
|
+
|
|
460
|
+
def __len__(self) -> int:
|
|
461
|
+
"""Get number of stored memories."""
|
|
462
|
+
return self.get_size()
|
|
463
|
+
|
|
464
|
+
def __repr__(self) -> str:
|
|
465
|
+
"""String representation."""
|
|
466
|
+
backend = "Redis" if self._use_redis else "In-Memory"
|
|
467
|
+
return f"LongTermMemory(backend={backend}, size={self.get_size()})"
|