mnemosyne-memory 1.9.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.
- hermes_memory_provider/__init__.py +445 -0
- hermes_memory_provider/cli.py +128 -0
- hermes_plugin/__init__.py +228 -0
- hermes_plugin/tools.py +441 -0
- mnemosyne/__init__.py +38 -0
- mnemosyne/cli.py +611 -0
- mnemosyne/core/__init__.py +1 -0
- mnemosyne/core/aaak.py +212 -0
- mnemosyne/core/beam.py +1161 -0
- mnemosyne/core/cost_log.py +78 -0
- mnemosyne/core/embeddings.py +159 -0
- mnemosyne/core/local_llm.py +192 -0
- mnemosyne/core/memory.py +469 -0
- mnemosyne/core/token_counter.py +72 -0
- mnemosyne/core/triples.py +192 -0
- mnemosyne/dr/__init__.py +1 -0
- mnemosyne/dr/recovery.py +314 -0
- mnemosyne/install.py +224 -0
- mnemosyne_memory-1.9.0.dist-info/METADATA +460 -0
- mnemosyne_memory-1.9.0.dist-info/RECORD +24 -0
- mnemosyne_memory-1.9.0.dist-info/WHEEL +5 -0
- mnemosyne_memory-1.9.0.dist-info/entry_points.txt +3 -0
- mnemosyne_memory-1.9.0.dist-info/licenses/LICENSE +21 -0
- mnemosyne_memory-1.9.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Mnemosyne Memory Provider for Hermes.
|
|
2
|
+
|
|
3
|
+
Deploy to Hermes via:
|
|
4
|
+
ln -s /path/to/mnemosyne/hermes_memory_provider ~/.hermes/plugins/mnemosyne
|
|
5
|
+
|
|
6
|
+
Then set in ~/.hermes/config.yaml:
|
|
7
|
+
memory:
|
|
8
|
+
provider: mnemosyne
|
|
9
|
+
|
|
10
|
+
This gives Mnemosyne first-class MemoryProvider integration (system prompt
|
|
11
|
+
injection, pre-turn prefetch, post-turn sync, tool dispatch) while remaining
|
|
12
|
+
a standalone plugin deployed through the plugin system.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
# Ensure mnemosyne core is importable from this directory
|
|
25
|
+
_mnemosyne_root = Path(__file__).resolve().parent.parent
|
|
26
|
+
if str(_mnemosyne_root) not in sys.path:
|
|
27
|
+
sys.path.insert(0, str(_mnemosyne_root))
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Lazy imports — fail gracefully if mnemosyne core is missing
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def _get_beam_class():
|
|
36
|
+
from mnemosyne.core.beam import BeamMemory
|
|
37
|
+
return BeamMemory
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_triple_module():
|
|
41
|
+
from mnemosyne.core.triples import add_triple, query_triples
|
|
42
|
+
return add_triple, query_triples
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Tool schemas
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
REMEMBER_SCHEMA = {
|
|
50
|
+
"name": "mnemosyne_remember",
|
|
51
|
+
"description": (
|
|
52
|
+
"Store a durable memory in Mnemosyne. Use for ANY fact, preference, "
|
|
53
|
+
"insight, or context that should persist across sessions. Higher importance "
|
|
54
|
+
"(0.0-1.0) surfaces the memory more often. Use scope='global' for user-level "
|
|
55
|
+
"facts; scope='session' for conversation-specific context. Use valid_until "
|
|
56
|
+
"(ISO date YYYY-MM-DD) for time-bound facts."
|
|
57
|
+
),
|
|
58
|
+
"parameters": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"content": {"type": "string", "description": "The memory content to store."},
|
|
62
|
+
"importance": {"type": "number", "description": "Importance 0.0-1.0. Default 0.5.", "default": 0.5},
|
|
63
|
+
"source": {"type": "string", "description": "Source tag: preference, fact, insight, task, etc.", "default": "user"},
|
|
64
|
+
"scope": {"type": "string", "description": "'session' (default) or 'global'.", "default": "session"},
|
|
65
|
+
"valid_until": {"type": "string", "description": "Optional expiry date YYYY-MM-DD.", "default": ""},
|
|
66
|
+
},
|
|
67
|
+
"required": ["content"],
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
RECALL_SCHEMA = {
|
|
72
|
+
"name": "mnemosyne_recall",
|
|
73
|
+
"description": (
|
|
74
|
+
"Search Mnemosyne for relevant memories. Uses hybrid ranking: 50% vector "
|
|
75
|
+
"similarity + 30% FTS5 text rank + 20% importance. Returns ranked results."
|
|
76
|
+
),
|
|
77
|
+
"parameters": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"query": {"type": "string", "description": "Natural language query."},
|
|
81
|
+
"limit": {"type": "integer", "description": "Max results. Default 5.", "default": 5},
|
|
82
|
+
},
|
|
83
|
+
"required": ["query"],
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
SLEEP_SCHEMA = {
|
|
88
|
+
"name": "mnemosyne_sleep",
|
|
89
|
+
"description": (
|
|
90
|
+
"Run the Mnemosyne consolidation cycle. Compresses old working memories "
|
|
91
|
+
"into episodic summaries. Call after long sessions or when memory feels stale."
|
|
92
|
+
),
|
|
93
|
+
"parameters": {"type": "object", "properties": {}},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
STATS_SCHEMA = {
|
|
97
|
+
"name": "mnemosyne_stats",
|
|
98
|
+
"description": "Return Mnemosyne memory statistics: working count, episodic count, BEAM tiers.",
|
|
99
|
+
"parameters": {"type": "object", "properties": {}},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
INVALIDATE_SCHEMA = {
|
|
103
|
+
"name": "mnemosyne_invalidate",
|
|
104
|
+
"description": (
|
|
105
|
+
"Mark a memory as expired or superseded. Provide memory_id from recall results. "
|
|
106
|
+
"Optionally provide replacement_id to chain old → new."
|
|
107
|
+
),
|
|
108
|
+
"parameters": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"properties": {
|
|
111
|
+
"memory_id": {"type": "string", "description": "ID of memory to invalidate."},
|
|
112
|
+
"replacement_id": {"type": "string", "description": "Optional new memory that replaces this one.", "default": ""},
|
|
113
|
+
},
|
|
114
|
+
"required": ["memory_id"],
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
TRIPLE_ADD_SCHEMA = {
|
|
119
|
+
"name": "mnemosyne_triple_add",
|
|
120
|
+
"description": (
|
|
121
|
+
"Add a temporal fact triple (subject, predicate, object) to the knowledge graph. "
|
|
122
|
+
"Example: ('user', 'prefers', 'neovim'). Use for structured relationships."
|
|
123
|
+
),
|
|
124
|
+
"parameters": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"subject": {"type": "string"},
|
|
128
|
+
"predicate": {"type": "string"},
|
|
129
|
+
"object": {"type": "string"},
|
|
130
|
+
"valid_from": {"type": "string", "description": "ISO date YYYY-MM-DD", "default": ""},
|
|
131
|
+
},
|
|
132
|
+
"required": ["subject", "predicate", "object"],
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
TRIPLE_QUERY_SCHEMA = {
|
|
137
|
+
"name": "mnemosyne_triple_query",
|
|
138
|
+
"description": "Query the temporal knowledge graph for facts matching subject/predicate/object patterns.",
|
|
139
|
+
"parameters": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"subject": {"type": "string", "default": ""},
|
|
143
|
+
"predicate": {"type": "string", "default": ""},
|
|
144
|
+
"object": {"type": "string", "default": ""},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ALL_TOOL_SCHEMAS = [
|
|
150
|
+
REMEMBER_SCHEMA, RECALL_SCHEMA, SLEEP_SCHEMA, STATS_SCHEMA,
|
|
151
|
+
INVALIDATE_SCHEMA, TRIPLE_ADD_SCHEMA, TRIPLE_QUERY_SCHEMA,
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# MemoryProvider implementation
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
from agent.memory_provider import MemoryProvider
|
|
161
|
+
except ImportError:
|
|
162
|
+
# Graceful fallback if ABC not available (shouldn't happen in practice)
|
|
163
|
+
MemoryProvider = object # type: ignore
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class MnemosyneMemoryProvider(MemoryProvider):
|
|
167
|
+
"""Mnemosyne native memory — local SQLite with vector + FTS5 hybrid search."""
|
|
168
|
+
|
|
169
|
+
def __init__(self):
|
|
170
|
+
self._beam: Optional[Any] = None
|
|
171
|
+
self._session_id = "hermes_default"
|
|
172
|
+
self._hermes_home = ""
|
|
173
|
+
self._platform = "cli"
|
|
174
|
+
self._agent_context = "primary"
|
|
175
|
+
self._turn_count = 0
|
|
176
|
+
self._auto_sleep_threshold = 50
|
|
177
|
+
self._auto_sleep_enabled = True
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def name(self) -> str:
|
|
181
|
+
return "mnemosyne"
|
|
182
|
+
|
|
183
|
+
def is_available(self) -> bool:
|
|
184
|
+
"""Check if Mnemosyne core is importable. No network calls."""
|
|
185
|
+
try:
|
|
186
|
+
_get_beam_class()
|
|
187
|
+
return True
|
|
188
|
+
except Exception:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def get_config_schema(self) -> List[Dict[str, Any]]:
|
|
192
|
+
return [
|
|
193
|
+
{"key": "auto_sleep", "description": "Auto-run sleep() when working memory exceeds threshold", "default": True},
|
|
194
|
+
{"key": "sleep_threshold", "description": "Working memory count before auto-sleep triggers", "default": 50},
|
|
195
|
+
{"key": "vector_type", "description": "Vector storage type", "choices": ["float32", "int8", "bit"], "default": "float32"},
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
def save_config(self, values: Dict[str, Any], hermes_home: str) -> None:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
def initialize(self, session_id: str, **kwargs) -> None:
|
|
202
|
+
"""Initialize Mnemosyne beam for this session."""
|
|
203
|
+
self._agent_context = kwargs.get("agent_context", "primary")
|
|
204
|
+
self._platform = kwargs.get("platform", "cli")
|
|
205
|
+
self._hermes_home = kwargs.get("hermes_home", "")
|
|
206
|
+
|
|
207
|
+
if self._agent_context in ("cron", "flush", "subagent"):
|
|
208
|
+
logger.debug("Mnemosyne skipped: non-primary context=%s", self._agent_context)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
self._session_id = f"hermes_{session_id}"
|
|
212
|
+
|
|
213
|
+
# Read config
|
|
214
|
+
try:
|
|
215
|
+
from hermes_cli.config import load_config
|
|
216
|
+
cfg = load_config()
|
|
217
|
+
mn_cfg = cfg.get("memory", {}).get("mnemosyne", {})
|
|
218
|
+
self._auto_sleep_enabled = mn_cfg.get("auto_sleep", True)
|
|
219
|
+
self._auto_sleep_threshold = mn_cfg.get("sleep_threshold", 50)
|
|
220
|
+
vec_type = mn_cfg.get("vector_type", "float32")
|
|
221
|
+
if vec_type:
|
|
222
|
+
os.environ.setdefault("MNEMOSYNE_VEC_TYPE", vec_type)
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
BeamMemory = _get_beam_class()
|
|
228
|
+
self._beam = BeamMemory(session_id=self._session_id)
|
|
229
|
+
logger.info("Mnemosyne initialized: session=%s", self._session_id)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.warning("Mnemosyne init failed: %s", e)
|
|
232
|
+
self._beam = None
|
|
233
|
+
|
|
234
|
+
def system_prompt_block(self) -> str:
|
|
235
|
+
if not self._beam:
|
|
236
|
+
return ""
|
|
237
|
+
return (
|
|
238
|
+
"# Mnemosyne Memory\n"
|
|
239
|
+
"Active (native local memory). Use mnemosyne_remember to store ANY "
|
|
240
|
+
"durable fact, preference, or insight. Use mnemosyne_recall to search. "
|
|
241
|
+
"The legacy memory tool is deprecated for durable storage — Mnemosyne is primary."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def prefetch(self, query: str, *, session_id: str = "") -> str:
|
|
245
|
+
"""Recall relevant context via Mnemosyne hybrid search."""
|
|
246
|
+
if not self._beam or self._agent_context in ("cron", "flush", "subagent"):
|
|
247
|
+
return ""
|
|
248
|
+
try:
|
|
249
|
+
results = self._beam.recall(query, top_k=8)
|
|
250
|
+
if not results:
|
|
251
|
+
return ""
|
|
252
|
+
lines = ["## Mnemosyne Context"]
|
|
253
|
+
for r in results:
|
|
254
|
+
content = r.get("content", "")[:200]
|
|
255
|
+
if len(r.get("content", "")) > 200:
|
|
256
|
+
content += "..."
|
|
257
|
+
ts = r.get("timestamp", "")[:16] if r.get("timestamp") else ""
|
|
258
|
+
imp = r.get("importance", 0.0)
|
|
259
|
+
lines.append(f" [{ts}] (importance {imp:.2f}) {content}")
|
|
260
|
+
return "\n".join(lines)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.debug("Mnemosyne prefetch failed: %s", e)
|
|
263
|
+
return ""
|
|
264
|
+
|
|
265
|
+
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
|
|
269
|
+
"""Persist the turn to Mnemosyne episodic memory."""
|
|
270
|
+
if not self._beam or self._agent_context in ("cron", "flush", "subagent"):
|
|
271
|
+
return
|
|
272
|
+
try:
|
|
273
|
+
if user_content and len(user_content) > 5:
|
|
274
|
+
self._beam.remember(
|
|
275
|
+
content=f"[USER] {user_content[:500]}",
|
|
276
|
+
source="conversation",
|
|
277
|
+
importance=0.3,
|
|
278
|
+
)
|
|
279
|
+
if assistant_content and len(assistant_content) > 10:
|
|
280
|
+
self._beam.remember(
|
|
281
|
+
content=f"[ASSISTANT] {assistant_content[:800]}",
|
|
282
|
+
source="conversation",
|
|
283
|
+
importance=0.2,
|
|
284
|
+
)
|
|
285
|
+
self._turn_count += 1
|
|
286
|
+
if self._auto_sleep_enabled and self._turn_count % 10 == 0:
|
|
287
|
+
self._maybe_auto_sleep()
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.debug("Mnemosyne sync_turn failed: %s", e)
|
|
290
|
+
|
|
291
|
+
def _maybe_auto_sleep(self) -> None:
|
|
292
|
+
try:
|
|
293
|
+
stats = self._beam.get_working_stats()
|
|
294
|
+
working = stats.get("count", 0)
|
|
295
|
+
if working > self._auto_sleep_threshold:
|
|
296
|
+
logger.info("Mnemosyne auto-sleep: working=%d > threshold=%d", working, self._auto_sleep_threshold)
|
|
297
|
+
self._beam.sleep()
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
302
|
+
"""Return tool schemas — static, do not depend on initialization state."""
|
|
303
|
+
return list(ALL_TOOL_SCHEMAS)
|
|
304
|
+
|
|
305
|
+
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
|
|
306
|
+
if not self._beam:
|
|
307
|
+
return json.dumps({"error": "Mnemosyne not initialized"})
|
|
308
|
+
try:
|
|
309
|
+
if tool_name == "mnemosyne_remember":
|
|
310
|
+
return self._handle_remember(args)
|
|
311
|
+
elif tool_name == "mnemosyne_recall":
|
|
312
|
+
return self._handle_recall(args)
|
|
313
|
+
elif tool_name == "mnemosyne_sleep":
|
|
314
|
+
return self._handle_sleep(args)
|
|
315
|
+
elif tool_name == "mnemosyne_stats":
|
|
316
|
+
return self._handle_stats(args)
|
|
317
|
+
elif tool_name == "mnemosyne_invalidate":
|
|
318
|
+
return self._handle_invalidate(args)
|
|
319
|
+
elif tool_name == "mnemosyne_triple_add":
|
|
320
|
+
return self._handle_triple_add(args)
|
|
321
|
+
elif tool_name == "mnemosyne_triple_query":
|
|
322
|
+
return self._handle_triple_query(args)
|
|
323
|
+
else:
|
|
324
|
+
return json.dumps({"error": f"Unknown Mnemosyne tool: {tool_name}"})
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error("Mnemosyne tool %s failed: %s", tool_name, e)
|
|
327
|
+
return json.dumps({"error": f"Mnemosyne tool '{tool_name}' failed: {e}"})
|
|
328
|
+
|
|
329
|
+
def _handle_remember(self, args: Dict[str, Any]) -> str:
|
|
330
|
+
content = args.get("content", "")
|
|
331
|
+
importance = float(args.get("importance", 0.5))
|
|
332
|
+
source = args.get("source", "user")
|
|
333
|
+
scope = args.get("scope", "session")
|
|
334
|
+
valid_until = args.get("valid_until", None) or None
|
|
335
|
+
if not content:
|
|
336
|
+
return json.dumps({"error": "content is required"})
|
|
337
|
+
memory_id = self._beam.remember(
|
|
338
|
+
content=content,
|
|
339
|
+
importance=importance,
|
|
340
|
+
source=source,
|
|
341
|
+
scope=scope,
|
|
342
|
+
valid_until=valid_until,
|
|
343
|
+
)
|
|
344
|
+
return json.dumps({"status": "stored", "memory_id": memory_id, "content_preview": content[:100]})
|
|
345
|
+
|
|
346
|
+
def _handle_recall(self, args: Dict[str, Any]) -> str:
|
|
347
|
+
query = args.get("query", "")
|
|
348
|
+
top_k = int(args.get("limit", 5))
|
|
349
|
+
if not query:
|
|
350
|
+
return json.dumps({"error": "query is required"})
|
|
351
|
+
results = self._beam.recall(query, top_k=top_k)
|
|
352
|
+
return json.dumps({"query": query, "count": len(results), "results": results})
|
|
353
|
+
|
|
354
|
+
def _handle_sleep(self, args: Dict[str, Any]) -> str:
|
|
355
|
+
self._beam.sleep()
|
|
356
|
+
working = self._beam.get_working_stats()
|
|
357
|
+
episodic = self._beam.get_episodic_stats()
|
|
358
|
+
return json.dumps({"status": "consolidated", "working": working, "episodic": episodic})
|
|
359
|
+
|
|
360
|
+
def _handle_stats(self, args: Dict[str, Any]) -> str:
|
|
361
|
+
working = self._beam.get_working_stats()
|
|
362
|
+
episodic = self._beam.get_episodic_stats()
|
|
363
|
+
return json.dumps({"provider": "mnemosyne", "session_id": self._session_id, "working": working, "episodic": episodic})
|
|
364
|
+
|
|
365
|
+
def _handle_invalidate(self, args: Dict[str, Any]) -> str:
|
|
366
|
+
memory_id = args.get("memory_id", "")
|
|
367
|
+
replacement_id = args.get("replacement_id", None) or None
|
|
368
|
+
if not memory_id:
|
|
369
|
+
return json.dumps({"error": "memory_id is required"})
|
|
370
|
+
self._beam.invalidate(memory_id, replacement_id=replacement_id if replacement_id else None)
|
|
371
|
+
return json.dumps({"status": "invalidated", "memory_id": memory_id})
|
|
372
|
+
|
|
373
|
+
def _handle_triple_add(self, args: Dict[str, Any]) -> str:
|
|
374
|
+
subject = args.get("subject", "")
|
|
375
|
+
predicate = args.get("predicate", "")
|
|
376
|
+
obj = args.get("object", "")
|
|
377
|
+
valid_from = args.get("valid_from", None) or None
|
|
378
|
+
if not all([subject, predicate, obj]):
|
|
379
|
+
return json.dumps({"error": "subject, predicate, and object are required"})
|
|
380
|
+
add_triple, _ = _get_triple_module()
|
|
381
|
+
triple_id = add_triple(subject, predicate, obj, valid_from=valid_from)
|
|
382
|
+
return json.dumps({"status": "stored", "triple_id": triple_id})
|
|
383
|
+
|
|
384
|
+
def _handle_triple_query(self, args: Dict[str, Any]) -> str:
|
|
385
|
+
subject = args.get("subject", "") or None
|
|
386
|
+
predicate = args.get("predicate", "") or None
|
|
387
|
+
obj = args.get("object", "") or None
|
|
388
|
+
_, query_triples = _get_triple_module()
|
|
389
|
+
results = query_triples(subject=subject, predicate=predicate, object=obj)
|
|
390
|
+
return json.dumps({"count": len(results), "results": results})
|
|
391
|
+
|
|
392
|
+
def on_turn_start(self, turn_number: int, message: str, **kwargs) -> None:
|
|
393
|
+
self._turn_count = turn_number
|
|
394
|
+
|
|
395
|
+
def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
|
|
396
|
+
if not self._beam:
|
|
397
|
+
return
|
|
398
|
+
try:
|
|
399
|
+
logger.info("Mnemosyne session end — running consolidation")
|
|
400
|
+
self._beam.sleep()
|
|
401
|
+
except Exception as e:
|
|
402
|
+
logger.debug("Mnemosyne session-end sleep failed: %s", e)
|
|
403
|
+
|
|
404
|
+
def on_memory_write(self, action: str, target: str, content: str) -> None:
|
|
405
|
+
if not self._beam or action not in ("add", "replace"):
|
|
406
|
+
return
|
|
407
|
+
try:
|
|
408
|
+
scope = "global" if target == "user" else "session"
|
|
409
|
+
self._beam.remember(
|
|
410
|
+
content=content,
|
|
411
|
+
source=f"builtin_memory_{target}",
|
|
412
|
+
importance=0.7 if target == "user" else 0.5,
|
|
413
|
+
scope=scope,
|
|
414
|
+
)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.debug("Mnemosyne mirror write failed: %s", e)
|
|
417
|
+
|
|
418
|
+
def shutdown(self) -> None:
|
|
419
|
+
self._beam = None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
# Plugin registration (used when loaded via plugins.memory discovery)
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
def register_memory_provider(ctx):
|
|
427
|
+
"""Called by Hermes memory provider discovery system."""
|
|
428
|
+
provider = MnemosyneMemoryProvider()
|
|
429
|
+
ctx.register_memory_provider(provider)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
# Plugin registration (used when loaded via Hermes plugin system)
|
|
434
|
+
# ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
def register(ctx):
|
|
437
|
+
"""Called by Hermes plugin loader to register CLI commands and tools."""
|
|
438
|
+
from .cli import register_cli, mnemosyne_command
|
|
439
|
+
ctx.register_cli_command(
|
|
440
|
+
name="mnemosyne",
|
|
441
|
+
help="Manage Mnemosyne local memory",
|
|
442
|
+
description="Inspect, consolidate, and manage Mnemosyne native memory.",
|
|
443
|
+
setup_fn=register_cli,
|
|
444
|
+
handler_fn=mnemosyne_command,
|
|
445
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""CLI commands for Mnemosyne memory provider.
|
|
2
|
+
|
|
3
|
+
Available via: hermes mnemosyne <subcommand>
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_mnemosyne_root = Path(__file__).resolve().parent.parent
|
|
13
|
+
if str(_mnemosyne_root) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(_mnemosyne_root))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_cli(subparser):
|
|
18
|
+
"""Register CLI subcommands for ``hermes mnemosyne``."""
|
|
19
|
+
mn_cmds = subparser.add_subparsers(dest="mnemosyne_cmd")
|
|
20
|
+
|
|
21
|
+
mn_cmds.add_parser("stats", help="Show memory statistics")
|
|
22
|
+
mn_cmds.add_parser("sleep", help="Run consolidation cycle")
|
|
23
|
+
mn_cmds.add_parser("version", help="Show Mnemosyne version")
|
|
24
|
+
|
|
25
|
+
inspect_cmd = mn_cmds.add_parser("inspect", help="Search memories")
|
|
26
|
+
inspect_cmd.add_argument("query", nargs="?", default="", help="Search query")
|
|
27
|
+
inspect_cmd.add_argument("--limit", type=int, default=10, help="Max results")
|
|
28
|
+
|
|
29
|
+
mn_cmds.add_parser("clear", help="Clear scratchpad")
|
|
30
|
+
|
|
31
|
+
export_cmd = mn_cmds.add_parser("export", help="Export all memories to a JSON file")
|
|
32
|
+
export_cmd.add_argument("--output", "-o", type=str, required=True, help="Output JSON file path")
|
|
33
|
+
|
|
34
|
+
import_cmd = mn_cmds.add_parser("import", help="Import memories from a JSON file")
|
|
35
|
+
import_cmd.add_argument("--input", "-i", type=str, required=True, help="Input JSON file path")
|
|
36
|
+
import_cmd.add_argument("--force", action="store_true", help="Overwrite existing records")
|
|
37
|
+
|
|
38
|
+
subparser.set_defaults(func=mnemosyne_command)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def mnemosyne_command(args):
|
|
42
|
+
"""Dispatch ``hermes mnemosyne <subcommand>``."""
|
|
43
|
+
cmd = getattr(args, "mnemosyne_cmd", None)
|
|
44
|
+
if not cmd:
|
|
45
|
+
print("Usage: hermes mnemosyne {stats|sleep|inspect|clear}")
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from mnemosyne.core.beam import BeamMemory
|
|
50
|
+
beam = BeamMemory(session_id="hermes_default")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"Error: Mnemosyne not available: {e}")
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
if cmd == "stats":
|
|
56
|
+
working = beam.get_working_stats()
|
|
57
|
+
episodic = beam.get_episodic_stats()
|
|
58
|
+
print(json.dumps({"working": working, "episodic": episodic}, indent=2))
|
|
59
|
+
|
|
60
|
+
elif cmd == "version":
|
|
61
|
+
from mnemosyne import __version__, __author__
|
|
62
|
+
print(f"Mnemosyne {__version__} by {__author__}")
|
|
63
|
+
|
|
64
|
+
elif cmd == "sleep":
|
|
65
|
+
beam.sleep()
|
|
66
|
+
working = beam.get_working_stats()
|
|
67
|
+
episodic = beam.get_episodic_stats()
|
|
68
|
+
print(f"Consolidation complete. Working: {working.get('count', 0)}, Episodic: {episodic.get('count', 0)}")
|
|
69
|
+
|
|
70
|
+
elif cmd == "inspect":
|
|
71
|
+
query = getattr(args, "query", "") or ""
|
|
72
|
+
limit = getattr(args, "limit", 10)
|
|
73
|
+
if not query:
|
|
74
|
+
query = input("Search query: ")
|
|
75
|
+
results = beam.recall(query, top_k=limit)
|
|
76
|
+
print(f"Results for '{query}': {len(results)}")
|
|
77
|
+
for i, r in enumerate(results, 1):
|
|
78
|
+
content = r.get("content", "")[:120]
|
|
79
|
+
imp = r.get("importance", 0.0)
|
|
80
|
+
print(f" {i}. [{imp:.2f}] {content}")
|
|
81
|
+
|
|
82
|
+
elif cmd == "clear":
|
|
83
|
+
confirm = input("Clear scratchpad? This cannot be undone. [y/N]: ")
|
|
84
|
+
if confirm.lower() in ("y", "yes"):
|
|
85
|
+
beam.scratchpad_clear()
|
|
86
|
+
print("Scratchpad cleared.")
|
|
87
|
+
else:
|
|
88
|
+
print("Cancelled.")
|
|
89
|
+
|
|
90
|
+
elif cmd == "export":
|
|
91
|
+
output_path = getattr(args, "output", None)
|
|
92
|
+
if not output_path:
|
|
93
|
+
print("Usage: hermes mnemosyne export --output <path>")
|
|
94
|
+
return 1
|
|
95
|
+
try:
|
|
96
|
+
from mnemosyne.core.memory import Mnemosyne
|
|
97
|
+
mem = Mnemosyne(session_id="hermes_default")
|
|
98
|
+
result = mem.export_to_file(output_path)
|
|
99
|
+
print(f"Exported {result['working_memory_count']} working, {result['episodic_memory_count']} episodic, {result['legacy_memories_count']} legacy, {result['triples_count']} triples to {output_path}")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"Export failed: {e}")
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
elif cmd == "import":
|
|
105
|
+
input_path = getattr(args, "input", None)
|
|
106
|
+
force = getattr(args, "force", False)
|
|
107
|
+
if not input_path:
|
|
108
|
+
print("Usage: hermes mnemosyne import --input <path> [--force]")
|
|
109
|
+
return 1
|
|
110
|
+
try:
|
|
111
|
+
from mnemosyne.core.memory import Mnemosyne
|
|
112
|
+
mem = Mnemosyne(session_id="hermes_default")
|
|
113
|
+
stats = mem.import_from_file(input_path, force=force)
|
|
114
|
+
beam_stats = stats.get("beam", {})
|
|
115
|
+
legacy_stats = stats.get("legacy", {})
|
|
116
|
+
triples_stats = stats.get("triples", {})
|
|
117
|
+
print(f"Import complete:")
|
|
118
|
+
print(f" Working: +{beam_stats.get('working_memory', {}).get('inserted', 0)}")
|
|
119
|
+
print(f" Episodic: +{beam_stats.get('episodic_memory', {}).get('inserted', 0)}")
|
|
120
|
+
print(f" Legacy: +{legacy_stats.get('inserted', 0)}")
|
|
121
|
+
print(f" Triples: +{triples_stats.get('inserted', 0)}")
|
|
122
|
+
if force:
|
|
123
|
+
print(f" (force mode: overwrites applied)")
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"Import failed: {e}")
|
|
126
|
+
return 1
|
|
127
|
+
|
|
128
|
+
return 0
|