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