230 lines
8.1 KiB
Python
230 lines
8.1 KiB
Python
import json
|
|
import time
|
|
from collections import defaultdict, deque
|
|
from pathlib import Path
|
|
from time import sleep
|
|
from rich.console import Console
|
|
|
|
from neuron import Neuron
|
|
from hippocampus import Hippocampus
|
|
from perception import Perception
|
|
from motor import Motor
|
|
|
|
from .neurotron_config import (
|
|
NEUROTRON_MODE, NEUROTRON_TICK, NEUROTRON_TICK_MIN, NEUROTRON_TICK_MAX, NEUROTRON_TICK_STEP,
|
|
NEUROTRON_DIAG_EVERY_TICKS, NEUROTRON_DATASET_PATH,
|
|
HEARTBEAT_ENABLED, HEARTBEAT_STYLE, NEUROTRON_THRESHOLDS,
|
|
TELEMETRY_MAXLEN, TELEMETRY_FLUSH_EVERY_TICKS,
|
|
)
|
|
from .autodiagnostic import AutoDiagnostic
|
|
|
|
|
|
class VitalSigns(Neuron):
|
|
name = "VitalSigns"
|
|
def observe(self) -> None:
|
|
snap = self.ctx.perception.snapshot()
|
|
self.publish("vitals", snap)
|
|
self.ctx.memory.remember("observe.vitals", snap)
|
|
|
|
|
|
class EchoAgent(Neuron):
|
|
name = "EchoAgent"
|
|
def think(self) -> None:
|
|
msg = self.consume("vitals")
|
|
if msg:
|
|
self.publish("actions", {"action": "echo", "text": f"CPU {msg.get('cpu_percent', '?')}%"})
|
|
|
|
|
|
class Cortex:
|
|
"""
|
|
Orquestrador: liga neurónios, bus de mensagens, memória, IO e ciclo cognitivo.
|
|
Agora com Telemetria Contínua (V5): heartbeat, microalertas e flush periódico.
|
|
"""
|
|
def __init__(self, runtime_dir, log_dir, tick_seconds=NEUROTRON_TICK):
|
|
self.runtime_dir = runtime_dir
|
|
self.log_dir = log_dir
|
|
self.tick = float(tick_seconds)
|
|
self.mode = NEUROTRON_MODE
|
|
self._tick_count = 0
|
|
self.diagnostic = AutoDiagnostic(runtime_dir, log_dir)
|
|
|
|
self.console = Console()
|
|
self.memory = Hippocampus(log_dir=log_dir)
|
|
self.perception = Perception()
|
|
self.motor = Motor()
|
|
|
|
# Message bus simples: channels → deque
|
|
self.bus = defaultdict(lambda: deque(maxlen=32))
|
|
|
|
# Telemetria em memória (curto prazo)
|
|
self.telemetry = deque(maxlen=TELEMETRY_MAXLEN)
|
|
|
|
# Regista neurónios (podes adicionar mais à medida)
|
|
self.neurons: list[Neuron] = [
|
|
VitalSigns(self),
|
|
EchoAgent(self),
|
|
]
|
|
|
|
self._booted = False
|
|
|
|
# Caminho para gravar a telemetria
|
|
self.telemetry_path = Path(NEUROTRON_DATASET_PATH) / "telemetry.json"
|
|
self.telemetry_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# ——— ciclo de vida ———
|
|
def boot(self) -> None:
|
|
if self._booted:
|
|
return
|
|
self.console.print("[bold cyan]🧠 Neurotron[/] — boot")
|
|
self.memory.remember("boot", {"version": "0.1", "tick": self.tick})
|
|
self._booted = True
|
|
state, _ = self.diagnostic.run_exam()
|
|
self._apply_homeostasis(state)
|
|
|
|
def _apply_homeostasis(self, state):
|
|
if state == "CRITICAL":
|
|
self.mode = "diagnostic"
|
|
self.tick = min(NEUROTRON_TICK_MAX, self.tick + NEUROTRON_TICK_STEP)
|
|
elif state == "ALERT":
|
|
self.tick = min(NEUROTRON_TICK_MAX, self.tick + NEUROTRON_TICK_STEP / 2)
|
|
elif state == "STABLE":
|
|
self.tick = max(NEUROTRON_TICK_MIN, self.tick - NEUROTRON_TICK_STEP / 2)
|
|
# UNKNOWN → não mexe
|
|
|
|
def shutdown(self, reason: str = ""):
|
|
self.console.print(f"[yellow]shutdown:[/] {reason}")
|
|
self.memory.remember("shutdown", {"reason": reason})
|
|
|
|
def fatal(self, e: Exception):
|
|
self.console.print(f"[red]fatal:[/] {e!r}")
|
|
self.memory.remember("fatal", {"error": repr(e)})
|
|
print(f"fatal: {repr(e)}")
|
|
raise
|
|
|
|
# ——— loop ———
|
|
def observe(self) -> None:
|
|
for n in self.neurons:
|
|
n.observe()
|
|
|
|
def think(self) -> None:
|
|
for n in self.neurons:
|
|
n.think()
|
|
|
|
def act(self) -> None:
|
|
# Consumir ações agregadas e executar
|
|
action = self.bus_consume("actions")
|
|
if action and action.get("action") == "echo":
|
|
res = self.motor.run("echo", [action.get("text", "")])
|
|
self.memory.remember("act.echo", res)
|
|
if res.get("stdout"):
|
|
self.console.print(f"[green]{res['stdout'].strip()}[/]")
|
|
|
|
def rest(self):
|
|
# Heartbeat e microalertas antes de dormir
|
|
if HEARTBEAT_ENABLED:
|
|
self._heartbeat_and_telemetry()
|
|
|
|
# Pausa regulada
|
|
sleep(self.tick)
|
|
|
|
# Contador e rotinas periódicas
|
|
self._tick_count += 1
|
|
|
|
if self._tick_count % NEUROTRON_DIAG_EVERY_TICKS == 0:
|
|
state, _ = self.diagnostic.run_exam()
|
|
self._apply_homeostasis(state)
|
|
|
|
if self._tick_count % TELEMETRY_FLUSH_EVERY_TICKS == 0:
|
|
self._flush_telemetry()
|
|
|
|
# ——— telemetria/alertas ———
|
|
def _heartbeat_and_telemetry(self):
|
|
snap = self.perception.snapshot()
|
|
cpu = snap.get("cpu_percent", "?")
|
|
mem = (snap.get("mem") or {}).get("percent", "?")
|
|
load = snap.get("loadavg") or []
|
|
|
|
# Adiciona ao buffer de telemetria
|
|
self.telemetry.append({
|
|
"ts": time.time(),
|
|
"cpu": cpu,
|
|
"mem": mem,
|
|
"load": load,
|
|
"tick": self.tick,
|
|
})
|
|
|
|
# Microalertas com base nos limiares
|
|
self._evaluate_microalerts(cpu, mem, load)
|
|
|
|
# Heartbeat visual
|
|
color = self._color_for_levels(cpu, mem, load)
|
|
if HEARTBEAT_STYLE == "compact":
|
|
self.console.print(f"[bold {color}]💓[/] CPU: {cpu}% | MEM: {mem}% | TICK: {self.tick:.2f}s")
|
|
else:
|
|
self.console.print(
|
|
f"[bold {color}]💓 [Heartbeat][/bold {color}] "
|
|
f"CPU: {cpu}% | MEM: {mem}% | LOAD: {load} | TICK: {self.tick:.2f}s | MODE: {self.mode}"
|
|
)
|
|
|
|
def _evaluate_microalerts(self, cpu, mem, load):
|
|
alerts = []
|
|
# Normaliza
|
|
load1 = load[0] if (isinstance(load, (list, tuple)) and load) else None
|
|
|
|
try:
|
|
if isinstance(cpu, (int, float)) and cpu >= NEUROTRON_THRESHOLDS["cpu_high"]:
|
|
alerts.append(("cpu", cpu))
|
|
if isinstance(mem, (int, float)) and mem >= NEUROTRON_THRESHOLDS["mem_high"]:
|
|
alerts.append(("mem", mem))
|
|
if isinstance(load1, (int, float)) and load1 >= NEUROTRON_THRESHOLDS["load1_high"]:
|
|
alerts.append(("load1", load1))
|
|
except KeyError:
|
|
pass # thresholds incompletos → sem microalertas
|
|
|
|
if not alerts:
|
|
return
|
|
|
|
for (metric, value) in alerts:
|
|
self.console.print(f"[yellow]⚠️ Microalerta:[/] {metric.upper()} {value} — ajustando homeostase (tick +{NEUROTRON_TICK_STEP:.2f}s)")
|
|
# Ajuste simples de segurança
|
|
self.tick = min(NEUROTRON_TICK_MAX, self.tick + NEUROTRON_TICK_STEP)
|
|
|
|
self.memory.remember("microalert", {
|
|
"ts": time.time(),
|
|
"alerts": alerts,
|
|
"new_tick": self.tick,
|
|
})
|
|
|
|
def _color_for_levels(self, cpu, mem, load):
|
|
# Heurística simples de cor
|
|
try:
|
|
load1 = load[0] if (isinstance(load, (list, tuple)) and load) else 0.0
|
|
high = (
|
|
(isinstance(cpu, (int, float)) and cpu >= NEUROTRON_THRESHOLDS["cpu_high"]) or
|
|
(isinstance(mem, (int, float)) and mem >= NEUROTRON_THRESHOLDS["mem_high"]) or
|
|
(isinstance(load1, (int, float)) and load1 >= NEUROTRON_THRESHOLDS["load1_high"])
|
|
)
|
|
if high:
|
|
return "yellow"
|
|
except Exception:
|
|
pass
|
|
return "green"
|
|
|
|
def _flush_telemetry(self):
|
|
# Grava o buffer de telemetria em JSON (mantendo histórico curto)
|
|
try:
|
|
data = list(self.telemetry)
|
|
with self.telemetry_path.open("w") as f:
|
|
json.dump(data, f)
|
|
self.memory.remember("telemetry.flush", {"count": len(data), "path": str(self.telemetry_path)})
|
|
except Exception as e:
|
|
self.console.print(f"[red]✖ Falha ao gravar telemetria:[/] {e!r}")
|
|
self.memory.remember("telemetry.error", {"error": repr(e)})
|
|
|
|
# ——— bus ———
|
|
def bus_publish(self, channel: str, payload: dict) -> None:
|
|
self.bus[channel].append(payload)
|
|
|
|
def bus_consume(self, channel: str) -> dict | None:
|
|
q = self.bus[channel]
|
|
return q.popleft() if q else None |