From 6c57a8591f2d70956b27e0f73a0d7c9bd9593c06 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Thu, 2 Jan 2025 17:53:16 -0800 Subject: [PATCH 1/3] upgrading script to v2 with history --- scripts/discordbot.py | 137 +++++++++++++++++++----------------------- v2/.env | 4 ++ v2/bot.py | 36 ++++++----- 3 files changed, 88 insertions(+), 89 deletions(-) create mode 100644 v2/.env diff --git a/scripts/discordbot.py b/scripts/discordbot.py index de00566..9edf967 100644 --- a/scripts/discordbot.py +++ b/scripts/discordbot.py @@ -1,111 +1,100 @@ +import os import discord from discord.ext import commands +import openai from openai import OpenAI -import os + +from collections import deque from dotenv import load_dotenv # Load environment variables load_dotenv() +# Get environment variables +DISCORD_TOKEN = os.getenv('DISCORD_TOKEN') +OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') +OPENWEBUI_API_BASE = os.getenv('OPENWEBUI_API_BASE') +MODEL_NAME = os.getenv('MODEL_NAME') + # Configure OpenAI client to point to OpenWebUI client = OpenAI( api_key=os.getenv('OPENAI_API_KEY'), base_url=os.getenv('OPENWEBUI_API_BASE') # e.g., "http://localhost:8080/v1" ) +# Configure OpenAI +# TODO: The 'openai.api_base' option isn't read in the client API. You will need to pass it when you instantiate the client, e.g. 'OpenAI(base_url=OPENWEBUI_API_BASE)' +# openai.api_base = OPENWEBUI_API_BASE + # Initialize Discord bot intents = discord.Intents.default() intents.message_content = True -bot = commands.Bot(command_prefix="!", intents=intents) +intents.messages = True +bot = commands.Bot(command_prefix='!', intents=intents) -# Add a dictionary to store conversation histories -conversation_histories = {} -DEFAULT_HISTORY_LIMIT = 50 -MAX_HISTORY_LIMIT = 200 +# Message history cache +channel_history = {} + +async def get_chat_history(channel, limit=100): + messages = [] + async for message in channel.history(limit=limit): + messages.append(f"{message.author.name}: {message.content}") + return "\n".join(reversed(messages)) + +async def get_ai_response(context, user_message): + formatted_prompt = f"##CONTEXT##\n{context}\n##ENDCONTEXT##\n\n{user_message}" -async def get_ai_response(prompt, channel_id=None): try: - messages = [] - - # Always include history if it exists for this channel - if channel_id in conversation_histories: - history = conversation_histories[channel_id][-400:] # Get last 400 messages (200 exchanges) - - # Format the history as a context block - history_text = "\n".join([ - f"{'User: ' if msg['role'] == 'user' else 'Assistant: '}{msg['content']}" - for msg in history - ]) - - formatted_prompt = f"{prompt}\n##CONTEXT##\n{history_text}\n##ENDCONTEXT##" - else: - formatted_prompt = prompt - - response = client.chat.completions.create( - model=os.getenv('MODEL_NAME') or "us.anthropic.claude-3-5-sonnet-20241022-v2:0", - messages=[{"role": "user", "content": formatted_prompt}], - temperature=0.7, - max_tokens=500 - ) - - # Store the conversation history if channel_id is provided - if channel_id: - if channel_id not in conversation_histories: - conversation_histories[channel_id] = [] - conversation_histories[channel_id].extend([ - {"role": "user", "content": prompt}, - {"role": "assistant", "content": response.choices[0].message.content} - ]) - - # Always keep last 400 messages (200 exchanges) - if len(conversation_histories[channel_id]) > 400: - conversation_histories[channel_id] = conversation_histories[channel_id][-400:] - + response = client.chat.completions.create(model=MODEL_NAME, + messages=[ + {"role": "user", "content": formatted_prompt} + ]) return response.choices[0].message.content except Exception as e: - print(f"Error getting AI response: {e}") - return "Sorry, I encountered an error while processing your request." + return f"Error: {str(e)}" + +@bot.event +async def on_ready(): + print(f'{bot.user} has connected to Discord!') @bot.event async def on_message(message): + # Ignore messages from the bot itself if message.author == bot.user: return - # Respond to DMs or when mentioned in a server - if isinstance(message.channel, discord.DMChannel) or bot.user in message.mentions: - # For mentions, remove the bot mention from the message - if bot.user in message.mentions: - prompt = message.content.replace(f'<@{bot.user.id}>', '').strip() - else: - prompt = message.content.strip() - - if not prompt: - await message.channel.send("Hello! How can I help you?") - return + should_respond = False + # Check if bot was mentioned + if bot.user in message.mentions: + should_respond = True + + # Check if message is a DM + if isinstance(message.channel, discord.DMChannel): + should_respond = True + + if should_respond: async with message.channel.typing(): - response = await get_ai_response( - prompt, - channel_id=str(message.channel.id) - ) - - if len(response) > 2000: - chunks = [response[i:i+2000] for i in range(0, len(response), 2000)] - for chunk in chunks: - await message.channel.send(chunk) - else: - await message.channel.send(response) + # Get chat history + history = await get_chat_history(message.channel) + + # Remove bot mention from the message + user_message = message.content.replace(f'<@{bot.user.id}>', '').strip() + + # Get AI response + response = await get_ai_response(history, user_message) + + # Send response + await message.reply(response) await bot.process_commands(message) def main(): - # Get the Discord token from environment variables - discord_token = os.getenv('DISCORD_TOKEN') - if not discord_token: - raise ValueError("Discord token not found in environment variables") + if not all([DISCORD_TOKEN, OPENAI_API_KEY, OPENWEBUI_API_BASE, MODEL_NAME]): + print("Error: Missing required environment variables") + return - # Run the bot - bot.run(discord_token) + bot.run(DISCORD_TOKEN) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/v2/.env b/v2/.env new file mode 100644 index 0000000..20240c5 --- /dev/null +++ b/v2/.env @@ -0,0 +1,4 @@ +DISCORD_TOKEN="MTMxOTQ2NjUwMDM2MDUwMzM4OA.GHRFj6.xb-SgxDr6s_XnxqJvu_dlwKWEjvRNoCfxFNDu4" +OPENAI_API_KEY="sk-2cdddfca57424dbfae6e9f474b239eb4" +OPENWEBUI_API_BASE="https://chat.dnspegasus.net/api" +MODEL_NAME="discord-ai-bot" \ No newline at end of file diff --git a/v2/bot.py b/v2/bot.py index e33a9c2..9edf967 100644 --- a/v2/bot.py +++ b/v2/bot.py @@ -2,6 +2,8 @@ import os import discord from discord.ext import commands import openai +from openai import OpenAI + from collections import deque from dotenv import load_dotenv @@ -14,9 +16,15 @@ OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') OPENWEBUI_API_BASE = os.getenv('OPENWEBUI_API_BASE') MODEL_NAME = os.getenv('MODEL_NAME') +# Configure OpenAI client to point to OpenWebUI +client = OpenAI( + api_key=os.getenv('OPENAI_API_KEY'), + base_url=os.getenv('OPENWEBUI_API_BASE') # e.g., "http://localhost:8080/v1" +) + # Configure OpenAI -openai.api_key = OPENAI_API_KEY -openai.api_base = OPENWEBUI_API_BASE +# TODO: The 'openai.api_base' option isn't read in the client API. You will need to pass it when you instantiate the client, e.g. 'OpenAI(base_url=OPENWEBUI_API_BASE)' +# openai.api_base = OPENWEBUI_API_BASE # Initialize Discord bot intents = discord.Intents.default() @@ -35,14 +43,12 @@ async def get_chat_history(channel, limit=100): async def get_ai_response(context, user_message): formatted_prompt = f"##CONTEXT##\n{context}\n##ENDCONTEXT##\n\n{user_message}" - + try: - response = openai.ChatCompletion.create( - model=MODEL_NAME, - messages=[ - {"role": "user", "content": formatted_prompt} - ] - ) + response = client.chat.completions.create(model=MODEL_NAME, + messages=[ + {"role": "user", "content": formatted_prompt} + ]) return response.choices[0].message.content except Exception as e: return f"Error: {str(e)}" @@ -58,11 +64,11 @@ async def on_message(message): return should_respond = False - + # Check if bot was mentioned if bot.user in message.mentions: should_respond = True - + # Check if message is a DM if isinstance(message.channel, discord.DMChannel): should_respond = True @@ -71,13 +77,13 @@ async def on_message(message): async with message.channel.typing(): # Get chat history history = await get_chat_history(message.channel) - + # Remove bot mention from the message user_message = message.content.replace(f'<@{bot.user.id}>', '').strip() - + # Get AI response response = await get_ai_response(history, user_message) - + # Send response await message.reply(response) @@ -87,7 +93,7 @@ def main(): if not all([DISCORD_TOKEN, OPENAI_API_KEY, OPENWEBUI_API_BASE, MODEL_NAME]): print("Error: Missing required environment variables") return - + bot.run(DISCORD_TOKEN) if __name__ == "__main__": From 91bbe303baed193f3497e037b9c30fa3eb74b7cb Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Thu, 2 Jan 2025 19:38:42 -0800 Subject: [PATCH 2/3] pushing updates for bot --- .gitignore | 3 +- open-webui-tool/bedrock-image-tool.py | 100 ++++++++++++++++++++++++++ open-webui-tool/readme.md | 5 ++ v2/bot.py | 67 +++++++++++++---- 4 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 open-webui-tool/bedrock-image-tool.py create mode 100644 open-webui-tool/readme.md diff --git a/.gitignore b/.gitignore index 3cf45bf..1ee9cad 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -scripts/.env \ No newline at end of file +scripts/.env +v2/.env \ No newline at end of file diff --git a/open-webui-tool/bedrock-image-tool.py b/open-webui-tool/bedrock-image-tool.py new file mode 100644 index 0000000..e88a1d3 --- /dev/null +++ b/open-webui-tool/bedrock-image-tool.py @@ -0,0 +1,100 @@ +""" +title: Bedrock Image Description +author: Josh Knapp +version: 0.1.0 +description="Provide Direct Bedrock call for image generation" +""" + +import subprocess +import json +from pydantic import BaseModel, Field + + +# Try to import boto3, install if not present +try: + import boto3 +except ImportError: + print("boto3 package not found. Attempting to install...") + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", "boto3"]) + import boto3 + + print("boto3 package installed successfully") + except subprocess.CalledProcessError as e: + print(f"Failed to install boto3 package: {str(e)}") + + +class Tools: + class Valves(BaseModel): + AWS_ACCESS_KEY: str = Field( + default="", + description="AWS Access Key", + ) + AWS_SECRET_KEY: str = Field( + default="", + description="AWS Secret Key", + ) + AWS_BEDROCK_MODEL: str = Field( + default="", + description="AWS Bedrock Model to use" + ) + + def __init__(self): + self.valves = self.Valves() + pass + + def analyze_image(self, base64_image: str) -> str: + """ + Analyze an image using AWS Bedrock's vision model + Args: + base64_image (str): Base64 encoded image string + Returns: + str: Description of the image + """ + try: + # Initialize Bedrock runtime client + bedrock = boto3.client( + service_name="bedrock-runtime", + aws_access_key_id=self.valves.AWS_ACCESS_KEY, + aws_secret_access_key=self.valves.AWS_SECRET_KEY, + region_name="us-east-1" # or your preferred region + ) + + # Prepare the request body + request_body = { + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 1000, + "messages": [ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": base64_image + } + }, + { + "type": "text", + "text": "Please describe this image in detail." + } + ] + } + ] + } + + # Invoke the model + response = bedrock.invoke_model( + modelId=self.valves.AWS_BEDROCK_MODEL, + body=json.dumps(request_body) + ) + + # Parse and return the response + response_body = json.loads(response['body'].read()) + return response_body['messages'][0]['content'][0]['text'] + + except Exception as e: + print(f"Error analyzing image: {str(e)}") + return f"Error analyzing image: {str(e)}" diff --git a/open-webui-tool/readme.md b/open-webui-tool/readme.md new file mode 100644 index 0000000..240d292 --- /dev/null +++ b/open-webui-tool/readme.md @@ -0,0 +1,5 @@ +For any model to use this tool, you must set the valve values, and add something to the model to let it know to use the tool. + +``` +You have access to a tool that allows you to get descriptions of images called "Bedrock Image Description". Any image handling should be sent through this tool. +``` \ No newline at end of file diff --git a/v2/bot.py b/v2/bot.py index 9edf967..cf24c9e 100644 --- a/v2/bot.py +++ b/v2/bot.py @@ -1,9 +1,10 @@ import os import discord from discord.ext import commands -import openai from openai import OpenAI - +import base64 +import requests +from io import BytesIO from collections import deque from dotenv import load_dotenv @@ -35,28 +36,55 @@ bot = commands.Bot(command_prefix='!', intents=intents) # Message history cache channel_history = {} +async def download_image(url): + response = requests.get(url) + if response.status_code == 200: + image_data = BytesIO(response.content) + base64_image = base64.b64encode(image_data.read()).decode('utf-8') + return base64_image + return None + async def get_chat_history(channel, limit=100): messages = [] async for message in channel.history(limit=limit): - messages.append(f"{message.author.name}: {message.content}") + content = f"{message.author.name}: {message.content}" + + # Handle attachments (images) + for attachment in message.attachments: + if any(attachment.filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']): + content += f" [Image: {attachment.url}]" + + messages.append(content) return "\n".join(reversed(messages)) -async def get_ai_response(context, user_message): - formatted_prompt = f"##CONTEXT##\n{context}\n##ENDCONTEXT##\n\n{user_message}" +async def get_ai_response(context, user_message, image_urls=None): + messages = [{"role": "user", "content": []}] + + # Add text content + text_content = f"##CONTEXT##\n{context}\n##ENDCONTEXT##\n\n{user_message}" + messages[0]["content"].append({"type": "text", "text": text_content}) + + # Add image content if present + if image_urls: + for url in image_urls: + base64_image = await download_image(url) + if base64_image: + messages[0]["content"].append({ + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}" + } + }) try: - response = client.chat.completions.create(model=MODEL_NAME, - messages=[ - {"role": "user", "content": formatted_prompt} - ]) + response = client.chat.completions.create( + model=MODEL_NAME, + messages=messages + ) return response.choices[0].message.content except Exception as e: return f"Error: {str(e)}" -@bot.event -async def on_ready(): - print(f'{bot.user} has connected to Discord!') - @bot.event async def on_message(message): # Ignore messages from the bot itself @@ -81,14 +109,25 @@ async def on_message(message): # Remove bot mention from the message user_message = message.content.replace(f'<@{bot.user.id}>', '').strip() + # Collect image URLs from the message + image_urls = [] + for attachment in message.attachments: + if any(attachment.filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']): + image_urls.append(attachment.url) + # Get AI response - response = await get_ai_response(history, user_message) + response = await get_ai_response(history, user_message, image_urls) # Send response await message.reply(response) await bot.process_commands(message) +@bot.event +async def on_ready(): + print(f'{bot.user} has connected to Discord!') + + def main(): if not all([DISCORD_TOKEN, OPENAI_API_KEY, OPENWEBUI_API_BASE, MODEL_NAME]): print("Error: Missing required environment variables") From 42a4cd7216bc5173697b22e21010428026f44ba2 Mon Sep 17 00:00:00 2001 From: jknapp Date: Fri, 3 Jan 2025 03:44:38 +0000 Subject: [PATCH 3/3] Delete v2/.env --- v2/.env | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 v2/.env diff --git a/v2/.env b/v2/.env deleted file mode 100644 index 20240c5..0000000 --- a/v2/.env +++ /dev/null @@ -1,4 +0,0 @@ -DISCORD_TOKEN="MTMxOTQ2NjUwMDM2MDUwMzM4OA.GHRFj6.xb-SgxDr6s_XnxqJvu_dlwKWEjvRNoCfxFNDu4" -OPENAI_API_KEY="sk-2cdddfca57424dbfae6e9f474b239eb4" -OPENWEBUI_API_BASE="https://chat.dnspegasus.net/api" -MODEL_NAME="discord-ai-bot" \ No newline at end of file