neurotron/src/cortex.py
2025-11-15 04:20:00 +01:00

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