The world's most clever kitty cat

Easier to contribute

bwc9876.dev ca00be72 bd2e8c1e

verified
+156 -8
+69
README.md
··· 1 + # Bingus Bot 2 + 3 + A very clever kitty 4 + 5 + Current features: 6 + 7 + - [Markov chain](https://en.wikipedia.org/wiki/Markov_chain) for a "smart" chatbot 8 + 9 + ## Pre-reqs 10 + 11 + - Python 3.12 12 + - Pipenv 13 + - Just (optional, QoL) 14 + 15 + ## Environment Setup 16 + 17 + With `just`: `just setup` 18 + 19 + Otherwise, `pipenv install` 20 + 21 + ### .env 22 + 23 + Minimal needed is an env var called `TOKEN` that contains the bot's token. 24 + 25 + Cogs may have additional options, these variables **should** be prefix with the cog's name, ex: 26 + 27 + Cog `Math` may have `Math.TEMP_UNIT=fahrenheit`. 28 + 29 + ## Cog Docs 30 + 31 + ### [Markov](src/cogs/markov.py) 32 + 33 + A markov chain is an incredibly simple model where it decides the next token based on previous 34 + knowledge of what tokens the current token has been proceeded by. 35 + 36 + #### Markov Commands 37 + 38 + - `/markov`: Make bingus try and reply to a prompt passed, use this to bypass the 80% change that bingus 39 + usually has to reply 40 + - `/scan_history`: Scan the history of the current channel and add it to the chain. Since Bingus only learns 41 + from *new* messages while he's active, you may need to do this when restarting him. This command can take a while depending on the number of messages. 42 + 43 + #### Markov Config 44 + 45 + - `Markov.REPLY_CHANNELS`: A *comma-delimited* list of channel IDs that the bot should have 46 + have a chance to reply to messages in. The bot still learns from all channels in realtime, but 47 + these channels it'll have an 80% of replying to any message 48 + 49 + ## Adding Cogs 50 + 51 + To start, you can run `just add-cog` (or `python src/dev/new_cog.py`). 52 + 53 + Follow the steps and you'll get a new cog in `src/cogs`. Here is where you can add 54 + commands, event listeners, etc. 55 + 56 + See the file generated for some simple examples, review the [PyCord docs](https://guide.pycord.dev/introduction) for more help and information. 57 + 58 + ### Best Practices 59 + 60 + Generally you'll want to make all state, config, etc. locallized to your cog file. That 61 + way we can easily disable it if needed and nothing will break. 62 + 63 + #### Config 64 + 65 + For simplicity we'll just use env vars for config. Prefix any env vars your cog will 66 + read with the cog's name and a `.` (example: `Markov` cog can have a var called `Markov.REPLY_CHANNELS`). 67 + 68 + Try to documents these options within this README file under the [The individual cogs docs](#cog-docs). 69 + Create a third-level heading with your cog's name, and a link to its source code.
+12
justfile
··· 1 + _default: 2 + @just --list --unsorted --justfile {{justfile()}} 3 + 4 + setup: 5 + pipenv install 6 + 7 + dev: 8 + pipenv run python src/main.py 9 + 10 + new-cog: 11 + python src/dev/new_cog.py 12 +
+3
src/cogs.json
··· 1 + [ 2 + "cogs.markov" 3 + ]
+4 -5
src/cogs/markov.py
··· 10 10 11 11 def __init__(self, bot): 12 12 self.bot = bot 13 - self.reply_channels = [int(x) for x in os.getenv("REPLY_CHANNELS", "0").split(",")] 13 + self.reply_channels = [int(x) for x in os.getenv("Markov.REPLY_CHANNELS", "0").split(",")] 14 14 self.markov = MarkovChain({}) 15 15 16 16 @commands.slash_command() 17 - async def scan_history(self, ctx): 17 + async def scan_history(self, ctx: discord.ApplicationContext): 18 18 buf = [] 19 19 await ctx.defer() 20 20 async for msg in ctx.history(limit=None): 21 - print("Learning...") 22 21 if msg.author.id != self.bot.application_id: 23 22 buf.append(msg.content) 24 23 self.markov.learn(" ".join(buf)) 25 24 await ctx.respond("Bingus Learned!") 26 25 27 26 @commands.slash_command() 28 - async def markov(self, ctx, prompt: discord.Option(str)): 27 + async def markov(self, ctx: discord.ApplicationContext, prompt: discord.Option(str)): 29 28 print("Bingus is responding!") 30 29 response = self.markov.respond(prompt) 31 30 if response is not None and len(response) != 0: 32 31 await ctx.respond(response) 33 32 else: 34 - await ctx.respond(":: Bingus couldn't think of what to say!") 33 + await ctx.respond("> Bingus couldn't think of what to say!") 35 34 print("Bingus Responded!") 36 35 37 36
+31
src/dev/cog.template.py
··· 1 + 2 + import discord 3 + from discord.ext import commands 4 + from discord.message import Message 5 + 6 + class __CName__(commands.Cog): 7 + 8 + # Setup any state for the cog here, this will 9 + # exist for the run of the program 10 + # If you need to change the parameters for __init__ 11 + # make sure the [setup] function below this class. 12 + def __init__(self, bot) -> None: 13 + self.bot = bot 14 + 15 + # Example of a slash command that will be loaded 16 + # with this cog 17 + # @commands.slash_command() 18 + # async def ping(self, ctx: discord.ApplicationContext): 19 + # await ctx.respond("pong!") 20 + 21 + # Example of listening to all messages 22 + # ever sent in any server the bot is in 23 + # while active 24 + # @commands.Cog.listener() 25 + # async def on_message(self, msg: Message): 26 + # pass 27 + 28 + # See the PyCord docs for more info and guides 29 + 30 + def setup(bot): 31 + bot.add_cog(__CName__(bot))
+27
src/dev/new_cog.py
··· 1 + 2 + import sys 3 + import json 4 + from pathlib import Path 5 + 6 + 7 + def main(): 8 + name = input("Enter new cog's name (PascalCase!): ") 9 + cogs_folder = Path(__file__).parent.parent.joinpath("cogs") 10 + path = cogs_folder.joinpath(f"{name.lower()}.py") 11 + if path.exists(): 12 + print("This cog seems to already exist! Refusing to overwrite.") 13 + sys.exit(1) 14 + else: 15 + print("Creating new Python file...") 16 + template_content = Path(__file__).parent.joinpath("cog.template.py").read_text() 17 + new_cog_file = template_content.replace("__CName__", name) 18 + path.write_text(new_cog_file) 19 + print("Adding to \"cogs.json\"...") 20 + cogs_json_path = cogs_folder.parent.joinpath("cogs.json") 21 + current: list[str] = json.loads(cogs_json_path.read_text()) 22 + current.append(f"cogs.{name.lower()}") 23 + cogs_json_path.write_text(json.dumps(current)) 24 + print(f"Cog Created at {path}!") 25 + 26 + if __name__ == "__main__": 27 + main()
-1
src/lib/markov.py
··· 74 74 75 75 def learn(self, source: str): 76 76 tokens = self._parse_source(source.lower()) 77 - print(tokens) 78 77 self._learn_from_tokens(tokens) 79 78 80 79 def _pick_next(self, current_token: Token, allow_end: bool) -> Token:
+10 -2
src/main.py
··· 3 3 from dotenv import load_dotenv 4 4 from discord import Bot 5 5 import discord 6 + import json 7 + from pathlib import Path 6 8 from os import getenv 7 9 8 10 class BingusBot(Bot): ··· 11 13 12 14 load_dotenv() 13 15 14 - print("Initializing...") 16 + print("Initializing Base Bot...") 15 17 16 18 intents = discord.Intents.default() 17 19 intents.message_content = True 18 20 19 21 bot = BingusBot(intents=intents) 20 22 21 - bot.load_extension("cogs.markov") 23 + EXTENSIONS: list[str] = json.loads(Path(__file__).parent.joinpath("cogs.json").read_text()) 24 + 25 + for ext in EXTENSIONS: 26 + print(f"Initializing \"{ext}\"...") 27 + bot.load_extension(ext) 28 + 29 + print("Connecting to Discord...") 22 30 23 31 bot.run(getenv("TOKEN"))