AthelaPerk commited on
Commit
d1fce55
·
verified ·
1 Parent(s): 38b3168

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +224 -104
app.py CHANGED
@@ -1,17 +1,175 @@
1
  """
2
  Mnemo HuggingFace Space Demo
3
- Compatible with Gradio 5.x
4
  """
5
 
6
  import gradio as gr
7
- from mnemo import Mnemo
8
  import time
 
 
 
 
 
9
 
10
- # Initialize Mnemo
11
- m = Mnemo()
 
 
 
12
 
13
- # Pre-populate with example memories
14
- examples = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  "User prefers dark mode and morning email notifications",
16
  "Project Alpha deadline is March 15th, budget $50,000",
17
  "Team standup every Tuesday 2pm, room 401",
@@ -22,129 +180,91 @@ examples = [
22
  "API rate limit is 1000 requests per minute",
23
  ]
24
 
25
- for ex in examples:
26
- m.add(ex)
27
 
28
 
29
- def search_memories(query, top_k):
30
- """Search memories"""
31
  if not query or not query.strip():
32
  return "Please enter a search query"
33
 
34
  start = time.time()
35
- results = m.search(query, top_k=int(top_k))
36
  latency = (time.time() - start) * 1000
37
 
38
  if not results:
39
  return "No results found"
40
 
41
- output = f"Found {len(results)} results in {latency:.2f}ms\n\n"
42
-
43
  for i, r in enumerate(results, 1):
44
- output += f"{i}. [{r.id}] (score: {r.score:.3f})\n"
45
- output += f" {r.content}\n"
46
- output += f" strategies: sem={r.strategy_scores.get('semantic', 0):.2f}, "
47
- output += f"bm25={r.strategy_scores.get('bm25', 0):.2f}, "
48
- output += f"graph={r.strategy_scores.get('graph', 0):.2f}\n\n"
49
 
50
- return output
51
 
52
 
53
- def add_memory(content):
54
- """Add a new memory"""
55
  if not content or not content.strip():
56
- return "Please enter some content", get_stats()
57
-
58
- mem_id = m.add(content)
59
- return f"Added memory: {mem_id}", get_stats()
60
-
61
-
62
- def record_feedback(query, memory_id, relevance):
63
- """Record feedback"""
64
- if not query or not query.strip() or not memory_id or not memory_id.strip():
65
- return "Please enter query and memory ID"
66
-
67
- m.feedback(query, memory_id, float(relevance))
68
- return f"Feedback recorded: {memory_id} = {relevance}"
69
 
70
 
71
- def get_stats():
72
- """Get system stats"""
73
- stats = m.get_stats()
74
- output = f"""System Stats:
75
- - Total memories: {stats['total_memories']}
76
- - Searches: {stats['searches']}
77
- - Feedback: {stats['feedback_count']}
78
- - Cache hit rate: {stats['cache_hit_rate']}
79
- - Strategy wins: {stats['strategy_wins']}
80
- """
81
- return output
82
 
83
 
84
- def clear_all():
85
- """Clear all memories"""
86
- m.clear()
87
- for ex in examples:
88
- m.add(ex)
89
- return "Cleared and reset to examples", get_stats()
90
 
91
 
92
- # Build Gradio interface using simpler patterns
93
- with gr.Blocks(title="Mnemo Demo") as demo:
94
- gr.Markdown("""
95
- # Mnemo: Semantic-Loop Memory
 
96
 
97
- *Named after Mnemosyne, Greek goddess of memory*
98
 
99
- **21x faster than mem0 | No API keys | Fully local | Learns from feedback**
100
- """)
 
101
 
102
  with gr.Row():
103
- with gr.Column(scale=2):
104
- gr.Markdown("### Search Memories")
105
- search_input = gr.Textbox(label="Query", placeholder="e.g., coffee preferences")
106
- top_k = gr.Slider(minimum=1, maximum=10, value=5, step=1, label="Results")
107
  search_btn = gr.Button("Search", variant="primary")
108
- search_output = gr.Textbox(label="Results", lines=10)
109
-
110
- gr.Markdown("### Add Memory")
111
- add_content = gr.Textbox(label="Content", placeholder="e.g., Meeting Friday 3pm")
112
- add_btn = gr.Button("Add Memory")
113
- add_output = gr.Textbox(label="Status", lines=1)
114
 
