nexo-brain 0.7.0 → 0.8.0
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.
- package/README.md +38 -9
- package/bin/nexo-brain.js +30 -3
- package/package.json +1 -1
- package/src/cognitive.py +47 -14
- package/src/dashboard/__init__.py +0 -0
- package/src/dashboard/app.py +277 -0
- package/src/dashboard/templates/adaptive.html +75 -0
- package/src/dashboard/templates/graph.html +198 -0
- package/src/dashboard/templates/memory.html +61 -0
- package/src/dashboard/templates/overview.html +75 -0
- package/src/dashboard/templates/sessions.html +52 -0
- package/src/dashboard/templates/somatic.html +57 -0
- package/src/db.py +23 -0
- package/src/kg_populate.py +287 -0
- package/src/knowledge_graph.py +257 -0
- package/src/maintenance.py +59 -0
- package/src/plugins/entities.py +6 -0
- package/src/plugins/episodic_memory.py +67 -55
- package/src/plugins/knowledge_graph_tools.py +105 -0
- package/src/scripts/nexo-cognitive-decay.py +73 -1
- package/src/storage_router.py +28 -0
- package/src/tools_learnings.py +47 -16
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# NEXO Brain — Your AI Gets a Brain
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/nexo-brain)
|
|
4
4
|
[](https://github.com/wazionapps/nexo/blob/main/benchmarks/locomo/results/)
|
|
5
5
|
[](https://github.com/snap-research/locomo/issues/33)
|
|
6
6
|
[](https://github.com/wazionapps/nexo/stargazers)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
-
> **v0.
|
|
9
|
+
> **v0.8.0** — Knowledge Graph (988 bi-temporal nodes, D3 visualization), Web Dashboard (6 pages at localhost:6174), Cross-Platform support (Linux + Windows), Smart dedup with event-sourced edges, and 4 new KG tools.
|
|
10
10
|
|
|
11
11
|
**NEXO Brain transforms any MCP-compatible AI agent from a stateless assistant into a cognitive partner that remembers, learns, forgets, adapts, and builds a relationship with you over time.**
|
|
12
12
|
|
|
@@ -132,7 +132,7 @@ If your Mac was asleep during any scheduled process, NEXO Brain catches up in or
|
|
|
132
132
|
|
|
133
133
|
## Cognitive Features
|
|
134
134
|
|
|
135
|
-
NEXO Brain provides
|
|
135
|
+
NEXO Brain provides 25 cognitive tools on top of the 76 base tools, totaling **109+ MCP tools**. These features implement cognitive science concepts that go beyond basic memory:
|
|
136
136
|
|
|
137
137
|
### Input Pipeline
|
|
138
138
|
|
|
@@ -198,7 +198,7 @@ NEXO Brain was evaluated on [LoCoMo](https://github.com/snap-research/locomo) (A
|
|
|
198
198
|
|
|
199
199
|
Full results in [`benchmarks/locomo/results/`](benchmarks/locomo/results/).
|
|
200
200
|
|
|
201
|
-
## Full Orchestration System (v0.
|
|
201
|
+
## Full Orchestration System (v0.7.0)
|
|
202
202
|
|
|
203
203
|
Memory alone doesn't make a co-operator. What makes the difference is the **behavioral loop** — the automated discipline that ensures every session starts informed, runs with guardrails, and ends with self-reflection.
|
|
204
204
|
|
|
@@ -257,6 +257,33 @@ npx nexo-brain # detects v0.5.0, migrates automatically
|
|
|
257
257
|
- **Never touches your data** (memories, learnings, preferences)
|
|
258
258
|
- Saves updated CLAUDE.md as reference (doesn't overwrite customizations)
|
|
259
259
|
|
|
260
|
+
## Knowledge Graph & Dashboard (v0.8.0)
|
|
261
|
+
|
|
262
|
+
### Knowledge Graph
|
|
263
|
+
A bi-temporal entity-relationship graph with 988 nodes and 896 edges. Entities and relationships carry both valid-time (when the fact was true) and system-time (when it was recorded), enabling temporal queries like "what did we know about X last Tuesday?". BFS traversal discovers multi-hop connections between concepts. Event-sourced edges with smart dedup (ADD/UPDATE/NOOP) prevent redundant writes while preserving full history.
|
|
264
|
+
|
|
265
|
+
4 new MCP tools: `nexo_kg_query` (SPARQL-like queries), `nexo_kg_path` (shortest path between entities), `nexo_kg_neighbors` (direct connections), `nexo_kg_stats` (graph metrics).
|
|
266
|
+
|
|
267
|
+
### Web Dashboard
|
|
268
|
+
A visual interface at `localhost:6174` with 6 pages: Overview (system health at a glance), Graph (interactive D3.js visualization of the knowledge graph), Memory (browse and search all memory stores), Somatic (pain map per file/area), Adaptive (personality signals and weights), and Sessions (active and historical sessions). Built with FastAPI backend and D3.js frontend.
|
|
269
|
+
|
|
270
|
+
### Cross-Platform Support
|
|
271
|
+
v0.8.0 adds full Linux and Windows support. The installer detects the platform and configures the appropriate process manager (LaunchAgents on macOS, systemd on Linux, Task Scheduler on Windows). Opportunistic maintenance runs cognitive processes when resources are available.
|
|
272
|
+
|
|
273
|
+
### Storage Router
|
|
274
|
+
A new abstraction layer routes storage operations through a unified interface, making the system multi-tenant ready. Each operator's data is isolated while sharing the same cognitive engine.
|
|
275
|
+
|
|
276
|
+
## Learned Weights & Somatic Markers (v0.7.0)
|
|
277
|
+
|
|
278
|
+
### Adaptive Learned Weights
|
|
279
|
+
Signal weights learn from real user feedback via Ridge regression. A 2-week shadow mode observes before activating. Weight momentum (85/15 blend) prevents personality whiplash. Automatic rollback if correction rate doubles.
|
|
280
|
+
|
|
281
|
+
### Somatic Markers (Pain Memory)
|
|
282
|
+
Files and areas that cause repeated errors accumulate a risk score (0.0–1.0). The guard system warns on HIGH RISK (>0.5) and CRITICAL RISK (>0.8), lowering thresholds for more paranoid checking. Clean guard checks reduce risk multiplicatively (×0.7). Nightly decay (×0.95) ensures old pain fades.
|
|
283
|
+
|
|
284
|
+
### Adaptive Personality v2
|
|
285
|
+
6 weighted signals: vibe, corrections, brevity, topic, tool errors, git diff. Emergency keywords bypass hysteresis. Severity-weighted decay. Manual override via `nexo_adaptive_override`.
|
|
286
|
+
|
|
260
287
|
## Quick Start
|
|
261
288
|
|
|
262
289
|
### Claude Code (Primary)
|
|
@@ -305,7 +332,7 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
|
|
|
305
332
|
| Component | What | Where |
|
|
306
333
|
|-----------|------|-------|
|
|
307
334
|
| Cognitive engine | Python: fastembed, numpy, vector search | pip packages |
|
|
308
|
-
| MCP server |
|
|
335
|
+
| MCP server | 105+ tools for memory, cognition, learning, guard | ~/.nexo/ |
|
|
309
336
|
| Plugins | Guard, episodic memory, cognitive memory, entities, preferences | ~/.nexo/plugins/ |
|
|
310
337
|
| Hooks (5) | SessionStart briefing, Stop post-mortem, PostToolUse capture, PreCompact checkpoint, Caffeinate | ~/.nexo/hooks/ |
|
|
311
338
|
| Reflection engine | Processes session buffer, extracts patterns, updates user model | ~/.nexo/scripts/ |
|
|
@@ -316,14 +343,14 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
|
|
|
316
343
|
|
|
317
344
|
### Requirements
|
|
318
345
|
|
|
319
|
-
- **macOS
|
|
346
|
+
- **macOS, Linux, or Windows**
|
|
320
347
|
- **Node.js 18+** (for the installer)
|
|
321
|
-
- **Claude Opus (latest version) strongly recommended.** NEXO Brain provides
|
|
348
|
+
- **Claude Opus (latest version) strongly recommended.** NEXO Brain provides 109+ MCP tools across 17 categories. This cognitive load requires a top-tier model with large context window. Smaller models (Haiku, Sonnet) may struggle with tool selection and produce inconsistent results. Opus handles all 105+ tools without hesitation.
|
|
322
349
|
- Python 3, Homebrew, and Claude Code are installed automatically if missing.
|
|
323
350
|
|
|
324
351
|
## Architecture
|
|
325
352
|
|
|
326
|
-
###
|
|
353
|
+
### 109+ MCP Tools across 19 Categories
|
|
327
354
|
|
|
328
355
|
| Category | Count | Tools | Purpose |
|
|
329
356
|
|----------|-------|-------|---------|
|
|
@@ -345,6 +372,8 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
|
|
|
345
372
|
| Agents | 5 | get, create, update, delete, list | Agent delegation registry |
|
|
346
373
|
| Backup | 3 | now, list, restore | SQLite data safety |
|
|
347
374
|
| Evolution | 5 | propose, approve, reject, status, history | Self-improvement proposals |
|
|
375
|
+
| Adaptive & Somatic (4) | nexo_adaptive_weights, nexo_adaptive_override, nexo_somatic_check, nexo_somatic_stats |
|
|
376
|
+
| Knowledge Graph | 4 | kg_query, kg_path, kg_neighbors, kg_stats | Bi-temporal entity-relationship graph |
|
|
348
377
|
|
|
349
378
|
### Plugin System
|
|
350
379
|
|
|
@@ -402,7 +431,7 @@ NEXO Brain is designed as an MCP server. Claude Code is the primary supported cl
|
|
|
402
431
|
npx nexo-brain
|
|
403
432
|
```
|
|
404
433
|
|
|
405
|
-
All
|
|
434
|
+
All 105+ tools are available immediately after installation. The installer configures Claude Code's `~/.claude/settings.json` automatically.
|
|
406
435
|
|
|
407
436
|
### OpenClaw
|
|
408
437
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -70,8 +70,8 @@ async function main() {
|
|
|
70
70
|
|
|
71
71
|
// Check prerequisites
|
|
72
72
|
const platform = process.platform;
|
|
73
|
-
if (platform !== "darwin") {
|
|
74
|
-
log(
|
|
73
|
+
if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
|
|
74
|
+
log(`Unsupported platform: ${platform}. NEXO supports macOS, Linux, and Windows.`);
|
|
75
75
|
process.exit(1);
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -105,6 +105,7 @@ async function main() {
|
|
|
105
105
|
// Update core Python files
|
|
106
106
|
const srcDir = path.join(__dirname, "..", "src");
|
|
107
107
|
["server.py", "db.py", "plugin_loader.py", "cognitive.py",
|
|
108
|
+
"knowledge_graph.py", "kg_populate.py", "maintenance.py", "storage_router.py",
|
|
108
109
|
"tools_sessions.py", "tools_coordination.py", "tools_reminders.py",
|
|
109
110
|
"tools_reminders_crud.py", "tools_learnings.py", "tools_credentials.py",
|
|
110
111
|
"tools_task_history.py", "tools_menu.py"].forEach((f) => {
|
|
@@ -126,6 +127,27 @@ async function main() {
|
|
|
126
127
|
}
|
|
127
128
|
log(" Plugins updated.");
|
|
128
129
|
|
|
130
|
+
// Update dashboard
|
|
131
|
+
const dashSrc = path.join(srcDir, "dashboard");
|
|
132
|
+
const dashDest = path.join(NEXO_HOME, "dashboard");
|
|
133
|
+
if (fs.existsSync(dashSrc)) {
|
|
134
|
+
fs.mkdirSync(dashDest, { recursive: true });
|
|
135
|
+
const copyDir = (src, dest) => {
|
|
136
|
+
fs.readdirSync(src).forEach(item => {
|
|
137
|
+
const srcPath = path.join(src, item);
|
|
138
|
+
const destPath = path.join(dest, item);
|
|
139
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
140
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
141
|
+
copyDir(srcPath, destPath);
|
|
142
|
+
} else {
|
|
143
|
+
fs.copyFileSync(srcPath, destPath);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
copyDir(dashSrc, dashDest);
|
|
148
|
+
log(" Dashboard updated.");
|
|
149
|
+
}
|
|
150
|
+
|
|
129
151
|
// Update scripts
|
|
130
152
|
const scriptsSrc = path.join(srcDir, "scripts");
|
|
131
153
|
const scriptsDest = path.join(NEXO_HOME, "scripts");
|
|
@@ -590,8 +612,9 @@ Operator name: ${operatorName}
|
|
|
590
612
|
fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
|
|
591
613
|
log("MCP server + hooks configured in Claude Code settings.");
|
|
592
614
|
|
|
593
|
-
// Step 7: Install LaunchAgents
|
|
615
|
+
// Step 7: Install LaunchAgents (macOS only)
|
|
594
616
|
log("Setting up automated processes...");
|
|
617
|
+
if (platform === "darwin") {
|
|
595
618
|
fs.mkdirSync(LAUNCH_AGENTS, { recursive: true });
|
|
596
619
|
|
|
597
620
|
const agents = [
|
|
@@ -724,6 +747,10 @@ Operator name: ${operatorName}
|
|
|
724
747
|
} catch {}
|
|
725
748
|
log("Caffeinate enabled — Mac will stay awake for cognitive processes.");
|
|
726
749
|
}
|
|
750
|
+
} else {
|
|
751
|
+
log("Non-macOS platform: background tasks will run via catch-up on startup.");
|
|
752
|
+
log(" No OS scheduler configured — NEXO runs maintenance when MCP starts.");
|
|
753
|
+
}
|
|
727
754
|
|
|
728
755
|
// Step 8: Create shell alias so user can just type the operator's name
|
|
729
756
|
log("Creating shell alias...");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
|
|
6
6
|
"bin": {
|
package/src/cognitive.py
CHANGED
|
@@ -30,7 +30,7 @@ DISCRIMINATING_ENTITIES = {
|
|
|
30
30
|
# OS / Environment
|
|
31
31
|
"linux", "mac", "macos", "windows", "darwin", "ubuntu", "debian", "alpine",
|
|
32
32
|
# Platforms
|
|
33
|
-
"shopify", "
|
|
33
|
+
"shopify", "whatsapp", "chrome", "firefox",
|
|
34
34
|
# Languages / Runtimes
|
|
35
35
|
"python", "php", "javascript", "typescript", "node", "deno", "ruby",
|
|
36
36
|
# Versions
|
|
@@ -65,12 +65,12 @@ URGENCY_SIGNALS = {
|
|
|
65
65
|
_DEFAULT_TRUST_EVENTS = {
|
|
66
66
|
# Positive
|
|
67
67
|
"explicit_thanks": +3,
|
|
68
|
-
"delegation": +2, #
|
|
69
|
-
"paradigm_shift": +2, #
|
|
68
|
+
"delegation": +2, # User delegates new task without micromanaging
|
|
69
|
+
"paradigm_shift": +2, # User teaches, NEXO learns
|
|
70
70
|
"sibling_detected": +3, # NEXO avoided context error on its own
|
|
71
71
|
"proactive_action": +2, # NEXO did something useful without being asked
|
|
72
72
|
# Negative
|
|
73
|
-
"correction": -3, #
|
|
73
|
+
"correction": -3, # User corrects NEXO
|
|
74
74
|
"repeated_error": -7, # Error on something NEXO already had a learning for
|
|
75
75
|
"override": -5, # NEXO's memory was wrong
|
|
76
76
|
"correction_fatigue": -10, # Same memory corrected 3+ times
|
|
@@ -395,7 +395,7 @@ def _init_tables(conn: sqlite3.Connection):
|
|
|
395
395
|
created_at TEXT DEFAULT (datetime('now'))
|
|
396
396
|
);
|
|
397
397
|
|
|
398
|
-
-- Sentiment readings:
|
|
398
|
+
-- Sentiment readings: User's detected mood per interaction
|
|
399
399
|
CREATE TABLE IF NOT EXISTS sentiment_log (
|
|
400
400
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
401
401
|
sentiment TEXT NOT NULL, -- 'positive', 'negative', 'neutral', 'urgent'
|
|
@@ -421,13 +421,13 @@ def _init_tables(conn: sqlite3.Connection):
|
|
|
421
421
|
status TEXT DEFAULT 'pending'
|
|
422
422
|
);
|
|
423
423
|
|
|
424
|
-
-- Correction tracking: when
|
|
424
|
+
-- Correction tracking: when User overrides a memory's guidance
|
|
425
425
|
CREATE TABLE IF NOT EXISTS memory_corrections (
|
|
426
426
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
427
427
|
memory_id INTEGER NOT NULL,
|
|
428
428
|
store TEXT NOT NULL, -- 'stm' or 'ltm'
|
|
429
429
|
correction_type TEXT NOT NULL, -- 'override', 'exception', 'paradigm_shift'
|
|
430
|
-
context TEXT DEFAULT '', -- what
|
|
430
|
+
context TEXT DEFAULT '', -- what User said
|
|
431
431
|
created_at TEXT DEFAULT (datetime('now'))
|
|
432
432
|
);
|
|
433
433
|
""")
|
|
@@ -494,6 +494,39 @@ def _init_tables(conn: sqlite3.Connection):
|
|
|
494
494
|
""")
|
|
495
495
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_somatic_target ON somatic_markers(target)")
|
|
496
496
|
|
|
497
|
+
conn.execute("""
|
|
498
|
+
CREATE TABLE IF NOT EXISTS kg_nodes (
|
|
499
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
500
|
+
node_type TEXT NOT NULL,
|
|
501
|
+
node_ref TEXT NOT NULL,
|
|
502
|
+
label TEXT NOT NULL,
|
|
503
|
+
properties TEXT DEFAULT '{}',
|
|
504
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
505
|
+
UNIQUE(node_type, node_ref)
|
|
506
|
+
)
|
|
507
|
+
""")
|
|
508
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_kg_nodes_type ON kg_nodes(node_type)")
|
|
509
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_kg_nodes_label ON kg_nodes(label)")
|
|
510
|
+
|
|
511
|
+
conn.execute("""
|
|
512
|
+
CREATE TABLE IF NOT EXISTS kg_edges (
|
|
513
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
514
|
+
source_id INTEGER NOT NULL REFERENCES kg_nodes(id),
|
|
515
|
+
target_id INTEGER NOT NULL REFERENCES kg_nodes(id),
|
|
516
|
+
relation TEXT NOT NULL,
|
|
517
|
+
weight REAL DEFAULT 1.0,
|
|
518
|
+
confidence REAL DEFAULT 1.0,
|
|
519
|
+
valid_from TEXT DEFAULT (datetime('now')),
|
|
520
|
+
valid_until TEXT DEFAULT NULL,
|
|
521
|
+
source_memory_id TEXT DEFAULT '',
|
|
522
|
+
properties TEXT DEFAULT '{}',
|
|
523
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
524
|
+
)
|
|
525
|
+
""")
|
|
526
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_kg_edges_source ON kg_edges(source_id)")
|
|
527
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_kg_edges_target ON kg_edges(target_id)")
|
|
528
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_kg_edges_relation ON kg_edges(relation)")
|
|
529
|
+
|
|
497
530
|
conn.commit()
|
|
498
531
|
|
|
499
532
|
|
|
@@ -2511,12 +2544,12 @@ def get_siblings(memory_id: int) -> list[dict]:
|
|
|
2511
2544
|
def detect_dissonance(new_instruction: str, min_score: float = 0.65) -> list[dict]:
|
|
2512
2545
|
"""Detect cognitive dissonance: find LTM memories that contradict a new instruction.
|
|
2513
2546
|
|
|
2514
|
-
When
|
|
2547
|
+
When User gives a new instruction that conflicts with established LTM memories
|
|
2515
2548
|
(strength > 0.8), this function surfaces the conflict so NEXO can verbalize it
|
|
2516
2549
|
rather than silently obeying or silently resisting.
|
|
2517
2550
|
|
|
2518
2551
|
Args:
|
|
2519
|
-
new_instruction: The new instruction or preference from
|
|
2552
|
+
new_instruction: The new instruction or preference from User
|
|
2520
2553
|
min_score: Minimum cosine similarity to consider as potential conflict
|
|
2521
2554
|
|
|
2522
2555
|
Returns:
|
|
@@ -2551,12 +2584,12 @@ def detect_dissonance(new_instruction: str, min_score: float = 0.65) -> list[dic
|
|
|
2551
2584
|
|
|
2552
2585
|
|
|
2553
2586
|
def resolve_dissonance(memory_id: int, resolution: str, context: str = "") -> str:
|
|
2554
|
-
"""Resolve a cognitive dissonance by applying
|
|
2587
|
+
"""Resolve a cognitive dissonance by applying User's decision.
|
|
2555
2588
|
|
|
2556
2589
|
Args:
|
|
2557
2590
|
memory_id: The LTM memory that conflicts with the new instruction
|
|
2558
2591
|
resolution: One of:
|
|
2559
|
-
- 'paradigm_shift':
|
|
2592
|
+
- 'paradigm_shift': User changed his mind permanently. Decay old memory,
|
|
2560
2593
|
new instruction becomes the standard.
|
|
2561
2594
|
- 'exception': This is a one-time override. Keep old memory as standard.
|
|
2562
2595
|
- 'override': Old memory was wrong. Mark as corrupted and decay to dormant.
|
|
@@ -2607,7 +2640,7 @@ def resolve_dissonance(memory_id: int, resolution: str, context: str = "") -> st
|
|
|
2607
2640
|
def check_correction_fatigue() -> list[dict]:
|
|
2608
2641
|
"""Find memories corrected 3+ times in the last 7 days — mark as 'under review'.
|
|
2609
2642
|
|
|
2610
|
-
These memories are unreliable:
|
|
2643
|
+
These memories are unreliable: User keeps overriding them, suggesting
|
|
2611
2644
|
the memory itself may be wrong or outdated.
|
|
2612
2645
|
|
|
2613
2646
|
Returns:
|
|
@@ -2655,7 +2688,7 @@ def check_correction_fatigue() -> list[dict]:
|
|
|
2655
2688
|
|
|
2656
2689
|
|
|
2657
2690
|
def detect_sentiment(text: str) -> dict:
|
|
2658
|
-
"""Analyze
|
|
2691
|
+
"""Analyze User's text for sentiment signals.
|
|
2659
2692
|
|
|
2660
2693
|
Returns detected sentiment, intensity, and action guidance for NEXO.
|
|
2661
2694
|
Not a model — keyword + heuristic based. Fast and deterministic.
|
|
@@ -2719,7 +2752,7 @@ def detect_sentiment(text: str) -> dict:
|
|
|
2719
2752
|
|
|
2720
2753
|
|
|
2721
2754
|
def log_sentiment(text: str) -> dict:
|
|
2722
|
-
"""Detect and log
|
|
2755
|
+
"""Detect and log User's sentiment. Returns the detection result."""
|
|
2723
2756
|
result = detect_sentiment(text)
|
|
2724
2757
|
if result["sentiment"] != "neutral":
|
|
2725
2758
|
db = _get_db()
|
|
File without changes
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""NEXO Brain Dashboard — FastAPI app for inspecting cognitive state.
|
|
2
|
+
|
|
3
|
+
Local dashboard: graphs, memories, somatic markers, trust, adaptive personality.
|
|
4
|
+
Runs on-demand (not embedded in MCP stdio). Opens browser automatically.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python3 -m dashboard.app [--port 6174] [--no-browser]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import webbrowser
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI, Query, Request
|
|
18
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
19
|
+
|
|
20
|
+
# Add parent dir to path so we can import nexo-mcp modules
|
|
21
|
+
_PARENT = str(Path(__file__).resolve().parent.parent)
|
|
22
|
+
if _PARENT not in sys.path:
|
|
23
|
+
sys.path.insert(0, _PARENT)
|
|
24
|
+
|
|
25
|
+
app = FastAPI(title="NEXO Brain Dashboard", version="1.0.0")
|
|
26
|
+
|
|
27
|
+
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Lazy imports — modules live in the parent nexo-mcp directory
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def _cognitive():
|
|
34
|
+
import cognitive
|
|
35
|
+
return cognitive
|
|
36
|
+
|
|
37
|
+
def _knowledge_graph():
|
|
38
|
+
import knowledge_graph as kg
|
|
39
|
+
return kg
|
|
40
|
+
|
|
41
|
+
def _db():
|
|
42
|
+
import db as nexo_db
|
|
43
|
+
return nexo_db
|
|
44
|
+
|
|
45
|
+
def _adaptive():
|
|
46
|
+
from plugins import adaptive_mode
|
|
47
|
+
return adaptive_mode
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# HTML page routes — serve template files
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def _render_template(name: str) -> HTMLResponse:
|
|
55
|
+
"""Read a template file and return as HTML."""
|
|
56
|
+
path = TEMPLATES_DIR / name
|
|
57
|
+
if not path.exists():
|
|
58
|
+
return HTMLResponse(
|
|
59
|
+
f"<html><body><h1>Template not found: {name}</h1>"
|
|
60
|
+
f"<p>Create it at <code>{path}</code></p></body></html>",
|
|
61
|
+
status_code=200,
|
|
62
|
+
)
|
|
63
|
+
return HTMLResponse(path.read_text(encoding="utf-8"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.get("/", response_class=HTMLResponse)
|
|
67
|
+
async def page_overview():
|
|
68
|
+
return _render_template("overview.html")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.get("/graph", response_class=HTMLResponse)
|
|
72
|
+
async def page_graph():
|
|
73
|
+
return _render_template("graph.html")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.get("/memory", response_class=HTMLResponse)
|
|
77
|
+
async def page_memory():
|
|
78
|
+
return _render_template("memory.html")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.get("/somatic", response_class=HTMLResponse)
|
|
82
|
+
async def page_somatic():
|
|
83
|
+
return _render_template("somatic.html")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.get("/adaptive", response_class=HTMLResponse)
|
|
87
|
+
async def page_adaptive():
|
|
88
|
+
return _render_template("adaptive.html")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.get("/sessions", response_class=HTMLResponse)
|
|
92
|
+
async def page_sessions():
|
|
93
|
+
return _render_template("sessions.html")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# API endpoints — JSON
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
@app.get("/api/stats")
|
|
101
|
+
async def api_stats():
|
|
102
|
+
"""Overview: trust score, memory counts, KG stats."""
|
|
103
|
+
cog = _cognitive()
|
|
104
|
+
kg = _knowledge_graph()
|
|
105
|
+
|
|
106
|
+
trust = cog.get_trust_score()
|
|
107
|
+
cog_stats = cog.get_stats()
|
|
108
|
+
kg_stats = kg.stats()
|
|
109
|
+
gate_stats = cog.get_gate_stats()
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"trust_score": trust,
|
|
113
|
+
"cognitive": cog_stats,
|
|
114
|
+
"knowledge_graph": kg_stats,
|
|
115
|
+
"prediction_gate": gate_stats,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.get("/api/graph")
|
|
120
|
+
async def api_graph(
|
|
121
|
+
center: int = Query(None, description="Center node ID for subgraph"),
|
|
122
|
+
depth: int = Query(2, ge=1, le=5, description="Traversal depth"),
|
|
123
|
+
node_type: str = Query(None, description="Filter by node type"),
|
|
124
|
+
node_ref: str = Query(None, description="Find node by type+ref"),
|
|
125
|
+
):
|
|
126
|
+
"""Subgraph for D3 visualization."""
|
|
127
|
+
kg = _knowledge_graph()
|
|
128
|
+
|
|
129
|
+
# If node_type+node_ref given, resolve to center ID
|
|
130
|
+
if center is None and node_type and node_ref:
|
|
131
|
+
node = kg.get_node(node_type, node_ref)
|
|
132
|
+
# Fallback: try with type prefix (refs stored as "area:wazion", "file:path")
|
|
133
|
+
if not node:
|
|
134
|
+
node = kg.get_node(node_type, f"{node_type}:{node_ref}")
|
|
135
|
+
if node:
|
|
136
|
+
center = node["id"]
|
|
137
|
+
|
|
138
|
+
if center is None:
|
|
139
|
+
# Return full graph stats + top connected nodes as starting points
|
|
140
|
+
s = kg.stats()
|
|
141
|
+
return {
|
|
142
|
+
"nodes": [],
|
|
143
|
+
"edges": [],
|
|
144
|
+
"hints": s.get("most_connected", []),
|
|
145
|
+
"stats": {
|
|
146
|
+
"total_nodes": s["nodes"],
|
|
147
|
+
"total_edges": s["edges_active"],
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
subgraph = kg.extract_subgraph(center, depth=depth)
|
|
152
|
+
return subgraph
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.get("/api/memories")
|
|
156
|
+
async def api_memories(
|
|
157
|
+
q: str = Query("", description="Search query"),
|
|
158
|
+
store: str = Query("both", description="stm, ltm, or both"),
|
|
159
|
+
limit: int = Query(20, ge=1, le=100),
|
|
160
|
+
):
|
|
161
|
+
"""Memory search via cognitive engine."""
|
|
162
|
+
cog = _cognitive()
|
|
163
|
+
|
|
164
|
+
if not q:
|
|
165
|
+
return {"results": [], "message": "Provide ?q= parameter to search"}
|
|
166
|
+
|
|
167
|
+
results = cog.search(q, top_k=limit, stores=store)
|
|
168
|
+
# Serialize — results may contain numpy arrays or sqlite Rows
|
|
169
|
+
serialized = []
|
|
170
|
+
for r in results:
|
|
171
|
+
item = dict(r) if hasattr(r, "keys") else r
|
|
172
|
+
# Remove embedding blob if present
|
|
173
|
+
item.pop("embedding", None)
|
|
174
|
+
item.pop("vec", None)
|
|
175
|
+
serialized.append(item)
|
|
176
|
+
return {"query": q, "store": store, "count": len(serialized), "results": serialized}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.get("/api/somatic")
|
|
180
|
+
async def api_somatic():
|
|
181
|
+
"""Somatic marker risk scores."""
|
|
182
|
+
cog = _cognitive()
|
|
183
|
+
top_risks = cog.somatic_top_risks(limit=20)
|
|
184
|
+
return {"risks": top_risks}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.get("/api/trust")
|
|
188
|
+
async def api_trust():
|
|
189
|
+
"""Trust score history (last 30 days)."""
|
|
190
|
+
cog = _cognitive()
|
|
191
|
+
current = cog.get_trust_score()
|
|
192
|
+
history = cog.get_trust_history(days=30)
|
|
193
|
+
return {
|
|
194
|
+
"current_score": current,
|
|
195
|
+
"history": history,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.get("/api/adaptive")
|
|
200
|
+
async def api_adaptive():
|
|
201
|
+
"""Adaptive personality: current weight state + mode history."""
|
|
202
|
+
adp = _adaptive()
|
|
203
|
+
state = adp._load_state()
|
|
204
|
+
# Get recent history from DB
|
|
205
|
+
db = _db()
|
|
206
|
+
conn = db.get_db()
|
|
207
|
+
rows = conn.execute(
|
|
208
|
+
"SELECT * FROM adaptive_log ORDER BY timestamp DESC LIMIT 50"
|
|
209
|
+
).fetchall()
|
|
210
|
+
history = [dict(r) for r in rows]
|
|
211
|
+
return {
|
|
212
|
+
"state": state,
|
|
213
|
+
"weights": adp.WEIGHTS,
|
|
214
|
+
"modes": {k: v["description"] for k, v in adp.MODES.items()},
|
|
215
|
+
"history": history,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@app.get("/api/sessions")
|
|
220
|
+
async def api_sessions(limit: int = Query(10, ge=1, le=50)):
|
|
221
|
+
"""Recent session diaries."""
|
|
222
|
+
db = _db()
|
|
223
|
+
conn = db.get_db()
|
|
224
|
+
rows = conn.execute(
|
|
225
|
+
"SELECT * FROM session_diary ORDER BY created_at DESC LIMIT ?",
|
|
226
|
+
(limit,),
|
|
227
|
+
).fetchall()
|
|
228
|
+
diaries = [dict(r) for r in rows]
|
|
229
|
+
return {"count": len(diaries), "sessions": diaries}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@app.get("/api/kg/nodes")
|
|
233
|
+
async def api_kg_nodes(
|
|
234
|
+
node_type: str = Query(None, description="Filter by node type"),
|
|
235
|
+
limit: int = Query(100, ge=1, le=500),
|
|
236
|
+
):
|
|
237
|
+
"""List KG nodes, optionally filtered by type."""
|
|
238
|
+
kg = _knowledge_graph()
|
|
239
|
+
db = kg._get_db()
|
|
240
|
+
if node_type:
|
|
241
|
+
rows = db.execute(
|
|
242
|
+
"SELECT * FROM kg_nodes WHERE node_type = ? ORDER BY id DESC LIMIT ?",
|
|
243
|
+
(node_type, limit),
|
|
244
|
+
).fetchall()
|
|
245
|
+
else:
|
|
246
|
+
rows = db.execute(
|
|
247
|
+
"SELECT * FROM kg_nodes ORDER BY id DESC LIMIT ?", (limit,)
|
|
248
|
+
).fetchall()
|
|
249
|
+
nodes = [dict(r) for r in rows]
|
|
250
|
+
return {"count": len(nodes), "nodes": nodes}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# Main — run with uvicorn
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def main():
|
|
258
|
+
parser = argparse.ArgumentParser(description="NEXO Brain Dashboard")
|
|
259
|
+
parser.add_argument("--port", type=int, default=6174, help="Port (default: 6174)")
|
|
260
|
+
parser.add_argument("--no-browser", action="store_true", help="Don't open browser")
|
|
261
|
+
args = parser.parse_args()
|
|
262
|
+
|
|
263
|
+
if not args.no_browser:
|
|
264
|
+
# Open browser after a short delay (uvicorn will be starting)
|
|
265
|
+
import threading
|
|
266
|
+
def _open():
|
|
267
|
+
import time
|
|
268
|
+
time.sleep(1.2)
|
|
269
|
+
webbrowser.open(f"http://localhost:{args.port}")
|
|
270
|
+
threading.Thread(target=_open, daemon=True).start()
|
|
271
|
+
|
|
272
|
+
import uvicorn
|
|
273
|
+
uvicorn.run(app, host="127.0.0.1", port=args.port, log_level="info")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
main()
|