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>
185 lines
6.9 KiB
Python
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)
|