115
- with gr.Column(scale=1):
116
- stats_output = gr.Textbox(label="Stats", value=get_stats(), lines=8)
117
- refresh_btn = gr.Button("Refresh Stats")
118
-
119
- gr.Markdown("### Feedback")
120
- fb_query = gr.Textbox(label="Query", placeholder="Original search")
121
- fb_id = gr.Textbox(label="Memory ID", placeholder="mem_abc123")
122
- fb_score = gr.Slider(minimum=-1, maximum=1, value=0.5, step=0.1, label="Relevance")
123
- fb_btn = gr.Button("Record Feedback")
124
- fb_output = gr.Textbox(label="Status", lines=1)
125
-
126
- clear_btn = gr.Button("Reset to Examples")
127
-
128
- # Events
129
- search_btn.click(search_memories, [search_input, top_k], search_output)
130
- search_input.submit(search_memories, [search_input, top_k], search_output)
131
- add_btn.click(add_memory, [add_content], [add_output, stats_output])
132
- refresh_btn.click(get_stats, [], stats_output)
133
- fb_btn.click(record_feedback, [fb_query, fb_id, fb_score], fb_output)
134
- clear_btn.click(clear_all, [], [add_output, stats_output])
135
-
136
- gr.Markdown("""
137
- ---
138
- ### Benchmarks vs mem0
139
-
140
- | Metric | mem0 | Mnemo |
141
- |--------|------|-------|
142
- | Search | 5.73ms | 0.27ms (21x faster) |
143
- | Ingestion | 31.1ms | 0.8ms (39x faster) |
144
- | API Required | Yes | No |
145
-
146
- [Get the library](https://huggingface.co/AthelaPerk/mnemo-memory)
147
- """)
148
-
149
- if __name__ == "__main__":
150
- demo.launch()
 
1
  """
2
  Mnemo HuggingFace Space Demo
3
+ Simple version - no async issues
4
  """
5
 
6
  import gradio as gr
7
+ import hashlib
8
  import time
9
+ import re
10
+ import numpy as np
11
+ from typing import Dict, List, Optional
12
+ from dataclasses import dataclass, field
13
+ from collections import defaultdict
14
 
15
+ try:
16
+ import faiss
17
+ HAS_FAISS = True
18
+ except ImportError:
19
+ HAS_FAISS = False
20
 
