Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ea339f2
Implement the plugin system
zavocc Mar 8, 2026
af7edf2
Add litellm support
zavocc Mar 8, 2026
fac645a
Adds check if imported plugin module have StoragePlugin class
zavocc Mar 8, 2026
57e0cbe
Add run script to make it easier to load custom model config
zavocc Mar 9, 2026
29269b9
Update dockerfile and install plugin dependencies if needed
zavocc Mar 9, 2026
94fd43c
Ensure requirements.txt is properly checked
zavocc Mar 9, 2026
84edcd1
Add a way to disable storage backends and use direct Discord URLs for…
zavocc Mar 9, 2026
bfd2198
[UNTESTED] Generative media model updates:
zavocc Mar 9, 2026
4b5d543
Remove Imagen 4 and Nano Banana 1 model
zavocc Mar 9, 2026
9b3c74e
Update InternetSearch tool to properly support YouTube watcher tool
zavocc Mar 10, 2026
d5a99e2
Update tips message and other interstitials to make it less noisy as
zavocc Mar 13, 2026
6a99f39
Fix integer overflow when using get_user_info tool which causes
zavocc Mar 16, 2026
c7e43b5
Update browse tool to use tavily search
zavocc Mar 19, 2026
f0f4cba
TO BE REVERSED: Temporarily play with chains of thought in the chat for
zavocc Mar 19, 2026
685837b
Revert "TO BE REVERSED: Temporarily play with chains of thought in th…
zavocc Mar 25, 2026
a465761
New and updated tools
zavocc Mar 26, 2026
d4dac40
Remove unused imports
zavocc Mar 27, 2026
08a0844
Use direct URLs for Google models instead of Files API
zavocc Apr 14, 2026
2218410
Revert "Use direct URLs for Google models instead of Files API"
zavocc Apr 16, 2026
7814570
use the default sampling parameters for google models
zavocc Apr 16, 2026
6956696
Use merge sort order instead of guessing which keys to pop
zavocc Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ _wavelink/logs
chat_history.db
dev.env
chroma.log
emojis.yaml
emojis.yaml
.zed/
13 changes: 8 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ RUN useradd -u 6969 --home-dir /jakeybot jakey
# Copy the source code
COPY . .

# Install C compiler
# Install C compiler and Nano text editor
RUN apt-get update
RUN apt-get install g++ --no-install-recommends --yes
RUN apt-get install g++ nano --no-install-recommends --yes

# Correct ownership
RUN chown -R 6969:6969 /jakeybot

# Change the user
USER jakey

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Install base dependencies and optionally plugin dependencies
RUN pip install --no-cache-dir -r /jakeybot/requirements.txt && \
if [ -f /jakeybot/plugins/requirements.txt ]; then \
pip install --no-cache-dir -r /jakeybot/plugins/requirements.txt; \
fi

# Start the bot
CMD ["python", "main.py"]
ENTRYPOINT ["/bin/bash", "/jakeybot/run.sh"]
33 changes: 15 additions & 18 deletions cogs/ai/generative_chat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from core.exceptions import *
from core.exceptions import CustomErrorMessage
from core.database import History as typehint_History
from discord import Message
from models.core import set_assistant_type
Expand Down Expand Up @@ -49,11 +49,11 @@ async def _ask(self, prompt: Message):
_chat_history = await load_history(user_id=prompt.author.id, thread_name=_thread_name, db_conn=self.DBConn)

