feat(emma): full bootstrap — MDP, MCoT, Morphic Resonance, Flynn, Guardrails, Identity, Gitea, OPAX
This commit is contained in:
parent
b042234078
commit
51f5624c26
51
emma/README.md
Normal file
51
emma/README.md
Normal file
|
|
@ -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
|
||||||
19
emma/__init__.py
Normal file
19
emma/__init__.py
Normal file
|
|
@ -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",
|
||||||
|
]
|
||||||
0
emma/data/.gitkeep
Normal file
0
emma/data/.gitkeep
Normal file
61
emma/emma_backend_ollama.py
Normal file
61
emma/emma_backend_ollama.py
Normal file
|
|
@ -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"]
|
||||||
63
emma/emma_flynn.py
Normal file
63
emma/emma_flynn.py
Normal file
|
|
@ -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"],
|
||||||
|
}
|
||||||
39
emma/emma_gitea.py
Normal file
39
emma/emma_gitea.py
Normal file
|
|
@ -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
|
||||||
64
emma/emma_guardrails.py
Normal file
64
emma/emma_guardrails.py
Normal file
|
|
@ -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
|
||||||
36
emma/emma_identity.py
Normal file
36
emma/emma_identity.py
Normal file
|
|
@ -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 <chris.christiansen@vauco.no>",
|
||||||
|
"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}"""
|
||||||
67
emma/emma_mcot.py
Normal file
67
emma/emma_mcot.py
Normal file
|
|
@ -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)}
|
||||||
56
emma/emma_mdp.py
Normal file
56
emma/emma_mdp.py
Normal file
|
|
@ -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
|
||||||
52
emma/emma_opax.py
Normal file
52
emma/emma_opax.py
Normal file
|
|
@ -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()
|
||||||
47
emma/emma_resonance.py
Normal file
47
emma/emma_resonance.py
Normal file
|
|
@ -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))
|
||||||
75
emma/emma_resonance_persistent.py
Normal file
75
emma/emma_resonance_persistent.py
Normal file
|
|
@ -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()]
|
||||||
64
emma/emma_run.py
Normal file
64
emma/emma_run.py
Normal file
|
|
@ -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()
|
||||||
48
emma/emma_seed.py
Normal file
48
emma/emma_seed.py
Normal file
|
|
@ -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()
|
||||||
6
emma/requirements.txt
Normal file
6
emma/requirements.txt
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user