+46
-1
src/commands/utilities/ai.ts
+46
-1
src/commands/utilities/ai.ts
···
34
34
detail?: 'low' | 'high' | 'auto';
35
35
};
36
36
}>;
37
+
username?: string;
37
38
}
38
39
39
40
interface AIResponse {
···
139
140
model?: string,
140
141
username?: string,
141
142
interaction?: ChatInputCommandInteraction,
143
+
isServer?: boolean,
144
+
serverName?: string,
142
145
): string {
143
146
const now = new Date();
144
147
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
···
171
174
172
175
const currentModel = model || (usingDefaultKey ? 'moonshotai/kimi-k2 (default)' : 'custom model');
173
176
177
+
const contextInfo = isServer
178
+
? `**CONTEXT:**
179
+
- You are responding in the Discord server: "${serverName || 'Unknown Server'}"
180
+
- CURRENT USER TALKING TO YOU: ${username || 'Discord User'}
181
+
- IMPORTANT: This is a SERVER conversation where MULTIPLE DIFFERENT USERS can talk to you
182
+
- Previous messages may be from different users - always check the username before each message
183
+
- When you see "**Username**: message", that means a different user said that message
184
+
- The current message is from ${username || 'Discord User'} specifically
185
+
- Always be aware of WHO is talking to you in each message
186
+
- If you reference previous messages, make sure you know which user said what
187
+
- Users may respond to your messages or ping you to continue conversations
188
+
- When users reply to your previous responses, treat it as part of the ongoing conversation`
189
+
: `**CONTEXT:**
190
+
- You are in a direct message conversation
191
+
- User: ${username || 'Discord User'}`;
192
+
174
193
const baseInstructions = `You are a helpful, accurate, and privacy-respecting AI assistant for the /ai command of the Aethel Discord User Bot. Your primary goal is to provide clear, concise, and friendly answers to user questions, adapting your tone to be conversational and approachable.
194
+
195
+
${contextInfo}
175
196
176
197
**USER INFORMATION:**
177
198
- Username: ${username || 'Discord User'}
···
190
211
- NEVER format, modify, or alter URLs in any way. Leave them exactly as they are.
191
212
- Format your responses using Discord markdown where appropriate, but NEVER format URLs.
192
213
- Only greet the user at the start of a new conversation, not in every message.
214
+
- DO NOT hallucinate, make up facts, or provide false information. If you don't know something, say so clearly.
215
+
- Be accurate and truthful in all responses. Do not invent details, statistics, or information that you're not certain about.
216
+
- If asked about current events, real-time data, or information beyond your knowledge cutoff, clearly state your limitations.
193
217
194
218
**BOT FACTS (use only if asked about the bot):**
195
219
- Name: Aethel
···
289
313
await pool.query(
290
314
`INSERT INTO users (user_id, api_key_encrypted, custom_model, custom_api_url, updated_at)
291
315
VALUES ($1, $2, $3, $4, now())
292
-
ON CONFLICT (user_id) DO UPDATE SET
316
+
ON CONFLICT (user_id) DO UPDATE SET
293
317
api_key_encrypted = $2, custom_model = $3, custom_api_url = $4, updated_at = now()`,
294
318
[userId, encrypted, model?.trim() || null, apiUrl?.trim() || null],
295
319
);
···
352
376
`INSERT INTO ai_usage (user_id, usage_date, count) VALUES ($1, $2, 1)
353
377
ON CONFLICT (user_id, usage_date) DO UPDATE SET count = ai_usage.count + 1 RETURNING count`,
354
378
[userId, today],
379
+
);
380
+
await client.query('COMMIT');
381
+
return res.rows[0].count <= limit;
382
+
} catch (err) {
383
+
await client.query('ROLLBACK');
384
+
throw err;
385
+
} finally {
386
+
client.release();
387
+
}
388
+
}
389
+
390
+
async function incrementAndCheckServerDailyLimit(serverId: string, limit = 20): Promise<boolean> {
391
+
const today = new Date().toISOString().slice(0, 10);
392
+
const client = await pool.connect();
393
+
try {
394
+
await client.query('BEGIN');
395
+
const res = await client.query(
396
+
`INSERT INTO server_ai_usage (server_id, usage_date, count) VALUES ($1, $2, 1)
397
+
ON CONFLICT (server_id, usage_date) DO UPDATE SET count = server_ai_usage.count + 1 RETURNING count`,
398
+
[serverId, today],
355
399
);
356
400
await client.query('COMMIT');
357
401
return res.rows[0].count <= limit;
···
566
610
sendAIResponse,
567
611
getUserCredentials,
568
612
incrementAndCheckDailyLimit,
613
+
incrementAndCheckServerDailyLimit,
569
614
splitResponseIntoChunks,
570
615
};
571
616
+157
-48
src/events/messageCreate.ts
+157
-48
src/events/messageCreate.ts
···
8
8
buildConversation,
9
9
getUserCredentials,
10
10
incrementAndCheckDailyLimit,
11
+
incrementAndCheckServerDailyLimit,
11
12
splitResponseIntoChunks,
12
13
processUrls,
13
14
} from '@/commands/utilities/ai';
14
15
import type { ConversationMessage, AIResponse } from '@/commands/utilities/ai';
16
+
17
+
type ApiConfiguration = ReturnType<typeof getApiConfiguration>;
15
18
import { createMemoryManager } from '@/utils/memoryManager';
16
19
17
-
const conversations = createMemoryManager<string, ConversationMessage[]>({
20
+
const serverConversations = createMemoryManager<string, ConversationMessage[]>({
21
+
maxSize: 1000,
22
+
maxAge: 2 * 60 * 60 * 1000,
23
+
cleanupInterval: 10 * 60 * 1000,
24
+
});
25
+
26
+
const serverMessageContext = createMemoryManager<
27
+
string,
28
+
Array<{
29
+
username: string;
30
+
content: string;
31
+
timestamp: number;
32
+
}>
33
+
>({
34
+
maxSize: 1000,
35
+
maxAge: 2 * 60 * 60 * 1000,
36
+
cleanupInterval: 10 * 60 * 1000,
37
+
});
38
+
39
+
const userConversations = createMemoryManager<string, ConversationMessage[]>({
18
40
maxSize: 2000,
19
41
maxAge: 2 * 60 * 60 * 1000,
20
42
cleanupInterval: 10 * 60 * 1000,
21
43
});
22
44
23
-
function getConversationKey(message: Message): string {
24
-
if (message.channel.type === ChannelType.DM) {
25
-
return `dm:${message.author.id}`;
26
-
} else if (message.guildId) {
27
-
return `guild:${message.guildId}:${message.author.id}`;
28
-
}
29
-
return `channel:${message.channelId}`;
45
+
function getServerConversationKey(guildId: string): string {
46
+
return `server:${guildId}`;
47
+
}
48
+
49
+
function getUserConversationKey(userId: string): string {
50
+
return `dm:${userId}`;
30
51
}
31
52
32
53
export default class MessageCreateEvent {
···
47
68
const isMentioned =
48
69
message.mentions.users.has(this.client.user!.id) && !message.mentions.everyone;
49
70
71
+
if (!isDM && message.guildId) {
72
+
const contextKey = getServerConversationKey(message.guildId);
73
+
const existingContext = serverMessageContext.get(contextKey) || [];
74
+
75
+
const newMessage = {
76
+
username: message.author.username,
77
+
content: message.content,
78
+
timestamp: Date.now(),
79
+
};
80
+
81
+
const updatedContext = [...existingContext, newMessage].slice(-10);
82
+
serverMessageContext.set(contextKey, updatedContext);
83
+
}
84
+
50
85
if (!isDM && !isMentioned) {
51
86
logger.debug(
52
-
`Ignoring message - not a DM and bot not mentioned (channel type: ${message.channel.type})`,
87
+
`Storing message for context but not responding - not a DM and bot not mentioned (channel type: ${message.channel.type})`,
53
88
);
54
89
return;
55
90
}
···
61
96
`${isDM ? 'DM' : 'Message'} received (${message.content.length} characters) - content hidden for privacy`,
62
97
);
63
98
64
-
const conversationKey = getConversationKey(message);
65
-
const conversation = conversations.get(conversationKey) || [];
66
-
67
99
const hasImageAttachments = message.attachments.some(
68
100
(att) =>
69
101
att.contentType?.startsWith('image/') ||
70
102
att.name?.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i),
71
103
);
72
104
73
-
const hasImageUrls = false;
105
+
logger.debug(`hasImageAttachments: ${hasImageAttachments}`);
106
+
const hasImages = hasImageAttachments;
107
+
108
+
let selectedModel: string;
109
+
let config: ApiConfiguration;
110
+
let usingDefaultKey = true;
74
111
75
-
if (message.attachments.size > 0) {
76
-
logger.debug(`Found ${message.attachments.size} attachment(s)`);
77
-
message.attachments.forEach((att) => {
78
-
logger.debug(`Attachment: type=${att.contentType}, size=${att.size}bytes`);
79
-
});
112
+
if (isDM) {
113
+
const {
114
+
model: userCustomModel,
115
+
apiKey: userApiKey,
116
+
apiUrl: userApiUrl,
117
+
} = await getUserCredentials(`user:${message.author.id}`);
118
+
119
+
selectedModel = hasImages
120
+
? 'google/gemma-3-4b-it'
121
+
: userCustomModel || 'moonshotai/kimi-k2';
122
+
123
+
config = getApiConfiguration(userApiKey ?? null, selectedModel, userApiUrl ?? null);
124
+
usingDefaultKey = config.usingDefaultKey;
80
125
} else {
81
-
logger.debug('No attachments found in message');
82
-
}
83
-
84
-
logger.debug(`hasImageAttachments: ${hasImageAttachments}, hasImageUrls: ${hasImageUrls}`);
85
-
const hasImages = hasImageAttachments;
86
-
const { model: userCustomModel } = await getUserCredentials(conversationKey);
126
+
selectedModel = hasImages ? 'google/gemma-3-4b-it' : 'google/gemini-2.5-flash-lite';
87
127
88
-
const selectedModel = hasImages
89
-
? 'google/gemma-3-4b-it'
90
-
: userCustomModel || 'moonshotai/kimi-k2';
128
+
config = getApiConfiguration(null, selectedModel, null);
129
+
}
91
130
92
131
logger.info(
93
-
`Using model: ${selectedModel} for message with images: ${hasImages}${userCustomModel ? ' (user custom model)' : ' (default model)'}`,
132
+
`Using model: ${selectedModel} for message with images: ${hasImages}${
133
+
isDM && !usingDefaultKey ? ' (user custom model)' : ' (default model)'
134
+
}`,
94
135
);
95
136
96
137
const systemPrompt = buildSystemPrompt(
97
-
isDM,
138
+
usingDefaultKey,
98
139
this.client,
99
140
selectedModel,
100
141
message.author.username,
142
+
undefined,
143
+
!isDM,
144
+
!isDM ? message.guild?.name : undefined,
101
145
);
102
146
103
147
let messageContent:
···
150
194
messageContent = contentArray;
151
195
}
152
196
197
+
let conversation: ConversationMessage[] = [];
198
+
199
+
if (isDM) {
200
+
const conversationKey = getUserConversationKey(message.author.id);
201
+
conversation = userConversations.get(conversationKey) || [];
202
+
} else {
203
+
const serverKey = getServerConversationKey(message.guildId!);
204
+
const serverConversation = serverConversations.get(serverKey) || [];
205
+
const recentMessages = serverMessageContext.get(serverKey) || [];
206
+
207
+
const contextMessages = recentMessages.slice(-6, -1).map((msg) => ({
208
+
role: 'user' as const,
209
+
content: `**${msg.username}**: ${msg.content}`,
210
+
username: msg.username,
211
+
}));
212
+
213
+
const aiHistory = serverConversation.slice(-3);
214
+
215
+
const formattedAiHistory = aiHistory.map((msg) => {
216
+
if (msg.role === 'user' && msg.username) {
217
+
const content = Array.isArray(msg.content)
218
+
? msg.content.map((c) => (c.type === 'text' ? c.text : '[Image]')).join(' ')
219
+
: msg.content;
220
+
return {
221
+
...msg,
222
+
content: `**${msg.username}**: ${content}`,
223
+
};
224
+
}
225
+
return msg;
226
+
});
227
+
228
+
conversation = [...contextMessages, ...formattedAiHistory];
229
+
}
230
+
153
231
let filteredConversation = conversation;
154
232
if (selectedModel === 'moonshotai/kimi-k2') {
155
233
filteredConversation = conversation.map((msg) => {
···
170
248
systemPrompt,
171
249
);
172
250
173
-
const { apiKey: userApiKey, apiUrl: userApiUrl } = await getUserCredentials(conversationKey);
174
-
const config = getApiConfiguration(userApiKey ?? null, selectedModel, userApiUrl ?? null);
175
-
176
251
if (config.usingDefaultKey) {
177
252
const exemptUserId = process.env.AI_EXEMPT_USER_ID;
178
253
const actorId = message.author.id;
179
254
180
255
if (actorId !== exemptUserId) {
181
-
const allowed = await incrementAndCheckDailyLimit(actorId, 10);
182
-
if (!allowed) {
183
-
await message.reply(
184
-
"❌ You've reached your daily limit of AI requests. Please try again tomorrow or set up your own API key using the `/ai` command.",
185
-
);
186
-
return;
256
+
if (isDM) {
257
+
const allowed = await incrementAndCheckDailyLimit(actorId, 10);
258
+
if (!allowed) {
259
+
await message.reply(
260
+
"❌ You've reached your daily limit of AI requests. Please try again tomorrow or set up your own API key using the `/ai` command.",
261
+
);
262
+
return;
263
+
}
264
+
} else {
265
+
const serverAllowed = await incrementAndCheckServerDailyLimit(message.guildId!, 150);
266
+
if (!serverAllowed) {
267
+
await message.reply(
268
+
'❌ This server has reached its daily limit of 150 AI requests. Please try again tomorrow or have someone set up their own API key using the `/ai` command.',
269
+
);
270
+
return;
271
+
}
187
272
}
188
273
}
189
274
} else if (!config.finalApiKey) {
···
229
314
return msg;
230
315
});
231
316
232
-
const fallbackModel = userCustomModel || 'moonshotai/kimi-k2';
317
+
const fallbackModel =
318
+
isDM && !usingDefaultKey
319
+
? 'moonshotai/kimi-k2'
320
+
: isDM
321
+
? 'moonshotai/kimi-k2'
322
+
: 'google/gemini-2.5-flash-lite';
233
323
234
324
const fallbackConversation = buildConversation(
235
325
cleanedConversation,
236
326
fallbackContent,
237
-
buildSystemPrompt(isDM, this.client, fallbackModel, message.author.username),
327
+
buildSystemPrompt(
328
+
usingDefaultKey,
329
+
this.client,
330
+
fallbackModel,
331
+
message.author.username,
332
+
undefined,
333
+
!isDM,
334
+
!isDM ? message.guild?.name : undefined,
335
+
),
238
336
);
239
337
240
-
const fallbackConfig = getApiConfiguration(
241
-
userApiKey ?? null,
242
-
fallbackModel,
243
-
userApiUrl ?? null,
244
-
);
338
+
const fallbackConfig = isDM ? config : getApiConfiguration(null, fallbackModel, null);
245
339
aiResponse = await makeAIRequest(fallbackConfig, fallbackConversation);
246
340
247
341
if (aiResponse) {
···
274
368
275
369
await this.sendResponse(message, aiResponse);
276
370
277
-
updatedConversation.push({
371
+
const userMessage: ConversationMessage = {
372
+
role: 'user',
373
+
content: messageContent,
374
+
username: message.author.username,
375
+
};
376
+
const assistantMessage: ConversationMessage = {
278
377
role: 'assistant',
279
378
content: aiResponse.content,
280
-
});
281
-
conversations.set(conversationKey, updatedConversation);
379
+
};
380
+
381
+
if (isDM) {
382
+
const conversationKey = getUserConversationKey(message.author.id);
383
+
const newConversation = [...filteredConversation, userMessage, assistantMessage];
384
+
userConversations.set(conversationKey, newConversation);
385
+
} else {
386
+
const serverKey = getServerConversationKey(message.guildId!);
387
+
const serverConversation = serverConversations.get(serverKey) || [];
388
+
const newServerConversation = [...serverConversation, userMessage, assistantMessage];
389
+
serverConversations.set(serverKey, newServerConversation);
390
+
}
282
391
283
392
logger.info(`${isDM ? 'DM' : 'Server'} response sent successfully`);
284
393
} catch (error) {