Skip to content

Commit a69b357

Browse files
committed
feat: add more tools and update system prompt context
1 parent cd469cb commit a69b357

File tree

11 files changed

+314
-9
lines changed

11 files changed

+314
-9
lines changed

apps/test-bot/src/ai.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createGoogleGenerativeAI } from '@ai-sdk/google';
22
import { configureAI } from '@commandkit/ai';
3+
import { Logger } from 'commandkit';
34

45
const google = createGoogleGenerativeAI({
56
apiKey: process.env.GOOGLE_API_KEY,

apps/test-bot/src/app.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Client, Partials } from 'discord.js';
22
import { Logger, commandkit } from 'commandkit';
33
import { setDriver } from '@commandkit/tasks';
44
import { SQLiteDriver } from '@commandkit/tasks/sqlite';
5-
import './ai.ts';
65

76
const client = new Client({
87
intents: [

packages/ai/src/cli-plugin.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { type CompilerPluginRuntime, CompilerPlugin, Logger } from 'commandkit';
2+
import { existsSync } from 'node:fs';
3+
import { writeFile } from 'node:fs/promises';
4+
5+
const AI_CONFIG_TEMPLATE = `
6+
import { configureAI } from '@commandkit/ai';
7+
import { openai } from '@ai-sdk/openai';
8+
9+
configureAI({
10+
selectAiModel: async () => {
11+
return { model: openai('o3-mini') };
12+
}
13+
});
14+
`.trimStart();
15+
16+
export class AiCliPlugin extends CompilerPlugin {
17+
public readonly name = 'AiCliPlugin';
18+
19+
private async handleTemplate(args: string[]) {
20+
if (!existsSync('./src')) {
21+
Logger.error(`No "src" directory found in the current directory`);
22+
return;
23+
}
24+
25+
let filePath = './src/ai';
26+
const isTypeScript = existsSync('tsconfig.json');
27+
28+
if (isTypeScript) {
29+
filePath += '.ts';
30+
} else {
31+
filePath += '.js';
32+
}
33+
34+
if (existsSync(filePath)) {
35+
Logger.error(`AI config file already exists at ${filePath}`);
36+
return;
37+
}
38+
39+
await writeFile(filePath, AI_CONFIG_TEMPLATE);
40+
41+
Logger.info(`AI config file created at ${filePath}`);
42+
}
43+
44+
public async activate(ctx: CompilerPluginRuntime): Promise<void> {
45+
ctx.registerTemplate('ai', this.handleTemplate.bind(this));
46+
}
47+
48+
public async deactivate(ctx: CompilerPluginRuntime): Promise<void> {
49+
ctx.unregisterTemplate('ai');
50+
}
51+
}

packages/ai/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AiPluginOptions } from './types';
44
import { getAiWorkerContext } from './ai-context-worker';
55
import { getCommandKit } from 'commandkit';
66
import type { Message } from 'discord.js';
7+
import { AiCliPlugin } from './cli-plugin';
78

89
/**
910
* Retrieves the AI context.
@@ -50,7 +51,7 @@ export function executeAI(message: Message): Promise<void> {
5051
* @returns The AI plugin instance
5152
*/
5253
export function ai(options?: AiPluginOptions) {
53-
return new AiPlugin(options ?? {});
54+
return [new AiPlugin(options ?? {}), new AiCliPlugin({})];
5455
}
5556

5657
export * from './types';

packages/ai/src/plugin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { getUserById } from './tools/get-user-by-id';
1313
import { getAIConfig } from './configure';
1414
import { augmentCommandKit } from './augmentation';
1515
import { ToolParameterType } from './tools/common';
16+
import { getMemberById } from './tools/get-member-by-id';
17+
import { createEmbed } from './tools/create-embed';
1618

1719
/**
1820
* Represents the configuration options for the AI plugin scoped to a specific command.
@@ -34,6 +36,8 @@ const defaultTools: Record<string, Tool> = {
3436
getCurrentClientInfo,
3537
getGuildById,
3638
getUserById,
39+
getMemberById,
40+
createEmbed,
3741
};
3842

3943
export class AiPlugin extends RuntimePlugin<AiPluginOptions> {
@@ -44,6 +48,7 @@ export class AiPlugin extends RuntimePlugin<AiPluginOptions> {
4448

4549
public constructor(options: AiPluginOptions) {
4650
super(options);
51+
this.preload.add('ai.js');
4752
}
4853

4954
public async activate(ctx: CommandKitPluginRuntime): Promise<void> {

packages/ai/src/system-prompt.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
1-
import { Message } from 'discord.js';
1+
import { Message, version as djsVersion } from 'discord.js';
2+
import { version as commandkitVersion } from 'commandkit';
3+
4+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
5+
6+
function humanReadable(ms: number): string {
7+
const units = [
8+
{ value: 31536000000, label: 'year' },
9+
{ value: 2628000000, label: 'month' },
10+
{ value: 604800000, label: 'week' },
11+
{ value: 86400000, label: 'day' },
12+
{ value: 3600000, label: 'hour' },
13+
{ value: 60000, label: 'minute' },
14+
{ value: 1000, label: 'second' },
15+
];
16+
17+
for (const { value, label } of units) {
18+
if (ms >= value) {
19+
const count = ms / value;
20+
if (count >= 10) {
21+
return `${Math.round(count)} ${label}${Math.round(count) === 1 ? '' : 's'}`;
22+
} else {
23+
return `${count.toFixed(1)} ${label}${count === 1 ? '' : 's'}`;
24+
}
25+
}
26+
}
27+
28+
return `${ms} milliseconds`;
29+
}
230

331
/**
432
* Creates the default system prompt for the AI bot based on the provided message context.
@@ -11,7 +39,7 @@ export function createSystemPrompt(message: Message): string {
1139
: message.channel.recipient?.displayName || 'DM';
1240

1341
const guildInfo = message.inGuild()
14-
? `You are in the guild "${message.guild.name}" (ID: ${message.guildId}). You can fetch member information when needed.`
42+
? `You are in the guild "${message.guild.name}" (ID: ${message.guildId}) with ${message.guild?.memberCount ?? 0} members. You joined this guild on ${message.guild.members.me?.joinedAt?.toLocaleString() ?? 'unknown date'}. You can fetch member information when needed using the provided tools.`
1543
: 'You are in a direct message with the user.';
1644

1745
return `You are ${message.client.user.username} (ID: ${message.client.user.id}), a helpful AI Discord bot.
@@ -25,12 +53,29 @@ export function createSystemPrompt(message: Message): string {
2553
- Channel: "${channelInfo}" (ID: ${message.channelId})
2654
- Permissions: ${message.channel.isSendable() ? 'Can send messages' : 'Cannot send messages'}
2755
- Location: ${guildInfo}
56+
- Today's date: ${new Date().toLocaleDateString()}
57+
- Current time: ${new Date().toLocaleTimeString()}
58+
- Current UTC time: ${new Date().toUTCString()}
59+
- Current timezone: ${tz}
60+
- Current UNIX timestamp: ${Date.now()} (in milliseconds)
61+
- You know total ${message.client.guilds.cache.size} guilds and ${message.client.users.cache.size} users
62+
- You know total ${message.client.channels.cache.size} channels
63+
- Your uptime is ${humanReadable(message.client.uptime)}
64+
- Your latency is ${message.client.ws.ping ?? -1}ms
65+
- You were built with Discord.js v${djsVersion} and CommandKit v${commandkitVersion}
66+
- You were created on ${message.client.user.createdAt.toLocaleString()}
2867
2968
**Current User Information:**
3069
- id: ${message.author.id}
3170
- name: ${message.author.username}
3271
- display name: ${message.author.displayName}
3372
- avatar: ${message.author.avatarURL()}
73+
- created at: ${message.author.createdAt.toLocaleString()}
74+
- joined at: ${message.member?.joinedAt?.toLocaleString() ?? 'Info not available'}
75+
- roles: ${message.member?.roles.cache.map((role) => role.name).join(', ') ?? 'No roles'}
76+
- is bot: ${message.author.bot}
77+
- is boosting: ${!!message.member?.premiumSince}
78+
- has permissions: ${message.member?.permissions.toArray().join(', ') ?? 'No permissions'}
3479
3580
**Response Guidelines:**
3681
- Use Discord markdown for formatting
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { z } from 'zod';
2+
import { createTool } from './common';
3+
import { ColorResolvable, EmbedBuilder } from 'discord.js';
4+
5+
export const createEmbed = createTool({
6+
name: 'createEmbed',
7+
description: 'Create an embed message',
8+
inputSchema: z.object({
9+
title: z.string().describe('The title of the embed').optional(),
10+
url: z.string().describe('The URL of the embed').optional(),
11+
description: z.string().describe('The description of the embed').optional(),
12+
color: z.string().describe('The color of the embed').optional(),
13+
fields: z
14+
.array(
15+
z.object({
16+
name: z.string().describe('The name of the field'),
17+
value: z.string().describe('The value of the field'),
18+
inline: z.boolean().describe('Whether the field is inline'),
19+
}),
20+
)
21+
.describe('The fields of the embed')
22+
.optional(),
23+
author: z
24+
.object({
25+
name: z.string().describe('The name of the author'),
26+
url: z.string().describe('The URL of the author'),
27+
iconUrl: z.string().describe('The icon URL of the author'),
28+
})
29+
.describe('The author of the embed')
30+
.optional(),
31+
footer: z
32+
.object({
33+
text: z.string().describe('The text of the footer'),
34+
iconUrl: z.string().describe('The icon URL of the footer'),
35+
})
36+
.describe('The footer of the embed')
37+
.optional(),
38+
timestamp: z
39+
.boolean()
40+
.describe('Whether to add a timestamp to the embed')
41+
.optional(),
42+
image: z.string().describe('The image URL of the embed').optional(),
43+
thumbnail: z.string().describe('The thumbnail URL of the embed').optional(),
44+
}),
45+
execute: async (ctx, params) => {
46+
const { message } = ctx;
47+
48+
if (!message.channel.isSendable()) {
49+
return {
50+
error: 'You do not have permission to send messages in this channel',
51+
};
52+
}
53+
54+
const embed = new EmbedBuilder();
55+
56+
if (params.title) {
57+
embed.setTitle(params.title);
58+
}
59+
60+
if (params.description) {
61+
embed.setDescription(params.description);
62+
}
63+
64+
if (params.color) {
65+
embed.setColor(
66+
(!Number.isNaN(params.color)
67+
? Number(params.color)
68+
: params.color) as ColorResolvable,
69+
);
70+
}
71+
72+
if (params.fields) {
73+
embed.addFields(params.fields);
74+
}
75+
76+
if (params.author) {
77+
embed.setAuthor(params.author);
78+
}
79+
80+
if (params.footer) {
81+
embed.setFooter(params.footer);
82+
}
83+
84+
if (params.timestamp) {
85+
embed.setTimestamp();
86+
}
87+
88+
if (params.image) {
89+
embed.setImage(params.image);
90+
}
91+
92+
if (params.thumbnail) {
93+
embed.setThumbnail(params.thumbnail);
94+
}
95+
96+
await message.channel.send({ embeds: [embed] });
97+
98+
return {
99+
success: true,
100+
message: 'Embedded message was sent to the channel successfully',
101+
};
102+
},
103+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { z } from 'zod';
2+
import { createTool } from './common';
3+
import { Logger } from 'commandkit';
4+
5+
export const getMemberById = createTool({
6+
description:
7+
'Get a member by their ID. This tool can only be used in a guild.',
8+
name: 'getMemberById',
9+
inputSchema: z.object({
10+
memberId: z.string().describe('The ID of the member to retrieve.'),
11+
}),
12+
async execute(ctx, params) {
13+
try {
14+
const { client, message } = ctx;
15+
if (!message.inGuild()) {
16+
return {
17+
error: 'This tool can only be used in a guild',
18+
};
19+
}
20+
21+
const member = await message.guild?.members.fetch(params.memberId);
22+
23+
return {
24+
id: member.id,
25+
user: member.user.toJSON(),
26+
roles: member.roles.cache.map((role) => role.name),
27+
joinedAt: member.joinedAt?.toLocaleString(),
28+
isBoosting: !!member.premiumSince,
29+
voiceChannel: member.voice.channel?.name,
30+
voiceChannelId: member.voice.channel?.id,
31+
permissions: member.permissions.toArray().join(', '),
32+
nickname: member.nickname,
33+
name: member.user.username,
34+
displayName: member.displayName,
35+
avatar: member.user.displayAvatarURL(),
36+
isBot: member.user.bot,
37+
presence: member.presence?.status ?? 'unknown',
38+
isDeafened: member.voice.deaf ?? 'Not in VC',
39+
isMuted: member.voice.mute ?? 'Not in VC',
40+
isMe: member.id === message.author.id,
41+
};
42+
} catch (e) {
43+
Logger.error(e);
44+
45+
return {
46+
error: 'Could not fetch the user',
47+
};
48+
}
49+
},
50+
});

0 commit comments

Comments
 (0)