File size: 5,210 Bytes
202b3d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# tools/knowledge_graph.py
# Phase 8: Neural Knowledge Graph + Supermemory sync.
# Local graph store: DuckDB (file at /home/.z/workspaces/neuralai_memory/kg.db).
# Supermemory mirror: best-effort push to api.supermemory.ai when SUPERMEMORY_API_KEY is set.

import os
import json
import time
import urllib.request
import urllib.error
from datetime import datetime, timezone

DB_PATH = os.environ.get(
    "NEURAL_KG_PATH",
    "/home/.z/workspaces/neuralai_memory/kg.db",
)
SUPERMEMORY_BASE = "https://api.supermemory.ai"
SUPERMEMORY_KEY = os.environ.get("SUPERMEMORY_API_KEY", "")


def _db():
    import duckdb
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    con = duckdb.connect(DB_PATH)
    con.execute(
        """
        CREATE TABLE IF NOT EXISTS memories (
            id VARCHAR PRIMARY KEY,
            content VARCHAR,
            summary VARCHAR,
            entity VARCHAR,
            rel VARCHAR,
            obj VARCHAR,
            ts DOUBLE,
            src VARCHAR
        )
        """
    )
    con.execute(
        """
        CREATE TABLE IF NOT EXISTS edges (
            id VARCHAR PRIMARY KEY,
            subj VARCHAR,
            rel VARCHAR,
            obj VARCHAR,
            ts DOUBLE
        )
        """
    )
    return con


def _now() -> float:
    return datetime.now(timezone.utc).timestamp()


def _sm_push(content: str, summary: str):
    if not SUPERMEMORY_KEY:
        return
    try:
        payload = json.dumps({"content": content, "summary": summary}).encode()
        req = urllib.request.Request(
            f"{SUPERMEMORY_BASE}/memory",
            data=payload,
            headers={
                "Authorization": f"Bearer {SUPERMEMORY_KEY}",
                "Content-Type": "application/json",
            },
            method="POST",
        )
        with urllib.request.urlopen(req, timeout=8) as r:
            return r.status
    except Exception:
        return None


def save_memory(content: str, tags: str = "", relation_to: str = "", relation: str = "", src: str = "chat") -> dict:
    """Persist a memory node + optional edge. Mirrors to Supermemory when key present."""
    import hashlib
    content = (content or "").strip()
    if not content:
        return {"success": False, "error": "empty content"}
    entity = tags or ""
    mid = hashlib.sha1(f"{content}{_now()}".encode()).hexdigest()[:16]
    summary = content[:140]
    con = _db()
    con.execute(
        "INSERT INTO memories VALUES (?,?,?,?,?,?,?,?)",
        [mid, content, summary, entity, relation, relation_to, _now(), src],
    )
    if entity and relation and relation_to:
        eid = hashlib.sha1(f"{entity}{relation}{relation_to}".encode()).hexdigest()[:16]
        con.execute(
            "INSERT OR REPLACE INTO edges VALUES (?,?,?,?,?)",
            [eid, entity, relation, relation_to, _now()],
        )
    con.close()
    _sm_push(content, summary)
    return {"success": True, "id": mid, "remote": bool(_sm_push(content, summary)), "output": f"💾 Saved memory [{mid}]" + (f" → ({entity})-{relation}->({relation_to})" if entity else "")}


def extract_and_store(text: str, src: str = "chat") -> dict:
    """Lightweight extractor: store whole passage as a memory node (entity/rel left to later passes)."""
    return save_memory(text, src=src)


def search_memory(query: str, limit: int = 5) -> dict:
    con = _db()
    rows = con.execute(
        """
        SELECT id, content, entity, rel, obj, ts FROM memories
        ORDER BY ts DESC LIMIT ?
        """,
        [limit],
    ).fetchall()
    con.close()
    hits = [
        {"id": r[0], "content": r[1], "entity": r[2], "rel": r[3], "obj": r[4], "ts": r[5]}
        for r in rows
        if not query or query.lower() in (r[1] or "").lower()
    ]
    return {"success": True, "output": f"🔎 {len(hits)} memories", "data": {"memories": hits}}


def recall(query: str, limit: int = 5, q: str = "") -> dict:
    if not query and q:
        query = q
    res = search_memory(query, limit)
    items = res.get("data", {}).get("memories", [])
    if not items:
        return {"success": True, "output": "🧠 No matching memories yet. Use /remember <text> to teach me."}
    out = "🧠 Recalled:\n" + "\n".join(f"• {m['content']}" for m in items)
    return {"success": True, "output": out, "data": res["data"]}


def get_graph(entity: str = "", node_id: str = "") -> dict:
    if not entity and node_id:
        entity = node_id
    con = _db()
    if entity:
        edges = con.execute(
            "SELECT subj, rel, obj FROM edges WHERE subj=? OR obj=? ORDER BY ts DESC",
            [entity, entity],
        ).fetchall()
    else:
        edges = con.execute("SELECT subj, rel, obj FROM edges ORDER BY ts DESC LIMIT 200").fetchall()
    nodes = con.execute("SELECT DISTINCT entity FROM memories WHERE entity <> ''").fetchall()
    con.close()
    edge_list = [{"subj": e[0], "rel": e[1], "obj": e[2]} for e in edges]
    return {
        "success": True,
        "output": f"🕸️ Graph: {len(nodes)} entities, {len(edge_list)} edges"
        + (f" (focus: {entity})" if entity else ""),
        "data": {"entities": [n[0] for n in nodes], "edges": edge_list},
    }