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:
258
adapters/base.py
Normal file
258
adapters/base.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
Base adapter interface for messaging platforms.
|
||||
|
||||
Inspired by OpenClaw's ChannelPlugin architecture but simplified
|
||||
for ajarbot's needs.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
"""Types of messages that can be sent or received."""
|
||||
TEXT = "text"
|
||||
MEDIA = "media"
|
||||
FILE = "file"
|
||||
REACTION = "reaction"
|
||||
|
||||
|
||||
@dataclass
|
||||
class InboundMessage:
|
||||
"""Represents a message received from a messaging platform."""
|
||||
platform: str
|
||||
user_id: str
|
||||
username: str
|
||||
text: str
|
||||
channel_id: str
|
||||
thread_id: Optional[str]
|
||||
reply_to_id: Optional[str]
|
||||
message_type: MessageType
|
||||
metadata: Dict[str, Any]
|
||||
raw: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutboundMessage:
|
||||
"""Represents a message to be sent to a messaging platform."""
|
||||
platform: str
|
||||
channel_id: str
|
||||
text: str
|
||||
thread_id: Optional[str] = None
|
||||
reply_to_id: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterConfig:
|
||||
"""Configuration for an adapter instance."""
|
||||
platform: str
|
||||
enabled: bool = True
|
||||
credentials: Dict[str, Any] = field(default_factory=dict)
|
||||
settings: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterCapabilities:
|
||||
"""Describes what a messaging platform adapter can do."""
|
||||
supports_threads: bool = False
|
||||
supports_reactions: bool = False
|
||||
supports_media: bool = False
|
||||
supports_files: bool = False
|
||||
supports_markdown: bool = False
|
||||
max_message_length: int = 2000
|
||||
chunking_strategy: Optional[str] = None # "word", "markdown", "char"
|
||||
|
||||
|
||||
class BaseAdapter(ABC):
|
||||
"""
|
||||
Base adapter interface for messaging platforms.
|
||||
|
||||
Core aspects:
|
||||
- Config: Platform configuration and credentials
|
||||
- Gateway: Connection lifecycle management
|
||||
- Outbound: Sending messages
|
||||
- Inbound: Receiving and parsing messages
|
||||
- Status: Health checks and monitoring
|
||||
"""
|
||||
|
||||
def __init__(self, config: AdapterConfig) -> None:
|
||||
self.config = config
|
||||
self.is_running = False
|
||||
self._message_handlers: List[Callable[[InboundMessage], None]] = []
|
||||
|
||||
# --- Core Interface (Required) ---
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform_name(self) -> str:
|
||||
"""Platform identifier (e.g., 'slack', 'telegram')."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def capabilities(self) -> AdapterCapabilities:
|
||||
"""Describe platform capabilities."""
|
||||
|
||||
@abstractmethod
|
||||
async def start(self) -> None:
|
||||
"""Start the adapter connection."""
|
||||
|
||||
@abstractmethod
|
||||
async def stop(self) -> None:
|
||||
"""Stop the adapter connection."""
|
||||
|
||||
@abstractmethod
|
||||
async def send_message(
|
||||
self, message: OutboundMessage
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Send a message to the platform.
|
||||
|
||||
Returns:
|
||||
Dict with at least {"success": bool, "message_id": str}
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate that the adapter is properly configured."""
|
||||
|
||||
# --- Message Handler Registration ---
|
||||
|
||||
def register_message_handler(
|
||||
self, handler: Callable[[InboundMessage], None]
|
||||
) -> None:
|
||||
"""Register a function to be called when messages are received."""
|
||||
self._message_handlers.append(handler)
|
||||
|
||||
def _dispatch_message(self, message: InboundMessage) -> None:
|
||||
"""Internal: Dispatch incoming message to all registered handlers."""
|
||||
for handler in self._message_handlers:
|
||||
try:
|
||||
handler(message)
|
||||
except Exception as e:
|
||||
print(f"Error in message handler: {e}")
|
||||
|
||||
# --- Optional Features (Can be overridden) ---
|
||||
|
||||
async def send_reaction(
|
||||
self, channel_id: str, message_id: str, emoji: str
|
||||
) -> bool:
|
||||
"""Send a reaction/emoji to a message. Optional."""
|
||||
return False
|
||||
|
||||
async def send_typing_indicator(self, channel_id: str) -> None:
|
||||
"""Show typing indicator. Optional."""
|
||||
|
||||
async def health_check(self) -> Dict[str, Any]:
|
||||
"""Perform health check on the adapter."""
|
||||
return {
|
||||
"platform": self.platform_name,
|
||||
"running": self.is_running,
|
||||
"healthy": self.is_running and self.validate_config(),
|
||||
}
|
||||
|
||||
def chunk_text(self, text: str) -> List[str]:
|
||||
"""Split long text into chunks based on platform limits."""
|
||||
max_len = self.capabilities.max_message_length
|
||||
|
||||
if len(text) <= max_len:
|
||||
return [text]
|
||||
|
||||
strategy = self.capabilities.chunking_strategy or "word"
|
||||
|
||||
if strategy == "word":
|
||||
return self._chunk_by_words(text, max_len)
|
||||
elif strategy == "char":
|
||||
return self._chunk_by_chars(text, max_len)
|
||||
elif strategy == "markdown":
|
||||
return self._chunk_by_lines(text, max_len)
|
||||
|
||||
return [text]
|
||||
|
||||
@staticmethod
|
||||
def _chunk_by_words(text: str, max_len: int) -> List[str]:
|
||||
"""Split text on word boundaries."""
|
||||
words = text.split()
|
||||
chunks: List[str] = []
|
||||
current_chunk: List[str] = []
|
||||
current_length = 0
|
||||
|
||||
for word in words:
|
||||
word_length = len(word) + 1 # +1 for space
|
||||
if current_length + word_length > max_len:
|
||||
chunks.append(" ".join(current_chunk))
|
||||
current_chunk = [word]
|
||||
current_length = word_length
|
||||
else:
|
||||
current_chunk.append(word)
|
||||
current_length += word_length
|
||||
|
||||
if current_chunk:
|
||||
chunks.append(" ".join(current_chunk))
|
||||
|
||||
return chunks
|
||||
|
||||
@staticmethod
|
||||
def _chunk_by_chars(text: str, max_len: int) -> List[str]:
|
||||
"""Split text at fixed character boundaries."""
|
||||
return [text[i:i + max_len] for i in range(0, len(text), max_len)]
|
||||
|
||||
@staticmethod
|
||||
def _chunk_by_lines(text: str, max_len: int) -> List[str]:
|
||||
"""Split text on line boundaries preserving markdown."""
|
||||
lines = text.split("\n")
|
||||
chunks: List[str] = []
|
||||
current_chunk: List[str] = []
|
||||
current_length = 0
|
||||
|
||||
for line in lines:
|
||||
line_length = len(line) + 1 # +1 for newline
|
||||
if current_length + line_length > max_len:
|
||||
chunks.append("\n".join(current_chunk))
|
||||
current_chunk = [line]
|
||||
current_length = line_length
|
||||
else:
|
||||
current_chunk.append(line)
|
||||
current_length += line_length
|
||||
|
||||
if current_chunk:
|
||||
chunks.append("\n".join(current_chunk))
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
class AdapterRegistry:
|
||||
"""Registry for managing multiple platform adapters."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._adapters: Dict[str, BaseAdapter] = {}
|
||||
|
||||
def register(self, adapter: BaseAdapter) -> None:
|
||||
"""Register an adapter instance."""
|
||||
self._adapters[adapter.platform_name] = adapter
|
||||
|
||||
def get(self, platform_name: str) -> Optional[BaseAdapter]:
|
||||
"""Get an adapter by platform name."""
|
||||
return self._adapters.get(platform_name)
|
||||
|
||||
def list_platforms(self) -> List[str]:
|
||||
"""List all registered platform names."""
|
||||
return list(self._adapters.keys())
|
||||
|
||||
def get_all(self) -> List[BaseAdapter]:
|
||||
"""Get all registered adapters."""
|
||||
return list(self._adapters.values())
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start all registered adapters."""
|
||||
for adapter in self._adapters.values():
|
||||
if adapter.config.enabled:
|
||||
await adapter.start()
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop all registered adapters."""
|
||||
for adapter in self._adapters.values():
|
||||
if adapter.is_running:
|
||||
await adapter.stop()
|
||||
Reference in New Issue
Block a user