21
+ try:
22
+ from rank_bm25 import BM25Okapi
23
+ HAS_BM25 = True
24
+ except ImportError:
25
+ HAS_BM25 = False
26
+
27
+
28
+ @dataclass
29
+ class SearchResult:
30
+ id: str
31
+ content: str
32
+ score: float
33
+ strategy_scores: Dict[str, float] = field(default_factory=dict)
34
+
35
+
36
+ class Mnemo:
37
+ """Simplified Mnemo for HF Spaces"""
38
+
39
+ STOP_WORDS = {"a", "an", "the", "is", "are", "was", "were", "be", "been",
40
+ "to", "of", "in", "for", "on", "with", "at", "by", "from",
41
+ "and", "but", "or", "not", "this", "that", "i", "me", "my"}
42
+
43
+ def __init__(self, embedding_dim: int = 384):
44
+ self.embedding_dim = embedding_dim
45
+ self.memories = {}
46
+ self._embeddings = []
47
+ self._ids = []
48
+ self._tokenized_docs = []
49
+ self.bm25 = None
50
+ self._doc_boosts = defaultdict(float)
51
+ self._query_doc_scores = defaultdict(dict)
52
+ self.stats = {"adds": 0, "searches": 0, "feedback": 0, "strategy_wins": defaultdict(int)}
53
+
54
+ if HAS_FAISS:
55
+ self.index = faiss.IndexFlatIP(embedding_dim)
56
+ else:
57
+ self.index = None
58
+
59
+ def _get_embedding(self, text: str) -> np.ndarray:
60
+ embedding = np.zeros(self.embedding_dim, dtype=np.float32)
61
+ words = text.lower().split()
62
+ for i, word in enumerate(words):
63
+ idx = hash(word) % self.embedding_dim
64
+ embedding[idx] += 1.0 / (i + 1)
65
+ norm = np.linalg.norm(embedding)
66
+ if norm > 0:
67
+ embedding = embedding / norm
68
+ return embedding
69
+
70
+ def add(self, content: str, memory_id: str = None) -> str:
71
+ if memory_id is None:
72
+ memory_id = f"mem_{hashlib.md5(content.encode()).hexdigest()[:8]}"
73
+
74
+ embedding = self._get_embedding(content)
75
+ self.memories[memory_id] = {"content": content, "embedding": embedding}
76
+ self._embeddings.append(embedding)
77
+ self._ids.append(memory_id)
78
+
79
+ if HAS_FAISS and self.index is not None:
80
+ self.index.add(embedding.reshape(1, -1))
81
+
82
+ tokens = content.lower().split()
83
+ self._tokenized_docs.append(tokens)
84
+ if HAS_BM25 and self._tokenized_docs:
85
+ self.bm25 = BM25Okapi(self._tokenized_docs)
86
+
87
+ self.stats["adds"] += 1
88
+ return memory_id
89
+
90
+ def search(self, query: str, top_k: int = 5) -> List[SearchResult]:
91
+ if not self.memories:
92
+ return []
93
+
94
+ self.stats["searches"] += 1
95
+ query_embedding = self._get_embedding(query)
96
+
97
+ # Semantic search
98
+ semantic_scores = {}
99
+ if HAS_FAISS and self.index is not None and self.index.ntotal > 0:
100
+ k = min(top_k * 2, self.index.ntotal)
101
+ scores, indices = self.index.search(query_embedding.reshape(1, -1), k)
102
+ for score, idx in zip(scores[0], indices[0]):
103
+ if 0 <= idx < len(self._ids):
104
+ semantic_scores[self._ids[idx]] = float(score)
105
+
106
+ # BM25 search
107
+ bm25_scores = {}
108
+ if HAS_BM25 and self.bm25 is not None:
109
+ tokens = query.lower().split()
110
+ scores = self.bm25.get_scores(tokens)
111
+ max_score = max(scores) if len(scores) > 0 and max(scores) > 0 else 1
112
+ for idx, score in enumerate(scores):
113
+ if score > 0.1 * max_score:
114
+ bm25_scores[self._ids[idx]] = float(score / max_score)
115
+
116
+ # Combine
117
+ all_ids = set(semantic_scores.keys()) | set(bm25_scores.keys())
118
+ results = []
119
+
120
+ for mem_id in all_ids:
121
+ strategy_scores = {
122
+ "semantic": semantic_scores.get(mem_id, 0),
123
+ "bm25": bm25_scores.get(mem_id, 0),
124
+ }
125
+ combined = 0.5 * strategy_scores["semantic"] + 0.5 * strategy_scores["bm25"]
126
+ combined += self._doc_boosts.get(mem_id, 0) * 0.1
127
+
128
+ mem = self.memories.get(mem_id)
129
+ if mem:
130
+ results.append(SearchResult(
131
+ id=mem_id,
132
+ content=mem["content"],
133
+ score=combined,
134
+ strategy_scores=strategy_scores
135
+ ))
136
+
137
+ results.sort(key=lambda x: x.score, reverse=True)
138
+
139
+ if results:
140
+ winner = max(results[0].strategy_scores, key=results[0].strategy_scores.get)
141
+ self.stats["strategy_wins"][winner] += 1
142
+
143
+ return results[:top_k]
144
+
145
+ def feedback(self, query: str, memory_id: str, relevance: float):
146
+ self._doc_boosts[memory_id] += 0.1 * relevance
147
+ self.stats["feedback"] += 1
148
+
149
+ def get_stats(self) -> Dict:
150
+ return {
151
+ "total_memories": len(self.memories),
152
+ "searches": self.stats["searches"],
153
+ "feedback": self.stats["feedback"],
154
+ "strategy_wins": dict(self.stats["strategy_wins"])
155
+ }
156
+
157
+ def clear(self):
158
+ self.memories.clear()
159
+ self._embeddings.clear()
160
+ self._ids.clear()
161
+ self._tokenized_docs.clear()
162
+ self.bm25 = None
163
+ self._doc_boosts.clear()
164
+ if HAS_FAISS:
165
+ self.index = faiss.IndexFlatIP(self.embedding_dim)
166
+
167
+
168
+ # Global instance
169
+ mnemo = Mnemo()
170
+
171
+ # Pre-load examples
172
+ EXAMPLES = [
173
  "User prefers dark mode and morning email notifications",
174
  "Project Alpha deadline is March 15th, budget $50,000",
175
  "Team standup every Tuesday 2pm, room 401",
 
180
  "API rate limit is 1000 requests per minute",
181
  ]
