|
|
@@ -0,0 +1,149 @@
|
|
|
+# bot.py
|
|
|
+
|
|
|
+import os
|
|
|
+import asyncio
|
|
|
+import discord
|
|
|
+from discord import app_commands
|
|
|
+import requests
|
|
|
+from dotenv import load_dotenv
|
|
|
+
|
|
|
+# --- Configuration ---
|
|
|
+load_dotenv()
|
|
|
+
|
|
|
+DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
|
|
|
+PERPLEXICA_API_URL = "http://localhost:3000/api/search"
|
|
|
+
|
|
|
+# Hardcoded model configurations
|
|
|
+CHAT_MODEL = { "provider": "ollama", "name": "granite4:micro-h" }
|
|
|
+EMBEDDING_MODEL = { "provider": "ollama", "name": "ryanshillington/Qwen3-Embedding-0.6B:latest" }
|
|
|
+
|
|
|
+# In-memory store for user-selected optimization mode
|
|
|
+user_modes = {}
|
|
|
+DEFAULT_OPTIMIZATION_MODE = "balanced"
|
|
|
+DEFAULT_FOCUS_MODE = "webSearch"
|
|
|
+
|
|
|
+# --- Discord Bot Setup ---
|
|
|
+intents = discord.Intents.default()
|
|
|
+intents.message_content = True
|
|
|
+client = discord.Client(intents=intents)
|
|
|
+tree = app_commands.CommandTree(client)
|
|
|
+
|
|
|
+@client.event
|
|
|
+async def on_ready():
|
|
|
+ """Event handler for when the bot is ready."""
|
|
|
+ print(f'Logged in as {client.user}')
|
|
|
+ await tree.sync()
|
|
|
+ print("Slash commands synced.")
|
|
|
+
|
|
|
+# --- Slash Commands ---
|
|
|
+
|
|
|
+@tree.command(name="search", description="Search with Perplexica")
|
|
|
+async def search(interaction: discord.Interaction, *, query: str):
|
|
|
+ """Performs a search using the Perplexica API."""
|
|
|
+ await interaction.response.send_message(f"<@{interaction.user.id}> 🧠 Thinking about your query: \"{query}\"...")
|
|
|
+
|
|
|
+ # Get user's selected mode, default to balanced
|
|
|
+ optimization_mode = user_modes.get(interaction.user.id, DEFAULT_OPTIMIZATION_MODE)
|
|
|
+
|
|
|
+ # Construct the payload
|
|
|
+ payload = {
|
|
|
+ "query": query,
|
|
|
+ "focusMode": DEFAULT_FOCUS_MODE,
|
|
|
+ "optimizationMode": optimization_mode,
|
|
|
+ "chatModel": CHAT_MODEL,
|
|
|
+ "embeddingModel": EMBEDDING_MODEL,
|
|
|
+ "stream": False
|
|
|
+ }
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Send request to Perplexica API
|
|
|
+ response = requests.post(PERPLEXICA_API_URL, json=payload, timeout=300)
|
|
|
+ response.raise_for_status() # Raise an exception for bad status codes
|
|
|
+
|
|
|
+ data = response.json()
|
|
|
+ message = data.get("message", "No answer found.")
|
|
|
+ sources = data.get("sources", [])
|
|
|
+
|
|
|
+ # Format the response
|
|
|
+ embed = discord.Embed(
|
|
|
+ title=f"🔎 {query}",
|
|
|
+ description=message,
|
|
|
+ color=discord.Color.blue()
|
|
|
+ )
|
|
|
+
|
|
|
+ if sources:
|
|
|
+ source_links = []
|
|
|
+ for i, source in enumerate(sources, 1):
|
|
|
+ title = source.get("title")
|
|
|
+ url = source.get("url")
|
|
|
+ if title and url:
|
|
|
+ source_links.append(f"[{i}] {title} ({url})")
|
|
|
+
|
|
|
+ # Join sources and add to a field, handling character limits
|
|
|
+ source_text = "\n".join(source_links)
|
|
|
+ if len(source_text) > 1024:
|
|
|
+ source_text = source_text[:1021] + "..."
|
|
|
+
|
|
|
+ embed.add_field(name="🔗 Sources", value=source_text, inline=False)
|
|
|
+
|
|
|
+ # Handle overall message length
|
|
|
+ if len(embed) > 2000:
|
|
|
+ original_description = embed.description
|
|
|
+
|
|
|
+ # Calculate available space for description in the first message
|
|
|
+ non_desc_len = len(embed) - len(original_description)
|
|
|
+
|
|
|
+ # Max length for description, keeping total embed length under 2000.
|
|
|
+ # With a small buffer for "..."
|
|
|
+ available_len = 2000 - non_desc_len - 10
|
|
|
+
|
|
|
+ # Ensure available_len is not negative
|
|
|
+ if available_len < 0:
|
|
|
+ available_len = 0
|
|
|
+
|
|
|
+ first_part = original_description[:available_len]
|
|
|
+ rest_of_message = original_description[available_len:]
|
|
|
+
|
|
|
+ embed.description = first_part
|
|
|
+ if rest_of_message:
|
|
|
+ embed.description += "..."
|
|
|
+
|
|
|
+ await interaction.edit_original_response(content=f"<@{interaction.user.id}>", embed=embed)
|
|
|
+
|
|
|
+ if rest_of_message:
|
|
|
+ # Split the rest of the message into chunks of 2000 characters
|
|
|
+ chunks = [rest_of_message[i:i + 2000] for i in range(0, len(rest_of_message), 2000)]
|
|
|
+ for i, chunk in enumerate(chunks):
|
|
|
+ await asyncio.sleep(1) # Add a small delay to prevent rate limiting issues
|
|
|
+ continuation_embed = discord.Embed(
|
|
|
+ title=f"🔎 {query} (continued {i + 1}/{len(chunks)})",
|
|
|
+ description=chunk,
|
|
|
+ color=discord.Color.blue()
|
|
|
+ )
|
|
|
+ await interaction.followup.send(content=f"<@{interaction.user.id}>", embed=continuation_embed)
|
|
|
+ else:
|
|
|
+ await interaction.edit_original_response(content=f"<@{interaction.user.id}>", embed=embed)
|
|
|
+
|
|
|
+ except requests.exceptions.RequestException as e:
|
|
|
+ await interaction.edit_original_response(content=f"An error occurred while contacting the Perplexica API: {e}")
|
|
|
+ except Exception as e:
|
|
|
+ await interaction.edit_original_response(content=f"An unexpected error occurred: {e}")
|
|
|
+
|
|
|
+
|
|
|
+@tree.command(name="mode", description="Set the search optimization mode")
|
|
|
+@app_commands.choices(mode=[
|
|
|
+ app_commands.Choice(name="Balanced", value="balanced"),
|
|
|
+ app_commands.Choice(name="Fast", value="speed"),
|
|
|
+])
|
|
|
+async def mode(interaction: discord.Interaction, mode: app_commands.Choice[str]):
|
|
|
+ """Sets the optimization mode for the user."""
|
|
|
+ user_modes[interaction.user.id] = mode.value
|
|
|
+ await interaction.response.send_message(f"✅ Your search mode has been set to **{mode.name}**.", ephemeral=True)
|
|
|
+
|
|
|
+
|
|
|
+# --- Run the Bot ---
|
|
|
+if __name__ == "__main__":
|
|
|
+ if DISCORD_BOT_TOKEN == "YOUR_DISCORD_BOT_TOKEN_HERE" or not DISCORD_BOT_TOKEN:
|
|
|
+ print("ERROR: Please set your DISCORD_BOT_TOKEN in the .env file.")
|
|
|
+ else:
|
|
|
+ client.run(DISCORD_BOT_TOKEN)
|