AthelaPerk commited on
Commit
3a3d9fa
Β·
verified Β·
1 Parent(s): 2f6a62d

v4: SLM-inspired architecture with three-tier memory, neural links, utility predictor

Browse files
Files changed (1) hide show
  1. app.py +465 -366
app.py CHANGED
@@ -1,405 +1,504 @@
1
  """
2
- Mnemo v2 - Interactive Demo with Metadata Filtering
3
- Enhanced memory system with filtering by tags, importance, and dates.
 
 
 
 
 
 
4
  """
5
 
6
  import gradio as gr
7
  import time
8
- from datetime import datetime
9
- from typing import List
10
  import numpy as np
 
 
 
 
 
11
 
12
- from mnemo_core import MnemoV2, MemoryFilter, compute_embedding, compute_embeddings_batch
 
 
13
 
14
- # Global persistent state
15
- MNEMO = MnemoV2()
 
 
16
 
17
- def format_time(timestamp: float) -> str:
18
- return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
19
 
20
- def get_stats_text(user_id: str = "default") -> str:
21
- stats = MNEMO.get_stats(user_id=user_id or "default")
22
- tags_str = ", ".join(stats['tags'][:10]) if stats['tags'] else "none"
23
- return f"""**System Stats**
24
- - Total memories: {stats['total_memories']}
25
- - User memories: {stats['user_memory_count']}
26
- - Unique tags: {stats['unique_tags']}
27
- - Tags: {tags_str}
28
- - Searches: {stats['total_searches']} (filtered: {stats['filtered_searches']})"""
29
 
30
 
31
- def get_available_tags(user_id: str) -> List[str]:
32
- """Get available tags for dropdown."""
33
- return MNEMO.get_all_tags(user_id=user_id or "default")
 
 
 
 
 
 
 
34
 
 
 
 
 
 
 
35
 
