diff --git a/docs/examples/discord-py-bot/.env.example b/docs/examples/discord-py-bot/.env.example index b4c7adf..abe2f91 100644 --- a/docs/examples/discord-py-bot/.env.example +++ b/docs/examples/discord-py-bot/.env.example @@ -1,3 +1,5 @@ -.env.example -DISCORD_TOKEN="YOUR_DISCORD_BOT_TOKEN_HERE" -PERPLEXITY_API_KEY="YOUR_PPLX_API_KEY_HERE" \ No newline at end of file +# Discord Bot Configuration +DISCORD_TOKEN="your_discord_bot_token_here" + +# Perplexity API Configuration +PERPLEXITY_API_KEY="your_perplexity_api_key_here" diff --git a/docs/examples/discord-py-bot/README.mdx b/docs/examples/discord-py-bot/README.mdx index 5aaa65b..02f667f 100644 --- a/docs/examples/discord-py-bot/README.mdx +++ b/docs/examples/discord-py-bot/README.mdx @@ -1,104 +1,156 @@ --- title: Perplexity Discord Bot -description: A runnable discord.py bot that adds a /ask_perplexity command to your server, using the Sonar API for web-connected answers. +description: A simple discord.py bot that integrates Perplexity's Sonar API to bring AI answers to your Discord server. sidebar_position: 4 keywords: [discord, bot, discord.py, python, chatbot, perplexity, sonar api, command, slash command] --- -# Perplexity Discord Bot +A simple `discord.py` bot that integrates [Perplexity's Sonar API](https://docs.perplexity.ai/) into your Discord server. Ask questions and get AI-powered answers with web access through slash commands or by mentioning the bot. -A runnable `discord.py` application that adds a `/ask_perplexity` slash command to your Discord server, using Perplexity's Sonar API for real-time, web-connected answers. +![Discord Bot Demo](../../static/img/discord-py-bot-demo.png) -## 🌟 Features +## ✨ Features -- **Direct Sonar API Integration**: Uses Perplexity's API for real-time, web-connected answers. -- **Slash Command Ready**: Implements a modern `/ask_perplexity` command that's easy to use. -- **Secure Configuration**: Uses environment variables (`.env`) for both Discord and Perplexity keys, keeping them safe. -- **Administrator Permissions**: Locked to server administrators by default for easy control over API usage. -- **Formatted Embeds**: Delivers answers in a clean, professional Discord embed. -- **Self-Contained Example**: Minimal, single-file code designed to be easy to run and understand. +- **🌐 Web-Connected AI**: Uses Perplexity's Sonar API for up-to-date information +- **⚡ Slash Command**: Simple `/ask` command for questions +- **💬 Mention Support**: Ask questions by mentioning the bot +- **🔗 Source Citations**: Automatically formats and links to sources +- **🔒 Secure Setup**: Environment-based configuration for API keys -## 📋 Prerequisites +## 🛠️ Prerequisites -- **Python 3.8+** -- **A Perplexity API Key** -- **A Discord Bot Token** + + + **Python 3.8+** installed on your system + + ```bash + python --version # Should be 3.8 or higher + ``` + + + + **Active Perplexity API Key** from [Perplexity AI Settings](https://www.perplexity.ai/settings/api) + + You'll need a paid Perplexity account to access the API. See the [pricing page](https://www.perplexity.ai/pricing) for current rates. + + + + **Discord Bot Token** from the [Discord Developer Portal](https://discord.com/developers/applications) + + + +## 🚀 Quick Start -## 🚀 Installation & Setup +### 1. Repository Setup -1. **Clone the Repository:** Fork and clone the `api-cookbook` repository to your local machine. +Clone the repository and navigate to the bot directory: + +```bash +git clone https://github.com/perplexity-ai/api-cookbook.git +cd api-cookbook/docs/examples/discord-py-bot/ +``` -2. **Navigate to this Example:** +### 2. Install Dependencies + +```bash +# Create a virtual environment (recommended) +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install required packages +pip install -r requirements.txt +``` + +### 3. Configure API Keys + + + + 1. Visit [Perplexity AI Account Settings](https://www.perplexity.ai/settings/api) + 2. Generate a new API key + 3. Copy the key to the .env file + + Keep your API key secure! Never commit it to version control or share it publicly. + + + + 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) + 2. Click **"New Application"** and give it a descriptive name + 3. Navigate to the **"Bot"** section + 4. Click **"Reset Token"** (or "Add Bot" if first time) + 5. Copy the bot token + + + + Copy the example environment file and add your keys: + ```bash - cd docs/examples/discord-py-bot/ + cp env.example .env ``` -3. **Install Dependencies:** + + Edit `.env` with your credentials: + + ```bash title=".env" + DISCORD_TOKEN="your_discord_bot_token_here" + PERPLEXITY_API_KEY="your_perplexity_api_key_here" + ``` + + + +## 🎯 Usage Guide + +### Bot Invitation & Setup + + + + In the Discord Developer Portal: + 1. Go to **OAuth2** → **URL Generator** + 2. Select scopes: `bot` and `applications.commands` + 3. Select bot permissions: `Send Messages`, `Use Slash Commands` + 4. Copy the generated URL + + + + 1. Paste the URL in your browser + 2. Select the Discord server to add the bot to + 3. Confirm the permissions + + + ```bash - pip install -r requirements.txt + python bot.py ``` -4. **Get Your Secret Keys:** - - **Perplexity API Key:** Get your key from the [Perplexity AI Account Settings](https://www.perplexity.ai/settings/api). - - **Discord Bot Token:** - 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications). - 2. Click **"New Application"** and give it a name. - 3. Go to the **"Bot"** tab and click **"Reset Token"** (or "Add Bot" first if needed). - 4. Copy the token. This is your bot's password – keep it safe! - -5. **Set Up Your Environment File:** - - Rename the `.env.example` file to `.env`. - - Open the `.env` file and add your secret keys: - ``` - DISCORD_TOKEN="YOUR_DISCORD_BOT_TOKEN_HERE" - PERPLEXITY_API_KEY="YOUR_PPLX_API_KEY_HERE" - ``` - -## 🔧 Usage - -### 1. Invite Your Bot to a Server - -- In the Discord Developer Portal, go to "OAuth2" -> "URL Generator". -- Select the `bot` and `applications.commands` scopes. -- Under "Bot Permissions," select **Administrator**. -- Copy the generated URL, paste it into your browser, and invite the bot to your server. - -### 2. Run the Bot Script - -Simply execute the `bot.py` script from your terminal: -```bash -python bot.py -This will connect the bot to Discord and sync the /ask_perplexity command. -3. Use the Command in Discord -In any channel in your server, an administrator can type: -code -Code -/ask_perplexity prompt:What is the latest news in the world of generative AI? -📄 Output Example -The bot will defer the interaction and then respond with a formatted embed containing the answer: -code -Code -HatchMate [APP] -🌐 Perplexity's Response - -The latest news in generative AI includes advancements in large language models... (and so on). - -Your Prompt``` -What is the latest news in the world of generative AI? -Requested by YourUsername -code -Code -## 🛠️ Extending the Bot - -- **Role-Based Access**: Modify the permission checks to allow specific roles, not just administrators. -- **Model Selection**: Add an option to the slash command to choose between different models like `sonar-pro` or `sonar-small-online`. -- **Conversation History**: Implement a database (like SQLite or Redis) to store recent messages and send them with new prompts for conversational context. - -## ⚠️ Limitations - -- The permission system is basic (administrator-only) to keep the example simple and database-free. -- The command is single-turn and does not remember conversation history. -- API rate limits may apply based on your Perplexity account status. - -## 🙏 Acknowledgements - -- This project uses the [Perplexity AI API](https://docs.perplexity.ai/). -- Built with the [discord.py](https://discordpy.readthedocs.io/en/stable/) library. \ No newline at end of file + + You should see output confirming the bot is online and commands are synced. + + + +### How to Use + +**Slash Command:** +``` +/ask [your question here] +``` + +![Slash Command Demo](../../static/img/discord-py-bot-slash-command.png) + +**Mention the Bot:** +``` +@YourBot [your question here] +``` + +![Mention Command Demo](../../static/img/discord-py-bot-mention-command.png) + +## 📊 Response Format + +The bot provides clean, readable responses with: +- **AI Answer**: Direct response from Perplexity's Sonar API +- **Source Citations**: Clickable links to sources (when available) +- **Automatic Truncation**: Responses are trimmed to fit Discord's limits + +## 🔧 Technical Details + +This bot uses: +- **Model**: Perplexity's `sonar-pro` model +- **Response Limit**: 2000 tokens from API, truncated to fit Discord +- **Temperature**: 0.2 for consistent, factual responses +- **No Permissions**: Anyone in the server can use the bot \ No newline at end of file diff --git a/docs/examples/discord-py-bot/bot.py b/docs/examples/discord-py-bot/bot.py index 6ee67be..8bb5bd3 100644 --- a/docs/examples/discord-py-bot/bot.py +++ b/docs/examples/discord-py-bot/bot.py @@ -1,119 +1,181 @@ -# This code is a simplified example of a Discord bot that integrates with the Perplexity AI API. This is a complete, minimal bot script that someone can run directly. Notice I have simplified the permission logic to only check for administrators to remove the need for an external database, making the example much more focused on the Perplexity API. - -# bot.py import os import discord from discord.ext import commands from discord import app_commands import openai from dotenv import load_dotenv +import logging +import re + +# Basic logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) -# --- Step 1: Load Environment Variables --- -# This ensures your secret keys are kept safe in a .env file. +# Load environment variables load_dotenv() DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY") -# --- Step 2: Bot Setup --- -# We define the bot's intents, which tell Discord what events our bot needs to receive. +# Bot setup intents = discord.Intents.default() +intents.message_content = True bot = commands.Bot(command_prefix="!", intents=intents) -# --- Step 3: Create a Command Cog --- -# Cogs are how modern discord.py bots organize commands, listeners, and state. -class AIChatCog(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot +# Perplexity client +perplexity_client = openai.OpenAI( + api_key=PERPLEXITY_API_KEY, + base_url="https://api.perplexity.ai" +) if PERPLEXITY_API_KEY else None - # Initialize the Perplexity AI Client using the key from our .env file. - # The `base_url` is the crucial part that directs the OpenAI library to Perplexity's API. - if PERPLEXITY_API_KEY: - self.perplexity_client = openai.OpenAI( - api_key=PERPLEXITY_API_KEY, - base_url="https://api.perplexity.ai" - ) - print("Perplexity AI Client Initialized.") - else: - self.perplexity_client = None - print("CRITICAL: PERPLEXITY_API_KEY not found in .env file.") +@bot.event +async def on_ready(): + """Bot startup""" + logger.info(f"Bot {bot.user} is ready!") + await bot.tree.sync() + logger.info("Commands synced") - # Define the slash command. - # The `has_permissions` check restricts this command to server administrators. - @app_commands.command(name="ask_perplexity", description="Ask a question to Perplexity AI (with web access).") - @app_commands.describe(prompt="The question you want to ask.") - @app_commands.checks.has_permissions(administrator=True) - async def ask_perplexity(self, interaction: discord.Interaction, prompt: str): - if not self.perplexity_client: - return await interaction.response.send_message( - "The Perplexity AI service is not configured on this bot.", - ephemeral=True - ) +@bot.tree.command(name="ask", description="Ask Perplexity AI a question") +@app_commands.describe(question="Your question") +async def ask(interaction: discord.Interaction, question: str): + """Ask Perplexity AI a question""" + if not perplexity_client: + await interaction.response.send_message("❌ Perplexity AI not configured", ephemeral=True) + return - # Defer the response to give the API time to process without a timeout. - await interaction.response.defer(thinking=True) + await interaction.response.defer() + + try: + response = perplexity_client.chat.completions.create( + model="sonar-pro", + messages=[ + { + "role": "system", + "content": "You are a helpful AI assistant. Provide clear, accurate answers with citations." + }, + {"role": "user", "content": question} + ], + max_tokens=2000, + temperature=0.2 + ) - try: - # Create the list of messages for the API call, following the standard format. - messages = [{"role": "user", "content": prompt}] - - # Call the Perplexity API with the desired model. - response = self.perplexity_client.chat.completions.create( - model="sonar-pro", - messages=messages - ) - - answer = response.choices[0].message.content + answer = response.choices[0].message.content + formatted_answer = format_citations(answer, response) + + # Truncate if too long + if len(formatted_answer) > 2000: + formatted_answer = formatted_answer[:1997] + "..." + + await interaction.followup.send(formatted_answer) + + except Exception as e: + logger.error(f"Error: {e}") + await interaction.followup.send("❌ Sorry, an error occurred. Please try again.", ephemeral=True) + +@bot.event +async def on_message(message): + """Handle mentions""" + if message.author == bot.user or message.author.bot: + return + + # Check if bot is mentioned + if bot.user in message.mentions and perplexity_client: + # Remove mention from content + content = message.content.replace(f'<@{bot.user.id}>', '').replace(f'<@!{bot.user.id}>', '').strip() + + if not content: + await message.reply("Hello! Ask me any question.") + return + + async with message.channel.typing(): + try: + response = perplexity_client.chat.completions.create( + model="sonar-pro", + messages=[ + { + "role": "system", + "content": "You are a helpful AI assistant. Provide clear, accurate answers with citations." + }, + {"role": "user", "content": content} + ], + max_tokens=2000, + temperature=0.2 + ) + + answer = response.choices[0].message.content + formatted_answer = format_citations(answer, response) + + # Truncate if too long + if len(formatted_answer) > 2000: + formatted_answer = formatted_answer[:1997] + "..." + + await message.reply(formatted_answer) + + except Exception as e: + logger.error(f"Error: {e}") + await message.reply("❌ Sorry, an error occurred. Please try again.") + + await bot.process_commands(message) - # Create and send a nicely formatted Discord embed for the response. - embed = discord.Embed( - title="🌐 Perplexity's Response", - description=answer, - color=discord.Color.from_rgb(0, 255, 0) # Perplexity Green - ) - embed.set_footer(text=f"Requested by {interaction.user.display_name}") +def format_citations(text: str, response_obj) -> str: + """Simple citation formatting that actually works""" + # Get search results from response + search_results = [] + + if hasattr(response_obj, 'search_results') and response_obj.search_results: + search_results = response_obj.search_results + elif hasattr(response_obj, 'model_dump'): + dumped = response_obj.model_dump() + search_results = dumped.get('search_results', []) + + if not search_results: + return text + + # Find existing citations like [1], [2], etc. + citation_pattern = r'\[(\d+)\]' + citations = re.findall(citation_pattern, text) + + if citations: + # Replace existing citations with clickable links + def replace_citation(match): + num = int(match.group(1)) + idx = num - 1 - # Truncate the original prompt if it's too long to fit in an embed field. - truncated_prompt = (prompt[:1020] + '...') if len(prompt) > 1024 else prompt - embed.add_field(name="Your Prompt", value=f"```{truncated_prompt}```", inline=False) + if 0 <= idx < len(search_results): + result = search_results[idx] + + # Extract URL from search result + url = "" + if isinstance(result, dict): + url = result.get('url', '') + elif hasattr(result, 'url'): + url = result.url + + if url: + return f"[[{num}]](<{url}>)" - await interaction.followup.send(embed=embed) + return f"[{num}]" + + text = re.sub(citation_pattern, replace_citation, text) + else: + # No citations in text, add them at the end + citations_list = [] + for i, result in enumerate(search_results[:5]): # Limit to 5 + url = "" + if isinstance(result, dict): + url = result.get('url', '') + elif hasattr(result, 'url'): + url = result.url - except Exception as e: - # Inform the user if an error occurs. - error_message = f"An error occurred with the Perplexity API: {e}" - print(error_message) - await interaction.followup.send(error_message, ephemeral=True) - - # A local error handler specifically for this command's permission check. - @ask_perplexity.error - async def on_ask_perplexity_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): - if isinstance(error, app_commands.MissingPermissions): - await interaction.response.send_message("You must be an administrator to use this command.", ephemeral=True) - else: - # For other errors, print them to the console. - print(f"An unhandled error occurred: {error}") - # Potentially send a generic error message back to the user as well. - if not interaction.response.is_done(): - await interaction.response.send_message("An unexpected error occurred.", ephemeral=True) - - -# --- Step 4: Main Bot Events and Startup Logic --- -@bot.event -async def on_ready(): - print(f'Logged in as {bot.user}!') - try: - # Add the cog to the bot so its commands are registered. - await bot.add_cog(AIChatCog(bot)) + if url: + citations_list.append(f"[[{i+1}]](<{url}>)") - # Sync the slash commands to Discord. This makes them appear for users. - synced = await bot.tree.sync() - print(f"Synced {len(synced)} command(s).") - except Exception as e: - print(f"Error during setup: {e}") + if citations_list: + text += "\n\n**Sources:** " + " ".join(citations_list) + + return text -# This is the entry point for running the bot. if __name__ == "__main__": if not DISCORD_TOKEN or not PERPLEXITY_API_KEY: - print("CRITICAL: DISCORD_TOKEN and/or PERPLEXITY_API_KEY not found in .env file. Bot cannot start.") + print("❌ Missing DISCORD_TOKEN or PERPLEXITY_API_KEY in .env file") else: bot.run(DISCORD_TOKEN) \ No newline at end of file diff --git a/docs/examples/discord-py-bot/requirements.txt b/docs/examples/discord-py-bot/requirements.txt index 5edec1f..18e3a13 100644 --- a/docs/examples/discord-py-bot/requirements.txt +++ b/docs/examples/discord-py-bot/requirements.txt @@ -1,4 +1,3 @@ -# requirements.txt -discord.py -openai -python-dotenv \ No newline at end of file +discord.py>=2.3.0 +openai>=1.0.0 +python-dotenv>=1.0.0 diff --git a/static/img/discord-py-bot-demo.png b/static/img/discord-py-bot-demo.png new file mode 100644 index 0000000..3f65c57 Binary files /dev/null and b/static/img/discord-py-bot-demo.png differ diff --git a/static/img/discord-py-bot-mention-command.png b/static/img/discord-py-bot-mention-command.png new file mode 100644 index 0000000..243a7ee Binary files /dev/null and b/static/img/discord-py-bot-mention-command.png differ diff --git a/static/img/discord-py-bot-slash-command.png b/static/img/discord-py-bot-slash-command.png new file mode 100644 index 0000000..b92e977 Binary files /dev/null and b/static/img/discord-py-bot-slash-command.png differ