Compare commits
2 commits
69354229d6
...
863d3e3c88
| Author | SHA1 | Date | |
|---|---|---|---|
| 863d3e3c88 | |||
| 864201214d |
9 changed files with 1189 additions and 18 deletions
1042
bot.log
1042
bot.log
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
src/__pycache__/context.cpython-310.pyc
Normal file
BIN
src/__pycache__/context.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/profilepic.cpython-310.pyc
Normal file
BIN
src/__pycache__/profilepic.cpython-310.pyc
Normal file
Binary file not shown.
24
src/ai.py
24
src/ai.py
|
|
@ -82,23 +82,33 @@ def get_current_model():
|
||||||
return get_model_name()
|
return get_model_name()
|
||||||
|
|
||||||
# Main LLM interaction — injects personality and sends prompt to Ollama
|
# 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()
|
model_name = get_model_name()
|
||||||
load_model(model_name) # Ensures the model is pulled and ready
|
load_model(model_name)
|
||||||
|
|
||||||
persona = load_persona()
|
persona = load_persona()
|
||||||
|
full_prompt = ""
|
||||||
|
|
||||||
|
# Inject persona first if available
|
||||||
if persona:
|
if persona:
|
||||||
# Clean fancy quotes and build final prompt with character injection
|
|
||||||
safe_inject = persona["prompt_inject"].replace("“", "\"").replace("”", "\"").replace("’", "'")
|
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:
|
else:
|
||||||
full_prompt = user_prompt # fallback to raw prompt if no persona loaded
|
full_prompt += user_prompt
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": model_name, # 🔧 Suggested fix: previously hardcoded to MODEL_NAME
|
"model": model_name,
|
||||||
"prompt": full_prompt,
|
"prompt": full_prompt,
|
||||||
"stream": False
|
"stream": False
|
||||||
# optional: add "keep_alive": 300 to keep model warm
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("🛰️ SENDING TO OLLAMA /generate")
|
logger.info("🛰️ SENDING TO OLLAMA /generate")
|
||||||
|
|
|
||||||
68
src/bot.py
68
src/bot.py
|
|
@ -9,10 +9,12 @@ from dotenv import load_dotenv
|
||||||
import random
|
import random
|
||||||
import yaml
|
import yaml
|
||||||
from scheduler import start_scheduler
|
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
|
from logger import setup_logger
|
||||||
logger = setup_logger("bot")
|
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')
|
dotenv_path = os.path.join(os.path.dirname(__file__), '..', '.env')
|
||||||
load_dotenv(dotenv_path)
|
load_dotenv(dotenv_path)
|
||||||
|
|
@ -33,16 +35,6 @@ else:
|
||||||
|
|
||||||
logger.info(f"✅ Final model in use: {MODEL_NAME}")
|
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 personality import apply_personality, set_persona
|
||||||
from discord.ext.commands import (
|
from discord.ext.commands import (
|
||||||
cooldown,
|
cooldown,
|
||||||
|
|
@ -95,6 +87,41 @@ async def global_command_cooldown(ctx):
|
||||||
raise CommandOnCooldown(bucket, retry_after, BucketType.user)
|
raise CommandOnCooldown(bucket, retry_after, BucketType.user)
|
||||||
return True
|
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()
|
@bot.command()
|
||||||
async def ping(ctx):
|
async def ping(ctx):
|
||||||
await ctx.send("🏓 Pong!")
|
await ctx.send("🏓 Pong!")
|
||||||
|
|
@ -200,6 +227,25 @@ async def list_models(ctx):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(f"❌ Failed to fetch models: {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
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
print(f"✅ Logged in as {bot.user.name}")
|
print(f"✅ Logged in as {bot.user.name}")
|
||||||
|
|
|
||||||
40
src/context.py
Normal file
40
src/context.py
Normal 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
29
src/profilepic.py
Normal 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
|
||||||
|
|
@ -6,6 +6,10 @@ messages:
|
||||||
cooldown:
|
cooldown:
|
||||||
- "🕒 Chill, wait {seconds}s before trying again."
|
- "🕒 Chill, wait {seconds}s before trying again."
|
||||||
|
|
||||||
|
context:
|
||||||
|
enabled: true
|
||||||
|
max_messages: 30
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
enabled: false
|
enabled: false
|
||||||
mode: simple # <- this activates simple mode
|
mode: simple # <- this activates simple mode
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue