#!/usr/bin/env python3 """gary-neuron-chat v3: a tiny brain that LEARNS FROM USE, in pure numpy. CORTEX 656K-param dialogue GPT (gary-4-petite fine-tuned on SODA) -- speaks. HIPPOCAMPUS two memory systems: * in-window plastic read (meta-trained attention; v2) for facts still in the 128-token context, and * a PERSISTENT EPISODIC STORE (v3): declarative facts are encoded at the moment you say them (surprise picks the value word), saved in brain.npz, and retrieved by cue pattern-completion -- so recall survives the window sliding AND program restarts. PARIETAL arithmetic routed to gary-neuron (the 26K-param async-NCA adder sibling) -- a trained net, not a calculator. SLEEP replays buffered conversations mixed with base corpus -> the cortex consolidates without forgetting. brain.npz = cortex weights + episodic store + replay buffer + age. It evolves. Usage: python brain.py [chat|demo|sleep]""" import os, sys, re, json, numpy as np import gpt_numpy as G from tokenizers import ByteLevelBPETokenizer D=os.path.dirname(os.path.abspath(__file__)) CFG=dict(E=96,H=4,L=8,BLK=192); EOT=0 tok=ByteLevelBPETokenizer(f"{D}/petite_vocab.json", f"{D}/petite_merges.txt") def _f(x): return float(np.ravel(x)[0]) # optional parietal module: gary-neuron adder (sibling repo) NEURON=None for cand in (os.path.join(D,"..","gary-neuron"), os.path.join(D,"gary-neuron")): if os.path.exists(os.path.join(cand,"solve.py")): sys.path.insert(0,cand) try: import solve as _gn; NEURON=_gn except Exception: NEURON=None break # ---------------- persistence ---------------- def load_brain(path=f"{D}/brain.npz"): if os.path.exists(path): z=np.load(path,allow_pickle=True) P={k[2:]:z[k].astype(np.float32) for k in z.files if k.startswith("P/")} Hm={k[3:]:z[k] for k in z.files if k.startswith("hm/")} buf=list(z["buffer"]) if "buffer" in z.files else [] epis=list(z["epis"]) if "epis" in z.files else [] age=int(z["age"]) if "age" in z.files else 0 else: zc=np.load(f"{D}/cortex.npz",allow_pickle=True); P={k[2:]:zc[k].astype(np.float32) for k in zc.files if k.startswith("P/")} zh=np.load(f"{D}/hippo.npz"); Hm={k:zh[k] for k in zh.files}; buf=[]; epis=[]; age=0 return P,Hm,buf,epis,age def save_brain(P,Hm,buf,epis,age,path=f"{D}/brain.npz"): d={"P/"+k:v for k,v in P.items()}; d.update({"hm/"+k:np.atleast_1d(v) for k,v in Hm.items()}) d["buffer"]=np.array(buf[-400:],dtype=object); d["epis"]=np.array(epis[-500:],dtype=object) d["age"]=age; np.savez(path,**d) def cortex(P, ids): x=np.array([ids[-CFG["BLK"]:]]); logits,cache=G.forward(P,x,CFG); return cache["xf"][0], logits[0] # ---------------- text utils ---------------- STOP={"what","is","my","the","a","an","you","your","i","me","do","did","does","how", "was","were","it","to","of","and","or","in","on","at","that","this","s","u","g", "who","whats","tell","about","im","ive","id","ill","am","are","be","will","wont","cant","dont", "yes","no","really","have","has","had","its","these","those","there","here", "remember","recall","know","knew","think","guess","again","still","any","some","my","mine", "get","got","getting","go","going","want","wanted","would","could","should","can","please", "me","us","we","they","them","he","she","him","her","his","hers","their","our","ours"} def _w1(w): w=w.lower().strip("?.!,;:'’\"><)([]}{") if w.endswith("'s") or w.endswith("’s"): w=w[:-2] return w def _words(s): return {_w1(w) for w in s.split()} - {""} def _content(s): return _words(s)-STOP IRR={"drank":"drink","ate":"eat","went":"go","goes":"go","saw":"see","met":"meet","took":"take", "bought":"buy","made":"make","ran":"run","drove":"drive","wrote":"write","slept":"sleep", "gave":"give","told":"tell","said":"say","felt":"feel","day":"today","days":"today","old":"age","aged":"age", "work":"job","working":"job","employed":"job","occupation":"job","profession":"job","career":"job","kids":"kid","children":"kid", "hometown":"city","town":"city"} def _stem(ws): ws={IRR.get(w,w) for w in ws} ws={w[:-1] if len(w)>=4 and w.endswith("s") and not w.endswith("ss") else w for w in ws} return {w[:4] if len(w)>=4 else w for w in ws} _SVS=None def _svs(): global _SVS if _SVS is None: _SVS=_stem(STOPVAL) return _SVS REL={"dog","cat","pet","bird","fish","sister","brother","mom","dad","mother","father","wife", "husband","son","daughter","grandma","grandpa","aunt","uncle","cousin", "car","truck","bike","cats","dogs"} _RS=None def _rels(): global _RS if _RS is None: _RS=_stem(REL) return _RS STOPVAL={"had","has","have","having","like","likes","liked","want","wants","wanted","went", "going","goes","got","get","gets","day","days","week","thing","things","something", "anything","nothing","everything","lot","bit","time","times","really","very","name", "named","names","favorite","favourite","color","colour","food","dog","cat","hello", "hi","hey","thanks","thank","okay","yes","lol","bye","good","great","nice","long","suppose","supposed","gonna","wanna","kinda","sorta","maybe","probably","definitely","oh","ya","yeah","yep","nah","hmm","right","sure","fine","cool","wow","oops","used","use","using","still","now","for","with","from","into","onto","over","under","after","before","out","off","down","up","around","through","there","here","then","than","while","because","but","not","no","so","just","too","also","still","even","back","well","if","as","by","be","been","being","actually","now","never","always","sometimes","today","tomorrow","yesterday","tonight","sister","brother","mom","dad","mother","father","wife","husband","son","daughter","grandma","grandpa","aunt","uncle","cousin","friend","boss","live","lives","lived","drive","drives","new","anyway","hows","heres","theres","lets","gotta","mostly","honestly","basically","literally"} _WSEP={".",",","!","?",";",":","'","'s","\"","(",")","-","\n"," "} def _continues_word(t): d=tok.decode([int(t)]) if not d or d[0].isspace() or d in _WSEP: return False return True # alnum OR a UTF-8 continuation byte (accents: 'Tomás' = Tom + bytes + s) # ---------------- episodic store (v3) ---------------- ENT=re.compile(r"\b(dog|cat|puppy|kitten|pet|bird|fish|son|daughter|sister|brother|wife|husband|friend|car|truck|bike|boat|house|cat)\b",re.I) CORE=re.compile(r"^\s*(her|his|its|their|the)\s+name\s+is\s+",re.I) def encode_episode(P, u, entity=None): """At the moment a declarative fact is stated, find its VALUE: the most surprising content word under the cortex (surprise writes to memory). Returns dict or None.""" ma=AGEIN.match(u.strip()) if ma: return {"t":u,"v":ma.group(2),"c":[ma.group(2)],"s":sorted(_stem({"age"})|{ma.group(2)}),"h":"age","self":True} mi=INTRO.match(u.strip()) if mi: nm=mi.group(2).strip("?.!,'") if nm.split()[0].lower() not in STOP and nm.split()[0].lower() not in ADV: return {"t":u,"v":nm,"c":[nm],"s":sorted(_stem({"name"})|{w.lower() for w in nm.split()}),"h":"name","self":True} cont=_content(u) if len(cont)<2 and not any(w.isdigit() for w in cont): return None selfish=bool({"my","i","im","mine"}&_words(u)) and not bool({"her","his","its","their","your"}&_words(u)) inject=set() if entity and CORE.match(u.strip()): inject={entity.lower()} # "her name is X" -> bind to recent entity ids=tok.encode(f"U: {u}\n").ids if len(ids)<3: return None x=np.array([ids]); lg,_=G.forward(P,x,CFG); lg=lg[0].astype(np.float64) lp=lg-lg.max(1,keepdims=True); lp=lp-np.log(np.exp(lp).sum(1,keepdims=True)) T=len(ids); nll=np.zeros(T) for t in range(T-1): nll[t+1]=-lp[t,ids[t+1]] cands=[] t=0 while t correct multibyte wl=w.strip().strip("?.!,'").lower() if wl and wl not in STOP and wl not in STOPVAL and any(c.isalnum() for c in wl) and (len(wl)>=3 or wl.isdigit()): cands.append((w.strip().strip("?.!,'"), sur+0.15*t)) t=j else: t+=1 if not cands: return None cands.sort(key=lambda x:-x[1]) self_name=selfish and ("name" in _stem(cont)) head="" for w in u.split(): w1=_w1(w) if w1 and w1 not in STOP and w1 not in ADV: head=next(iter(_stem({w1}))); break return {"t":u,"v":cands[0][0],"c":[w for w,_ in cands[:4]],"s":sorted(_stem(cont)|_stem(inject)),"h":(next(iter(_stem(inject))) if inject else head),"self":self_name and not inject} PETS=_stem({"dog","cat","pet","bird","fish","puppy","kitten","hamster"}) def recall_episode(epis, cue_words): """Cue pattern-completion over the persistent store: best stem overlap, newest wins.""" cue=_stem(cue_words) if not cue: return None pet_query=bool(cue&_stem({"pet","animal"})) name_query=("name" in cue) and not (cue&_rels()) best=None; bk=(-1,-1,-1,0,-1) for i,e in enumerate(epis): es=set(e["s"]); ov=len(cue&es) if ov<1 and pet_query and (es&PETS): ov=1 # "pet/animal" cue matches a specific-pet episode if ov<1: continue if (es&_rels())-cue and not (pet_query and (es&PETS)): continue # relation-bound, but pet-hypernym exempt if (cue&_rels())-es and not (pet_query and (es&PETS)): continue # cue's relation absent (pet-hypernym exempt) selfp=1 if (name_query and e.get("self",False)) else 0 sc=(ov,selfp,1 if e.get("h","") in cue else 0,-len(es-cue-_svs()),i) # overlap, self-name, subject, specificity, recency if sc>bk: bk=sc; best=e if best is None: return None bes=set(best["s"]); cov=len(cue&bes)/max(1,len(cue)) if pet_query and (bes&PETS): cov=1.0 for w in best.get("c",[best["v"]]): if not (_stem({w.lower()})&cue): return best["t"],w,(bk[0],bk[3]),cov return best["t"],best["v"],(bk[0],bk[3]),cov # ---------------- in-window plastic read (v2, unchanged) ---------------- GATE_QN=30.0 def hippo_read(Hm, xf, logits, win_ids, win_roles, win_tids, cue_words): if not cue_words: return False,-1 Wq=Hm["Wq"].astype(np.float64); bN=_f(Hm["bN"]); T=xf.shape[0] ids=np.asarray(win_ids[-T:]); roles=np.asarray(win_roles[-T:]); tids=np.asarray(win_tids[-T:]) nxtv=np.concatenate([ids[1:],[0]]) cue=_stem(cue_words) cand={} for tu in sorted(set(int(x) for x in tids if x>=0)): txt=tok.decode([int(i) for i in ids[tids==tu]]) es=_stem(_content(txt)); ov=len(es&cue) if "?" not in txt and ov>0 and not ((es&_rels())-cue) and not ((cue&_rels())-es): cand[tu]=ov if not cand: return False,-1 mo=max(cand.values()); elig=[tu for tu,ov in cand.items() if ov==mo] ok=np.zeros(T,bool) for t in range(T-1): if roles[t]==0 and int(tids[t]) in elig: st=tok.decode([int(ids[t])]).strip() if st and any(c.isalnum() for c in st): ok[t]=True if not ok.any(): return False,-1 Q=xf.astype(np.float64)@Wq; qa=Q[-1]; qn=np.linalg.norm(qa) if qnbv2: bv2=sur; best2=t elif sur>bv: bv=sur; best=t if best<0: best=best2 # all candidates matched the cue -> echo the fact word if best<0: return False,-1 return True,best # ---------------- decoder ---------------- COPY_MAX=3 def gen_reply(P,Hm,ids,roles,tids,max_new=26,temp=0.72,top_k=24,rep_pen=1.28, use_hippo=True,allow_fire=True,seed=None,prev_reply_ids=(),cue_words=frozenset(), forced_ids=()): rng=np.random.default_rng(seed); out=[]; tri=set() for a,b,c in zip(prev_reply_ids,prev_reply_ids[1:],prev_reply_ids[2:]): tri.add((a,b,c)) ptr=-1; copied=0; fired=bool(forced_ids); post_fire=len(forced_ids)-1 if forced_ids else -1 for step in range(max_new): win=ids[-CFG["BLK"]:]; wroles=roles[-CFG["BLK"]:]; wtids=tids[-CFG["BLK"]:]; off=len(ids)-len(win) xf,logits=cortex(P,win); ll=logits[-1].astype(np.float64) forced=-1 if step=0 and copied crisp stop if use_hippo and allow_fire and not fired and forced<0: f,vs=hippo_read(Hm,xf,logits,win,wroles,wtids,cue_words) if f: forced=ids[off+vs]; ptr=off+vs; copied=0; fired=True if forced>=0: nx=forced else: recent=set(win[-64:])|set(out) for t in recent: ll[t]=ll[t]/rep_pen if ll[t]>0 else ll[t]*rep_pen for t in set(out): ll[t]-=0.6*out.count(t) for t in set(prev_reply_ids): ll[t]-=0.5 if len(out)>=2: for c in [c for (a,b,c) in tri if a==out[-2] and b==out[-1]]: ll[c]-=1e9 if out[-1]==out[-2]: ll[out[-1]]-=1e9 if len(out)<2: ll[EOT]-=1e9 if temp<=0: nx=int(ll.argmax()) else: k=np.argpartition(-ll,top_k)[:top_k]; lk=ll[k]/temp pk=np.exp(lk-lk.max()); pk/=pk.sum(); nx=int(k[rng.choice(len(k),p=pk)]) if nx==EOT and len(out)>=2: break ids=ids+[nx]; roles=roles+[1]; tids=tids+[-1]; out.append(nx) if len(out)>=3: tri.add((out[-3],out[-2],out[-1])) d=tok.decode(out) if "\n" in d: out=tok.encode(d.split("\n")[0]).ids; break if post_fire>=0 and step>=post_fire and d.rstrip()[-1:] in ".!?,": break if post_fire>=0 and step>=post_fire+4: break return tok.decode(out).strip(), out, fired # ---------------- sleep ---------------- def sleep(P, buf, base_path=f"{D}/train.bin", steps=30, lr=1.2e-4, mix=0.12): if not buf: return P, None base=np.memmap(base_path,dtype=np.uint16,mode="r"); T=CFG["BLK"] ci=[]; [ci.extend(tok.encode(t).ids+[EOT]) for t in buf]; convo=np.array(ci,dtype=np.int64) opt=G.Adam(P,lr=lr); rng=np.random.default_rng(0) for _ in range(steps): xs=[];ys=[] for _ in range(16): if rng.random()T+1: s=convo[(i:=rng.integers(0,len(convo)-T-1)):i+T+1] else: j=rng.integers(0,len(base)-T-1); s=np.asarray(base[j:j+T+1],dtype=np.int64) xs.append(s[:T]); ys.append(s[1:T+1]) loss,cache=G.forward(P,np.stack(xs),CFG,np.stack(ys)); opt.step(P,G.backward(P,CFG,cache)) return P, None # ---------------- conversation ---------------- QWORDS=("what","whats","where","who","when","how","which") MATH=re.compile(r"(\d{1,7})\s*\+\s*(\d{1,7})") ASSERT=re.compile(r"=\s*-?\d") YNWORDS=("am","is","are","do","does","did","can","could","will","would","was","were","have","has") SELFQ=re.compile(r"real\s+person|human|alive|robot|an?\s+ai\b|sentient|a\s+machine|realy\s+person",re.I) CORR=re.compile(r"^(no\b|nope\b|wrong\b|not\b|actually\b)") INTRO=re.compile(r"^\s*(i'?m|i am|this is|call me|my name'?s?|the name is|name is)\s+([^\W\d]\S{1,24}(?:\s+[A-Z]\S{1,24})?)\s*$",re.U) AGEIN=re.compile(r"^\s*(i'?m|i am)\s+(\d{1,3})(\s*(years?\s*old|yo))?\s*$",re.I) ADV={"actually","anyway","honestly","basically","literally","oh","ok","okay","well","yeah", "yep","nah","hmm","wow","oops","so","but","and","also","still","just","then","wait", "sorry","hey","um","uh","no","yes","listen","look"} class Convo: def __init__(self, epis=None): self.ids=[]; self.roles=[]; self.tids=[]; self.prev=[]; self._t=0; self._last_recall=None; self._last_entity=None self.epis=epis if epis is not None else [] self.turns={} # tid -> (role, text) def add(self,s,r): t=tok.encode(s).ids; self.ids+=t; self.roles+=[r]*len(t); self.tids+=[self._t]*len(t) self.turns[self._t]=(r,s); self._t+=1 def _window_score(self, cue): wt=set(int(x) for x in self.tids[-CFG["BLK"]:] if x>=0) cue=_stem(cue); best=None for tu in wt: r,txt=self.turns.get(tu,(1,"")) if r!=0 or "?" in txt: continue es=_stem(_content(txt)); ov=len(es&cue) if ov<1: continue if (es&_rels())-cue or (cue&_rels())-es: continue sc=(ov,-len(es-cue-_svs()),tu) if best is None or sc>best: best=sc return best def turn(self,P,Hm,u,seed=None,use_hippo=True,temp=0.72): uw=_words(u); first=u.strip().split()[0].lower().strip("?,!") if u.strip() else "" questionish=("?" in u) or (first in QWORDS) about_gary=bool({"your","you","yours"}&uw) and not ({"my","i","im","mine"}&uw) yn=(first in YNWORDS) or ("yes or no" in u.lower()) if yn: questionish=True # self-model: honest answers about what gary is if about_gary and SELFQ.search(u): r="i'm a tiny numpy brain -- 1.1M parameters, a plastic memory, and an adder. not a person, but i do remember you." self.add(f"U: {u}\n",0); self.add(f"G: {r}\n",1); self.prev=tok.encode(r).ids return r # correction binding: "no, it is X" right after a recall answer rebinds the value if self._last_recall and CORR.match(u.strip().lower()): corr=re.sub(r"^((no|nope|nah|wrong|not|actually|wait|um|uh|sorry|hmm|its|it's)[\s,]+)+","",u.strip(),flags=re.I) ccont=_content(corr); cs,_old=self._last_recall has_subj=bool(_stem(ccont)&cs) or bool(_stem(ccont)&_rels()) if ccont and has_subj: # "no, my name is X" -> normal fact encode (rebinds subject) e=encode_episode(P,corr) if e: e["self"]=True; self.epis.append(e); r=f"got it -- {e['v']}." else: r="oh -- noted." else: # "no its Luna" / bare "no" -> reuse last question's subject cand=[w for w in corr.split() if _w1(w) and _w1(w) not in STOP and _w1(w) not in STOPVAL] if cand: v=cand[-1].strip("?.!,") self.epis.append({"t":u,"v":v,"c":[v],"s":sorted(cs|_stem({_w1(v)})),"h":next(iter(cs)) if cs else "","self":True}) r=f"got it -- {v}." else: r="oh -- what is it then?" self.add(f"U: {u}\n",0); self.add(f"G: {r}\n",1); self.prev=tok.encode(r).ids; self._last_recall=None return r # parietal: arithmetic -> gary-neuron (a trained net, not a calculator) um=re.sub(r"\bplus\b","+",u.lower()) if NEURON is not None and (re.search(r"\d\s*(-|minus)\s*\d", um)): self.add(f"U: {u}\n",0); r="i only know addition so far -- my adder neurons can't subtract yet!" self.add(f"G: {r}\n",1); self.prev=tok.encode(r).ids; return r m=MATH.search(um) if m and NEURON is not None and not ASSERT.search(um): a,b=int(m.group(1)),int(m.group(2)) if a+b<10**7: ans=int(NEURON.solve(a,b)) r=f"{a} + {b} = {ans}" else: r="that one's too big for my seven-digit adder!" self.add(f"U: {u}\n",0); self.add(f"G: {r}\n",1); self.prev=tok.encode(r).ids return r self.add(f"U: {u}\n",0); self.add("G:",1) allow=questionish and not about_gary if allow and use_hippo: ws=self._window_score(_content(u)) hit=recall_episode(self.epis,_content(u)) if yn and _content(u): # yes/no question -> coverage logic if hit and hit[3]>=0.99: r="yes." elif hit: r=f"hmm -- what i remember is: {hit[0]}" else: r="not that i know." self._last_recall=(_stem(_content(u)),hit[1] if hit else "") self.add(f"U: {u}\n",0); self.add(f"G: {r}\n",1); self.prev=tok.encode(r).ids return r if hit and (ws is None or hit[2]>=ws[:2]): # store >= window -> crisp store answer _,val,_,_=hit; r=f"{val}." t=tok.encode(f" {r}\n").ids self.ids+=t; self.roles+=[1]*len(t); self.tids+=[-1]*len(t) self.prev=t[:-1]; self._last_recall=(_stem(_content(u)),val) return r txt,out,fired=gen_reply(P,Hm,self.ids,self.roles,self.tids,use_hippo=use_hippo, allow_fire=allow,seed=seed,temp=temp, prev_reply_ids=tuple(self.prev),cue_words=_content(u),forced_ids=()) self.ids+=out+tok.encode("\n").ids; self.roles+=[1]*(len(out)+1); self.tids+=[-1]*(len(out)+1) self.prev=out if "?" not in u and not m: # declarative -> write to episodic store e=encode_episode(P,u,entity=self._last_entity) if e: self.epis.append(e) me=ENT.search(u) # remember most-recent concrete entity for coreference if me and "?" not in u: self._last_entity=me.group(1).lower() return txt def chat(): P,Hm,buf,epis,age=load_brain() n="with" if NEURON else "without" print(f"gary-neuron-chat v3 (age {age}, {len(epis)} memories, {n} math). Tell me things; ask me later -- even next session. '/sleep' consolidates, '/exit' leaves.") cv=Convo(epis); sess=[] while True: try: u=input("\nyou: ").strip() except (EOFError,KeyboardInterrupt): break if u=="/exit": break if u=="/sleep": P,_=sleep(P,sess); age+=1; save_brain(P,Hm,buf+sess,cv.epis,age); sess=[]; print("...slept; cortex consolidated."); continue if not u: continue r=cv.turn(P,Hm,u); print("gary:",r); sess.append(f"U: {u}\nG: {r}") save_brain(P,Hm,buf+sess,cv.epis,age); print(f"brain saved ({len(cv.epis)} memories).") if __name__=="__main__": cmd=sys.argv[1] if len(sys.argv)>1 else "chat" if cmd=="chat": chat() elif cmd=="demo": P,Hm,buf,epis,age=load_brain(); cv=Convo() script=["hello","my name is gary","what is 17 + 25?","i like to read books","how is the weather today?", "did you sleep well?","i had a long day at work","my favorite color is blue","tell me something", "what is my name?","what is my favorite color?"] for u in script: print(f"you : {u}\ngary: {cv.turn(P,Hm,u,seed=3)}") print(f"[episodic store: {[(e['t'],e['v']) for e in cv.epis]}]") elif cmd=="sleep": P,Hm,buf,epis,age=load_brain(); P,_=sleep(P,buf); age+=1; save_brain(P,Hm,buf,epis,age); print(f"slept. age {age}.")