neural-memory 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- neural_memory/__init__.py +38 -0
- neural_memory/cli/__init__.py +15 -0
- neural_memory/cli/__main__.py +6 -0
- neural_memory/cli/config.py +176 -0
- neural_memory/cli/main.py +2702 -0
- neural_memory/cli/storage.py +169 -0
- neural_memory/cli/tui.py +471 -0
- neural_memory/core/__init__.py +52 -0
- neural_memory/core/brain.py +301 -0
- neural_memory/core/brain_mode.py +273 -0
- neural_memory/core/fiber.py +236 -0
- neural_memory/core/memory_types.py +331 -0
- neural_memory/core/neuron.py +168 -0
- neural_memory/core/project.py +257 -0
- neural_memory/core/synapse.py +215 -0
- neural_memory/engine/__init__.py +15 -0
- neural_memory/engine/activation.py +335 -0
- neural_memory/engine/encoder.py +391 -0
- neural_memory/engine/retrieval.py +440 -0
- neural_memory/extraction/__init__.py +42 -0
- neural_memory/extraction/entities.py +547 -0
- neural_memory/extraction/parser.py +337 -0
- neural_memory/extraction/router.py +396 -0
- neural_memory/extraction/temporal.py +428 -0
- neural_memory/mcp/__init__.py +9 -0
- neural_memory/mcp/__main__.py +6 -0
- neural_memory/mcp/server.py +621 -0
- neural_memory/py.typed +0 -0
- neural_memory/safety/__init__.py +31 -0
- neural_memory/safety/freshness.py +238 -0
- neural_memory/safety/sensitive.py +304 -0
- neural_memory/server/__init__.py +5 -0
- neural_memory/server/app.py +99 -0
- neural_memory/server/dependencies.py +33 -0
- neural_memory/server/models.py +138 -0
- neural_memory/server/routes/__init__.py +7 -0
- neural_memory/server/routes/brain.py +221 -0
- neural_memory/server/routes/memory.py +169 -0
- neural_memory/server/routes/sync.py +387 -0
- neural_memory/storage/__init__.py +17 -0
- neural_memory/storage/base.py +441 -0
- neural_memory/storage/factory.py +329 -0
- neural_memory/storage/memory_store.py +896 -0
- neural_memory/storage/shared_store.py +650 -0
- neural_memory/storage/sqlite_store.py +1613 -0
- neural_memory/sync/__init__.py +5 -0
- neural_memory/sync/client.py +435 -0
- neural_memory/unified_config.py +315 -0
- neural_memory/utils/__init__.py +5 -0
- neural_memory/utils/config.py +98 -0
- neural_memory-0.1.0.dist-info/METADATA +314 -0
- neural_memory-0.1.0.dist-info/RECORD +55 -0
- neural_memory-0.1.0.dist-info/WHEEL +4 -0
- neural_memory-0.1.0.dist-info/entry_points.txt +4 -0
- neural_memory-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""JSON-based persistent storage for CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from neural_memory.core.brain import Brain, BrainConfig, BrainSnapshot
|
|
11
|
+
from neural_memory.storage.memory_store import InMemoryStorage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PersistentStorage(InMemoryStorage):
|
|
15
|
+
"""InMemoryStorage with JSON file persistence.
|
|
16
|
+
|
|
17
|
+
Wraps InMemoryStorage and adds save/load functionality for CLI use.
|
|
18
|
+
Data is stored in ~/.neural-memory/brains/<brain_name>.json
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, file_path: Path) -> None:
|
|
22
|
+
"""Initialize persistent storage.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
file_path: Path to the JSON file for this brain.
|
|
26
|
+
"""
|
|
27
|
+
super().__init__()
|
|
28
|
+
self._file_path = file_path
|
|
29
|
+
self._auto_save = True
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
async def load(cls, file_path: Path) -> PersistentStorage:
|
|
33
|
+
"""Load storage from file, or create new if doesn't exist."""
|
|
34
|
+
storage = cls(file_path)
|
|
35
|
+
|
|
36
|
+
if file_path.exists():
|
|
37
|
+
await storage._load_from_file()
|
|
38
|
+
else:
|
|
39
|
+
# Create default brain
|
|
40
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
brain_name = file_path.stem
|
|
42
|
+
brain = Brain.create(name=brain_name)
|
|
43
|
+
await storage.save_brain(brain)
|
|
44
|
+
storage.set_brain(brain.id)
|
|
45
|
+
await storage._save_to_file()
|
|
46
|
+
|
|
47
|
+
return storage
|
|
48
|
+
|
|
49
|
+
async def _load_from_file(self) -> None:
|
|
50
|
+
"""Load data from JSON file."""
|
|
51
|
+
with open(self._file_path, encoding="utf-8") as f:
|
|
52
|
+
data = json.load(f)
|
|
53
|
+
|
|
54
|
+
# Reconstruct brain
|
|
55
|
+
brain_data = data.get("brain", {})
|
|
56
|
+
if brain_data:
|
|
57
|
+
config = BrainConfig(**brain_data.get("config", {}))
|
|
58
|
+
brain = Brain(
|
|
59
|
+
id=brain_data["id"],
|
|
60
|
+
name=brain_data["name"],
|
|
61
|
+
config=config,
|
|
62
|
+
owner_id=brain_data.get("owner_id"),
|
|
63
|
+
is_public=brain_data.get("is_public", False),
|
|
64
|
+
created_at=datetime.fromisoformat(brain_data["created_at"]),
|
|
65
|
+
updated_at=datetime.fromisoformat(brain_data["updated_at"]),
|
|
66
|
+
)
|
|
67
|
+
await super().save_brain(brain)
|
|
68
|
+
self.set_brain(brain.id)
|
|
69
|
+
|
|
70
|
+
# Import snapshot data if exists
|
|
71
|
+
if "snapshot" in data:
|
|
72
|
+
snapshot_data = data["snapshot"]
|
|
73
|
+
snapshot = BrainSnapshot(
|
|
74
|
+
brain_id=snapshot_data["brain_id"],
|
|
75
|
+
brain_name=snapshot_data["brain_name"],
|
|
76
|
+
exported_at=datetime.fromisoformat(snapshot_data["exported_at"]),
|
|
77
|
+
version=snapshot_data["version"],
|
|
78
|
+
neurons=snapshot_data["neurons"],
|
|
79
|
+
synapses=snapshot_data["synapses"],
|
|
80
|
+
fibers=snapshot_data["fibers"],
|
|
81
|
+
config=snapshot_data.get("config", {}),
|
|
82
|
+
metadata=snapshot_data.get("metadata", {}),
|
|
83
|
+
)
|
|
84
|
+
await self.import_brain(snapshot, brain.id if brain_data else None)
|
|
85
|
+
|
|
86
|
+
async def _save_to_file(self) -> None:
|
|
87
|
+
"""Save data to JSON file."""
|
|
88
|
+
if not self._current_brain_id:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
brain = await self.get_brain(self._current_brain_id)
|
|
92
|
+
if not brain:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Export as snapshot
|
|
96
|
+
snapshot = await self.export_brain(self._current_brain_id)
|
|
97
|
+
|
|
98
|
+
data = {
|
|
99
|
+
"brain": {
|
|
100
|
+
"id": brain.id,
|
|
101
|
+
"name": brain.name,
|
|
102
|
+
"config": {
|
|
103
|
+
"decay_rate": brain.config.decay_rate,
|
|
104
|
+
"reinforcement_delta": brain.config.reinforcement_delta,
|
|
105
|
+
"activation_threshold": brain.config.activation_threshold,
|
|
106
|
+
"max_spread_hops": brain.config.max_spread_hops,
|
|
107
|
+
"max_context_tokens": brain.config.max_context_tokens,
|
|
108
|
+
},
|
|
109
|
+
"owner_id": brain.owner_id,
|
|
110
|
+
"is_public": brain.is_public,
|
|
111
|
+
"created_at": brain.created_at.isoformat(),
|
|
112
|
+
"updated_at": brain.updated_at.isoformat(),
|
|
113
|
+
},
|
|
114
|
+
"snapshot": {
|
|
115
|
+
"brain_id": snapshot.brain_id,
|
|
116
|
+
"brain_name": snapshot.brain_name,
|
|
117
|
+
"exported_at": snapshot.exported_at.isoformat(),
|
|
118
|
+
"version": snapshot.version,
|
|
119
|
+
"neurons": snapshot.neurons,
|
|
120
|
+
"synapses": snapshot.synapses,
|
|
121
|
+
"fibers": snapshot.fibers,
|
|
122
|
+
"config": snapshot.config,
|
|
123
|
+
"metadata": snapshot.metadata,
|
|
124
|
+
},
|
|
125
|
+
"saved_at": datetime.now().isoformat(),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
self._file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
with open(self._file_path, "w", encoding="utf-8") as f:
|
|
130
|
+
json.dump(data, f, indent=2, default=str)
|
|
131
|
+
|
|
132
|
+
async def save(self) -> None:
|
|
133
|
+
"""Explicitly save to file."""
|
|
134
|
+
await self._save_to_file()
|
|
135
|
+
|
|
136
|
+
# Override methods to auto-save
|
|
137
|
+
|
|
138
|
+
async def add_neuron(self, neuron: Any) -> str:
|
|
139
|
+
"""Add neuron and save."""
|
|
140
|
+
result = await super().add_neuron(neuron)
|
|
141
|
+
if self._auto_save:
|
|
142
|
+
await self._save_to_file()
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
async def add_synapse(self, synapse: Any) -> str:
|
|
146
|
+
"""Add synapse and save."""
|
|
147
|
+
result = await super().add_synapse(synapse)
|
|
148
|
+
if self._auto_save:
|
|
149
|
+
await self._save_to_file()
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
async def add_fiber(self, fiber: Any) -> str:
|
|
153
|
+
"""Add fiber and save."""
|
|
154
|
+
result = await super().add_fiber(fiber)
|
|
155
|
+
if self._auto_save:
|
|
156
|
+
await self._save_to_file()
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
async def batch_save(self) -> None:
|
|
160
|
+
"""Save after batch operations (call manually when auto_save is off)."""
|
|
161
|
+
await self._save_to_file()
|
|
162
|
+
|
|
163
|
+
def disable_auto_save(self) -> None:
|
|
164
|
+
"""Disable auto-save for batch operations."""
|
|
165
|
+
self._auto_save = False
|
|
166
|
+
|
|
167
|
+
def enable_auto_save(self) -> None:
|
|
168
|
+
"""Enable auto-save."""
|
|
169
|
+
self._auto_save = True
|
neural_memory/cli/tui.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Terminal UI components for NeuralMemory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.layout import Layout
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from rich.tree import Tree
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from neural_memory.cli.storage import PersistentStorage
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# Color Schemes
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
MEMORY_TYPE_COLORS = {
|
|
25
|
+
"fact": "white",
|
|
26
|
+
"decision": "blue",
|
|
27
|
+
"preference": "magenta",
|
|
28
|
+
"todo": "yellow",
|
|
29
|
+
"insight": "green",
|
|
30
|
+
"context": "cyan",
|
|
31
|
+
"instruction": "bright_blue",
|
|
32
|
+
"error": "red",
|
|
33
|
+
"workflow": "bright_cyan",
|
|
34
|
+
"reference": "bright_black",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
PRIORITY_COLORS = {
|
|
38
|
+
"critical": "bold red",
|
|
39
|
+
"high": "red",
|
|
40
|
+
"normal": "white",
|
|
41
|
+
"low": "bright_black",
|
|
42
|
+
"lowest": "dim",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
FRESHNESS_COLORS = {
|
|
46
|
+
"fresh": "green",
|
|
47
|
+
"recent": "bright_green",
|
|
48
|
+
"aging": "yellow",
|
|
49
|
+
"stale": "red",
|
|
50
|
+
"ancient": "bright_black",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# Dashboard
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def render_dashboard(storage: PersistentStorage) -> None:
|
|
60
|
+
"""Render a rich dashboard with brain stats and recent activity."""
|
|
61
|
+
from neural_memory.safety.freshness import analyze_freshness, evaluate_freshness
|
|
62
|
+
|
|
63
|
+
brain = await storage.get_brain(storage._current_brain_id)
|
|
64
|
+
if not brain:
|
|
65
|
+
console.print("[red]No brain configured[/red]")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Gather data
|
|
69
|
+
stats = await storage.get_stats(brain.id)
|
|
70
|
+
fibers = await storage.get_fibers(limit=100)
|
|
71
|
+
typed_memories = await storage.find_typed_memories(limit=1000)
|
|
72
|
+
expired_memories = await storage.get_expired_memories()
|
|
73
|
+
|
|
74
|
+
# Analyze freshness
|
|
75
|
+
created_dates = [f.created_at for f in fibers]
|
|
76
|
+
freshness_report = analyze_freshness(created_dates)
|
|
77
|
+
|
|
78
|
+
# Count by type and priority
|
|
79
|
+
type_counts: dict[str, int] = {}
|
|
80
|
+
priority_counts: dict[str, int] = {}
|
|
81
|
+
for tm in typed_memories:
|
|
82
|
+
type_name = tm.memory_type.value
|
|
83
|
+
type_counts[type_name] = type_counts.get(type_name, 0) + 1
|
|
84
|
+
pri_name = tm.priority.name.lower()
|
|
85
|
+
priority_counts[pri_name] = priority_counts.get(pri_name, 0) + 1
|
|
86
|
+
|
|
87
|
+
# Create layout
|
|
88
|
+
layout = Layout()
|
|
89
|
+
layout.split_column(
|
|
90
|
+
Layout(name="header", size=3),
|
|
91
|
+
Layout(name="main", ratio=1),
|
|
92
|
+
Layout(name="footer", size=3),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
layout["main"].split_row(
|
|
96
|
+
Layout(name="left", ratio=1),
|
|
97
|
+
Layout(name="right", ratio=1),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
layout["left"].split_column(
|
|
101
|
+
Layout(name="stats", ratio=1),
|
|
102
|
+
Layout(name="types", ratio=1),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
layout["right"].split_column(
|
|
106
|
+
Layout(name="freshness", ratio=1),
|
|
107
|
+
Layout(name="recent", ratio=1),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Header
|
|
111
|
+
header_text = Text()
|
|
112
|
+
header_text.append("[*] ", style="bold")
|
|
113
|
+
header_text.append("NeuralMemory Dashboard", style="bold cyan")
|
|
114
|
+
header_text.append(f" - {brain.name}", style="bright_black")
|
|
115
|
+
layout["header"].update(Panel(header_text, style="cyan"))
|
|
116
|
+
|
|
117
|
+
# Stats panel
|
|
118
|
+
stats_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
119
|
+
stats_table.add_column("Metric", style="bright_black")
|
|
120
|
+
stats_table.add_column("Value", style="bold")
|
|
121
|
+
stats_table.add_row("Neurons", f"[cyan]{stats['neuron_count']:,}[/cyan]")
|
|
122
|
+
stats_table.add_row("Synapses", f"[blue]{stats['synapse_count']:,}[/blue]")
|
|
123
|
+
stats_table.add_row("Fibers", f"[green]{stats['fiber_count']:,}[/green]")
|
|
124
|
+
stats_table.add_row("Typed Memories", f"[yellow]{len(typed_memories):,}[/yellow]")
|
|
125
|
+
if expired_memories:
|
|
126
|
+
stats_table.add_row(
|
|
127
|
+
"Expired", f"[red]{len(expired_memories)}[/red] [dim](run cleanup)[/dim]"
|
|
128
|
+
)
|
|
129
|
+
layout["stats"].update(Panel(stats_table, title="Brain Stats", border_style="cyan"))
|
|
130
|
+
|
|
131
|
+
# Types panel
|
|
132
|
+
types_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
133
|
+
types_table.add_column("Type")
|
|
134
|
+
types_table.add_column("Count", justify="right")
|
|
135
|
+
for mem_type, count in sorted(type_counts.items(), key=lambda x: -x[1])[:8]:
|
|
136
|
+
color = MEMORY_TYPE_COLORS.get(mem_type, "white")
|
|
137
|
+
types_table.add_row(f"[{color}]*[/{color}] {mem_type}", str(count))
|
|
138
|
+
layout["types"].update(Panel(types_table, title="By Type", border_style="blue"))
|
|
139
|
+
|
|
140
|
+
# Freshness panel
|
|
141
|
+
fresh_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
142
|
+
fresh_table.add_column("Age")
|
|
143
|
+
fresh_table.add_column("Count", justify="right")
|
|
144
|
+
fresh_table.add_column("Bar")
|
|
145
|
+
|
|
146
|
+
total = max(freshness_report.total, 1)
|
|
147
|
+
for label, count, color in [
|
|
148
|
+
("Fresh (<7d)", freshness_report.fresh, "green"),
|
|
149
|
+
("Recent (7-30d)", freshness_report.recent, "bright_green"),
|
|
150
|
+
("Aging (30-90d)", freshness_report.aging, "yellow"),
|
|
151
|
+
("Stale (90-365d)", freshness_report.stale, "red"),
|
|
152
|
+
("Ancient (>365d)", freshness_report.ancient, "bright_black"),
|
|
153
|
+
]:
|
|
154
|
+
pct = count / total
|
|
155
|
+
bar = "#" * int(pct * 15) + "-" * (15 - int(pct * 15))
|
|
156
|
+
fresh_table.add_row(f"[{color}]{label}[/{color}]", str(count), f"[{color}]{bar}[/{color}]")
|
|
157
|
+
|
|
158
|
+
layout["freshness"].update(Panel(fresh_table, title="Memory Freshness", border_style="green"))
|
|
159
|
+
|
|
160
|
+
# Recent memories panel
|
|
161
|
+
recent_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
162
|
+
recent_table.add_column("Memory", overflow="ellipsis", max_width=40)
|
|
163
|
+
|
|
164
|
+
recent_typed = sorted(typed_memories, key=lambda x: x.created_at, reverse=True)[:6]
|
|
165
|
+
for tm in recent_typed:
|
|
166
|
+
fiber = await storage.get_fiber(tm.fiber_id)
|
|
167
|
+
content = ""
|
|
168
|
+
if fiber:
|
|
169
|
+
if fiber.summary:
|
|
170
|
+
content = fiber.summary[:35]
|
|
171
|
+
elif fiber.anchor_neuron_id:
|
|
172
|
+
anchor = await storage.get_neuron(fiber.anchor_neuron_id)
|
|
173
|
+
if anchor:
|
|
174
|
+
content = anchor.content[:35]
|
|
175
|
+
|
|
176
|
+
if len(content) > 35:
|
|
177
|
+
content = content[:32] + "..."
|
|
178
|
+
|
|
179
|
+
type_color = MEMORY_TYPE_COLORS.get(tm.memory_type.value, "white")
|
|
180
|
+
freshness = evaluate_freshness(tm.created_at)
|
|
181
|
+
age_color = FRESHNESS_COLORS.get(freshness.level.value, "white")
|
|
182
|
+
|
|
183
|
+
row_text = Text()
|
|
184
|
+
row_text.append(f"[{tm.memory_type.value[:4].upper()}] ", style=type_color)
|
|
185
|
+
row_text.append(content, style="white")
|
|
186
|
+
row_text.append(f" ({freshness.label})", style=age_color)
|
|
187
|
+
recent_table.add_row(row_text)
|
|
188
|
+
|
|
189
|
+
layout["recent"].update(Panel(recent_table, title="Recent Memories", border_style="yellow"))
|
|
190
|
+
|
|
191
|
+
# Footer
|
|
192
|
+
footer_text = Text()
|
|
193
|
+
footer_text.append("Commands: ", style="bright_black")
|
|
194
|
+
footer_text.append("nmem ui", style="cyan")
|
|
195
|
+
footer_text.append(" (browse) ", style="bright_black")
|
|
196
|
+
footer_text.append("nmem graph", style="cyan")
|
|
197
|
+
footer_text.append(" (visualize) ", style="bright_black")
|
|
198
|
+
footer_text.append("nmem recall", style="cyan")
|
|
199
|
+
footer_text.append(" (search)", style="bright_black")
|
|
200
|
+
layout["footer"].update(Panel(footer_text, style="bright_black"))
|
|
201
|
+
|
|
202
|
+
console.print(layout)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# Memory Browser (UI)
|
|
207
|
+
# =============================================================================
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def render_memory_browser(
|
|
211
|
+
storage: PersistentStorage,
|
|
212
|
+
memory_type: str | None = None,
|
|
213
|
+
limit: int = 20,
|
|
214
|
+
search: str | None = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Render an interactive memory browser."""
|
|
217
|
+
from neural_memory.safety.freshness import evaluate_freshness, format_age
|
|
218
|
+
|
|
219
|
+
brain = await storage.get_brain(storage._current_brain_id)
|
|
220
|
+
if not brain:
|
|
221
|
+
console.print("[red]No brain configured[/red]")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# Get memories
|
|
225
|
+
mem_type_filter = None
|
|
226
|
+
if memory_type:
|
|
227
|
+
from neural_memory.core.memory_types import MemoryType
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
mem_type_filter = MemoryType(memory_type.lower())
|
|
231
|
+
except ValueError:
|
|
232
|
+
console.print(f"[red]Invalid memory type: {memory_type}[/red]")
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
typed_memories = await storage.find_typed_memories(
|
|
236
|
+
memory_type=mem_type_filter,
|
|
237
|
+
limit=limit * 2, # Get extra for filtering
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Filter by search if provided
|
|
241
|
+
if search:
|
|
242
|
+
search_lower = search.lower()
|
|
243
|
+
filtered = []
|
|
244
|
+
for tm in typed_memories:
|
|
245
|
+
fiber = await storage.get_fiber(tm.fiber_id)
|
|
246
|
+
if fiber:
|
|
247
|
+
content = fiber.summary or ""
|
|
248
|
+
if not content and fiber.anchor_neuron_id:
|
|
249
|
+
anchor = await storage.get_neuron(fiber.anchor_neuron_id)
|
|
250
|
+
if anchor:
|
|
251
|
+
content = anchor.content
|
|
252
|
+
if search_lower in content.lower():
|
|
253
|
+
filtered.append((tm, content))
|
|
254
|
+
typed_memories_with_content = filtered[:limit]
|
|
255
|
+
else:
|
|
256
|
+
typed_memories_with_content = []
|
|
257
|
+
for tm in typed_memories[:limit]:
|
|
258
|
+
fiber = await storage.get_fiber(tm.fiber_id)
|
|
259
|
+
content = ""
|
|
260
|
+
if fiber:
|
|
261
|
+
if fiber.summary:
|
|
262
|
+
content = fiber.summary
|
|
263
|
+
elif fiber.anchor_neuron_id:
|
|
264
|
+
anchor = await storage.get_neuron(fiber.anchor_neuron_id)
|
|
265
|
+
if anchor:
|
|
266
|
+
content = anchor.content
|
|
267
|
+
typed_memories_with_content.append((tm, content))
|
|
268
|
+
|
|
269
|
+
if not typed_memories_with_content:
|
|
270
|
+
console.print("[yellow]No memories found.[/yellow]")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
# Create table
|
|
274
|
+
table = Table(
|
|
275
|
+
title="Memory Browser",
|
|
276
|
+
show_lines=True,
|
|
277
|
+
title_style="bold cyan",
|
|
278
|
+
border_style="bright_black",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
table.add_column("#", style="bright_black", width=3)
|
|
282
|
+
table.add_column("Type", style="bold", width=10)
|
|
283
|
+
table.add_column("Priority", width=8)
|
|
284
|
+
table.add_column("Content", overflow="fold")
|
|
285
|
+
table.add_column("Age", width=10)
|
|
286
|
+
table.add_column("Tags", width=15)
|
|
287
|
+
|
|
288
|
+
for idx, (tm, content) in enumerate(typed_memories_with_content, 1):
|
|
289
|
+
type_color = MEMORY_TYPE_COLORS.get(tm.memory_type.value, "white")
|
|
290
|
+
priority_color = PRIORITY_COLORS.get(tm.priority.name.lower(), "white")
|
|
291
|
+
freshness = evaluate_freshness(tm.created_at)
|
|
292
|
+
age_color = FRESHNESS_COLORS.get(freshness.level.value, "white")
|
|
293
|
+
|
|
294
|
+
# Truncate content
|
|
295
|
+
display_content = content[:80] + "..." if len(content) > 80 else content
|
|
296
|
+
|
|
297
|
+
# Format tags
|
|
298
|
+
tags_str = ", ".join(list(tm.tags)[:3]) if tm.tags else "-"
|
|
299
|
+
if tm.tags and len(tm.tags) > 3:
|
|
300
|
+
tags_str += f" +{len(tm.tags) - 3}"
|
|
301
|
+
|
|
302
|
+
table.add_row(
|
|
303
|
+
str(idx),
|
|
304
|
+
f"[{type_color}]{tm.memory_type.value}[/{type_color}]",
|
|
305
|
+
f"[{priority_color}]{tm.priority.name.lower()}[/{priority_color}]",
|
|
306
|
+
display_content,
|
|
307
|
+
f"[{age_color}]{format_age(freshness.age_days)}[/{age_color}]",
|
|
308
|
+
f"[bright_black]{tags_str}[/bright_black]",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Print summary
|
|
312
|
+
filter_info = []
|
|
313
|
+
if memory_type:
|
|
314
|
+
filter_info.append(f"type={memory_type}")
|
|
315
|
+
if search:
|
|
316
|
+
filter_info.append(f"search='{search}'")
|
|
317
|
+
|
|
318
|
+
if filter_info:
|
|
319
|
+
console.print(f"[bright_black]Filters: {', '.join(filter_info)}[/bright_black]")
|
|
320
|
+
|
|
321
|
+
console.print(table)
|
|
322
|
+
console.print(
|
|
323
|
+
f"\n[bright_black]Showing {len(typed_memories_with_content)} memories. "
|
|
324
|
+
f"Use --limit N to show more.[/bright_black]"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# =============================================================================
|
|
329
|
+
# Graph Visualization
|
|
330
|
+
# =============================================================================
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
async def render_graph(
|
|
334
|
+
storage: PersistentStorage,
|
|
335
|
+
query: str | None = None,
|
|
336
|
+
depth: int = 2,
|
|
337
|
+
) -> None:
|
|
338
|
+
"""Render a text-based graph visualization."""
|
|
339
|
+
from neural_memory.core.synapse import SynapseType
|
|
340
|
+
|
|
341
|
+
brain = await storage.get_brain(storage._current_brain_id)
|
|
342
|
+
if not brain:
|
|
343
|
+
console.print("[red]No brain configured[/red]")
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# If query provided, find related neurons
|
|
347
|
+
if query:
|
|
348
|
+
from neural_memory.engine.retrieval import DepthLevel, ReflexPipeline
|
|
349
|
+
|
|
350
|
+
pipeline = ReflexPipeline(storage, brain.config)
|
|
351
|
+
result = await pipeline.query(
|
|
352
|
+
query=query,
|
|
353
|
+
depth=DepthLevel(min(depth, 3)),
|
|
354
|
+
max_tokens=1000,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if result.confidence < 0.1:
|
|
358
|
+
console.print("[yellow]No relevant memories found for query.[/yellow]")
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# Get fibers matched
|
|
362
|
+
fibers = []
|
|
363
|
+
for fiber_id in result.fibers_matched[:5]:
|
|
364
|
+
fiber = await storage.get_fiber(fiber_id)
|
|
365
|
+
if fiber:
|
|
366
|
+
fibers.append(fiber)
|
|
367
|
+
else:
|
|
368
|
+
# Get recent fibers
|
|
369
|
+
fibers = await storage.get_fibers(limit=5)
|
|
370
|
+
|
|
371
|
+
if not fibers:
|
|
372
|
+
console.print("[yellow]No memories to visualize.[/yellow]")
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
# Build tree visualization
|
|
376
|
+
tree = Tree(
|
|
377
|
+
"[bold cyan][*] Neural Graph[/bold cyan]",
|
|
378
|
+
guide_style="bright_black",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
synapse_icons = {
|
|
382
|
+
SynapseType.CAUSED_BY: "<-",
|
|
383
|
+
SynapseType.LEADS_TO: "->",
|
|
384
|
+
SynapseType.CO_OCCURS: "<->",
|
|
385
|
+
SynapseType.RELATED_TO: "~",
|
|
386
|
+
SynapseType.SIMILAR_TO: "~~",
|
|
387
|
+
SynapseType.HAPPENED_AT: "@",
|
|
388
|
+
SynapseType.AT_LOCATION: "#",
|
|
389
|
+
SynapseType.INVOLVES: "&",
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for fiber in fibers:
|
|
393
|
+
# Get fiber content
|
|
394
|
+
content = fiber.summary or ""
|
|
395
|
+
if not content and fiber.anchor_neuron_id:
|
|
396
|
+
anchor = await storage.get_neuron(fiber.anchor_neuron_id)
|
|
397
|
+
if anchor:
|
|
398
|
+
content = anchor.content
|
|
399
|
+
|
|
400
|
+
display_content = content[:50] + "..." if len(content) > 50 else content
|
|
401
|
+
|
|
402
|
+
fiber_branch = tree.add(
|
|
403
|
+
f"[green]*[/green] {display_content}",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Get connected neurons
|
|
407
|
+
if fiber.anchor_neuron_id:
|
|
408
|
+
neighbors = await storage.get_neighbors(fiber.anchor_neuron_id, direction="both")
|
|
409
|
+
|
|
410
|
+
for neighbor, synapse in neighbors[:5]:
|
|
411
|
+
icon = synapse_icons.get(synapse.type, "─")
|
|
412
|
+
neighbor_content = neighbor.content[:30]
|
|
413
|
+
if len(neighbor.content) > 30:
|
|
414
|
+
neighbor_content += "..."
|
|
415
|
+
|
|
416
|
+
synapse_color = "blue" if synapse.weight > 0.5 else "bright_black"
|
|
417
|
+
fiber_branch.add(
|
|
418
|
+
f"[{synapse_color}]{icon}[/{synapse_color}] "
|
|
419
|
+
f"[bright_black]{synapse.type.value}[/bright_black] "
|
|
420
|
+
f"[white]{neighbor_content}[/white]"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
console.print()
|
|
424
|
+
console.print(tree)
|
|
425
|
+
console.print()
|
|
426
|
+
|
|
427
|
+
# Legend
|
|
428
|
+
legend = Table(show_header=False, box=None, padding=(0, 2))
|
|
429
|
+
legend.add_column("Symbol")
|
|
430
|
+
legend.add_column("Meaning")
|
|
431
|
+
legend.add_row("[green]*[/green]", "Memory (Fiber)")
|
|
432
|
+
legend.add_row("->", "leads_to")
|
|
433
|
+
legend.add_row("<-", "caused_by")
|
|
434
|
+
legend.add_row("<->", "co_occurs")
|
|
435
|
+
legend.add_row("@", "happened_at")
|
|
436
|
+
legend.add_row("#", "at_location")
|
|
437
|
+
|
|
438
|
+
console.print(Panel(legend, title="Legend", border_style="bright_black"))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# =============================================================================
|
|
442
|
+
# Quick Stats (for prompt injection)
|
|
443
|
+
# =============================================================================
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
async def render_quick_stats(storage: PersistentStorage) -> str:
|
|
447
|
+
"""Render a compact stats string for context injection."""
|
|
448
|
+
brain = await storage.get_brain(storage._current_brain_id)
|
|
449
|
+
if not brain:
|
|
450
|
+
return "No brain configured"
|
|
451
|
+
|
|
452
|
+
stats = await storage.get_stats(brain.id)
|
|
453
|
+
typed_memories = await storage.find_typed_memories(limit=1000)
|
|
454
|
+
|
|
455
|
+
# Count by type
|
|
456
|
+
type_counts: dict[str, int] = {}
|
|
457
|
+
for tm in typed_memories:
|
|
458
|
+
type_name = tm.memory_type.value
|
|
459
|
+
type_counts[type_name] = type_counts.get(type_name, 0) + 1
|
|
460
|
+
|
|
461
|
+
# Format
|
|
462
|
+
parts = [f"Brain: {brain.name}"]
|
|
463
|
+
parts.append(f"Memories: {stats['fiber_count']}")
|
|
464
|
+
|
|
465
|
+
if type_counts:
|
|
466
|
+
type_summary = ", ".join(
|
|
467
|
+
f"{count} {t}" for t, count in sorted(type_counts.items(), key=lambda x: -x[1])[:3]
|
|
468
|
+
)
|
|
469
|
+
parts.append(f"Types: {type_summary}")
|
|
470
|
+
|
|
471
|
+
return " | ".join(parts)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Core data models for NeuralMemory."""
|
|
2
|
+
|
|
3
|
+
from neural_memory.core.brain import Brain, BrainConfig
|
|
4
|
+
from neural_memory.core.brain_mode import (
|
|
5
|
+
BrainMode,
|
|
6
|
+
BrainModeConfig,
|
|
7
|
+
HybridConfig,
|
|
8
|
+
SharedConfig,
|
|
9
|
+
SyncStrategy,
|
|
10
|
+
)
|
|
11
|
+
from neural_memory.core.fiber import Fiber
|
|
12
|
+
from neural_memory.core.memory_types import (
|
|
13
|
+
Confidence,
|
|
14
|
+
MemoryType,
|
|
15
|
+
Priority,
|
|
16
|
+
Provenance,
|
|
17
|
+
TypedMemory,
|
|
18
|
+
suggest_memory_type,
|
|
19
|
+
)
|
|
20
|
+
from neural_memory.core.neuron import Neuron, NeuronState, NeuronType
|
|
21
|
+
from neural_memory.core.project import MemoryScope, Project
|
|
22
|
+
from neural_memory.core.synapse import Direction, Synapse, SynapseType
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Brain
|
|
26
|
+
"Brain",
|
|
27
|
+
"BrainConfig",
|
|
28
|
+
# Brain mode (local/shared toggle)
|
|
29
|
+
"BrainMode",
|
|
30
|
+
"BrainModeConfig",
|
|
31
|
+
"SharedConfig",
|
|
32
|
+
"HybridConfig",
|
|
33
|
+
"SyncStrategy",
|
|
34
|
+
# Memory structures
|
|
35
|
+
"Fiber",
|
|
36
|
+
"Neuron",
|
|
37
|
+
"NeuronState",
|
|
38
|
+
"NeuronType",
|
|
39
|
+
"Synapse",
|
|
40
|
+
"SynapseType",
|
|
41
|
+
"Direction",
|
|
42
|
+
# Memory types (MemoCore integration)
|
|
43
|
+
"MemoryType",
|
|
44
|
+
"Priority",
|
|
45
|
+
"Confidence",
|
|
46
|
+
"Provenance",
|
|
47
|
+
"TypedMemory",
|
|
48
|
+
"suggest_memory_type",
|
|
49
|
+
# Project scoping
|
|
50
|
+
"Project",
|
|
51
|
+
"MemoryScope",
|
|
52
|
+
]
|