Files
ajarbot/agent.py
Jordan Ramos a99799bf3d 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>
2026-02-13 19:06:28 -07:00

185 lines
6.9 KiB
Python

"""AI Agent with Memory and LLM Integration."""
from typing import List, Optional
from heartbeat import Heartbeat
from hooks import HooksSystem
from llm_interface import LLMInterface
from memory_system import MemorySystem
from tools import TOOL_DEFINITIONS, execute_tool
# Maximum number of recent messages to include in LLM context
MAX_CONTEXT_MESSAGES = 3 # Reduced from 5 to save tokens
# Maximum characters of agent response to store in memory
MEMORY_RESPONSE_PREVIEW_LENGTH = 200
class Agent:
"""AI Agent with memory, LLM, heartbeat, and hooks."""
def __init__(
self,
provider: str = "claude",
workspace_dir: str = "./memory_workspace",
enable_heartbeat: bool = False,
) -> None:
self.memory = MemorySystem(workspace_dir)
self.llm = LLMInterface(provider)
self.hooks = HooksSystem()
self.conversation_history: List[dict] = []
self.memory.sync()
self.hooks.trigger("agent", "startup", {"workspace_dir": workspace_dir})
self.heartbeat: Optional[Heartbeat] = None
if enable_heartbeat:
self.heartbeat = Heartbeat(self.memory, self.llm)
self.heartbeat.on_alert = self._on_heartbeat_alert
self.heartbeat.start()
def _on_heartbeat_alert(self, message: str) -> None:
"""Handle heartbeat alerts."""
print(f"\nHeartbeat Alert:\n{message}\n")
def chat(self, user_message: str, username: str = "default") -> str:
"""Chat with context from memory and tool use."""
# Handle model switching commands
if user_message.lower().startswith("/model "):
model_name = user_message[7:].strip()
self.llm.set_model(model_name)
return f"Switched to model: {model_name}"
elif user_message.lower() == "/sonnet":
self.llm.set_model("claude-sonnet-4-5-20250929")
return "Switched to Claude Sonnet 4.5 (more capable, higher cost)"
elif user_message.lower() == "/haiku":
self.llm.set_model("claude-haiku-4-5-20251001")
return "Switched to Claude Haiku 4.5 (faster, cheaper)"
elif user_message.lower() == "/status":
current_model = self.llm.model
is_sonnet = "sonnet" in current_model.lower()
cache_status = "enabled" if is_sonnet else "disabled (Haiku active)"
return (
f"Current model: {current_model}\n"
f"Prompt caching: {cache_status}\n"
f"Context messages: {MAX_CONTEXT_MESSAGES}\n"
f"Memory results: 2\n\n"
f"Commands: /sonnet, /haiku, /status"
)
soul = self.memory.get_soul()
user_profile = self.memory.get_user(username)
relevant_memory = self.memory.search(user_message, max_results=2)
memory_lines = [f"- {mem['snippet']}" for mem in relevant_memory]
system = (
f"{soul}\n\nUser Profile:\n{user_profile}\n\n"
f"Relevant Memory:\n" + "\n".join(memory_lines) +
f"\n\nYou have access to tools for file operations and command execution. "
f"Use them freely to help the user."
)
self.conversation_history.append(
{"role": "user", "content": user_message}
)
# Tool execution loop
max_iterations = 5 # Reduced from 10 to save costs
# Enable caching for Sonnet to save 90% on repeated system prompts
use_caching = "sonnet" in self.llm.model.lower()
for iteration in range(max_iterations):
response = self.llm.chat_with_tools(
self.conversation_history[-MAX_CONTEXT_MESSAGES:],
tools=TOOL_DEFINITIONS,
system=system,
use_cache=use_caching,
)
# Check stop reason
if response.stop_reason == "end_turn":
# Extract text response
text_content = []
for block in response.content:
if block.type == "text":
text_content.append(block.text)
final_response = "\n".join(text_content)
self.conversation_history.append(
{"role": "assistant", "content": final_response}
)
preview = final_response[:MEMORY_RESPONSE_PREVIEW_LENGTH]
self.memory.write_memory(
f"**User ({username})**: {user_message}\n"
f"**Agent**: {preview}...",
daily=True,
)
return final_response
elif response.stop_reason == "tool_use":
# Build assistant message with tool uses
assistant_content = []
tool_uses = []
for block in response.content:
if block.type == "text":
assistant_content.append({
"type": "text",
"text": block.text
})
elif block.type == "tool_use":
assistant_content.append({
"type": "tool_use",
"id": block.id,
"name": block.name,
"input": block.input
})
tool_uses.append(block)
self.conversation_history.append({
"role": "assistant",
"content": assistant_content
})
# Execute tools and build tool result message
tool_results = []
for tool_use in tool_uses:
result = execute_tool(tool_use.name, tool_use.input)
print(f"[Tool] {tool_use.name}: {result[:100]}...")
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result
})
self.conversation_history.append({
"role": "user",
"content": tool_results
})
else:
# Unexpected stop reason
return f"Unexpected stop reason: {response.stop_reason}"
return "Error: Maximum tool use iterations exceeded"
def switch_model(self, provider: str) -> None:
"""Switch LLM provider."""
self.llm = LLMInterface(provider)
if self.heartbeat:
self.heartbeat.llm = self.llm
def shutdown(self) -> None:
"""Cleanup and stop background services."""
if self.heartbeat:
self.heartbeat.stop()
self.memory.close()
self.hooks.trigger("agent", "shutdown", {})
if __name__ == "__main__":
agent = Agent(provider="claude")
response = agent.chat("What's my current project?", username="alice")
print(response)