""" Pulse & Brain Architecture for Ajarbot. PULSE (Pure Python): - Runs every N seconds - Zero API token cost - Checks: server health, disk space, log files, task queue - Only wakes the BRAIN when needed BRAIN (Agent/SDK): - Only invoked when: 1. Pulse detects an issue (error logs, low disk space, etc.) 2. Scheduled time for content generation (morning briefing) 3. Manual trigger requested This is much more efficient than running Agent in a loop. """ import asyncio import shutil import string import threading import time import traceback from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import Any, Callable, Dict, List, Optional from agent import Agent # How many seconds between brain invocations to avoid duplicates _BRAIN_COOLDOWN_SECONDS = 3600 class CheckType(Enum): """Type of check to perform.""" PURE_PYTHON = "pure_python" CONDITIONAL = "conditional" SCHEDULED = "scheduled" @dataclass class PulseCheck: """A check performed by the Pulse (pure Python).""" name: str check_func: Callable[[], Dict[str, Any]] interval_seconds: int = 60 last_run: Optional[datetime] = None @dataclass class BrainTask: """A task that requires the Brain (Agent/SDK).""" name: str check_type: CheckType prompt_template: str # For CONDITIONAL: condition to check condition_func: Optional[Callable[[Dict[str, Any]], bool]] = None # For SCHEDULED: when to run schedule_time: Optional[str] = None # "08:00", "18:00", etc. last_brain_run: Optional[datetime] = None # Output options send_to_platform: Optional[str] = None send_to_channel: Optional[str] = None _STATUS_ICONS = {"ok": "+", "warn": "!", "error": "x"} class PulseBrain: """ Hybrid monitoring system with zero-cost pulse and smart brain. The Pulse runs continuously checking system health (zero tokens). The Brain only activates when needed (uses tokens wisely). """ def __init__( self, agent: Agent, pulse_interval: int = 60, enable_defaults: bool = True, ) -> None: """ Initialize Pulse & Brain system. Args: agent: The Agent instance to use for brain tasks. pulse_interval: How often pulse loop runs (seconds). enable_defaults: Load example checks. Set False to start clean. """ self.agent = agent self.pulse_interval = pulse_interval self.pulse_checks: List[PulseCheck] = [] self.brain_tasks: List[BrainTask] = [] self.running = False self.thread: Optional[threading.Thread] = None self.adapters: Dict[str, Any] = {} # State tracking (protected by lock) self._lock = threading.Lock() self.pulse_data: Dict[str, Any] = {} self.brain_invocations = 0 if enable_defaults: self._setup_default_checks() print("[PulseBrain] Loaded default example checks") print( " To start clean: " "PulseBrain(agent, enable_defaults=False)" ) def _setup_default_checks(self) -> None: """Set up default pulse checks and brain tasks.""" def check_disk_space() -> Dict[str, Any]: """Check disk space (pure Python, no agent).""" try: usage = shutil.disk_usage(".") percent_used = (usage.used / usage.total) * 100 gb_free = usage.free / (1024 ** 3) if percent_used > 90: status = "error" elif percent_used > 80: status = "warn" else: status = "ok" return { "status": status, "percent_used": percent_used, "gb_free": gb_free, "message": ( f"Disk: {percent_used:.1f}% used, " f"{gb_free:.1f} GB free" ), } except Exception as e: return {"status": "error", "message": str(e)} def check_memory_tasks() -> Dict[str, Any]: """Check for stale tasks (pure Python).""" try: pending = self.agent.memory.get_tasks(status="pending") stale_count = len(pending) status = "warn" if stale_count > 5 else "ok" return { "status": status, "pending_count": len(pending), "stale_count": stale_count, "message": ( f"{len(pending)} pending tasks, " f"{stale_count} stale" ), } except Exception as e: return {"status": "error", "message": str(e)} def check_log_errors() -> Dict[str, Any]: """Check recent logs for errors (pure Python).""" return { "status": "ok", "errors_found": 0, "message": "No errors in recent logs", } self.pulse_checks.extend([ PulseCheck( "disk-space", check_disk_space, interval_seconds=300, ), PulseCheck( "memory-tasks", check_memory_tasks, interval_seconds=600, ), PulseCheck( "log-errors", check_log_errors, interval_seconds=60, ), ]) self.brain_tasks.extend([ BrainTask( name="disk-space-advisor", check_type=CheckType.CONDITIONAL, prompt_template=( "Disk space is running low:\n" "- Used: {percent_used:.1f}%\n" "- Free: {gb_free:.1f} GB\n\n" "Please analyze:\n" "1. Is this critical?\n" "2. What files/directories should I check?\n" "3. Should I set up automated cleanup?\n\n" "Be concise and actionable." ), condition_func=lambda data: ( data.get("status") == "error" ), ), BrainTask( name="error-analyst", check_type=CheckType.CONDITIONAL, prompt_template=( "Errors detected in logs:\n" "{message}\n\n" "Please analyze:\n" "1. What does this error mean?\n" "2. How critical is it?\n" "3. What should I do to fix it?" ), condition_func=lambda data: ( data.get("errors_found", 0) > 0 ), ), BrainTask( name="morning-briefing", check_type=CheckType.SCHEDULED, schedule_time="08:00", prompt_template=( "Good morning! Please provide a brief summary:\n\n" "1. System health " "(disk: {disk_space_message}, " "tasks: {tasks_message})\n" "2. Any pending tasks that need attention\n" "3. Priorities for today\n" "4. A motivational message\n\n" "Keep it brief and actionable." ), ), BrainTask( name="evening-summary", check_type=CheckType.SCHEDULED, schedule_time="18:00", prompt_template=( "Good evening! Daily wrap-up:\n\n" "1. What was accomplished today\n" "2. Tasks still pending: {pending_count}\n" "3. Any issues detected (disk, errors, etc.)\n" "4. Preview for tomorrow\n\n" "Keep it concise." ), ), ]) def add_adapter(self, platform: str, adapter: Any) -> None: """Register an adapter for sending messages.""" self.adapters[platform] = adapter def start(self) -> None: """Start the Pulse & Brain system.""" if self.running: return self.running = True self.thread = threading.Thread( target=self._pulse_loop, daemon=True ) self.thread.start() print("=" * 60) print("PULSE & BRAIN Started") print("=" * 60) print(f"\nPulse interval: {self.pulse_interval}s") print(f"Pulse checks: {len(self.pulse_checks)}") print(f"Brain tasks: {len(self.brain_tasks)}\n") for check in self.pulse_checks: print( f" [Pulse] {check.name} " f"(every {check.interval_seconds}s)" ) for task in self.brain_tasks: if task.check_type == CheckType.SCHEDULED: print( f" [Brain] {task.name} " f"(scheduled {task.schedule_time})" ) else: print(f" [Brain] {task.name} (conditional)") print("\n" + "=" * 60 + "\n") def stop(self) -> None: """Stop the Pulse & Brain system.""" self.running = False if self.thread: self.thread.join() print( f"\nPULSE & BRAIN Stopped " f"(Brain invoked {self.brain_invocations} times)" ) def _pulse_loop(self) -> None: """Main pulse loop (runs continuously, zero cost).""" while self.running: try: now = datetime.now() for check in self.pulse_checks: should_run = ( check.last_run is None or (now - check.last_run).total_seconds() >= check.interval_seconds ) if not should_run: continue result = check.check_func() check.last_run = now # Thread-safe update of pulse_data with self._lock: self.pulse_data[check.name] = result icon = _STATUS_ICONS.get( result.get("status"), "?" ) print( f"[{icon}] {check.name}: " f"{result.get('message', 'OK')}" ) self._check_brain_tasks(now) except Exception as e: print(f"Pulse error: {e}") traceback.print_exc() time.sleep(self.pulse_interval) def _check_brain_tasks(self, now: datetime) -> None: """Check if any brain tasks need to be invoked.""" for task in self.brain_tasks: should_invoke = False prompt_data: Dict[str, Any] = {} if ( task.check_type == CheckType.CONDITIONAL and task.condition_func ): for check_name, check_data in self.pulse_data.items(): if task.condition_func(check_data): should_invoke = True prompt_data = check_data print( f"Condition met for brain task: " f"{task.name}" ) break elif ( task.check_type == CheckType.SCHEDULED and task.schedule_time ): target_time = datetime.strptime( task.schedule_time, "%H:%M" ).time() current_time = now.time() time_match = ( current_time.hour == target_time.hour and current_time.minute == target_time.minute ) already_ran_recently = ( task.last_brain_run and (now - task.last_brain_run).total_seconds() < _BRAIN_COOLDOWN_SECONDS ) if time_match and not already_ran_recently: should_invoke = True prompt_data = self._gather_scheduled_data() print( f"Scheduled time for brain task: {task.name}" ) if should_invoke: self._invoke_brain(task, prompt_data) task.last_brain_run = now def _gather_scheduled_data(self) -> Dict[str, Any]: """Gather data from all pulse checks for scheduled brain tasks.""" disk_data = self.pulse_data.get("disk-space", {}) task_data = self.pulse_data.get("memory-tasks", {}) return { "disk_space_message": disk_data.get( "message", "Unknown" ), "tasks_message": task_data.get("message", "Unknown"), "pending_count": task_data.get("pending_count", 0), **disk_data, } def _invoke_brain( self, task: BrainTask, data: Dict[str, Any] ) -> None: """Invoke the Brain (Agent/SDK) for a task.""" print(f"Invoking brain: {task.name}") # Thread-safe increment with self._lock: self.brain_invocations += 1 try: # Use safe_substitute to prevent format string injection # Convert all data values to strings first safe_data = {k: str(v) for k, v in data.items()} template = string.Template(task.prompt_template) prompt = template.safe_substitute(safe_data) response = self.agent.chat( user_message=prompt, username="pulse-brain" ) print(f"Brain response ({len(response)} chars)") print(f" Preview: {response[:100]}...") if task.send_to_platform and task.send_to_channel: asyncio.run(self._send_to_platform(task, response)) except Exception as e: print(f"Brain error: {e}") traceback.print_exc() async def _send_to_platform( self, task: BrainTask, response: str ) -> None: """Send brain output to messaging platform.""" adapter = self.adapters.get(task.send_to_platform) if not adapter: return from adapters.base import OutboundMessage message = OutboundMessage( platform=task.send_to_platform, channel_id=task.send_to_channel, text=f"**{task.name}**\n\n{response}", ) result = await adapter.send_message(message) if result.get("success"): print(f"Sent to {task.send_to_platform}") def get_status(self) -> Dict[str, Any]: """Get current status of Pulse & Brain.""" # Thread-safe read of shared state with self._lock: return { "running": self.running, "pulse_interval": self.pulse_interval, "brain_invocations": self.brain_invocations, "pulse_checks": len(self.pulse_checks), "brain_tasks": len(self.brain_tasks), "latest_pulse_data": dict(self.pulse_data), # Copy } if __name__ == "__main__": agent = Agent( provider="claude", workspace_dir="./memory_workspace", enable_heartbeat=False, ) pb = PulseBrain(agent, pulse_interval=10) pb.start() try: print("Running... Press Ctrl+C to stop\n") while True: time.sleep(1) except KeyboardInterrupt: pb.stop()