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>
166 lines
5.7 KiB
Python
166 lines
5.7 KiB
Python
"""
|
|
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))
|