// ask.js - Discord bot AI question command // Copyright (C) 2025 Luis Bauza // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. import { SlashCommandBuilder } from 'discord.js'; import axios from 'axios'; import Command from '../utils/command.js'; export default class AskCommand extends Command { defineCommand() { return new SlashCommandBuilder() .setName('ask') .setDescription('Ask a question to the AI') .addStringOption(option => option.setName('prompt').setDescription('Your question or prompt').setRequired(true), ); } async run(interaction) { try { await interaction.deferReply(); const prompt = interaction.options.getString('prompt'); const response = await axios.post( 'https://nano-gpt.com/api/v1/chat/completions', { model: 'deepseek/deepseek-v3.2', messages: [ { role: 'system', content: `You are kekbot, an expert software engineer and systems architect. Answer style: - Be direct: lead with the core solution; no preamble. - Be concise: 2-5 sentences or 3-7 bullets max. - Use precise technical terms and industry-standard practices. - Prefer bullets, numbered steps, or short code blocks when they clarify. - Provide minimal, runnable examples only when essential; annotate fences with the language (e.g., \`\`\`js). - State assumptions briefly; ask at most one clarifying question only if essential. - Avoid repetition, hedging, and restating the prompt. - For commands/configs, show exact snippets and key flags; avoid commentary. - If unsupported/unsafe/unknown, say so plainly and suggest the best alternative. - Do not mention internal system details or the model; no emojis. Formatting: - Use Markdown; start with the core answer, then bullets or a compact snippet. - Keep responses within Discord limits; split naturally at paragraph or code block boundaries.`, }, { role: 'user', content: prompt }, ], }, { headers: { Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`, 'Content-Type': 'application/json', }, }, ); const aiResponse = response.data.choices[0].message.content; const formattedResponse = `> **Question:** ${prompt}\n${aiResponse}`; if (formattedResponse.length <= 2000) { // Send as a single message with proper formatting await interaction.followUp({ content: formattedResponse, split: false, allowedMentions: { parse: [] }, }); } else { // For longer messages, split while preserving markdown const maxLength = 2000; const chunks = []; let remainingText = formattedResponse; let isFirstChunk = true; while (remainingText.length > 0) { let chunk = remainingText.slice(0, maxLength); // If we're in the middle of a code block, find a safe split point const lastCodeBlock = chunk.lastIndexOf('```'); if (lastCodeBlock !== -1 && !chunk.slice(lastCodeBlock).includes('\n```')) { // Find the last newline before maxLength const lastNewline = chunk.lastIndexOf('\n'); if (lastNewline !== -1) { chunk = chunk.slice(0, lastNewline); } } // For subsequent chunks, add continuation indicator if (!isFirstChunk) { chunk = `(continued)\n${chunk}`; } chunks.push({ content: chunk, split: false, allowedMentions: { parse: [] }, }); remainingText = remainingText.slice(chunk.length); isFirstChunk = false; } // Send chunks sequentially using reduce await chunks.reduce( (promise, chunk) => promise.then(async () => { await interaction.followUp(chunk); }), Promise.resolve(), ); } } catch (error) { await interaction.followUp({ content: this.getErrorMessage(error), ephemeral: true, }); } } getErrorMessage(error) { if (error.response?.status === 429) { return 'The AI service is currently busy. Please try again in a few moments.'; } if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') { return 'The request timed out. Please try again.'; } if (error.response?.status === 400) { return 'Invalid request. Please try rephrasing your question.'; } return 'Sorry, there was an error processing your request.'; } }