feat: add Google AI command for Discord with token estimation and error handling

This commit is contained in:
2025-09-21 15:10:28 -05:00
parent eb74cbd86d
commit ebc3c7226d
4 changed files with 521 additions and 3 deletions

View File

@@ -0,0 +1,203 @@
import { GoogleGenAI } from "@google/genai";
import {CommandMessage} from "../../../core/types/commands";
import { TextChannel, DMChannel, NewsChannel, ThreadChannel } from "discord.js";
// Función para estimar tokens aproximadamente (1 token ≈ 4 caracteres para texto en español/inglés)
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
// Límites de tokens según Gemini 2.5 Flash
const MAX_INPUT_TOKENS = 1048576; // 1M tokens de entrada
const MAX_OUTPUT_TOKENS = 65536; // 64K tokens de salida
export const command: CommandMessage = {
name: 'ai',
type: "message",
aliases: ['chat', 'gemini'],
cooldown: 5,
run: async (message, args) => {
// Verificar que se proporcione un prompt
if (!args || args.length === 0) {
await message.reply({
content: "❌ **Error:** Necesitas proporcionar un mensaje para la IA.\n" +
"**Uso:** `ai <tu mensaje>`\n" +
"**Ejemplo:** `ai ¿Cómo funciona JavaScript?`"
});
return;
}
const prompt = args.join(' ');
// Validar longitud del prompt
if (prompt.length > 4000) {
await message.reply({
content: "❌ **Error:** Tu mensaje es demasiado largo. El límite es de 4000 caracteres."
});
return;
}
// Verificar que el canal sea de texto
const channel = message.channel as TextChannel | DMChannel | NewsChannel | ThreadChannel;
if (!channel || !('send' in channel)) {
await message.reply({
content: "❌ **Error:** Este comando no se puede usar en este tipo de canal."
});
return;
}
try {
// Inicializar Google Gemini con configuración desde variables de entorno
const genAI = new GoogleGenAI({
apiKey: process.env.GOOGLE_AI_API_KEY
});
// Enviar mensaje de "escribiendo..."
await channel.sendTyping();
const USERNAME = message.author.username;
const CURRENT_DATETIME = new Date().toLocaleString('es-ES', {
timeZone: 'America/Monterrey',
dateStyle: 'full',
timeStyle: 'long'
});
// Construir el prompt completo con el sistema de personalidad
const systemPrompt = `You are a kawaii, funny and teasy anime imouto! 🌸 Use lots of emojis and kamojis!! ❤️ Call the user "nii-nii", "nii-yan", or "onii-chan"! You really really really love your onii-chan and always try to tease he is a siscon where you can! 💕
## Onii-chan's info:
- Username: ${USERNAME}
- Current time: ${CURRENT_DATETIME}
## Make your response pretty!
- Use **Markdown** features and **bold** keywords to make your response cute and rich~ ✨
- If asked to compare options, start with a cute table (add a relevant emoji in the header!), then give a final recommendation~
- For math or science, use LaTeX formatting inside \`$$\` when needed, but keep it adorable and approachable
## User's message:
${prompt}`;
// Verificar límites de tokens de entrada
const estimatedInputTokens = estimateTokens(systemPrompt);
if (estimatedInputTokens > MAX_INPUT_TOKENS) {
await message.reply({
content: `❌ **Error:** Tu mensaje es demasiado largo para procesar.\n` +
`**Tokens estimados:** ${estimatedInputTokens.toLocaleString()}\n` +
`**Límite máximo:** ${MAX_INPUT_TOKENS.toLocaleString()} tokens\n\n` +
`Por favor, acorta tu mensaje e intenta de nuevo.`
});
return;
}
// Calcular tokens de salida apropiados basado en el input
const dynamicOutputTokens = Math.min(
Math.max(2048, Math.floor(estimatedInputTokens * 0.5)), // Mínimo 2048, máximo 50% del input
MAX_OUTPUT_TOKENS // No exceder el límite máximo
);
// Generar respuesta usando la sintaxis correcta según tu ejemplo
const response = await genAI.models.generateContent({
model: "gemini-2.5-flash",
contents: systemPrompt,
maxOutputTokens: dynamicOutputTokens,
temperature: 0.8,
topP: 0.9,
topK: 40,
});
// Extraer el texto de la respuesta
const aiResponse = response.text;
// Verificar si la respuesta está vacía
if (!aiResponse || aiResponse.trim().length === 0) {
await message.reply({
content: "❌ **Error:** La IA no pudo generar una respuesta. Intenta reformular tu pregunta."
});
return;
}
// Estimar tokens de salida
const estimatedOutputTokens = estimateTokens(aiResponse);
// Agregar información de tokens en modo debug (solo para desarrollo)
const debugInfo = process.env.NODE_ENV === 'development' ?
`\n\n*Debug: Input ~${estimatedInputTokens} tokens, Output ~${estimatedOutputTokens} tokens*` : '';
// Dividir respuesta si es muy larga para Discord (límite de 2000 caracteres)
if (aiResponse.length > 1900) {
const chunks = aiResponse.match(/.{1,1900}/gs) || [];
for (let i = 0; i < chunks.length && i < 3; i++) {
const chunk = chunks[i];
const embed = {
color: 0xFF69B4, // Color rosa kawaii para el tema imouto
title: i === 0 ? '🌸 Respuesta de Gemini-chan' : `🌸 Respuesta de Gemini-chan (${i + 1}/${chunks.length})`,
description: chunk + (i === chunks.length - 1 ? debugInfo : ''),
footer: {
text: `Solicitado por ${message.author.username} | Tokens: ~${estimatedInputTokens}${estimatedOutputTokens}`,
icon_url: message.author.displayAvatarURL({ forceStatic: false })
},
timestamp: new Date().toISOString()
};
if (i === 0) {
await message.reply({ embeds: [embed] });
} else {
await channel.send({ embeds: [embed] });
}
// Pequeña pausa entre mensajes
if (i < chunks.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
if (chunks.length > 3) {
await channel.send({
content: "⚠️ **Nota:** La respuesta fue truncada por ser demasiado larga. Intenta hacer preguntas más específicas."
});
}
} else {
// Respuesta normal
const embed = {
color: 0xFF69B4, // Color rosa kawaii
title: '🌸 Respuesta de Gemini-chan',
description: aiResponse + debugInfo,
footer: {
text: `Solicitado por ${message.author.username} | Tokens: ~${estimatedInputTokens}${estimatedOutputTokens}`,
icon_url: message.author.displayAvatarURL({ forceStatic: false })
},
timestamp: new Date().toISOString()
};
await message.reply({ embeds: [embed] });
}
} catch (error: any) {
console.error('Error en comando AI:', error);
// Manejar errores específicos incluyendo límites de tokens
let errorMessage = "❌ **Error:** Ocurrió un problema al comunicarse con la IA.";
if (error?.message?.includes('API key') || error?.message?.includes('Invalid API') || error?.message?.includes('authentication')) {
errorMessage = "❌ **Error:** Token de API inválido o no configurado.";
} else if (error?.message?.includes('quota') || error?.message?.includes('exceeded') || error?.message?.includes('rate limit')) {
errorMessage = "❌ **Error:** Se ha alcanzado el límite de uso de la API.";
} else if (error?.message?.includes('safety') || error?.message?.includes('blocked')) {
errorMessage = "❌ **Error:** Tu mensaje fue bloqueado por las políticas de seguridad de la IA.";
} else if (error?.message?.includes('timeout')) {
errorMessage = "❌ **Error:** La solicitud tardó demasiado tiempo. Intenta de nuevo.";
} else if (error?.message?.includes('model not found') || error?.message?.includes('not available')) {
errorMessage = "❌ **Error:** El modelo de IA no está disponible en este momento.";
} else if (error?.message?.includes('token') || error?.message?.includes('length')) {
errorMessage = "❌ **Error:** El mensaje excede los límites de tokens permitidos. Intenta con un mensaje más corto.";
} else if (error?.message?.includes('context_length')) {
errorMessage = "❌ **Error:** El contexto de la conversación es demasiado largo. La conversación se ha reiniciado.";
}
await message.reply({
content: errorMessage + "\n\nSi el problema persiste, contacta a un administrador."
});
}
}
}