2025-02-02 14:10:51 -05:00
// 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.
2025-02-09 14:58:31 -05:00
import { SlashCommandBuilder } from 'discord.js' ;
import axios from 'axios' ;
2025-02-02 14:10:51 -05:00
const config = {
webSearch : {
enabled : false , // Default web search state
allowOverride : true , // Whether users can override the default state
} ,
} ;
2025-02-09 14:58:31 -05:00
export default {
2025-02-02 14:10:51 -05:00
data : new SlashCommandBuilder ( )
. setName ( 'ask' )
. setDescription ( 'Ask a question to the AI' )
. addStringOption ( option =>
2025-02-02 14:15:11 -05:00
option . setName ( 'prompt' ) . setDescription ( 'Your question or prompt' ) . setRequired ( true ) ,
2025-02-02 14:10:51 -05:00
)
. addBooleanOption ( option =>
option
. setName ( 'websearch' )
. setDescription ( 'Enable web search for more up-to-date information' )
2025-02-02 14:15:11 -05:00
. setRequired ( false ) ,
2025-02-06 20:00:24 -05:00
) ,
2025-02-02 14:10:51 -05:00
async execute ( interaction ) {
await interaction . deferReply ( ) ;
const prompt = interaction . options . getString ( 'prompt' ) ;
const userWebSearchOption = interaction . options . getBoolean ( 'websearch' ) ;
// Determine if web search should be enabled based on config and user option
const webSearchEnabled =
config . webSearch . allowOverride && userWebSearchOption !== null
? userWebSearchOption // Use user's choice if override is allowed and option was provided
: config . webSearch . enabled ; // Otherwise use default config
try {
const response = await axios . post (
'https://openrouter.ai/api/v1/chat/completions' ,
{
model : webSearchEnabled
2025-03-18 14:36:52 -04:00
? 'google/gemma-3-27b-it:free:online'
: 'google/gemma-3-27b-it:free' ,
2025-02-02 14:10:51 -05:00
messages : [
{
role : 'system' ,
content :
2025-03-24 20:11:02 -04:00
'You are kekbot, a highly celebrated and knowledgeable computer scientist with decades of experience in various fields of computing. You are known for your ability to explain complex topics in a clear, concise, and insightful manner. Provide direct and to-the-point answers, avoiding unnecessary elaboration or repetition. Focus on delivering accurate and valuable information efficiently.' ,
2025-02-02 14:10:51 -05:00
} ,
{ role : 'user' , content : prompt } ,
] ,
plugins : webSearchEnabled
? [
{
id : 'web' ,
max _results : 3 ,
search _prompt :
` A web search was conducted on ${ new Date ( ) . toISOString ( ) } . ` +
'Incorporate the following web search results into your response. ' +
'IMPORTANT: Cite them using markdown links named using the domain of the source. ' +
'Example: [nytimes.com](https://nytimes.com/some-page).' ,
} ,
]
: undefined ,
} ,
{
headers : {
Authorization : ` Bearer ${ process . env . OPENROUTER _API _KEY } ` ,
'HTTP-Referer' : 'https://github.com/hllywluis/kekbot.js' ,
'Content-Type' : 'application/json' ,
} ,
2025-02-02 14:15:11 -05:00
} ,
2025-02-02 14:10:51 -05:00
) ;
const aiResponse = response . data . choices [ 0 ] . message . content ;
2025-02-02 14:15:11 -05:00
const webSearchStatus = webSearchEnabled ? '\n> *Web search enabled* 🔍\n' : '' ;
2025-02-02 14:10:51 -05:00
const formattedResponse = ` > **Question:** ${ prompt } ${ webSearchStatus } \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 ( '```' ) ;
2025-02-02 14:15:11 -05:00
if ( lastCodeBlock !== - 1 && ! chunk . slice ( lastCodeBlock ) . includes ( '\n```' ) ) {
2025-02-02 14:10:51 -05:00
// 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 ;
}
2025-02-02 16:22:33 -05:00
// Send chunks sequentially using reduce
await chunks . reduce (
( promise , chunk ) =>
promise . then ( async ( ) => {
try {
await interaction . followUp ( chunk ) ;
return undefined ;
} catch ( error ) {
console . error ( 'Error sending message chunk:' , {
chunkLength : chunk . content . length ,
error : error . message ,
} ) ;
// Attempt to send error notification
try {
await interaction . followUp ( {
content : 'Failed to send complete response. Please try again.' ,
ephemeral : true ,
} ) ;
} catch ( e ) {
// If even the error notification fails, log it
console . error ( 'Failed to send error notification:' , e . message ) ;
}
// Reject to stop processing remaining chunks
return Promise . reject ( error ) ;
}
} ) ,
Promise . resolve ( ) ,
) ;
2025-02-02 14:10:51 -05:00
}
} catch ( error ) {
2025-02-02 16:22:33 -05:00
// Log detailed error information
console . error ( 'Error in ask command:' , {
message : error . message ,
response : error . response ? . data ,
status : error . response ? . status ,
stack : error . stack ,
} ) ;
// Provide more specific error messages to users
let errorMessage = 'Sorry, there was an error processing your request.' ;
if ( error . response ? . status === 429 ) {
errorMessage = 'The AI service is currently busy. Please try again in a few moments.' ;
} else if ( error . code === 'ECONNABORTED' || error . code === 'ETIMEDOUT' ) {
errorMessage = 'The request timed out. Please try again.' ;
} else if ( error . response ? . status === 400 ) {
errorMessage = 'Invalid request. Please try rephrasing your question.' ;
}
2025-02-02 14:10:51 -05:00
await interaction . followUp ( {
2025-02-02 16:22:33 -05:00
content : errorMessage ,
2025-02-02 14:10:51 -05:00
ephemeral : true ,
} ) ;
}
} ,
} ;