sync: merge GitHub main into Gitea
Some checks are pending
Check Python Version Consistency / Check Python Version (push) Waiting to run
Some checks are pending
Check Python Version Consistency / Check Python Version (push) Waiting to run
This commit is contained in:
commit
2edc4d8fd3
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