gnosion 0.2.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.
gnosion/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """Gnosion — a portable, self-improving cognitive engine.
2
+
3
+ Pluggable cognition heads (vision / text / design / tabular / memory) that learn
4
+ from every input, guarded so they never regress or memorise junk, and exportable
5
+ as one portable .gnosion file you can import into any project.
6
+
7
+ Zero required dependencies (pure-Python core); optional extras:
8
+ pip install "gnosion[embed]" -> fastembed ONNX embeddings (real semantic quality)
9
+ pip install "gnosion[ml]" -> numpy/scikit-learn acceleration
10
+ """
11
+ from .core import Gnosion
12
+
13
+ __version__ = "0.2.0"
14
+ __all__ = ["Gnosion", "__version__"]
gnosion/agent.py ADDED
@@ -0,0 +1,190 @@
1
+ """Universal coding-agent memory — a full agent-memory model usable by ANY agent
2
+ (Claude Code, Cursor, Windsurf, Cline, Aider, or your own scripts).
3
+
4
+ It persists into one portable `.gnosion` in the repo (`.gnosion/project.gnosion`) and
5
+ covers the standard agent-memory types:
6
+
7
+ fact world/semantic facts
8
+ entity facts ABOUT a subject — the user, the project, the environment
9
+ decision choices made (and why)
10
+ convention how we do things here
11
+ structure system/architecture notes
12
+ bug bugs + their fixes
13
+ preference user/team preferences
14
+ experience EPISODIC: "did X -> got Y" events, timestamped (experience tracking)
15
+ observation raw things noticed in the environment
16
+ skill PROCEDURAL: named how-to procedures the agent can recall + follow;
17
+ fully user-addable (add_skill)
18
+
19
+ Recall matches shared WORDS by default (zero-dep); `pip install "gnosion[embed]"` +
20
+ `GNOSION_SEMANTIC=1` upgrades it to semantic (paraphrase) recall.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import time
26
+ from typing import List, Optional
27
+
28
+ from .core import Gnosion
29
+
30
+ # every memory type is a semantic memory domain
31
+ KINDS = ["fact", "entity", "decision", "convention", "structure", "bug",
32
+ "preference", "experience", "observation", "skill"]
33
+ ENTITY_SUBJECTS = ["user", "project", "environment", "team", "domain"]
34
+ _DEFAULT_REL = os.path.join(".gnosion", "project.gnosion")
35
+
36
+
37
+ def _domains():
38
+ return [{"name": k, "head": "memory", "modality": "text"} for k in KINDS] + \
39
+ [{"name": "all", "head": "memory", "modality": "text"}]
40
+
41
+
42
+ def _now() -> str:
43
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
44
+
45
+
46
+ class AgentMemory:
47
+ """Repo-scoped, agent-agnostic project memory backed by a portable .gnosion."""
48
+
49
+ def __init__(self, root: str = ".", path: Optional[str] = None, semantic: Optional[bool] = None):
50
+ if semantic is None:
51
+ semantic = os.environ.get("GNOSION_SEMANTIC", "") in ("1", "true", "yes")
52
+ self.root = root
53
+ self.path = path or os.path.join(root, _DEFAULT_REL)
54
+ if os.path.exists(self.path):
55
+ self.bx = Gnosion.load(self.path)
56
+ for d in _domains(): # add any new kinds to old files
57
+ if d["name"] not in self.bx.domains:
58
+ self.bx.add_domain(d["name"], d["head"], d["modality"])
59
+ else:
60
+ self.bx = Gnosion(prefer_fastembed=semantic, domains=_domains())
61
+
62
+ # ---- generic write
63
+ def remember(self, text: str, kind: str = "fact", subject: Optional[str] = None,
64
+ tags: Optional[list] = None, meta: Optional[dict] = None, save: bool = True) -> dict:
65
+ kind = kind if kind in KINDS else "fact"
66
+ m = {"kind": kind, "ts": _now()}
67
+ if subject:
68
+ m["subject"] = subject
69
+ if tags:
70
+ m["tags"] = list(tags)
71
+ if meta:
72
+ m.update(meta)
73
+ self.bx.remember(kind, text, text, meta=m)
74
+ self.bx.remember("all", text, text, meta=m)
75
+ if save:
76
+ self.save()
77
+ return {"ok": True, "kind": kind, "stored": text[:100]}
78
+
79
+ note = remember # alias
80
+
81
+ # ---- typed helpers ---------------------------------------------------------
82
+ def fact(self, text: str, subject: Optional[str] = None, **kw) -> dict:
83
+ return self.remember(text, kind="fact", subject=subject, **kw)
84
+
85
+ def about(self, subject: str, text: str, **kw) -> dict:
86
+ """A fact ABOUT the user / project / environment (an entity)."""
87
+ return self.remember(text, kind="entity", subject=subject, **kw)
88
+
89
+ def experience(self, action: str, outcome: Optional[str] = None, tags: Optional[list] = None,
90
+ **kw) -> dict:
91
+ """Episodic memory: record that an action led to an outcome (experience tracking)."""
92
+ text = action if not outcome else f"{action} -> {outcome}"
93
+ return self.remember(text, kind="experience", tags=tags,
94
+ meta={"action": action, "outcome": outcome}, **kw)
95
+
96
+ def observe(self, text: str, tags: Optional[list] = None, **kw) -> dict:
97
+ """Raw observation noticed in the environment."""
98
+ return self.remember(text, kind="observation", tags=tags, **kw)
99
+
100
+ def add_skill(self, name: str, when_to_use: str, how: str, save: bool = True) -> dict:
101
+ """Add a user-defined SKILL: a named procedure the agent recalls and follows.
102
+ (Gnosion stores/serves it; the agent executes it — it isn't code Gnosion runs.)"""
103
+ value = f"SKILL: {name}\nWhen to use: {when_to_use}\nHow: {how}"
104
+ # index on name + when-to-use so it's found by task description
105
+ self.bx.remember("skill", f"{name}. {when_to_use}", value,
106
+ meta={"kind": "skill", "name": name, "when": when_to_use,
107
+ "how": how, "ts": _now()})
108
+ if save:
109
+ self.save()
110
+ return {"ok": True, "skill": name}
111
+
112
+ def knowledge_domains(self) -> List[str]:
113
+ """Custom knowledge domains fed via `gns feed` (marketing/seo/…) — every memory
114
+ domain that isn't a built-in agent-memory kind or the 'all' mirror."""
115
+ from .heads import MemoryHead
116
+ skip = set(KINDS) | {"all"}
117
+ return [n for n, d in self.bx.domains.items()
118
+ if n not in skip and isinstance(d.head, MemoryHead)]
119
+
120
+ # ---- read ------------------------------------------------------------------
121
+ def recall(self, query: str, kind: Optional[str] = None, k: int = 5,
122
+ min_sim: float = 0.30) -> List[dict]:
123
+ """Most relevant memories for a query. `kind` restricts scope; else all kinds
124
+ AND any fed knowledge domains."""
125
+ scopes = KINDS + self.knowledge_domains()
126
+ if kind and (kind in KINDS or kind in self.bx.domains):
127
+ hits = self.bx.search(kind, query, k=k, min_sim=min_sim)
128
+ for h in hits:
129
+ h["kind"] = kind
130
+ return hits
131
+ merged: List[dict] = []
132
+ for kk in scopes:
133
+ for h in self.bx.search(kk, query, k=k, min_sim=min_sim):
134
+ h["kind"] = kk
135
+ merged.append(h)
136
+ seen, out = set(), []
137
+ for h in sorted(merged, key=lambda x: x["similarity"], reverse=True):
138
+ if h["value"] in seen:
139
+ continue
140
+ seen.add(h["value"]); out.append(h)
141
+ return out[:k]
142
+
143
+ def recall_skill(self, task: str, k: int = 3, min_sim: float = 0.30) -> List[dict]:
144
+ """Which skill(s) apply to a task."""
145
+ return self.bx.search("skill", task, k=k, min_sim=min_sim)
146
+
147
+ def skills(self) -> List[dict]:
148
+ d = self.bx.domains.get("skill")
149
+ return [{"name": e["meta"].get("name"), "when": e["meta"].get("when"),
150
+ "how": e["meta"].get("how")} for e in (d.head.entries if d else [])]
151
+
152
+ def about_subject(self, subject: str, k: int = 10) -> List[dict]:
153
+ """Everything remembered about a subject (user/project/environment)."""
154
+ d = self.bx.domains.get("entity")
155
+ return [{"value": e["value"], "meta": e["meta"]}
156
+ for e in (d.head.entries if d else []) if e["meta"].get("subject") == subject]
157
+
158
+ def briefing(self, k_per_kind: int = 3) -> str:
159
+ """Session-start briefing: entity facts, decisions, conventions, structure,
160
+ preferences, and available skills — to load into an agent's context."""
161
+ lines = []
162
+ for kk in ("entity", "decision", "convention", "structure", "preference"):
163
+ d = self.bx.domains.get(kk)
164
+ recent = (d.head.entries[-k_per_kind:] if d and d.head.entries else [])
165
+ if recent:
166
+ lines.append(f"## {kk.title()}")
167
+ lines += [f"- {e['value']}" for e in recent]
168
+ sk = self.skills()
169
+ if sk:
170
+ lines.append("## Skills available")
171
+ lines += [f"- {s['name']}: {s['when']}" for s in sk]
172
+ return "\n".join(lines) if lines else "(no project memory yet)"
173
+
174
+ def stats(self) -> dict:
175
+ s = {"path": self.path, "kinds": {}}
176
+ for kk in KINDS:
177
+ d = self.bx.domains.get(kk)
178
+ s["kinds"][kk] = len(d.head.entries) if d else 0
179
+ s["total"] = sum(s["kinds"].values())
180
+ return s
181
+
182
+ def save(self) -> str:
183
+ os.makedirs(os.path.dirname(os.path.abspath(self.path)) or ".", exist_ok=True)
184
+ out = self.bx.export(self.path)
185
+ try: # keep the global project registry live
186
+ from . import registry
187
+ registry.record(self.root, self.stats())
188
+ except Exception: # noqa: BLE001
189
+ pass
190
+ return out
gnosion/artifact.py ADDED
@@ -0,0 +1,64 @@
1
+ """The portable .gnosion artifact — one file that holds the whole brain (all domains,
2
+ learned samples, memories, centroids, config). Pure stdlib (json + zip + gzip).
3
+
4
+ bx.export("company.gnosion") # -> a single portable file
5
+ Gnosion.load("company.gnosion") # import into any project / machine
6
+
7
+ Layout inside the zip:
8
+ manifest.json — version, dim, embedder ids, per-domain summary (human-readable)
9
+ brain.json.gz — the full serialized brain (gzip-compressed)
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import gzip
14
+ import json
15
+ import os
16
+ import time
17
+ import zipfile
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from .core import Gnosion
22
+
23
+ MAGIC = "gnosion"
24
+ FORMAT = 1
25
+
26
+
27
+ def export_brain(brain: "Gnosion", path: str) -> str:
28
+ if not path.endswith(".gnosion"):
29
+ path += ".gnosion"
30
+ data = brain.to_dict()
31
+ manifest = {
32
+ "magic": MAGIC, "format": FORMAT, "gnosion_version": brain.VERSION,
33
+ "dim": brain.dim, "created_at": int(time.time()),
34
+ "domains": [{"name": d["name"], "head": d["head"], "modality": d["modality"],
35
+ "embedder": d["embedder"],
36
+ "size": len(d["state"].get("samples", d["state"].get("entries", [])))}
37
+ for d in data["domains"]],
38
+ }
39
+ os.makedirs(os.path.dirname(os.path.abspath(path)) or ".", exist_ok=True)
40
+ tmp = path + ".tmp"
41
+ with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as z:
42
+ z.writestr("manifest.json", json.dumps(manifest, indent=2))
43
+ z.writestr("brain.json.gz", gzip.compress(json.dumps(data).encode("utf-8")))
44
+ os.replace(tmp, path)
45
+ return path
46
+
47
+
48
+ def load_brain(path: str) -> "Gnosion":
49
+ from .core import Gnosion
50
+ with zipfile.ZipFile(path, "r") as z:
51
+ names = set(z.namelist())
52
+ if "brain.json.gz" in names:
53
+ data = json.loads(gzip.decompress(z.read("brain.json.gz")).decode("utf-8"))
54
+ elif "brain.json" in names: # uncompressed fallback
55
+ data = json.loads(z.read("brain.json").decode("utf-8"))
56
+ else:
57
+ raise ValueError("not a valid .gnosion file (no brain payload)")
58
+ return Gnosion.from_dict(data)
59
+
60
+
61
+ def inspect(path: str) -> dict:
62
+ """Read just the manifest — quick peek without loading the whole brain."""
63
+ with zipfile.ZipFile(path, "r") as z:
64
+ return json.loads(z.read("manifest.json").decode("utf-8"))
gnosion/cli.py ADDED
@@ -0,0 +1,167 @@
1
+ """gnosion CLI — `gnosion <command>`.
2
+
3
+ Low-level (explicit .gnosion file):
4
+ gns inspect brain.gnosion peek at a .gnosion file
5
+ gns stats brain.gnosion full stats
6
+ gns learn brain.gnosion text "reset password" --label account
7
+ gns train brain.gnosion [--domain text]
8
+ gns predict brain.gnosion text "how to log in"
9
+ gns recall brain.gnosion design "how should we do auth"
10
+ gns new brain.gnosion create an empty brain
11
+
12
+ Universal agent memory (repo-scoped ./.gnosion/project.gnosion — for ANY coding agent):
13
+ gns note "we use JWT in an httpOnly cookie" --kind decision
14
+ gns ask "how should we do auth" [--kind decision] [--k 5]
15
+ gns brief session-start briefing
16
+ gns mem memory stats
17
+ gns mcp run the MCP server (stdio)
18
+
19
+ Files are created on first write. Zero deps.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import sys
27
+
28
+
29
+ def _load_or_new(path: str):
30
+ from .core import Gnosion
31
+ if os.path.exists(path):
32
+ return Gnosion.load(path)
33
+ return Gnosion()
34
+
35
+
36
+ def main(argv=None) -> int:
37
+ from . import __version__
38
+ p = argparse.ArgumentParser(prog="gns", description="Portable self-improving brain")
39
+ p.add_argument("--version", "-V", action="version", version=f"gns {__version__}")
40
+ sub = p.add_subparsers(dest="cmd", required=True)
41
+
42
+ def add(name, needs_domain=True, needs_text=False):
43
+ sp = sub.add_parser(name)
44
+ sp.add_argument("path")
45
+ if needs_domain:
46
+ sp.add_argument("domain", nargs="?")
47
+ if needs_text:
48
+ sp.add_argument("text")
49
+ return sp
50
+
51
+ add("new", needs_domain=False)
52
+ add("inspect", needs_domain=False)
53
+ add("stats", needs_domain=False)
54
+ lp = add("learn", needs_text=True); lp.add_argument("--label"); lp.add_argument("--value")
55
+ tp = sub.add_parser("train"); tp.add_argument("path"); tp.add_argument("--domain")
56
+ add("predict", needs_text=True)
57
+ add("recall", needs_text=True)
58
+
59
+ # ---- universal agent-memory commands (repo-scoped ./.gnosion/project.gnosion) ----
60
+ npn = sub.add_parser("note"); npn.add_argument("text")
61
+ npn.add_argument("--kind", default="fact"); npn.add_argument("--subject")
62
+ npn.add_argument("--root", default=".")
63
+ ask = sub.add_parser("ask"); ask.add_argument("query")
64
+ ask.add_argument("--kind"); ask.add_argument("--k", type=int, default=5); ask.add_argument("--root", default=".")
65
+ bpn = sub.add_parser("brief"); bpn.add_argument("--root", default=".")
66
+ mpn = sub.add_parser("mem"); mpn.add_argument("--root", default=".")
67
+ mcpn = sub.add_parser("mcp"); mcpn.add_argument("--root", default=".")
68
+ obs = sub.add_parser("observe"); obs.add_argument("text"); obs.add_argument("--root", default=".")
69
+ exp = sub.add_parser("experience"); exp.add_argument("action"); exp.add_argument("--outcome"); exp.add_argument("--root", default=".")
70
+ sk = sub.add_parser("skill"); sk.add_argument("name"); sk.add_argument("--when", required=True)
71
+ sk.add_argument("--how", required=True); sk.add_argument("--root", default=".")
72
+ skl = sub.add_parser("skills"); skl.add_argument("--root", default=".")
73
+ mp = sub.add_parser("mapping")
74
+ mp.add_argument("action", nargs="?", default="build",
75
+ help="a path to map, or: query | path | explain")
76
+ mp.add_argument("args", nargs="*")
77
+ mp.add_argument("--root", default="."); mp.add_argument("--k", type=int, default=8)
78
+ uip = sub.add_parser("ui"); uip.add_argument("path", nargs="?")
79
+ uip.add_argument("--root", default="."); uip.add_argument("--port", type=int, default=8799)
80
+ fd = sub.add_parser("feed"); fd.add_argument("target", nargs="?")
81
+ fd.add_argument("--domain", default="knowledge"); fd.add_argument("--text")
82
+ fd.add_argument("--crawl", action="store_true"); fd.add_argument("--depth", type=int, default=1)
83
+ fd.add_argument("--max", type=int, default=20); fd.add_argument("--root", default=".")
84
+ ho = sub.add_parser("handoff"); ho.add_argument("--root", default="."); ho.add_argument("-o", "--out")
85
+ ho.add_argument("--as-claude", action="store_true"); ho.add_argument("--as-agents", action="store_true")
86
+
87
+ a = p.parse_args(argv)
88
+
89
+ if a.cmd == "mapping":
90
+ from . import mapping
91
+ act, rest = a.action, a.args
92
+ if act == "query":
93
+ print(json.dumps(mapping.query(a.root, " ".join(rest), k=a.k), indent=2)); return 0
94
+ if act == "path":
95
+ if len(rest) < 2:
96
+ print("usage: gns mapping path \"A\" \"B\""); return 1
97
+ print(json.dumps(mapping.path(a.root, rest[0], rest[1]), indent=2)); return 0
98
+ if act == "explain":
99
+ print(json.dumps(mapping.explain(a.root, " ".join(rest)), indent=2)); return 0
100
+ target = a.root if act == "build" else act # `gns mapping .` or `gns mapping ../x`
101
+ print(json.dumps(mapping.build_map(target), indent=2)); return 0
102
+ if a.cmd == "ui":
103
+ from .ui import serve
104
+ serve(brain_path=a.path, root=a.root, port=a.port); return 0
105
+ if a.cmd == "feed":
106
+ from .feed import feed
107
+ print(json.dumps(feed(a.target, domain=a.domain, text=a.text, crawl=a.crawl,
108
+ depth=a.depth, max_pages=a.max, root=a.root), indent=2)); return 0
109
+ if a.cmd == "handoff":
110
+ from .handoff import handoff
111
+ r = handoff(a.root, out=a.out, as_claude=a.as_claude, as_agents=a.as_agents)
112
+ print(r["markdown"] if "markdown" in r else json.dumps(r, indent=2)); return 0
113
+
114
+ if a.cmd == "note":
115
+ from .agent import AgentMemory
116
+ print(json.dumps(AgentMemory(root=a.root).remember(a.text, kind=a.kind, subject=a.subject))); return 0
117
+ if a.cmd == "ask":
118
+ from .agent import AgentMemory
119
+ hits = AgentMemory(root=a.root).recall(a.query, kind=a.kind, k=a.k)
120
+ print(json.dumps(hits, indent=2) if hits else "no relevant memory yet"); return 0
121
+ if a.cmd == "brief":
122
+ from .agent import AgentMemory
123
+ print(AgentMemory(root=a.root).briefing()); return 0
124
+ if a.cmd == "mem":
125
+ from .agent import AgentMemory
126
+ print(json.dumps(AgentMemory(root=a.root).stats(), indent=2)); return 0
127
+ if a.cmd == "observe":
128
+ from .agent import AgentMemory
129
+ print(json.dumps(AgentMemory(root=a.root).observe(a.text))); return 0
130
+ if a.cmd == "experience":
131
+ from .agent import AgentMemory
132
+ print(json.dumps(AgentMemory(root=a.root).experience(a.action, outcome=a.outcome))); return 0
133
+ if a.cmd == "skill":
134
+ from .agent import AgentMemory
135
+ print(json.dumps(AgentMemory(root=a.root).add_skill(a.name, a.when, a.how))); return 0
136
+ if a.cmd == "skills":
137
+ from .agent import AgentMemory
138
+ print(json.dumps(AgentMemory(root=a.root).skills(), indent=2)); return 0
139
+ if a.cmd == "mcp":
140
+ from .mcp_server import serve
141
+ serve(a.root); return 0
142
+
143
+ if a.cmd == "inspect":
144
+ from .artifact import inspect
145
+ print(json.dumps(inspect(a.path), indent=2)); return 0
146
+
147
+ bx = _load_or_new(a.path)
148
+
149
+ if a.cmd == "new":
150
+ bx.export(a.path); print(f"created {a.path}"); return 0
151
+ if a.cmd == "stats":
152
+ print(json.dumps(bx.stats(), indent=2)); return 0
153
+ if a.cmd == "learn":
154
+ bx.learn(a.domain, a.text, label=a.label, value=a.value)
155
+ bx.export(a.path); print(f"learned into '{a.domain}' -> {a.path}"); return 0
156
+ if a.cmd == "train":
157
+ out = bx.train(a.domain); bx.export(a.path)
158
+ print(json.dumps(out, indent=2)); return 0
159
+ if a.cmd == "predict":
160
+ print(json.dumps(bx.predict(a.domain, a.text), indent=2)); return 0
161
+ if a.cmd == "recall":
162
+ print(json.dumps(bx.recall(a.domain, a.text), indent=2)); return 0
163
+ return 1
164
+
165
+
166
+ if __name__ == "__main__":
167
+ sys.exit(main())
gnosion/core.py ADDED
@@ -0,0 +1,223 @@
1
+ """Gnosion — one portable, self-improving brain with pluggable cognition domains.
2
+
3
+ from gnosion import Gnosion
4
+ bx = Gnosion() # 5 default cognition domains
5
+ bx.learn("vision", image_bytes, label="plumbing")
6
+ bx.learn("text", "how do I reset my password", label="account")
7
+ bx.remember("design", "auth pattern", "Use JWT in an httpOnly cookie; refresh rotates.")
8
+ bx.train("vision") # refit (champion/challenger)
9
+ bx.predict("vision", other_image_bytes) # -> {label, confidence}
10
+ bx.recall("design", "how should we do auth") # -> {value, similarity}
11
+ bx.export("company.gnosion") # one portable file
12
+ bx2 = Gnosion.load("company.gnosion") # import anywhere
13
+
14
+ Default domains (the "5 cognitive units"):
15
+ vision (classifier, image) — what kind of thing is in the picture
16
+ text (classifier, text) — intent / category of a message
17
+ design (memory, text) — system-design & structure conventions (recall)
18
+ tabular (classifier, vector) — outcomes from numeric features
19
+ memory (memory, text) — general knowledge recall (learns from 1 example)
20
+ Add your own with add_domain().
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from typing import Dict, List, Optional
25
+
26
+ from .embed import DEFAULT_DIM, make_embedder
27
+ from .heads import HEAD_TYPES, ClassifierHead, MemoryHead
28
+
29
+ __all__ = ["Gnosion"]
30
+
31
+ DEFAULT_DOMAINS = [
32
+ {"name": "vision", "head": "classifier", "modality": "image"},
33
+ {"name": "text", "head": "classifier", "modality": "text"},
34
+ {"name": "design", "head": "memory", "modality": "text"},
35
+ {"name": "tabular", "head": "classifier", "modality": "vector"},
36
+ {"name": "memory", "head": "memory", "modality": "text"},
37
+ ]
38
+
39
+
40
+ class _Domain:
41
+ def __init__(self, name, head, modality, embedder):
42
+ self.name = name
43
+ self.head = head
44
+ self.modality = modality
45
+ self.embedder = embedder
46
+
47
+
48
+ class Gnosion:
49
+ VERSION = "0.1.0" # .gnosion on-disk FORMAT version (bump only when the file layout
50
+ # changes) — NOT the package version (see gnosion.__version__)
51
+
52
+ def __init__(self, dim: int = DEFAULT_DIM, prefer_fastembed=None,
53
+ domains: Optional[List[dict]] = None):
54
+ # prefer_fastembed: None = auto (use fastembed if installed), True/False = force.
55
+ self.dim = dim
56
+ self.prefer_fastembed = prefer_fastembed
57
+ self.domains: Dict[str, _Domain] = {}
58
+ for d in (DEFAULT_DOMAINS if domains is None else domains):
59
+ self.add_domain(d["name"], d.get("head", "classifier"), d.get("modality", "text"))
60
+
61
+ # ---------------------------------------------------------------- domains
62
+ def add_domain(self, name: str, head: str = "classifier", modality: str = "text",
63
+ prefer_fastembed="__inherit__") -> None:
64
+ if head not in HEAD_TYPES:
65
+ raise ValueError(f"head must be one of {list(HEAD_TYPES)}")
66
+ pf = self.prefer_fastembed if prefer_fastembed == "__inherit__" else prefer_fastembed
67
+ emb = make_embedder(modality, self.dim, pf)
68
+ self.domains[name] = _Domain(name, HEAD_TYPES[head](), modality, emb)
69
+
70
+ def _dom(self, name: str) -> _Domain:
71
+ d = self.domains.get(name)
72
+ if d is None:
73
+ raise KeyError(f"unknown domain '{name}' (have {list(self.domains)})")
74
+ return d
75
+
76
+ def _embed(self, name: str, x) -> List[float]:
77
+ return self._dom(name).embedder.embed(x)
78
+
79
+ # ---------------------------------------------------------------- learning
80
+ def learn(self, domain: str, x, label: Optional[str] = None,
81
+ value: Optional[str] = None, key: Optional[str] = None,
82
+ meta: Optional[dict] = None) -> None:
83
+ """Teach the brain. classifier domains need `label`; memory domains store
84
+ `value` (or `label`) keyed by the input, with optional `meta`. Gets smarter
85
+ on every call."""
86
+ d = self._dom(domain)
87
+ vec = d.embedder.embed(x)
88
+ if isinstance(d.head, ClassifierHead):
89
+ if label is None:
90
+ raise ValueError(f"domain '{domain}' is a classifier — pass label=")
91
+ d.head.learn(vec, label, key=key or (str(x)[:60] if not isinstance(x, (bytes, bytearray)) else None))
92
+ else: # MemoryHead
93
+ val = value if value is not None else label
94
+ if val is None:
95
+ raise ValueError(f"domain '{domain}' is a memory — pass value=")
96
+ d.head.learn(vec, val, key=key or str(x)[:200], meta=meta)
97
+
98
+ def remember(self, domain: str, cue, value: str, key: Optional[str] = None,
99
+ meta: Optional[dict] = None) -> None:
100
+ """Ergonomic alias for teaching a memory domain: remember(cue -> value)."""
101
+ self.learn(domain, cue, value=value, key=key, meta=meta)
102
+
103
+ def absorb(self, domain: str, question: str, answer: str) -> None:
104
+ """Learn a Q->A into a memory domain (skips empty/short answers)."""
105
+ if answer and len(answer.strip()) >= 2:
106
+ self.learn(domain, question, value=answer)
107
+
108
+ def train(self, domain: Optional[str] = None) -> dict:
109
+ """Refit classifier domain(s). No-op for memory domains. Returns metrics."""
110
+ out = {}
111
+ names = [domain] if domain else list(self.domains)
112
+ for n in names:
113
+ h = self._dom(n).head
114
+ if isinstance(h, ClassifierHead):
115
+ out[n] = h.train()
116
+ return out
117
+
118
+ # ---------------------------------------------------------------- inference
119
+ def predict(self, domain: str, x) -> dict:
120
+ d = self._dom(domain)
121
+ if not isinstance(d.head, ClassifierHead):
122
+ raise ValueError(f"domain '{domain}' is a memory — use recall()")
123
+ return d.head.predict(d.embedder.embed(x))
124
+
125
+ def recall(self, domain: str, query, min_sim: Optional[float] = None) -> Optional[dict]:
126
+ d = self._dom(domain)
127
+ if not isinstance(d.head, MemoryHead):
128
+ raise ValueError(f"domain '{domain}' is a classifier — use predict()")
129
+ return d.head.recall(d.embedder.embed(query), min_sim=min_sim)
130
+
131
+ def search(self, domain: str, query, k: int = 5, min_sim: float = 0.0) -> List[dict]:
132
+ """Top-k nearest memory entries in a memory domain."""
133
+ d = self._dom(domain)
134
+ if not isinstance(d.head, MemoryHead):
135
+ raise ValueError(f"domain '{domain}' is a classifier — use predict()")
136
+ return d.head.search(d.embedder.embed(query), k=k, min_sim=min_sim)
137
+
138
+ # ---------------------------------------------------------------- graph
139
+ def graph(self, k: int = 6, min_sim: float = 0.33, max_nodes_per_domain: int = 300,
140
+ exclude=()) -> dict:
141
+ """The brain as a graph: nodes = neurons (memory entries / classifier samples),
142
+ edges = cosine similarity (top-k >= min_sim) WITHIN each domain. Feeds the UI.
143
+ `exclude` = domain names to skip (e.g. the mirror 'all' domain, or 'structure'
144
+ which has its own project view)."""
145
+ from .linalg import cosine
146
+ nodes: List[dict] = []
147
+ blocks = [] # (start_index, [vecs])
148
+ for name, d in self.domains.items():
149
+ if name in exclude:
150
+ continue
151
+ if isinstance(d.head, MemoryHead):
152
+ items = [((e.get("value") or e.get("key") or "")[:80],
153
+ int((e.get("meta") or {}).get("hits", 0)),
154
+ (e.get("meta") or {}), e["vec"]) for e in d.head.entries]
155
+ else:
156
+ items = [(str(s.get("label") or "")[:80], 0, {}, s["vec"]) for s in d.head.samples]
157
+ start = len(nodes)
158
+ for label, hits, meta, _v in items:
159
+ node = {"id": len(nodes), "domain": name, "label": label, "hits": hits}
160
+ if meta.get("path"):
161
+ node["path"] = meta["path"]
162
+ nodes.append(node)
163
+ blocks.append((start, [v for _l, _h, _m, v in items] if len(items) <= max_nodes_per_domain else []))
164
+ edges, seen = [], set()
165
+ for start, vecs in blocks:
166
+ n = len(vecs)
167
+ for a in range(n):
168
+ sims = sorted(((cosine(vecs[a], vecs[b]), b) for b in range(n) if b != a),
169
+ reverse=True)
170
+ for s, b in sims[:k]:
171
+ if s < min_sim:
172
+ break
173
+ lo, hi = sorted((start + a, start + b))
174
+ if (lo, hi) not in seen:
175
+ seen.add((lo, hi)); edges.append({"source": lo, "target": hi, "sim": round(s, 3)})
176
+ return {"nodes": nodes, "edges": edges,
177
+ "domains": [{"name": n, "count": sum(1 for x in nodes if x["domain"] == n)}
178
+ for n in self.domains]}
179
+
180
+ # ---------------------------------------------------------------- introspection
181
+ def stats(self) -> dict:
182
+ out = {"version": self.VERSION, "dim": self.dim, "domains": {}}
183
+ for n, d in self.domains.items():
184
+ if isinstance(d.head, ClassifierHead):
185
+ out["domains"][n] = {"type": "classifier", "modality": d.modality,
186
+ "samples": len(d.head.samples), "metrics": d.head.metrics}
187
+ else:
188
+ out["domains"][n] = {"type": "memory", "modality": d.modality,
189
+ "entries": len(d.head.entries)}
190
+ return out
191
+
192
+ # ---------------------------------------------------------------- (de)serialize
193
+ def to_dict(self) -> dict:
194
+ return {
195
+ "gnosion_version": self.VERSION, "dim": self.dim,
196
+ "prefer_fastembed": self.prefer_fastembed,
197
+ "domains": [{"name": n, "modality": d.modality,
198
+ "head": d.head.kind, "embedder": d.embedder.kind,
199
+ "state": d.head.to_dict()} for n, d in self.domains.items()],
200
+ }
201
+
202
+ @classmethod
203
+ def from_dict(cls, data: dict) -> "Gnosion":
204
+ bx = cls(dim=data.get("dim", DEFAULT_DIM),
205
+ prefer_fastembed=data.get("prefer_fastembed", False), domains=[])
206
+ for d in data.get("domains", []):
207
+ # rebuild the SAME embedder family the brain was built with, so a
208
+ # fastembed-trained brain reloads with fastembed (dims stay consistent)
209
+ pf = str(d.get("embedder", "")).startswith("fastembed")
210
+ bx.add_domain(d["name"], d.get("head", "classifier"), d.get("modality", "text"),
211
+ prefer_fastembed=pf)
212
+ bx.domains[d["name"]].head = HEAD_TYPES[d.get("head", "classifier")].from_dict(d["state"])
213
+ return bx
214
+
215
+ # ---------------------------------------------------------------- portability
216
+ def export(self, path: str) -> str:
217
+ from .artifact import export_brain
218
+ return export_brain(self, path)
219
+
220
+ @classmethod
221
+ def load(cls, path: str) -> "Gnosion":
222
+ from .artifact import load_brain
223
+ return load_brain(path)