neural-memory 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {neural_memory-0.2.0 → neural_memory-0.4.0}/PKG-INFO +1 -1
  2. {neural_memory-0.2.0 → neural_memory-0.4.0}/pyproject.toml +1 -1
  3. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/__init__.py +1 -1
  4. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/cli/main.py +201 -0
  5. neural_memory-0.4.0/src/neural_memory/engine/lifecycle.py +233 -0
  6. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/mcp/server.py +1 -1
  7. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/server/app.py +68 -0
  8. neural_memory-0.4.0/src/neural_memory/server/static/index.html +403 -0
  9. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/storage/sqlite_store.py +16 -0
  10. {neural_memory-0.2.0 → neural_memory-0.4.0}/.gitignore +0 -0
  11. {neural_memory-0.2.0 → neural_memory-0.4.0}/LICENSE +0 -0
  12. {neural_memory-0.2.0 → neural_memory-0.4.0}/README.md +0 -0
  13. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/cli/__init__.py +0 -0
  14. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/cli/__main__.py +0 -0
  15. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/cli/config.py +0 -0
  16. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/cli/storage.py +0 -0
  17. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/cli/tui.py +0 -0
  18. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/core/__init__.py +0 -0
  19. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/core/brain.py +0 -0
  20. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/core/brain_mode.py +0 -0
  21. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/core/fiber.py +0 -0
  22. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/core/memory_types.py +0 -0
  23. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/core/neuron.py +0 -0
  24. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/core/project.py +0 -0
  25. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/core/synapse.py +0 -0
  26. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/engine/__init__.py +0 -0
  27. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/engine/activation.py +0 -0
  28. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/engine/encoder.py +0 -0
  29. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/engine/retrieval.py +0 -0
  30. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/extraction/__init__.py +0 -0
  31. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/extraction/entities.py +0 -0
  32. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/extraction/parser.py +0 -0
  33. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/extraction/router.py +0 -0
  34. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/extraction/temporal.py +0 -0
  35. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/mcp/__init__.py +0 -0
  36. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/mcp/__main__.py +0 -0
  37. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/mcp/prompt.py +0 -0
  38. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/py.typed +0 -0
  39. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/safety/__init__.py +0 -0
  40. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/safety/freshness.py +0 -0
  41. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/safety/sensitive.py +0 -0
  42. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/server/__init__.py +0 -0
  43. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/server/dependencies.py +0 -0
  44. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/server/models.py +0 -0
  45. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/server/routes/__init__.py +0 -0
  46. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/server/routes/brain.py +0 -0
  47. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/server/routes/memory.py +0 -0
  48. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/server/routes/sync.py +0 -0
  49. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/storage/__init__.py +0 -0
  50. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/storage/base.py +0 -0
  51. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/storage/factory.py +0 -0
  52. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/storage/memory_store.py +0 -0
  53. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/storage/shared_store.py +0 -0
  54. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/sync/__init__.py +0 -0
  55. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/sync/client.py +0 -0
  56. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/unified_config.py +0 -0
  57. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/utils/__init__.py +0 -0
  58. {neural_memory-0.2.0 → neural_memory-0.4.0}/src/neural_memory/utils/config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neural-memory
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Reflex-based memory system for AI agents - retrieval through activation, not search
5
5
  Project-URL: Homepage, https://github.com/nhadaututtheky/neural-memory
6
6
  Project-URL: Documentation, https://github.com/nhadaututtheky/neural-memory#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "neural-memory"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "Reflex-based memory system for AI agents - retrieval through activation, not search"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -13,7 +13,7 @@ from neural_memory.core.synapse import Direction, Synapse, SynapseType
13
13
  from neural_memory.engine.encoder import EncodingResult, MemoryEncoder
14
14
  from neural_memory.engine.retrieval import DepthLevel, ReflexPipeline, RetrievalResult
15
15
 
16
- __version__ = "0.2.0"
16
+ __version__ = "0.4.0"
17
17
 
