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 +14 -0
- gnosion/agent.py +190 -0
- gnosion/artifact.py +64 -0
- gnosion/cli.py +167 -0
- gnosion/core.py +223 -0
- gnosion/embed.py +167 -0
- gnosion/feed.py +201 -0
- gnosion/handoff.py +121 -0
- gnosion/heads.py +176 -0
- gnosion/linalg.py +69 -0
- gnosion/mapping.py +411 -0
- gnosion/mcp_server.py +157 -0
- gnosion/ml.py +66 -0
- gnosion/registry.py +57 -0
- gnosion/ui.py +562 -0
- gnosion-0.2.0.dist-info/METADATA +444 -0
- gnosion-0.2.0.dist-info/RECORD +21 -0
- gnosion-0.2.0.dist-info/WHEEL +5 -0
- gnosion-0.2.0.dist-info/entry_points.txt +2 -0
- gnosion-0.2.0.dist-info/licenses/LICENSE +21 -0
- gnosion-0.2.0.dist-info/top_level.txt +1 -0
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)
|