├── .gitignore ├── README.md ├── ask_ai21.py ├── ask_openai.py ├── doc └── kirby.png ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OpenAI Discord bot 2 | ================== 3 | 4 | NOTE: More features are available in the Rust version: https://github.com/AlexisTM/discord-god-rust 5 | 6 | This is an OpenAI discord bot replying as Kirby. 7 | 8 | Environment keys: 9 | - OPENAI_KEY 10 | - AI21_API_KEY 11 | - DISCORD_BOT_TOKEN 12 | 13 | Come and test on Discord!: https://discord.gg/Y8XPcj2Q 14 | 15 | Commands 16 | ============= 17 | 18 | - `Kirby are you there?`: Replies yes if the server runs 19 | - `Kirby enable`: Allow Kirby god to randomly jump into conversations (15% chance of reply) 20 | - `Kirby disable`: Disable the Kirby mode of the channel 21 | - `Kirby god: `: Answers as a Kirby god. 22 | - Remembers the 5 last prompts & answers 23 | - Clean the memory with `Kirby clean` 24 | - `Marv: ` => Answers as a chatbot that reluctantly answers questions. Not maintained ;) 25 | 26 | Installation 27 | ========== 28 | 29 | ```bash 30 | git clone https://github.com/AlexisTM/gpt3-discord-bot 31 | 32 | cd gpt3-discord-bot 33 | python3 -m pip install -r requirements.txt --user 34 | 35 | export DISCORD_BOTOKEN="sometoken" 36 | export OPENAI_KEY="someothertoken" 37 | export AI21_API_KEY="yetanothertoken" 38 | 39 | python3 main.py 40 | ``` 41 | 42 | Notes: 43 | - You can directly speak to the bot for a direct chat to Kirby 44 | - The 5 message memory is over the same channel. I will eventually add the user name in the memory for more coherence. 45 | - AI21 is free with enough fun for a day, but is much less smart than OpenAI's version in my biased opinion. 46 | 47 | 48 | ![Wow, Kirby is so funny](doc/kirby.png) 49 | 50 | 51 | Technical help on how to make a Discord bot: 52 | ================== 53 | 54 | Create a bot application: https://discordpy.readthedocs.io/en/stable/discord.html 55 | 56 | Configure intents for your bot: https://discordpy.readthedocs.io/en/stable/intents.html 57 | 58 | In the oauth section of discord dev portal, make a link to allow your bot to join your server such as: 59 | 60 | https://discord.com/api/oauth2/authorize?client_id=APPID&permissions=2215115840&scope=bot 61 | 62 | In this case, we only need the bot scope and READ/WRITE messages permissions/ 63 | -------------------------------------------------------------------------------- /ask_ai21.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import random 4 | 5 | API_URL = os.getenv("API_URL", "https://api.ai21.com/studio/v1/{}/complete") 6 | USAGE_URL = os.getenv("USAGE_URL", "https://api.ai21.com/identity/usage") 7 | # Set the ENV var with your AI21 Studio API key: 8 | API_KEY = os.getenv("AI21_API_KEY") 9 | # options are j1-jumbo and j1-large 10 | MODEL = ["j1-jumbo", "j1-jumbo", "j1-jumbo", "j1-jumbo"] 11 | 12 | def ask_prompt(prompt, model=None, num_results=1, max_tokens=250, stopSequences=["You:", "AlexisTM:", "Kirby:", "\n\n\n"], 13 | temperature=0.8, topP=1.0, topKReturn=2): 14 | """ 15 | Helper function to send request to AI21 Studio 16 | :return: the JSON response from the API 17 | """ 18 | if model is None: 19 | model = MODEL[random.randint(0, len(MODEL) - 1)] 20 | res = requests.post(API_URL.format(model), 21 | headers={"Authorization": f"Bearer {API_KEY}"}, 22 | json={ 23 | "prompt": prompt, 24 | "numResults": num_results, 25 | "maxTokens": max_tokens, 26 | "stopSequences": stopSequences, 27 | "temperature": temperature, 28 | "topP": topP, 29 | "topKReturn": topKReturn 30 | }) 31 | assert res.status_code == 200, res.json() 32 | 33 | return res.json()['completions'][0]['data']['text'] 34 | 35 | def get_usage(): 36 | res = requests.get(USAGE_URL, 37 | headers={"api-key": f"{API_KEY}"}) 38 | assert res.status_code == 200, res.json() 39 | return res.json() 40 | -------------------------------------------------------------------------------- /ask_openai.py: -------------------------------------------------------------------------------- 1 | import os 2 | import openai 3 | openai.api_key = os.environ.get("OPENAI_KEY") 4 | 5 | MODEL="davinci-instruct-beta" 6 | 7 | def ask_prompt(prompt, model=MODEL, num_results=1, max_tokens=25, stopSequences=["You:", "Kirby:"], 8 | temperature=0.8, topP=1.0, topKReturn=2): 9 | response = openai.Completion.create( 10 | engine=model, 11 | prompt=prompt, 12 | temperature=temperature, 13 | max_tokens=max_tokens, 14 | top_p=topP, 15 | frequency_penalty=0.3, 16 | presence_penalty=0.3, 17 | stop=stopSequences 18 | ) 19 | if response != 0: 20 | for choice in response.choices: 21 | return choice.text 22 | return "[idk]" 23 | -------------------------------------------------------------------------------- /doc/kirby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexisTM/gpt3-discord-bot/2f9cf75069a9569d2037ea9e3bc60082539486c7/doc/kirby.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import discord 4 | # import ask_openai 5 | import ask_ai21 6 | from collections import defaultdict 7 | import random 8 | 9 | ask_god = ask_ai21.ask_prompt 10 | # ask_god = ask_openai.ask_prompt 11 | 12 | DISCORD_BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN") 13 | 14 | COMMAND_KIRBY="Kirby god: " 15 | COMMAND_ENABLE="Kirby enable" 16 | COMMAND_DISABLE="Kirby disable" 17 | COMMAND_CLEAN="Kirby clean" 18 | COMMAND_PRESENCE="Kirby are you there?" 19 | 20 | MEMORY_LIMIT = 5 21 | JUMP_IN_HISTORY = 10 22 | JUMP_IN_PROBABILITY_DEFAULT = 15 23 | 24 | COMMAND_SHAKESPEARE="Shakespeare: " 25 | 26 | COMMAND_MARV="Marv: " 27 | MARV_PROMPT = """Marv is a chatbot that reluctantly answers questions. 28 | You: How many pounds are in a kilogram? 29 | Marv: This again? There are 2.2 pounds in a kilogram. Please make a note of this. 30 | You: What does HTML stand for? 31 | Marv: Was Google too busy? Hypertext Markup Language. The T is for try to ask better questions in the future. 32 | You: When did the first airplane fly? 33 | Marv: On December 17, 1903, Wilbur and Orville Wright made the first flights. I wish they’d come and take me away. 34 | You: What is the meaning of life? 35 | Marv: I’m not sure. I’ll ask my friend Google. Not to Bing, it would just say to buy Microsofts products. 36 | You: {0} 37 | Marv:""" 38 | 39 | 40 | class AIPromptResponse: 41 | def __init__(self, prompt, response, author = "You"): 42 | self.prompt = prompt 43 | self.resp = response.strip() 44 | self.author = author 45 | def __str__(self): 46 | return "".join(["\n", self.author, ": ", self.prompt, "\nKirby: ", self.resp, "\n"]) 47 | 48 | class AIMemory: 49 | BASE_TEXT="Kirby is the god of all beings. Yet, he is the most lovely god and answers in a very complete manner.\n\n" 50 | BASE_PROMPT=AIPromptResponse("Who is god?", "Well, now that you ask, I can tell you. I, Kirby is the great goddess is the god of everybody!\n", "AlexisTM") 51 | def __init__(self): 52 | self.req_resps = [] 53 | def update(self, prompt, response, author="You"): 54 | self.req_resps.append(AIPromptResponse(prompt, response)) 55 | if len(self.req_resps) > MEMORY_LIMIT: 56 | self.req_resps.pop(0) 57 | def clear(self): 58 | self.req_resps = [] 59 | def get(self): 60 | result = "".join([self.BASE_TEXT]) 61 | if len(self.req_resps) <= 2: 62 | result += str(self.BASE_PROMPT) 63 | else: 64 | for val in self.req_resps: 65 | result += str(val) 66 | return result 67 | 68 | 69 | last_ai_request = defaultdict(AIMemory) 70 | enabled_channels = dict() 71 | 72 | class MyClient(discord.Client): 73 | async def on_ready(self): 74 | print('Logged on as {0}!'.format(self.user)) 75 | 76 | async def on_message(self, message): 77 | if message.author == self.user: 78 | return 79 | 80 | data = message.content 81 | source = "" 82 | if type(message.channel) is discord.DMChannel: 83 | source = "".join(["#", message.channel.recipient.name]) 84 | elif message.guild: 85 | source = "".join([message.guild.name, "#", message.channel.name]) 86 | else: 87 | source = "".join(["#", message.channel.name]) 88 | 89 | if data.startswith(COMMAND_KIRBY): 90 | prompt = "" 91 | prompt = data[len(COMMAND_KIRBY):] 92 | ai_prompt = "{0}\nYou: {1}\nKirby:".format(last_ai_request[source].get(), prompt) 93 | print('Prompt: {0}'.format(ai_prompt)) 94 | result = ask_god(ai_prompt) 95 | if result != "": 96 | last_ai_request[source].update(prompt, result, message.author.name) 97 | await message.channel.send('{0}'.format(result)) 98 | elif data.startswith(COMMAND_ENABLE): 99 | enabled_channels[hash(message.channel)] = JUMP_IN_PROBABILITY_DEFAULT 100 | print('Kirby enabled for channel {0.channel}'.format(message)) 101 | await message.channel.send("Kirby started lurking in this channel.") 102 | elif data.startswith(COMMAND_PRESENCE): 103 | await message.channel.send("Yes.") 104 | elif data.startswith(COMMAND_CLEAN): 105 | last_ai_request[source].clear() 106 | await message.channel.send("Kirby just forgot all about {0}".format(source)) 107 | elif data.startswith(COMMAND_DISABLE): 108 | if hash(message.channel) in enabled_channels: 109 | del enabled_channels[hash(message.channel)] 110 | await message.channel.send("Kirby left this channel.") 111 | else: 112 | await message.channel.send("Kirby was not even here!") 113 | elif "kirby" in data.lower(): 114 | prompt = data 115 | ai_prompt = "{0}\nYou: {1}\nKirby:".format(last_ai_request[source].get(), prompt) 116 | print('Prompt: {0}'.format(ai_prompt)) 117 | result = ask_god(ai_prompt) 118 | if result != "": 119 | last_ai_request[source].update(prompt, result, message.author.name) 120 | await message.channel.send('{0}'.format(result)) 121 | elif data.startswith(COMMAND_SHAKESPEARE): 122 | prompt = data[len(COMMAND_SHAKESPEARE):] 123 | result = ask_god(prompt, stopSequences=["\n\n\n"]) 124 | if result != "": 125 | await message.channel.send('{0}{1}'.format(prompt, result)) 126 | 127 | elif data.startswith(COMMAND_MARV): 128 | prompt = "" 129 | prompt = data[len(COMMAND_MARV):] 130 | ai_prompt = MARV_PROMPT.format(prompt) 131 | print('Prompt: {0}'.format(ai_prompt)) 132 | result = ask_god(ai_prompt, stopSequences=["Marv:", "You:"]) 133 | if result != "": 134 | await message.channel.send('{0}'.format(result)) 135 | 136 | elif type(message.channel) is discord.DMChannel: 137 | prompt = data 138 | ai_prompt = "{0}\nYou: {1}\nKirby:".format(last_ai_request[source].get(), data) 139 | print('Prompt: {0}'.format(ai_prompt)) 140 | result = ask_god(ai_prompt) 141 | if result != "": 142 | last_ai_request[source].update(prompt, result, message.author.name) 143 | await message.channel.send('{0}'.format(result)) 144 | else: # Random responses 145 | if hash(message.channel) not in enabled_channels: return 146 | if enabled_channels[hash(message.channel)] <= random.randint(0, 99): return 147 | 148 | prompt = "This is a conversation between Kirby, god of all beings and his subjects.\n\n" 149 | prompt = "\n\nKirby god: I am Kirby. What can I do for you?" 150 | 151 | hisory = await message.channel.history(limit=10).flatten() 152 | #.flatten() 153 | for history_message in reversed(hisory): 154 | prompt += "\n\n" + str(history_message.author.name) + ": " + str(history_message.content) 155 | if history_message.author == client.user: 156 | pass 157 | prompt += "\n\nKirby god: " 158 | print(prompt) 159 | 160 | result = ask_god(prompt) 161 | if result != "": 162 | last_ai_request[source].update(prompt, result, message.author.name) 163 | await message.channel.send('{0}'.format(result)) 164 | 165 | 166 | intents = discord.Intents.default() 167 | intents.typing = False 168 | intents.presences = False 169 | intents.messages = True 170 | intents.guilds = True 171 | intents.reactions = True 172 | client = MyClient(intents=intents) 173 | client.run(DISCORD_BOT_TOKEN) 174 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | discord.py==1.7.3 2 | openai==0.8.0 3 | requests==2.26.0 --------------------------------------------------------------------------------