runtime-memory 3.0.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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
"""Memory Engine orchestrator for Runtime Memory.
|
|
2
|
+
|
|
3
|
+
The MemoryEngine is the main orchestrator that coordinates:
|
|
4
|
+
- Storage layer for persistence
|
|
5
|
+
- Embedding provider for vector embeddings
|
|
6
|
+
- Hybrid retriever for intelligent search
|
|
7
|
+
- Outcome tracking for learning from feedback
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import builtins
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from runtime_memory.core.embeddings import (
|
|
18
|
+
EmbeddingConfig,
|
|
19
|
+
EmbeddingProvider,
|
|
20
|
+
MockEmbeddingProvider,
|
|
21
|
+
get_embedding_provider,
|
|
22
|
+
)
|
|
23
|
+
from runtime_memory.core.logging import get_logger
|
|
24
|
+
from runtime_memory.core.models import (
|
|
25
|
+
ContextResponse,
|
|
26
|
+
Memory,
|
|
27
|
+
MemoryCategory,
|
|
28
|
+
MemoryCreate,
|
|
29
|
+
MemoryScope,
|
|
30
|
+
MemorySource,
|
|
31
|
+
MemoryUpdate,
|
|
32
|
+
Outcome,
|
|
33
|
+
SearchResult,
|
|
34
|
+
)
|
|
35
|
+
from runtime_memory.core.paths import default_db_path
|
|
36
|
+
from runtime_memory.core.retrieval import HybridRetriever, RetrievalConfig
|
|
37
|
+
from runtime_memory.core.storage import (
|
|
38
|
+
MemoryNotFoundError,
|
|
39
|
+
MemoryStorage,
|
|
40
|
+
StorageStats,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from collections.abc import Sequence
|
|
45
|
+
|
|
46
|
+
# Type alias to avoid conflict with list() method
|
|
47
|
+
_List = builtins.list
|
|
48
|
+
|
|
49
|
+
logger = get_logger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class EngineConfig:
|
|
54
|
+
"""Configuration for the Memory Engine."""
|
|
55
|
+
|
|
56
|
+
# Storage settings
|
|
57
|
+
db_path: str | Path = field(default_factory=lambda: str(default_db_path()))
|
|
58
|
+
"""Path to SQLite database file."""
|
|
59
|
+
|
|
60
|
+
pool_size: int = 5
|
|
61
|
+
"""Number of database connections in the pool."""
|
|
62
|
+
|
|
63
|
+
secure_permissions: bool = True
|
|
64
|
+
"""Whether to set secure file permissions (0600) on database."""
|
|
65
|
+
|
|
66
|
+
# Embedding settings
|
|
67
|
+
embedding_provider: str = "local"
|
|
68
|
+
"""Embedding provider type: 'local', 'openai', 'voyage', 'mock', 'null'."""
|
|
69
|
+
|
|
70
|
+
embedding_config: EmbeddingConfig | None = None
|
|
71
|
+
"""Optional embedding provider configuration."""
|
|
72
|
+
|
|
73
|
+
# Retrieval settings
|
|
74
|
+
retrieval_config: RetrievalConfig | None = None
|
|
75
|
+
"""Optional retrieval configuration."""
|
|
76
|
+
|
|
77
|
+
# Auto-archival settings
|
|
78
|
+
auto_archive_enabled: bool = True
|
|
79
|
+
"""Whether to automatically archive low-score memories."""
|
|
80
|
+
|
|
81
|
+
auto_archive_threshold: float = -0.5
|
|
82
|
+
"""Outcome score threshold below which memories are archived."""
|
|
83
|
+
|
|
84
|
+
auto_archive_on_search: bool = False
|
|
85
|
+
"""Whether to run auto-archival after searches (can impact performance)."""
|
|
86
|
+
|
|
87
|
+
# Last search tracking
|
|
88
|
+
track_last_search: bool = True
|
|
89
|
+
"""Whether to track the last search for easy outcome recording."""
|
|
90
|
+
|
|
91
|
+
def __post_init__(self) -> None:
|
|
92
|
+
"""Expand paths and set defaults."""
|
|
93
|
+
if isinstance(self.db_path, str):
|
|
94
|
+
self.db_path = Path(self.db_path).expanduser()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class EngineStats:
|
|
99
|
+
"""Statistics from the Memory Engine."""
|
|
100
|
+
|
|
101
|
+
storage_stats: StorageStats
|
|
102
|
+
"""Underlying storage statistics."""
|
|
103
|
+
|
|
104
|
+
indexed_memories: int
|
|
105
|
+
"""Number of memories indexed in retriever."""
|
|
106
|
+
|
|
107
|
+
indexed_with_embeddings: int
|
|
108
|
+
"""Number of memories with embeddings in retriever."""
|
|
109
|
+
|
|
110
|
+
last_search_result_count: int
|
|
111
|
+
"""Number of results from last search (if tracked)."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class LastSearchInfo:
|
|
116
|
+
"""Information about the last search performed."""
|
|
117
|
+
|
|
118
|
+
query: str
|
|
119
|
+
"""The search query."""
|
|
120
|
+
|
|
121
|
+
results: _List[SearchResult]
|
|
122
|
+
"""Search results."""
|
|
123
|
+
|
|
124
|
+
memory_ids: _List[str] = field(default_factory=list)
|
|
125
|
+
"""IDs of memories returned in search results."""
|
|
126
|
+
|
|
127
|
+
def __post_init__(self) -> None:
|
|
128
|
+
"""Extract memory IDs from results."""
|
|
129
|
+
self.memory_ids = [r.memory.id for r in self.results]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class EngineError(Exception):
|
|
133
|
+
"""Base exception for engine errors."""
|
|
134
|
+
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class EngineNotInitializedError(EngineError):
|
|
139
|
+
"""Raised when engine is used before initialization."""
|
|
140
|
+
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MemoryEngine:
|
|
145
|
+
"""Main orchestrator for the Runtime Memory.
|
|
146
|
+
|
|
147
|
+
Coordinates storage, embeddings, and retrieval to provide
|
|
148
|
+
a unified interface for memory operations with outcome-based learning.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
```python
|
|
152
|
+
engine = MemoryEngine()
|
|
153
|
+
await engine.initialize()
|
|
154
|
+
|
|
155
|
+
# Add a memory
|
|
156
|
+
memory = await engine.add(
|
|
157
|
+
content="Use async/await for I/O operations",
|
|
158
|
+
category=MemoryCategory.PATTERN,
|
|
159
|
+
project="my-project",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Search for relevant memories
|
|
163
|
+
results = await engine.search("async patterns")
|
|
164
|
+
|
|
165
|
+
# Record outcome feedback
|
|
166
|
+
await engine.record_outcome([results[0].memory.id], Outcome.WORKED)
|
|
167
|
+
|
|
168
|
+
# Get context for injection
|
|
169
|
+
context = await engine.get_context(project="my-project")
|
|
170
|
+
print(context.to_markdown())
|
|
171
|
+
|
|
172
|
+
await engine.close()
|
|
173
|
+
```
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
config: EngineConfig | None = None,
|
|
179
|
+
storage: MemoryStorage | None = None,
|
|
180
|
+
embedding_provider: EmbeddingProvider | None = None,
|
|
181
|
+
retriever: HybridRetriever | None = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Initialize the Memory Engine.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
config: Engine configuration. Uses defaults if not provided.
|
|
187
|
+
storage: Optional pre-configured storage instance.
|
|
188
|
+
embedding_provider: Optional pre-configured embedding provider.
|
|
189
|
+
retriever: Optional pre-configured retriever.
|
|
190
|
+
"""
|
|
191
|
+
self.config = config or EngineConfig()
|
|
192
|
+
|
|
193
|
+
# Components (initialized lazily or provided)
|
|
194
|
+
self._storage = storage
|
|
195
|
+
self._embedding_provider = embedding_provider
|
|
196
|
+
self._retriever = retriever
|
|
197
|
+
|
|
198
|
+
# State
|
|
199
|
+
self._initialized = False
|
|
200
|
+
self._last_search: LastSearchInfo | None = None
|
|
201
|
+
|
|
202
|
+
async def initialize(self) -> None:
|
|
203
|
+
"""Initialize the engine and all components.
|
|
204
|
+
|
|
205
|
+
Creates and initializes storage, embedding provider, and retriever.
|
|
206
|
+
Also loads existing memories into the retriever index.
|
|
207
|
+
"""
|
|
208
|
+
if self._initialized:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
logger.info("Initializing Memory Engine...")
|
|
212
|
+
|
|
213
|
+
# Initialize storage
|
|
214
|
+
if self._storage is None:
|
|
215
|
+
self._storage = MemoryStorage(
|
|
216
|
+
db_path=self.config.db_path,
|
|
217
|
+
pool_size=self.config.pool_size,
|
|
218
|
+
secure_permissions=self.config.secure_permissions,
|
|
219
|
+
)
|
|
220
|
+
await self._storage.initialize()
|
|
221
|
+
|
|
222
|
+
# Initialize embedding provider
|
|
223
|
+
if self._embedding_provider is None:
|
|
224
|
+
self._embedding_provider = get_embedding_provider(
|
|
225
|
+
provider_type=self.config.embedding_provider,
|
|
226
|
+
config=self.config.embedding_config,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Initialize retriever
|
|
230
|
+
if self._retriever is None:
|
|
231
|
+
self._retriever = HybridRetriever(
|
|
232
|
+
embedding_provider=self._embedding_provider,
|
|
233
|
+
config=self.config.retrieval_config,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Load existing memories into retriever
|
|
237
|
+
await self._load_memories_to_retriever()
|
|
238
|
+
|
|
239
|
+
self._initialized = True
|
|
240
|
+
logger.info("Memory Engine initialized successfully")
|
|
241
|
+
|
|
242
|
+
async def close(self) -> None:
|
|
243
|
+
"""Close the engine and release resources."""
|
|
244
|
+
if self._storage:
|
|
245
|
+
await self._storage.close()
|
|
246
|
+
self._initialized = False
|
|
247
|
+
logger.info("Memory Engine closed")
|
|
248
|
+
|
|
249
|
+
async def __aenter__(self) -> MemoryEngine:
|
|
250
|
+
"""Async context manager entry."""
|
|
251
|
+
await self.initialize()
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
255
|
+
"""Async context manager exit."""
|
|
256
|
+
await self.close()
|
|
257
|
+
|
|
258
|
+
def _ensure_initialized(self) -> None:
|
|
259
|
+
"""Ensure the engine is initialized.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
EngineNotInitializedError: If engine is not initialized.
|
|
263
|
+
"""
|
|
264
|
+
if not self._initialized:
|
|
265
|
+
raise EngineNotInitializedError(
|
|
266
|
+
"Engine not initialized. Call initialize() first or use async context manager."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def storage(self) -> MemoryStorage:
|
|
271
|
+
"""Get the storage instance."""
|
|
272
|
+
self._ensure_initialized()
|
|
273
|
+
assert self._storage is not None
|
|
274
|
+
return self._storage
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def embedding_provider(self) -> EmbeddingProvider:
|
|
278
|
+
"""Get the embedding provider instance."""
|
|
279
|
+
self._ensure_initialized()
|
|
280
|
+
assert self._embedding_provider is not None
|
|
281
|
+
return self._embedding_provider
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def retriever(self) -> HybridRetriever:
|
|
285
|
+
"""Get the retriever instance."""
|
|
286
|
+
self._ensure_initialized()
|
|
287
|
+
assert self._retriever is not None
|
|
288
|
+
return self._retriever
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def last_search(self) -> LastSearchInfo | None:
|
|
292
|
+
"""Get information about the last search performed."""
|
|
293
|
+
return self._last_search
|
|
294
|
+
|
|
295
|
+
# =========================================================================
|
|
296
|
+
# Core CRUD Operations
|
|
297
|
+
# =========================================================================
|
|
298
|
+
|
|
299
|
+
async def add(
|
|
300
|
+
self,
|
|
301
|
+
content: str,
|
|
302
|
+
category: MemoryCategory,
|
|
303
|
+
project: str | None = None,
|
|
304
|
+
scope: MemoryScope = MemoryScope.PROJECT,
|
|
305
|
+
source: MemorySource = MemorySource.EXPLICIT,
|
|
306
|
+
confidence: float = 1.0,
|
|
307
|
+
importance: float = 0.5,
|
|
308
|
+
tags: _List[str] | None = None,
|
|
309
|
+
entities: _List[str] | None = None,
|
|
310
|
+
supersedes: str | None = None,
|
|
311
|
+
metadata: dict[str, Any] | None = None,
|
|
312
|
+
) -> Memory:
|
|
313
|
+
"""Add a new memory.
|
|
314
|
+
|
|
315
|
+
Creates a memory, generates embedding, stores in database, and
|
|
316
|
+
indexes in retriever.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
content: The memory content text.
|
|
320
|
+
category: Classification category.
|
|
321
|
+
project: Project scope (None for global).
|
|
322
|
+
scope: Visibility scope.
|
|
323
|
+
source: How the memory was created.
|
|
324
|
+
confidence: Source reliability (0.0-1.0).
|
|
325
|
+
importance: Importance weight (0.0-1.0).
|
|
326
|
+
tags: Optional tags for categorization.
|
|
327
|
+
entities: Detected entities (files, functions, etc.).
|
|
328
|
+
supersedes: ID of memory this one replaces.
|
|
329
|
+
metadata: Additional metadata.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
The created Memory.
|
|
333
|
+
"""
|
|
334
|
+
self._ensure_initialized()
|
|
335
|
+
|
|
336
|
+
# Validate using Pydantic model
|
|
337
|
+
create_model = MemoryCreate(
|
|
338
|
+
content=content,
|
|
339
|
+
category=category,
|
|
340
|
+
project=project,
|
|
341
|
+
scope=scope,
|
|
342
|
+
source=source,
|
|
343
|
+
confidence=confidence,
|
|
344
|
+
importance=importance,
|
|
345
|
+
tags=tags or [],
|
|
346
|
+
entities=entities or [],
|
|
347
|
+
supersedes=supersedes,
|
|
348
|
+
metadata=metadata or {},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Convert to Memory dataclass
|
|
352
|
+
memory = create_model.to_memory()
|
|
353
|
+
|
|
354
|
+
# Generate embedding
|
|
355
|
+
embedding_result = await self._embedding_provider.embed(memory.content)
|
|
356
|
+
memory.embedding = embedding_result.embedding
|
|
357
|
+
|
|
358
|
+
# Store in database
|
|
359
|
+
await self._storage.create(memory)
|
|
360
|
+
|
|
361
|
+
# Add to retriever index
|
|
362
|
+
self._retriever.add_memory(memory, embedding_result.embedding)
|
|
363
|
+
|
|
364
|
+
# Archive superseded memory if specified
|
|
365
|
+
if supersedes:
|
|
366
|
+
try:
|
|
367
|
+
await self._storage.archive(supersedes)
|
|
368
|
+
self._retriever.remove_memory(supersedes)
|
|
369
|
+
except MemoryNotFoundError:
|
|
370
|
+
logger.warning(f"Superseded memory {supersedes} not found")
|
|
371
|
+
|
|
372
|
+
logger.debug(f"Added memory {memory.id}: {content[:50]}...")
|
|
373
|
+
return memory
|
|
374
|
+
|
|
375
|
+
async def add_memory(self, memory: Memory) -> Memory:
|
|
376
|
+
"""Add an existing Memory object.
|
|
377
|
+
|
|
378
|
+
Useful when you have a pre-constructed Memory object.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
memory: The Memory to add.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
The stored Memory (with embedding if not present).
|
|
385
|
+
"""
|
|
386
|
+
self._ensure_initialized()
|
|
387
|
+
|
|
388
|
+
# Generate embedding if not present
|
|
389
|
+
if memory.embedding is None:
|
|
390
|
+
embedding_result = await self._embedding_provider.embed(memory.content)
|
|
391
|
+
memory.embedding = embedding_result.embedding
|
|
392
|
+
|
|
393
|
+
# Store in database
|
|
394
|
+
await self._storage.create(memory)
|
|
395
|
+
|
|
396
|
+
# Add to retriever index
|
|
397
|
+
self._retriever.add_memory(memory, memory.embedding)
|
|
398
|
+
|
|
399
|
+
logger.debug(f"Added memory {memory.id}")
|
|
400
|
+
return memory
|
|
401
|
+
|
|
402
|
+
async def add_many(self, memories: Sequence[Memory]) -> _List[Memory]:
|
|
403
|
+
"""Add multiple memories in a batch.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
memories: Memories to add.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
List of stored memories.
|
|
410
|
+
"""
|
|
411
|
+
self._ensure_initialized()
|
|
412
|
+
|
|
413
|
+
# Generate embeddings for memories without them
|
|
414
|
+
texts_to_embed = []
|
|
415
|
+
indices_to_embed = []
|
|
416
|
+
for i, memory in enumerate(memories):
|
|
417
|
+
if memory.embedding is None:
|
|
418
|
+
texts_to_embed.append(memory.content)
|
|
419
|
+
indices_to_embed.append(i)
|
|
420
|
+
|
|
421
|
+
if texts_to_embed:
|
|
422
|
+
batch_result = await self._embedding_provider.embed_many(texts_to_embed)
|
|
423
|
+
for i, embedding in zip(indices_to_embed, batch_result.embeddings, strict=True):
|
|
424
|
+
memories[i].embedding = embedding
|
|
425
|
+
|
|
426
|
+
# Store all in database
|
|
427
|
+
memories_list = list(memories)
|
|
428
|
+
await self._storage.create_many(memories_list)
|
|
429
|
+
|
|
430
|
+
# Add all to retriever
|
|
431
|
+
for memory in memories_list:
|
|
432
|
+
self._retriever.add_memory(memory, memory.embedding)
|
|
433
|
+
|
|
434
|
+
logger.debug(f"Added {len(memories_list)} memories")
|
|
435
|
+
return memories_list
|
|
436
|
+
|
|
437
|
+
async def get(self, memory_id: str) -> Memory:
|
|
438
|
+
"""Get a memory by ID.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
memory_id: The memory ID.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
The Memory.
|
|
445
|
+
|
|
446
|
+
Raises:
|
|
447
|
+
MemoryNotFoundError: If memory not found.
|
|
448
|
+
"""
|
|
449
|
+
self._ensure_initialized()
|
|
450
|
+
return await self._storage.get(memory_id)
|
|
451
|
+
|
|
452
|
+
async def get_many(self, memory_ids: _List[str]) -> _List[Memory]:
|
|
453
|
+
"""Get multiple memories by ID.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
memory_ids: List of memory IDs.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
List of memories (preserves order, excludes missing).
|
|
460
|
+
"""
|
|
461
|
+
self._ensure_initialized()
|
|
462
|
+
return await self._storage.get_many(memory_ids)
|
|
463
|
+
|
|
464
|
+
async def update(
|
|
465
|
+
self,
|
|
466
|
+
memory_id: str,
|
|
467
|
+
content: str | None = None,
|
|
468
|
+
category: MemoryCategory | None = None,
|
|
469
|
+
confidence: float | None = None,
|
|
470
|
+
importance: float | None = None,
|
|
471
|
+
tags: _List[str] | None = None,
|
|
472
|
+
entities: _List[str] | None = None,
|
|
473
|
+
metadata: dict[str, Any] | None = None,
|
|
474
|
+
) -> Memory:
|
|
475
|
+
"""Update an existing memory.
|
|
476
|
+
|
|
477
|
+
Only provided fields are updated. If content is changed, a new
|
|
478
|
+
embedding is generated.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
memory_id: ID of memory to update.
|
|
482
|
+
content: New content (optional).
|
|
483
|
+
category: New category (optional).
|
|
484
|
+
confidence: New confidence (optional).
|
|
485
|
+
importance: New importance (optional).
|
|
486
|
+
tags: New tags (optional).
|
|
487
|
+
entities: New entities (optional).
|
|
488
|
+
metadata: New metadata (optional).
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
The updated Memory.
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
MemoryNotFoundError: If memory not found.
|
|
495
|
+
"""
|
|
496
|
+
self._ensure_initialized()
|
|
497
|
+
|
|
498
|
+
# Get existing memory
|
|
499
|
+
memory = await self._storage.get(memory_id)
|
|
500
|
+
|
|
501
|
+
# Apply updates using Pydantic model
|
|
502
|
+
update_model = MemoryUpdate(
|
|
503
|
+
content=content,
|
|
504
|
+
category=category,
|
|
505
|
+
confidence=confidence,
|
|
506
|
+
importance=importance,
|
|
507
|
+
tags=tags,
|
|
508
|
+
entities=entities,
|
|
509
|
+
metadata=metadata,
|
|
510
|
+
)
|
|
511
|
+
memory = update_model.apply_to(memory)
|
|
512
|
+
|
|
513
|
+
# Re-generate embedding if content changed
|
|
514
|
+
new_embedding = None
|
|
515
|
+
if content is not None:
|
|
516
|
+
embedding_result = await self._embedding_provider.embed(memory.content)
|
|
517
|
+
memory.embedding = embedding_result.embedding
|
|
518
|
+
new_embedding = embedding_result.embedding
|
|
519
|
+
|
|
520
|
+
# Update in database
|
|
521
|
+
await self._storage.update(memory)
|
|
522
|
+
|
|
523
|
+
# Update in retriever
|
|
524
|
+
self._retriever.update_memory(memory, new_embedding)
|
|
525
|
+
|
|
526
|
+
logger.debug(f"Updated memory {memory_id}")
|
|
527
|
+
return memory
|
|
528
|
+
|
|
529
|
+
async def delete(
|
|
530
|
+
self,
|
|
531
|
+
memory_id: str,
|
|
532
|
+
hard_delete: bool = False,
|
|
533
|
+
) -> None:
|
|
534
|
+
"""Delete a memory.
|
|
535
|
+
|
|
536
|
+
By default, performs a soft delete (archive). Use hard_delete=True
|
|
537
|
+
to permanently remove the memory.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
memory_id: ID of memory to delete.
|
|
541
|
+
hard_delete: If True, permanently delete. If False, archive.
|
|
542
|
+
|
|
543
|
+
Raises:
|
|
544
|
+
MemoryNotFoundError: If memory not found.
|
|
545
|
+
"""
|
|
546
|
+
self._ensure_initialized()
|
|
547
|
+
|
|
548
|
+
await self._storage.delete(memory_id, hard_delete=hard_delete)
|
|
549
|
+
self._retriever.remove_memory(memory_id)
|
|
550
|
+
|
|
551
|
+
action = "deleted" if hard_delete else "archived"
|
|
552
|
+
logger.debug(f"Memory {memory_id} {action}")
|
|
553
|
+
|
|
554
|
+
async def archive(self, memory_id: str) -> Memory:
|
|
555
|
+
"""Archive a memory (soft delete).
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
memory_id: ID of memory to archive.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
The archived Memory.
|
|
562
|
+
"""
|
|
563
|
+
self._ensure_initialized()
|
|
564
|
+
|
|
565
|
+
memory = await self._storage.archive(memory_id)
|
|
566
|
+
self._retriever.remove_memory(memory_id)
|
|
567
|
+
|
|
568
|
+
logger.debug(f"Archived memory {memory_id}")
|
|
569
|
+
return memory
|
|
570
|
+
|
|
571
|
+
async def unarchive(self, memory_id: str) -> Memory:
|
|
572
|
+
"""Unarchive a previously archived memory.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
memory_id: ID of memory to unarchive.
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
The unarchived Memory.
|
|
579
|
+
"""
|
|
580
|
+
self._ensure_initialized()
|
|
581
|
+
|
|
582
|
+
memory = await self._storage.unarchive(memory_id)
|
|
583
|
+
|
|
584
|
+
# Re-add to retriever
|
|
585
|
+
self._retriever.add_memory(memory, memory.embedding)
|
|
586
|
+
|
|
587
|
+
logger.debug(f"Unarchived memory {memory_id}")
|
|
588
|
+
return memory
|
|
589
|
+
|
|
590
|
+
# =========================================================================
|
|
591
|
+
# Search and Retrieval
|
|
592
|
+
# =========================================================================
|
|
593
|
+
|
|
594
|
+
async def search(
|
|
595
|
+
self,
|
|
596
|
+
query: str,
|
|
597
|
+
limit: int = 10,
|
|
598
|
+
category: MemoryCategory | None = None,
|
|
599
|
+
project: str | None = None,
|
|
600
|
+
include_archived: bool = False,
|
|
601
|
+
min_score: float = 0.0,
|
|
602
|
+
track_usage: bool = True,
|
|
603
|
+
) -> _List[SearchResult]:
|
|
604
|
+
"""Search for relevant memories.
|
|
605
|
+
|
|
606
|
+
Uses hybrid retrieval combining BM25 text search and vector
|
|
607
|
+
similarity for intelligent ranking.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
query: Search query text.
|
|
611
|
+
limit: Maximum number of results.
|
|
612
|
+
category: Filter by category.
|
|
613
|
+
project: Filter by project.
|
|
614
|
+
include_archived: Whether to include archived memories.
|
|
615
|
+
min_score: Minimum relevance score threshold.
|
|
616
|
+
track_usage: Whether to increment use counts for returned memories.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
List of SearchResults sorted by relevance.
|
|
620
|
+
"""
|
|
621
|
+
self._ensure_initialized()
|
|
622
|
+
|
|
623
|
+
# Execute search
|
|
624
|
+
results = await self._retriever.search(
|
|
625
|
+
query=query,
|
|
626
|
+
limit=limit,
|
|
627
|
+
category=category,
|
|
628
|
+
project=project,
|
|
629
|
+
include_archived=include_archived,
|
|
630
|
+
min_score=min_score,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Track last search if enabled
|
|
634
|
+
if self.config.track_last_search:
|
|
635
|
+
self._last_search = LastSearchInfo(query=query, results=results)
|
|
636
|
+
|
|
637
|
+
# Increment use counts for returned memories
|
|
638
|
+
if track_usage and results:
|
|
639
|
+
memory_ids = [r.memory.id for r in results]
|
|
640
|
+
await self._storage.increment_use_counts(memory_ids)
|
|
641
|
+
|
|
642
|
+
# Run auto-archival if enabled
|
|
643
|
+
if self.config.auto_archive_enabled and self.config.auto_archive_on_search:
|
|
644
|
+
await self.auto_archive(project=project)
|
|
645
|
+
|
|
646
|
+
logger.debug(f"Search for '{query}' returned {len(results)} results")
|
|
647
|
+
return results
|
|
648
|
+
|
|
649
|
+
async def list(
|
|
650
|
+
self,
|
|
651
|
+
project: str | None = None,
|
|
652
|
+
category: MemoryCategory | None = None,
|
|
653
|
+
scope: MemoryScope | None = None,
|
|
654
|
+
source: MemorySource | None = None,
|
|
655
|
+
include_archived: bool = False,
|
|
656
|
+
min_score: float | None = None,
|
|
657
|
+
max_score: float | None = None,
|
|
658
|
+
limit: int = 100,
|
|
659
|
+
offset: int = 0,
|
|
660
|
+
order_by: str = "created_at",
|
|
661
|
+
descending: bool = True,
|
|
662
|
+
) -> _List[Memory]:
|
|
663
|
+
"""List memories with filtering.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
project: Filter by project.
|
|
667
|
+
category: Filter by category.
|
|
668
|
+
scope: Filter by scope.
|
|
669
|
+
source: Filter by source.
|
|
670
|
+
include_archived: Whether to include archived memories.
|
|
671
|
+
min_score: Minimum outcome score filter.
|
|
672
|
+
max_score: Maximum outcome score filter.
|
|
673
|
+
limit: Maximum results to return.
|
|
674
|
+
offset: Results offset for pagination.
|
|
675
|
+
order_by: Column to order by.
|
|
676
|
+
descending: Whether to sort descending.
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
List of memories matching filters.
|
|
680
|
+
"""
|
|
681
|
+
self._ensure_initialized()
|
|
682
|
+
|
|
683
|
+
return await self._storage.list(
|
|
684
|
+
project=project,
|
|
685
|
+
category=category,
|
|
686
|
+
scope=scope,
|
|
687
|
+
source=source,
|
|
688
|
+
include_archived=include_archived,
|
|
689
|
+
min_score=min_score,
|
|
690
|
+
max_score=max_score,
|
|
691
|
+
limit=limit,
|
|
692
|
+
offset=offset,
|
|
693
|
+
order_by=order_by,
|
|
694
|
+
descending=descending,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# =========================================================================
|
|
698
|
+
# Outcome Tracking and Learning
|
|
699
|
+
# =========================================================================
|
|
700
|
+
|
|
701
|
+
async def record_outcome(
|
|
702
|
+
self,
|
|
703
|
+
memory_ids: _List[str] | str,
|
|
704
|
+
outcome: Outcome,
|
|
705
|
+
) -> _List[Memory]:
|
|
706
|
+
"""Record outcome feedback for memories.
|
|
707
|
+
|
|
708
|
+
Updates outcome scores for specified memories based on feedback.
|
|
709
|
+
This enables the system to learn from experience.
|
|
710
|
+
|
|
711
|
+
Args:
|
|
712
|
+
memory_ids: Memory ID(s) to update. Can be a single ID or list.
|
|
713
|
+
outcome: The outcome to record (WORKED, FAILED, PARTIAL).
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
List of updated memories.
|
|
717
|
+
"""
|
|
718
|
+
self._ensure_initialized()
|
|
719
|
+
|
|
720
|
+
# Normalize to list
|
|
721
|
+
if isinstance(memory_ids, str):
|
|
722
|
+
memory_ids = [memory_ids]
|
|
723
|
+
|
|
724
|
+
memories = await self._storage.record_outcomes(memory_ids, outcome)
|
|
725
|
+
|
|
726
|
+
# Update memories in retriever
|
|
727
|
+
for memory in memories:
|
|
728
|
+
self._retriever.update_memory(memory, memory.embedding)
|
|
729
|
+
|
|
730
|
+
logger.debug(f"Recorded {outcome.value} for {len(memories)} memories")
|
|
731
|
+
return memories
|
|
732
|
+
|
|
733
|
+
async def record_outcome_for_last_search(self, outcome: Outcome) -> _List[Memory]:
|
|
734
|
+
"""Record outcome for the memories from the last search.
|
|
735
|
+
|
|
736
|
+
Convenience method for recording feedback on the most recent
|
|
737
|
+
search results.
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
outcome: The outcome to record.
|
|
741
|
+
|
|
742
|
+
Returns:
|
|
743
|
+
List of updated memories.
|
|
744
|
+
|
|
745
|
+
Raises:
|
|
746
|
+
EngineError: If no last search is tracked.
|
|
747
|
+
"""
|
|
748
|
+
self._ensure_initialized()
|
|
749
|
+
|
|
750
|
+
if not self._last_search or not self._last_search.memory_ids:
|
|
751
|
+
raise EngineError("No last search results to record outcome for")
|
|
752
|
+
|
|
753
|
+
return await self.record_outcome(self._last_search.memory_ids, outcome)
|
|
754
|
+
|
|
755
|
+
async def auto_archive(
|
|
756
|
+
self,
|
|
757
|
+
project: str | None = None,
|
|
758
|
+
threshold: float | None = None,
|
|
759
|
+
) -> int:
|
|
760
|
+
"""Archive memories with low outcome scores.
|
|
761
|
+
|
|
762
|
+
Automatically archives memories that have accumulated
|
|
763
|
+
negative feedback below the threshold.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
project: Optional project filter.
|
|
767
|
+
threshold: Score threshold (default from config).
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
Number of memories archived.
|
|
771
|
+
"""
|
|
772
|
+
self._ensure_initialized()
|
|
773
|
+
|
|
774
|
+
threshold = threshold or self.config.auto_archive_threshold
|
|
775
|
+
count = await self._storage.archive_low_score_memories(
|
|
776
|
+
threshold=threshold,
|
|
777
|
+
project=project,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
if count > 0:
|
|
781
|
+
# Refresh retriever index to remove archived memories
|
|
782
|
+
await self._refresh_retriever(project=project)
|
|
783
|
+
logger.info(f"Auto-archived {count} memories below score {threshold}")
|
|
784
|
+
|
|
785
|
+
return count
|
|
786
|
+
|
|
787
|
+
# =========================================================================
|
|
788
|
+
# Context Generation
|
|
789
|
+
# =========================================================================
|
|
790
|
+
|
|
791
|
+
async def get_context(
|
|
792
|
+
self,
|
|
793
|
+
query: str | None = None,
|
|
794
|
+
project: str | None = None,
|
|
795
|
+
max_memories: int = 10,
|
|
796
|
+
category_distribution: dict[MemoryCategory, int] | None = None,
|
|
797
|
+
) -> ContextResponse:
|
|
798
|
+
"""Get formatted context for injection into prompts.
|
|
799
|
+
|
|
800
|
+
Retrieves relevant memories and formats them for use as context
|
|
801
|
+
in AI agent prompts.
|
|
802
|
+
|
|
803
|
+
Args:
|
|
804
|
+
query: Optional query to find relevant memories.
|
|
805
|
+
project: Project filter.
|
|
806
|
+
max_memories: Maximum memories to include.
|
|
807
|
+
category_distribution: Optional category -> count mapping.
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
ContextResponse with formatted context.
|
|
811
|
+
"""
|
|
812
|
+
self._ensure_initialized()
|
|
813
|
+
|
|
814
|
+
# Get relevant memories
|
|
815
|
+
if query:
|
|
816
|
+
results = await self._retriever.get_context_memories(
|
|
817
|
+
query=query,
|
|
818
|
+
project=project,
|
|
819
|
+
max_memories=max_memories,
|
|
820
|
+
category_distribution=category_distribution,
|
|
821
|
+
)
|
|
822
|
+
memories = [r.memory for r in results]
|
|
823
|
+
else:
|
|
824
|
+
# Get most recent/important memories without query
|
|
825
|
+
memories = await self._storage.list(
|
|
826
|
+
project=project,
|
|
827
|
+
include_archived=False,
|
|
828
|
+
limit=max_memories,
|
|
829
|
+
order_by="outcome_score",
|
|
830
|
+
descending=True,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# Count by category
|
|
834
|
+
categories: dict[str, int] = {}
|
|
835
|
+
for memory in memories:
|
|
836
|
+
cat_name = memory.category.value
|
|
837
|
+
categories[cat_name] = categories.get(cat_name, 0) + 1
|
|
838
|
+
|
|
839
|
+
# Get total count
|
|
840
|
+
total_count = await self._storage.count(project=project)
|
|
841
|
+
|
|
842
|
+
# Build response
|
|
843
|
+
response = ContextResponse(
|
|
844
|
+
memories=memories,
|
|
845
|
+
project=project,
|
|
846
|
+
total_count=total_count,
|
|
847
|
+
included_count=len(memories),
|
|
848
|
+
categories=categories,
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
# Generate formatted string
|
|
852
|
+
response.formatted = response.to_markdown()
|
|
853
|
+
|
|
854
|
+
return response
|
|
855
|
+
|
|
856
|
+
# =========================================================================
|
|
857
|
+
# Statistics and Health
|
|
858
|
+
# =========================================================================
|
|
859
|
+
|
|
860
|
+
async def stats(self, project: str | None = None) -> EngineStats:
|
|
861
|
+
"""Get engine statistics.
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
project: Optional project filter.
|
|
865
|
+
|
|
866
|
+
Returns:
|
|
867
|
+
EngineStats with various metrics.
|
|
868
|
+
"""
|
|
869
|
+
self._ensure_initialized()
|
|
870
|
+
|
|
871
|
+
storage_stats = await self._storage.get_stats(project=project)
|
|
872
|
+
|
|
873
|
+
return EngineStats(
|
|
874
|
+
storage_stats=storage_stats,
|
|
875
|
+
indexed_memories=self._retriever.memory_count,
|
|
876
|
+
indexed_with_embeddings=self._retriever.indexed_with_embeddings,
|
|
877
|
+
last_search_result_count=len(self._last_search.results) if self._last_search else 0,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
async def health_check(self) -> dict[str, Any]:
|
|
881
|
+
"""Check engine health.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Health status dictionary.
|
|
885
|
+
"""
|
|
886
|
+
status: dict[str, Any] = {
|
|
887
|
+
"initialized": self._initialized,
|
|
888
|
+
"storage": None,
|
|
889
|
+
"embedding_provider": None,
|
|
890
|
+
"retriever": None,
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if not self._initialized:
|
|
894
|
+
status["status"] = "not_initialized"
|
|
895
|
+
return status
|
|
896
|
+
|
|
897
|
+
# Check storage health
|
|
898
|
+
status["storage"] = await self._storage.health_check()
|
|
899
|
+
|
|
900
|
+
# Check embedding provider
|
|
901
|
+
if not self._embedding_provider.available:
|
|
902
|
+
# Not an error: retrieval still works on keyword matching alone.
|
|
903
|
+
status["embedding_provider"] = {
|
|
904
|
+
"status": "unavailable",
|
|
905
|
+
"model": self._embedding_provider.model_name,
|
|
906
|
+
"dimensions": 0,
|
|
907
|
+
"detail": (
|
|
908
|
+
"No embedding backend installed, so semantic search is off "
|
|
909
|
+
"and retrieval uses keyword matching only. Enable it with: "
|
|
910
|
+
"pip install 'runtime-memory[embedding]'"
|
|
911
|
+
),
|
|
912
|
+
}
|
|
913
|
+
else:
|
|
914
|
+
try:
|
|
915
|
+
# Quick embedding test
|
|
916
|
+
test_result = await self._embedding_provider.embed("test")
|
|
917
|
+
status["embedding_provider"] = {
|
|
918
|
+
"status": "healthy",
|
|
919
|
+
"model": self._embedding_provider.model_name,
|
|
920
|
+
"dimensions": test_result.dimensions,
|
|
921
|
+
}
|
|
922
|
+
except Exception as e:
|
|
923
|
+
status["embedding_provider"] = {
|
|
924
|
+
"status": "unhealthy",
|
|
925
|
+
"error": str(e),
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
# Check retriever
|
|
929
|
+
status["retriever"] = {
|
|
930
|
+
"status": "healthy",
|
|
931
|
+
"indexed_memories": self._retriever.memory_count,
|
|
932
|
+
"with_embeddings": self._retriever.indexed_with_embeddings,
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
# Overall status
|
|
936
|
+
storage_healthy = status["storage"].get("status") == "healthy"
|
|
937
|
+
embedding_healthy = status["embedding_provider"].get("status") == "healthy"
|
|
938
|
+
status["status"] = "healthy" if (storage_healthy and embedding_healthy) else "degraded"
|
|
939
|
+
|
|
940
|
+
return status
|
|
941
|
+
|
|
942
|
+
# =========================================================================
|
|
943
|
+
# Internal Helpers
|
|
944
|
+
# =========================================================================
|
|
945
|
+
|
|
946
|
+
async def _load_memories_to_retriever(self) -> None:
|
|
947
|
+
"""Load existing memories from storage into the retriever."""
|
|
948
|
+
# Load all non-archived memories
|
|
949
|
+
memories = await self._storage.list(
|
|
950
|
+
include_archived=False,
|
|
951
|
+
limit=10000, # Load up to 10K memories
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
for memory in memories:
|
|
955
|
+
self._retriever.add_memory(memory, memory.embedding)
|
|
956
|
+
|
|
957
|
+
logger.info(f"Loaded {len(memories)} memories into retriever")
|
|
958
|
+
|
|
959
|
+
async def _refresh_retriever(self, project: str | None = None) -> None:
|
|
960
|
+
"""Refresh the retriever index from storage.
|
|
961
|
+
|
|
962
|
+
Args:
|
|
963
|
+
project: Optional project filter.
|
|
964
|
+
"""
|
|
965
|
+
# Clear and reload
|
|
966
|
+
self._retriever.clear()
|
|
967
|
+
await self._load_memories_to_retriever()
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
# Factory function for easy creation
|
|
971
|
+
async def create_engine(
|
|
972
|
+
db_path: str | Path | None = None,
|
|
973
|
+
embedding_provider: str = "local",
|
|
974
|
+
**kwargs: Any,
|
|
975
|
+
) -> MemoryEngine:
|
|
976
|
+
"""Create and initialize a Memory Engine.
|
|
977
|
+
|
|
978
|
+
Convenience factory function that creates and initializes an engine
|
|
979
|
+
in one call.
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
db_path: Path to database file.
|
|
983
|
+
embedding_provider: Embedding provider type.
|
|
984
|
+
**kwargs: Additional EngineConfig parameters.
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
Initialized MemoryEngine.
|
|
988
|
+
|
|
989
|
+
Example:
|
|
990
|
+
```python
|
|
991
|
+
engine = await create_engine(
|
|
992
|
+
db_path="~/.my-app/memories.db",
|
|
993
|
+
embedding_provider="local",
|
|
994
|
+
)
|
|
995
|
+
```
|
|
996
|
+
"""
|
|
997
|
+
config_kwargs: dict[str, Any] = {
|
|
998
|
+
"embedding_provider": embedding_provider,
|
|
999
|
+
**kwargs,
|
|
1000
|
+
}
|
|
1001
|
+
if db_path:
|
|
1002
|
+
config_kwargs["db_path"] = db_path
|
|
1003
|
+
|
|
1004
|
+
config = EngineConfig(**config_kwargs)
|
|
1005
|
+
engine = MemoryEngine(config=config)
|
|
1006
|
+
await engine.initialize()
|
|
1007
|
+
return engine
|