18
18
  __all__ = [
19
19
  "__version__",
@@ -2768,6 +2768,207 @@ def prompt(
2768
2768
  typer.echo(text)
2769
2769
 
2770
2770
 
2771
+ @app.command(name="export")
2772
+ def export_brain_cmd(
2773
+ output: Annotated[
2774
+ str, typer.Argument(help="Output file path (e.g., my-brain.json)")
2775
+ ],
2776
+ brain: Annotated[
2777
+ str | None, typer.Option("--brain", "-b", help="Brain to export (default: current)")
2778
+ ] = None,
2779
+ ) -> None:
2780
+ """Export brain to JSON file for backup or sharing.
2781
+
2782
+ Examples:
2783
+ nmem export backup.json # Export current brain
2784
+ nmem export work.json -b work # Export specific brain
2785
+ """
2786
+ from pathlib import Path
2787
+
2788
+ from neural_memory.unified_config import get_config, get_shared_storage
2789
+
2790
+ async def _export() -> None:
2791
+ config = get_config()
2792
+ brain_name = brain or config.current_brain
2793
+ storage = await get_shared_storage(brain_name)
2794
+
2795
+ snapshot = await storage.export_brain(brain_name)
2796
+
2797
+ output_path = Path(output)
2798
+ export_data = {
2799
+ "brain_id": snapshot.brain_id,
2800
+ "brain_name": snapshot.brain_name,
2801
+ "exported_at": snapshot.exported_at.isoformat(),
2802
+ "version": snapshot.version,
2803
+ "neurons": snapshot.neurons,
2804
+ "synapses": snapshot.synapses,
2805
+ "fibers": snapshot.fibers,
2806
+ "config": snapshot.config,
2807
+ "metadata": snapshot.metadata,
2808
+ }
2809
+
2810
+ output_path.write_text(json.dumps(export_data, indent=2, default=str))
2811
+
2812
+ typer.echo(f"Exported brain '{brain_name}' to {output_path}")
2813
+ typer.echo(f" Neurons: {len(snapshot.neurons)}")
2814
+ typer.echo(f" Synapses: {len(snapshot.synapses)}")
2815
+ typer.echo(f" Fibers: {len(snapshot.fibers)}")
2816
+
2817
+ asyncio.run(_export())
2818
+
2819
+
2820
+ @app.command(name="import")
2821
+ def import_brain_cmd(
2822
+ input_file: Annotated[
2823
+ str, typer.Argument(help="Input file path (e.g., my-brain.json)")
2824
+ ],
2825
+ brain: Annotated[
2826
+ str | None, typer.Option("--brain", "-b", help="Target brain name (default: from file)")
2827
+ ] = None,
2828
+ merge: Annotated[
2829
+ bool, typer.Option("--merge", "-m", help="Merge with existing brain")
2830
+ ] = False,
2831
+ ) -> None:
2832
+ """Import brain from JSON file.
2833
+
2834
+ Examples:
2835
+ nmem import backup.json # Import as original brain name
2836
+ nmem import backup.json -b new # Import as 'new' brain
2837
+ nmem import backup.json --merge # Merge into existing brain
2838
+ """
2839
+ from pathlib import Path
2840
+
2841
+ from neural_memory.core.brain import BrainSnapshot
2842
+ from neural_memory.unified_config import get_shared_storage
2843
+
2844
+ async def _import() -> None:
2845
+ input_path = Path(input_file)
2846
+ if not input_path.exists():
2847
+ typer.echo(f"Error: File not found: {input_path}", err=True)
2848
+ raise typer.Exit(1)
2849
+
2850
+ data = json.loads(input_path.read_text())
2851
+
2852
+ brain_name = brain or data.get("brain_name", "imported")
2853
+ storage = await get_shared_storage(brain_name)
2854
+
2855
+ snapshot = BrainSnapshot(
2856
+ brain_id=data.get("brain_id", brain_name),
2857
+ brain_name=data["brain_name"],
2858
+ exported_at=datetime.fromisoformat(data["exported_at"]),
2859
+ version=data["version"],
2860
+ neurons=data["neurons"],
2861
+ synapses=data["synapses"],
2862
+ fibers=data["fibers"],
2863
+ config=data["config"],
2864
+ metadata=data.get("metadata", {}),
2865
+ )
2866
+
2867
+ imported_id = await storage.import_brain(snapshot, brain_name)
2868
+
2869
+ typer.echo(f"Imported brain '{brain_name}' from {input_path}")
2870
+ typer.echo(f" Neurons: {len(snapshot.neurons)}")
2871
+ typer.echo(f" Synapses: {len(snapshot.synapses)}")
2872
+ typer.echo(f" Fibers: {len(snapshot.fibers)}")
2873
+
2874
+ asyncio.run(_import())
2875
+
2876
+
2877
+ @app.command()
2878
+ def serve(
2879
+ host: Annotated[
2880
+ str, typer.Option("--host", "-h", help="Host to bind to")
2881
+ ] = "127.0.0.1",
2882
+ port: Annotated[
2883
+ int, typer.Option("--port", "-p", help="Port to bind to")
2884
+ ] = 8000,
2885
+ reload: Annotated[
2886
+ bool, typer.Option("--reload", "-r", help="Enable auto-reload for development")
2887
+ ] = False,
2888
+ ) -> None:
2889
+ """Run the NeuralMemory API server.
2890
+
2891
+ Examples:
2892
+ nmem serve # Run on localhost:8000
2893
+ nmem serve -p 9000 # Run on port 9000
2894
+ nmem serve --host 0.0.0.0 # Expose to network
2895
+ nmem serve --reload # Development mode
2896
+ """
2897
+ try:
2898
+ import uvicorn
2899
+ except ImportError:
2900
+ typer.echo("Error: uvicorn not installed. Run: pip install neural-memory[server]", err=True)
2901
+ raise typer.Exit(1)
2902
+
2903
+ typer.echo(f"Starting NeuralMemory API server on http://{host}:{port}")
2904
+ typer.echo(f" UI: http://{host}:{port}/ui")
2905
+ typer.echo(f" Docs: http://{host}:{port}/docs")
2906
+
2907
+ uvicorn.run(
2908
+ "neural_memory.server.app:create_app",
2909
+ host=host,
2910
+ port=port,
2911
+ reload=reload,
2912
+ factory=True,
2913
+ )
2914
+
2915
+
2916
+ @app.command()
2917
+ def decay(
2918
+ brain: Annotated[
2919
+ str | None, typer.Option("--brain", "-b", help="Brain to apply decay to")
2920
+ ] = None,
2921
+ dry_run: Annotated[
2922
+ bool, typer.Option("--dry-run", "-n", help="Preview changes without applying")
2923
+ ] = False,
2924
+ prune_threshold: Annotated[
2925
+ float, typer.Option("--prune", "-p", help="Prune below this activation level")
2926
+ ] = 0.01,
2927
+ ) -> None:
2928
+ """Apply memory decay to simulate forgetting.
2929
+
2930
+ Memories that haven't been accessed recently will have their
2931
+ activation levels reduced following the Ebbinghaus forgetting curve.
2932
+
2933
+ Examples:
2934
+ nmem decay # Apply decay to current brain
2935
+ nmem decay -b work # Apply to specific brain
2936
+ nmem decay --dry-run # Preview without changes
2937
+ nmem decay --prune 0.05 # More aggressive pruning
2938
+ """
2939
+ from neural_memory.engine.lifecycle import DecayManager
2940
+ from neural_memory.unified_config import get_config, get_shared_storage
2941
+
2942
+ async def _decay() -> None:
2943
+ config = get_config()
2944
+ brain_name = brain or config.current_brain
2945
+
2946
+ typer.echo(f"Applying decay to brain '{brain_name}'...")
2947
+ if dry_run:
2948
+ typer.echo("(dry run - no changes will be saved)")
2949
+
2950
+ storage = await get_shared_storage(brain_name)
2951
+
2952
+ manager = DecayManager(
2953
+ decay_rate=config.brain.decay_rate,
2954
+ prune_threshold=prune_threshold,
2955
+ )
2956
+
2957
+ report = await manager.apply_decay(storage, dry_run=dry_run)
2958
+
2959
+ typer.echo("")
2960
+ typer.echo(report.summary())
2961
+
2962
+ if report.neurons_pruned > 0 or report.synapses_pruned > 0:
2963
+ typer.echo("")
2964
+ typer.echo(
2965
+ f"Pruned {report.neurons_pruned} neurons and "
2966
+ f"{report.synapses_pruned} synapses below threshold {prune_threshold}"
2967
+ )
2968
+
2969
+ asyncio.run(_decay())
2970
+
2971
+
2771
2972
  @app.command()
2772
2973
  def version() -> None:
2773
2974
  """Show version information."""
@@ -0,0 +1,233 @@
1
+ """Memory lifecycle management - decay, reinforcement, compression.
2
+
3
+ Implements the Ebbinghaus forgetting curve for natural memory decay
4
+ and reinforcement for frequently accessed memories.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timedelta
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from neural_memory.storage.base import NeuralStorage
16
+
17
+
18
+ @dataclass
19
+ class DecayReport:
20
+ """Report of decay operation results."""
21
+
22
+ neurons_processed: int = 0
23
+ neurons_decayed: int = 0
24
+ neurons_pruned: int = 0
25
+ synapses_processed: int = 0
26
+ synapses_decayed: int = 0
27
+ synapses_pruned: int = 0
28
+ duration_ms: float = 0.0
29
+ reference_time: datetime = field(default_factory=datetime.now)
30
+
31
+ def summary(self) -> str:
32
+ """Generate human-readable summary."""
33
+ lines = [
34
+ f"Decay Report ({self.reference_time.strftime('%Y-%m-%d %H:%M')})",
35
+ f" Neurons: {self.neurons_decayed}/{self.neurons_processed} decayed, {self.neurons_pruned} pruned",
36
+ f" Synapses: {self.synapses_decayed}/{self.synapses_processed} decayed, {self.synapses_pruned} pruned",
37
+ f" Duration: {self.duration_ms:.1f}ms",
38
+ ]
39
+ return "\n".join(lines)
40
+
41
+
42
+ class DecayManager:
43
+ """Manage memory decay using Ebbinghaus forgetting curve.
44
+
45
+ Decay formula: retention = e^(-decay_rate * days_since_access)
46
+
47
+ Memories that haven't been accessed recently will have their
48
+ activation levels reduced. Memories below the prune threshold
49
+ can be marked as dormant or removed.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ decay_rate: float = 0.1,
55
+ prune_threshold: float = 0.01,
56
+ min_age_days: float = 1.0,
57
+ ):
58
+ """Initialize decay manager.
59
+
60
+ Args:
61
+ decay_rate: Rate of decay per day (0.1 = 10% per day)
62
+ prune_threshold: Activation level below which to prune
63
+ min_age_days: Minimum age before applying decay
64
+ """
65
+ self.decay_rate = decay_rate
66
+ self.prune_threshold = prune_threshold
67
+ self.min_age_days = min_age_days
68
+
69
+ async def apply_decay(
70
+ self,
71
+ storage: NeuralStorage,
72
+ reference_time: datetime | None = None,
73
+ dry_run: bool = False,
74
+ ) -> DecayReport:
75
+ """Apply decay to all neurons and synapses in storage.
76
+
77
+ Args:
78
+ storage: Storage instance to apply decay to
79
+ reference_time: Reference time for decay calculation (default: now)
80
+ dry_run: If True, calculate but don't save changes
81
+
82
+ Returns:
83
+ DecayReport with statistics
84
+ """
85
+ import time
86
+
87
+ start_time = time.perf_counter()
88
+ reference_time = reference_time or datetime.now()
89
+ report = DecayReport(reference_time=reference_time)
90
+
91
+ # Get all neuron states
92
+ states = await storage.get_all_neuron_states()
93
+ report.neurons_processed = len(states)
94
+
95
+ for state in states:
96
+ if state.last_activated is None:
97
+ continue
98
+
99
+ # Calculate time since last activation
100
+ time_diff = reference_time - state.last_activated
101
+ days_elapsed = time_diff.total_seconds() / 86400
102
+
103
+ # Skip if too recent
104
+ if days_elapsed < self.min_age_days:
105
+ continue
106
+
107
+ # Calculate decay
108
+ decay_factor = math.exp(-self.decay_rate * days_elapsed)
109
+ new_level = state.activation_level * decay_factor
110
+
111
+ if new_level < state.activation_level:
112
+ report.neurons_decayed += 1
113
+
114
+ if new_level < self.prune_threshold:
115
+ report.neurons_pruned += 1
116
+ new_level = 0.0
117
+
118
+ if not dry_run:
119
+ # Update the neuron state
120
+ updated_state = state.with_activation(new_level)
121
+ updated_state = type(state)(
122
+ neuron_id=state.neuron_id,
123
+ activation_level=new_level,
124
+ access_frequency=state.access_frequency,
125
+ last_activated=state.last_activated,
126
+ decay_rate=state.decay_rate,
127
+ created_at=state.created_at,
128
+ )
129
+ await storage.update_neuron_state(updated_state)
130
+
131
+ # Get all synapses and apply decay
132
+ synapses = await storage.get_all_synapses()
133
+ report.synapses_processed = len(synapses)
134
+
135
+ for synapse in synapses:
136
+ if synapse.last_activated is None:
137
+ continue
138
+
139
+ time_diff = reference_time - synapse.last_activated
140
+ days_elapsed = time_diff.total_seconds() / 86400
141
+
142
+ if days_elapsed < self.min_age_days:
143
+ continue
144
+
145
+ # Decay synapse weight
146
+ decay_factor = math.exp(-self.decay_rate * days_elapsed)
147
+ new_weight = synapse.weight * decay_factor
148
+
149
+ if new_weight < synapse.weight:
150
+ report.synapses_decayed += 1
151
+
152
+ if new_weight < self.prune_threshold:
153
+ report.synapses_pruned += 1
154
+ if not dry_run:
155
+ # Could delete synapse here, but just set weight to 0 for now
156
+ pass
157
+
158
+ if not dry_run and new_weight >= self.prune_threshold:
159
+ decayed_synapse = synapse.decay(decay_factor)
160
+ await storage.update_synapse(decayed_synapse)
161
+
162
+ report.duration_ms = (time.perf_counter() - start_time) * 1000
163
+ return report
164
+
165
+
166
+ class ReinforcementManager:
167
+ """Strengthen frequently accessed memory paths.
168
+
169
+ When memories are accessed, their activation levels and
170
+ synapse weights are increased (reinforced).
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ reinforcement_delta: float = 0.05,
176
+ max_activation: float = 1.0,
177
+ max_weight: float = 1.0,
178
+ ):
179
+ """Initialize reinforcement manager.
180
+
181
+ Args:
182
+ reinforcement_delta: Amount to increase on each access
183
+ max_activation: Maximum activation level
184
+ max_weight: Maximum synapse weight
185
+ """
186
+ self.reinforcement_delta = reinforcement_delta
187
+ self.max_activation = max_activation
188
+ self.max_weight = max_weight
189
+
190
+ async def reinforce(
191
+ self,
192
+ storage: NeuralStorage,
193
+ neuron_ids: list[str],
194
+ synapse_ids: list[str] | None = None,
195
+ ) -> int:
196
+ """Reinforce accessed neurons and synapses.
197
+
198
+ Args:
199
+ storage: Storage instance
200
+ neuron_ids: List of accessed neuron IDs
201
+ synapse_ids: Optional list of accessed synapse IDs
202
+
203
+ Returns:
204
+ Number of items reinforced
205
+ """
206
+ reinforced = 0
207
+
208
+ for neuron_id in neuron_ids:
209
+ state = await storage.get_neuron_state(neuron_id)
210
+ if state:
211
+ new_level = min(
212
+ state.activation_level + self.reinforcement_delta,
213
+ self.max_activation,
214
+ )
215
+ activated_state = state.activate(new_level - state.activation_level)
216
+ await storage.update_neuron_state(activated_state)
217
+ reinforced += 1
218
+
219
+ if synapse_ids:
220
+ for synapse_id in synapse_ids:
221
+ synapse = await storage.get_synapse(synapse_id)
222
+ if synapse:
223
+ new_weight = min(
224
+ synapse.weight + self.reinforcement_delta,
225
+ self.max_weight,
226
+ )
227
+ reinforced_synapse = synapse.reinforce(
228
+ new_weight - synapse.weight
229
+ )
230
+ await storage.update_synapse(reinforced_synapse)
231
+ reinforced += 1
232
+
233
+ return reinforced
@@ -603,7 +603,7 @@ async def handle_message(server: MCPServer, message: dict[str, Any]) -> dict[str
603
603
  "id": msg_id,
604
604
  "result": {
605
605
  "protocolVersion": "2024-11-05",
606
- "serverInfo": {"name": "neural-memory", "version": "0.2.0"},
606
+ "serverInfo": {"name": "neural-memory", "version": "0.4.0"},
607
607
  "capabilities": {"tools": {}, "resources": {}},
608
608
  },
609
609
  }
@@ -4,15 +4,21 @@ from __future__ import annotations
4
4
 
5
5
  from collections.abc import AsyncGenerator
6
6
  from contextlib import asynccontextmanager
7
+ from pathlib import Path
7
8
 
8
9
  from fastapi import FastAPI
9
10
  from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import FileResponse
12
+ from fastapi.staticfiles import StaticFiles
10
13
 
11
14
  from neural_memory import __version__
12
15
  from neural_memory.server.models import HealthResponse
13
16
  from neural_memory.server.routes import brain_router, memory_router, sync_router
14
17
  from neural_memory.storage.memory_store import InMemoryStorage
15
18
 
19
+ # Static files directory
20
+ STATIC_DIR = Path(__file__).parent / "static"
21
+
16
22
 
17
23
  @asynccontextmanager
18
24
  async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
@@ -90,8 +96,70 @@ def create_app(
90
96
  "version": __version__,
91
97
  "docs": "/docs",
92
98
  "health": "/health",
99
+ "ui": "/ui",
100
+ }
101
+
102
+ # Graph visualization API
103
+ @app.get("/api/graph", tags=["visualization"])
104
+ async def get_graph_data() -> dict:
105
+ """Get graph data for visualization."""
106
+ from neural_memory.unified_config import get_shared_storage
107
+
108
+ storage = await get_shared_storage()
109
+
110
+ # Get all data
111
+ neurons = await storage.find_neurons()
112
+ synapses = await storage.get_all_synapses()
113
+ fibers = await storage.get_fibers(limit=1000)
114
+
115
+ stats = {
116
+ "neuron_count": len(neurons),
117
+ "synapse_count": len(synapses),
118
+ "fiber_count": len(fibers),
93
119
  }
94
120
 
121
+ return {
122
+ "neurons": [
123
+ {
124
+ "id": n.id,
125
+ "type": n.type.value,
126
+ "content": n.content,
127
+ "metadata": n.metadata,
128
+ }
129
+ for n in neurons
130
+ ],
131
+ "synapses": [
132
+ {
133
+ "id": s.id,
134
+ "source_id": s.source_id,
135
+ "target_id": s.target_id,
136
+ "type": s.type.value,
137
+ "weight": s.weight,
138
+ "direction": s.direction.value,
139
+ }
140
+ for s in synapses
141
+ ],
142
+ "fibers": [
143
+ {
144
+ "id": f.id,
145
+ "summary": f.summary,
146
+ "neuron_count": len(f.neuron_ids) if f.neuron_ids else 0,
147
+ }
148
+ for f in fibers
149
+ ],
150
+ "stats": stats,
151
+ }
152
+
153
+ # UI endpoint
154
+ @app.get("/ui", tags=["visualization"])
155
+ async def ui() -> FileResponse:
156
+ """Serve the visualization UI."""
157
+ return FileResponse(STATIC_DIR / "index.html")
158
+
159
+ # Mount static files (for potential future assets)
160
+ if STATIC_DIR.exists():
161
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
162
+
95
163
  return app
96
164
 
97
165
 
@@ -0,0 +1,403 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NeuralMemory - Brain Visualization</title>
7
+ <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
16
+ background: #1a1a2e;
17
+ color: #eee;
18
+ }
19
+ .container {
20
+ display: flex;
21
+ height: 100vh;
22
+ }
23
+ .sidebar {
24
+ width: 300px;
25
+ background: #16213e;
26
+ padding: 20px;
27
+ overflow-y: auto;
28
+ }
29
+ .main {
30
+ flex: 1;
31
+ display: flex;
32
+ flex-direction: column;
33
+ }
34
+ .header {
35
+ background: #0f3460;
36
+ padding: 15px 20px;
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ }
41
+ .header h1 {
42
+ font-size: 1.5rem;
43
+ color: #e94560;
44
+ }
45
+ .stats {
46
+ display: flex;
47
+ gap: 20px;
48
+ }
49
+ .stat {
50
+ text-align: center;
51
+ }
52
+ .stat-value {
53
+ font-size: 1.5rem;
54
+ font-weight: bold;
55
+ color: #e94560;
56
+ }
57
+ .stat-label {
58
+ font-size: 0.8rem;
59
+ color: #aaa;
60
+ }
61
+ #graph {
62
+ flex: 1;
63
+ background: #1a1a2e;
64
+ }
65
+ .sidebar h2 {
66
+ color: #e94560;
67
+ margin-bottom: 15px;
68
+ font-size: 1.1rem;
69
+ }
70
+ .sidebar h3 {
71
+ color: #aaa;
72
+ margin: 15px 0 10px;
73
+ font-size: 0.9rem;
74
+ text-transform: uppercase;
75
+ }
76
+ .brain-select {
77
+ width: 100%;
78
+ padding: 10px;
79
+ background: #0f3460;
80
+ border: 1px solid #e94560;
81
+ color: #eee;
82
+ border-radius: 5px;
83
+ margin-bottom: 15px;
84
+ }
85
+ .btn {
86
+ width: 100%;
87
+ padding: 10px;
88
+ background: #e94560;
89
+ border: none;
90
+ color: white;
91
+ border-radius: 5px;
92
+ cursor: pointer;
93
+ margin-bottom: 10px;
94
+ font-size: 0.9rem;
95
+ }
96
+ .btn:hover {
97
+ background: #ff6b6b;
98
+ }
99
+ .btn-secondary {
100
+ background: #0f3460;
101
+ border: 1px solid #e94560;
102
+ }
103
+ .btn-secondary:hover {
104
+ background: #16213e;
105
+ }
106
+ .node-info {
107
+ background: #0f3460;
108
+ padding: 15px;
109
+ border-radius: 5px;
110
+ margin-top: 15px;
111
+ }
112
+ .node-info p {
113
+ margin: 5px 0;
114
+ font-size: 0.85rem;
115
+ }
116
+ .node-info .label {
117
+ color: #aaa;
118
+ }
119
+ .node-info .value {
120
+ color: #eee;
121
+ }
122
+ .legend {
123
+ display: flex;
124
+ flex-wrap: wrap;
125
+ gap: 10px;
126
+ margin-top: 15px;
127
+ }
128
+ .legend-item {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 5px;
132
+ font-size: 0.75rem;
133
+ }
134
+ .legend-color {
135
+ width: 12px;
136
+ height: 12px;
137
+ border-radius: 50%;
138
+ }
139
+ .loading {
140
+ display: flex;
141
+ justify-content: center;
142
+ align-items: center;
143
+ height: 100%;
144
+ font-size: 1.2rem;
145
+ color: #aaa;
146
+ }
147
+ .error {
148
+ color: #ff6b6b;
149
+ padding: 10px;
150
+ background: rgba(255,107,107,0.1);
151
+ border-radius: 5px;
152
+ margin-top: 10px;
153
+ }
154
+ </style>
155
+ </head>
156
+ <body>
157
+ <div class="container">
158
+ <aside class="sidebar">
159
+ <h2>NeuralMemory</h2>
160
+
161
+ <select id="brainSelect" class="brain-select">
162
+ <option value="">Loading brains...</option>
163
+ </select>
164
+
165
+ <button class="btn" onclick="loadGraph()">Refresh Graph</button>
166
+ <button class="btn btn-secondary" onclick="resetView()">Reset View</button>
167
+
168
+ <h3>Statistics</h3>
169
+ <div class="stats" style="flex-direction: column; gap: 10px;">
170
+ <div class="stat" style="text-align: left;">
171
+ <span class="stat-label">Neurons: </span>
172
+ <span class="stat-value" id="neuronCount">0</span>
173
+ </div>
174
+ <div class="stat" style="text-align: left;">
175
+ <span class="stat-label">Synapses: </span>
176
+ <span class="stat-value" id="synapseCount">0</span>
177
+ </div>
178
+ <div class="stat" style="text-align: left;">
179
+ <span class="stat-label">Fibers: </span>
180
+ <span class="stat-value" id="fiberCount">0</span>
181
+ </div>
182
+ </div>
183
+
184
+ <h3>Selected Node</h3>
185
+ <div class="node-info" id="nodeInfo">
186
+ <p><span class="label">Click a node to see details</span></p>
187
+ </div>
188
+
189
+ <h3>Legend</h3>
190
+ <div class="legend">
191
+ <div class="legend-item">
192
+ <div class="legend-color" style="background: #e94560;"></div>
193
+ <span>Concept</span>
194
+ </div>
195
+ <div class="legend-item">
196
+ <div class="legend-color" style="background: #4ecdc4;"></div>
197
+ <span>Entity</span>
198
+ </div>
199
+ <div class="legend-item">
200
+ <div class="legend-color" style="background: #ffe66d;"></div>
201
+ <span>Time</span>
202
+ </div>
203
+ <div class="legend-item">
204
+ <div class="legend-color" style="background: #95e1d3;"></div>
205
+ <span>Action</span>
206
+ </div>
207
+ <div class="legend-item">
208
+ <div class="legend-color" style="background: #f38181;"></div>
209
+ <span>State</span>
210
+ </div>
211
+ <div class="legend-item">
212
+ <div class="legend-color" style="background: #aa96da;"></div>
213
+ <span>Other</span>
214
+ </div>
215
+ </div>
216
+ </aside>
217
+
218
+ <main class="main">
219
+ <header class="header">
220
+ <h1>Brain Visualization</h1>
221
+ <div class="stats">
222
+ <div class="stat">
223
+ <div class="stat-value" id="visibleNodes">0</div>
224
+ <div class="stat-label">Visible Nodes</div>
225
+ </div>
226
+ <div class="stat">
227
+ <div class="stat-value" id="visibleEdges">0</div>
228
+ <div class="stat-label">Visible Edges</div>
229
+ </div>
230
+ </div>
231
+ </header>
232
+ <div id="graph">
233
+ <div class="loading">Loading graph...</div>
234
+ </div>
235
+ </main>
236
+ </div>
237
+
238
+ <script>
239
+ const API_BASE = window.location.origin;
240
+ let network = null;
241
+ let allNodes = [];
242
+ let allEdges = [];
243
+
244
+ const COLORS = {
245
+ concept: '#e94560',
246
+ entity: '#4ecdc4',
247
+ time: '#ffe66d',
248
+ action: '#95e1d3',
249
+ state: '#f38181',
250
+ spatial: '#45b7d1',
251
+ sensory: '#96ceb4',
252
+ intent: '#dda0dd',
253
+ default: '#aa96da'
254
+ };
255
+
256
+ async function loadBrains() {
257
+ try {
258
+ // For now, use default brain from unified config
259
+ const select = document.getElementById('brainSelect');
260
+ select.innerHTML = '<option value="default">default</option>';
261
+ } catch (error) {
262
+ console.error('Error loading brains:', error);
263
+ }
264
+ }
265
+
266
+ async function loadGraph() {
267
+ const graphContainer = document.getElementById('graph');
268
+ graphContainer.innerHTML = '<div class="loading">Loading graph...</div>';
269
+
270
+ try {
271
+ const response = await fetch(`${API_BASE}/api/graph`);
272
+ if (!response.ok) {
273
+ throw new Error(`HTTP ${response.status}`);
274
+ }
275
+ const data = await response.json();
276
+
277
+ // Update stats
278
+ document.getElementById('neuronCount').textContent = data.stats.neuron_count;
279
+ document.getElementById('synapseCount').textContent = data.stats.synapse_count;
280
+ document.getElementById('fiberCount').textContent = data.stats.fiber_count;
281
+
282
+ // Convert to vis.js format
283
+ allNodes = data.neurons.map(n => ({
284
+ id: n.id,
285
+ label: truncate(n.content, 20),
286
+ title: n.content,
287
+ color: COLORS[n.type] || COLORS.default,
288
+ type: n.type,
289
+ content: n.content,
290
+ metadata: n.metadata
291
+ }));
292
+
293
+ allEdges = data.synapses.map(s => ({
294
+ id: s.id,
295
+ from: s.source_id,
296
+ to: s.target_id,
297
+ label: s.type,
298
+ title: `${s.type} (weight: ${s.weight.toFixed(2)})`,
299
+ arrows: s.direction === 'uni' ? 'to' : 'to, from',
300
+ width: Math.max(1, s.weight * 3),
301
+ color: { color: '#555', highlight: '#e94560' }
302
+ }));
303
+
304
+ renderGraph();
305
+
306
+ } catch (error) {
307
+ console.error('Error loading graph:', error);
308
+ graphContainer.innerHTML = `<div class="error">Error loading graph: ${error.message}<br>Make sure the server is running with UI enabled.</div>`;
309
+ }
310
+ }
311
+
312
+ function renderGraph() {
313
+ const container = document.getElementById('graph');
314
+ container.innerHTML = '';
315
+
316
+ const nodes = new vis.DataSet(allNodes);
317
+ const edges = new vis.DataSet(allEdges);
318
+
319
+ document.getElementById('visibleNodes').textContent = allNodes.length;
320
+ document.getElementById('visibleEdges').textContent = allEdges.length;
321
+
322
+ const options = {
323
+ nodes: {
324
+ shape: 'dot',
325
+ size: 16,
326
+ font: {
327
+ size: 12,
328
+ color: '#eee'
329
+ },
330
+ borderWidth: 2,
331
+ shadow: true
332
+ },
333
+ edges: {
334
+ font: {
335
+ size: 10,
336
+ color: '#888',
337
+ strokeWidth: 0
338
+ },
339
+ smooth: {
340
+ type: 'continuous'
341
+ }
342
+ },
343
+ physics: {
344
+ stabilization: {
345
+ iterations: 100
346
+ },
347
+ barnesHut: {
348
+ gravitationalConstant: -2000,
349
+ springLength: 150
350
+ }
351
+ },
352
+ interaction: {
353
+ hover: true,
354
+ tooltipDelay: 200
355
+ }
356
+ };
357
+
358
+ network = new vis.Network(container, { nodes, edges }, options);
359
+
360
+ network.on('selectNode', function(params) {
361
+ if (params.nodes.length > 0) {
362
+ const nodeId = params.nodes[0];
363
+ const node = allNodes.find(n => n.id === nodeId);
364
+ showNodeInfo(node);
365
+ }
366
+ });
367
+
368
+ network.on('deselectNode', function() {
369
+ document.getElementById('nodeInfo').innerHTML =
370
+ '<p><span class="label">Click a node to see details</span></p>';
371
+ });
372
+ }
373
+
374
+ function showNodeInfo(node) {
375
+ if (!node) return;
376
+
377
+ const infoDiv = document.getElementById('nodeInfo');
378
+ infoDiv.innerHTML = `
379
+ <p><span class="label">Type:</span> <span class="value">${node.type}</span></p>
380
+ <p><span class="label">Content:</span> <span class="value">${node.content}</span></p>
381
+ <p><span class="label">ID:</span> <span class="value" style="font-size:0.7rem">${node.id}</span></p>
382
+ `;
383
+ }
384
+
385
+ function resetView() {
386
+ if (network) {
387
+ network.fit();
388
+ }
389
+ }
390
+
391
+ function truncate(str, len) {
392
+ if (!str) return '';
393
+ return str.length > len ? str.substring(0, len) + '...' : str;
394
+ }
395
+
396
+ // Initialize
397
+ document.addEventListener('DOMContentLoaded', () => {
398
+ loadBrains();
399
+ loadGraph();
400
+ });
401
+ </script>
402
+ </body>
403
+ </html>
@@ -396,6 +396,18 @@ class SQLiteStorage(NeuralStorage):
396
396
  created_at=datetime.fromisoformat(row["created_at"]),
397
397
  )
398
398
 
399
+ async def get_all_neuron_states(self) -> list[NeuronState]:
400
+ """Get all neuron states for current brain."""
401
+ conn = self._ensure_conn()
402
+ brain_id = self._get_brain_id()
403
+
404
+ async with conn.execute(
405
+ "SELECT * FROM neuron_states WHERE brain_id = ?",
406
+ (brain_id,),
407
+ ) as cursor:
408
+ rows = await cursor.fetchall()
409
+ return [self._row_to_neuron_state(row) for row in rows]
410
+
399
411
  # ========== Synapse Operations ==========
400
412
 
401
413
  async def add_synapse(self, synapse: Synapse) -> str:
@@ -486,6 +498,10 @@ class SQLiteStorage(NeuralStorage):
486
498
  rows = await cursor.fetchall()
487
499
  return [self._row_to_synapse(row) for row in rows]
488
500
 
501
+ async def get_all_synapses(self) -> list[Synapse]:
502
+ """Get all synapses for current brain."""
503
+ return await self.get_synapses()
504
+
489
505
  async def update_synapse(self, synapse: Synapse) -> None:
490
506
  conn = self._ensure_conn()
491
507
  brain_id = self._get_brain_id()
File without changes
File without changes
File without changes