Features: - Multi-platform bot (Slack, Telegram) - Memory system with SQLite FTS - Tool use capabilities (file ops, commands) - Scheduled tasks system - Dynamic model switching (/sonnet, /haiku) - Prompt caching for cost optimization Optimizations: - Default to Haiku 4.5 (12x cheaper) - Reduced context: 3 messages, 2 memory results - Optimized SOUL.md (48% smaller) - Automatic caching when using Sonnet (90% savings) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
488 lines
16 KiB
Python
488 lines
16 KiB
Python
"""
|
|
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()
|