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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # NEXO Brain — Your AI Gets a Brain
2
2
 
3
- [![npm v0.6.0](https://img.shields.io/npm/v/nexo-brain?label=npm&color=purple)](https://www.npmjs.com/package/nexo-brain)
3
+ [![npm v0.8.0](https://img.shields.io/npm/v/nexo-brain?label=npm&color=purple)](https://www.npmjs.com/package/nexo-brain)
4
4
  [![F1 0.588 on LoCoMo](https://img.shields.io/badge/LoCoMo_F1-0.588-brightgreen)](https://github.com/wazionapps/nexo/blob/main/benchmarks/locomo/results/)
5
5
  [![+55% vs GPT-4](https://img.shields.io/badge/vs_GPT--4-%2B55%25-blue)](https://github.com/snap-research/locomo/issues/33)
6
6
  [![GitHub stars](https://img.shields.io/github/stars/wazionapps/nexo?style=social)](https://github.com/wazionapps/nexo/stargazers)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
- > **v0.6.0** — Now ships with **full orchestration**: 5 automated hooks, mandatory post-mortem with self-critique, pre-compaction context preservation, reflection engine, and auto-migration. Plus: F1 **0.588** on [LoCoMo](https://github.com/snap-research/locomo) (ACL 2024) — outperforms GPT-4 by 55%. Runs on CPU. [Full results](benchmarks/locomo/results/)
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 21 cognitive tools on top of the 76 base tools, totaling **97+ MCP tools**. These features implement cognitive science concepts that go beyond basic memory:
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.6.0)
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 | 97+ tools for memory, cognition, learning, guard | ~/.nexo/ |
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** (Linux support planned)
346
+ - **macOS, Linux, or Windows**
320
347
  - **Node.js 18+** (for the installer)
321
- - **Claude Opus (latest version) strongly recommended.** NEXO Brain provides 97+ 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 97+ tools without hesitation.
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
- ### 97+ MCP Tools across 17 Categories
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 97+ tools are available immediately after installation. The installer configures Claude Code's `~/.claude/settings.json` automatically.
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("NEXO currently supports macOS only. Linux support coming soon.");
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.7.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", "wazion", "whatsapp", "chrome", "firefox",
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, # the user delegates new task without micromanaging
69
- "paradigm_shift": +2, # the user teaches, NEXO learns
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, # the user corrects NEXO
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: the user's detected mood per interaction
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 the user overrides a memory's guidance
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 the user said
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 the user gives a new instruction that conflicts with established LTM memories
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 the user
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 the user's decision.
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': the user changed his mind permanently. Decay old memory,
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: the user keeps overriding them, suggesting
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 the user's text for sentiment signals.
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 the user's sentiment. Returns the detection result."""
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()