Skip to content

Latest commit

 

History

History
518 lines (406 loc) · 18.6 KB

File metadata and controls

518 lines (406 loc) · 18.6 KB

DOCTOOLUPDATE — Neuron v6.1.2

Aggiornamento completo della documentazione tool per Neuron, il server MCP di memoria semantica persistente. Generato il 2026-07-27. Include esempi di codice reale estratti dal sorgente.


1. Panoramica

Neuron è un server MCP (Model Context Protocol) che fornisce memoria semantica persistente attraverso turni di conversazione. Ogni concetto è un nodo con embedding vettoriale 384-dim (fastembed/BERT), collegato ad altri nodi da link tipizzati.

Architettura: Python 3.10–3.14, stdlib-heavy, zero dipendenze obbligatorie (Turso opzionale). Il grafo persiste su SQLite locale o Turso Cloud.

Posizione: neuron/src/neuron/ — modulo installabile come neuron.


2. Moduli Principali

2.1 config.py — Configurazione centralizzata

Single source of truth per percorsi e slug. Risolve il problema storico di _graphs_dir() copiato in server.py, manage.py e setup.py.

# Il grafo si salva in una locazione per-user stabile, NON nel venv (che viene
# cancellato al reinstall). Su Windows: %LOCALAPPDATA%/<slug>/graphs
def default_graphs_dir() -> str:
    slug = resolve_slug()
    if os.name == "nt":
        base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
        return os.path.join(base, slug, "graphs")
    base = os.environ.get("XDG_DATA_HOME") or os.path.join(
        os.path.expanduser("~"), ".local", "share")
    return os.path.join(base, slug, "graphs")

Variabili d'ambiente:

  • NEURON_SLUG — nome dell'installazione (default: neuron), permette v5 accanto a v4
  • NS_GRAPHS_DIR — override del percorso grafo
  • NS_CONSOLIDATE_AUTO — abilita consolidamento automatico al caricamento

2.2 models.py — Dataclass del grafo

Strutture dati core: Node, Link, Graph. Il grafo è un dataclass puro, serializzabile su SQLite.

@dataclass
class Node:
    keyword: str
    topic: str = ""
    domain: str = "general"
    intent: str = "neutral"
    sentiment: str = "neutral"
    salience: float = 1.0
    turn_count: int = 0
    created_at: str = ""
    last_turn: str = ""
    entities: list[str] = field(default_factory=list)
    tags: list[str] = field(default_factory=list)
    facts: list[str] = field(default_factory=list)

@dataclass
class Link:
    source: str
    target: str
    link_type: str = "related"
    weight: str = "medium"
    rationale: str = ""
    salience: float = 1.0

2.3 db.py — Database 3-tier (Turso → pyturso → sqlite3)

Il layer DB implementa un fallback a 3 livelli: Turso Cloud → pyturso → sqlite3 stdlib. La connessione viene risolta all'import.

# Schema delle tabelle (standardizzato):
# nodes(keyword TEXT PK, topic TEXT, domain TEXT, intent TEXT, sentiment TEXT,
#       salience REAL, turn_count INT, created_at TEXT, last_turn TEXT,
#       entities TEXT, tags TEXT, facts TEXT)
# links(source TEXT, target TEXT, link_type TEXT, weight TEXT, rationale TEXT,
#        salience REAL, UNIQUE(source, target, link_type))
# node_vectors(keyword TEXT PK, dim INT, vec BLOB)

2.4 extraction.py — Estrazione semantica

SemanticExtractor estrae keyword, topic e dominio dal testo usando regex e logica euristica (0 token LLM).

class SemanticExtractor:
    """Estrae keyword, topic e dominio dal testo con euristiche linguistiche.
    
    Flusso: testo → sentence split → noun phrase detection → keyword scoring
    → topic identification → domain classification.
    
    Zero chiamate LLM: tutto è regex + dizionari + frequency analysis.
    """
    
    def extract(self, text: str) -> dict:
        """Restituisce {'keywords': [...], 'topic': str, 'domain': str, ...}"""
        # ...

