macayaven commited on
Commit
c33e6fa
·
verified ·
1 Parent(s): ffbabf0

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +23 -20
  2. app.py +676 -612
  3. requirements.txt +4 -2
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🧠
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
- sdk_version: 4.0.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
@@ -12,34 +12,38 @@ license: mit
12
 
13
  # Docker Neural Memory
14
 
15
- **Memory that LEARNS, not just stores**
16
 
17
- This demo showcases containerized neural memory using Google's Titans architecture (Dec 2024). Unlike RAG/vector databases that just store and retrieve embeddings, this system's **weights actually update during inference**.
18
 
19
- ## Key Features
 
 
 
 
 
20
 
21
- - **Real Learning**: Weights change on every `observe()` call
22
- - **Pattern Recognition**: Surprise decreases as patterns are learned
23
- - **Bounded Capacity**: Fixed parameter count (doesn't grow like vector DBs)
24
- - **Docker-Native**: Designed for containerized deployment with persistent volumes
25
 
26
- ## How It Works
27
 
28
- ```
29
- Traditional Memory: Input Embed Store Retrieve (static)
30
- Neural Memory: Input Learn Update Weights Infer (dynamic)
31
- ```
32
 
33
- ## Demo Tabs
34
 
35
- 1. **Chat with Advocate**: Ask about the project and the developer
36
- 2. **Live Demo**: Watch weights change and surprise decrease
37
- 3. **Interactive**: Try observing your own content
38
- 4. **About Carlos**: Meet the developer
 
 
39
 
40
  ## Built By
41
 
42
- **Carlos Crespo Macaya** - AI Engineer specializing in GenAI Systems & Applied MLOps
43
 
44
  - 10+ years production ML experience
45
  - Expert in Docker, Kubernetes, MCP servers
@@ -50,5 +54,4 @@ Contact: macayaven@gmail.com
50
  ## Links
51
 
52
  - [GitHub Repository](https://github.com/macayaven/docker-neural-memory)
53
- - [Technical Specification](https://github.com/macayaven/docker-neural-memory/blob/main/SPEC.md)
54
  - [Titans Paper](https://arxiv.org/abs/2501.00663)
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 5.9.1
8
  app_file: app.py
9
  pinned: false
10
  license: mit
 
12
 
13
  # Docker Neural Memory
14
 
15
+ **Real Test-Time Training - Not a Simulation**
16
 
17
+ This demo runs **actual PyTorch** code implementing Google's Titans architecture. When you observe content, real gradients flow and real neural network weights update.
18
 
19
+ ## What Makes This Real
20
+
21
+ - **Real Neural Network**: 2-layer MLP with ~250K parameters
22
+ - **Real Gradient Descent**: `torch.autograd.grad()` computes gradients
23
+ - **Real Weight Updates**: Parameters physically change during inference
24
+ - **Real Surprise Metric**: MSE loss measures prediction error
25
 
26
+ ## Docker-Native Design
 
 
 
27
 
28
+ This project demonstrates production-grade AI infrastructure:
29
 
30
+ - **MCP Server**: Model Context Protocol for Claude Desktop integration
31
+ - **Docker Volumes**: Persist learned state across container restarts
32
+ - **CI/CD Pipeline**: GitHub Actions with Docker build and deploy
33
+ - **Kubernetes Ready**: Designed for orchestrated deployment
34
 
35
+ ## Key Features
36
 
37
+ | Feature | Implementation |
38
+ |---------|---------------|
39
+ | Test-Time Training | PyTorch autograd during inference |
40
+ | State Persistence | Docker volumes for checkpoints |
41
+ | MCP Integration | Tools: observe, surprise, checkpoint, restore |
42
+ | Bounded Memory | Fixed parameters (doesn't grow like vector DBs) |
43
 
44
  ## Built By
45
 
46
+ **Carlos Crespo Macaya** - AI Engineer
47
 
48
  - 10+ years production ML experience
49
  - Expert in Docker, Kubernetes, MCP servers
 
54
  ## Links
55
 
56
  - [GitHub Repository](https://github.com/macayaven/docker-neural-memory)
 
57
  - [Titans Paper](https://arxiv.org/abs/2501.00663)
app.py CHANGED
@@ -1,793 +1,857 @@
1
  """
2
- Docker Neural Memory - Interactive Learning Demo
3
 
4
- Step-by-step guided demo showing how Neural Memory (Titans) differs from RAG.
5
- Transparent visualization of: Surprise, Momentum, Forgetting, Learning.
6
 
7
  Deploy to: https://huggingface.co/spaces
8
  """
9
 
 
10
  import sys
 
 
11
  from pathlib import Path
 
12
 
13
  import gradio as gr
14
  import matplotlib
15
  import matplotlib.pyplot as plt
16
  import numpy as np
 
 
 
 
17
 
18
  matplotlib.use("Agg")
19
 
20
- # Add src to path
21
- sys.path.insert(0, str(Path(__file__).parent.parent.parent))
 
 
 
 
 
22
 
23
  try:
24
- from src.config import MemoryConfig # noqa: F401
25
- from src.memory.neural_memory import NeuralMemory # noqa: F401
26
-
27
- MEMORY_AVAILABLE = True
28
- except ImportError:
29
- MEMORY_AVAILABLE = False
30
-
31
-
32
- class NeuralMemoryDemo:
33
- """Neural Memory with full transparency for demo."""
34
-
35
- def __init__(self):
36
- self.reset()
37
-
38
- def reset(self):
39
- self._weights = np.random.randn(16, 16) * 0.1
40
- self._initial_weights = self._weights.copy()
41
- self._surprise_history = []
42
- self._momentum = 0.0
43
- self._momentum_history = []
44
- self._content_history = []
45
- self._weight_history = [self._weights.copy()]
46
- self._forgetting_applied = []
47
- self._observation_count = 0
48
-
49
- def observe(self, text: str) -> dict:
50
- """Observe content with full transparency."""
51
- self._observation_count += 1
52
-
53
- # Calculate surprise (gradient-based novelty)
54
- text_hash = sum(ord(c) for c in text) % 1000
55
- base_surprise = 0.9
56
-
57
- # Check similarity to previous content
58
- for prev_content in self._content_history:
59
- similarity = self._text_similarity(text, prev_content)
60
- base_surprise -= similarity * 0.3
61
-
62
- surprise = max(0.05, min(0.95, base_surprise))
63
-
64
- # Update momentum (exponential moving average of surprise)
65
- momentum_decay = 0.7
66
- self._momentum = momentum_decay * self._momentum + (1 - momentum_decay) * surprise
67
-
68
- # Adaptive forgetting (weight decay based on capacity)
69
- forgetting_rate = 0.02 * (1 + len(self._content_history) / 10)
70
- self._weights *= (1 - forgetting_rate)
71
- forgot_amount = forgetting_rate * np.abs(self._weights).mean()
72
-
73
- # Learning: update weights based on surprise
74
- if surprise > 0.3: # Only learn if surprising enough
75
- learning_rate = 0.05 * surprise
76
- delta = np.random.randn(16, 16) * learning_rate
77
- # Direction influenced by content
78
- np.random.seed(text_hash)
79
- direction = np.random.randn(16, 16)
80
- delta = delta * np.sign(direction)
81
- self._weights += delta
82
- learned = True
83
- else:
84
- delta = np.zeros((16, 16))
85
- learned = False
86
-
87
- # Record history
88
- self._surprise_history.append(surprise)
89
- self._momentum_history.append(self._momentum)
90
- self._content_history.append(text)
91
- self._weight_history.append(self._weights.copy())
92
- self._forgetting_applied.append(forgot_amount)
93
-
94
- return {
95
- "surprise": surprise,
96
- "momentum": self._momentum,
97
- "learned": learned,
98
- "forgot": forgot_amount,
99
- "weight_delta": np.abs(delta).mean(),
100
- "total_observations": self._observation_count,
101
- }
102
-
103
- def _text_similarity(self, text1: str, text2: str) -> float:
104
- """Simple word overlap similarity."""
105
- words1 = set(text1.lower().split())
106
- words2 = set(text2.lower().split())
107
- if not words1 or not words2:
108
- return 0.0
109
- overlap = len(words1 & words2)
110
- return overlap / max(len(words1), len(words2))
111
-
112
- def get_weights(self) -> np.ndarray:
113
- return self._weights.copy()
114
-
115
- def get_weight_change(self) -> np.ndarray:
116
- """Get total weight change from initial."""
117
- return self._weights - self._initial_weights
118
-
119
-
120
- class MockRAG:
121
- """RAG simulation - stores, doesn't learn."""
122
-
123
- def __init__(self):
124
- self.reset()
125
-
126
- def reset(self):
127
- self.vectors = []
128
- self.storage_size = 0
129
-
130
- def store(self, text: str) -> dict:
131
- """Store text (no learning, just accumulation)."""
132
- self.vectors.append(text)
133
- self.storage_size += len(text.encode())
134
- return {
135
- "similarity": 0.73, # Always same for same query
136
- "vector_count": len(self.vectors),
137
- "storage_bytes": self.storage_size,
138
- }
139
-
140
-
141
- # Global instances
142
- neural = NeuralMemoryDemo()
143
- rag = MockRAG()
144
-
145
-
146
- def reset_all():
147
- """Reset both systems."""
148
- neural.reset()
149
- rag.reset()
150
- return "Both systems reset. Ready to learn!"
151
 
 
 
152
 
153
  # =============================================================================
154
- # VISUALIZATION FUNCTIONS
155
  # =============================================================================
156
 
 
 
157
 
158
- def create_weight_heatmap(weights: np.ndarray, title: str = "Neural Weights") -> plt.Figure:
159
- """Create a heatmap of weights."""
160
- fig, ax = plt.subplots(figsize=(5, 4))
161
- im = ax.imshow(weights, cmap="RdBu_r", aspect="auto", vmin=-0.5, vmax=0.5)
162
- ax.set_title(title, fontsize=12, fontweight="bold")
163
- ax.axis("off")
164
- plt.colorbar(im, ax=ax, label="Weight Value")
165
- plt.tight_layout()
166
- return fig
167
 
 
 
 
168
 
169
- def create_surprise_gauge(surprise: float, momentum: float) -> plt.Figure:
170
- """Create surprise and momentum gauges."""
171
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 3))
172
-
173
- # Surprise gauge
174
- colors = ["#27ae60", "#f39c12", "#e74c3c"]
175
- surprise_color = colors[0] if surprise < 0.3 else (colors[1] if surprise < 0.6 else colors[2])
176
-
177
- theta = np.linspace(np.pi, 0, 100)
178
- ax1.plot(np.cos(theta), np.sin(theta), "k-", linewidth=2)
179
- ax1.fill_between(np.cos(theta), 0, np.sin(theta), alpha=0.1, color="gray")
180
-
181
- angle = np.pi * (1 - surprise)
182
- ax1.arrow(0, 0, 0.65 * np.cos(angle), 0.65 * np.sin(angle),
183
- head_width=0.08, head_length=0.04, fc=surprise_color, ec=surprise_color)
184
- ax1.plot(0, 0, "ko", markersize=8)
185
- ax1.text(-0.9, -0.15, "Familiar", ha="center", fontsize=9)
186
- ax1.text(0.9, -0.15, "Novel", ha="center", fontsize=9)
187
- ax1.text(0, 0.45, f"{surprise:.2f}", ha="center", fontsize=20, fontweight="bold", color=surprise_color)
188
- ax1.set_title("SURPRISE\n(How novel is this?)", fontsize=11, fontweight="bold")
189
- ax1.set_xlim(-1.2, 1.2)
190
- ax1.set_ylim(-0.3, 1.1)
191
- ax1.axis("off")
192
 
193
- # Momentum gauge
194
- momentum_color = colors[0] if momentum < 0.3 else (colors[1] if momentum < 0.6 else colors[2])
 
195
 
196
- ax2.plot(np.cos(theta), np.sin(theta), "k-", linewidth=2)
197
- ax2.fill_between(np.cos(theta), 0, np.sin(theta), alpha=0.1, color="gray")
 
 
 
198
 
199
- angle = np.pi * (1 - momentum)
200
- ax2.arrow(0, 0, 0.65 * np.cos(angle), 0.65 * np.sin(angle),
201
- head_width=0.08, head_length=0.04, fc=momentum_color, ec=momentum_color)
202
- ax2.plot(0, 0, "ko", markersize=8)
203
- ax2.text(-0.9, -0.15, "Stable", ha="center", fontsize=9)
204
- ax2.text(0.9, -0.15, "Active", ha="center", fontsize=9)
205
- ax2.text(0, 0.45, f"{momentum:.2f}", ha="center", fontsize=20, fontweight="bold", color=momentum_color)
206
- ax2.set_title("MOMENTUM\n(Recent activity level)", fontsize=11, fontweight="bold")
207
- ax2.set_xlim(-1.2, 1.2)
208
- ax2.set_ylim(-0.3, 1.1)
209
- ax2.axis("off")
210
 
211
- plt.tight_layout()
212
- return fig
213
 
 
214
 
215
- def create_history_plot() -> plt.Figure:
216
- """Create history of surprise and momentum."""
217
- fig, ax = plt.subplots(figsize=(8, 3))
218
 
219
- if neural._surprise_history:
220
- x = range(1, len(neural._surprise_history) + 1)
221
- ax.plot(x, neural._surprise_history, "o-", label="Surprise", color="#e74c3c", linewidth=2, markersize=8)
222
- ax.plot(x, neural._momentum_history, "s--", label="Momentum", color="#3498db", linewidth=2, markersize=6)
223
- ax.axhline(y=0.3, color="gray", linestyle=":", alpha=0.5, label="Learning threshold")
224
- ax.set_xlabel("Observation #", fontsize=10)
225
- ax.set_ylabel("Score", fontsize=10)
226
- ax.legend(loc="upper right")
227
- ax.set_ylim(0, 1)
228
- ax.grid(True, alpha=0.3)
229
- else:
230
- ax.text(0.5, 0.5, "No observations yet", ha="center", va="center", fontsize=12, color="gray")
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  ax.set_xlim(0, 1)
232
  ax.set_ylim(0, 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
- ax.set_title("Learning History", fontsize=12, fontweight="bold")
235
  plt.tight_layout()
236
  return fig
237
 
238
 
239
- def create_comparison_chart() -> plt.Figure:
240
- """Create side-by-side comparison."""
241
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
242
 
243
- # Neural Memory - Weight change visualization
244
- if len(neural._weight_history) > 1:
245
- change = neural.get_weight_change()
246
- im = ax1.imshow(change, cmap="RdBu_r", aspect="auto", vmin=-0.3, vmax=0.3)
247
- plt.colorbar(im, ax=ax1, label="Change from initial")
 
 
 
 
 
 
 
 
 
 
 
248
  else:
249
- ax1.text(0.5, 0.5, "No changes yet", ha="center", va="center", transform=ax1.transAxes)
 
250
 
251
- ax1.set_title(f"Neural Memory\n{neural._observation_count} observations, FIXED size", fontsize=11, fontweight="bold", color="#2980b9")
252
- ax1.axis("off")
 
 
 
 
253
 
254
- # RAG - Vector accumulation
255
- if rag.vectors:
256
- y_pos = np.arange(min(len(rag.vectors), 10))
257
- ax2.barh(y_pos, [len(v) for v in rag.vectors[-10:]], color="#95a5a6")
258
- ax2.set_yticks(y_pos)
259
- ax2.set_yticklabels([f"Vec {i+1}" for i in range(len(y_pos))])
260
- ax2.set_xlabel("Characters stored")
261
- else:
262
- ax2.text(0.5, 0.5, "No vectors stored", ha="center", va="center", transform=ax2.transAxes)
263
 
264
- ax2.set_title(f"RAG\n{len(rag.vectors)} vectors, {rag.storage_size} bytes (GROWING)", fontsize=11, fontweight="bold", color="#7f8c8d")
 
 
 
 
 
 
265
 
266
  plt.tight_layout()
267
  return fig
268
 
269
 
270
- # =============================================================================
271
- # GUIDED TOUR STEPS
272
- # =============================================================================
 
273
 
274
- TOUR_STEPS = [
275
- {
276
- "title": "Welcome: Two Types of Memory",
277
- "content": """## The Student Analogy
278
 
279
- Imagine two students taking an exam:
280
 
281
- **RAG (Retrieval)** = Student with a textbook
282
- - Has all the information available
283
- - Must look up each answer in the index
284
- - Slower, depends on finding exact matches
285
- - Book keeps growing with every new topic
286
 
287
- **Neural Memory (Titans)** = Student with photographic memory
288
- - Studies material just before (and during!) the exam
289
- - Synthesizes and integrates concepts
290
- - Responds fluidly without external lookup
291
- - Fixed brain capacity, but keeps learning
292
 
293
- **Click "Next Step" to see this in action!**
294
- """,
295
- "action": None,
296
- },
297
- {
298
- "title": "Step 1: First Observation",
299
- "content": """## Teaching Something New
300
 
301
- Let's teach both systems: **"Docker containers provide process isolation"**
 
 
 
 
 
 
 
302
 
303
- Watch what happens:
304
- - **Neural Memory**: Calculates SURPRISE (is this new?)
305
- - **RAG**: Just stores the vector (no thinking)
306
 
307
- **Click "Run This Step" to observe!**
308
- """,
309
- "action": "Docker containers provide process isolation",
310
- },
311
- {
312
- "title": "Step 2: Repetition = Learning",
313
- "content": """## Same Content Again
314
-
315
- Now we'll teach the SAME thing: **"Docker containers provide process isolation"**
316
-
317
- **Key insight**:
318
- - Neural Memory's SURPRISE will DROP (it recognizes this!)
319
- - RAG will just add another vector (no recognition)
320
 
321
- This is the fundamental difference: **learning vs storing**.
 
 
 
322
 
323
- **Click "Run This Step" to see surprise decrease!**
324
- """,
325
- "action": "Docker containers provide process isolation",
326
- },
327
- {
328
- "title": "Step 3: The Power of Momentum",
329
- "content": """## Momentum: Memory of Surprise
330
 
331
- MOMENTUM tracks surprise over time - it's like short-term memory of activity.
 
332
 
333
- Teaching again: **"Docker containers provide process isolation"**
 
 
 
 
 
 
 
334
 
335
- Watch:
336
- - Surprise: Very low now (familiar content)
337
- - Momentum: Decreasing (less overall activity)
338
 
339
- **Momentum helps capture the "flow" of events in a sequence.**
340
 
341
- **Click "Run This Step"!**
342
- """,
343
- "action": "Docker containers provide process isolation",
344
- },
345
- {
346
- "title": "Step 4: Generalization",
347
- "content": """## Can It Generalize?
348
-
349
- Now the real test - a PARAPHRASE: **"Containers isolate processes in Docker"**
350
-
351
- Same meaning, different words!
352
-
353
- - **Neural Memory**: Should recognize similarity (moderate surprise)
354
- - **RAG**: Treats it as completely new (just stores another vector)
355
 
356
- **This is why Titans beats RAG with 70x fewer parameters!**
357
-
358
- **Click "Run This Step"!**
359
- """,
360
- "action": "Containers isolate processes in Docker",
361
- },
362
- {
363
- "title": "Step 5: Adaptive Forgetting",
364
- "content": """## The Forgetting Mechanism
365
-
366
- Neural Memory doesn't just learn - it also FORGETS!
367
-
368
- Teaching something new: **"Kubernetes orchestrates container deployments"**
369
-
370
- Watch the "Forgot" metric - old, less relevant information decays.
371
-
372
- **Why forgetting matters:**
373
- - Prevents memory overflow
374
- - Keeps capacity bounded
375
- - Prioritizes recent/relevant info
376
- - Scales to 2M+ token windows!
377
-
378
- **Click "Run This Step"!**
379
- """,
380
- "action": "Kubernetes orchestrates container deployments",
381
- },
382
- {
383
- "title": "Step 6: What This Enables",
384
- "content": """## Capabilities Unlocked by Neural Memory
385
-
386
- These mechanisms enable powerful new functionalities:
387
 
388
- ### 1. Extreme Long Context (2M+ tokens)
389
- Process entire codebases, books, or document collections in a single pass.
390
- RAG struggles with context fragmentation; Neural Memory synthesizes continuously.
391
 
392
- ### 2. Test-Time Adaptation
393
- The model keeps learning DURING inference. Feed it your coding style,
394
- your domain terminology, your preferences - it adapts on the fly.
395
 
396
- ### 3. No Re-indexing Required
397
- Traditional RAG needs to re-embed documents when they change.
398
- Neural Memory learns incrementally - just observe the new content.
 
 
399
 
400
- ### 4. Privacy-Friendly Bounded Memory
401
- Fixed capacity means you control exactly how much is remembered.
402
- Old information naturally decays - no accumulating sensitive data forever.
403
 
404
- ### 5. Semantic Compression
405
- Instead of storing raw text, Neural Memory distills PATTERNS.
406
- This is why it achieves 98% accuracy with 70x fewer parameters than RAG.
407
 
408
- **Click "Next Step" to understand the trade-offs...**
409
- """,
410
- "action": None,
411
- },
412
- {
413
- "title": "Step 7: Honest Drawbacks",
414
- "content": """## When RAG Might Be Better
415
 
416
- No technology is perfect. Here's when Neural Memory has limitations:
 
 
 
 
 
417
 
418
- ### Drawbacks of Neural Memory:
 
419
 
420
- **1. Forgetting Can Lose Important Info**
421
- The adaptive forgetting mechanism might decay critical facts if not reinforced.
422
- RAG's explicit storage guarantees nothing is lost.
423
 
424
- **2. Less Interpretable**
425
- RAG can show you exactly which documents it retrieved.
426
- Neural Memory's knowledge is encoded in weights - harder to audit.
 
427
 
428
- **3. No Exact Retrieval**
429
- Need to quote a specific passage verbatim? RAG excels here.
430
- Neural Memory synthesizes - it may paraphrase or miss exact wording.
 
431
 
432
- **4. Compute Overhead**
433
- Online learning during inference adds computational cost.
434
- RAG's vector lookup is simpler and faster for basic retrieval.
 
 
 
 
 
 
435
 
436
- **5. Newer, Less Battle-Tested**
437
- RAG has years of production deployment experience.
438
- Neural Memory (Titans) is cutting-edge research (Dec 2024).
 
 
439
 
440
- ### The Right Choice Depends on Your Use Case:
441
- - **Use RAG** for: Exact quotes, audit trails, simple Q&A, proven stability
442
- - **Use Neural Memory** for: Long context, adaptation, compression, learning
443
 
444
- **Click "Next Step" for the summary!**
445
- """,
446
- "action": None,
447
- },
448
- {
449
- "title": "Summary: Making the Right Choice",
450
- "content": """## What You've Learned
451
 
452
- ### The Core Mechanisms
 
 
 
 
 
453
 
454
- | Feature | Neural Memory | RAG |
455
- |---------|--------------|-----|
456
- | **Surprise** | Measures novelty via gradients | N/A |
457
- | **Momentum** | Tracks activity over time | N/A |
458
- | **Forgetting** | Adaptive weight decay | Never forgets |
459
- | **Learning** | Continuous, during inference | None |
460
 
461
- ### When to Use Each
462
 
463
- | Use Case | Best Choice | Why |
464
- |----------|-------------|-----|
465
- | Long documents (2M+ tokens) | Neural Memory | Handles extreme context |
466
- | Exact quote retrieval | RAG | Explicit storage |
467
- | Adapting to user style | Neural Memory | Test-time learning |
468
- | Audit/compliance needs | RAG | Interpretable retrieval |
469
- | Resource-constrained | Neural Memory | 70x fewer parameters |
470
- | Production stability | RAG | Battle-tested |
471
 
472
- ### The Key Insight
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
- Neural Memory **LEARNS and FORGETS** like a brain.
475
- RAG **STORES and RETRIEVES** like a filing cabinet.
476
 
477
- Neither is universally better - choose based on your needs.
 
 
 
 
 
 
478
 
479
- **Try the Playground tab to experiment yourself!**
480
- """,
481
- "action": None,
482
- },
483
- ]
484
 
485
- current_step = {"index": 0}
 
 
 
 
 
486
 
487
 
488
- def get_current_step():
489
- """Get current tour step content."""
490
- step = TOUR_STEPS[current_step["index"]]
491
- return step["title"], step["content"]
492
 
493
 
494
- def run_step():
495
- """Execute the current step's action."""
496
- step = TOUR_STEPS[current_step["index"]]
 
 
 
497
 
498
- if step["action"] is None:
499
- return (
500
- "No action for this step - it's informational.",
501
- None, None, None, None
502
- )
503
 
504
- content = step["action"]
 
 
505
 
506
- # Run on both systems
507
- neural_result = neural.observe(content)
508
- rag_result = rag.store(content)
 
 
 
 
 
 
 
 
 
509
 
510
- # Create visualizations
511
- gauge_fig = create_surprise_gauge(neural_result["surprise"], neural_result["momentum"])
512
- weights_fig = create_weight_heatmap(neural.get_weights(), "Current Neural Weights")
513
- history_fig = create_history_plot()
514
- comparison_fig = create_comparison_chart()
515
 
516
- # Format result
517
- learned_text = "YES - weights updated!" if neural_result["learned"] else "NO - too familiar"
518
- result_text = f"""### Results for: "{content}"
519
-
520
- **Neural Memory:**
521
- - Surprise: {neural_result['surprise']:.3f} ({"Novel!" if neural_result['surprise'] > 0.6 else "Familiar" if neural_result['surprise'] < 0.3 else "Moderate"})
522
- - Momentum: {neural_result['momentum']:.3f}
523
- - Learned: {learned_text}
524
- - Forgot: {neural_result['forgot']:.4f} (weight decay applied)
525
-
526
- **RAG:**
527
- - Similarity: {rag_result['similarity']:.2f} (always the same!)
528
- - Vectors stored: {rag_result['vector_count']}
529
- - Storage: {rag_result['storage_bytes']} bytes (growing!)
530
- """
531
 
532
- return result_text, gauge_fig, weights_fig, history_fig, comparison_fig
 
 
 
 
 
 
 
 
 
 
 
 
 
533
 
 
 
 
534
 
535
- def next_step():
536
- """Go to next step."""
537
- if current_step["index"] < len(TOUR_STEPS) - 1:
538
- current_step["index"] += 1
539
- return get_current_step()
540
 
 
 
 
541
 
542
- def prev_step():
543
- """Go to previous step."""
544
- if current_step["index"] > 0:
545
- current_step["index"] -= 1
546
- return get_current_step()
547
 
 
 
 
 
 
 
548
 
549
- def reset_tour():
550
- """Reset tour to beginning."""
551
- current_step["index"] = 0
552
- reset_all()
553
- return get_current_step()
554
 
 
 
555
 
556
- # =============================================================================
557
- # PLAYGROUND FUNCTIONS
558
- # =============================================================================
559
 
 
 
 
 
 
 
 
560
 
561
- def playground_observe(content: str):
562
- """Observe content in playground mode."""
563
- if not content.strip():
564
- return "Please enter some content.", None, None, None
565
 
566
- neural_result = neural.observe(content)
567
- rag_result = rag.store(content)
568
 
569
- gauge_fig = create_surprise_gauge(neural_result["surprise"], neural_result["momentum"])
570
- history_fig = create_history_plot()
571
- comparison_fig = create_comparison_chart()
572
 
573
- learned_text = "YES" if neural_result["learned"] else "NO (below threshold)"
574
- result = f"""### Observation Results
 
 
 
 
 
575
 
576
- **Content:** "{content[:50]}{'...' if len(content) > 50 else ''}"
577
 
578
- | Metric | Neural Memory | RAG |
579
- |--------|--------------|-----|
580
- | Novelty | Surprise: {neural_result['surprise']:.3f} | Similarity: {rag_result['similarity']:.2f} |
581
- | Action | Learned: {learned_text} | Stored vector #{rag_result['vector_count']} |
582
- | Memory | Forgot: {neural_result['forgot']:.4f} | +{len(content)} bytes |
583
- | Capacity | Fixed parameters | {rag_result['storage_bytes']} bytes total |
584
 
585
- **Interpretation:**
586
- {"🔴 HIGH surprise - this is novel content, worth learning!" if neural_result['surprise'] > 0.6 else "🟡 MODERATE surprise - somewhat familiar content." if neural_result['surprise'] > 0.3 else "🟢 LOW surprise - very familiar, minimal learning needed."}
587
  """
588
- return result, gauge_fig, history_fig, comparison_fig
589
 
 
590
 
591
- # =============================================================================
592
- # METRICS & USE CASES
593
- # =============================================================================
594
 
595
- USE_CASES_MD = """
596
- ## Use Cases & Evidence
 
 
597
 
598
- ### 1. Long-Context Understanding (2M+ tokens)
599
- | Model | Accuracy at 2M tokens | Memory Type |
600
- |-------|----------------------|-------------|
601
- | Titans (MAC) | **98.2%** | Neural Memory |
602
- | Llama 3.1 8B + RAG | 71.3% | Retrieval |
603
- | GPT-4 Turbo | 54.1% | Fixed Context |
604
 
605
- *Source: Titans paper, needle-in-haystack benchmark*
606
 
607
- ---
608
 
609
- ### 2. Parameter Efficiency
610
- | Model | Parameters | BABILong Score |
611
- |-------|-----------|----------------|
612
- | Titans-MAC | **760M** | 93.2% |
613
- | Llama 3.1 + RAG | 8B (10x more) | 89.1% |
614
 
615
- **Neural Memory achieves better results with 70x fewer parameters!**
 
 
 
616
 
617
- ---
 
618
 
619
- ### 3. Continuous Learning
620
- | Scenario | RAG | Neural Memory |
621
- |----------|-----|---------------|
622
- | Same fact 3x | 3 vectors stored | Surprise: 0.9 → 0.2 |
623
- | Paraphrase | New vector (no recognition) | Recognized (moderate surprise) |
624
- | After 1000 facts | 1000 vectors | Same fixed capacity |
625
 
626
- ---
 
 
627
 
628
- ### 4. Real-World Applications
629
 
630
- **Code Assistant Memory:**
631
- - Remember coding patterns across sessions
632
- - Learn project-specific conventions
633
- - Forget outdated patterns automatically
 
 
 
 
634
 
635
- **Document Analysis:**
636
- - Process entire codebases (2M+ tokens)
637
- - Learn document structure on-the-fly
638
- - No re-indexing needed
639
 
640
- **Conversational AI:**
641
- - Remember user preferences
642
- - Adapt to communication style
643
- - Bounded memory (privacy-friendly)
644
 
645
- ---
 
 
646
 
647
- ### Key Metrics Explained
648
 
649
- | Metric | What It Measures | Why It Matters |
650
- |--------|-----------------|----------------|
651
- | **Surprise** | Gradient-based novelty | Decides what to learn |
652
- | **Momentum** | Surprise over time | Captures event flow |
653
- | **Forgetting** | Weight decay rate | Prevents overflow |
654
- | **Weight Delta** | Learning magnitude | Shows active learning |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
655
 
656
  ---
657
 
658
- *Based on Google's Titans paper (Dec 2024) and TTT research (Jul 2024)*
659
  """
660
 
 
 
661
 
662
- # =============================================================================
663
- # GRADIO INTERFACE
664
- # =============================================================================
665
 
666
- with gr.Blocks(title="Neural Memory vs RAG - Interactive Demo", theme=gr.themes.Soft()) as demo:
667
- gr.Markdown("""
668
- # Neural Memory vs RAG
669
- ## Memory that LEARNS vs Memory that STORES
670
 
671
- An interactive, step-by-step guide to understanding the difference.
672
- """)
 
 
673
 
674
- with gr.Tabs():
675
- # TAB 1: GUIDED TOUR
676
- with gr.TabItem("📚 Learn (Guided Tour)"):
677
- gr.Markdown("### Follow along step-by-step to understand the key concepts")
678
 
679
- with gr.Row():
680
- with gr.Column(scale=1):
681
- step_title = gr.Markdown(value=f"## {TOUR_STEPS[0]['title']}")
682
- step_content = gr.Markdown(value=TOUR_STEPS[0]["content"])
683
 
684
- with gr.Row():
685
- prev_btn = gr.Button("← Previous", variant="secondary", size="sm")
686
- run_btn = gr.Button("▶ Run This Step", variant="primary", size="lg")
687
- next_btn = gr.Button("Next →", variant="secondary", size="sm")
688
 
689
- reset_tour_btn = gr.Button("🔄 Restart Tour", variant="secondary", size="sm")
690
- step_result = gr.Markdown()
691
 
692
- with gr.Column(scale=1):
693
- gauge_plot = gr.Plot(label="Surprise & Momentum")
694
- weights_plot = gr.Plot(label="Neural Weights")
695
 
696
- with gr.Row():
697
- history_plot = gr.Plot(label="Learning History")
698
- comparison_plot = gr.Plot(label="Neural vs RAG Comparison")
699
 
700
- # Event handlers
701
- def update_step_display(title, content):
702
- return f"## {title}", content
703
 
704
- prev_btn.click(prev_step, outputs=[step_title, step_content])
705
- next_btn.click(next_step, outputs=[step_title, step_content])
706
- run_btn.click(run_step, outputs=[step_result, gauge_plot, weights_plot, history_plot, comparison_plot])
707
- reset_tour_btn.click(reset_tour, outputs=[step_title, step_content])
708
 
709
- # TAB 2: PLAYGROUND
710
- with gr.TabItem("🎮 Playground"):
711
- gr.Markdown("""
712
- ### Experiment Freely
 
713
 
714
- Try your own content and see how both systems respond!
715
 
716
- **Suggestions to try:**
717
- 1. Enter the same text multiple times → watch surprise drop
718
- 2. Try paraphrases → see if neural memory recognizes them
719
- 3. Enter completely new topics → see high surprise
720
- 4. Watch the history graph build up
721
- """)
722
 
723
- with gr.Row():
724
- playground_input = gr.Textbox(
725
- label="Content to observe",
726
- placeholder="Enter any text... try repeating it!",
727
- lines=2
728
- )
729
- playground_btn = gr.Button("Observe", variant="primary", size="lg")
730
 
731
- playground_result = gr.Markdown()
 
 
732
 
733
- with gr.Row():
734
- pg_gauge = gr.Plot(label="Surprise & Momentum")
735
- pg_history = gr.Plot(label="Learning History")
 
736
 
737
- pg_comparison = gr.Plot(label="Neural vs RAG Comparison")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
 
739
  with gr.Row():
740
- reset_pg_btn = gr.Button("🔄 Reset Both Systems", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
 
742
- playground_btn.click(
743
- playground_observe,
744
- inputs=[playground_input],
745
- outputs=[playground_result, pg_gauge, pg_history, pg_comparison]
 
 
 
 
746
  )
747
- reset_pg_btn.click(reset_all, outputs=[playground_result])
748
 
749
- # TAB 3: USE CASES & METRICS
750
- with gr.TabItem("📊 Evidence & Use Cases"):
751
- gr.Markdown(USE_CASES_MD)
752
 
753
- # TAB 4: ABOUT
754
- with gr.TabItem("About"):
755
- gr.Markdown("""
756
- ## About This Demo
 
757
 
758
- **Docker Neural Memory** implements test-time training (TTT) memory based on Google's Titans architecture.
 
 
759
 
760
- ### Key Papers
761
- - [Titans: Learning to Memorize at Test Time](https://arxiv.org/abs/2501.00663) (Dec 2024)
762
- - [Learning to Learn at Test Time](https://arxiv.org/abs/2407.04620) (Jul 2024)
 
 
763
 
764
- ### The Core Innovation
765
 
766
- Traditional AI memory (RAG) is like a **filing cabinet**:
767
- - Store documents → Retrieve by similarity → No learning
 
 
 
 
 
 
 
768
 
769
- Neural Memory (Titans) is like a **brain**:
770
- - Observe content → Update weights (learn) Forget old info → Generalize
 
771
 
772
- ### Built By
 
 
 
 
 
 
 
 
773
 
774
- **Carlos Crespo Macaya**
775
- AI Engineer - GenAI Systems & Applied MLOps
776
 
777
- - 10+ years production ML experience
778
- - Expert in Docker, Kubernetes, MCP servers
779
- - Currently building AI systems at HP AICoE
780
 
781
- 📧 [macayaven@gmail.com](mailto:macayaven@gmail.com)
 
 
 
 
782
 
783
- ---
784
 
785
- *This project demonstrates the ability to take cutting-edge research and ship production-ready infrastructure.*
786
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
 
788
  gr.Markdown("""
789
  ---
790
- *Docker Neural Memory - Containerized AI memory that actually learns*
791
 
792
  [GitHub](https://github.com/macayaven/docker-neural-memory) |
793
  [Contact](mailto:macayaven@gmail.com)
 
1
  """
2
+ Docker Neural Memory - Production Demo
3
 
4
+ REAL neural memory implementation using Titans architecture.
5
+ Demonstrates Docker-native AI memory with MCP server integration.
6
 
7
  Deploy to: https://huggingface.co/spaces
8
  """
9
 
10
+ import os
11
  import sys
12
+ import time
13
+ from dataclasses import dataclass, field
14
  from pathlib import Path
15
+ from typing import Dict, List, Optional, Tuple
16
 
17
  import gradio as gr
18
  import matplotlib
19
  import matplotlib.pyplot as plt
20
  import numpy as np
21
+ import torch
22
+ from huggingface_hub import InferenceClient
23
+ from sklearn.manifold import TSNE
24
+ from sklearn.decomposition import PCA
25
 
26
  matplotlib.use("Agg")
27
 
28
+ # =============================================================================
29
+ # HUGGINGFACE INFERENCE CLIENT
30
+ # =============================================================================
31
+
32
+ # Use a free model - Mistral or Qwen work well
33
+ HF_MODEL = os.getenv("HF_MODEL", "mistralai/Mistral-7B-Instruct-v0.3")
34
+ HF_TOKEN = os.getenv("HF_TOKEN", None) # Optional - works without for many models
35
 
36
  try:
37
+ hf_client = InferenceClient(model=HF_MODEL, token=HF_TOKEN)
38
+ LLM_AVAILABLE = True
39
+ except Exception as e:
40
+ print(f"Warning: Could not initialize HF client: {e}")
41
+ hf_client = None
42
+ LLM_AVAILABLE = False
43
+
44
+ # Add src to path for real implementation
45
+ sys.path.insert(0, str(Path(__file__).parent))
46
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
+ from src.config import MemoryConfig
49
+ from src.memory.neural_memory import NeuralMemory
50
 
51
  # =============================================================================
52
+ # REAL NEURAL MEMORY INSTANCE
53
  # =============================================================================
54
 
55
+ # Initialize the REAL neural memory - this is actual PyTorch, not a simulation
56
+ memory = NeuralMemory(MemoryConfig(dim=256, learning_rate=0.02))
57
 
58
+ # Track history for visualization
59
+ observation_history: List[Dict] = []
 
 
 
 
 
 
 
60
 
61
+ # =============================================================================
62
+ # COMPARISON METRICS & KNOWLEDGE BASE
63
+ # =============================================================================
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ @dataclass
67
+ class ComparisonMetrics:
68
+ """Track comparison between vanilla and memory-augmented responses."""
69
 
70
+ # With Neural Memory
71
+ nm_queries: int = 0
72
+ nm_correct: int = 0
73
+ nm_hallucinations: int = 0
74
+ nm_response_times: List[float] = field(default_factory=list)
75
 
76
+ # Vanilla (no memory)
77
+ vanilla_queries: int = 0
78
+ vanilla_correct: int = 0
79
+ vanilla_hallucinations: int = 0
80
+ vanilla_response_times: List[float] = field(default_factory=list)
 
 
 
 
 
 
81
 
 
 
82
 
83
+ metrics = ComparisonMetrics()
84
 
85
+ # Knowledge base - facts the user teaches
86
+ knowledge_base: List[Dict[str, str]] = []
 
87
 
88
+ # Store embeddings for t-SNE visualization
89
+ embeddings_store: List[Dict] = []
90
+
91
+
92
+ def get_embedding(text: str) -> np.ndarray:
93
+ """Get the neural memory's internal representation of text."""
94
+ with torch.no_grad():
95
+ # Convert text to tensor using memory's encoding
96
+ tensor = memory._text_to_tensor(text)
97
+ # Pass through memory network to get learned representation
98
+ output = memory.memory_net(tensor)
99
+ # Return flattened representation
100
+ return output.cpu().numpy().flatten()
101
+
102
+
103
+ def create_tsne_visualization() -> plt.Figure:
104
+ """Create t-SNE visualization of learned representations."""
105
+ fig, ax = plt.subplots(figsize=(10, 8))
106
+
107
+ if len(embeddings_store) < 2:
108
+ ax.text(
109
+ 0.5, 0.5,
110
+ "Add at least 2 facts to see the embedding space",
111
+ ha="center", va="center", fontsize=14, color="gray"
112
+ )
113
  ax.set_xlim(0, 1)
114
  ax.set_ylim(0, 1)
115
+ ax.axis("off")
116
+ return fig
117
+
118
+ # Extract embeddings and labels
119
+ embeddings = np.array([e["embedding"] for e in embeddings_store])
120
+ labels = [e["label"][:30] + "..." if len(e["label"]) > 30 else e["label"]
121
+ for e in embeddings_store]
122
+ surprises = [e["surprise"] for e in embeddings_store]
123
+
124
+ # Use PCA if few samples, t-SNE otherwise
125
+ n_samples = len(embeddings)
126
+ if n_samples < 5:
127
+ # PCA for small sample sizes
128
+ reducer = PCA(n_components=2)
129
+ reduced = reducer.fit_transform(embeddings)
130
+ method = "PCA"
131
+ else:
132
+ # t-SNE for larger sample sizes
133
+ perplexity = min(30, n_samples - 1)
134
+ reducer = TSNE(n_components=2, perplexity=perplexity, random_state=42)
135
+ reduced = reducer.fit_transform(embeddings)
136
+ method = "t-SNE"
137
+
138
+ # Color by surprise (red = high surprise/novel, blue = low surprise/familiar)
139
+ colors = plt.cm.RdYlBu_r(surprises)
140
+
141
+ # Plot points
142
+ scatter = ax.scatter(
143
+ reduced[:, 0], reduced[:, 1],
144
+ c=surprises, cmap="RdYlBu_r",
145
+ s=150, alpha=0.7, edgecolors="white", linewidth=2
146
+ )
147
+
148
+ # Add labels
149
+ for i, label in enumerate(labels):
150
+ ax.annotate(
151
+ label, (reduced[i, 0], reduced[i, 1]),
152
+ xytext=(5, 5), textcoords="offset points",
153
+ fontsize=9, alpha=0.8,
154
+ bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.7)
155
+ )
156
+
157
+ # Colorbar
158
+ cbar = plt.colorbar(scatter, ax=ax)
159
+ cbar.set_label("Surprise (Red=Novel, Blue=Familiar)", fontsize=10)
160
+
161
+ ax.set_title(f"Neural Memory Embedding Space ({method})\n"
162
+ f"{n_samples} observations - Similar concepts cluster together",
163
+ fontsize=12, fontweight="bold")
164
+ ax.set_xlabel("Dimension 1")
165
+ ax.set_ylabel("Dimension 2")
166
+ ax.grid(True, alpha=0.3)
167
 
 
168
  plt.tight_layout()
169
  return fig
170
 
171
 
172
+ def create_embedding_comparison() -> plt.Figure:
173
+ """Create side-by-side: weight heatmap + embedding space."""
174
+ fig, axes = plt.subplots(1, 2, figsize=(14, 6))
175
 
176
+ # Left: Weight heatmap
177
+ ax1 = axes[0]
178
+ weights = get_weight_sample()
179
+ im = ax1.imshow(weights, cmap="RdBu_r", aspect="auto", vmin=-0.5, vmax=0.5)
180
+ ax1.set_title("Neural Network Weights\n(These update during learning)",
181
+ fontsize=11, fontweight="bold")
182
+ ax1.axis("off")
183
+ plt.colorbar(im, ax=ax1, label="Weight Value")
184
+
185
+ # Right: Embedding space (simplified if few points)
186
+ ax2 = axes[1]
187
+ if len(embeddings_store) < 2:
188
+ ax2.text(0.5, 0.5, "Add facts to see\nembedding space",
189
+ ha="center", va="center", fontsize=12, color="gray")
190
+ ax2.set_xlim(0, 1)
191
+ ax2.set_ylim(0, 1)
192
  else:
193
+ embeddings = np.array([e["embedding"] for e in embeddings_store])
194
+ surprises = [e["surprise"] for e in embeddings_store]
195
 
196
+ n_samples = len(embeddings)
197
+ if n_samples < 5:
198
+ reducer = PCA(n_components=2)
199
+ else:
200
+ perplexity = min(30, n_samples - 1)
201
+ reducer = TSNE(n_components=2, perplexity=perplexity, random_state=42)
202
 
203
+ reduced = reducer.fit_transform(embeddings)
 
 
 
 
 
 
 
 
204
 
205
+ scatter = ax2.scatter(reduced[:, 0], reduced[:, 1], c=surprises,
206
+ cmap="RdYlBu_r", s=100, alpha=0.7)
207
+ plt.colorbar(scatter, ax=ax2, label="Surprise")
208
+ ax2.grid(True, alpha=0.3)
209
+
210
+ ax2.set_title("Learned Representations\n(Similar facts cluster together)",
211
+ fontsize=11, fontweight="bold")
212
 
213
  plt.tight_layout()
214
  return fig
215
 
216
 
217
+ def call_llm(prompt: str, context: str = "") -> Tuple[str, float]:
218
+ """Call HuggingFace LLM. Returns (response, time)."""
219
+ if not LLM_AVAILABLE or hf_client is None:
220
+ return "[LLM not available - set HF_TOKEN for comparison demo]", 0.0
221
 
222
+ try:
223
+ full_prompt = prompt
224
+ if context:
225
+ full_prompt = f"""You have access to the following knowledge:
226
 
227
+ {context}
228
 
229
+ Based ONLY on the knowledge above, answer this question. If the information is not in the knowledge provided, say "I don't have information about that."
 
 
 
 
230
 
231
+ Question: {prompt}
 
 
 
 
232
 
233
+ Answer:"""
 
 
 
 
 
 
234
 
235
+ start = time.time()
236
+ response = hf_client.text_generation(
237
+ full_prompt,
238
+ max_new_tokens=150,
239
+ temperature=0.7,
240
+ do_sample=True,
241
+ )
242
+ elapsed = time.time() - start
243
 
244
+ return response.strip(), elapsed
245
+ except Exception as e:
246
+ return f"Error: {str(e)}", 0.0
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
+ def add_to_knowledge_base(fact: str) -> Tuple[str, plt.Figure]:
250
+ """Add a fact to the knowledge base and observe it in neural memory."""
251
+ if not fact.strip():
252
+ return "Please enter a fact to add.", create_tsne_visualization()
253
 
254
+ # Add to knowledge base
255
+ knowledge_base.append({"fact": fact, "timestamp": time.time()})
 
 
 
 
 
256
 
257
+ # Observe in neural memory
258
+ result = memory.observe(fact)
259
 
260
+ # Store embedding for visualization
261
+ embedding = get_embedding(fact)
262
+ embeddings_store.append({
263
+ "label": fact,
264
+ "embedding": embedding,
265
+ "surprise": result["surprise"],
266
+ "timestamp": time.time(),
267
+ })
268
 
269
+ output = f"""### Fact Added
 
 
270
 
271
+ **Fact:** "{fact}"
272
 
273
+ **Neural Memory Response:**
274
+ - Surprise: {result['surprise']:.4f}
275
+ - Weight Delta: {result['weight_delta']:.6f}
276
+ - Learned: {'Yes' if result['learned'] else 'No'}
 
 
 
 
 
 
 
 
 
 
277
 
278
+ **Knowledge Base Size:** {len(knowledge_base)} facts
279
+ **Embeddings Stored:** {len(embeddings_store)}
280
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
+ return output, create_tsne_visualization()
 
 
283
 
 
 
 
284
 
285
+ def get_knowledge_context() -> str:
286
+ """Get all facts as context string."""
287
+ if not knowledge_base:
288
+ return ""
289
+ return "\n".join([f"- {item['fact']}" for item in knowledge_base])
290
 
 
 
 
291
 
292
+ def compare_responses(question: str) -> Tuple[str, str, str]:
293
+ """Compare vanilla LLM vs memory-augmented LLM on the same question."""
294
+ global metrics
295
 
296
+ if not question.strip():
297
+ return "", "", ""
 
 
 
 
 
298
 
299
+ if not LLM_AVAILABLE:
300
+ return (
301
+ "LLM not available. Please set HF_TOKEN environment variable.",
302
+ "LLM not available.",
303
+ "Comparison requires LLM access.",
304
+ )
305
 
306
+ # Get context from knowledge base
307
+ context = get_knowledge_context()
308
 
309
+ # Check surprise (is this question familiar?)
310
+ surprise = memory.surprise(question)
 
311
 
312
+ # Query WITH memory context
313
+ nm_response, nm_time = call_llm(question, context)
314
+ metrics.nm_queries += 1
315
+ metrics.nm_response_times.append(nm_time)
316
 
317
+ # Query WITHOUT memory context (vanilla)
318
+ vanilla_response, vanilla_time = call_llm(question)
319
+ metrics.vanilla_queries += 1
320
+ metrics.vanilla_response_times.append(vanilla_time)
321
 
322
+ # Simple hallucination detection (if answer is too confident without knowledge)
323
+ vanilla_hedges = any(
324
+ phrase in vanilla_response.lower()
325
+ for phrase in ["i don't know", "i don't have", "i'm not sure", "cannot"]
326
+ )
327
+ nm_hedges = any(
328
+ phrase in nm_response.lower()
329
+ for phrase in ["i don't know", "i don't have", "i'm not sure", "cannot"]
330
+ )
331
 
332
+ # If knowledge base has relevant info and vanilla doesn't hedge, likely hallucinating
333
+ if knowledge_base and not vanilla_hedges:
334
+ metrics.vanilla_hallucinations += 1
335
+ if not nm_hedges and context:
336
+ metrics.nm_correct += 1
337
 
338
+ # Format outputs
339
+ nm_output = f"""### With Neural Memory
 
340
 
341
+ {nm_response}
 
 
 
 
 
 
342
 
343
+ ---
344
+ **Metrics:**
345
+ - Surprise: {surprise:.3f}
346
+ - Response Time: {nm_time:.2f}s
347
+ - Knowledge Used: {len(knowledge_base)} facts
348
+ """
349
 
350
+ vanilla_output = f"""### Vanilla LLM (No Memory)
 
 
 
 
 
351
 
352
+ {vanilla_response}
353
 
354
+ ---
355
+ **Metrics:**
356
+ - Response Time: {vanilla_time:.2f}s
357
+ - No context provided
358
+ """
 
 
 
359
 
360
+ # Comparison summary
361
+ comparison = get_comparison_summary()
362
+
363
+ return nm_output, vanilla_output, comparison
364
+
365
+
366
+ def get_comparison_summary() -> str:
367
+ """Generate comparison metrics summary."""
368
+ nm_avg_time = (
369
+ sum(metrics.nm_response_times) / len(metrics.nm_response_times)
370
+ if metrics.nm_response_times
371
+ else 0
372
+ )
373
+ vanilla_avg_time = (
374
+ sum(metrics.vanilla_response_times) / len(metrics.vanilla_response_times)
375
+ if metrics.vanilla_response_times
376
+ else 0
377
+ )
378
+
379
+ nm_accuracy = (
380
+ metrics.nm_correct / metrics.nm_queries * 100 if metrics.nm_queries else 0
381
+ )
382
+ vanilla_halluc_rate = (
383
+ metrics.vanilla_hallucinations / metrics.vanilla_queries * 100
384
+ if metrics.vanilla_queries
385
+ else 0
386
+ )
387
+
388
+ return f"""## Comparison Summary
389
+
390
+ | Metric | With Neural Memory | Vanilla LLM |
391
+ |--------|-------------------|-------------|
392
+ | **Queries** | {metrics.nm_queries} | {metrics.vanilla_queries} |
393
+ | **Grounded Answers** | {metrics.nm_correct} ({nm_accuracy:.0f}%) | N/A |
394
+ | **Potential Hallucinations** | {metrics.nm_hallucinations} | {metrics.vanilla_hallucinations} ({vanilla_halluc_rate:.0f}%) |
395
+ | **Avg Response Time** | {nm_avg_time:.2f}s | {vanilla_avg_time:.2f}s |
396
+
397
+ ### Knowledge Base
398
+ {len(knowledge_base)} facts stored
399
+
400
+ ### Key Insight
401
+ - **Neural Memory** grounds responses in observed facts
402
+ - **Vanilla LLM** may hallucinate without context
403
+ - Surprise score indicates how novel the question is
404
+ """
405
 
 
 
406
 
407
+ def reset_comparison() -> Tuple[str, plt.Figure]:
408
+ """Reset comparison metrics and knowledge base."""
409
+ global metrics, knowledge_base, embeddings_store
410
+ metrics = ComparisonMetrics()
411
+ knowledge_base = []
412
+ embeddings_store = []
413
+ return "Comparison reset. Knowledge base and embeddings cleared.", create_tsne_visualization()
414
 
 
 
 
 
 
415
 
416
+ def reset_memory():
417
+ """Reset to fresh memory state."""
418
+ global memory, observation_history
419
+ memory = NeuralMemory(MemoryConfig(dim=256, learning_rate=0.02))
420
+ observation_history = []
421
+ return "Memory reset. Fresh neural network initialized."
422
 
423
 
424
+ # =============================================================================
425
+ # VISUALIZATION
426
+ # =============================================================================
 
427
 
428
 
429
+ def get_weight_sample() -> np.ndarray:
430
+ """Extract 16x16 sample of actual neural weights."""
431
+ with torch.no_grad():
432
+ # Get weights from first linear layer
433
+ weights = memory.memory_net[0].weight.data[:16, :16]
434
+ return weights.cpu().numpy()
435
 
 
 
 
 
 
436
 
437
+ def create_weight_visualization() -> plt.Figure:
438
+ """Visualize actual neural network weights."""
439
+ weights = get_weight_sample()
440
 
441
+ fig, ax = plt.subplots(figsize=(6, 5))
442
+ im = ax.imshow(weights, cmap="RdBu_r", aspect="auto", vmin=-0.5, vmax=0.5)
443
+ ax.set_title(
444
+ f"Neural Memory Weights\n({sum(p.numel() for p in memory.memory_net.parameters()):,} parameters)",
445
+ fontsize=12,
446
+ fontweight="bold",
447
+ )
448
+ ax.set_xlabel("These weights UPDATE during inference (TTT)")
449
+ ax.axis("off")
450
+ plt.colorbar(im, ax=ax, label="Weight Value")
451
+ plt.tight_layout()
452
+ return fig
453
 
 
 
 
 
 
454
 
455
+ def create_history_plot() -> plt.Figure:
456
+ """Plot surprise history."""
457
+ fig, ax = plt.subplots(figsize=(8, 3))
 
 
 
 
 
 
 
 
 
 
 
 
458
 
459
+ if observation_history:
460
+ surprises = [h["surprise"] for h in observation_history]
461
+ x = range(1, len(surprises) + 1)
462
+ ax.plot(x, surprises, "o-", color="#e74c3c", linewidth=2, markersize=8)
463
+ ax.axhline(y=0.5, color="gray", linestyle="--", alpha=0.5, label="Threshold")
464
+ ax.set_xlabel("Observation #")
465
+ ax.set_ylabel("Surprise")
466
+ ax.set_ylim(0, 1)
467
+ ax.grid(True, alpha=0.3)
468
+ ax.legend()
469
+ else:
470
+ ax.text(0.5, 0.5, "No observations yet", ha="center", va="center", fontsize=12, color="gray")
471
+ ax.set_xlim(0, 1)
472
+ ax.set_ylim(0, 1)
473
 
474
+ ax.set_title("Learning Progress (Surprise Over Time)", fontsize=12, fontweight="bold")
475
+ plt.tight_layout()
476
+ return fig
477
 
 
 
 
 
 
478
 
479
+ # =============================================================================
480
+ # CORE MEMORY OPERATIONS
481
+ # =============================================================================
482
 
 
 
 
 
 
483
 
484
+ def observe_content(content: str) -> tuple[str, plt.Figure, plt.Figure]:
485
+ """
486
+ Feed content to REAL neural memory - triggers actual gradient updates.
487
+ """
488
+ if not content.strip():
489
+ return "Please enter content to observe.", None, None
490
 
491
+ # Get weight hash BEFORE
492
+ hash_before = memory.get_weight_hash()
 
 
 
493
 
494
+ # REAL observation with actual gradient descent
495
+ result = memory.observe(content)
496
 
497
+ # Get weight hash AFTER
498
+ hash_after = memory.get_weight_hash()
 
499
 
500
+ # Record history
501
+ observation_history.append({
502
+ "content": content[:50],
503
+ "surprise": result["surprise"],
504
+ "weight_delta": result["weight_delta"],
505
+ "learned": result["learned"],
506
+ })
507
 
508
+ # Format result
509
+ weights_changed = hash_before != hash_after
510
+ output = f"""## Observation Result
 
511
 
512
+ **Content:** "{content[:100]}{'...' if len(content) > 100 else ''}"
 
513
 
514
+ ### Metrics (REAL - from PyTorch gradient descent)
 
 
515
 
516
+ | Metric | Value |
517
+ |--------|-------|
518
+ | **Surprise** | {result['surprise']:.4f} |
519
+ | **Weight Delta** | {result['weight_delta']:.6f} |
520
+ | **Weights Changed** | {'YES' if weights_changed else 'NO'} |
521
+ | **Hash Before** | `{hash_before}` |
522
+ | **Hash After** | `{hash_after}` |
523
 
524
+ ### What Just Happened
525
 
526
+ 1. Text was encoded to tensor representation
527
+ 2. Forward pass through neural memory network
528
+ 3. **Surprise computed** via prediction error (MSE loss)
529
+ 4. **Gradients calculated** via `torch.autograd.grad()`
530
+ 5. **Weights updated** via gradient descent: `param -= lr * grad`
 
531
 
532
+ This is REAL test-time training. The neural network's weights physically changed.
 
533
  """
 
534
 
535
+ return output, create_weight_visualization(), create_history_plot()
536
 
 
 
 
537
 
538
+ def check_surprise(content: str) -> str:
539
+ """Check surprise WITHOUT learning."""
540
+ if not content.strip():
541
+ return "Please enter content to check."
542
 
543
+ # REAL surprise computation (no learning)
544
+ surprise = memory.surprise(content)
 
 
 
 
545
 
546
+ return f"""## Surprise Check (No Learning)
547
 
548
+ **Content:** "{content[:100]}{'...' if len(content) > 100 else ''}"
549
 
550
+ **Surprise Score:** {surprise:.4f}
 
 
 
 
551
 
552
+ Interpretation:
553
+ - **< 0.3**: Very familiar - memory has seen similar patterns
554
+ - **0.3 - 0.6**: Moderately novel
555
+ - **> 0.6**: Highly novel - worth learning
556
 
557
+ {'This content is FAMILIAR to the memory.' if surprise < 0.3 else 'This content is NOVEL to the memory.' if surprise > 0.6 else 'This content is somewhat familiar.'}
558
+ """
559
 
 
 
 
 
 
 
560
 
561
+ def get_memory_stats() -> str:
562
+ """Get real memory statistics."""
563
+ stats = memory.get_stats()
564
 
565
+ return f"""## Memory Statistics
566
 
567
+ | Metric | Value |
568
+ |--------|-------|
569
+ | **Total Observations** | {stats['total_observations']} |
570
+ | **Parameters** | {stats['weight_parameters']:,} |
571
+ | **Dimension** | {stats['dimension']} |
572
+ | **Learning Rate** | {stats['learning_rate']:.4f} |
573
+ | **Avg Recent Surprise** | {stats['avg_surprise']:.4f} |
574
+ | **Current Weight Hash** | `{memory.get_weight_hash()}` |
575
 
576
+ ### This is a Real Neural Network
 
 
 
577
 
578
+ - **Architecture**: 2-layer MLP with GELU activation and LayerNorm
579
+ - **Framework**: PyTorch with autograd
580
+ - **Learning**: Test-time training via gradient descent
581
+ - **Memory**: ~{stats['weight_parameters'] * 4 / 1024:.1f} KB of weights
582
 
583
+ Unlike RAG which stores vectors in a database, this IS the memory.
584
+ The weights encode everything learned.
585
+ """
586
 
 
587
 
588
+ # =============================================================================
589
+ # DOCKER ECOSYSTEM INTEGRATION
590
+ # =============================================================================
591
+
592
+ DOCKER_INTEGRATION_MD = """
593
+ ## Docker Ecosystem Integration
594
+
595
+ This neural memory is designed for **containerized deployment** with full Docker integration.
596
+
597
+ ### MCP Server Interface
598
+
599
+ The memory exposes tools via Model Context Protocol (MCP):
600
+
601
+ ```python
602
+ # MCP Tools Available
603
+ @mcp.tool()
604
+ def observe(content: str) -> dict:
605
+ '''Feed context, trigger learning.'''
606
+ return memory.observe(content)
607
+
608
+ @mcp.tool()
609
+ def surprise(content: str) -> float:
610
+ '''Measure novelty without learning.'''
611
+ return memory.surprise(content)
612
+
613
+ @mcp.tool()
614
+ def checkpoint(name: str) -> str:
615
+ '''Save learned state to Docker volume.'''
616
+ return save_checkpoint(name)
617
+
618
+ @mcp.tool()
619
+ def restore(name: str) -> str:
620
+ '''Load previous state from Docker volume.'''
621
+ return load_checkpoint(name)
622
+ ```
623
+
624
+ ### Docker Compose Deployment
625
+
626
+ ```yaml
627
+ version: '3.8'
628
+ services:
629
+ neural-memory:
630
+ build: .
631
+ ports:
632
+ - "8000:8000" # MCP server
633
+ volumes:
634
+ - memory-state:/app/checkpoints # Persistent state
635
+ environment:
636
+ - MEMORY_DIM=512
637
+ - LEARNING_RATE=0.01
638
+
639
+ volumes:
640
+ memory-state: # State survives container restarts
641
+ ```
642
+
643
+ ### Key Docker-Native Features
644
+
645
+ | Feature | Implementation |
646
+ |---------|---------------|
647
+ | **State Persistence** | Docker volumes for checkpoints |
648
+ | **Horizontal Scaling** | Stateless inference, shared state via volume |
649
+ | **CI/CD Integration** | GitHub Actions with Docker build |
650
+ | **Resource Control** | Container limits for GPU/memory |
651
+ | **Health Checks** | `/health` endpoint with memory stats |
652
+
653
+ ### Why Docker + Neural Memory?
654
+
655
+ 1. **Containerized AI Memory**: Package learned state with your app
656
+ 2. **Version Control**: Checkpoint states like Git commits
657
+ 3. **Reproducibility**: Same container = same behavior
658
+ 4. **Orchestration Ready**: Deploy to Kubernetes, ECS, etc.
659
+ 5. **MCP Protocol**: Claude Desktop integration via container
660
 
661
  ---
662
 
663
+ *This project demonstrates production-grade AI infrastructure with Docker.*
664
  """
665
 
666
+ ABOUT_MD = """
667
+ ## About This Project
668
 
669
+ ### What Makes This Special
 
 
670
 
671
+ This is **NOT a simulation**. The demo runs real PyTorch code:
 
 
 
672
 
673
+ 1. **Real Neural Network**: 2-layer MLP with ~250K parameters
674
+ 2. **Real Gradient Descent**: `torch.autograd.grad()` computes gradients
675
+ 3. **Real Weight Updates**: Parameters change during inference
676
+ 4. **Real Surprise Metric**: MSE loss measures prediction error
677
 
678
+ ### The Titans Architecture
 
 
 
679
 
680
+ Based on Google's December 2024 paper: [arxiv.org/abs/2501.00663](https://arxiv.org/abs/2501.00663)
 
 
 
681
 
682
+ **Key Innovation**: The memory IS a neural network. Instead of storing vectors,
683
+ it learns patterns by updating weights during inference.
 
 
684
 
685
+ ### Docker Integration
 
686
 
687
+ - **MCP Server**: Model Context Protocol for Claude Desktop
688
+ - **Checkpoints**: Save/restore learned state via Docker volumes
689
+ - **Container-Native**: Designed for orchestrated deployment
690
 
691
+ ### Built By
 
 
692
 
693
+ **Carlos Crespo Macaya**
694
+ AI Engineer - GenAI Systems & Applied MLOps
 
695
 
696
+ - 10+ years production ML experience
697
+ - Expert in Docker, Kubernetes, MCP servers
698
+ - Currently at HP AICoE building multi-agent systems
 
699
 
700
+ This project demonstrates the ability to:
701
+ 1. Read cutting-edge research (Titans paper)
702
+ 2. Implement it correctly (PyTorch TTT)
703
+ 3. Productionize it (Docker, MCP, CI/CD)
704
+ 4. Make it compelling (this demo)
705
 
706
+ **Contact:** [macayaven@gmail.com](mailto:macayaven@gmail.com)
707
 
708
+ **GitHub:** [macayaven/docker-neural-memory](https://github.com/macayaven/docker-neural-memory)
709
+ """
 
 
 
 
710
 
 
 
 
 
 
 
 
711
 
712
+ # =============================================================================
713
+ # GRADIO INTERFACE
714
+ # =============================================================================
715
 
716
+ with gr.Blocks(title="Docker Neural Memory", theme=gr.themes.Soft()) as demo:
717
+ gr.Markdown("""
718
+ # Docker Neural Memory
719
+ ## Real Test-Time Training - Not a Simulation
720
 
721
+ This demo runs **actual PyTorch** code. When you observe content,
722
+ real gradients flow and real weights update.
723
+ """)
724
+
725
+ with gr.Tabs():
726
+ # TAB 1: Comparison Demo (NEW - Main Feature)
727
+ with gr.TabItem("LLM Comparison"):
728
+ gr.Markdown("""
729
+ ### Vanilla LLM vs Memory-Augmented LLM
730
+
731
+ **Step 1:** Teach the system some facts (knowledge base)
732
+ **Step 2:** Ask questions and compare responses
733
+
734
+ The vanilla LLM has no memory - it may hallucinate.
735
+ The memory-augmented LLM uses your observed facts.
736
+ """)
737
 
738
  with gr.Row():
739
+ with gr.Column(scale=1):
740
+ gr.Markdown("#### Step 1: Teach Facts")
741
+ fact_input = gr.Textbox(
742
+ label="Add a Fact",
743
+ placeholder="e.g., 'Carlos prefers VSCode over Vim'",
744
+ lines=2,
745
+ )
746
+ add_fact_btn = gr.Button("Add to Knowledge Base", variant="secondary")
747
+ fact_output = gr.Markdown()
748
+ gr.Markdown("#### Example Facts to Try")
749
+ gr.Markdown("""
750
+ - "My favorite programming language is Rust"
751
+ - "I always use dark mode in my editor"
752
+ - "The project deadline is March 15th"
753
+ - "Our API uses JWT authentication"
754
+ - "The database runs on PostgreSQL 15"
755
+ """)
756
 
757
+ with gr.Column(scale=1):
758
+ gr.Markdown("#### Embedding Space (t-SNE)")
759
+ tsne_plot = gr.Plot(label="Neural Memory Representations")
760
+
761
+ add_fact_btn.click(
762
+ add_to_knowledge_base,
763
+ inputs=[fact_input],
764
+ outputs=[fact_output, tsne_plot]
765
  )
 
766
 
767
+ gr.Markdown("---")
768
+ gr.Markdown("#### Step 2: Ask Questions")
 
769
 
770
+ question_input = gr.Textbox(
771
+ label="Ask a Question",
772
+ placeholder="e.g., 'What editor should I use?' or 'What's the project deadline?'",
773
+ lines=2,
774
+ )
775
 
776
+ with gr.Row():
777
+ compare_btn = gr.Button("Compare Responses", variant="primary", size="lg")
778
+ reset_compare_btn = gr.Button("Reset Comparison", variant="secondary")
779
 
780
+ with gr.Row():
781
+ with gr.Column():
782
+ nm_response = gr.Markdown(label="With Neural Memory")
783
+ with gr.Column():
784
+ vanilla_response = gr.Markdown(label="Vanilla LLM")
785
 
786
+ comparison_summary = gr.Markdown(label="Comparison Metrics")
787
 
788
+ compare_btn.click(
789
+ compare_responses,
790
+ inputs=[question_input],
791
+ outputs=[nm_response, vanilla_response, comparison_summary],
792
+ )
793
+ reset_compare_btn.click(
794
+ reset_comparison,
795
+ outputs=[comparison_summary, tsne_plot]
796
+ )
797
 
798
+ # TAB 2: Live Demo (original)
799
+ with gr.TabItem("Neural Memory Playground"):
800
+ gr.Markdown("### Watch Real Neural Learning")
801
 
802
+ with gr.Row():
803
+ with gr.Column(scale=1):
804
+ observe_input = gr.Textbox(
805
+ label="Content to Observe",
806
+ placeholder="Enter text to trigger real learning...",
807
+ lines=3,
808
+ )
809
+ observe_btn = gr.Button("Observe (Learn)", variant="primary", size="lg")
810
+ observe_output = gr.Markdown()
811
 
812
+ with gr.Column(scale=1):
813
+ weights_plot = gr.Plot(label="Neural Weights (Real PyTorch)")
814
 
815
+ history_plot = gr.Plot(label="Learning History")
 
 
816
 
817
+ observe_btn.click(
818
+ observe_content,
819
+ inputs=[observe_input],
820
+ outputs=[observe_output, weights_plot, history_plot],
821
+ )
822
 
823
+ gr.Markdown("---")
824
 
825
+ with gr.Row():
826
+ with gr.Column():
827
+ surprise_input = gr.Textbox(
828
+ label="Check Surprise (No Learning)",
829
+ placeholder="Check novelty without updating weights...",
830
+ )
831
+ surprise_btn = gr.Button("Check Surprise")
832
+ surprise_output = gr.Markdown()
833
+ surprise_btn.click(check_surprise, inputs=[surprise_input], outputs=[surprise_output])
834
+
835
+ with gr.Column():
836
+ stats_btn = gr.Button("Get Memory Stats")
837
+ stats_output = gr.Markdown()
838
+ stats_btn.click(get_memory_stats, outputs=[stats_output])
839
+
840
+ reset_btn = gr.Button("Reset Memory", variant="secondary")
841
+ reset_output = gr.Markdown()
842
+ reset_btn.click(reset_memory, outputs=[reset_output])
843
+
844
+ # TAB 2: Docker Integration
845
+ with gr.TabItem("Docker Integration"):
846
+ gr.Markdown(DOCKER_INTEGRATION_MD)
847
+
848
+ # TAB 3: About
849
+ with gr.TabItem("About"):
850
+ gr.Markdown(ABOUT_MD)
851
 
852
  gr.Markdown("""
853
  ---
854
+ *Docker Neural Memory - Containerized AI memory with real test-time training*
855
 
856
  [GitHub](https://github.com/macayaven/docker-neural-memory) |
857
  [Contact](mailto:macayaven@gmail.com)
requirements.txt CHANGED
@@ -1,9 +1,11 @@
1
  # Requirements for HuggingFace Spaces deployment
2
- # Dependencies for neural memory visual demo
3
 
4
  torch>=2.0.0
5
- gradio>=4.0.0
6
  pydantic>=2.0.0
7
  pydantic-settings>=2.0.0
8
  matplotlib>=3.7.0
9
  numpy>=1.24.0
 
 
 
1
  # Requirements for HuggingFace Spaces deployment
2
+ # Docker Neural Memory - Real implementation
3
 
4
  torch>=2.0.0
5
+ gradio>=5.9.0
6
  pydantic>=2.0.0
7
  pydantic-settings>=2.0.0
8
  matplotlib>=3.7.0
9
  numpy>=1.24.0
10
+ huggingface_hub>=0.20.0
11
+ scikit-learn>=1.3.0