Compare commits

...

2 commits

Author SHA1 Message Date
863d3e3c88 Implemented feature #7 2025-05-14 20:27:49 -04:00
864201214d Added features #8 and #31 2025-05-14 16:01:12 -04:00
9 changed files with 1189 additions and 18 deletions

1042
bot.log

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -82,23 +82,33 @@ def get_current_model():
return get_model_name()
# Main LLM interaction — injects personality and sends prompt to Ollama
def get_ai_response(user_prompt):
def get_ai_response(user_prompt, context=None):
model_name = get_model_name()
load_model(model_name) # Ensures the model is pulled and ready
load_model(model_name)
persona = load_persona()
full_prompt = ""
# Inject persona first if available
if persona:
# Clean fancy quotes and build final prompt with character injection
safe_inject = persona["prompt_inject"].replace("", "\"").replace("", "\"").replace("", "'")
full_prompt = f"{safe_inject}\nUser: {user_prompt}\n{persona['name']}:"
full_prompt += f"{safe_inject}\n"
# Add recent conversation context, if available
if context:
logger.info("🧠 Injected context block (pre-prompt):\n" + context)
full_prompt += f"[Recent Conversation]\n{context}\n\n"
# Add user prompt + character or plain ending
if persona:
full_prompt += f"User: {user_prompt}\n{persona['name']}:"
else:
full_prompt = user_prompt # fallback to raw prompt if no persona loaded
full_prompt += user_prompt
payload = {
"model": model_name, # 🔧 Suggested fix: previously hardcoded to MODEL_NAME
"model": model_name,
"prompt": full_prompt,
"stream": False
# optional: add "keep_alive": 300 to keep model warm
}
logger.info("🛰️ SENDING TO OLLAMA /generate")

View file

@ -9,10 +9,12 @@ from dotenv import load_dotenv
import random
import yaml
from scheduler import start_scheduler
from profilepic import set_avatar_from_bytes
from context import fetch_recent_context, format_context
from logger import setup_logger
logger = setup_logger("bot")
from ai import unload_model, load_model, get_current_model
from ai import unload_model, load_model, get_current_model, get_ai_response
dotenv_path = os.path.join(os.path.dirname(__file__), '..', '.env')
load_dotenv(dotenv_path)
@ -33,16 +35,6 @@ else:
logger.info(f"✅ Final model in use: {MODEL_NAME}")
from ai import get_ai_response, load_model
MODEL_NAME = os.getenv("MODEL_NAME", "llama3:latest")
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}")
from personality import apply_personality, set_persona
from discord.ext.commands import (
cooldown,
@ -95,6 +87,41 @@ async def global_command_cooldown(ctx):
raise CommandOnCooldown(bucket, retry_after, BucketType.user)
return True
@bot.event
async def on_message(message):
if message.author == bot.user:
return
if bot.user.mentioned_in(message):
prompt = message.content.replace(f"<@{bot.user.id}>", "").strip()
context_msgs = await fetch_recent_context(message.channel)
formatted_context = format_context(context_msgs)
logger.info("🧠 Injected context block:\n" + formatted_context)
async with message.channel.typing():
reply = get_ai_response(prompt, context=formatted_context)
await message.channel.send(reply)
await bot.process_commands(message)
@bot.event
async def on_ready():
print(f"✅ Logged in as {bot.user.name}")
logger.info(f"Logged in as {bot.user.name}")
# Optional: rename itself in servers (if it has permission)
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))
@bot.command()
async def ping(ctx):
await ctx.send("🏓 Pong!")
@ -200,6 +227,25 @@ async def list_models(ctx):
except Exception as e:
await ctx.send(f"❌ Failed to fetch models: {e}")
@bot.command(name="setavatar")
@commands.is_owner() # Only the bot owner can run this
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)
if success:
await ctx.send("✅ Avatar updated successfully!")
else:
await ctx.send("❌ Failed to update avatar.")
@bot.event
async def on_ready():
print(f"✅ Logged in as {bot.user.name}")

40
src/context.py Normal file
View file

@ -0,0 +1,40 @@
# context.py
import os
import yaml
import discord
base_dir = os.path.dirname(__file__)
with open(os.path.join(base_dir, "settings.yml"), "r", encoding="utf-8") as f:
settings = yaml.safe_load(f)
CONTEXT_LIMIT = settings["context"].get("max_messages", 15)
async def fetch_recent_context(channel, limit=CONTEXT_LIMIT):
messages = []
async for message in channel.history(limit=100):
# Skip other bots (but not Delta herself)
if message.author.bot and message.author.id != channel.guild.me.id:
continue
raw = message.clean_content
clean = raw.strip().replace("\n", " ").replace("\r", "")
clean = " ".join(clean.split()) # Collapse all extra whitespace
if not clean:
continue
if clean.startswith("!"):
continue
line = f"{message.created_at.strftime('%Y-%m-%d %H:%M')} - {message.author.display_name}: {clean}"
messages.append(line)
if len(messages) >= limit:
break
messages.reverse()
return messages
def format_context(lines: list[str]) -> str:
return "\n".join(lines)

29
src/profilepic.py Normal file
View file

@ -0,0 +1,29 @@
# profilepic.py
import base64
import requests
import logging
import os
logger = logging.getLogger("bot")
DISCORD_API = "https://discord.com/api/v10"
def set_avatar_from_bytes(image_bytes: bytes, token: str) -> bool:
try:
b64_avatar = base64.b64encode(image_bytes).decode("utf-8")
payload = {
"avatar": f"data:image/png;base64,{b64_avatar}"
}
headers = {
"Authorization": f"Bot {token}",
"Content-Type": "application/json"
}
response = requests.patch(f"{DISCORD_API}/users/@me", json=payload, headers=headers)
logger.info(f"🖼️ Avatar update status: {response.status_code} - {response.text}")
return response.status_code == 200
except Exception as e:
logger.error(f"❌ Failed to update avatar: {str(e)}")
return False

View file

@ -6,6 +6,10 @@ messages:
cooldown:
- "🕒 Chill, wait {seconds}s before trying again."
context:
enabled: true
max_messages: 30
scheduler:
enabled: false
mode: simple # <- this activates simple mode