Skip to content

Commit

Permalink
What started off as a friendly auto-reply cmd turned into a full open…
Browse files Browse the repository at this point in the history
…ai integration...
  • Loading branch information
nickheyer committed Nov 28, 2024
1 parent eb8e319 commit 73c05b6
Show file tree
Hide file tree
Showing 18 changed files with 1,019 additions and 422 deletions.
3 changes: 0 additions & 3 deletions DiscoFlix/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@

from DiscoFlixClient.routing import websocket_urlpatterns as client_websocket


# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
Expand Down
2 changes: 2 additions & 0 deletions DiscoFlixBot/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Command:
aliases: Parsed commands allowed, ie: !df movie == !df film if ['movie', 'film'].
description: Description to be given in help menu.
requires_input: If command requires input following the command.
invokable: Can be called as a command directly, set to False when non-standard interpretation is required
conditions: Can take 3 different data-types as input...
- tuple: (function, arguments-for-function)
- function: function that returns truthy/falsey value
Expand All @@ -21,6 +22,7 @@ def __init__(self) -> None:
self.description = ''
self.requires_input = False
self.conditions = []
self.invokable = True

"""
--- CLASS METHOD ARGUMENTS DESCRIPTIONS --- [ SEE BELOW ]
Expand Down
2 changes: 1 addition & 1 deletion DiscoFlixBot/commands/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self) -> None:

async def execute(self, message, ctx):
registry = CommandRegistry()
all_commands = registry.all()
all_commands = registry.all(invokable=True)
commands = ListCommands(ctx, all_commands)
embed = await commands.generate_embed()
await message.reply(embed=embed)
64 changes: 64 additions & 0 deletions DiscoFlixBot/commands/mentioned.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from openai import AsyncOpenAI
import random
from DiscoFlixBot.base_command import Command

class MentionedCommand(Command):
# Special non-standard command registered for scenarios when the bot itself is mentioned

def __init__(self) -> None:
super().__init__()
self.name = "mentioned"
self.permissions = ["user", "developer"]
self.description = "A fairly simple interface for openai. Great for recommending movies and tv shows."
self.aliases = ["mentioned"]

async def execute(self, message, ctx):
content = message.content.strip()

general_responses = [
"You called? 🤔",
"I'm here! What do you need? 😄",
"At your service! 🛠️",
"Did someone summon me? 🧙‍♂️",
]
bot_response = random.choice(general_responses) # FALLBACK FOR THE MAJORITY THAT WONT USE THIS FEATURE
try:
token = getattr(ctx.config, 'openai_token', '')
if len(token) > 0 and getattr(ctx.config, 'is_openai_enabled', False):
# CHECK MENTIONS, CONVERT TO STR USERS
for user in message.mentions:
mention_str = f'<@{user.id}>'
content = content.replace(mention_str, f'@{user.name}')

# UNCLE GIPPITY
client = AsyncOpenAI(
api_key = token
)

chat_completion = await client.chat.completions.create(
model="gpt-3.5-turbo", #"gpt-4o-mini",
messages=[
{"role": "system",
"content": f"""You are a fun and clever discord chatbot named {ctx.bot.client.user}, specializing in movies, games, and TV shows,
Keep the tone engaging, clever, and fun. Your response will be displayed within a Discord chat message, so keep the format in mind.
"""
},
{
"role": "user",
"content": f"The following message was sent by a discord user: '{content}'\n"
}
], # PROMPT WAS ACTUALLY CO-AUTHORED BY CHAT GPT, I WONDER IF THIS IS EVEN GOOD.
max_tokens=150,
temperature=0.8,
n=1,
top_p=0.9,
frequency_penalty=0.2,
presence_penalty=0.6,
user=f'{ctx.username}'
)

bot_response = chat_completion.choices[0].message.content.strip()
except Exception as e:
print(f"Error with OpenAI API: {e}")

await message.channel.send(bot_response)
17 changes: 15 additions & 2 deletions DiscoFlixBot/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ def __init__(self, bot, message, config) -> None:


async def __parse_message(self):

req_parsing_steps = [ # WILL IGNORE MESSAGE ON FAILURE
self.__parse_sender,
self.__parse_prefix,
Expand All @@ -37,6 +36,9 @@ async def __parse_message(self):
(self.__parse_args, 'Failed to parse command arguments. Arguments invalid.'),
]

if self.__parse_bot_mentions():
return True

for step in req_parsing_steps:
result = await step() if asyncio.iscoroutinefunction(step) else step()
if not result:
Expand Down Expand Up @@ -65,13 +67,24 @@ def __parse_prefix(self):
self.split_message = self.message_content.strip().split()
return len(self.split_message) > 1 and self.split_message[0] == prefix

def __parse_bot_mentions(self):
client = self.bot.client.user
self.is_mentioned = client.mentioned_in(self.message) or client.name in self.message.content
if self.is_mentioned:
self.command = CommandRegistry().get('mentioned')
self.sender = self.message.author
self.username = str(self.message.author)
self.is_bot = self.sender.bot
return not self.is_bot
return False

def __parse_command(self):
if len(self.split_message) < 2:
return False
else:
registry = CommandRegistry()
cmd_str = self.split_message[1].lower()
cmd = registry.get(cmd_str)
cmd = registry.get(cmd_str, invokable=True)
if cmd:
self.command = cmd
return True
Expand Down
22 changes: 16 additions & 6 deletions DiscoFlixBot/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,23 @@ async def stub_callback(interaction: discord.Interaction):
pass
return stub_callback

def get(self, name):
command_dict = self.commands
def get_filtered_commands(self, **kwargs):
return {
name: cls_inst
for name, cls_inst in self.commands.items()
if all(getattr(cls_inst, key, None) == value for key, value in kwargs.items())
}

def get(self, name, **kwargs):
command_dict = self.get_filtered_commands(**kwargs)
command_list = "\n".join([f"{name} : {cls_inst.name}" for name, cls_inst in command_dict.items()])
print(f'GETTING ALL COMMANDS: {command_list}')
return self.commands.get(name)
return command_dict.get(name)

def all(self):
if not self.commands:
def all(self, **kwargs):
command_dict = self.get_filtered_commands(**kwargs)
if not command_dict:
return []
return set(self.commands.values())

return set(command_dict.values())

39 changes: 37 additions & 2 deletions DiscoFlixClient/consumers/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import os
import sys
import asyncio
import signal
import psutil

from django.core.serializers.json import DjangoJSONEncoder
from channels.generic.websocket import AsyncWebsocketConsumer
from django.core.management.color import color_style
from django.forms.models import model_to_dict
from django.core.management import call_command

from DiscoFlixBot.controller import (
main,
Expand All @@ -26,6 +29,7 @@
update_config,
get_config,
add_user,
add_log,
edit_user,
delete_user,
get_user,
Expand All @@ -42,6 +46,7 @@
validate_discord_token,
validate_sonarr,
validate_radarr,
validate_openai_token,
)


Expand Down Expand Up @@ -99,13 +104,30 @@ async def send_log(self, entry, num=100):
logs = await add_refresh_log(f"[CLIENT] {entry}", num)
return await self.emit({"event": "bot_log_added", "data": {"log": logs}})

async def shutdown_linux(self, *args, **kwargs):
try:
print('[CLIENT] INITIATING CLEANUP BEFORE SIGKILL...')
state = await get_state()
if state.discord_state:
await add_log('[BOT] SIGINT: BOT SHUTTING DOWN')
await add_log('[CLIENT] SIGINT: SERVER SHUTTING DOWN')
status = {"current_activity": 'Offline'}
await update_state(status)

logger.debug('SUCCESFUL SIGKILL SHUTDOWN')
os.system("pkill -9 -f 'daphne'")
os.system("pkill -9 -f 'poetry'")

except Exception as e:
print(f"Error during shutdown: {e}")

async def shutdown_server(self, *args, **kwargs):
await update_state({"discord_state": False, "current_activity": "Offline"})
if sys.platform in ["linux", "linux2"]:
os.system("pkill -f daphne")
await self.shutdown_linux(*args, **kwargs)
else:
os.system("pkill -f django")
sys.exit()
sys.exit()

async def update_client(self, callback_id=None):
response_data = {
Expand Down Expand Up @@ -151,6 +173,16 @@ async def validate_startup(self):
await self.send_log("DISCORD VALIDATION FAILED ✖ CANCELLING STARTUP.")
return False
await self.send_log("DISCORD VALIDATION PASSED ✔")

if config.is_openai_enabled:
await self.send_log(
"EXPERIMENTAL OPENAI CHAT RESPONSES ENABLED - VALIDATING CONFIGURATION/CONNECTION..."
)
valid_openai = validate_openai_token(config.openai_token)
if not valid_openai:
await self.send_log("OPENAI VALIDATION FAILED ✖ CANCELLING STARTUP.")
return False
await self.send_log("OPENAI VALIDATION PASSED ✔")
return True

# CLIENT COMMANDS/ROUTES
Expand Down Expand Up @@ -384,6 +416,9 @@ async def test_connection(self, data=None, callback_id=None):
elif validation_type == "discord":
if not validate_discord_token(config.get("discord_token")):
errors.append("Discord token is invalid")
elif validation_type == "openai":
if not validate_openai_token(config.get("openai_token")):
errors.append("OpenAI token is invalid")

if errors:
await self.emit(
Expand Down
18 changes: 18 additions & 0 deletions DiscoFlixClient/migrations/0003_configuration_openai_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-28 08:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('DiscoFlixClient', '0002_configuration_is_tagging_enabled_and_more'),
]

operations = [
migrations.AddField(
model_name='configuration',
name='openai_token',
field=models.CharField(default='', max_length=255, null=True, verbose_name='OpenAI Token'),
),
]
18 changes: 18 additions & 0 deletions DiscoFlixClient/migrations/0004_configuration_is_openai_enabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-28 09:52

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('DiscoFlixClient', '0003_configuration_openai_token'),
]

operations = [
migrations.AddField(
model_name='configuration',
name='is_openai_enabled',
field=models.BooleanField(default=False, verbose_name='Enable OpenAI Chatbot'),
),
]
2 changes: 2 additions & 0 deletions DiscoFlixClient/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Configuration(models.Model):
radarr_token = models.CharField("Radarr Token", max_length=255, null=True, default="")
sonarr_url = models.CharField("Sonarr Host-Url", max_length=255, null=True, default="")
sonarr_token = models.CharField("Sonarr Token", max_length=255, null=True, default="")
openai_token = models.CharField("OpenAI Token", max_length=255, null=True, default="")

session_timeout = models.IntegerField("Session Timeout", null=True, default=60)
max_check_time = models.IntegerField("Monitoring Timeout (Seconds)", null=True, default=600)
Expand All @@ -23,6 +24,7 @@ class Configuration(models.Model):
is_radarr_enabled = models.BooleanField("Enable Radarr Requests", default=True)
is_sonarr_enabled = models.BooleanField("Enable Sonarr Requests", default=True)
is_trailers_enabled = models.BooleanField("Enable YouTube Trailers", default=True)
is_openai_enabled = models.BooleanField("Enable OpenAI Chatbot", default=False)

is_tagging_enabled = models.BooleanField("Enable Tagging Content", default=False)
tag_label = models.CharField("Tag Label", max_length=16, null=True, default="DF")
Expand Down
Loading

0 comments on commit 73c05b6

Please sign in to comment.