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:
2026-02-13 19:06:28 -07:00
commit a99799bf3d
58 changed files with 11434 additions and 0 deletions

38
config/adapters.yaml Normal file
View File

@@ -0,0 +1,38 @@
# Adapter configuration for ajarbot
# Copy this to adapters.local.yaml and fill in your credentials
adapters:
slack:
enabled: false
credentials:
# Get these from https://api.slack.com/apps
# 1. Create a new app
# 2. Enable Socket Mode and generate an App-Level Token (xapp-...)
# 3. Add Bot Token Scopes: chat:write, channels:history, groups:history, im:history, mpim:history
# 4. Install app to workspace to get Bot User OAuth Token (xoxb-...)
bot_token: "xoxb-YOUR-BOT-TOKEN"
app_token: "xapp-YOUR-APP-TOKEN"
settings:
# Optional: Auto-react to messages with emoji
auto_react_emoji: null # e.g., "thinking_face"
telegram:
enabled: false
credentials:
# Get this from @BotFather on Telegram
# 1. Message @BotFather
# 2. Send /newbot
# 3. Follow prompts to create bot
# 4. Copy the token
bot_token: "YOUR-BOT-TOKEN"
settings:
# Optional: Restrict bot to specific user IDs
allowed_users: [] # e.g., [123456789, 987654321]
# Message parsing mode
parse_mode: "Markdown" # or "HTML"
# User mapping (optional)
# Map platform user IDs to ajarbot usernames for memory system
user_mapping:
# slack:U12345: "alice"
# telegram:123456789: "bob"

165
config/config_loader.py Normal file
View File

@@ -0,0 +1,165 @@
"""
Configuration loader for adapter system.
Loads from YAML with environment variable override support.
"""
import os
from pathlib import Path
from typing import Any, Dict, Optional
import yaml
# Environment variable mappings: env var name -> (adapter, credential key)
_ENV_OVERRIDES = {
"AJARBOT_SLACK_BOT_TOKEN": ("slack", "bot_token"),
"AJARBOT_SLACK_APP_TOKEN": ("slack", "app_token"),
"AJARBOT_TELEGRAM_BOT_TOKEN": ("telegram", "bot_token"),
}
class ConfigLoader:
"""Load adapter configuration from YAML files with env var support."""
def __init__(self, config_dir: Optional[str] = None) -> None:
if config_dir is None:
config_dir = str(Path(__file__).parent)
self.config_dir = Path(config_dir)
self.config: Dict[str, Any] = {}
def load(self, filename: str = "adapters.yaml") -> Dict[str, Any]:
"""
Load configuration from YAML file.
Looks for files in this order:
1. {filename}.local.yaml (gitignored, for secrets)
2. {filename}
Environment variables can override any setting:
AJARBOT_SLACK_BOT_TOKEN -> adapters.slack.credentials.bot_token
AJARBOT_TELEGRAM_BOT_TOKEN -> adapters.telegram.credentials.bot_token
"""
local_file = self.config_dir / f"{Path(filename).stem}.local.yaml"
config_file = self.config_dir / filename
if local_file.exists():
print(f"[Config] Loading from {local_file}")
with open(local_file) as f:
self.config = yaml.safe_load(f) or {}
elif config_file.exists():
print(f"[Config] Loading from {config_file}")
with open(config_file) as f:
self.config = yaml.safe_load(f) or {}
else:
print("[Config] No config file found, using defaults")
self.config = {"adapters": {}}
self._apply_env_overrides()
return self.config
def _apply_env_overrides(self) -> None:
"""Apply environment variable overrides."""
for env_var, (adapter, credential_key) in _ENV_OVERRIDES.items():
value = os.getenv(env_var)
if not value:
continue
adapters = self.config.setdefault("adapters", {})
adapter_config = adapters.setdefault(adapter, {})
credentials = adapter_config.setdefault("credentials", {})
credentials[credential_key] = value
print(f"[Config] Using {env_var} from environment")
def get_adapter_config(
self, platform: str
) -> Optional[Dict[str, Any]]:
"""Get configuration for a specific platform."""
return self.config.get("adapters", {}).get(platform)
def is_adapter_enabled(self, platform: str) -> bool:
"""Check if an adapter is enabled."""
adapter_config = self.get_adapter_config(platform)
if not adapter_config:
return False
return adapter_config.get("enabled", False)
def get_user_mapping(self) -> Dict[str, str]:
"""Get user ID to username mapping."""
return self.config.get("user_mapping", {})
def save_template(
self, filename: str = "adapters.local.yaml"
) -> Path:
"""Save a template configuration file."""
template = {
"adapters": {
"slack": {
"enabled": False,
"credentials": {
"bot_token": "xoxb-YOUR-BOT-TOKEN",
"app_token": "xapp-YOUR-APP-TOKEN",
},
"settings": {},
},
"telegram": {
"enabled": False,
"credentials": {
"bot_token": "YOUR-BOT-TOKEN",
},
"settings": {
"allowed_users": [],
"parse_mode": "Markdown",
},
},
},
"user_mapping": {},
}
output_path = self.config_dir / filename
with open(output_path, "w") as f:
yaml.dump(
template, f, default_flow_style=False, sort_keys=False
)
print(f"[Config] Template saved to {output_path}")
return output_path
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "init":
loader = ConfigLoader()
path = loader.save_template()
print(f"\nConfiguration template created at: {path}")
print(
"\nEdit this file with your credentials, "
"then set enabled: true for each adapter you want to use."
)
else:
loader = ConfigLoader()
config = loader.load()
# Redact credentials before printing
def redact_credentials(data):
"""Redact sensitive credential values."""
if isinstance(data, dict):
redacted = {}
for key, value in data.items():
if key == "credentials" and isinstance(value, dict):
redacted[key] = {
k: f"{str(v)[:4]}****{str(v)[-4:]}" if v else None
for k, v in value.items()
}
elif isinstance(value, (dict, list)):
redacted[key] = redact_credentials(value)
else:
redacted[key] = value
return redacted
elif isinstance(data, list):
return [redact_credentials(item) for item in data]
return data
safe_config = redact_credentials(config)
print("\nLoaded configuration (credentials redacted):")
print(yaml.dump(safe_config, default_flow_style=False))

View File

@@ -0,0 +1,307 @@
"""
Custom Pulse & Brain configuration.
Define your own pulse checks (zero cost) and brain tasks (uses tokens).
"""
import subprocess
from typing import Any, Dict, List
import requests
from pulse_brain import BrainTask, CheckType, PulseCheck
# === PULSE CHECKS (Pure Python, Zero Cost) ===
def check_server_uptime() -> Dict[str, Any]:
"""Check if server is responsive (pure Python, no agent)."""
try:
response = requests.get(
"http://localhost:8000/health", timeout=5
)
status = "ok" if response.status_code == 200 else "error"
return {
"status": status,
"message": f"Server responded: {response.status_code}",
}
except Exception as e:
return {
"status": "error",
"message": f"Server unreachable: {e}",
}
def check_docker_containers() -> Dict[str, Any]:
"""Check Docker container status (pure Python)."""
try:
result = subprocess.run(
["docker", "ps", "--format", "{{.Status}}"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode != 0:
return {
"status": "error",
"message": "Docker check failed",
}
unhealthy = sum(
1
for line in result.stdout.split("\n")
if "unhealthy" in line.lower()
)
if unhealthy > 0:
message = f"{unhealthy} unhealthy container(s)"
else:
message = "All containers healthy"
return {
"status": "error" if unhealthy > 0 else "ok",
"unhealthy_count": unhealthy,
"message": message,
}
except Exception as e:
return {"status": "error", "message": str(e)}
def check_plex_server() -> Dict[str, Any]:
"""Check if Plex is running (pure Python)."""
try:
response = requests.get(
"http://localhost:32400/identity", timeout=5
)
is_ok = response.status_code == 200
return {
"status": "ok" if is_ok else "warn",
"message": (
"Plex server is running"
if is_ok
else "Plex unreachable"
),
}
except Exception as e:
return {
"status": "warn",
"message": f"Plex check failed: {e}",
}
def check_unifi_controller() -> Dict[str, Any]:
"""Check UniFi controller (pure Python)."""
try:
requests.get(
"https://localhost:8443", verify=False, timeout=5
)
return {
"status": "ok",
"message": "UniFi controller responding",
}
except Exception as e:
return {
"status": "error",
"message": f"UniFi unreachable: {e}",
}
def check_gpu_temperature() -> Dict[str, Any]:
"""Check GPU temperature (pure Python, requires nvidia-smi)."""
try:
result = subprocess.run(
[
"nvidia-smi",
"--query-gpu=temperature.gpu",
"--format=csv,noheader",
],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode != 0:
return {"status": "ok", "message": "GPU check skipped"}
temp = int(result.stdout.strip())
if temp > 85:
status = "error"
elif temp > 75:
status = "warn"
else:
status = "ok"
return {
"status": status,
"temperature": temp,
"message": f"GPU temperature: {temp}C",
}
except Exception:
return {"status": "ok", "message": "GPU check skipped"}
def check_star_citizen_patch() -> Dict[str, Any]:
"""Check for Star Citizen patches (pure Python, placeholder)."""
return {
"status": "ok",
"new_patch": False,
"message": "No new Star Citizen patches",
}
# === CUSTOM PULSE CHECKS ===
CUSTOM_PULSE_CHECKS: List[PulseCheck] = [
PulseCheck(
"server-uptime", check_server_uptime,
interval_seconds=60,
),
PulseCheck(
"docker-health", check_docker_containers,
interval_seconds=120,
),
PulseCheck(
"plex-status", check_plex_server,
interval_seconds=300,
),
PulseCheck(
"unifi-controller", check_unifi_controller,
interval_seconds=300,
),
PulseCheck(
"gpu-temp", check_gpu_temperature,
interval_seconds=60,
),
PulseCheck(
"star-citizen", check_star_citizen_patch,
interval_seconds=3600,
),
]
# === BRAIN TASKS (Agent/SDK, Uses Tokens) ===
CUSTOM_BRAIN_TASKS: List[BrainTask] = [
BrainTask(
name="server-medic",
check_type=CheckType.CONDITIONAL,
prompt_template=(
"Server is down!\n\n"
"Status: $message\n\n"
"Please analyze:\n"
"1. What could cause this?\n"
"2. What should I check first?\n"
"3. Should I restart services?\n\n"
"Be concise and actionable."
),
condition_func=lambda data: data.get("status") == "error",
send_to_platform="slack",
send_to_channel="C_ALERTS",
),
BrainTask(
name="docker-diagnostician",
check_type=CheckType.CONDITIONAL,
prompt_template=(
"Docker containers unhealthy!\n\n"
"Unhealthy count: $unhealthy_count\n\n"
"Please diagnose:\n"
"1. What might cause container health issues?\n"
"2. Should I restart them?\n"
"3. What logs should I check?"
),
condition_func=lambda data: (
data.get("unhealthy_count", 0) > 0
),
send_to_platform="telegram",
send_to_channel="123456789",
),
BrainTask(
name="gpu-thermal-advisor",
check_type=CheckType.CONDITIONAL,
prompt_template=(
"GPU temperature is high!\n\n"
"Current: $temperatureC\n\n"
"Please advise:\n"
"1. Is this dangerous?\n"
"2. What can I do to cool it down?\n"
"3. Should I stop current workloads?"
),
condition_func=lambda data: (
data.get("temperature", 0) > 80
),
),
BrainTask(
name="homelab-briefing",
check_type=CheckType.SCHEDULED,
schedule_time="08:00",
prompt_template=(
"Good morning! Homelab status report:\n\n"
"Server: $server_message\n"
"Docker: $docker_message\n"
"Plex: $plex_message\n"
"UniFi: $unifi_message\n"
"Star Citizen: $star_citizen_message\n\n"
"Overnight summary:\n"
"1. Any services restart?\n"
"2. Notable events?\n"
"3. Action items for today?\n\n"
"Keep it brief and friendly."
),
send_to_platform="slack",
send_to_channel="C_HOMELAB",
),
BrainTask(
name="homelab-evening-report",
check_type=CheckType.SCHEDULED,
schedule_time="22:00",
prompt_template=(
"Evening homelab report:\n\n"
"Today's status:\n"
"- Server uptime: $server_message\n"
"- Docker health: $docker_message\n"
"- GPU temp: $gpu_message\n\n"
"Summary:\n"
"1. Any issues today?\n"
"2. Services that needed attention?\n"
"3. Overnight monitoring notes?"
),
send_to_platform="telegram",
send_to_channel="123456789",
),
BrainTask(
name="patch-notifier",
check_type=CheckType.CONDITIONAL,
prompt_template=(
"New Star Citizen patch detected!\n\n"
"Please:\n"
"1. Summarize patch notes (if available)\n"
"2. Note any breaking changes\n"
"3. Recommend if I should update now or wait"
),
condition_func=lambda data: data.get("new_patch", False),
send_to_platform="discord",
send_to_channel="GAMING_CHANNEL",
),
]
def apply_custom_config(pulse_brain: Any) -> None:
"""Apply custom configuration to PulseBrain instance."""
existing_pulse_names = {c.name for c in pulse_brain.pulse_checks}
for check in CUSTOM_PULSE_CHECKS:
if check.name not in existing_pulse_names:
pulse_brain.pulse_checks.append(check)
existing_brain_names = {t.name for t in pulse_brain.brain_tasks}
for task in CUSTOM_BRAIN_TASKS:
if task.name not in existing_brain_names:
pulse_brain.brain_tasks.append(task)
print(
f"Applied custom config: "
f"{len(CUSTOM_PULSE_CHECKS)} pulse checks, "
f"{len(CUSTOM_BRAIN_TASKS)} brain tasks"
)

View File

@@ -0,0 +1,90 @@
# Scheduled Tasks Configuration (EXAMPLE)
# Copy this to scheduled_tasks.yaml and customize with your values
tasks:
# Morning briefing - sent to Slack/Telegram
- name: morning-weather
prompt: |
Good morning! Please provide a weather report and daily briefing:
1. Current weather (you can infer or say you need an API key)
2. Any pending tasks from yesterday
3. Priorities for today
4. A motivational quote to start the day
Keep it brief and friendly.
schedule: "daily 06:00"
enabled: true
send_to_platform: "telegram"
send_to_channel: "YOUR_TELEGRAM_USER_ID" # Replace with your Telegram user ID
# Evening summary
- name: evening-report
prompt: |
Good evening! Time for the daily wrap-up:
1. What was accomplished today?
2. Any tasks still pending?
3. Preview of tomorrow's priorities
4. Weather forecast for tomorrow (infer or API needed)
Keep it concise and positive.
schedule: "daily 18:00"
enabled: false
send_to_platform: "telegram"
send_to_channel: "YOUR_TELEGRAM_USER_ID"
# Hourly health check (no message sending)
- name: system-health-check
prompt: |
Quick health check:
1. Are there any tasks that have been pending > 24 hours?
2. Is the memory system healthy?
3. Any alerts or issues?
Respond with "HEALTHY" if all is well, otherwise describe the issue.
schedule: "hourly"
enabled: false
username: "health-checker"
# Weekly review on Friday
- name: weekly-summary
prompt: |
It's Friday! Time for the weekly review:
1. Major accomplishments this week
2. Challenges faced and lessons learned
3. Key metrics (tasks completed, etc.)
4. Goals for next week
5. Team shoutouts (if applicable)
Make it comprehensive but engaging.
schedule: "weekly fri 17:00"
enabled: false
send_to_platform: "slack"
send_to_channel: "YOUR_SLACK_CHANNEL_ID"
# Custom: Midday standup
- name: midday-standup
prompt: |
Midday check-in! Quick standup report:
1. Morning accomplishments
2. Current focus
3. Any blockers?
4. Afternoon plan
Keep it brief - standup style.
schedule: "daily 12:00"
enabled: false
send_to_platform: "slack"
send_to_channel: "YOUR_SLACK_CHANNEL_ID"
# Configuration notes:
# - schedule formats:
# - "hourly" - Every hour on the hour
# - "daily HH:MM" - Every day at specified time (24h format)
# - "weekly day HH:MM" - Every week on specified day (mon, tue, wed, thu, fri, sat, sun)
# - send_to_platform: null = don't send to messaging (only log)
# - username: Agent memory username to use for this task