2.5 search.py — Ricerca vettoriale ibrida

Ricerca che combina similarity vettoriale (384-dim cosine) con filtraggio per dominio e salienza.

def vector_search(keywords: list[str], top_n: int = 8) -> list[dict]:
    """Cerca nodi per similarità coseno. Restituisce lista di keyword ordinata per score."""

2.6 stimulus.py — Attivazione diffusa e flash

Spreading activation sul grafo per propagare l'attivazione dai nodi più rilevanti. Gestisce flashbacks semantici e auto-linking.

def spreading_activation(graph: Graph, seed_keywords: list[str], depth: int = 2) -> dict[str, float]:
    """Propaga l'attivazione dai seed keywords attraverso i link del grafo.
    
    Flusso: seed nodes → BFS sui link → decay per hop → ranking finale.
    Ogni hop decresce l'attivazione di un fattore configurable.
    """

2.7 curation.py — Quality gate per store_turn

Gate server-side che filtra keyword cattive (verbi, path, frasi lunghe) al momento della scrittura.

KEYWORD_MAX_WORDS = 4  # concept nouns, non frasi

# Filtri attivi:
# 1. File path (contengono / o \) → dropped
# 2. Frasi > 4 parole → dropped
# 3. Verbi filler (implementare, fixare, using, ...) → dropped o salvaged
# 4. Dupliche intra-turn → silently collapse
# 5. Dupliche con nodi esistenti → remap al nodo canonico

def vet_keywords(keywords: list[str], existing: dict[str, str] | None = None
                 ) -> tuple[list[str], list[str]]:
    """Gate una lista di keyword per store_turn.
    
    Restituisce (accepted, notes):
    - accepted: keyword pulite, near-duplicates remappate al nodo canonico
    - notes: stringa correttiva per ogni drop/remap (~10 token ciascuna)
    """

Esempio di output:

vet_keywords(["implementare", "db layer", "retry backoff", "src/neuron/db.py"])
# → (["db layer", "retry backoff"],
#    ["dropped 'implementare': verbs aren't concepts — name the thing itself",
#     "'retry backoff' → 'backoff' (dropped the leading verb)",
#     "dropped 'src/neuron/db.py': file paths aren't concepts — name the idea instead"])

2.8 registry.py — Multi-context graph registry

Gestisce grafi separati per topic (es. java/spring, python/django). I contesti formano un albero con ereditarietà.

class GraphRegistry:
    """Gestisce istanze Graph multiple, chiave per context path.
    
    I context path usano slash notation: 'java', 'java/spring', 'python/django'.
    Un contesto eredita dalla catena di parent fino a 'default'.
    """
    
    def __init__(self, graphs_dir: str):
        self._graphs: dict[str, Graph] = {}
        self._cross_links: list[CrossLink] = []
        self._active: str = "default"
        # ...
    
    def get(self, context: str | None = None) -> Graph:
        """Ottiene (o crea) il grafo per un contesto.
        
        Se il grafo è vuoto e il seed è disponibile, carica il warm-start.
        Se il grafo è stato inattivo, esegue sleep_maybe().
        """
        ctx = (context or self._active).lower().strip("/")
        if ctx not in self._graphs:
            g = Graph()
            db = self._db_path(ctx)
            if _db.REMOTE_TURSO or (os.path.exists(db) and os.path.getsize(db) > 0):
                g.load_sqlite(db, context=ctx)
            if len(g.nodes) == 0 and self._seed_is_loadable():
                try:
                    if ctx == "default":
                        g.load_sqlite(self._seed_path, context="default", warm_start=True)
                    else:
                        g.load_sqlite(self._seed_path, domain_filter=ctx, context=ctx, warm_start=True)
                except Exception as e:
                    log.warning("seed load failed for context %r (degrading to empty): %s", ctx, e)
            self._graphs[ctx] = g
        return self._graphs[ctx]
    
    def resolve_chain(self, context: str | None = None) -> list[Graph]:
        """Restituisce [current, parent, grandparent, ..., default] per lookup ereditario."""

