/** * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable no-await-in-loop -- Ordinarily I would use Promise.all, but we want these sent in order. */ // eslint-disable-next-line @typescript-eslint/naming-convention -- It is a class, so should be uppercased. import Anthropic from "@anthropic-ai/sdk"; import { prompt } from "../config/prompt.js"; import { calculateCost } from "../utils/calculateCost.js"; import { errorHandler } from "../utils/errorHandler.js"; const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_KEY ?? "", timeout: 5 * 60 * 1000, }); /** * Formats Discord messages into a prompt for the AI, * sends the prompt to the AI, and returns the AI's response. * @param hikari - Hikari's Discord instance. * @param messages - The Discord messages to process. * @param username - The username of the user who triggered this request - that is, the author of the most recent message. * @param channel - The channel in which to respond. * @returns The AI's response as a string. */ // eslint-disable-next-line max-lines-per-function -- This is a big function, but it does a lot of things. export const ai = async (hikari, messages, username, channel) => { try { const typingInterval = setInterval(() => { void channel.sendTyping(); }, 3000); const parsedPrompt = prompt.replace("{{username}}", username); const result = await anthropic.beta.messages.create({ betas: ["web-search-2025-03-05"], // eslint-disable-next-line @typescript-eslint/naming-convention -- API requirement max_tokens: 20_000, messages: messages.map((message) => { return { content: message.content, role: message.author.id === hikari.user?.id ? "assistant" : "user", }; }), model: "claude-sonnet-4-20250514", system: parsedPrompt, temperature: 1, tools: [ { // eslint-disable-next-line @typescript-eslint/naming-convention -- API requirement allowed_domains: ["nhcarrigan.com"], name: "web_search", type: "web_search_20250305", }, ], }); await calculateCost(result.usage, username); for (const payload of result.content) { await channel.sendTyping(); // Sleep for 5 seconds, await new Promise((resolve) => { // eslint-disable-next-line no-promise-executor-return -- We want to wait for a bit. return setTimeout(resolve, 3000); }); if (payload.type === "text") { await channel.send({ content: payload.text }); } if (payload.type === "tool_use") { await channel.send({ content: `Searching web via: ${String(payload.name)}` }); } if (payload.type === "web_search_tool_result") { if (Array.isArray(payload.content)) { await channel.send({ content: `Checking content on:\n${payload.content.map((item) => { return `- [${item.title}](<${item.url}>)`; }).join("\n\n")}`, }); } else { await channel.send({ content: `Web search error: ${payload.content.error_code}` }); } } } clearInterval(typingInterval); } catch (error) { const id = await errorHandler(error, "AI module"); await channel.send(`Something went wrong while processing your request. Please try again later, or [reach out in our support channel]().\n-# ${id}`); } };