182
 
183
+ for ex in EXAMPLES:
184
+ mnemo.add(ex)
185
 
186
 
187
+ def do_search(query, top_k):
 
188
  if not query or not query.strip():
189
  return "Please enter a search query"
190
 
191
  start = time.time()
192
+ results = mnemo.search(query.strip(), top_k=int(top_k))
193
  latency = (time.time() - start) * 1000
194
 
195
  if not results:
196
  return "No results found"
197
 
198
+ lines = [f"Found {len(results)} results in {latency:.2f}ms\n"]
 
199
  for i, r in enumerate(results, 1):
200
+ lines.append(f"{i}. [{r.id}] score={r.score:.3f}")
201
+ lines.append(f" {r.content}")
202
+ lines.append(f" sem={r.strategy_scores.get('semantic',0):.2f} bm25={r.strategy_scores.get('bm25',0):.2f}\n")
 
 
203
 
204
+ return "\n".join(lines)
205
 
206
 
207
+ def do_add(content):
 
208
  if not content or not content.strip():
209
+ return "Please enter content", do_stats()
210
+ mem_id = mnemo.add(content.strip())
211
+ return f"Added: {mem_id}", do_stats()
 
 
 
 
 
 
 
 
 
 
212
 
213
 
214
+ def do_feedback(query, mem_id, score):
215
+ if not query or not mem_id:
216
+ return "Enter query and memory ID"
217
+ mnemo.feedback(query.strip(), mem_id.strip(), float(score))
218
+ return f"Recorded: {mem_id} = {score}"
 
 
 
 
 
 
219
 
220
 
221
+ def do_stats():
222
+ s = mnemo.get_stats()
223
+ return f"Memories: {s['total_memories']} | Searches: {s['searches']} | Feedback: {s['feedback']} | Wins: {s['strategy_wins']}"
 
 
 
224
 
225
 
226
+ def do_reset():
227
+ mnemo.clear()
228
+ for ex in EXAMPLES:
229
+ mnemo.add(ex)
230
+ return "Reset complete", do_stats()
231
 
 
232
 
233
+ # Simple Gradio interface
234
+ with gr.Blocks(title="Mnemo") as demo:
235
+ gr.Markdown("# 🧠 Mnemo: Semantic-Loop Memory\n*21x faster than mem0 | No API keys | Learns from feedback*")
236
 
237
  with gr.Row():
238
+ with gr.Column():
239
+ query_box = gr.Textbox(label="Search Query", placeholder="coffee preferences")
240
+ topk_slider = gr.Slider(1, 10, 5, step=1, label="Results")
 
241
  search_btn = gr.Button("Search", variant="primary")
242
+ results_box = gr.Textbox(label="Results", lines=12)
243
+
244
+ with gr.Column():
245
+ add_box = gr.Textbox(label="Add Memory", placeholder="New memory content")
246
+ add_btn = gr.Button("Add")
247
+ add_status = gr.Textbox(label="Status", lines=1)
248
 
249
+ gr.Markdown("---")
250
+ stats_box = gr.Textbox(label="Stats", value=do_stats(), lines=2)
251
+ refresh_btn = gr.Button("Refresh")
252
+ reset_btn = gr.Button("Reset")
253
+
254
+ with gr.Row():
255
+ fb_query = gr.Textbox(label="Feedback Query", scale=2)
256
+ fb_id = gr.Textbox(label="Memory ID", scale=2)
257
+ fb_score = gr.Slider(-1, 1, 0.5, step=0.1, label="Score", scale=1)
258
+ fb_btn = gr.Button("Record", scale=1)
259
+ fb_status = gr.Textbox(label="", scale=2)
260
+
261
+ gr.Markdown("**Benchmarks:** Search 0.27ms (mem0: 5.73ms) | Ingestion 0.8ms (mem0: 31ms)")
262
+
263
+ search_btn.click(do_search, [query_box, topk_slider], results_box)
264
+ query_box.submit(do_search, [query_box, topk_slider], results_box)
265
+ add_btn.click(do_add, add_box, [add_status, stats_box])
266
+ refresh_btn.click(do_stats, None, stats_box)
267
+ reset_btn.click(do_reset, None, [add_status, stats_box])
268
+ fb_btn.click(do_feedback, [fb_query, fb_id, fb_score], fb_status)
269
+
270
+ demo.launch()