major ai update

This commit is contained in:
esef committed 2026-06-29 18:42:37 +03:00
1 parent 1283569622
commit d338e474b1
10 files changed
+524 -1

No files matched your search

+321
View File
@@ -0,0 +1,321 @@
import os
import re
import time
import asyncio
import discord
from discord.ext import commands
from collections import defaultdict, deque
from openai import AsyncOpenAI
#THIS IS DEEPSEEK-V4-FLASH, BUT CAN SWITCH TO V4-PRO
#only last 12 messages on discord will be fed into the ai's memory
MEMORY_SIZE = 12
#the very much needed ai shit
#loads persona file
def load_persona(name, tier='flash'):
suffix = '_prompt_pro.txt' if tier == 'pro' else '_prompt.txt'
path = os.path.join(os.path.dirname(__file__), 'personalities', f'{name.lower()}{suffix}')
if not os.path.exists(path):
# fallback to base prompt if pro version doesn't exist yet
path = os.path.join(os.path.dirname(__file__), 'personalities', f'{name.lower()}_prompt.txt')
if not os.path.exists(path):
return None
with open(path, 'r', encoding='utf-8') as f:
return f.read().strip()
#this command lists all personas
def list_personas():
names = []
persona_dir = os.path.join(os.path.dirname(__file__), 'personalities')
if os.path.exists(persona_dir):
for fname in os.listdir(persona_dir):
# only show base prompts, not _pro variants
if fname.endswith('_prompt.txt') and '_prompt_pro' not in fname:
names.append(fname.replace('_prompt.txt', ''))
return sorted(names)
#this splits messages into chunks if they are too long (optimization)
def split_msg(text, limit=2000):
chunks = []
while len(text) > limit:
split_at = text.rfind('\n', 0, limit)
if split_at == -1:
split_at = limit
chunks.append(text[:split_at])
text = text[split_at:].lstrip('\n')
if text:
chunks.append(text)
return chunks
class AIRoleplay(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.client = AsyncOpenAI(
api_key=os.getenv('DEEPSEEK_API'),
base_url='https://api.deepseek.com'
)
self.allowed_users = set()
for key in ('OWNER_ID', 'SIDE_OWNER_ID'):
val = os.getenv(key)
if val:
try:
self.allowed_users.add(int(val))
except ValueError:
pass
self.memory = defaultdict(lambda: deque(maxlen=MEMORY_SIZE))
self.active_personas = {}
self.prompts = {}
self.input_tokens = 0
self.output_tokens = 0
self.session_start = time.time()
self.webhooks = {}
self.active_model = 'deepseek-v4-flash'
self.active_channels = set()
#only accepts me and hizuki
def is_allowed(self, user):
return user.id in self.allowed_users
#this loads the persona file
def get_prompt(self, channel_id):
tier = 'pro' if 'pro' in self.active_model else 'flash'
cache_key = (channel_id, tier)
if cache_key not in self.prompts:
name = self.active_personas.get(channel_id, 'yuuka')
prompt = load_persona(name, tier)
if not prompt:
prompt = load_persona('yuuka', tier) or ''
self.prompts[cache_key] = prompt
return self.prompts[cache_key]
#this gets the channel webhook, creates one if it doesnt exist
async def get_webhook(self, channel):
if channel.id in self.webhooks:
return self.webhooks[channel.id]
try:
whs = await channel.webhooks()
wh = next((w for w in whs if w.name == 'YuukaAI'), None)
if not wh:
wh = await channel.create_webhook(name='YuukaAI')
self.webhooks[channel.id] = wh
return wh
except discord.Forbidden:
return None
#this sends the message as the persona, with vision support (if images are present)
async def send_as_char(self, message, text, persona):
avatar_path = os.path.join(os.path.dirname(__file__), 'personalities', f'{persona.lower()}_avatar.png')
avatar_bytes = None
if os.path.exists(avatar_path):
with open(avatar_path, 'rb') as f:
avatar_bytes = f.read()
wh = await self.get_webhook(message.channel)
chunks = split_msg(text)
if wh:
display_name = persona.capitalize()
for i, chunk in enumerate(chunks):
kwargs = {'content': chunk, 'username': display_name, 'wait': True}
if i == 0 and avatar_bytes:
kwargs['avatar'] = avatar_bytes
await wh.send(**kwargs)
else:
first = True
for chunk in chunks:
if first:
await message.reply(chunk)
first = False
else:
await message.channel.send(chunk)
#this is the main listener, it handles all messages
@commands.Cog.listener()
async def on_message(self, message):
if message.author.bot or not message.guild:
return
mentioned = self.bot.user in message.mentions
in_active = message.channel.id in self.active_channels
# reject non-allowed users who actively ping the bot
if mentioned and not self.is_allowed(message.author):
await message.reply("You dont have access to AI features")
return
# determine if we should respond at all
if not mentioned and not in_active:
return
# in active mode: skip commands and ignore non-allowed users silently
if in_active and not mentioned:
if not self.is_allowed(message.author):
return
if message.content.startswith('!'):
return
#this removes the mention from the message
content = re.sub(r'<@!?' + str(self.bot.user.id) + r'>', '', message.content).strip()
if not content:
return
cid = message.channel.id
mem = self.memory[cid]
persona = self.active_personas.get(cid, 'yuuka')
prompt = self.get_prompt(cid)
# VISION SUPPORT
user_content = [{"type": "text", "text": f'{message.author.display_name}: {content}'}]
has_images = False
for att in message.attachments:
if att.content_type and att.content_type.startswith('image/'):
has_images = True
user_content.append({
"type": "image_url",
"image_url": {"url": att.url}
})
# DeepSeek and older models prefer pure strings if there are no images
final_content = user_content if has_images else user_content[0]["text"]
mem.append({'role': 'user', 'content': final_content})
api_msgs = [{'role': 'system', 'content': prompt}] + list(mem)
model_to_use = self.active_model
#VISION SUPPORT END
#this sends the message to the ai and handles the response
async with message.channel.typing():
try:
resp = await self.client.chat.completions.create(
model=model_to_use,
messages=api_msgs,
max_tokens=1024
)
except Exception as e:
await message.reply(f'api error: {e}')
mem.pop()
return
reply = resp.choices[0].message.content.strip()
if resp.usage:
self.input_tokens += resp.usage.prompt_tokens
self.output_tokens += resp.usage.completion_tokens
mem.append({'role': 'assistant', 'content': reply})
# simple typing delay calculation for better rp
words = len(reply.split())
delay = max(1.5, min(words / 60 * 2, 5.0))
await asyncio.sleep(delay)
await self.send_as_char(message, reply, persona)
#this command lists all the ai commands, not an actual ai command##
@commands.command()
async def ai(self, ctx):
ai_commands = [f'!{cmd.name}' for cmd in self.get_commands()]
await ctx.reply('\n'.join(ai_commands))
######################################################################
#this sets the persona, only usable by me and hizu
@commands.command()
async def setpersona(self, ctx, name: str):
if not self.is_allowed(ctx.author):
return
prompt = load_persona(name)
if not prompt:
avail = ', '.join(list_personas()) or 'none'
return await ctx.reply(f'couldnt find {name}. available: {avail}')
self.active_personas[ctx.channel.id] = name.lower()
self.prompts.pop(ctx.channel.id, None)
self.memory[ctx.channel.id].clear()
await ctx.reply(f'switched to {name} and wiped memory')
#this clears the memory of the ai, only usable by me and hizu
@commands.command()
async def clearchat(self, ctx):
if not self.is_allowed(ctx.author):
return
self.memory[ctx.channel.id].clear()
await ctx.reply('memory cleared')
#this enables active chat, only usable by me and hizu
@commands.command()
async def activechat(self, ctx):
if not self.is_allowed(ctx.author):
return
self.active_channels.add(ctx.channel.id)
await ctx.reply('active chat enabled — no need to ping, just talk')
#this disables active chat, only usable by me and hizu
@commands.command()
async def stopchat(self, ctx):
if not self.is_allowed(ctx.author):
return
self.active_channels.discard(ctx.channel.id)
await ctx.reply('active chat disabled')
#this lists all the personas, only usable by me and hizu
@commands.command()
async def personas(self, ctx):
if not self.is_allowed(ctx.author):
return
names = list_personas()
current = self.active_personas.get(ctx.channel.id, 'yuuka')
lines = [f'- {n} {"(active)" if n == current else ""}' for n in names]
await ctx.reply('**personas:**\n' + '\n'.join(lines) if lines else 'none found')
#this shows the current token usage, only usable by me and hizu
@commands.command()
async def aicost(self, ctx):
if not self.is_allowed(ctx.author):
return
elapsed = time.time() - self.session_start
hours, r = divmod(int(elapsed), 3600)
mins, _ = divmod(r, 60)
cost_in = self.input_tokens * (0.14 / 1_000_000)
cost_out = self.output_tokens * (0.28 / 1_000_000)
total = cost_in + cost_out
embed = discord.Embed(title='ai usage', color=0x00ff00)
embed.add_field(name='uptime', value=f'{hours}h {mins}m', inline=False)
embed.add_field(name='tokens', value=f'in: {self.input_tokens:,}\nout: {self.output_tokens:,}', inline=True)
embed.add_field(name='cost', value=f'~${total:.5f}', inline=True)
await ctx.reply(embed=embed)
#THIS IS WHERE TURBO COMMANDS LIVE IN
@commands.command()
async def turbo(self, ctx):
if not self.is_allowed(ctx.author):
return
if self.active_model == 'deepseek-v4-flash':
self.active_model = 'deepseek-v4-pro'
self.prompts.clear()
await ctx.reply('switched to **deepseek-v4-pro**')
else:
self.active_model = 'deepseek-v4-flash'
self.prompts.clear()
await ctx.reply('switched to **deepseek-v4-flash**')
#this shows the current model, only usable by me and hizu
@commands.command()
async def current(self, ctx):
if not self.is_allowed(ctx.author):
return
await ctx.reply(f'currently using: **{self.active_model}**')
async def setup(bot):
await bot.add_cog(AIRoleplay(bot))