2026-02-13 19:06:28 -07:00
|
|
|
"""
|
|
|
|
|
Multi-platform bot runner for ajarbot.
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
python bot_runner.py # Run with config from adapters.yaml
|
|
|
|
|
python bot_runner.py --config custom.yaml # Use custom config file
|
|
|
|
|
python bot_runner.py --init # Generate config template
|
|
|
|
|
|
|
|
|
|
Environment variables:
|
|
|
|
|
AJARBOT_SLACK_BOT_TOKEN # Slack bot token (xoxb-...)
|
|
|
|
|
AJARBOT_SLACK_APP_TOKEN # Slack app token (xapp-...)
|
|
|
|
|
AJARBOT_TELEGRAM_BOT_TOKEN # Telegram bot token
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import asyncio
|
|
|
|
|
import traceback
|
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
# Load environment variables from .env file
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
|
|
|
|
from adapters.base import AdapterConfig
|
|
|
|
|
from adapters.runtime import AdapterRuntime
|
|
|
|
|
from adapters.slack.adapter import SlackAdapter
|
|
|
|
|
from adapters.telegram.adapter import TelegramAdapter
|
|
|
|
|
from agent import Agent
|
|
|
|
|
from config.config_loader import ConfigLoader
|
2026-02-14 10:29:28 -07:00
|
|
|
from google_tools.oauth_manager import GoogleOAuthManager
|
2026-02-13 19:06:28 -07:00
|
|
|
from scheduled_tasks import TaskScheduler
|
|
|
|
|
|
|
|
|
|
# Adapter class registry mapping platform names to their classes
|
|
|
|
|
_ADAPTER_CLASSES = {
|
|
|
|
|
"slack": SlackAdapter,
|
|
|
|
|
"telegram": TelegramAdapter,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BotRunner:
|
|
|
|
|
"""Main bot runner that manages all adapters."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, config_file: str = "adapters.yaml") -> None:
|
|
|
|
|
self.config_loader = ConfigLoader()
|
|
|
|
|
self.config = self.config_loader.load(config_file)
|
|
|
|
|
self.runtime: AdapterRuntime = None
|
|
|
|
|
self.agent: Agent = None
|
|
|
|
|
self.scheduler: TaskScheduler = None
|
|
|
|
|
|
|
|
|
|
def _load_adapter(self, platform: str) -> bool:
|
|
|
|
|
"""Load and register a single adapter. Returns True if loaded."""
|
|
|
|
|
if not self.config_loader.is_adapter_enabled(platform):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
print(f"\n[Setup] Loading {platform.title()} adapter...")
|
|
|
|
|
adapter_cls = _ADAPTER_CLASSES[platform]
|
|
|
|
|
platform_config = self.config_loader.get_adapter_config(platform)
|
|
|
|
|
adapter = adapter_cls(AdapterConfig(
|
|
|
|
|
platform=platform,
|
|
|
|
|
enabled=True,
|
|
|
|
|
credentials=platform_config.get("credentials", {}),
|
|
|
|
|
settings=platform_config.get("settings", {}),
|
|
|
|
|
))
|
|
|
|
|
self.runtime.add_adapter(adapter)
|
|
|
|
|
print(f"[Setup] {platform.title()} adapter loaded")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def setup(self) -> bool:
|
|
|
|
|
"""Set up agent and adapters."""
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
print("Ajarbot Multi-Platform Runner")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
|
|
print("\n[Setup] Initializing agent...")
|
|
|
|
|
self.agent = Agent(
|
|
|
|
|
provider="claude",
|
|
|
|
|
workspace_dir="./memory_workspace",
|
|
|
|
|
enable_heartbeat=False,
|
|
|
|
|
)
|
|
|
|
|
print("[Setup] Agent initialized")
|
|
|
|
|
|
|
|
|
|
self.runtime = AdapterRuntime(self.agent)
|
|
|
|
|
|
|
|
|
|
enabled_count = sum(
|
|
|
|
|
self._load_adapter(platform)
|
|
|
|
|
for platform in _ADAPTER_CLASSES
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Load user mappings
|
|
|
|
|
user_mapping = self.config_loader.get_user_mapping()
|
|
|
|
|
for platform_user_id, username in user_mapping.items():
|
|
|
|
|
self.runtime.map_user(platform_user_id, username)
|
|
|
|
|
print(f"[Setup] User mapping: {platform_user_id} -> {username}")
|
|
|
|
|
|
|
|
|
|
if enabled_count == 0:
|
|
|
|
|
print("\nWARNING: No adapters enabled!")
|
|
|
|
|
print("Edit config/adapters.local.yaml and set enabled: true")
|
|
|
|
|
print("Or run: python bot_runner.py --init")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
print(f"\n[Setup] {enabled_count} adapter(s) configured")
|
|
|
|
|
|
|
|
|
|
# Initialize scheduler
|
|
|
|
|
print("\n[Setup] Initializing task scheduler...")
|
|
|
|
|
self.scheduler = TaskScheduler(
|
|
|
|
|
self.agent,
|
|
|
|
|
config_file="config/scheduled_tasks.yaml"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Register adapters with scheduler
|
|
|
|
|
for platform, adapter in self.runtime.registry._adapters.items():
|
|
|
|
|
self.scheduler.add_adapter(platform, adapter)
|
|
|
|
|
|
|
|
|
|
# List scheduled tasks
|
|
|
|
|
tasks = self.scheduler.list_tasks()
|
|
|
|
|
enabled_tasks = [t for t in tasks if t.get("enabled")]
|
|
|
|
|
if enabled_tasks:
|
|
|
|
|
print(f"[Setup] {len(enabled_tasks)} scheduled task(s) enabled:")
|
|
|
|
|
for task_info in enabled_tasks:
|
|
|
|
|
print(f" - {task_info['name']}: {task_info['schedule']}")
|
|
|
|
|
if task_info.get("send_to_platform"):
|
|
|
|
|
print(f" → {task_info['send_to_platform']}")
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
async def run(self) -> None:
|
|
|
|
|
"""Start all adapters and run until interrupted."""
|
|
|
|
|
if not self.setup():
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
|
print("Starting bot...")
|
|
|
|
|
print("=" * 60 + "\n")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await self.runtime.start()
|
|
|
|
|
|
|
|
|
|
# Start scheduler if configured
|
|
|
|
|
if self.scheduler:
|
|
|
|
|
self.scheduler.start()
|
|
|
|
|
print("[Scheduler] Task scheduler started\n")
|
|
|
|
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
print("Bot is running! Press Ctrl+C to stop.")
|
|
|
|
|
print("=" * 60 + "\n")
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print("\n\n[Shutdown] Received interrupt signal...")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"\n[Error] {e}")
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
finally:
|
|
|
|
|
if self.scheduler:
|
|
|
|
|
self.scheduler.stop()
|
|
|
|
|
print("[Scheduler] Task scheduler stopped")
|
|
|
|
|
await self.runtime.stop()
|
|
|
|
|
print("\n[Shutdown] Bot stopped cleanly")
|
|
|
|
|
|
|
|
|
|
async def health_check(self) -> None:
|
|
|
|
|
"""Check health of all adapters."""
|
|
|
|
|
if not self.runtime:
|
|
|
|
|
print("Runtime not initialized")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
status = await self.runtime.health_check()
|
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
|
print("Health Check")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
print(f"\nRuntime running: {status['runtime_running']}")
|
|
|
|
|
print("\nAdapters:")
|
|
|
|
|
for platform, adapter_status in status["adapters"].items():
|
|
|
|
|
print(f"\n {platform.upper()}:")
|
|
|
|
|
for key, value in adapter_status.items():
|
|
|
|
|
print(f" {key}: {value}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
"""Main entry point."""
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
description="Ajarbot Multi-Platform Runner"
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--config",
|
|
|
|
|
default="adapters.yaml",
|
|
|
|
|
help="Config file to use (default: adapters.yaml)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--init",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Generate config template",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--health",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Run health check",
|
|
|
|
|
)
|
2026-02-14 10:29:28 -07:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--setup-google",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Set up Google OAuth for Gmail/Calendar integration",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--manual",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Use manual OAuth code entry (for headless servers)",
|
|
|
|
|
)
|
2026-02-13 19:06:28 -07:00
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
if args.init:
|
|
|
|
|
print("Generating configuration template...")
|
|
|
|
|
loader = ConfigLoader()
|
|
|
|
|
path = loader.save_template()
|
|
|
|
|
print(f"\nConfiguration template created at: {path}")
|
|
|
|
|
print("\nNext steps:")
|
|
|
|
|
print("1. Edit the file with your credentials")
|
|
|
|
|
print("2. Set enabled: true for adapters you want to use")
|
|
|
|
|
print("3. Run: python bot_runner.py")
|
|
|
|
|
return
|
|
|
|
|
|
2026-02-14 10:29:28 -07:00
|
|
|
if args.setup_google:
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
print("Google OAuth Setup")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
print()
|
|
|
|
|
oauth_manager = GoogleOAuthManager()
|
|
|
|
|
|
|
|
|
|
if oauth_manager.is_authorized():
|
|
|
|
|
print("✓ Already authorized!")
|
|
|
|
|
print(f"✓ Tokens found at {oauth_manager.token_file}")
|
|
|
|
|
print("\nTo re-authorize, delete the token file and run this command again.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
success = oauth_manager.run_oauth_flow(manual=args.manual)
|
|
|
|
|
if success:
|
|
|
|
|
print("You can now use Gmail and Calendar tools!")
|
|
|
|
|
print("\nTest it:")
|
|
|
|
|
print(" Via Telegram: \"What's on my calendar?\"")
|
|
|
|
|
print(" Via Telegram: \"Send an email to john@example.com\"")
|
|
|
|
|
else:
|
|
|
|
|
print("\nSetup failed. Please check:")
|
|
|
|
|
print("1. config/google_credentials.yaml exists with valid client_id/client_secret")
|
|
|
|
|
print("2. You authorized the app in your browser")
|
|
|
|
|
print("3. No firewall blocking localhost:8080")
|
|
|
|
|
return
|
|
|
|
|
|
2026-02-13 19:06:28 -07:00
|
|
|
runner = BotRunner(config_file=args.config)
|
|
|
|
|
|
|
|
|
|
if args.health:
|
|
|
|
|
asyncio.run(runner.health_check())
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
asyncio.run(runner.run())
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print("\nExiting...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|