feat: joel may sometimes answer even when he's not called for.
This commit is contained in:
@@ -39,6 +39,12 @@ interface BotConfig {
|
|||||||
mentionCooldown: number;
|
mentionCooldown: number;
|
||||||
/** Chance of mentioning a random user (0-1) */
|
/** Chance of mentioning a random user (0-1) */
|
||||||
mentionProbability: number;
|
mentionProbability: number;
|
||||||
|
/** Enable random-time spontaneous mention scheduler */
|
||||||
|
spontaneousSchedulerEnabled: boolean;
|
||||||
|
/** Minimum delay between spontaneous posts (ms) */
|
||||||
|
spontaneousSchedulerMinIntervalMs: number;
|
||||||
|
/** Maximum delay between spontaneous posts (ms) */
|
||||||
|
spontaneousSchedulerMaxIntervalMs: number;
|
||||||
};
|
};
|
||||||
web: {
|
web: {
|
||||||
port: number;
|
port: number;
|
||||||
@@ -59,6 +65,16 @@ function getEnvOrDefault(key: string, defaultValue: string): string {
|
|||||||
return Bun.env[key] ?? defaultValue;
|
return Bun.env[key] ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBooleanEnvOrDefault(key: string, defaultValue: boolean): boolean {
|
||||||
|
const raw = Bun.env[key];
|
||||||
|
if (raw === undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = raw.toLowerCase();
|
||||||
|
return normalized === "1" || normalized === "true" || normalized === "yes";
|
||||||
|
}
|
||||||
|
|
||||||
export const config: BotConfig = {
|
export const config: BotConfig = {
|
||||||
discord: {
|
discord: {
|
||||||
token: getEnvOrThrow("DISCORD_TOKEN"),
|
token: getEnvOrThrow("DISCORD_TOKEN"),
|
||||||
@@ -97,6 +113,9 @@ export const config: BotConfig = {
|
|||||||
memoryChance: 0.3,
|
memoryChance: 0.3,
|
||||||
mentionCooldown: 24 * 60 * 60 * 1000, // 24 hours
|
mentionCooldown: 24 * 60 * 60 * 1000, // 24 hours
|
||||||
mentionProbability: 0.001,
|
mentionProbability: 0.001,
|
||||||
|
spontaneousSchedulerEnabled: getBooleanEnvOrDefault("BOT_SPONTANEOUS_SCHEDULER_ENABLED", true),
|
||||||
|
spontaneousSchedulerMinIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MIN_INTERVAL_MS", String(45 * 60 * 1000))),
|
||||||
|
spontaneousSchedulerMaxIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MAX_INTERVAL_MS", String(180 * 60 * 1000))),
|
||||||
},
|
},
|
||||||
web: {
|
web: {
|
||||||
port: parseInt(getEnvOrDefault("WEB_PORT", "3000")),
|
port: parseInt(getEnvOrDefault("WEB_PORT", "3000")),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { EventHandler } from "../types";
|
|||||||
import { loadCommands, registerCommands } from "../../commands";
|
import { loadCommands, registerCommands } from "../../commands";
|
||||||
import { guildRepository } from "../../database";
|
import { guildRepository } from "../../database";
|
||||||
import { createLogger } from "../../core/logger";
|
import { createLogger } from "../../core/logger";
|
||||||
|
import { startSpontaneousMentionsCron } from "../../features/joel";
|
||||||
|
|
||||||
const logger = createLogger("Events:Ready");
|
const logger = createLogger("Events:Ready");
|
||||||
|
|
||||||
@@ -38,6 +39,9 @@ export const readyHandler: EventHandler<"ready"> = {
|
|||||||
type: ActivityType.Custom,
|
type: ActivityType.Custom,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start random-time spontaneous mentions
|
||||||
|
startSpontaneousMentionsCron(client);
|
||||||
|
|
||||||
logger.info("Bot is ready!");
|
logger.info("Bot is ready!");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { joelResponder, type TemplateVariables } from "./responder";
|
export { joelResponder, type TemplateVariables } from "./responder";
|
||||||
export { getRandomMention } from "./mentions";
|
export { getRandomMention, getRandomMemberMention } from "./mentions";
|
||||||
|
export { startSpontaneousMentionsCron, stopSpontaneousMentionsCron } from "./spontaneous-cron";
|
||||||
export { TypingIndicator } from "./typing";
|
export { TypingIndicator } from "./typing";
|
||||||
export { personalities, getPersonality, buildStyledPrompt, STYLE_MODIFIERS } from "./personalities";
|
export { personalities, getPersonality, buildStyledPrompt, STYLE_MODIFIERS } from "./personalities";
|
||||||
|
|||||||
@@ -12,6 +12,39 @@ const logger = createLogger("Features:Mentions");
|
|||||||
// Track last mention time per guild
|
// Track last mention time per guild
|
||||||
const lastMentionTime = new Map<string, number>();
|
const lastMentionTime = new Map<string, number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick a random non-bot guild member mention string.
|
||||||
|
* Returns null if no eligible members are found.
|
||||||
|
*/
|
||||||
|
export async function getRandomMemberMention(
|
||||||
|
message: Message<true>,
|
||||||
|
excludedUserIds: string[] = []
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const members = await message.guild.members.fetch({ limit: 100 });
|
||||||
|
|
||||||
|
const excludedSet = new Set(excludedUserIds);
|
||||||
|
const validMembers = members.filter(
|
||||||
|
(member) => !member.user.bot && !excludedSet.has(member.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validMembers.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomMember = validMembers.random();
|
||||||
|
if (!randomMember) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Selected random user for mention: ${randomMember.displayName}`);
|
||||||
|
return `<@${randomMember.id}>`;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to select random user mention", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a random user mention string, if conditions are met
|
* Get a random user mention string, if conditions are met
|
||||||
*/
|
*/
|
||||||
@@ -30,32 +63,13 @@ export async function getRandomMention(message: Message<true>): Promise<string>
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const mention = await getRandomMemberMention(message, [message.author.id]);
|
||||||
// Fetch recent members
|
if (!mention) {
|
||||||
const members = await message.guild.members.fetch({ limit: 100 });
|
|
||||||
|
|
||||||
// Filter out bots and the message author
|
|
||||||
const validMembers = members.filter(
|
|
||||||
(member) => !member.user.bot && member.id !== message.author.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validMembers.size === 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomMember = validMembers.random();
|
|
||||||
if (!randomMember) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record this mention
|
|
||||||
lastMentionTime.set(guildId, now);
|
|
||||||
|
|
||||||
logger.debug(`Mentioning random user: ${randomMember.displayName}`);
|
|
||||||
|
|
||||||
return ` <@${randomMember.id}>`;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to get random mention", error);
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record this mention
|
||||||
|
lastMentionTime.set(guildId, now);
|
||||||
|
|
||||||
|
return ` ${mention}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ import { TypingIndicator } from "./typing";
|
|||||||
|
|
||||||
const logger = createLogger("Features:Joel");
|
const logger = createLogger("Features:Joel");
|
||||||
|
|
||||||
// Regex to match various spellings of "Joel"
|
const DIRECTED_CLASSIFICATION_LIMIT_PER_HOUR = 20;
|
||||||
const JOEL_VARIATIONS = /\b(joel|jogel|johogel|jorl|jole|joeel|jöel|joal|jol|johel)\b/i;
|
const DIRECTED_CLASSIFICATION_WINDOW_MS = 60 * 60 * 1000;
|
||||||
|
const directedClassificationBudget = new Map<string, { windowStart: number; count: number }>();
|
||||||
|
|
||||||
|
type ResponseTrigger = "free-will" | "summoned" | "classifier" | "none";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Template variables that can be used in custom system prompts
|
* Template variables that can be used in custom system prompts
|
||||||
@@ -62,12 +65,14 @@ export const joelResponder = {
|
|||||||
* Handle an incoming message and potentially respond as Joel
|
* Handle an incoming message and potentially respond as Joel
|
||||||
*/
|
*/
|
||||||
async handleMessage(client: BotClient, message: Message<true>): Promise<void> {
|
async handleMessage(client: BotClient, message: Message<true>): Promise<void> {
|
||||||
const shouldRespond = this.shouldRespond(client, message);
|
const responseTrigger = await this.shouldRespond(client, message);
|
||||||
|
|
||||||
if (!shouldRespond) return;
|
if (responseTrigger === "none") return;
|
||||||
|
|
||||||
// Check channel restriction
|
// Check channel restriction
|
||||||
const channelCheck = await this.checkChannelRestriction(message);
|
const channelCheck = responseTrigger === "free-will"
|
||||||
|
? { allowed: true, rebellionResponse: false }
|
||||||
|
: await this.checkChannelRestriction(message);
|
||||||
if (!channelCheck.allowed) {
|
if (!channelCheck.allowed) {
|
||||||
if (channelCheck.rebellionResponse) {
|
if (channelCheck.rebellionResponse) {
|
||||||
// Joel is breaking the rules - he'll respond anyway but acknowledge it
|
// Joel is breaking the rules - he'll respond anyway but acknowledge it
|
||||||
@@ -177,24 +182,64 @@ export const joelResponder = {
|
|||||||
/**
|
/**
|
||||||
* Determine if Joel should respond to a message
|
* Determine if Joel should respond to a message
|
||||||
*/
|
*/
|
||||||
shouldRespond(client: BotClient, message: Message<true>): boolean {
|
async shouldRespond(client: BotClient, message: Message<true>): Promise<ResponseTrigger> {
|
||||||
const text = message.cleanContent;
|
const text = message.cleanContent;
|
||||||
const mentionsBot = message.mentions.has(client.user!);
|
const mentionsBot = message.mentions.has(client.user!);
|
||||||
const mentionsJoel = JOEL_VARIATIONS.test(text);
|
|
||||||
const freeWill = Math.random() < config.bot.freeWillChance;
|
const freeWill = Math.random() < config.bot.freeWillChance;
|
||||||
|
|
||||||
const shouldRespond = mentionsBot || mentionsJoel || freeWill;
|
if (freeWill) {
|
||||||
|
|
||||||
if (shouldRespond) {
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
freeWill
|
"Joel inserts himself (free will)",
|
||||||
? "Joel inserts himself (free will)"
|
|
||||||
: "Joel was summoned",
|
|
||||||
{ text: text.slice(0, 50) }
|
{ text: text.slice(0, 50) }
|
||||||
);
|
);
|
||||||
|
return "free-will";
|
||||||
}
|
}
|
||||||
|
|
||||||
return shouldRespond;
|
if (mentionsBot) {
|
||||||
|
logger.debug("Joel was summoned", { text: text.slice(0, 50) });
|
||||||
|
return "summoned";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.consumeDirectedClassificationBudget(message.guildId)) {
|
||||||
|
logger.debug("Directed classifier hourly limit reached", {
|
||||||
|
guildId: message.guildId,
|
||||||
|
limit: DIRECTED_CLASSIFICATION_LIMIT_PER_HOUR,
|
||||||
|
});
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ai = getAiService();
|
||||||
|
const directedAtJoel = await ai.classifyJoelDirected(text);
|
||||||
|
|
||||||
|
if (directedAtJoel) {
|
||||||
|
logger.debug("Joel summoned by cheap directed classifier", {
|
||||||
|
text: text.slice(0, 50),
|
||||||
|
});
|
||||||
|
return "classifier";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "none";
|
||||||
|
},
|
||||||
|
|
||||||
|
consumeDirectedClassificationBudget(guildId: string): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
const current = directedClassificationBudget.get(guildId);
|
||||||
|
|
||||||
|
if (!current || now - current.windowStart >= DIRECTED_CLASSIFICATION_WINDOW_MS) {
|
||||||
|
directedClassificationBudget.set(guildId, {
|
||||||
|
windowStart: now,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.count >= DIRECTED_CLASSIFICATION_LIMIT_PER_HOUR) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.count += 1;
|
||||||
|
directedClassificationBudget.set(guildId, current);
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
193
src/features/joel/spontaneous-cron.ts
Normal file
193
src/features/joel/spontaneous-cron.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* Random-time spontaneous Joel mentions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ChannelType, PermissionFlagsBits, type Guild, type TextChannel } from "discord.js";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { BotClient } from "../../core/client";
|
||||||
|
import { config } from "../../core/config";
|
||||||
|
import { createLogger } from "../../core/logger";
|
||||||
|
import { db } from "../../database";
|
||||||
|
import { botOptions } from "../../database/schema";
|
||||||
|
import { getAiService } from "../../services/ai";
|
||||||
|
|
||||||
|
const logger = createLogger("Features:Joel:SpontaneousCron");
|
||||||
|
|
||||||
|
const SPONTANEOUS_TOPICS = [
|
||||||
|
"a weird thought",
|
||||||
|
"a funny joke",
|
||||||
|
"a chaotic world update",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
export function startSpontaneousMentionsCron(client: BotClient): void {
|
||||||
|
if (started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.bot.spontaneousSchedulerEnabled) {
|
||||||
|
logger.info("Spontaneous mention scheduler is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
started = true;
|
||||||
|
logger.info("Starting spontaneous mention scheduler");
|
||||||
|
scheduleNext(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopSpontaneousMentionsCron(): void {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNext(client: BotClient): void {
|
||||||
|
const delayMs = getRandomDelayMs();
|
||||||
|
|
||||||
|
logger.debug("Scheduled next spontaneous message", { delayMs });
|
||||||
|
|
||||||
|
timer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await runTick(client);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Spontaneous scheduler tick failed", error);
|
||||||
|
} finally {
|
||||||
|
if (started) {
|
||||||
|
scheduleNext(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomDelayMs(): number {
|
||||||
|
const min = config.bot.spontaneousSchedulerMinIntervalMs;
|
||||||
|
const max = config.bot.spontaneousSchedulerMaxIntervalMs;
|
||||||
|
|
||||||
|
const lower = Math.max(1_000, Math.min(min, max));
|
||||||
|
const upper = Math.max(lower, Math.max(min, max));
|
||||||
|
|
||||||
|
return Math.floor(Math.random() * (upper - lower + 1)) + lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTick(client: BotClient): Promise<void> {
|
||||||
|
const availableGuilds = client.guilds.cache.filter((guild) => guild.available);
|
||||||
|
const guild = availableGuilds.random();
|
||||||
|
|
||||||
|
if (!guild) {
|
||||||
|
logger.debug("No available guilds for spontaneous message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await resolveTargetChannel(client, guild);
|
||||||
|
if (!channel) {
|
||||||
|
logger.debug("No valid channel for spontaneous message", { guildId: guild.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mention = await getRandomGuildMemberMention(guild);
|
||||||
|
if (!mention) {
|
||||||
|
logger.debug("No eligible guild members to mention", { guildId: guild.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonsense = await generateNonsense();
|
||||||
|
if (!nonsense) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await channel.send(`${mention} ${nonsense}`);
|
||||||
|
logger.info("Sent spontaneous random mention", {
|
||||||
|
guildId: guild.id,
|
||||||
|
channelId: channel.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveTargetChannel(client: BotClient, guild: Guild): Promise<TextChannel | null> {
|
||||||
|
|
||||||
|
const options = await db
|
||||||
|
.select()
|
||||||
|
.from(botOptions)
|
||||||
|
.where(eq(botOptions.guild_id, guild.id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const restrictedChannelId = options[0]?.restricted_channel_id;
|
||||||
|
|
||||||
|
if (restrictedChannelId) {
|
||||||
|
const restrictedChannel = guild.channels.cache.get(restrictedChannelId);
|
||||||
|
if (isWritableTextChannel(restrictedChannel, client)) {
|
||||||
|
return restrictedChannel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = guild.channels.cache.filter((channel): channel is TextChannel => {
|
||||||
|
return channel.type === ChannelType.GuildText && isWritableTextChannel(channel, client);
|
||||||
|
});
|
||||||
|
|
||||||
|
return candidates.random() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWritableTextChannel(channel: unknown, client: BotClient): channel is TextChannel {
|
||||||
|
if (!channel || !(channel as TextChannel).isTextBased?.()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textChannel = channel as TextChannel;
|
||||||
|
const permissions = textChannel.permissionsFor(client.user);
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
permissions?.has(PermissionFlagsBits.ViewChannel) &&
|
||||||
|
permissions.has(PermissionFlagsBits.SendMessages)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRandomGuildMemberMention(guild: Guild): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const members = await guild.members.fetch({ limit: 200 });
|
||||||
|
const eligible = members.filter((member) => !member.user.bot);
|
||||||
|
|
||||||
|
const randomMember = eligible.random();
|
||||||
|
if (!randomMember) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<@${randomMember.id}>`;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed selecting random member for spontaneous message", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateNonsense(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const ai = getAiService();
|
||||||
|
const topic = SPONTANEOUS_TOPICS[Math.floor(Math.random() * SPONTANEOUS_TOPICS.length)];
|
||||||
|
|
||||||
|
const response = await ai.generateResponse(
|
||||||
|
`Write one short Discord message containing ${topic}.`,
|
||||||
|
`You are Joel posting an unsolicited random message.
|
||||||
|
Rules:
|
||||||
|
- Mention nothing about being an AI
|
||||||
|
- 1-2 sentences, max 220 characters
|
||||||
|
- Funny or absurd tone
|
||||||
|
- No lists, no hashtags`
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = response.text.trim().replace(/\s+/g, " ");
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.slice(0, 220);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed generating spontaneous nonsense", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { BotClient } from "./core/client";
|
|||||||
import { config } from "./core/config";
|
import { config } from "./core/config";
|
||||||
import { createLogger } from "./core/logger";
|
import { createLogger } from "./core/logger";
|
||||||
import { registerEvents } from "./events";
|
import { registerEvents } from "./events";
|
||||||
|
import { stopSpontaneousMentionsCron } from "./features/joel";
|
||||||
import { startWebServer } from "./web";
|
import { startWebServer } from "./web";
|
||||||
|
|
||||||
const logger = createLogger("Main");
|
const logger = createLogger("Main");
|
||||||
@@ -55,12 +56,14 @@ async function main(): Promise<void> {
|
|||||||
// Handle graceful shutdown
|
// Handle graceful shutdown
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
logger.info("Shutting down...");
|
logger.info("Shutting down...");
|
||||||
|
stopSpontaneousMentionsCron();
|
||||||
client.destroy();
|
client.destroy();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
logger.info("Shutting down...");
|
logger.info("Shutting down...");
|
||||||
|
stopSpontaneousMentionsCron();
|
||||||
client.destroy();
|
client.destroy();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ export class AiService {
|
|||||||
return "snarky";
|
return "snarky";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify whether the message is directed at Joel
|
||||||
|
*/
|
||||||
|
async classifyJoelDirected(message: string): Promise<boolean> {
|
||||||
|
if (this.provider.classifyJoelDirected) {
|
||||||
|
return this.provider.classifyJoelDirected(message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract and save memorable information from a message
|
* Extract and save memorable information from a message
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -281,4 +281,44 @@ Category:`,
|
|||||||
return "snarky"; // Default to snarky on error
|
return "snarky"; // Default to snarky on error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cheap binary classifier to detect if a message is directed at Joel
|
||||||
|
*/
|
||||||
|
async classifyJoelDirected(message: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const classification = await this.client.chat.completions.create({
|
||||||
|
model: config.ai.classificationModel,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `Determine if this Discord message is directed at Joel (the bot), or talking about Joel in a way Joel should respond.
|
||||||
|
|
||||||
|
Only respond with one token: YES or NO.
|
||||||
|
|
||||||
|
Guidance:
|
||||||
|
- YES if the user is asking Joel a question, requesting Joel to do something, replying conversationally to Joel, or maybe discussing Joel as a participant.
|
||||||
|
- NO if it's general chat between humans, statements that do not involve Joel.
|
||||||
|
|
||||||
|
Message: "${message}"
|
||||||
|
|
||||||
|
Answer:`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
max_tokens: 3,
|
||||||
|
temperature: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = classification.choices[0]?.message?.content?.trim().toUpperCase();
|
||||||
|
return result?.startsWith("YES") ?? false;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to classify directed message", {
|
||||||
|
method: "classifyJoelDirected",
|
||||||
|
model: config.ai.classificationModel,
|
||||||
|
messageLength: message.length,
|
||||||
|
});
|
||||||
|
logger.error("Directed classification error details", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ export interface AiProvider {
|
|||||||
*/
|
*/
|
||||||
classifyMessage?(message: string): Promise<MessageStyle>;
|
classifyMessage?(message: string): Promise<MessageStyle>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify whether a message is directed at Joel
|
||||||
|
*/
|
||||||
|
classifyJoelDirected?(message: string): Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract memorable information from a message
|
* Extract memorable information from a message
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user