dedicated slash command
All checks were successful
Build and Push Discord Bot Docker Image / build (push) Successful in 58s
All checks were successful
Build and Push Discord Bot Docker Image / build (push) Successful in 58s
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Cavepedia Discord Bot - Entry point.
|
Cavepedia Discord Bot - Entry point.
|
||||||
|
|
||||||
A Discord bot that provides access to the Cavepedia AI assistant.
|
A Discord bot that provides access to the Cavepedia AI assistant via /cavesearch command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -28,6 +28,7 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
from src.config import Config
|
from src.config import Config
|
||||||
from src.agent_client import AgentClient
|
from src.agent_client import AgentClient
|
||||||
@@ -38,14 +39,11 @@ class CavepediaBot(discord.Client):
|
|||||||
"""Discord bot for Cavepedia AI assistant."""
|
"""Discord bot for Cavepedia AI assistant."""
|
||||||
|
|
||||||
def __init__(self, config: Config):
|
def __init__(self, config: Config):
|
||||||
# Set up intents
|
|
||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
intents.message_content = True # Required to read message content
|
|
||||||
intents.guilds = True
|
|
||||||
|
|
||||||
super().__init__(intents=intents)
|
super().__init__(intents=intents)
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.tree = app_commands.CommandTree(self)
|
||||||
self.agent_client = AgentClient(
|
self.agent_client = AgentClient(
|
||||||
base_url=config.agent_url,
|
base_url=config.agent_url,
|
||||||
default_roles=config.default_roles,
|
default_roles=config.default_roles,
|
||||||
@@ -59,6 +57,22 @@ class CavepediaBot(discord.Client):
|
|||||||
async def setup_hook(self):
|
async def setup_hook(self):
|
||||||
"""Called when the bot is starting up."""
|
"""Called when the bot is starting up."""
|
||||||
await self.agent_client.start()
|
await self.agent_client.start()
|
||||||
|
|
||||||
|
# Register the cavesearch command
|
||||||
|
@self.tree.command(name="cavesearch", description="Search the caving knowledge base")
|
||||||
|
@app_commands.describe(query="Your question about caving")
|
||||||
|
async def cavesearch(interaction: discord.Interaction, query: str):
|
||||||
|
await self.handle_search(interaction, query)
|
||||||
|
|
||||||
|
# Sync commands to specific guilds for instant availability
|
||||||
|
for guild_id in [1137321345718439959, 1454125232439955471]:
|
||||||
|
guild = discord.Object(id=guild_id)
|
||||||
|
self.tree.copy_global_to(guild=guild)
|
||||||
|
try:
|
||||||
|
await self.tree.sync(guild=guild)
|
||||||
|
logger.info(f"Commands synced to guild {guild_id}")
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
logger.warning(f"No access to guild {guild_id}, skipping sync")
|
||||||
logger.info("Bot setup complete")
|
logger.info("Bot setup complete")
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
@@ -70,7 +84,6 @@ class CavepediaBot(discord.Client):
|
|||||||
"""Called when the bot has connected to Discord."""
|
"""Called when the bot has connected to Discord."""
|
||||||
logger.info(f"Logged in as {self.user} (ID: {self.user.id})")
|
logger.info(f"Logged in as {self.user} (ID: {self.user.id})")
|
||||||
logger.info(f"Allowed channels: {self.config.allowed_channels}")
|
logger.info(f"Allowed channels: {self.config.allowed_channels}")
|
||||||
logger.info(f"Ambient channels: {self.config.ambient_channels}")
|
|
||||||
|
|
||||||
# Check agent health
|
# Check agent health
|
||||||
if await self.agent_client.health_check():
|
if await self.agent_client.health_check():
|
||||||
@@ -78,81 +91,49 @@ class CavepediaBot(discord.Client):
|
|||||||
else:
|
else:
|
||||||
logger.warning("Agent server health check failed")
|
logger.warning("Agent server health check failed")
|
||||||
|
|
||||||
async def on_message(self, message: discord.Message):
|
async def handle_search(self, interaction: discord.Interaction, query: str):
|
||||||
"""Handle incoming messages."""
|
"""Handle the /cavesearch command."""
|
||||||
# Ignore messages from the bot itself
|
# Check if channel is allowed
|
||||||
if message.author == self.user:
|
if interaction.channel_id not in self.config.allowed_channels:
|
||||||
return
|
await interaction.response.send_message(
|
||||||
|
"This command is not available in this channel.",
|
||||||
# Ignore DMs
|
ephemeral=True,
|
||||||
if message.guild is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
channel_id = message.channel.id
|
|
||||||
|
|
||||||
# Check if this is an allowed channel
|
|
||||||
if channel_id not in self.config.allowed_channels:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Determine if we should respond
|
|
||||||
is_ambient = channel_id in self.config.ambient_channels
|
|
||||||
is_mentioned = self.user in message.mentions
|
|
||||||
|
|
||||||
# In ambient channels, respond to all messages
|
|
||||||
# In non-ambient allowed channels, only respond to mentions
|
|
||||||
if not is_ambient and not is_mentioned:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Extract the actual query (remove bot mention if present)
|
|
||||||
query = message.content
|
|
||||||
if is_mentioned:
|
|
||||||
# Remove the mention from the message
|
|
||||||
query = query.replace(f"<@{self.user.id}>", "").strip()
|
|
||||||
query = query.replace(f"<@!{self.user.id}>", "").strip()
|
|
||||||
|
|
||||||
# Skip empty messages
|
|
||||||
if not query:
|
|
||||||
await message.reply(
|
|
||||||
"Please include a question after mentioning me. "
|
|
||||||
"For example: @Cavepedia What is the deepest cave in Virginia?"
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check rate limits
|
# Check rate limits
|
||||||
allowed, error_msg = self.rate_limiter.check(message.author.id)
|
allowed, error_msg = self.rate_limiter.check(interaction.user.id)
|
||||||
if not allowed:
|
if not allowed:
|
||||||
await message.reply(error_msg)
|
await interaction.response.send_message(error_msg, ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show typing indicator while processing
|
# Defer response since agent calls take time
|
||||||
async with message.channel.typing():
|
await interaction.response.defer()
|
||||||
try:
|
|
||||||
logger.info(
|
|
||||||
f"Processing query from {message.author} in #{message.channel.name}: {query[:100]}..."
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await self.agent_client.query(query)
|
try:
|
||||||
|
logger.info(
|
||||||
|
f"Processing query from {interaction.user} in #{interaction.channel}: {query[:100]}..."
|
||||||
|
)
|
||||||
|
|
||||||
# Discord has a 2000 character limit
|
response = await self.agent_client.query(query)
|
||||||
if len(response) > 2000:
|
|
||||||
# Split into chunks, trying to break at newlines
|
|
||||||
chunks = self._split_response(response, max_length=1900)
|
|
||||||
for i, chunk in enumerate(chunks):
|
|
||||||
if i == 0:
|
|
||||||
await message.reply(chunk)
|
|
||||||
else:
|
|
||||||
await message.channel.send(chunk)
|
|
||||||
else:
|
|
||||||
await message.reply(response)
|
|
||||||
|
|
||||||
logger.info(f"Response sent to {message.author}")
|
# Discord has a 2000 character limit
|
||||||
|
if len(response) > 2000:
|
||||||
|
chunks = self._split_response(response, max_length=1900)
|
||||||
|
await interaction.followup.send(chunks[0])
|
||||||
|
for chunk in chunks[1:]:
|
||||||
|
await interaction.channel.send(chunk)
|
||||||
|
else:
|
||||||
|
await interaction.followup.send(response)
|
||||||
|
|
||||||
except Exception as e:
|
logger.info(f"Response sent to {interaction.user}")
|
||||||
logger.error(f"Error processing query: {e}", exc_info=True)
|
|
||||||
await message.reply(
|
except Exception as e:
|
||||||
"Sorry, I encountered an error processing your question. "
|
logger.error(f"Error processing query: {e}", exc_info=True)
|
||||||
"Please try again later."
|
await interaction.followup.send(
|
||||||
)
|
"Sorry, I encountered an error processing your question. "
|
||||||
|
"Please try again later."
|
||||||
|
)
|
||||||
|
|
||||||
def _split_response(self, text: str, max_length: int = 1900) -> list[str]:
|
def _split_response(self, text: str, max_length: int = 1900) -> list[str]:
|
||||||
"""Split a long response into chunks that fit Discord's limit."""
|
"""Split a long response into chunks that fit Discord's limit."""
|
||||||
@@ -187,7 +168,7 @@ def main():
|
|||||||
logger.info(f"Default roles: {config.default_roles}")
|
logger.info(f"Default roles: {config.default_roles}")
|
||||||
|
|
||||||
bot = CavepediaBot(config)
|
bot = CavepediaBot(config)
|
||||||
bot.run(config.discord_token, log_handler=None) # We handle logging ourselves
|
bot.run(config.discord_token, log_handler=None)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user