├── .gitignore ├── LICENSE ├── README.md ├── bot.py ├── emoji_manager.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Keith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Bot Setup Guide with Ollama 2 | 3 | A simple Discord bot powered by local Ollama models for chat interactions. 4 | 5 | ## Prerequisites 6 | 7 | - **Python** installed on your machine 8 | - **Ollama** installed from [Ollama's official website](https://ollama.ai/) 9 | - A **Discord account** and server 10 | 11 | ## Setup Steps 12 | 13 | 1. Install Ollama and pull the model: 14 | ```bash 15 | ollama pull hermes3 16 | ``` 17 | 18 | 2. Create Discord Bot: 19 | - Go to [Discord Developer Portal](https://discord.com/developers/applications) 20 | - Click "New Application" and name it 21 | - Go to "Bot" tab, click "Add Bot" 22 | - Copy your bot token 23 | 24 | 3. Set up the bot: 25 | ```bash 26 | git clone https://github.com/gnukeith/DiscordChatAI.git 27 | cd DiscordChatAI 28 | ``` 29 | 30 | 4. Create a `.env` file: 31 | ```env 32 | DISCORD_TOKEN=your_bot_token_here 33 | ``` 34 | 35 | 5. Install Python dependencies: 36 | ```bash 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | ## Running the Bot 41 | 42 | 1. Start the bot: 43 | ```bash 44 | python bot.py 45 | ``` 46 | 47 | 2. Test by mentioning the bot in Discord: `@YourBotName hello` 48 | 49 | ## Notes 50 | - Bot responds to mentions only 51 | - Conversations are logged in `conversations.log` -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import requests 3 | import json 4 | import os 5 | import datetime 6 | from dotenv import load_dotenv 7 | import random 8 | import re 9 | 10 | # Load environment variables from .env file 11 | load_dotenv() 12 | 13 | TOKEN = os.getenv('DISCORD_TOKEN') 14 | print(f"Token loaded: {'Found token' if TOKEN else 'No token found'}") 15 | 16 | # Initialize Discord bot with more intents 17 | intents = discord.Intents.default() 18 | intents.message_content = True 19 | intents.reactions = True 20 | client = discord.Client(intents=intents) 21 | 22 | # Ollama API configuration 23 | ollama_url = "http://localhost:11434/api/generate" 24 | model_name = "hermes3" 25 | 26 | # Emoji collections 27 | HAPPY_EMOJIS = ['😊', '😄', '😃', '🥰', '😘', '😋', '😍', '🤗', '🌟', '✨', '💫', '⭐'] 28 | SAD_EMOJIS = ['😢', '😭', '🥺', '😔', '😪', '💔', '😿', '🫂', '🥀'] 29 | PLAYFUL_EMOJIS = ['😜', '🤪', '😝', '😋', '🤭', '🙈', '🙉', '🙊', '🐱', '🐰', '🦊', '🐼'] 30 | COOL_EMOJIS = ['😎', '🕶️', '🔥', '💯', '🆒', '🤙', '👑', '💪', '✌️', '🎮', '🎯', '🎪'] 31 | LOVE_EMOJIS = ['❤️', '🧡', '💛', '💚', '💙', '💜', '🤍', '🖤', '💝', '💖', '💗', '💓'] 32 | SPOOKY_EMOJIS = ['💀', '👻', '🎃', '🦇', '🕷️', '🕸️', '🧟‍♂️', '🧟‍♀️', '👺', '👹', '😈', '🤡'] 33 | THINKING_EMOJIS = ['🤔', '🧐', '💭', '💡', '🎯', '📚', '🔍', '💻', '📝'] 34 | MISC_EMOJIS = ['🌈', '🎨', '🎭', '🎪', '🎡', '🎢', '🎠', '🌸', '🌺', '🌷', '🌹', '🍀'] 35 | 36 | 37 | # Message history storage (you might want to use a proper database in production) 38 | message_history = {} # {channel_id: [last_n_messages]} 39 | HISTORY_LENGTH = 10 # Number of messages to keep in history 40 | 41 | def should_respond(message_content, bot_name): 42 | """ 43 | Determine if the bot should respond to a message 44 | """ 45 | # Convert both to lowercase for case-insensitive matching 46 | content_lower = message_content.lower() 47 | bot_name_lower = bot_name.lower() 48 | 49 | # Direct mention check (already handled in main function) 50 | if f"<@{client.user.id}>" in message_content: 51 | return True 52 | 53 | # Check if the bot's name is mentioned 54 | if bot_name_lower in content_lower: 55 | return True 56 | 57 | # Check for question patterns 58 | question_patterns = [ 59 | r'\?$', # Ends with question mark 60 | r'^(what|who|where|when|why|how|can|could|would|should|is|are|do|does|did)', # Starts with question word 61 | ] 62 | 63 | for pattern in question_patterns: 64 | if re.search(pattern, content_lower): 65 | return random.random() < 0.5 # 50% chance to respond to questions 66 | 67 | # Random chance to respond to messages (10%) 68 | return random.random() < 0.1 69 | 70 | def analyze_message_context(message_content, response_text): 71 | """Analyze message context to determine appropriate emoji categories""" 72 | content = message_content.lower() + " " + response_text.lower() 73 | 74 | relevant_categories = [] 75 | 76 | # Check for different emotional contexts 77 | if any(word in content for word in ['happy', 'great', 'awesome', 'wonderful', 'yay', 'good']): 78 | relevant_categories.append(HAPPY_EMOJIS) 79 | 80 | if any(word in content for word in ['sad', 'sorry', 'unfortunate', 'bad', 'wrong', 'error']): 81 | relevant_categories.append(SAD_EMOJIS) 82 | 83 | if any(word in content for word in ['love', 'heart', 'care', 'sweet', 'cute']): 84 | relevant_categories.append(LOVE_EMOJIS) 85 | 86 | if any(word in content for word in ['think', 'question', 'how', 'what', 'why', 'learn', 'study']): 87 | relevant_categories.append(THINKING_EMOJIS) 88 | 89 | if any(word in content for word in ['fun', 'play', 'game', 'lol', 'haha', 'joke']): 90 | relevant_categories.append(PLAYFUL_EMOJIS) 91 | 92 | if any(word in content for word in ['cool', 'awesome', 'nice', 'amazing', 'wow']): 93 | relevant_categories.append(COOL_EMOJIS) 94 | 95 | if any(word in content for word in ['spooky', 'scary', 'halloween', 'ghost', 'dead', 'monster']): 96 | relevant_categories.append(SPOOKY_EMOJIS) 97 | 98 | # If no relevant categories found, use a default set 99 | 100 | if not relevant_categories: 101 | relevant_categories = [HAPPY_EMOJIS, MISC_EMOJIS, PLAYFUL_EMOJIS] 102 | 103 | return relevant_categories 104 | 105 | 106 | def get_conversation_context(channel_id): 107 | """Get the recent conversation context from the channel""" 108 | if channel_id in message_history: 109 | return "\n".join([f"{msg['author']}: {msg['content']}" for msg in message_history[channel_id]]) 110 | return "" 111 | 112 | 113 | def safe_log_conversation(user_message, bot_response): 114 | """Safely log conversation with proper encoding handling""" 115 | try: 116 | with open('conversations.log', 'a', encoding='utf-8') as f: 117 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 118 | f.write(f"[{timestamp}]\nUser: {user_message}\nBot: {bot_response}\n\n") 119 | except Exception as e: 120 | print(f"Logging error: {e}") 121 | 122 | @client.event 123 | async def on_ready(): 124 | print(f'Logged in as {client.user}') 125 | 126 | @client.event 127 | async def on_message(message): 128 | if message.author == client.user: 129 | return 130 | 131 | # Update message history 132 | channel_id = message.channel.id 133 | if channel_id not in message_history: 134 | message_history[channel_id] = [] 135 | 136 | message_history[channel_id].append({ 137 | 'author': message.author.name, 138 | 'content': message.content 139 | }) 140 | 141 | # Keep only the last N messages 142 | message_history[channel_id] = message_history[channel_id][-HISTORY_LENGTH:] 143 | 144 | # Check if the bot should respond 145 | if should_respond(message.content, client.user.name) or client.user in message.mentions: 146 | try: 147 | # Show typing indicator 148 | async with message.channel.typing(): 149 | # Get conversation context 150 | context = get_conversation_context(channel_id) 151 | 152 | # Prepare prompt with context 153 | prompt = f"Context:\n{context}\n\nCurrent messAage: {message.content}" 154 | 155 | # Generate response using Ollama API 156 | data = { 157 | "model": model_name, 158 | "prompt": prompt, 159 | "stream": False 160 | } 161 | response = requests.post(ollama_url, headers={"Content-Type": "application/json"}, data=json.dumps(data)) 162 | response.raise_for_status() 163 | 164 | response_text = json.loads(response.text)['response'] 165 | 166 | # Log the conversation with safe encoding handling 167 | safe_log_conversation(message.content, response_text) 168 | 169 | # Get contextually appropriate emoji categories 170 | relevant_categories = analyze_message_context(message.content, response_text) 171 | 172 | # Randomly select number of emojis (1-20) 173 | num_reactions = random.randint(1, 20) # Random number of reactions, this is chaotic 174 | 175 | # If we have fewer categories than desired reactions, we can reuse categories 176 | if len(relevant_categories) < num_reactions: 177 | relevant_categories = relevant_categories * num_reactions 178 | 179 | # Select random emojis from relevant categories 180 | selected_emojis = [random.choice(category) for category in random.sample(relevant_categories, num_reactions)] 181 | 182 | # Add reactions 183 | for emoji in selected_emojis: 184 | await message.add_reaction(emoji) 185 | 186 | # Add random emojis to the response text 187 | response_with_emoji = f"{response_text} {' '.join(random.sample(selected_emojis, min(2, len(selected_emojis))))}" 188 | await message.reply(response_with_emoji, mention_author=True) 189 | 190 | # Handle sticker requests 191 | if "sticker" in message.content.lower(): 192 | if message.guild and message.guild.stickers: 193 | sticker = random.choice(message.guild.stickers) 194 | await message.channel.send(stickers=[sticker]) 195 | 196 | except Exception as e: 197 | print(f"Error: {e}") 198 | error_emojis = random.sample(SAD_EMOJIS + THINKING_EMOJIS, 2) 199 | await message.reply(f"Something went wrong! {' '.join(error_emojis)}", mention_author=True) 200 | 201 | @client.event 202 | async def on_reaction_add(reaction, user): 203 | if user != client.user: 204 | message_content = reaction.message.content 205 | relevant_categories = analyze_message_context(message_content, "") 206 | chosen_category = random.choice(relevant_categories) 207 | await reaction.message.add_reaction(random.choice(chosen_category)) 208 | 209 | client.run(TOKEN) -------------------------------------------------------------------------------- /emoji_manager.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | class EmojiManager: 4 | def __init__(self): 5 | self.EMOJI_CATEGORIES = { 6 | 'happy': ['😊', '😄', '😃', '🥰', '😘', '😋', '😍', '🤗', '🌟', '✨', '💫', '⭐', '😁', '😸', '😺'], 7 | 'sad': ['😢', '😭', '🥺', '😔', '😪', '💔', '😿', '🫂', '🥀', '😕', '😓', '😩'], 8 | 'playful': ['😜', '🤪', '😝', '😋', '🤭', '🙈', '🙉', '🙊', '🐱', '🐰', '🦊', '🐼', '🦝', '🐨', '🐯'], 9 | 'cool': ['😎', '🕶️', '🔥', '💯', '🆒', '🤙', '👑', '💪', '✌️', '🎮', '🎯', '🎪', '🌟', '⚡', '🌈'], 10 | 'love': ['❤️', '🧡', '💛', '💚', '💙', '💜', '🤍', '🖤', '💝', '💖', '💗', '💓', '💕', '💞', '💘'], 11 | 'spooky': ['💀', '👻', '🎃', '🦇', '🕷️', '🕸️', '🧟‍♂️', '🧟‍♀️', '👺', '👹', '😈', '🤡', '🌚', '🦴', '☠️'], 12 | 'thinking': ['🤔', '🧐', '💭', '💡', '🎯', '📚', '🔍', '💻', '📝', '🎓', '🧮', '📊'], 13 | 'misc': ['🌈', '🎨', '🎭', '🎪', '🎡', '🎢', '🎠', '🌸', '🌺', '🌷', '🌹', '🍀', '🌙', '⭐', '🌟'], 14 | 'food': ['🍜', '🍱', '🍣', '🍙', '🍡', '🍵', '🧋', '🍰', '🍪', '🍩', '🍦', '🍭'], 15 | 'magic': ['✨', '💫', '🌟', '⭐', '🔮', '🎭', '🎪', '🎇', '🎆', '🪄', '🧙‍♀️', '🧙‍♂️'], 16 | 'nature': ['🌸', '🌺', '🌷', '🌹', '🌱', '🌲', '🍀', '🌿', '🍃', '🌾', '🌻', '🌼'], 17 | 'tech': ['💻', '📱', '⌨️', '🖥️', '🎮', '🕹️', '🤖', '👾', '💾', '📡', '🔌', '💡'] 18 | } 19 | 20 | self.SENTIMENT_MAPPING = { 21 | 'happy': ['happy', 'great', 'awesome', 'wonderful', 'yay', 'good', 'nice', 'excellent'], 22 | 'sad': ['sad', 'sorry', 'unfortunate', 'bad', 'wrong', 'error', 'fail', 'mistake'], 23 | 'love': ['love', 'heart', 'care', 'sweet', 'cute', 'adorable', 'precious'], 24 | 'thinking': ['think', 'question', 'how', 'what', 'why', 'learn', 'study', 'curious'], 25 | 'playful': ['fun', 'play', 'game', 'lol', 'haha', 'joke', 'silly', 'funny'], 26 | 'cool': ['cool', 'awesome', 'nice', 'amazing', 'wow', 'impressive', 'sick'], 27 | 'spooky': ['spooky', 'scary', 'halloween', 'ghost', 'dead', 'monster', 'creepy'], 28 | 'tech': ['code', 'program', 'computer', 'tech', 'bot', 'digital', 'online'], 29 | 'magic': ['magic', 'special', 'wonderful', 'amazing', 'mysterious', 'fantasy'], 30 | 'nature': ['nature', 'flower', 'tree', 'plant', 'garden', 'grow', 'bloom'] 31 | } 32 | 33 | def analyze_message_sentiment(self, content): 34 | """Analyze message content and return relevant emoji categories""" 35 | content = content.lower() 36 | matched_categories = set() 37 | 38 | # Check content against each sentiment category 39 | for category, keywords in self.SENTIMENT_MAPPING.items(): 40 | if any(keyword in content for keyword in keywords): 41 | matched_categories.add(category) 42 | 43 | # Always include some default categories if nothing matches 44 | if not matched_categories: 45 | matched_categories = {'happy', 'misc', 'playful'} 46 | 47 | # Add 'misc' category for additional variety 48 | matched_categories.add('misc') 49 | 50 | return matched_categories 51 | 52 | def get_random_emojis(self, categories, num_emojis=3): 53 | """Get random emojis from specified categories""" 54 | all_relevant_emojis = [] 55 | for category in categories: 56 | all_relevant_emojis.extend(self.EMOJI_CATEGORIES.get(category, [])) 57 | 58 | # Ensure we don't try to get more emojis than we have 59 | num_emojis = min(num_emojis, len(all_relevant_emojis)) 60 | 61 | return random.sample(all_relevant_emojis, num_emojis) 62 | 63 | def get_error_emojis(self): 64 | """Get error-specific emojis""" 65 | return random.sample(self.EMOJI_CATEGORIES['sad'] + self.EMOJI_CATEGORIES['thinking'], 2) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | discord.py 2 | requests 3 | python-dotenv 4 | audioop-lts --------------------------------------------------------------------------------