Initial commit: Ajarbot with optimizations
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>
This commit is contained in:
487
pulse_brain.py
Normal file
487
pulse_brain.py
Normal file
@@ -0,0 +1,487 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user