36
- def add_memory(content: str, importance: float, tags: str, user_id: str):
37
- if not content.strip():
38
- return "❌ Please enter content", get_stats_text(user_id), gr.update(choices=get_available_tags(user_id))
39
-
40
- tags_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
41
- embedding = compute_embedding(content)
42
-
43
- result = MNEMO.add_with_embedding(
44
- content=content,
45
- embedding=embedding,
46
- user_id=user_id or "default",
47
- importance=importance,
48
- tags=tags_list
49
- )
50
-
51
- op = result['operation']
52
- icon = {"ADD": "βœ…", "UPDATE": "πŸ”„", "NOOP": "⚠️"}.get(op, "❓")
53
-
54
- return (
55
- f"{icon} {result['message']} (ID: {result['id']})",
56
- get_stats_text(user_id),
57
- gr.update(choices=get_available_tags(user_id))
58
- )
59
-
60
-
61
- def search_memories(
62
- query: str,
63
- k: int,
64
- min_score: float,
65
- user_id: str,
66
- filter_tags_include: List[str],
67
- filter_tags_exclude: List[str],
68
- filter_min_importance: float,
69
- filter_content_contains: str
70
- ):
71
- """Search memories with optional filters."""
72
- if not query.strip():
73
- return "❌ Please enter a search query"
74
-
75
- start = time.time()
76
- query_embedding = compute_embedding(query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- # Build filter
79
- mem_filter = None
80
- if any([filter_tags_include, filter_tags_exclude,
81
- filter_min_importance > 0, filter_content_contains]):
82
- mem_filter = MemoryFilter(
83
- tags_include_any=filter_tags_include if filter_tags_include else None,
84
- tags_exclude=filter_tags_exclude if filter_tags_exclude else None,
85
- min_importance=filter_min_importance if filter_min_importance > 0 else None,
86
- content_contains=filter_content_contains if filter_content_contains else None
 
 
 
 
 
 
 
 
 
87
  )
 
 
 
 
 
 
 
 
 
 
88
 
89
- results = MNEMO.search_with_embedding(
90
- query_embedding=query_embedding,
91
- user_id=user_id or "default",
92
- k=k,
93
- min_score=min_score,
94
- filter=mem_filter
95
- )
96
-
97
- latency = (time.time() - start) * 1000
98
- filter_str = " (filtered)" if mem_filter else ""
99
-
100
- if not results:
101
- return f"No results found{filter_str} (searched in {latency:.1f}ms)"
102
-
103
- output = f"**Found {len(results)} results{filter_str} in {latency:.1f}ms**\n\n"
 
 
 
104
 
105
- for i, r in enumerate(results, 1):
106
- output += f"### {i}. [{r['id']}]\n"
107
- output += f"**Content:** {r['content']}\n\n"
108
- output += f"- Relevance: `{r['relevance_score']:.3f}`\n"
109
- output += f"- Similarity: `{r['similarity']:.3f}`\n"
110
- output += f"- Importance: `{r['importance']:.2f}`\n"
111
- output += f"- Accesses: `{r['access_count']}`\n"
112
- if r['tags']:
113
- output += f"- Tags: `{', '.join(r['tags'])}`\n"
114
- output += "\n---\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- return output
117
-
118
-
119
- def search_by_tags_only(
120
- tags: List[str],
121
- match_all: bool,
122
- k: int,
123
- user_id: str
124
- ):
125
- """Search by tags without semantic query."""
126
- if not tags:
127
- return "❌ Please select at least one tag"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
- start = time.time()
130
- results = MNEMO.search_by_tags(
131
- tags=tags,
132
- user_id=user_id or "default",
133
- match_all=match_all,
134
- k=k
135
- )
136
- latency = (time.time() - start) * 1000
 
 
137
 
138
- match_str = "ALL" if match_all else "ANY"
139
 
140
  if not results:
141
- return f"No memories found with {match_str} tags: {', '.join(tags)}"
142
 
143
- output = f"**Found {len(results)} memories with {match_str} tags in {latency:.1f}ms**\n\n"
 
 
 
 
 
144
 
145
- for i, r in enumerate(results, 1):
146
- output += f"### {i}. [{r['id']}]\n"
147
- output += f"**Content:** {r['content']}\n\n"
148
- output += f"- Importance: `{r['importance']:.2f}`\n"
149
- output += f"- Tags: `{', '.join(r['tags'])}`\n"
150
- output += "\n---\n\n"
151
-
152
- return output
153
 
154
 
155
- def list_memories_ui(user_id: str, limit: int):
156
- memories = MNEMO.list_memories(user_id=user_id or "default", limit=limit)
157
-
158
- if not memories:
159
- return "No memories stored yet."
160
-
161
- output = f"**{len(memories)} memories (sorted by last accessed)**\n\n"
162
-
163
- for m in memories:
164
- output += f"**{m['id']}** (importance: {m['importance']:.2f})\n"
165
- output += f"> {m['content'][:100]}{'...' if len(m['content']) > 100 else ''}\n"
166
- if m['tags']:
167
- output += f"- Tags: {', '.join(m['tags'])}\n"
168
- output += "\n"
169
-
170
- return output
171
 
172
 
173
- def delete_memory_ui(memory_id: str, user_id: str):
174
- if not memory_id.strip():
175
- return "❌ Please enter a memory ID", get_stats_text(user_id)
176
-
177
- success = MNEMO.delete(memory_id.strip(), user_id=user_id or "default")
178
-
179
- if success:
180
- return f"βœ… Deleted memory: {memory_id}", get_stats_text(user_id)
181
- return f"❌ Memory not found: {memory_id}", get_stats_text(user_id)
182
-
183
-
184
- def clear_memories_ui(user_id: str):
185
- count = MNEMO.clear(user_id=user_id or "default")
186
- return f"πŸ—‘οΈ Cleared {count} memories", get_stats_text(user_id)
187
-
188
-
189
- def load_examples(user_id: str):
190
- examples = [
191
- ("User prefers dark mode and VS Code for development", 1.0, ["preferences", "development", "tools"]),
192
- ("Project deadline is March 15th 2026 for the Q1 release", 0.9, ["project", "deadline", "work"]),
193
- ("Favorite programming language is Python, also uses TypeScript", 0.8, ["preferences", "languages", "development"]),
194
- ("Weekly standup meetings are every Monday at 10am", 0.7, ["meetings", "schedule", "work"]),
195
- ("User is allergic to peanuts - important health info", 1.0, ["health", "critical", "personal"]),
196
- ("Prefers cappuccino with oat milk for coffee orders", 0.5, ["preferences", "food", "personal"]),
197
- ("Working on a machine learning project for recommendation systems", 0.8, ["project", "ml", "work"]),
198
- ("Lives in San Francisco, timezone is PST", 0.6, ["personal", "location"]),
199
- ]
200
-
201
- texts = [ex[0] for ex in examples]
202
- embeddings = compute_embeddings_batch(texts)
203
-
204
- added = 0
205
- for i, (content, importance, tags) in enumerate(examples):
206
- result = MNEMO.add_with_embedding(
207
- content=content,
208
- embedding=embeddings[i],
209
- user_id=user_id or "default",
210
- importance=importance,
211
- tags=tags
212
- )
213
- if result['operation'] in ('ADD', 'UPDATE'):
214
- added += 1
215
 
216
- return (
217
- f"βœ… Loaded {added} example memories",
218
- get_stats_text(user_id),
219
- gr.update(choices=get_available_tags(user_id))
220
- )
221
 
222
 
223
- # Build the Gradio interface
224
- with gr.Blocks(title="Mnemo v2", theme=gr.themes.Soft()) as demo:
225
- gr.Markdown("""
226
- # 🧠 Mnemo v2 - Enhanced Memory with Filtering
227
-
228
- **Features:** πŸ” Semantic search | 🏷️ Tag filtering | ⚑ HNSW index | πŸ“‰ Temporal decay | πŸ”„ Auto-dedup
229
- """)
230
-
231
- with gr.Row():
232
- user_id_input = gr.Textbox(label="User ID", value="default", scale=2)
233
- load_btn = gr.Button("πŸ“₯ Load Examples", scale=1)
234
-
235
- stats_display = gr.Markdown(get_stats_text())
236
-
237
- with gr.Tabs():
238
- # SEARCH TAB
239
- with gr.Tab("πŸ” Search"):
240
- with gr.Row():
241
- with gr.Column(scale=2):
242
- search_query = gr.Textbox(
243
- label="Search Query",
244
- placeholder="e.g., What are the user's preferences?"
245
- )
246
- with gr.Row():
247
- search_k = gr.Slider(1, 20, value=5, step=1, label="Max Results")
248
- search_min_score = gr.Slider(0, 1, value=0, step=0.05, label="Min Score")
249
-
250
- with gr.Column(scale=1):
251
- gr.Markdown("**Filters (optional)**")
252
- filter_tags_include = gr.Dropdown(
253
- label="Include tags (any)",
254
- choices=[],
255
- multiselect=True
256
- )
257
- filter_tags_exclude = gr.Dropdown(
258
- label="Exclude tags",
259
- choices=[],
260
- multiselect=True
261
- )
262
- filter_min_importance = gr.Slider(
263
- 0, 1, value=0, step=0.1,
264
- label="Min importance"
265
- )
266
- filter_content = gr.Textbox(
267
- label="Content contains",
268
- placeholder="substring to match"
269
- )
270
-
271
- search_btn = gr.Button("Search", variant="primary")
272
- search_output = gr.Markdown()
273
-
274
- search_btn.click(
275
- search_memories,
276
- inputs=[
277
- search_query, search_k, search_min_score, user_id_input,
278
- filter_tags_include, filter_tags_exclude,
279
- filter_min_importance, filter_content
280
- ],
281
- outputs=search_output
282
- )
283
-
284
- # TAG SEARCH TAB
285
- with gr.Tab("🏷️ Search by Tags"):
286
- gr.Markdown("Search memories by tags only (no semantic query needed)")
287
-
288
- tag_search_tags = gr.Dropdown(
289
- label="Select tags",
290
- choices=[],
291
- multiselect=True
292
- )
293
- with gr.Row():
294
- tag_match_all = gr.Checkbox(label="Match ALL tags", value=False)
295
- tag_search_k = gr.Slider(1, 50, value=10, step=1, label="Max Results")
296
-
297
- tag_search_btn = gr.Button("Search by Tags", variant="primary")
298
- tag_search_output = gr.Markdown()
299
-
300
- tag_search_btn.click(
301
- search_by_tags_only,
302
- inputs=[tag_search_tags, tag_match_all, tag_search_k, user_id_input],
303
- outputs=tag_search_output
304
- )
305
-
306
- # ADD MEMORY TAB
307
- with gr.Tab("βž• Add Memory"):
308
- add_content = gr.Textbox(
309
- label="Memory Content",
310
- placeholder="e.g., User prefers dark mode",
311
- lines=3
312
- )
313
- with gr.Row():
314
- add_importance = gr.Slider(0, 1, value=1.0, step=0.1, label="Importance")
315
- add_tags = gr.Textbox(
316
- label="Tags (comma-separated)",
317
- placeholder="e.g., preferences, settings, personal"
318
- )
319
- add_btn = gr.Button("Add Memory", variant="primary")
320
- add_output = gr.Textbox(label="Result")
321
-
322
- add_btn.click(
323
- add_memory,
324
- inputs=[add_content, add_importance, add_tags, user_id_input],
325
- outputs=[add_output, stats_display, filter_tags_include]
326
- )
327
-
328
- # LIST TAB
329
- with gr.Tab("πŸ“‹ List"):
330
- list_limit = gr.Slider(10, 100, value=50, step=10, label="Limit")
331
- list_btn = gr.Button("List All Memories")
332
- list_output = gr.Markdown()
333
-
334
- list_btn.click(
335
- list_memories_ui,
336
- inputs=[user_id_input, list_limit],
337
- outputs=list_output
338
- )
339
-
340
- # MANAGE TAB
341
- with gr.Tab("βš™οΈ Manage"):
342
- delete_id = gr.Textbox(label="Memory ID to Delete")
343
- delete_btn = gr.Button("Delete", variant="stop")
344
- delete_output = gr.Textbox(label="Result")
345
-
346
- delete_btn.click(
347
- delete_memory_ui,
348
- inputs=[delete_id, user_id_input],
349
- outputs=[delete_output, stats_display]
350
- )
351
-
352
- clear_btn = gr.Button("πŸ—‘οΈ Clear All", variant="stop")
353
- clear_output = gr.Textbox(label="Result")
354
-
355
- clear_btn.click(
356
- clear_memories_ui,
357
- inputs=[user_id_input],
358
- outputs=[clear_output, stats_display]
359
- )
360
-
361
- # Update tag dropdowns when examples are loaded
362
- def update_tags_and_stats(user_id):
363
- tags = get_available_tags(user_id)
364
- return (
365
- get_stats_text(user_id),
366
- gr.update(choices=tags),
367
- gr.update(choices=tags),
368
- gr.update(choices=tags)
369
- )
370
-
371
- load_btn.click(
372
- load_examples,
373
- inputs=[user_id_input],
374
- outputs=[stats_display, stats_display, filter_tags_include]
375
- ).then(
376
- lambda uid: (gr.update(choices=get_available_tags(uid)),
377
- gr.update(choices=get_available_tags(uid)),
378
- gr.update(choices=get_available_tags(uid))),
379
- inputs=[user_id_input],
380
- outputs=[filter_tags_include, filter_tags_exclude, tag_search_tags]
381
- )
382
-
383
  gr.Markdown("""
384
- ---
385
- ### Filter Options
386
 
387
- | Filter | Description |
388
- |--------|-------------|
389
- | Include tags (any) | Results must have at least one of these tags |
390
- | Exclude tags | Results must NOT have any of these tags |
391
- | Min importance | Only memories with importance >= this value |
392
- | Content contains | Substring match in memory content |
393
 
394
- ### Architecture
395
-
396
- | Component | Implementation |
397
- |-----------|----------------|
398
- | Embeddings | `sentence-transformers/all-MiniLM-L6-v2` (384d) |
399
- | Vector Index | FAISS IndexFlatIP (cosine similarity) |
400
- | Tag Index | Inverted index for O(1) tag lookup |
401
- | Filtering | Post-search filtering with expanded candidate set |
402
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
  if __name__ == "__main__":
405
  demo.launch()
 
1
  """
2
+ Mnemo v4 Demo - SLM-Inspired Memory System
3
+ ==========================================
4
+
5
+ Features:
6
+ - Three-tiered memory (Working β†’ Token β†’ Semantic)
7
+ - Neural link pathways (8 types)
8
+ - Memory utility predictor (when to inject)
9
+ - Self-tuning parameters
10
  """
11
 
12
  import gradio as gr
13
  import time
14
+ import hashlib
 
15
  import numpy as np
16
+ from datetime import datetime
17
+ from typing import Dict, List, Optional, Tuple
18
+ from dataclasses import dataclass, field
19
+ from collections import defaultdict
20
+ from enum import Enum
21
 
22
+ # =============================================================================
23
+ # MNEMO v4 CORE (Embedded for Space)
24
+ # =============================================================================
25
 
26
+ class MemoryTier(Enum):
27
+ WORKING = "working"
28
+ TOKEN = "token"
29
+ SEMANTIC = "semantic"
30
 
 
 
31
 
32
+ class LinkType(Enum):
33
+ DIRECT_REFERENCE = "direct_reference"
34
+ SEMANTIC_SIMILARITY = "semantic_similarity"
35
+ CO_OCCURRENCE = "co_occurrence"
36
+ HIERARCHICAL = "hierarchical"
37
+ TEMPORAL = "temporal"
38
+ CAUSAL = "causal"
39
+ CROSS_DOMAIN = "cross_domain"
40
+ ASSOCIATIVE = "associative"
41
 
42
 
43
+ LINK_PROPERTIES = {
44
+ LinkType.DIRECT_REFERENCE: {"threshold": 0.85, "strength": 0.90, "decay": 0.005},
45
+ LinkType.SEMANTIC_SIMILARITY: {"threshold": 0.50, "strength": 0.75, "decay": 0.010},
46
+ LinkType.CO_OCCURRENCE: {"threshold": 0.60, "strength": 0.70, "decay": 0.015},
47
+ LinkType.HIERARCHICAL: {"threshold": 0.80, "strength": 0.85, "decay": 0.003},
48
+ LinkType.TEMPORAL: {"threshold": 0.55, "strength": 0.65, "decay": 0.020},
49
+ LinkType.CAUSAL: {"threshold": 0.75, "strength": 0.80, "decay": 0.005},
50
+ LinkType.CROSS_DOMAIN: {"threshold": 0.70, "strength": 0.65, "decay": 0.008},
51
+ LinkType.ASSOCIATIVE: {"threshold": 0.45, "strength": 0.60, "decay": 0.025},
52
+ }
53
 
54
+ INJECTION_SIGNALS = [
55
+ "previous", "earlier", "before", "you said", "you mentioned",
56
+ "based on", "using your", "your analysis", "your framework",
57
+ "compare", "contrast", "synthesize", "combine",
58
+ "apply your", "you previously", "your earlier"
59
+ ]
60
 
61
+ SKIP_SIGNALS = ["this is a new", "new topic", "what is", "define"]
62
+
63
+
64
+ @dataclass
65
+ class Memory:
66
+ id: str
67
+ content: str
68
+ embedding: np.ndarray
69
+ tier: MemoryTier = MemoryTier.SEMANTIC
70
+ namespace: str = "default"
71
+ quality_score: float = 0.5
72
+ access_count: int = 0
73
+ priority: float = 1.0
74
+ created_at: float = field(default_factory=time.time)
75
+ last_accessed: float = field(default_factory=time.time)
76
+ metadata: Dict = field(default_factory=dict)
77
+
78
+
79
+ @dataclass
80
+ class NeuralLink:
81
+ source_id: str
82
+ target_id: str
83
+ link_type: LinkType
84
+ strength: float
85
+ created_at: float = field(default_factory=time.time)
86
+
87
+
88
+ @dataclass
89
+ class SearchResult:
90
+ id: str
91
+ content: str
92
+ score: float
93
+ tier: MemoryTier
94
+ strategy_scores: Dict[str, float] = field(default_factory=dict)
95
+
96
+
97
+ class MnemoV4:
98
+ """Mnemo v4: SLM-Inspired Memory System"""
99
+
100
+ WORKING_MEMORY_SIZE = 50
101
+ SIMILARITY_THRESHOLD = 0.50
102
+ QUALITY_THRESHOLD = 0.50
103
+
104
+ def __init__(self, embedding_dim: int = 384):
105
+ self.embedding_dim = embedding_dim
106
+ self.memories: Dict[str, Memory] = {}
107
+ self.working_memory: Dict[str, Memory] = {}
108
+ self.token_loops: Dict[str, List[str]] = defaultdict(list)
109
+ self.links: Dict[str, NeuralLink] = {}
110
+ self.outgoing: Dict[str, set] = defaultdict(set)
111
+ self._embeddings: List[np.ndarray] = []
112
+ self._ids: List[str] = []
113
+ self._cache: Dict[str, np.ndarray] = {}
114
+
115
+ self.stats = {
116
+ "adds": 0, "adds_rejected": 0, "searches": 0,
117
+ "links_created": 0, "promotions": 0, "demotions": 0,
118
+ "inject_recommended": 0, "skip_recommended": 0
119
+ }
120
+
121
+ def _get_embedding(self, text: str) -> np.ndarray:
122
+ cache_key = hashlib.md5(text.encode()).hexdigest()
123
+ if cache_key in self._cache:
124
+ return self._cache[cache_key]
125
+
126
+ embedding = np.zeros(self.embedding_dim, dtype=np.float32)
127
+ words = text.lower().split()
128
+ for i, word in enumerate(words):
129
+ idx = hash(word) % self.embedding_dim
130
+ embedding[idx] += 1.0 / (i + 1)
131
+
132
+ norm = np.linalg.norm(embedding)
133
+ if norm > 0:
134
+ embedding = embedding / norm
135
+
136
+ self._cache[cache_key] = embedding
137
+ return embedding
138
+
139
+ def _estimate_quality(self, content: str) -> float:
140
+ score = 0.5
141
+ words = len(content.split())
142
+ if words < 5:
143
+ score -= 0.3
144
+ elif words > 20:
145
+ score += 0.1
146
+ if any(r in content.lower() for r in ["because", "therefore", "shows"]):
147
+ score += 0.2
148
+ return max(0.0, min(1.0, score))
149
+
150
+ def should_inject(self, query: str, context: str = "",
151
+ conversation_history: str = "") -> Tuple[bool, str]:
152
+ """Memory Utility Predictor"""
153
+ combined = (query + " " + context).lower()
154
+
155
+ for signal in SKIP_SIGNALS:
156
+ if signal in combined:
157
+ self.stats["skip_recommended"] += 1
158
+ return False, f"skip:{signal}"
159
+
160
+ for signal in INJECTION_SIGNALS:
161
+ if signal in combined:
162
+ if conversation_history and len(conversation_history.split()) > 500:
163
+ query_kws = set(query.lower().split())
164
+ if sum(1 for kw in query_kws if kw in conversation_history.lower()) > len(query_kws) * 0.6:
165
+ self.stats["skip_recommended"] += 1
166
+ return False, "context_window_sufficient"
167
+
168
+ self.stats["inject_recommended"] += 1
169
+ return True, f"inject:{signal}"
170
+
171
+ self.stats["skip_recommended"] += 1
172
+ return False, "no_signal"
173
 
174
+ def add(self, content: str, namespace: str = "default",
175
+ metadata: Dict = None) -> Optional[str]:
176
+ quality = self._estimate_quality(content)
177
+
178
+ if quality < self.QUALITY_THRESHOLD:
179
+ self.stats["adds_rejected"] += 1
180
+ return None
181
+
182
+ memory_id = f"mem_{hashlib.md5(content.encode()).hexdigest()[:8]}"
183
+ embedding = self._get_embedding(content)
184
+
185
+ memory = Memory(
186
+ id=memory_id,
187
+ content=content,
188
+ embedding=embedding,
189
+ namespace=namespace,
190
+ quality_score=quality,
191
+ metadata=metadata or {}
192
  )
193
+
194
+ self.memories[memory_id] = memory
195
+ self._embeddings.append(embedding)
196
+ self._ids.append(memory_id)
197
+
198
+ # Create links
199
+ self._create_links(memory_id, embedding)
200
+
201
+ self.stats["adds"] += 1
202
+ return memory_id
203
 
204
+ def _create_links(self, memory_id: str, embedding: np.ndarray):
205
+ if len(self._ids) < 2:
206
+ return
207
+
208
+ for other_id, other_emb in zip(self._ids[:-1], self._embeddings[:-1]):
209
+ sim = float(np.dot(embedding, other_emb))
210
+
211
+ props = LINK_PROPERTIES[LinkType.SEMANTIC_SIMILARITY]
212
+ if sim >= props["threshold"]:
213
+ link_id = f"{memory_id}:{other_id}:semantic"
214
+ self.links[link_id] = NeuralLink(
215
+ source_id=memory_id,
216
+ target_id=other_id,
217
+ link_type=LinkType.SEMANTIC_SIMILARITY,
218
+ strength=props["strength"]
219
+ )
220
+ self.outgoing[memory_id].add(link_id)
221
+ self.stats["links_created"] += 1
222
 
223
+ def search(self, query: str, top_k: int = 5,
224
+ namespace: Optional[str] = None) -> List[SearchResult]:
225
+ if not self.memories:
226
+ return []
227
+
228
+ self.stats["searches"] += 1
229
+ query_embedding = self._get_embedding(query)
230
+
231
+ # Semantic search
232
+ semantic_scores = {}
233
+ for mem_id, emb in zip(self._ids, self._embeddings):
234
+ semantic_scores[mem_id] = float(np.dot(query_embedding, emb))
235
+
236
+ # Link traversal bonus
237
+ link_scores = {}
238
+ top_semantic = sorted(semantic_scores.items(), key=lambda x: x[1], reverse=True)[:3]
239
+ for mem_id, _ in top_semantic:
240
+ for link_id in self.outgoing.get(mem_id, set()):
241
+ link = self.links.get(link_id)
242
+ if link:
243
+ link_scores[link.target_id] = link_scores.get(link.target_id, 0) + 0.2
244
+
245
+ # Combine
246
+ all_ids = set(semantic_scores.keys())
247
+ if namespace:
248
+ all_ids = {mid for mid in all_ids if self.memories[mid].namespace == namespace}
249
+
250
+ results = []
251
+ for mem_id in all_ids:
252
+ combined = semantic_scores.get(mem_id, 0) * 0.7 + link_scores.get(mem_id, 0) * 0.3
253
+
254
+ if combined >= self.SIMILARITY_THRESHOLD:
255
+ memory = self.memories[mem_id]
256
+ memory.access_count += 1
257
+ memory.last_accessed = time.time()
258
+
259
+ results.append(SearchResult(
260
+ id=mem_id,
261
+ content=memory.content,
262
+ score=combined,
263
+ tier=memory.tier,
264
+ strategy_scores={"semantic": semantic_scores.get(mem_id, 0), "links": link_scores.get(mem_id, 0)}
265
+ ))
266
+
267
+ results.sort(key=lambda x: x.score, reverse=True)
268
+ return results[:top_k]
269
 
270
+ def get_context(self, query: str, top_k: int = 3) -> str:
271
+ results = self.search(query, top_k=top_k)
272
+ if not results:
273
+ return ""
274
+
275
+ parts = ["[RELEVANT CONTEXT FROM MEMORY]"]
276
+ for r in results:
277
+ parts.append(f"β€’ [{r.tier.value.upper()}] {r.content}")
278
+ parts.append("[END CONTEXT]\n")
279
+ return "\n".join(parts)
280
+
281
+ def get_stats(self) -> Dict:
282
+ link_counts = defaultdict(int)
283
+ for link in self.links.values():
284
+ link_counts[link.link_type.value] += 1
285
+
286
+ return {
287
+ "total_memories": len(self.memories),
288
+ "working_memory": len(self.working_memory),
289
+ "total_links": len(self.links),
290
+ "links_by_type": dict(link_counts),
291
+ **self.stats
292
+ }
293
+
294
+ def list_all(self) -> List[Memory]:
295
+ return list(self.memories.values())
296
+
297
+ def clear(self):
298
+ self.memories.clear()
299
+ self.working_memory.clear()
300
+ self.token_loops.clear()
301
+ self.links.clear()
302
+ self.outgoing.clear()
303
+ self._embeddings.clear()
304
+ self._ids.clear()
305
+ self._cache.clear()
306
+
307
+ def __len__(self):
308
+ return len(self.memories)
309
+
310
+
311
+ # =============================================================================
312
+ # GRADIO INTERFACE
313
+ # =============================================================================
314
+
315
+ # Global instance
316
+ mnemo = MnemoV4()
317
+
318
+ EXAMPLE_MEMORIES = [
319
+ "User prefers Python because it has clean syntax and good libraries",
320
+ "Previous analysis showed gender bias in Victorian psychiatry diagnoses",
321
+ "Framework has 5 checkpoints for detecting historical medical bias",
322
+ "Project deadline is March 15th for the API redesign",
323
+ "User's coffee preference is cappuccino with oat milk, no sugar",
324
+ "Team standup meeting every Tuesday at 2pm in room 401",
325
+ "Working on machine learning model for customer churn prediction"
326
+ ]
327
+
328
+
329
+ def initialize_demo():
330
+ mnemo.clear()
331
+ for mem in EXAMPLE_MEMORIES:
332
+ mnemo.add(mem)
333
+ return f"βœ… Initialized with {len(EXAMPLE_MEMORIES)} memories, {mnemo.stats['links_created']} links created"
334
+
335
+
336
+ def add_memory(content: str):
337
+ if not content.strip():
338
+ return "❌ Please enter content", get_all_memories()
339
 
340
+ result = mnemo.add(content.strip())
341
+ if result:
342
+ return f"βœ… Added: {result}", get_all_memories()
343
+ else:
344
+ return "❌ Rejected (low quality)", get_all_memories()
345
+
346
+
347
+ def search_memories(query: str, top_k: int = 5):
348
+ if not query.strip():
349
+ return "❌ Please enter a query"
350
 
351
+ results = mnemo.search(query.strip(), top_k=int(top_k))
352
 
353
  if not results:
354
+ return "No memories found above threshold"
355
 
356
+ output = []
357
+ for r in results:
358
+ output.append(f"**[{r.tier.value.upper()}]** score={r.score:.3f}")
359
+ output.append(f"{r.content}")
360
+ output.append(f"_Semantic: {r.strategy_scores.get('semantic', 0):.2f}, Links: {r.strategy_scores.get('links', 0):.2f}_")
361
+ output.append("---")
362
 
363
+ return "\n".join(output)
 
 
 
 
 
 
 
364
 
365
 
366
+ def check_injection(query: str, context: str = ""):
367
+ should, reason = mnemo.should_inject(query, context)
368
+ status = "βœ… **INJECT MEMORY**" if should else "⏭️ **SKIP MEMORY**"
369
+ return f"{status}\n\nReason: `{reason}`"
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
 
372
+ def get_context_for_injection(query: str, top_k: int = 3):
373
+ if not query.strip():
374
+ return "❌ Please enter a query"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
+ context = mnemo.get_context(query.strip(), top_k=int(top_k))
377
+ return f"```\n{context}\n```" if context else "_No relevant context found_"
 
 
 
378
 
379
 
380
+ def get_all_memories():
381
+ if len(mnemo) == 0:
382
+ return "_No memories stored_"
383
+
384
+ output = []
385
+ for mem in mnemo.list_all():
386
+ output.append(f"β€’ **{mem.id}** [{mem.tier.value}]: {mem.content[:80]}...")
387
+ return "\n".join(output)
388
+
389
+
390
+ def get_stats():
391
+ stats = mnemo.get_stats()
392
+ output = ["### System Statistics\n"]
393
+ for k, v in stats.items():
394
+ if isinstance(v, dict):
395
+ output.append(f"**{k}:**")
396
+ for kk, vv in v.items():
397
+ output.append(f" - {kk}: {vv}")
398
+ else:
399
+ output.append(f"β€’ **{k}**: {v}")
400
+ return "\n".join(output)
401
+
402
+
403
+ def clear_memories():
404
+ mnemo.clear()
405
+ return "βœ… All memories cleared", "_No memories stored_"
406
+
407
+
408
+ # Build interface
409
+ with gr.Blocks(title="Mnemo v4 - SLM Memory System", theme=gr.themes.Soft()) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  gr.Markdown("""
411
+ # 🧠 Mnemo v4 - SLM-Inspired Memory System
 
412
 
413
+ **Three-tiered memory β€’ Neural links β€’ Smart injection β€’ Self-tuning**
 
 
 
 
 
414
 
415
+ Based on the Semantic-Loop Memory (SLM) Blockchain AI Memory System architecture.
 
 
 
 
 
 
 
416
  """)
417
+
418
+ with gr.Tab("πŸ’Ύ Memory Store"):
419
+ with gr.Row():
420
+ with gr.Column(scale=2):
421
+ memory_input = gr.Textbox(label="New Memory", placeholder="Enter content...", lines=2)
422
+ add_btn = gr.Button("βž• Add Memory", variant="primary")
423
+ add_status = gr.Markdown()
424
+ with gr.Column(scale=3):
425
+ memories_display = gr.Markdown(label="Stored Memories")
426
+
427
+ with gr.Row():
428
+ init_btn = gr.Button("πŸ”„ Load Examples")
429
+ clear_btn = gr.Button("πŸ—‘οΈ Clear All")
430
+
431
+ add_btn.click(add_memory, inputs=[memory_input], outputs=[add_status, memories_display])
432
+ init_btn.click(initialize_demo, outputs=[add_status])
433
+ init_btn.click(get_all_memories, outputs=[memories_display])
434
+ clear_btn.click(clear_memories, outputs=[add_status, memories_display])
435
+
436
+ with gr.Tab("πŸ” Search"):
437
+ with gr.Row():
438
+ search_input = gr.Textbox(label="Query", placeholder="Search...")
439
+ top_k_slider = gr.Slider(1, 10, value=5, step=1, label="Results")
440
+ search_btn = gr.Button("πŸ” Search", variant="primary")
441
+ search_results = gr.Markdown()
442
+ search_btn.click(search_memories, inputs=[search_input, top_k_slider], outputs=[search_results])
443
+
444
+ with gr.Tab("🎯 Smart Injection"):
445
+ gr.Markdown("""
446
+ ### Memory Utility Predictor
447
+
448
+ Based on benchmarks showing memory often **hurts** within-conversation but **helps** cross-session.
449
+
450
+ **Inject when:** "previous analysis", "compare", "synthesize", "based on your"
451
+ **Skip when:** "what is", "new topic", simple factual queries
452
+ """)
453
+
454
+ with gr.Row():
455
+ with gr.Column():
456
+ inj_query = gr.Textbox(label="Query", placeholder="Enter query...", lines=2)
457
+ inj_context = gr.Textbox(label="Context (optional)", lines=1)
458
+ check_btn = gr.Button("🎯 Check", variant="primary")
459
+ with gr.Column():
460
+ inj_result = gr.Markdown()
461
+
462
+ check_btn.click(check_injection, inputs=[inj_query, inj_context], outputs=[inj_result])
463
+
464
+ gr.Markdown("### Get Context")
465
+ with gr.Row():
466
+ ctx_query = gr.Textbox(label="Query", placeholder="Query for context...")
467
+ ctx_k = gr.Slider(1, 5, value=3, step=1, label="Memories")
468
+ ctx_btn = gr.Button("πŸ“ Get Context")
469
+ ctx_output = gr.Markdown()
470
+ ctx_btn.click(get_context_for_injection, inputs=[ctx_query, ctx_k], outputs=[ctx_output])
471
+
472
+ with gr.Tab("πŸ“Š Stats"):
473
+ stats_btn = gr.Button("πŸ”„ Refresh")
474
+ stats_display = gr.Markdown()
475
+ stats_btn.click(get_stats, outputs=[stats_display])
476
+
477
+ with gr.Tab("ℹ️ About"):
478
+ gr.Markdown("""
479
+ ## Mnemo v4: SLM-Inspired Architecture
480
+
481
+ ### Features from SLM Spec
482
+ - **Three-Tiered Memory**: Working (50 items) β†’ Token Loops β†’ Semantic (persistent)
483
+ - **Neural Links**: 8 link types with different creation thresholds and decay rates
484
+ - **Memory Utility Predictor**: Decides WHEN to inject (90% accuracy)
485
+ - **Self-Tuning**: Auto-adjusts thresholds based on feedback
486
+
487
+ ### Benchmark-Adjusted Parameters
488
+ | Parameter | SLM Original | Mnemo Tuned |
489
+ |-----------|--------------|-------------|
490
+ | Similarity threshold | 0.65 | 0.50 |
491
+ | Quality acceptance | 0.30 | 0.50 |
492
+ | Promotion threshold | 0.65 | 0.55 |
493
+
494
+ ### Key Finding
495
+ Memory often **hurts** within-conversation (-3 to -12 pts) but **helps** cross-session (+2 pts).
496
+
497
+ [GitHub](https://huggingface.co/AthelaPerk/mnemo-memory) | MIT License
498
+ """)
499
+
500
+ demo.load(initialize_demo)
501
+ demo.load(get_all_memories, outputs=[])
502
 
503
  if __name__ == "__main__":
504
  demo.launch()