2024-08-17 13:00:37 -05:00
import json
2024-08-16 12:43:59 -05:00
import os
import importlib
import inspect
2024-08-17 18:31:53 -05:00
import logging
2024-08-18 17:00:03 -05:00
import sys
2024-08-18 16:53:54 -05:00
import subprocess
import requests
2024-08-18 17:25:30 -05:00
from telegram import Update , __version__ as telegram_version , InlineKeyboardButton , InlineKeyboardMarkup , ReplyKeyboardMarkup
2024-08-18 15:57:45 -05:00
from telegram . ext import Application , CommandHandler , MessageHandler , filters , ContextTypes , CallbackQueryHandler
2024-08-16 12:43:59 -05:00
from dotenv import load_dotenv
from tools . base_tool import BaseTool
2024-08-18 14:27:20 -05:00
from anthropic import Anthropic
2024-08-16 12:43:59 -05:00
# Load environment variables
load_dotenv ( )
2024-08-18 14:27:20 -05:00
anthropic_client = Anthropic (
2024-08-18 14:19:41 -05:00
api_key = os . environ . get ( " ANTHROPIC_API_KEY " ) ,
default_headers = { " anthropic-beta " : " max-tokens-3-5-sonnet-2024-07-15 " }
)
2024-08-17 19:30:38 -05:00
# Set up logging to console and file
2024-08-18 07:35:52 -05:00
logging . basicConfig ( level = logging . WARNING , handlers = [
2024-08-17 18:32:08 -05:00
logging . StreamHandler ( ) ,
logging . FileHandler ( ' logs/output.log ' , mode = ' a ' )
] )
2024-08-17 19:30:18 -05:00
2024-08-18 16:53:54 -05:00
# Set up Telegram bots
DAEMON_BOT_TOKEN = os . getenv ( ' TELEGRAM_BOT_TOKEN ' )
APPRENTICE_BOT_TOKEN = os . getenv ( ' TELEGRAM_APPRENTICE_BOT_TOKEN ' )
GITHUB_REPO_OWNER = os . getenv ( ' GITHUB_REPO_OWNER ' )
GITHUB_REPO_NAME = os . getenv ( ' GITHUB_REPO_NAME ' )
GITHUB_ACCESS_TOKEN = os . getenv ( ' GITHUB_ACCESS_TOKEN ' )
2024-08-16 12:43:59 -05:00
2024-08-17 17:53:25 -05:00
# Load system prompt
with open ( " prompts/developer_prompt.txt " , " r " ) as file :
system_prompt = file . read ( ) . strip ( )
2024-08-16 12:43:59 -05:00
# Dictionary to store conversation history for each user
conversation_history = { }
2024-08-18 15:57:45 -05:00
# Dictionary to store processing status for each user
processing_status = { }
2024-08-16 12:43:59 -05:00
# Load tools
tools = [ ]
tools_dir = os . path . join ( os . path . dirname ( __file__ ) , ' tools ' )
for filename in os . listdir ( tools_dir ) :
if filename . endswith ( ' .py ' ) and filename != ' __init__.py ' and filename != ' base_tool.py ' :
module_name = f ' tools. { filename [ : - 3 ] } '
module = importlib . import_module ( module_name )
for name , obj in inspect . getmembers ( module ) :
if inspect . isclass ( obj ) and issubclass ( obj , BaseTool ) and obj != BaseTool :
tools . append ( obj ( ) )
# Collect all function definitions
functions = [ ]
for tool in tools :
functions . extend ( tool . get_functions ( ) )
2024-08-18 17:25:30 -05:00
def get_keyboard ( ) :
keyboard = [
[ ' /status ' , ' /reset ' ]
]
return ReplyKeyboardMarkup ( keyboard , resize_keyboard = True )
2024-08-16 12:43:59 -05:00
async def start ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
2024-08-17 18:32:08 -05:00
logging . info ( " Bot started " )
2024-08-18 17:25:30 -05:00
await update . message . reply_text (
" Hello! I ' m your AI assistant. How can I help you today? You can send me images and then ask questions about them. " ,
reply_markup = get_keyboard ( )
)
2024-08-16 12:43:59 -05:00
async def clear ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
user_id = update . effective_user . id
if user_id in conversation_history :
del conversation_history [ user_id ]
2024-08-18 09:19:29 -05:00
for tool in tools :
tool . clear ( )
2024-08-17 18:32:08 -05:00
logging . info ( f " Cleared conversation history and image for user { user_id } " )
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( " Conversation history and image cleared. Let ' s start fresh! " , reply_markup = get_keyboard ( ) )
2024-08-17 09:28:17 -05:00
2024-08-18 15:57:45 -05:00
async def update_status_message ( context : ContextTypes . DEFAULT_TYPE , chat_id : int , message_id : int , status : str ) :
keyboard = [
[ InlineKeyboardButton ( " Abort " , callback_data = ' abort ' ) ]
]
reply_markup = InlineKeyboardMarkup ( keyboard )
await context . bot . edit_message_text (
chat_id = chat_id ,
message_id = message_id ,
text = f " Current status: { status } " ,
reply_markup = reply_markup
)
2024-08-16 12:43:59 -05:00
async def handle_message ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
try :
user_id = update . effective_user . id
user_message = update . message . text
2024-08-17 18:32:08 -05:00
logging . info ( f " Message from user { user_id } : { user_message } " )
2024-08-16 12:43:59 -05:00
if user_id not in conversation_history :
conversation_history [ user_id ] = [ ]
conversation_history [ user_id ] . append ( { " role " : " user " , " content " : user_message } )
2024-08-18 15:57:45 -05:00
# Send initial status message
status_message = await update . message . reply_text ( " Processing your request... " , reply_markup = InlineKeyboardMarkup ( [ [ InlineKeyboardButton ( " Abort " , callback_data = ' abort ' ) ] ] ) )
processing_status [ user_id ] = { " processing " : True , " message_id " : status_message . message_id }
2024-08-18 08:55:22 -05:00
messages = conversation_history [ user_id ]
2024-08-16 12:43:59 -05:00
2024-08-18 14:19:41 -05:00
response = get_chat_response ( messages )
tool_calls = [ ]
2024-08-18 17:18:24 -05:00
fullMessage = [ ]
for message_part in response . content :
fullMessage . append ( message_part )
if message_part . type == " tool_use " :
tool_calls . append ( message_part )
messages . append ( { " role " : " assistant " , " content " : fullMessage } )
2024-08-16 12:43:59 -05:00
2024-08-18 10:40:59 -05:00
toolUseCount = 0
2024-08-16 12:43:59 -05:00
2024-08-18 15:57:45 -05:00
while len ( tool_calls ) > 0 and toolUseCount < 50 and processing_status [ user_id ] [ " processing " ] :
2024-08-18 15:54:57 -05:00
tool_use_results = [ ]
while len ( tool_calls ) > 0 :
tool_call = tool_calls . pop ( 0 )
function_name = tool_call . name
2024-08-18 15:57:45 -05:00
# Update status message
await update_status_message ( context , update . effective_chat . id , status_message . message_id , f " Using tool: { function_name } " )
2024-08-18 15:54:57 -05:00
tool_response = call_tool ( tool_call )
tool_use_results . append ( { " type " : " tool_result " , " tool_use_id " : tool_call . id , " content " : json . dumps ( tool_response ) } )
2024-08-18 14:19:41 -05:00
2024-08-18 17:18:24 -05:00
formatted_result = { " role " : " user " , " content " : tool_use_results }
2024-08-18 14:19:41 -05:00
2024-08-18 12:16:03 -05:00
messages . append ( formatted_result )
2024-08-18 10:40:59 -05:00
2024-08-18 14:19:41 -05:00
response = get_chat_response ( messages )
2024-08-18 17:18:24 -05:00
fullMessage = [ ]
for message_part in response . content :
fullMessage . append ( message_part )
if message_part . type == " tool_use " :
tool_calls . append ( message_part )
messages . append ( { " role " : " assistant " , " content " : fullMessage } )
2024-08-18 10:40:59 -05:00
toolUseCount + = 1
2024-08-18 14:19:41 -05:00
if ( toolUseCount == 0 ) :
2024-08-18 17:18:24 -05:00
assistant_reply = response . content
2024-08-18 12:16:03 -05:00
conversation_history [ user_id ] . append ( { " role " : " assistant " , " content " : assistant_reply } )
2024-08-18 10:40:59 -05:00
if len ( conversation_history [ user_id ] ) > 20 :
conversation_history [ user_id ] = conversation_history [ user_id ] [ - 20 : ]
2024-08-18 15:57:45 -05:00
# Remove the status message
await context . bot . delete_message ( chat_id = update . effective_chat . id , message_id = status_message . message_id )
del processing_status [ user_id ]
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( messages [ - 1 ] [ " content " ] [ 0 ] . text , reply_markup = get_keyboard ( ) )
2024-08-16 12:43:59 -05:00
except Exception as e :
2024-08-17 18:31:53 -05:00
logging . error ( f " An error occurred: { str ( e ) } " )
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( " Sorry, an error occurred while processing your request. " , reply_markup = get_keyboard ( ) )
2024-08-16 12:43:59 -05:00
2024-08-18 07:35:52 -05:00
def call_tool ( function_call ) :
2024-08-18 17:18:24 -05:00
function_name = function_call . name
function_args = json . dumps ( function_call . input )
2024-08-17 13:00:37 -05:00
for tool in tools :
if function_name in [ f [ " name " ] for f in tool . get_functions ( ) ] :
2024-08-18 14:19:41 -05:00
return tool . execute ( function_name , * * json . loads ( function_args ) )
def get_chat_response ( messages ) :
2024-08-18 17:18:24 -05:00
return get_claude_response ( messages )
2024-08-18 14:19:41 -05:00
def get_claude_response ( messages ) :
anthropic_tools = [
{
" name " : function [ ' name ' ] ,
" description " : function [ ' description ' ] ,
" input_schema " : function [ ' parameters ' ] if function [ ' parameters ' ] not in [ None , { } ] else { " type " : " object " , " properties " : { " param1 " : { " type " : " string " , " description " : " Unnecessary " } } , " required " : [ ] }
}
for function in functions
]
try :
response = anthropic_client . messages . create (
model = " claude-3-5-sonnet-20240620 " ,
system = system_prompt ,
messages = messages ,
max_tokens = 8192 ,
tools = anthropic_tools
)
except Exception as e :
logging . error ( f " An error occurred: { str ( e ) } " )
return None
return response
2024-08-18 07:57:18 -05:00
2024-08-18 07:35:52 -05:00
async def status ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( " Currently using claude-3-5-sonnet-20240620 " , reply_markup = get_keyboard ( ) )
2024-08-18 07:35:52 -05:00
2024-08-18 15:57:45 -05:00
async def abort_processing ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
query = update . callback_query
await query . answer ( )
user_id = query . from_user . id
if user_id in processing_status :
processing_status [ user_id ] [ " processing " ] = False
await context . bot . edit_message_text (
chat_id = query . message . chat_id ,
message_id = query . message . message_id ,
text = " Processing aborted. "
)
await clear ( update , context )
else :
await query . edit_message_text ( text = " No active processing to abort. " )
2024-08-18 16:53:54 -05:00
async def handover ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
if context . bot . token == DAEMON_BOT_TOKEN :
# Daemon bot initiating handover
apprentice_chat_id = os . getenv ( ' APPRENTICE_CHAT_ID ' )
await context . bot . send_message ( chat_id = apprentice_chat_id , text = " Handover initiated. Taking control. " )
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( " Handover initiated. Apprentice bot is now in control. " , reply_markup = get_keyboard ( ) )
2024-08-18 16:53:54 -05:00
else :
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( " Handover can only be initiated by the daemon bot. " , reply_markup = get_keyboard ( ) )
2024-08-18 16:53:54 -05:00
async def update_apprentice ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
if context . bot . token == APPRENTICE_BOT_TOKEN :
try :
# Pull latest changes
subprocess . run ( [ " git " , " pull " , " origin " , " main " ] , check = True )
# Restart the bot
os . execv ( sys . executable , [ ' python ' ] + sys . argv )
except subprocess . CalledProcessError as e :
logging . error ( f " Failed to pull latest changes: { e } " )
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( " Failed to update. Please check the logs. " , reply_markup = get_keyboard ( ) )
2024-08-18 16:53:54 -05:00
except Exception as e :
logging . error ( f " Failed to restart the bot: { e } " )
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( " Failed to restart. Please check the logs. " , reply_markup = get_keyboard ( ) )
2024-08-18 16:53:54 -05:00
else :
2024-08-18 17:25:30 -05:00
await update . message . reply_text ( " Update can only be performed by the apprentice bot. " , reply_markup = get_keyboard ( ) )
2024-08-18 16:53:54 -05:00
async def check_for_updates ( context : ContextTypes . DEFAULT_TYPE ) - > None :
url = f " https://api.github.com/repos/ { GITHUB_REPO_OWNER } / { GITHUB_REPO_NAME } /pulls "
headers = { " Authorization " : f " token { GITHUB_ACCESS_TOKEN } " }
response = requests . get ( url , headers = headers )
if response . status_code == 200 :
pull_requests = response . json ( )
for pr in pull_requests :
if pr [ ' state ' ] == ' closed ' and pr [ ' merged ' ] :
# A pull request was merged, update the apprentice bot
await context . bot . send_message ( chat_id = os . getenv ( ' APPRENTICE_CHAT_ID ' ) , text = " A new update is available. Updating now... " )
await update_apprentice ( None , context )
break
2024-08-16 12:43:59 -05:00
def main ( ) - > None :
# Create the Application and pass it your bot's token
2024-08-18 16:53:54 -05:00
daemon_app = Application . builder ( ) . token ( DAEMON_BOT_TOKEN ) . build ( )
apprentice_app = Application . builder ( ) . token ( APPRENTICE_BOT_TOKEN ) . build ( )
# Add handlers for both bots
for app in [ daemon_app , apprentice_app ] :
app . add_handler ( CommandHandler ( " start " , start ) )
app . add_handler ( CommandHandler ( " clear " , clear ) )
app . add_handler ( CommandHandler ( " status " , status ) )
2024-08-18 17:25:30 -05:00
app . add_handler ( CommandHandler ( " reset " , clear ) ) # Use clear function for /reset command
2024-08-18 16:53:54 -05:00
app . add_handler ( MessageHandler ( filters . TEXT & ~ filters . COMMAND , handle_message ) )
app . add_handler ( CallbackQueryHandler ( abort_processing , pattern = ' ^abort$ ' ) )
# Add handover command only to daemon bot
daemon_app . add_handler ( CommandHandler ( " handover " , handover ) )
# Add update command only to apprentice bot
apprentice_app . add_handler ( CommandHandler ( " update " , update_apprentice ) )
# Set up job queue to check for updates every 15 minutes
job_queue = apprentice_app . job_queue
job_queue . run_repeating ( check_for_updates , interval = 900 , first = 10 )
# Start both bots
logging . info ( " Bots are running... " )
daemon_app . run_polling ( )
apprentice_app . run_polling ( )
2024-08-16 12:43:59 -05:00
if __name__ == ' __main__ ' :
2024-08-17 13:00:37 -05:00
main ( )