@@ -0,0 +1,144 @@
import yaml
import asyncio
import discord
from discord . ext import commands
import aiohttp
# ────────────────────────────────────────────────
def load_config ( path : str ) :
""" Return a dict loaded from a YAML file. """
with open ( path , " r " , encoding = " utf‑ 8 " ) as fh :
return yaml . safe_load ( fh )
config = load_config ( " config.yml " ) # <- loads the config file
DISCORD_TOKEN = config [ " discord_token " ] # Discord bot token
WHITELIST_CHANNELS = set ( map ( int , config . get ( " whitelist_channels " , [ ] ) ) ) # Set of whitelisted channel IDs (int)
OPENWEBUI_URL = config [ " open_webui_url " ] . rstrip ( ' / ' ) # Ensure no trailing slash
OPENWEBUI_API_KEY = config [ " open-webui_api_key " ] # API key for Open-WebUI
MODEL_NAME = config [ " model_name " ] # Model name to use, e.g., "gpt-3.5-turbo"
KNOW_BASE = config [ " knowledge_base " ] # Knowledge base to use, e.g., "knowledge_base_v1"
TOOLS = config . get ( " tools " , [ ] ) # list of tool-ids
SYSTEM_PROMPT = config . get ( " system_prompt " , None ) # Optional system prompt to prepend to user messages
ALLOW_DMS = config . get ( " allow_dms " , False ) # Allow DMs to the bot (default: False)
async def _query_openwebui ( user_text : str , channel_id : int , tools_list : list ) :
"""
Payload structure for the OpenAI-compatible endpoint.
Args:
user_text (str): The user ' s message to send to the Open-WebUI.
channel_id (int): The Discord channel ID where the message was sent.
tools_list (list): List of tool IDs to use, if any.
"""
async with aiohttp . ClientSession ( ) as session :
# This payload structure is for the OpenAI-compatible endpoint from open-webui
payload = {
" model " : MODEL_NAME ,
" stream " : False ,
" messages " : [
{
" role " : " user " ,
" content " : user_text
}
]
}
# Attach tools if provided in the config file
if tools_list :
payload [ " tool_ids " ] = tools_list
print ( f " 🔧 Using tools: { payload [ ' tool_ids ' ] } " )
# The endpoint path for your instance appears to be /api/chat/completions
async with session . post ( f " { OPENWEBUI_URL } /api/chat/completions " ,
json = payload ,
headers = { " Authorization " : f " Bearer { OPENWEBUI_API_KEY } " } ) as resp :
if resp . status != 200 :
# If the response is not 200, raise an error with the response text
data = await resp . text ( )
raise RuntimeError ( f " Open‑ WebUI responded { resp . status } : { data } " )
# If the response is OK, parse the JSON and return the content
response_data = await resp . json ( )
return response_data [ ' choices ' ] [ 0 ] [ ' message ' ] [ ' content ' ]
# --------------------------------------------------------------------------- #
# Discord bot logic – discord.py
# --------------------------------------------------------------------------- #
intents = discord . Intents . default ( ) # Default intents
intents . message_content = True # Required to read message content
intents . members = True # Required to read member info (if needed)
bot = commands . Bot ( command_prefix = ' ! ' , intents = intents ) # Command prefix is '!' by default to allow commands like !ping
@bot.event
async def on_ready ( ) :
print ( f " ✅ Logged in as { bot . user } (id= { bot . user . id } ) " )
# Only a test for commands, I add later
@bot.command ( name = " ping " )
async def ping ( ctx ) :
await ctx . send ( " 🏓 Pong! " )
# --------------------------------------------------------------------------- #
# Main logic – only respond to allowed channels / DM flag
# --------------------------------------------------------------------------- #
@bot.event
async def on_message ( message ) :
# Ignore messages from bots (incl. the bot itself)
if message . author . bot : return
# Allow commands to be processed
await bot . process_commands ( message )
# Skip if we are in a DM and that is disabled
if not ALLOW_DMS and isinstance ( message . channel , discord . DMChannel ) : return
# --- debugging ---
print ( f " ℹ ️ Message received in channel: { message . channel . id } " )
print ( f " 📢 Whitelisted channels are: { WHITELIST_CHANNELS } " )
# -----------------------------
# Allow only the whitelist channels – empty list means “all channels”
if WHITELIST_CHANNELS and message . channel . id not in WHITELIST_CHANNELS :
return
# ----------------------------------------------------------------------- #
# A. Prepare payload
# ----------------------------------------------------------------------- #
# The OpenAI endpoint works better without the extra context in the prompt
prompt = message . content
if SYSTEM_PROMPT :
# The system prompt is handled differently in the OpenAI-compatible API
# For simplicity, we'll prepend it here. A more robust solution
# would add it as a separate message with the 'system' role.
prompt = f " { SYSTEM_PROMPT } \n \n User Question: { message . content } "
# ----------------------------------------------------------------------- #
# B. Query Open‑ WebUI and show typing indicator
# ----------------------------------------------------------------------- #
try :
async with message . channel . typing ( ) :
# Query the Open-WebUI API while showing "Bot is typing..."
reply = await _query_openwebui ( prompt , message . channel . id , TOOLS )
# Send the reply
await message . reply ( reply )
except Exception as e :
await message . reply ( f " ⚠ Error contacting the Open‑ WebUI API: { e } " )
# No need to return here as the function ends after this block.
# --------------------------------------------------------------------------- #
# Start bot
# --------------------------------------------------------------------------- #
if __name__ == " __main__ " :
try :
asyncio . run ( bot . run ( DISCORD_TOKEN ) )
except KeyboardInterrupt :
print ( " 🤖 Shutting down… " )