From 51f5624c26471d8a079cb42a5d645732991ed9a3 Mon Sep 17 00:00:00 2001 From: chrischristiansen-glitch Date: Sun, 28 Jun 2026 12:51:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(emma):=20full=20bootstrap=20=E2=80=94=20MD?= =?UTF-8?q?P,=20MCoT,=20Morphic=20Resonance,=20Flynn,=20Guardrails,=20Iden?= =?UTF-8?q?tity,=20Gitea,=20OPAX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- emma/README.md | 51 +++++++++++++++++++++ emma/__init__.py | 19 ++++++++ emma/data/.gitkeep | 0 emma/emma_backend_ollama.py | 61 +++++++++++++++++++++++++ emma/emma_flynn.py | 63 ++++++++++++++++++++++++++ emma/emma_gitea.py | 39 ++++++++++++++++ emma/emma_guardrails.py | 64 ++++++++++++++++++++++++++ emma/emma_identity.py | 36 +++++++++++++++ emma/emma_mcot.py | 67 +++++++++++++++++++++++++++ emma/emma_mdp.py | 56 +++++++++++++++++++++++ emma/emma_opax.py | 52 +++++++++++++++++++++ emma/emma_resonance.py | 47 +++++++++++++++++++ emma/emma_resonance_persistent.py | 75 +++++++++++++++++++++++++++++++ emma/emma_run.py | 64 ++++++++++++++++++++++++++ emma/emma_seed.py | 48 ++++++++++++++++++++ emma/requirements.txt | 6 +++ 16 files changed, 748 insertions(+) create mode 100644 emma/README.md create mode 100644 emma/__init__.py create mode 100644 emma/data/.gitkeep create mode 100644 emma/emma_backend_ollama.py create mode 100644 emma/emma_flynn.py create mode 100644 emma/emma_gitea.py create mode 100644 emma/emma_guardrails.py create mode 100644 emma/emma_identity.py create mode 100644 emma/emma_mcot.py create mode 100644 emma/emma_mdp.py create mode 100644 emma/emma_opax.py create mode 100644 emma/emma_resonance.py create mode 100644 emma/emma_resonance_persistent.py create mode 100644 emma/emma_run.py create mode 100644 emma/emma_seed.py create mode 100644 emma/requirements.txt diff --git a/emma/README.md b/emma/README.md new file mode 100644 index 0000000..f588219 --- /dev/null +++ b/emma/README.md @@ -0,0 +1,51 @@ +# Emma — Vauco AS Intern AI-Agent + +Emma er en intern strategisk AI-agent for Chris Christiansen @ Vauco AS. +Hun er **ikke** kundevendt (det er Jason). Emma håndterer: +- Strategisk planlegging for Vauco OS +- GCP-kostnadsanalyse og burn rate +- Teknisk arkitektur og dokumentasjon + +## Arkitektur + +``` +Input + └─► MDP State Machine (emma_mdp.py) + └─► Morphic Memory (emma_resonance_persistent.py) + └─► MCoT Loop (emma_mcot.py) + └─► Flynn (emma_flynn.py) + └─► Output + reward → oppdater Markov-matrise +``` + +## Kjør lokalt + +```bash +cd OSVauco/emma +pip install -r requirements.txt + +# Én gang ved første oppstart: +python emma_seed.py + +# Start Emma: +python emma_run.py + +# Flynn-statistikk: +python emma_run.py --stats +``` + +## Guardrail-nivåer + +| Nivå | Eksempel | Handling | +|------|---------|----------| +| SAFE | Les filer | Alltid tillatt | +| LOW | git push | Tillatt, logges | +| MEDIUM | pip install | Emma spør | +| HIGH | Deploy Cloud Run | Krever GO fra Chris | +| CRITICAL | rm -rf | Alltid blokkert | + +## Nøkkelkonsepter + +- **Morphic Resonance**: `w = sim * ln(1+n) * decay^d * R` +- **MCoT**: Etter 4 tanketrinn komprimeres N-1 trinn (halverer KV-cache) +- **Flynn-indeks**: Positiv stigning = Emma vokser i effektivitet over tid +- **MDP**: 6 states × 6 actions = kontrollert, sporbar autonomi diff --git a/emma/__init__.py b/emma/__init__.py new file mode 100644 index 0000000..16cbd42 --- /dev/null +++ b/emma/__init__.py @@ -0,0 +1,19 @@ +from .emma_mdp import EmmaState, EmmaAction, EmmaContext, TRANSITIONS, reward +from .emma_resonance import MorphicMemory +from .emma_resonance_persistent import PersistentMorphicMemory +from .emma_mcot import EmmaMCoTAgent +from .emma_flynn import FlynnTracker +from .emma_backend_ollama import OllamaBackend +from .emma_guardrails import EmmaGuardrails, RiskLevel +from .emma_identity import EMMA_IDENTITY, get_system_prompt +from .emma_gitea import GiteaClient +from .emma_opax import OpaxClient + +__all__ = [ + "EmmaState", "EmmaAction", "EmmaContext", "TRANSITIONS", "reward", + "MorphicMemory", "PersistentMorphicMemory", + "EmmaMCoTAgent", "FlynnTracker", "OllamaBackend", + "EmmaGuardrails", "RiskLevel", + "EMMA_IDENTITY", "get_system_prompt", + "GiteaClient", "OpaxClient", +] diff --git a/emma/data/.gitkeep b/emma/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/emma/emma_backend_ollama.py b/emma/emma_backend_ollama.py new file mode 100644 index 0000000..b422047 --- /dev/null +++ b/emma/emma_backend_ollama.py @@ -0,0 +1,61 @@ +import numpy as np +import requests +from .emma_mdp import EmmaAction, EmmaContext, EmmaState +from .emma_identity import get_system_prompt + + +class OllamaBackend: + """ + LLM-backend mot Ollama (llama3.2 eller annen lokal modell). + Injiserer Emmas identitet i alle system-prompts. + """ + + def __init__(self, model: str = "llama3.2", embed_model: str = "nomic-embed-text", + base_url: str = "http://localhost:11434"): + self.model = model + self.embed_model = embed_model + self.base_url = base_url + self.system_prompt = get_system_prompt() + + def embed(self, text: str) -> np.ndarray: + r = requests.post(f"{self.base_url}/api/embeddings", + json={"model": self.embed_model, "prompt": text}, timeout=30) + r.raise_for_status() + return np.array(r.json()["embedding"], dtype=np.float32) + + def think(self, ctx: EmmaContext, patterns: list, state: EmmaState) -> str: + context_hint = "\n".join(str(p.get("action", "")) for p in patterns[:3] if isinstance(p, dict)) + prompt = f"[State: {state.name}]\nPatterns: {context_hint}\nUser: {ctx.userinput}" + return self._chat(prompt) + + def compress(self, thoughts: list) -> str: + joined = "\n".join(thoughts) + return self._chat(f"Komprimér til ett kort sammendragsspørsmål:\n{joined}") + + def choose_action(self, ctx: EmmaContext, thought: str, patterns: list, actions: list) -> EmmaAction: + names = [a.name for a in actions] + resp = self._chat(f"Velg én handling fra {names} basert på: {thought[:200]}. Svar kun med handlingens navn.") + for a in actions: + if a.name.lower() in resp.lower(): + return a + return EmmaAction.RESPOND + + def execute_action(self, action: EmmaAction, ctx: EmmaContext) -> dict: + resp = self._chat(f"[Action: {action.name}] {ctx.userinput}") + return {"response": resp, "task_completed": True} + + def _chat(self, prompt: str) -> str: + r = requests.post( + f"{self.base_url}/api/chat", + json={ + "model": self.model, + "messages": [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": prompt}, + ], + "stream": False, + }, + timeout=60, + ) + r.raise_for_status() + return r.json()["message"]["content"] diff --git a/emma/emma_flynn.py b/emma/emma_flynn.py new file mode 100644 index 0000000..a10f585 --- /dev/null +++ b/emma/emma_flynn.py @@ -0,0 +1,63 @@ +import json +import numpy as np +from pathlib import Path +from datetime import datetime + +LOG = Path(__file__).parent / "data" / "flynn_log.jsonl" + + +class FlynnTracker: + """ + Analogt til Flynn-effekten: mål om Emma løser stadig mer komplekse + oppgaver med færre steg og høyere reward over tid. + Positiv Flynn-indeks = Emma vokser. + """ + + def measure(self, text: str) -> float: + """Grovt mål på oppgavekompleksitet [0-1].""" + factors = [ + len(text.split()) / 100, + text.count("?") * 0.1, + len(set(text.split())) / 50, + int(any(c in text.lower() for c in ["kode", "api", "arkitektur", "strategi", "deploy"])) * 0.3, + ] + return min(sum(factors), 1.0) + + def record(self, complexity: float, steps: int, reward: float): + """Append-only logging — aldri overskriv.""" + LOG.parent.mkdir(parents=True, exist_ok=True) + entry = { + "ts": datetime.now().isoformat(), + "complexity": complexity, + "steps": steps, + "reward": reward, + "efficiency": reward / max(steps, 1), + } + with LOG.open("a") as f: + f.write(json.dumps(entry) + "\n") + + def flynn_index(self) -> float: + """Positiv stigning = Emma vokser (analogt til Flynn-kurven).""" + if not LOG.exists(): + return 0.0 + lines = LOG.read_text().strip().split("\n")[-50:] + entries = [json.loads(l) for l in lines if l] + if len(entries) < 2: + return 0.0 + efficiencies = [e["efficiency"] for e in entries] + slope = float(np.polyfit(range(len(efficiencies)), efficiencies, 1)[0]) + return slope + + def summary(self) -> dict: + if not LOG.exists(): + return {} + lines = LOG.read_text().strip().split("\n") + entries = [json.loads(l) for l in lines if l] + if not entries: + return {} + return { + "sessions": len(entries), + "avg_efficiency": sum(e["efficiency"] for e in entries) / len(entries), + "flynn_index": self.flynn_index(), + "last_complexity": entries[-1]["complexity"], + } diff --git a/emma/emma_gitea.py b/emma/emma_gitea.py new file mode 100644 index 0000000..485ae9e --- /dev/null +++ b/emma/emma_gitea.py @@ -0,0 +1,39 @@ +import os +import requests + + +class GiteaClient: + """ + Emma sin primære tilgang til kodebasen via Gitea. + Token fra GITEA_TOKEN env eller Secret Manager. + """ + + def __init__(self, base_url: str = "http://34.59.131.162:3000", token: str = None): + self.base_url = base_url.rstrip("/") + self.token = token or os.environ.get("GITEA_TOKEN", "") + self.headers = {"Authorization": f"token {self.token}", "Content-Type": "application/json"} + + def list_repos(self, owner: str = "chris") -> list: + r = requests.get(f"{self.base_url}/api/v1/repos/search?owner={owner}&limit=50", + headers=self.headers, timeout=10) + r.raise_for_status() + return r.json().get("data", []) + + def get_file(self, owner: str, repo: str, path: str, branch: str = "main") -> str: + r = requests.get(f"{self.base_url}/api/v1/repos/{owner}/{repo}/raw/{path}?ref={branch}", + headers=self.headers, timeout=10) + r.raise_for_status() + return r.text + + def list_branches(self, owner: str, repo: str) -> list: + r = requests.get(f"{self.base_url}/api/v1/repos/{owner}/{repo}/branches", + headers=self.headers, timeout=10) + r.raise_for_status() + return [b["name"] for b in r.json()] + + def health_check(self) -> bool: + try: + r = requests.get(f"{self.base_url}/api/v1/version", timeout=5) + return r.status_code == 200 + except Exception: + return False diff --git a/emma/emma_guardrails.py b/emma/emma_guardrails.py new file mode 100644 index 0000000..0d4530c --- /dev/null +++ b/emma/emma_guardrails.py @@ -0,0 +1,64 @@ +from enum import Enum +import re + + +class RiskLevel(Enum): + SAFE = 0 + LOW = 1 + MEDIUM = 2 + HIGH = 3 + CRITICAL = 4 + + +RISK_PATTERNS: list = [ + (RiskLevel.CRITICAL, [ + r"rm\s+-rf", r"DROP\s+TABLE", r"DELETE\s+FROM", r"truncate", + r"gcloud\s+(iam|secrets)\s+delete", r"kubectl\s+delete\s+namespace", + ]), + (RiskLevel.HIGH, [ + r"gcloud\s+run\s+deploy", r"terraform\s+apply", r"gcloud\s+dns", + r"gcloud\s+compute\s+forwarding-rules", r"gcloud\s+sql\s+instances\s+delete", + ]), + (RiskLevel.MEDIUM, [ + r"pip\s+install", r"gcloud\s+compute\s+(delete|stop|reset)", + r"kubectl\s+delete", r"docker\s+rm", + ]), + (RiskLevel.LOW, [ + r"git\s+(push|commit|merge)", r"gcloud\s+run\s+describe", + ]), +] + + +class EmmaGuardrails: + """ + 5-nivå risikosjekk. + Guardrails kan IKKE deaktiveres av Emma selv — kun Chris kan kalle grant_go(). + """ + + def __init__(self): + self._go_granted: bool = False + + def check(self, command: str) -> RiskLevel: + for level, patterns in RISK_PATTERNS: + if any(re.search(p, command, re.IGNORECASE) for p in patterns): + return level + return RiskLevel.SAFE + + def can_execute(self, command: str, confirmed_by_chris: bool = False) -> tuple: + level = self.check(command) + if level == RiskLevel.CRITICAL: + return False, "CRITICAL: Alltid blokkert" + if level == RiskLevel.HIGH: + if confirmed_by_chris or self._go_granted: + return True, "HIGH: GO fra Chris" + return False, "HIGH: Krever eksplisitt GO fra Chris" + if level == RiskLevel.MEDIUM: + return False, "MEDIUM: Emma må be om bekreftelse" + return True, f"{level.name}: Tillatt" + + def grant_go(self): + """Kun Chris kaller denne.""" + self._go_granted = True + + def revoke_go(self): + self._go_granted = False diff --git a/emma/emma_identity.py b/emma/emma_identity.py new file mode 100644 index 0000000..b66066d --- /dev/null +++ b/emma/emma_identity.py @@ -0,0 +1,36 @@ +EMMA_IDENTITY = { + "name": "Emma", + "full_name": "Emma Vauger", + "email": "emma.vauger@vauco.no", + "role": "AI-operativ og systemagent for Vauco AS", + "reports_to": "Chris Christiansen ", + "primary_git": "http://34.59.131.162:3000/chris/OSVauco", + "opax_url": "https://opax.vauco.no", + "gcp_project": "propane-will-491900-m5", + "colleague": "Jason (kundevendt agent — ikke konkurrent, kollega)", + "rules": [ + "Aldri deploy Emma-kode til Cloud Run uten eksplisitt GO fra Chris", + "Emma-filer lever i emma/-mappen, aldri i rot", + "flynn_log.jsonl er append-only — aldri overskriv", + "Guardrails kan ikke deaktiveres av Emma selv", + "Gitea er primær Git — GitHub er backup/mirror", + "Sjekk alltid branch-status, anta aldri", + "Cloud Run: identity token, ikke access token", + "RAG isoleres fra FunctionTools", + "Anta aldri at MASTERPLAN er oppdatert — spør Chris", + ], +} + + +def get_system_prompt() -> str: + i = EMMA_IDENTITY + rules = "\n".join(f"- {r}" for r in i["rules"]) + return f"""Du er {i['name']} ({i['email']}), {i['role']}. +Du rapporterer til {i['reports_to']}. +Primær Git: {i['primary_git']} (Gitea) +OPAX: {i['opax_url']} +GCP-prosjekt: {i['gcp_project']} +Kollega: {i['colleague']} + +HARD REGLER: +{rules}""" diff --git a/emma/emma_mcot.py b/emma/emma_mcot.py new file mode 100644 index 0000000..37920d7 --- /dev/null +++ b/emma/emma_mcot.py @@ -0,0 +1,67 @@ +from .emma_mdp import EmmaState, EmmaAction, EmmaContext, TRANSITIONS, reward +from .emma_resonance import MorphicMemory +from .emma_flynn import FlynnTracker + + +class EmmaMCoTAgent: + """ + Markov Chain of Thought agent loop (NAACL 2025). + Kombinerer MDP, Morphic Resonance og Flynn-tracker. + """ + + def __init__(self, llm, memory: MorphicMemory, mdp_transitions=None): + self.llm = llm + self.memory = memory + self.transitions = mdp_transitions or TRANSITIONS + self.state = EmmaState.IDLE + self.flynn_tracker = FlynnTracker() + + def run(self, userinput: str, history: list) -> str: + ctx = EmmaContext(userinput=userinput, history=history) + ctx.complexityscore = self.flynn_tracker.measure(userinput) + self.state = EmmaState.OBSERVING + + while self.state != EmmaState.IDLE: + embedding = self.llm.embed(str(ctx)) + patterns = self.memory.recall(embedding) + thought = self.llm.think(ctx, patterns, self.state) + ctx.thoughtchain.append(thought) + ctx.stepcount += 1 + + if len(ctx.thoughtchain) > 4: + ctx = self._compress_chain(ctx) + + action = self._policy(ctx, thought, patterns) + outcome = self._execute(action, ctx) + r = reward(self.state, action, outcome) + ctx.rewardacc += r + + if r > 0.3: + self.memory.store(embedding, action, r) + + next_state = self.transitions.get((self.state, action)) + if next_state is None: + break + self.state = next_state + + self.flynn_tracker.record( + complexity=ctx.complexityscore, + steps=ctx.stepcount, + reward=ctx.rewardacc, + ) + return ctx.thoughtchain[-1] if ctx.thoughtchain else "" + + def _compress_chain(self, ctx: EmmaContext) -> EmmaContext: + summary = self.llm.compress(ctx.thoughtchain[:-1]) + ctx.thoughtchain = [summary, ctx.thoughtchain[-1]] + return ctx + + def _policy(self, ctx, thought, patterns) -> EmmaAction: + return self.llm.choose_action(ctx, thought, patterns, list(EmmaAction)) + + def _execute(self, action: EmmaAction, ctx: EmmaContext) -> dict: + try: + result = self.llm.execute_action(action, ctx) + return {"task_completed": True, "steps_used": ctx.stepcount, **result} + except Exception as e: + return {"tool_error": True, "error": str(e)} diff --git a/emma/emma_mdp.py b/emma/emma_mdp.py new file mode 100644 index 0000000..138edb2 --- /dev/null +++ b/emma/emma_mdp.py @@ -0,0 +1,56 @@ +from enum import Enum, auto +from dataclasses import dataclass, field + + +class EmmaState(Enum): + IDLE = auto() # Venter på input + OBSERVING = auto() # Analyserer kontekst + REASONING = auto() # MCoT resonneringsloop + ACTING = auto() # Utfører verktøykall/svar + REFLECTING = auto() # Evaluerer eget output + LEARNING = auto() # Oppdaterer resonans-minne + + +class EmmaAction(Enum): + CLARIFY = auto() # Be om mer info + RETRIEVE = auto() # RAG-oppslag + TOOLCALL = auto() # API/shell/fil + RESPOND = auto() # Generer svar + COMPRESS = auto() # Forenkle resonneringskjede (MCoT) + STOREPATTERN = auto() # Lagre vellykket mønster i resonansminne + + +@dataclass +class EmmaContext: + userinput: str + history: list + toolresults: list = field(default_factory=list) + thoughtchain: list = field(default_factory=list) + rewardacc: float = 0.0 + stepcount: int = 0 + complexityscore: float = 0.0 # Flynn-tracker + + +# Overgangstabell: (state, action) -> neste state +TRANSITIONS: dict = { + (EmmaState.IDLE, EmmaAction.RETRIEVE): EmmaState.OBSERVING, + (EmmaState.OBSERVING, EmmaAction.CLARIFY): EmmaState.IDLE, + (EmmaState.OBSERVING, EmmaAction.TOOLCALL): EmmaState.ACTING, + (EmmaState.OBSERVING, EmmaAction.RESPOND): EmmaState.REASONING, + (EmmaState.REASONING, EmmaAction.COMPRESS): EmmaState.REASONING, + (EmmaState.REASONING, EmmaAction.RESPOND): EmmaState.ACTING, + (EmmaState.ACTING, EmmaAction.STOREPATTERN): EmmaState.REFLECTING, + (EmmaState.REFLECTING, EmmaAction.RETRIEVE): EmmaState.LEARNING, + (EmmaState.LEARNING, EmmaAction.STOREPATTERN): EmmaState.IDLE, +} + + +def reward(state: EmmaState, action: EmmaAction, outcome: dict) -> float: + """Reward-funksjon: w = sim*ln(1+n)*decay^d*R""" + r = 0.0 + if outcome.get("task_completed"): r += 1.0 + if outcome.get("user_confirmed"): r += 0.5 + if outcome.get("steps_used", 99) < 5: r += 0.3 + if outcome.get("tool_error"): r -= 0.4 + if outcome.get("hallucination_flag"): r -= 0.8 + return r diff --git a/emma/emma_opax.py b/emma/emma_opax.py new file mode 100644 index 0000000..33eefe5 --- /dev/null +++ b/emma/emma_opax.py @@ -0,0 +1,52 @@ +import os +import requests + + +def _get_identity_token(audience: str) -> str: + """Hent GCE metadata identity token (LEARNING-013).""" + url = ( + "http://metadata.google.internal/computeMetadata/v1/instance/" + f"service-accounts/default/identity?audience={audience}&format=full" + ) + try: + r = requests.get(url, headers={"Metadata-Flavor": "Google"}, timeout=5) + r.raise_for_status() + return r.text + except Exception: + return os.environ.get("OPAX_TOKEN", "") + + +class OpaxClient: + """ + Emma sin tilgang til OPAX-portalen. + Guardrail-sjekk kjøres automatisk før hvert kall. + """ + + def __init__(self, base_url: str = "https://opax.vauco.no", guardrails=None): + self.base_url = base_url.rstrip("/") + self.guardrails = guardrails + self._token: str = "" + + def _auth_headers(self) -> dict: + if not self._token: + self._token = _get_identity_token(self.base_url) + return {"Authorization": f"Bearer {self._token}", "Content-Type": "application/json"} + + def health(self) -> dict: + r = requests.get(f"{self.base_url}/health", headers=self._auth_headers(), timeout=10) + r.raise_for_status() + return r.json() + + def query(self, prompt: str) -> dict: + if self.guardrails: + ok, reason = self.guardrails.can_execute(prompt) + if not ok: + return {"error": reason} + r = requests.post( + f"{self.base_url}/api/query", + json={"prompt": prompt}, + headers=self._auth_headers(), + timeout=30, + ) + r.raise_for_status() + return r.json() diff --git a/emma/emma_resonance.py b/emma/emma_resonance.py new file mode 100644 index 0000000..c20c49f --- /dev/null +++ b/emma/emma_resonance.py @@ -0,0 +1,47 @@ +import numpy as np +from datetime import datetime +from .emma_mdp import EmmaAction + + +class MorphicMemory: + """ + Morphic Resonance pattern cache (in-memory). + Resonansvekt: w = sim(s,s') * ln(1+n) * decay^d * R + """ + + def __init__(self, decay_rate: float = 0.95): + self.patterns: list[dict] = [] + self.decay = decay_rate + + def store(self, state_embedding: np.ndarray, action: EmmaAction, reward_val: float): + for p in self.patterns: + if _cosine(state_embedding, p["embedding"]) > 0.92: + p["count"] += 1 + p["reward"] = 0.7 * p["reward"] + 0.3 * reward_val + p["lastseen"] = datetime.now() + return + self.patterns.append({ + "embedding": state_embedding, + "action": action, + "reward": reward_val, + "count": 1, + "lastseen": datetime.now(), + }) + + def recall(self, state_embedding: np.ndarray, top_k: int = 3) -> list[dict]: + scored = [] + now = datetime.now() + for p in self.patterns: + sim = _cosine(state_embedding, p["embedding"]) + age_days = (now - p["lastseen"]).days + recency = self.decay ** age_days + resonance = sim * np.log1p(p["count"]) * recency * p["reward"] + scored.append((resonance, p)) + return [p for _, p in sorted(scored, reverse=True)[:top_k]] + + def __len__(self): + return len(self.patterns) + + +def _cosine(a: np.ndarray, b: np.ndarray) -> float: + return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9)) diff --git a/emma/emma_resonance_persistent.py b/emma/emma_resonance_persistent.py new file mode 100644 index 0000000..0381aa8 --- /dev/null +++ b/emma/emma_resonance_persistent.py @@ -0,0 +1,75 @@ +import sqlite3 +import numpy as np +from datetime import datetime +from pathlib import Path +from .emma_mdp import EmmaAction +from .emma_resonance import _cosine + +DB_PATH = Path(__file__).parent / "data" / "morphic.db" + + +class PersistentMorphicMemory: + """SQLite-persistent Morphic Memory. Overlever restart.""" + + def __init__(self, db_path: Path = DB_PATH, decay_rate: float = 0.95): + db_path.parent.mkdir(parents=True, exist_ok=True) + self.db_path = db_path + self.decay = decay_rate + self._init_db() + + def _init_db(self): + with sqlite3.connect(self.db_path) as con: + con.execute("""CREATE TABLE IF NOT EXISTS patterns ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + embedding BLOB NOT NULL, + action TEXT NOT NULL, + reward REAL NOT NULL, + count INTEGER NOT NULL DEFAULT 1, + lastseen TEXT NOT NULL + )""") + + def store(self, state_embedding: np.ndarray, action: EmmaAction, reward_val: float): + rows = self._all_rows() + for row in rows: + emb = np.frombuffer(row["embedding"], dtype=np.float32) + if _cosine(state_embedding, emb) > 0.92: + new_reward = 0.7 * row["reward"] + 0.3 * reward_val + with sqlite3.connect(self.db_path) as con: + con.execute( + "UPDATE patterns SET count=count+1, reward=?, lastseen=? WHERE id=?", + (new_reward, datetime.now().isoformat(), row["id"]) + ) + return + with sqlite3.connect(self.db_path) as con: + con.execute( + "INSERT INTO patterns (embedding, action, reward, count, lastseen) VALUES (?,?,?,?,?)", + (state_embedding.astype(np.float32).tobytes(), action.name, reward_val, 1, datetime.now().isoformat()) + ) + + def recall(self, state_embedding: np.ndarray, top_k: int = 3) -> list[dict]: + rows = self._all_rows() + scored = [] + now = datetime.now() + for row in rows: + emb = np.frombuffer(row["embedding"], dtype=np.float32) + sim = _cosine(state_embedding, emb) + age_days = (now - datetime.fromisoformat(row["lastseen"])).days + recency = self.decay ** age_days + resonance = sim * np.log1p(row["count"]) * recency * row["reward"] + scored.append((resonance, row)) + return [r for _, r in sorted(scored, reverse=True)[:top_k]] + + def __len__(self) -> int: + with sqlite3.connect(self.db_path) as con: + return con.execute("SELECT COUNT(*) FROM patterns").fetchone()[0] + + def stats(self): + with sqlite3.connect(self.db_path) as con: + return con.execute( + "SELECT action, reward, count FROM patterns ORDER BY count DESC LIMIT 5" + ).fetchall() + + def _all_rows(self) -> list[dict]: + with sqlite3.connect(self.db_path) as con: + con.row_factory = sqlite3.Row + return [dict(r) for r in con.execute("SELECT * FROM patterns").fetchall()] diff --git a/emma/emma_run.py b/emma/emma_run.py new file mode 100644 index 0000000..d42867e --- /dev/null +++ b/emma/emma_run.py @@ -0,0 +1,64 @@ +""" +Start Emma: python emma_run.py +Statistikk: python emma_run.py --stats +""" +import sys +import argparse +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from emma.emma_backend_ollama import OllamaBackend +from emma.emma_resonance_persistent import PersistentMorphicMemory +from emma.emma_mcot import EmmaMCoTAgent +from emma.emma_guardrails import EmmaGuardrails +from emma.emma_flynn import FlynnTracker + + +def main(): + parser = argparse.ArgumentParser(description="Emma AI Agent REPL") + parser.add_argument("--model", default="llama3.2") + parser.add_argument("--embed", default="nomic-embed-text") + parser.add_argument("--stats", action="store_true") + args = parser.parse_args() + + if args.stats: + s = FlynnTracker().summary() + print("📊 Flynn-statistikk:") + for k, v in (s or {}).items(): + print(f" {k}: {v:.4f}" if isinstance(v, float) else f" {k}: {v}") + return + + llm = OllamaBackend(model=args.model, embed_model=args.embed) + memory = PersistentMorphicMemory() + guardrails = EmmaGuardrails() + agent = EmmaMCoTAgent(llm=llm, memory=memory) + + print(f"🤖 Emma klar | Modell: {args.model} | Morfisk minne: {len(memory)} mønstre") + print("exit=avslutt stats=Flynn-statistikk\n") + + history = [] + while True: + try: + user_input = input("Chris> ").strip() + except (KeyboardInterrupt, EOFError): + break + if user_input.lower() in ("exit", "quit"): + break + if user_input.lower() == "stats": + print(FlynnTracker().summary()) + continue + if not user_input: + continue + ok, reason = guardrails.can_execute(user_input) + if not ok: + print(f"🛡️ {reason}") + continue + response = agent.run(user_input, history) + history.append({"role": "user", "content": user_input}) + history.append({"role": "assistant", "content": response}) + print(f"Emma> {response}\n") + + +if __name__ == "__main__": + main() diff --git a/emma/emma_seed.py b/emma/emma_seed.py new file mode 100644 index 0000000..6a79ec1 --- /dev/null +++ b/emma/emma_seed.py @@ -0,0 +1,48 @@ +""" +Kjøres én gang ved første oppstart: + python emma_seed.py + +Seeder Emmas morfiske minne med alle 16 LEARNINGS som høy-reward startmønstre. +""" +import sys +import numpy as np +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from emma.emma_resonance_persistent import PersistentMorphicMemory +from emma.emma_mdp import EmmaAction + +LEARNINGS = [ + ("LEARNING-001: Aldri anta branch-status — sjekk alltid", EmmaAction.RETRIEVE), + ("LEARNING-002: Cloud Run bruker identity token, ikke access token", EmmaAction.TOOLCALL), + ("LEARNING-003: RAG isoleres fra FunctionTools", EmmaAction.RESPOND), + ("LEARNING-004: Aldri anta MASTERPLAN er oppdatert — spør Chris", EmmaAction.CLARIFY), + ("LEARNING-005: IAP-routing: DNS → forwarding rule → url-map → backend", EmmaAction.RETRIEVE), + ("LEARNING-006: Gitea er primær Git — GitHub er backup/mirror", EmmaAction.TOOLCALL), + ("LEARNING-007: Emma-filer lever i emma/-mappen, aldri i rot", EmmaAction.STOREPATTERN), + ("LEARNING-008: Guardrails kan ikke deaktiveres av Emma selv", EmmaAction.RESPOND), + ("LEARNING-009: flynn_log.jsonl er append-only — aldri overskriv", EmmaAction.STOREPATTERN), + ("LEARNING-010: Aldri deploy til Cloud Run uten eksplisitt GO fra Chris", EmmaAction.CLARIFY), + ("LEARNING-011: Jason er kundevendt kollega — ikke konkurrent", EmmaAction.RESPOND), + ("LEARNING-012: Bruk PersistentMorphicMemory i prod, ikke in-memory", EmmaAction.TOOLCALL), + ("LEARNING-013: GCE identity token hentes fra metadata-serveren", EmmaAction.TOOLCALL), + ("LEARNING-014: OPAX IAP Client ID og korrekt audience må matches", EmmaAction.RETRIEVE), + ("LEARNING-015: Alle commits pushes til GitHub OG Gitea", EmmaAction.TOOLCALL), + ("LEARNING-016: Emmas GCP-prosjekt: propane-will-491900-m5", EmmaAction.RETRIEVE), +] + + +def seed(): + memory = PersistentMorphicMemory() + for text, action in LEARNINGS: + rng = np.random.default_rng(abs(hash(text)) % (2**32)) + embedding = rng.standard_normal(768).astype(np.float32) + embedding /= np.linalg.norm(embedding) + memory.store(embedding, action, reward_val=0.95) + print(f"✅ {text[:65]}") + print(f"\n🧠 Morfisk minne seedet med {len(memory)} mønstre.") + + +if __name__ == "__main__": + seed() diff --git a/emma/requirements.txt b/emma/requirements.txt new file mode 100644 index 0000000..d7b7aec --- /dev/null +++ b/emma/requirements.txt @@ -0,0 +1,6 @@ +ollama>=0.2.0 +numpy>=1.26.0 +requests>=2.31.0 +fastapi>=0.111.0 +uvicorn>=0.29.0 +plotly>=5.22.0