generated from nhcarrigan/template
This commit is contained in:
@@ -10,8 +10,8 @@ import {
|
|||||||
type Message,
|
type Message,
|
||||||
type OmitPartialGroupDMChannel,
|
type OmitPartialGroupDMChannel,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { personality } from "../config/personality.js";
|
import { makeAiRequest } from "../modules/makeAiRequest.js";
|
||||||
import { ai } from "../utils/ai.js";
|
import { sendAiResponse } from "../modules/sendAiResponse.js";
|
||||||
import { calculateCost } from "../utils/calculateCost.js";
|
import { calculateCost } from "../utils/calculateCost.js";
|
||||||
import { isNaomiMessage } from "../utils/isNaomi.js";
|
import { isNaomiMessage } from "../utils/isNaomi.js";
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
@@ -22,7 +22,7 @@ import type { MessageParam } from "@anthropic-ai/sdk/resources/index.js";
|
|||||||
* @param message - The message payload from Discord.
|
* @param message - The message payload from Discord.
|
||||||
*/
|
*/
|
||||||
export const handleDmMessage
|
export const handleDmMessage
|
||||||
// eslint-disable-next-line max-lines-per-function, max-statements -- We're off by one bloody line.
|
// eslint-disable-next-line max-lines-per-function, max-statements -- We're off by one bloody line.
|
||||||
= async(message: OmitPartialGroupDMChannel<Message>): Promise<void> => {
|
= async(message: OmitPartialGroupDMChannel<Message>): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (message.author.bot) {
|
if (message.author.bot) {
|
||||||
@@ -32,17 +32,19 @@ export const handleDmMessage
|
|||||||
if (!isNaomi) {
|
if (!isNaomi) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const historyRequest
|
await message.channel.sendTyping();
|
||||||
= await message.channel.messages.fetch({ limit: 20 });
|
const historyRequest = await message.channel.messages.fetch({
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
const history = [ ...historyRequest.values() ];
|
const history = [ ...historyRequest.values() ];
|
||||||
const clearMessageIndex = history.findIndex((messageInner) => {
|
const clearMessageIndex = history.findIndex((messageInner) => {
|
||||||
return (
|
return (
|
||||||
messageInner.content === "<Clear History>"
|
messageInner.content === "<Clear History>"
|
||||||
&& messageInner.author.id === message.client.user.id
|
&& messageInner.author.id === message.client.user.id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (clearMessageIndex !== -1) {
|
if (clearMessageIndex !== -1) {
|
||||||
// Remove the clear message and everything sent before it, which means everything after in the array because the array is backwards
|
// Remove the clear message and everything sent before it, which means everything after in the array because the array is backwards
|
||||||
history.splice(clearMessageIndex, history.length - clearMessageIndex);
|
history.splice(clearMessageIndex, history.length - clearMessageIndex);
|
||||||
}
|
}
|
||||||
const context: Array<MessageParam> = history.
|
const context: Array<MessageParam> = history.
|
||||||
@@ -51,34 +53,30 @@ export const handleDmMessage
|
|||||||
return {
|
return {
|
||||||
content: messageInner.content,
|
content: messageInner.content,
|
||||||
role:
|
role:
|
||||||
messageInner.author.id === message.client.user.id
|
messageInner.author.id === message.client.user.id
|
||||||
? "assistant"
|
? "assistant"
|
||||||
: "user",
|
: "user",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const messages = await ai.messages.create({
|
const { content, usage } = await makeAiRequest(
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Required key format for SDK.
|
context,
|
||||||
max_tokens: 3000,
|
message.author.displayName,
|
||||||
messages: context,
|
);
|
||||||
model: "claude-sonnet-4-5-20250929",
|
const cost = calculateCost(usage);
|
||||||
system: `${personality} The user's name is ${message.author.displayName}`,
|
|
||||||
temperature: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = messages.content.find((messageInner) => {
|
await sendAiResponse(
|
||||||
return messageInner.type === "text";
|
[ ...content, cost ],
|
||||||
});
|
message.channel.send.bind(message.channel),
|
||||||
|
message.channel.sendTyping.bind(message.channel),
|
||||||
const cost = calculateCost(messages.usage);
|
|
||||||
|
|
||||||
await message.channel.send(
|
|
||||||
`${response?.text ?? "There was an error. Please try again later."}\n\n${cost}`,
|
|
||||||
);
|
);
|
||||||
await logger.metric("dm_message", 1, { cost });
|
await logger.metric("dm_message", 1, { cost });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await logger.error("message event", error instanceof Error
|
await logger.error(
|
||||||
? error
|
"message event",
|
||||||
: new Error(String(error)));
|
error instanceof Error
|
||||||
|
? error
|
||||||
|
: new Error(String(error)),
|
||||||
|
);
|
||||||
const button = new ButtonBuilder().
|
const button = new ButtonBuilder().
|
||||||
setLabel("Need help?").
|
setLabel("Need help?").
|
||||||
setStyle(ButtonStyle.Link).
|
setStyle(ButtonStyle.Link).
|
||||||
@@ -86,9 +84,10 @@ export const handleDmMessage
|
|||||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);
|
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);
|
||||||
await message.reply({
|
await message.reply({
|
||||||
components: [ row ],
|
components: [ row ],
|
||||||
content: error instanceof Error
|
content:
|
||||||
? error.message
|
error instanceof Error
|
||||||
: "Something went wrong.",
|
? error.message
|
||||||
|
: "Something went wrong.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
ButtonStyle,
|
ButtonStyle,
|
||||||
type Message,
|
type Message,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { personality } from "../config/personality.js";
|
import { makeAiRequest } from "../modules/makeAiRequest.js";
|
||||||
import { ai } from "../utils/ai.js";
|
import { sendAiResponse } from "../modules/sendAiResponse.js";
|
||||||
import { calculateCost } from "../utils/calculateCost.js";
|
import { calculateCost } from "../utils/calculateCost.js";
|
||||||
import { isNaomiMessage } from "../utils/isNaomi.js";
|
import { isNaomiMessage } from "../utils/isNaomi.js";
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
@@ -20,7 +20,7 @@ import { logger } from "../utils/logger.js";
|
|||||||
* @param message - The message payload from Discord.
|
* @param message - The message payload from Discord.
|
||||||
*/
|
*/
|
||||||
export const handleGuildMessage
|
export const handleGuildMessage
|
||||||
// eslint-disable-next-line max-lines-per-function -- We're off by one bloody line.
|
|
||||||
= async(message: Message<true>): Promise<void> => {
|
= async(message: Message<true>): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (message.author.bot) {
|
if (message.author.bot) {
|
||||||
@@ -39,26 +39,21 @@ export const handleGuildMessage
|
|||||||
if (!isNaomi) {
|
if (!isNaomi) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const messages = await ai.messages.create({
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Required key format for SDK.
|
|
||||||
max_tokens: 3000,
|
|
||||||
messages: [ { content: message.content, role: "user" } ],
|
|
||||||
model: "claude-sonnet-4-5-20250929",
|
|
||||||
system: `${personality} The user's name is ${message.author.displayName}`,
|
|
||||||
temperature: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = messages.content.find((messageInner) => {
|
|
||||||
return messageInner.type === "text";
|
|
||||||
});
|
|
||||||
|
|
||||||
const cost = calculateCost(messages.usage);
|
|
||||||
|
|
||||||
const thread = await message.startThread({
|
const thread = await message.startThread({
|
||||||
name: `${message.author.displayName}'s Thread`,
|
name: `${message.author.displayName}'s Thread`,
|
||||||
});
|
});
|
||||||
await thread.send(
|
await thread.sendTyping();
|
||||||
`${response?.text ?? "There was an error. Please try again later."}\n\n${cost}`,
|
|
||||||
|
const { content, usage } = await makeAiRequest(
|
||||||
|
[ { content: message.content, role: "user" } ],
|
||||||
|
message.author.displayName,
|
||||||
|
);
|
||||||
|
const cost = calculateCost(usage);
|
||||||
|
|
||||||
|
await sendAiResponse(
|
||||||
|
[ ...content, cost ],
|
||||||
|
thread.send.bind(thread),
|
||||||
|
thread.sendTyping.bind(thread),
|
||||||
);
|
);
|
||||||
await logger.metric("guild_message", 1, { cost });
|
await logger.metric("guild_message", 1, { cost });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
type AnyThreadChannel,
|
type AnyThreadChannel,
|
||||||
type Message,
|
type Message,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { personality } from "../config/personality.js";
|
import { makeAiRequest } from "../modules/makeAiRequest.js";
|
||||||
import { ai } from "../utils/ai.js";
|
import { sendAiResponse } from "../modules/sendAiResponse.js";
|
||||||
import { calculateCost } from "../utils/calculateCost.js";
|
import { calculateCost } from "../utils/calculateCost.js";
|
||||||
import { isNaomiMessage } from "../utils/isNaomi.js";
|
import { isNaomiMessage } from "../utils/isNaomi.js";
|
||||||
import { logger } from "../utils/logger.js";
|
import { logger } from "../utils/logger.js";
|
||||||
@@ -38,6 +38,7 @@ export const handleThreadMessage
|
|||||||
if (!isNaomi) {
|
if (!isNaomi) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await channel.sendTyping();
|
||||||
const historyRequest = await message.channel.messages.fetch({ limit: 20 });
|
const historyRequest = await message.channel.messages.fetch({ limit: 20 });
|
||||||
const history = [ ...historyRequest.values() ];
|
const history = [ ...historyRequest.values() ];
|
||||||
const context: Array<MessageParam> = history.
|
const context: Array<MessageParam> = history.
|
||||||
@@ -51,23 +52,16 @@ export const handleThreadMessage
|
|||||||
: "user",
|
: "user",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const messages = await ai.messages.create({
|
const { content, usage } = await makeAiRequest(
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Required key format for SDK.
|
context,
|
||||||
max_tokens: 3000,
|
message.author.displayName,
|
||||||
messages: context,
|
);
|
||||||
model: "claude-sonnet-4-5-20250929",
|
const cost = calculateCost(usage);
|
||||||
system: `${personality} The user's name is ${message.author.displayName}`,
|
|
||||||
temperature: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = messages.content.find((messageInner) => {
|
await sendAiResponse(
|
||||||
return messageInner.type === "text";
|
[ ...content, cost ],
|
||||||
});
|
message.channel.send.bind(message.channel),
|
||||||
|
message.channel.sendTyping.bind(message.channel),
|
||||||
const cost = calculateCost(messages.usage);
|
|
||||||
|
|
||||||
await message.channel.send(
|
|
||||||
`${response?.text ?? "There was an error. Please try again later."}\n\n${cost}`,
|
|
||||||
);
|
);
|
||||||
await logger.metric("thread_message", 1, { cost });
|
await logger.metric("thread_message", 1, { cost });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* @copyright nhcarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { personality } from "../config/personality.js";
|
||||||
|
import { ai } from "../utils/ai.js";
|
||||||
|
import type {
|
||||||
|
MessageParam,
|
||||||
|
Usage,
|
||||||
|
} from "@anthropic-ai/sdk/resources/messages.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an AI request to the Anthropic API.
|
||||||
|
* @param context - The message context to send to the API.
|
||||||
|
* @param username - The username of the user making the request.
|
||||||
|
* @returns The content of the response and the usage.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line max-lines-per-function -- The formatting ruins it.
|
||||||
|
export const makeAiRequest = async(
|
||||||
|
context: Array<MessageParam>,
|
||||||
|
username: string,
|
||||||
|
): Promise<{ content: Array<string>; usage: Usage }> => {
|
||||||
|
const response = await ai.messages.create({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- Required key format for SDK.
|
||||||
|
max_tokens: 3000,
|
||||||
|
messages: context,
|
||||||
|
model: "claude-sonnet-4-5-20250929",
|
||||||
|
system: `${personality} The user's name is ${username}`,
|
||||||
|
temperature: 1,
|
||||||
|
tools: [ {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- Required key format for SDK.
|
||||||
|
max_uses: 5,
|
||||||
|
name: "web_search",
|
||||||
|
type: "web_search_20250305",
|
||||||
|
} ],
|
||||||
|
});
|
||||||
|
const { usage } = response;
|
||||||
|
const content = response.content.map((message) => {
|
||||||
|
if (message.type === "text") {
|
||||||
|
if (
|
||||||
|
message.citations?.length !== undefined
|
||||||
|
&& message.citations.length > 0
|
||||||
|
) {
|
||||||
|
return `**${message.text}**\n\n-# ${message.citations.
|
||||||
|
filter((citation) => {
|
||||||
|
return citation.type === "web_search_result_location";
|
||||||
|
}).
|
||||||
|
map((citation) => {
|
||||||
|
return `${citation.title ?? "Unknown Title"}\n${citation.url}`;
|
||||||
|
}).
|
||||||
|
join(", ")}`;
|
||||||
|
}
|
||||||
|
return message.text;
|
||||||
|
}
|
||||||
|
if (message.type === "server_tool_use") {
|
||||||
|
return `Searching for: ${JSON.stringify(message.input)}`;
|
||||||
|
}
|
||||||
|
if (message.type === "web_search_tool_result") {
|
||||||
|
if (!Array.isArray(message.content)) {
|
||||||
|
return `-# Found: ${JSON.stringify(message.content)}`;
|
||||||
|
}
|
||||||
|
return `-# Found: ${message.content.
|
||||||
|
map((entry) => {
|
||||||
|
return `[${entry.title}](<${entry.url}>)`;
|
||||||
|
}).
|
||||||
|
join(", ")}`;
|
||||||
|
}
|
||||||
|
if (message.type === "thinking") {
|
||||||
|
return `-# Thinking: ${message.thinking}`;
|
||||||
|
}
|
||||||
|
if (message.type === "redacted_thinking") {
|
||||||
|
return `-# Thinking: [Redacted]`;
|
||||||
|
}
|
||||||
|
return `-# Tool use: ${message.name}`;
|
||||||
|
});
|
||||||
|
return { content, usage };
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/* eslint-disable no-await-in-loop -- This is necessary so we can send the responses sequentially.*/
|
||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { sleep } from "../utils/sleep.js";
|
||||||
|
import type { DMChannel, GuildTextBasedChannel, Message } from "discord.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an AI response to a channel.
|
||||||
|
* @param content - The content to send.
|
||||||
|
* @param send - The send or reply function to use.
|
||||||
|
* @param type - The sendTyping function to use.
|
||||||
|
*/
|
||||||
|
export const sendAiResponse = async(
|
||||||
|
content: Array<string>,
|
||||||
|
send: GuildTextBasedChannel["send"] | DMChannel["send"] | Message["reply"],
|
||||||
|
type: GuildTextBasedChannel["sendTyping"],
|
||||||
|
): Promise<void> => {
|
||||||
|
for (const line of content) {
|
||||||
|
await send(line);
|
||||||
|
await type();
|
||||||
|
await sleep(2500);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleeps for a given number of milliseconds.
|
||||||
|
* @param milliseconds - The number of milliseconds to sleep.
|
||||||
|
* @returns A promise that resolves after the given number of milliseconds.
|
||||||
|
*/
|
||||||
|
export const sleep = async(milliseconds: number): Promise<void> => {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, milliseconds);
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user