# Check for /chat:ephemeral only if enable_threads is true
if not "/chat:ephemeral" in prompt.content:
if "/chat:ephemeral" not in prompt.content:
_append_history = True
else:
await prompt.channel.send("🔒 This conversation is not saved and Jakey won't remember this")
_append_history = False
await prompt.channel.send("> -# This conversation is not saved and Jakey won't remember this")
_append_history = False
else:
await prompt.channel.send("> -# ⚠️ This model doesn't support threads and therefore this interaction won't remember the previous and won't be saved.")
_chat_history = None
Expand All @@ -70,7 +70,6 @@ async def _ask(self, prompt: Message):
if prompt.attachments:
if _model_props.enable_files:
_uploadedFilesCount = 0
_processFileInterstitial = await prompt.channel.send("⬆️ Please wait...")
for _attachment in prompt.attachments:
# Check for alt text
_extraMetadata = inspect.cleandoc(
Expand All @@ -83,10 +82,9 @@ async def _ask(self, prompt: Message):
</meta>
""")
await _chat_session.upload_files(attachment=_attachment, extra_metadata=_extraMetadata)

# Update status
_uploadedFilesCount += 1
await _processFileInterstitial.edit(f"✅ Added: **{_uploadedFilesCount}** file(s)...")
else:
raise CustomErrorMessage("⚠️ This model doesn't support file attachments, please choose another model to continue")

Expand Down Expand Up @@ -130,21 +128,21 @@ async def on_message(self, message: Message):
_command = message.content.split(" ")[0].replace(self.bot.command_prefix, "")
if self.bot.get_command(_command):
return

# User ID
_userID = message.author.id

# Check if the user is in the pending list
if _userID in self.pending_ids:
await message.reply("⚠️ I'm still processing your previous request, please wait for a moment...")
return

# Check if the bot was only mentioned without any content or image attachments
# If none, then on main.py event, proceed sending the introductory message
if not message.attachments \
and not re.sub(f"<@{self.bot.user.id}>", '', message.content).strip():
return

# Remove the mention from the prompt
message.content = re.sub(f"<@{self.bot.user.id}>", '', message.content).strip()

Expand All @@ -154,13 +152,13 @@ async def on_message(self, message: Message):
# Skip if the mentioned user is the bot itself
if _mentioned_user.id == self.bot.user.id:
continue

# Get member object for guild-specific display name, fallback to user if not in guild
if message.guild:
_member = message.guild.get_member(_mentioned_user.id)
else:
_member = None

_user_metadata = inspect.cleandoc(
f"""<additional_user_metadata_pinged>
Username: @{_mentioned_user.name}
Expand All @@ -172,7 +170,7 @@ async def on_message(self, message: Message):
</additional_user_metadata_pinged>"""
)
_mentioned_users_metadata.append(_user_metadata)

# Append mentioned users metadata to the message content
if _mentioned_users_metadata:
message.content = message.content + "\n\n" + "\n".join(_mentioned_users_metadata)
Expand All @@ -183,20 +181,20 @@ async def on_message(self, message: Message):
_context_message = await message.channel.fetch_message(message.reference.message_id)
message.content = inspect.cleandoc(
f"""<reply_metadata>

# Replying to referenced message excerpt from {_context_message.author.display_name} (username: @{_context_message.author.name}):
<|begin_msg_contexts|diff>
{_context_message.content}
<|end_msg_contexts|diff>

<constraints>Do not echo this metadata, only use for retrieval purposes</constraints>
</reply_metadata>
{message.content}"""
)
await message.channel.send(f" Referenced message: {_context_message.jump_url}")
await message.channel.send(f"> -# Referenced message: {_context_message.jump_url}")


# For now the entire function is under try
# For now the entire function is under try
# Maybe this can be separated into another function
try:
# Add the user to the pending list
Expand Down Expand Up @@ -232,4 +230,3 @@ async def on_message(self, message: Message):
# Remove the user from the pending list
if _userID in self.pending_ids:
self.pending_ids.remove(_userID)

13 changes: 7 additions & 6 deletions cogs/ai/tasks/avatartools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from discord.ext import commands
from discord import Member, DiscordException
from os import environ
import asyncio
import base64
import discord
import importlib
Expand Down Expand Up @@ -197,13 +196,15 @@ async def remix(self, ctx: discord.ApplicationContext, style: str, user: Member
_params = {
"prompt": _crafted_prompt,
"image_urls": [_avatar_url],
"limit_generations": True,
"num_images": 1
}

# Run the image generation
_imageURL = await run_image(
model_name="gemini-25-flash-image/edit",
_image_payload = await run_image(
model_name="nano-banana-2/edit",
aiohttp_session=self.bot.aiohttp_instance,
send_url_only=True,
send_bytes=False,
**_params
)

Expand All @@ -213,8 +214,8 @@ async def remix(self, ctx: discord.ApplicationContext, style: str, user: Member
description=f"Here's a remixed avatar of {_user.name}",
color=discord.Color.random()
)
_embed.set_image(url=_imageURL[0])
_embed.set_footer(text=f"Powered by Nano Banana")
_embed.set_image(url=_image_payload["images_urls"][0])
_embed.set_footer(text=f"Powered by Nano Banana 2")
await ctx.respond(embed=_embed, ephemeral=True)

@remix.error
Expand Down
28 changes: 19 additions & 9 deletions core/startup.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# plugins
from plugins.storage_plugin import StoragePluginLoader

from discord.ext import bridge
from google import genai
from os import environ
Expand All @@ -10,7 +13,22 @@ class SubClassBotPlugServices(bridge.Bot):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

async def start_services(self):
# Load storage plugin
self.plugins_storage = StoragePluginLoader()

def start_plugins(self):
# Start storage plugin client if it has start method
if hasattr(self.plugins_storage, 'start_storage_client'):
self.plugins_storage.start_storage_client()
logging.info("Storage plugin client started successfully")

async def stop_plugins(self):
# Close storage plugin client if it has close method
if hasattr(self.plugins_storage, 'close_storage_client'):
await self.plugins_storage.close_storage_client()
logging.info("Storage plugin client closed successfully")

def start_services(self):
# Gemini API Client
self.gemini_api_client = genai.Client(api_key=environ.get("GEMINI_API_KEY"))
logging.info("Gemini API client initialized successfully")
Expand All @@ -27,14 +45,6 @@ async def start_services(self):
)
logging.info("OpenAI client for OpenRouter initialized successfully")

# OpenAI client for Groq based models
# NOTE: Use litellm SDK instead of OpenAI SDK for Groq models
#self.openai_client_groq = openai.AsyncClient(
# api_key=environ.get("GROQ_API_KEY"),
# base_url="https://api.groq.com/openai/v1"
#)
#logging.info("OpenAI client for Groq initialized successfully")

async def stop_services(self):
# Close aiohttp client sessions
await self.aiohttp_instance.close()
Expand Down
41 changes: 24 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
dotenv.load_dotenv("dev.env")

# Logging
logging.basicConfig(format='%(levelname)s %(asctime)s [%(pathname)s:%(lineno)d - %(module)s.%(funcName)s()]: %(message)s',
datefmt='%m/%d/%Y %I:%M:%S %p',
logging.basicConfig(format='%(levelname)s %(asctime)s [%(pathname)s:%(lineno)d - %(module)s.%(funcName)s()]: %(message)s',
datefmt='%m/%d/%Y %I:%M:%S %p',
level=logging.INFO)

# Check if TOKEN is set
Expand Down Expand Up @@ -51,14 +51,17 @@ def __init__(self, *args, **kwargs):
mkdir(environ.get("TEMP_DIR"))

# Initialize SDK clients
self.loop.create_task(self.start_services())
self.start_services()
logging.info("Services initialized successfully")

# Initialize Plugins
self.start_plugins()
logging.info("Plugins initialized successfully")

# HTTP Client
self.aiohttp_instance = aiohttp.ClientSession(loop=self.loop)
logging.info("HTTP client session initialized successfully")


def _lock_socket_instance(self, port):
try:
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Expand All @@ -67,15 +70,19 @@ def _lock_socket_instance(self, port):
except socket.error as e:
logging.error("Failed to bind socket port: %s, reason: %s", port, str(e))
raise e

async def on_ready(self):
await self.change_presence(activity=discord.Game(f"Preparing the bot for it's first use..."))
await self.change_presence(activity=discord.Game("Preparing the bot for it's first use..."))
#https://stackoverflow.com/a/65780398 - for multiple statuses
await self.change_presence(activity=discord.Game(f"@ me to get started!"))
await self.change_presence(activity=discord.Game("@ me to get started!"))
logging.info("%s is ready and online!", self.user)

# Shutdown the bot
async def close(self):
# Close Plugins
await self.stop_plugins()
logging.info("Plugins stopped successfully")

# Close services
await self.stop_services()
logging.info("Services stopped successfully")
Expand All @@ -84,7 +91,7 @@ async def close(self):
if Path(environ.get("TEMP_DIR", "temp")).exists():
for file in Path(environ.get("TEMP_DIR", "temp")).iterdir():
await aiofiles.os.remove(file)

# Close socket
self._socket.close()

Expand All @@ -102,7 +109,7 @@ async def on_message(message: discord.Message):

if message.author == bot.user:
return

# Check if the bot was only mentioned without any content or image attachments
# On generative ask command, the same logic is used but it will just invoke return and the bot will respond with this
if bot.user.mentioned_in(message) \
Expand All @@ -113,15 +120,15 @@ async def on_message(message: discord.Message):
I am an AI bot and I can also make your server fun and entertaining! 🎉

You just pinged me, but what can I do for you? 🤔
- You can ask me anything by typing **/ask** and get started or by mentioning me again but with a message
- You can access most of my useful commands with **/**slash commands or use `{bot.command_prefix}help` to see the list prefixed commands I have.

- You can ask me anything by mentioning me with a message
- You can access most of my useful commands with **/**slash commands or ask me what I can do to pull my internal knowledge base.
- You can access my apps by **tapping and holding any message** or **clicking the three-dots menu** and click **Apps** to see the list of apps I have

You can ask me questions, such as:
- **@{bot.user.name}** How many R's in the word strawberry?
- **/ask** `prompt:`Can you tell me a joke?
- Hey **@{bot.user.name}** can you give me quotes for today?
- **@{bot.user.name}** How many R's in the word strawberry?
- Hey **@{bot.user.name}** can you give me quotes for today?
- **@{bot.user.name}** list me your slash commands

If you have any questions, you can visit my [documentation or contact me here](https://zavocc.github.io)"""))

Expand All @@ -135,4 +142,4 @@ async def on_message(message: discord.Message):
logging.error("cogs.%s failed to load, skipping... The following error of the cog: %s", command, e)
continue

bot.run(environ.get('TOKEN'))
bot.run(environ.get('TOKEN'))
9 changes: 9 additions & 0 deletions models/chat_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@
from core.database import History
from core.exceptions import CustomErrorMessage
import aiofiles
import aiohttp
import logging
import yaml
# Methods for generative_chat.py

# Download File Attachments method
async def download_attachment_to_file(attachment_url: str, file_path: str, aiohttp_session: aiohttp.ClientSession) -> None:
async with aiohttp_session.get(attachment_url, allow_redirects=True) as file_dl:
async with aiofiles.open(file_path, "wb") as filepath:
async for _chunk in file_dl.content.iter_chunked(8192):
await filepath.write(_chunk)
logging.info("File downloaded successfully to %s", file_path)

# Fetch and validate models
async def fetch_model(model_alias: str) -> ModelProps:
# Load the models list from YAML file
Expand Down
Loading
Loading