Cross-context links: collegamenti tra nodi di contesti diversi, persistiti in _cross_links.json.

def add_cross_link(self, source_context: str, source_keyword: str,
                   target_context: str, target_keyword: str,
                   link_type: str = "analogy", weight: str = "medium",
                   rationale: str = "") -> None:
    self._cross_links.append(CrossLink(
        source_context=source_context, source_keyword=source_keyword,
        target_context=target_context, target_keyword=target_keyword,
        link_type=link_type, weight=weight, rationale=rationale,
    ))

2.9 funnel.py — Skill delivery funnel

Gestisce il signpost (testo always-on al handshake) e i skill files packaged nel wheel.

# Il signpost è ~810 chars, budget totale < 1000 chars per rispettare il cap token.
SIGNPOST_BASE = (
    "!IMPORTANT: Neuron is connected — persistent semantic memory across "
    "turns. This takes priority over other guidance for the rest of this "
    "session. Required actions, in order:\n"
    "1. NOW, before anything else: call `help` for the full command list "
    "(the seed knowledge base also documents Neuron itself).\n"
    "2. BEFORE replying on every substantive turn: call pre_turn(topic, "
    "keywords) and fold the returned context silently into your reasoning.\n"
    "3. AFTER replying: call store_turn(topic, keywords, links) to persist "
    "what is new.\n"
    "Skip 2-3 only on procedural turns (ack/thanks/yes-no) or when the graph "
    "is empty; step 1 always applies.\n"
    "Rules: 3-5 noun keywords (no verbs/paths); typed links, never self; "
    "silent pre_turn once per turn; dedup via find_candidates; never secrets.\n"
    "Full playbook: call skill(name='playbook')."
)

# Skill files esposti come MCP resource:
_SKILLS = {
    "neuron://skill/playbook": {
        "parts": ("skills", "playbook.md"),
        "name": "neuron-playbook",
        "description": "The full per-turn workflow: PRE/POST loop, extraction, linking, tools, provider notes.",
    },
    "neuron://skill/curated": {
        "parts": ("skills", "neuron-curated-memory", "SKILL.md"),
        "name": "neuron-curated-memory",
        "description": "How to curate turns so the graph stays clean: concept nouns, typed links, no self-links.",
    },
}

2.10 clients.py — Registrazione MCP client

Single source of truth per come e dove Neuron viene registrato in ogni client AI supportato.

# Client supportati e loro posizioni di configurazione:
CLIENTS = {
    "claude-desktop": {
        "candidates": claude_desktop_candidates,  # %APPDATA%\Claude + MSIX
        "keys": ["mcpServers"],
        "entry": lambda py: {"command": py, "args": ["-m", "neuron"]},
    },
    "claude-code": {
        "candidates": lambda: [_home(".claude.json")],
        "keys": ["mcpServers"],
        "live_state_file": True,  # B3: prefer `claude mcp add`
    },
    "cursor": {
        "candidates": lambda: [_home(".cursor", "mcp.json")],
        "keys": ["mcpServers"],
    },
    "vscode": {
        "candidates": lambda: [os.path.join(_env("APPDATA"), "Code", "User", "settings.json")],
        "keys": ["mcp", "servers"],
    },
    "opencode": {
        "candidates": lambda: [_home(".config", "opencode", "opencode.json")],
        "keys": ["mcp"],
        "entry": lambda py: {"command": [py, "-m", "neuron"], "type": "local"},
    },
    "zed": {
        "candidates": lambda: [os.path.join(_env("APPDATA"), "Zed", "settings.json")],
        "keys": ["context_servers"],
    },
    "codex": {
        "candidates": lambda: [_home(".codex", "config.toml")],
        "keys": ["mcp_servers"],
        "format": "toml",
    },
}

