AI-Discord-Bot/src/bot.py

276 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# bot.py
import time
import os
import discord
import yaml
import random
from dotenv import load_dotenv
from textwrap import wrap
from discord.ext import commands
from discord.ext.commands import (
cooldown,
BucketType,
CooldownMapping,
CommandOnCooldown
)
# Local imports
from scheduler import start_scheduler
from profilepic import set_avatar_from_bytes
from context import fetch_recent_context, format_context
from user_profiles import (
load_user_profile,
update_last_seen,
increment_interactions,
format_profile_for_block,
set_pronouns,
set_custom_prompt
)
from personality import apply_personality, set_persona
from logger import setup_logger
from ai import (
unload_model,
load_model,
get_current_model,
get_ai_response,
TAGS_ENDPOINT
)
from time_logger import log_message_activity
from autochat import should_auto_reply, generate_auto_reply, update_reply_timer
debug_mode = os.getenv("DEBUG_MODE", "false").lower() == "true"
from user_profiles import format_profile_for_block as format_user_profile_block
# Setup logger and environment
logger = setup_logger("bot")
dotenv_path = os.path.join(os.path.dirname(__file__), '..', '.env')
load_dotenv(dotenv_path)
# Load model settings
MODEL_NAME = os.getenv("MODEL_NAME", "llama3:latest")
logger.info(f"🔍 Loaded MODEL_NAME from .env: {MODEL_NAME}")
if debug_mode:
logger.info(f"🧹 Attempting to clear VRAM before loading {MODEL_NAME}...")
unload_model(MODEL_NAME)
if load_model(MODEL_NAME):
logger.info(f"🚀 Model `{MODEL_NAME}` preloaded on startup.")
else:
logger.warning(f"⚠️ Failed to preload model `{MODEL_NAME}`.")
logger.info(f"✅ Final model in use: {MODEL_NAME}")
# Load YAML settings
base_dir = os.path.dirname(__file__)
settings_path = os.path.join(base_dir, "settings.yml")
with open(settings_path, "r", encoding="utf-8") as f:
settings = yaml.safe_load(f)
ROAST_COOLDOWN_SECONDS = settings["cooldowns"]["roast"]
GLOBAL_COOLDOWN_SECONDS = settings["cooldowns"]["global"]
COOLDOWN_MSG_TEMPLATE = settings["messages"]["cooldown"]
# Configure Discord bot
TOKEN = os.getenv("DISCORD_TOKEN")
if not TOKEN:
logger.error("❌ DISCORD_TOKEN not set in .env file.")
raise SystemExit("DISCORD_TOKEN not set.")
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
# Handle cooldown errors globally
@bot.event
async def on_command_error(ctx, error):
if isinstance(error, CommandOnCooldown):
retry_secs = round(error.retry_after, 1)
template = random.choice(COOLDOWN_MSG_TEMPLATE) if isinstance(COOLDOWN_MSG_TEMPLATE, list) else COOLDOWN_MSG_TEMPLATE
msg = template.replace("{seconds}", str(retry_secs))
logger.info(f"Command {ctx.command} on cooldown. Retry after {retry_secs} seconds.")
await ctx.send(msg)
else:
raise error
# Global cooldown
global_cooldown = CooldownMapping.from_cooldown(1, GLOBAL_COOLDOWN_SECONDS, BucketType.user)
@bot.check
async def global_command_cooldown(ctx):
bucket = global_cooldown.get_bucket(ctx.message)
retry_after = bucket.update_rate_limit()
if retry_after:
raise CommandOnCooldown(bucket, retry_after, BucketType.user)
return True
# Handle direct bot mentions
@bot.event
async def on_message(message):
if not message.author.bot:
log_message_activity(message)
# Ignore messages from the bot itself
if message.author == bot.user:
return
# Only respond if the bot is mentioned
if bot.user.mentioned_in(message):
# Strip the mention from the message to extract the actual prompt
prompt = message.content.replace(f"<@{bot.user.id}>", "").strip()
if not prompt:
return # Nothing to respond to
# Update the user's interaction history
user_id = str(message.author.id)
update_last_seen(user_id)
profile = load_user_profile(message.author)
# Log summary info about the profile (but dont inject it)
logger.info("=" * 60 + " AI Response " + "=" * 60)
logger.info(f"🧠 Profile loaded for {profile['display_name']} (interactions: {profile['interactions']})")
# Fetch recent messages for conversation context
context_msgs = await fetch_recent_context(message.channel)
formatted_context = format_context(context_msgs)
# Log number of context messages, not the entire block (to reduce clutter)
logger.info(f"📚 Retrieved {len(context_msgs)} messages for context")
# Let ai.py handle all prompt construction (persona + context + profile)
async with message.channel.typing():
reply = get_ai_response(prompt, context=formatted_context, user_profile=profile)
await message.channel.send(reply)
elif should_auto_reply():
reply = await generate_auto_reply(message, bot)
if reply and len(reply) > 5:
await message.channel.send(reply)
update_reply_timer()
# Ensure bot commands still work (!setprompt, !roast, etc.)
await bot.process_commands(message)
# Bot startup event
@bot.event
async def on_ready():
print(f"✅ Logged in as {bot.user.name}")
logger.info(f"Logged in as {bot.user.name}")
for guild in bot.guilds:
me = guild.me
if me.nick != "Delta":
try:
await me.edit(nick="Delta")
logger.info(f"🔄 Renamed self to Delta in {guild.name}")
except Exception as e:
logger.warning(f"⚠️ Failed to rename in {guild.name}: {e}")
bot.loop.create_task(start_scheduler(bot))
# Commands
@bot.command(name="setprompt")
async def set_prompt_cmd(ctx, *, prompt):
set_custom_prompt(ctx.author.id, prompt)
await ctx.send("✅ Custom prompt saved.")
@bot.command(name="setpronouns")
async def set_pronouns_cmd(ctx, *, pronouns):
success = set_pronouns(ctx.author, pronouns)
if success:
await ctx.send(f"✅ Got it, {ctx.author.display_name}! Your pronouns have been updated.")
else:
await ctx.send("⚠️ Failed to update pronouns. Try interacting with Delta first to generate your profile.")
@bot.command()
async def ping(ctx):
await ctx.send("🏓 Pong!")
@bot.command()
async def chat(ctx, *, prompt):
await ctx.send("🤖 Thinking...")
reply = get_ai_response(prompt)
for chunk in wrap(reply, 2000):
await ctx.send(chunk)
@bot.command()
async def setpersona(ctx, *, description):
set_persona(description)
await ctx.send("✅ Persona updated! New style will be used in replies.")
@bot.command(name='roast')
@cooldown(rate=1, per=ROAST_COOLDOWN_SECONDS, type=BucketType.user)
async def roast(ctx):
target = ctx.message.mentions[0].mention if ctx.message.mentions else ctx.author.mention
prompt = f"Roast {target}. Be dramatic, insulting, and sarcastic. Speak in your usual chaotic RGB catgirl personality."
response = get_ai_response(prompt)
await ctx.send(f"😼 {response}")
@bot.command(name="clearmodel")
async def clear_model(ctx):
model = get_current_model()
success = unload_model(model)
msg = f"✅ Unloaded model: `{model}`" if success else f"❌ Failed to unload model: `{model}`"
await ctx.send(msg)
@bot.command(name="model")
async def current_model(ctx):
model = get_current_model()
await ctx.send(f"📦 Current model: `{model}`")
@bot.command(name="setmodel")
async def set_model(ctx, *, model_name):
current_model = get_current_model()
if model_name == current_model:
return await ctx.send(f"⚠️ `{model_name}` is already active.")
await ctx.send(f"🔄 Switching from `{current_model}` to `{model_name}`…")
if unload_model(current_model):
await ctx.send(f"🧽 Unloaded `{current_model}` from VRAM.")
else:
await ctx.send(f"⚠️ Couldnt unload `{current_model}`.")
if not load_model(model_name):
return await ctx.send(f"❌ Failed to pull `{model_name}`.")
os.environ["MODEL_NAME"] = model_name
env_path = os.path.join(os.path.dirname(__file__), '..', '.env')
lines = []
with open(env_path, 'r', encoding='utf-8') as f:
for line in f:
lines.append(f"MODEL_NAME={model_name}\n" if line.startswith("MODEL_NAME=") else line)
with open(env_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
await ctx.send(f"✅ Model switched to `{model_name}` and `.env` updated.")
@bot.command(name="models")
async def list_models(ctx):
import requests
try:
resp = requests.get(TAGS_ENDPOINT)
models = [m["name"] for m in resp.json().get("models", [])]
if models:
await ctx.send("🧠 Available models:\n" + "\n".join(f"- `{m}`" for m in models))
else:
await ctx.send("❌ No models found.")
except Exception as e:
await ctx.send(f"❌ Failed to fetch models: {e}")
@bot.command(name="setavatar")
@commands.is_owner()
async def set_avatar(ctx):
if not ctx.message.attachments:
return await ctx.send("❌ Please attach an image (PNG) to use as the new avatar.")
image = ctx.message.attachments[0]
image_bytes = await image.read()
token = os.getenv("DISCORD_TOKEN")
if not token:
return await ctx.send("❌ Bot token not found in environment.")
success = set_avatar_from_bytes(image_bytes, token)
await ctx.send("✅ Avatar updated successfully!" if success else "❌ Failed to update avatar.")
# Run bot
bot.run(TOKEN)