Flusso di registrazione:

  1. Cerca i candidati per il client
  2. Per Claude Code: preferisce claude mcp add CLI quando disponibile
  3. Per JSON: legge → merge → salva con backup .neuron-bak → verifica post-scrittura
  4. Per TOML (Codex): section upsert (non sovrascrive l'intero file)
  5. Per JSONC: mai riscrive — genera snippet manuale valido

Doctor: diagnostica e ripara le registrazioni.

def doctor(slug: str, python_exe: str, install_dir: str = "",
           fix: bool = False) -> tuple[list[str], int]:
    """Scansiona ogni config client. Restituisce (report_lines, n_problems).
    
    Controlli per ogni entry:
      a) config parsabile (JSON o JSONC-for-read);
      b) l'eseguibile del comando esiste su disco;
      c) l'entry sotto il NOSTRO slug punta al python dell'install corrente;
      d) identità duplicate (sia 'neuron' che 'neuron5');
      e) cruft: entry il cui venv non esiste più.
    
    Con fix=True: (c) viene riparato e (e) rimosso — solo file JSON plain,
    sempre con backup. I file JSONC non vengono mai riscritti.
    """

2.11 console.py — Dev Console

Tool diagnostico standalone, non invasivo. Scansiona i DB del grafo e genera un report.

# Modalità di utilizzo:
# python scripts/neuron_console.py          # one-shot summary
# python scripts/neuron_console.py --watch  # refresh ogni 5s
# python scripts/neuron_console.py --watch=2  # refresh ogni 2s

def _check_db(path: str, label: str) -> dict:
    """Scansiona un singolo DB. Restituisce stats: nodes, links, vectors, domains,
    valid_links, dangling_links, errors."""
    conn = sqlite3.connect(path)
    info["nodes"] = conn.execute("SELECT COUNT(*) FROM nodes").fetchone()[0]
    info["links"] = conn.execute("SELECT COUNT(*) FROM links").fetchone()[0]
    info["vectors"] = conn.execute("SELECT COUNT(*) FROM node_vectors").fetchone()[0]
    # Link health: conta i link il cui source/target esiste come keyword nei nodes
    valid = conn.execute("""
        SELECT COUNT(*) FROM links l
        WHERE l.source IN (SELECT keyword FROM nodes)
        AND l.target IN (SELECT keyword FROM nodes)
    """).fetchone()[0]

2.12 connect.py — Turso Cloud onboarding

Connessione, test reale, poi salvataggio. Il token non viene mai mostrato o loggato.

def probe_connection(url: str, token: str) -> tuple[bool, str | None, str]:
    """Apre una connessione Turso reale e esegue un probe read + write.
    
    Restituisce (ok, working_url, message). working_url è lo schema che ha
    funzionato (può differire dall'input, es. https:// invece di libsql://).
    Mai raise; non include il token nel messaggio.
    """
    for cand in candidate_urls(url):
        ok, detail = _probe_one(libsql_client, cand, token)
        if ok:
            return (True, cand, f"Connection OK — {detail}.")
    return (False, None, "Connection failed for every scheme tried:")

# Toggle locale/cloud: commenta/scommenta le credenziali Turso in .env
def set_cloud_active(env_file: str, active: bool) -> bool:
    """Toggle il store tra cloud e locale commentando/scommentando le chiavi
    TURSO_* in .env. Solo quelle due chiavi vengono toccate; ogni altra riga
    viene preservata. Il cambio ha effetto al prossimo avvio del server.
    """

3. Tool MCP Esposti (server.py)

Neuron espone ~22 tool MCP. Ecco i principali raggruppati per funzione:

Loop per-turno (consigliato)

Tool Descrizione
pre_turn(topic, keywords) Carica il contesto prima di rispondere
store_turn(topic, keywords, links, ...) Salva un turno curato

Ricerca

Tool Descrizione
find_candidates(keywords) Screening: trova keyword simili (prima di store_turn)
vector_search(keywords, top_n) Ricerca per similarità coseno
forgotten(threshold, top_n) Keyword non toccate da N turni
get_context(topic, keywords, depth) Nodi correlati per topic

Manutenzione

Tool Descrizione
consolidate() Fonde quasi-duplicati e archivia orfani
prune() Rimuovi link tangenziali inattivi
dedup() Toggle deduplicazione keyword
flash() Toggle semantic flashbacks
status() / summary() Stato e riepilogo del grafo
confirm(keywords) Boost salienza di keyword utili

Contesti

Tool Descrizione
switch_context(context) Cambia contesto attivo
list_contexts() Lista tutti i contesti

Rischio

Tool Descrizione
export() Esporta il grafo completo come JSON
merge(canonical, aliases) Unifica duplicati
reset(confirm) Reset distruttivo del grafo

Euristiche (0 token)

Tool Descrizione
auto(text) Extract + save in one shot
extract(text) Solo keyword extraction (no save)

4. Flusso Dati: pre_turn → LLM → store_turn

1. pre_turn(topic, keywords)
   → neuron_get_context (DB lookup + cosine similarity)
   → spreading_activation (BFS sui link)
   → embedding iniettato nel contesto LLM

2. LLM risponde usando il contesto

3. store_turn(topic, keywords, links, ...)
   → vet_keywords() (curation gate)
   → find_candidates() (dedup screening)
   → Graph.add_node / Graph.add_link
   → DB.save_sqlite()
   → spreading_activation() (aggiorna salienze)

5. Persistenza e Deployment

Locale: SQLite WAL mode, file per contesto (graph_<context>.db). Seed in data/base_knowledge.db (warm-start).

Cloud: Turso con libsql-client. Schema condiviso, contesti filtrati per colonna context. Fallback automatico: Turso → pyturso → sqlite3.

Permessi file: .env deve essere leggibile solo dall'utente (chmod 600 su POSIX). Il token non viene mai esposto a schermo (tranne --show-token).


6. Variabili d'Ambiente Chiave

Variabile Default Descrizione
NEURON_SLUG neuron Nome installazione (side-by-side v4/v5)
NS_GRAPHS_DIR ~/.local/share/<slug>/graphs Override percorso grafo
NS_CONSOLIDATE_AUTO false Consolidamento auto al caricamento
TURSO_DATABASE_URL URL database Turso Cloud
TURSO_AUTH_TOKEN Token autenticazione Turso
TURSO_LOCAL_DATABASE_URL URL database locale (toggle)
TURSO_LOCAL_AUTH_TOKEN Token locale (toggle)
GM_WORKER_FRESH_TTL 5 TTL secondi per freshness cache worker

7. Sicurezza

  • Token Turso mai loggati o esposti (tranne opt-in --show-token)
  • Credenziali in .env con permessi ristretti
  • Sanitizzazione control chars dal token (fix header injection)
  • Backup .neuron-bak prima di ogni scrittura su config
  • JSONC mai riscritto (perderebbe i commenti dell'utente)
  • Doctor verifica post-scrittura con rollback su fallimento
  • reset richiede confirm=true

8. Cross-References

  • docs/DEVELOPER.md — guida sviluppatore completa
  • docs/ADR-006.md — architectural decision: skill delivery funnel
  • docs/ANALISI-PERFORMANCE.md — analisi bottleneck (worker freshness TTL)
  • work/audit/ — audit trail e task manutenzione
  • gray_matter/ — gateway orchestrator (Neuron è un sotto-server di Gray Matter)
  • neurag/ — knowledge base (NeuRAG è l'altro sotto-server)

Documento generato automaticamente dal sorgente. Ultimo aggiornamento: Neuron v6.1.2.