/** * @copyright NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { AttachmentBuilder, MessageFlags, type ChatInputCommandInteraction, } from "discord.js"; import { ids } from "../config/ids.js"; import { anthropic } from "../utils/anthropic.js"; import { logger } from "../utils/logger.js"; import { amariPersonality } from "../utils/makeAiRequest.js"; import type { Amari } from "../interfaces/amari.js"; import type { ContentBlock } from "@anthropic-ai/sdk/resources/messages.js"; const researchSystemPrompt = `${amariPersonality}\n\nYour role in this` + " conversation is to research the user's query thoroughly using web search." + " Provide a comprehensive, well-structured response with cited sources." + " Format your response clearly with headers and sections where" + " appropriate."; /** * Formats a single content block into a markdown string. * @param block - The content block to format. * @returns A formatted markdown string. */ const formatBlock = (block: ContentBlock): string => { if (block.type === "text") { if ((block.citations?.length ?? 0) > 0) { const sources = (block.citations ?? []). filter((citation) => { return citation.type === "web_search_result_location"; }). map((citation) => { return `- [${citation.title ?? "Unknown Title"}](${citation.url})`; }). join("\n"); return `${block.text}\n\n**Sources:**\n${sources}`; } return block.text; } if (block.type === "server_tool_use") { return `> 🔍 *Searching: ${JSON.stringify(block.input)}*`; } if (block.type === "web_search_tool_result") { if (!Array.isArray(block.content)) { return ""; } const links = block.content. map((entry) => { return `[${entry.title}](${entry.url})`; }). join(", "); return `> 📄 *Found: ${links}*`; } if (block.type === "thinking") { return `> 💭 *${block.thinking}*`; } return ""; }; /** * Builds a markdown buffer from the research prompt and API response content. * @param prompt - The original research prompt. * @param content - The content blocks returned from the API. * @returns A Buffer containing the formatted markdown. */ const buildMarkdownFile = ( prompt: string, content: Array, ): Buffer => { const markdown = [ `# Research: ${prompt}`, "", ...content. map((block) => { return formatBlock(block); }). filter((block) => { return block.length > 0; }), ].join("\n\n"); return Buffer.from(markdown, "utf-8"); }; /** * Runs a web-search-backed research query and returns the result as a * markdown file attachment. * @param _amari - The Amari instance (unused but kept for handler consistency). * @param interaction - The Discord slash command interaction. */ export const research = async( _amari: Amari, interaction: ChatInputCommandInteraction, ): Promise => { if (interaction.user.id !== ids.users.naomi) { await interaction.reply({ content: "This command is restricted to Naomi.", flags: [ MessageFlags.Ephemeral ], }); return; } const prompt = interaction.options.getString("prompt", true); await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] }); try { const response = await anthropic.messages.create({ // eslint-disable-next-line @typescript-eslint/naming-convention -- Anthropic API field. max_tokens: 3000, messages: [ { content: prompt, role: "user" } ], model: "claude-sonnet-4-6", system: researchSystemPrompt, tools: [ { // eslint-disable-next-line @typescript-eslint/naming-convention -- Anthropic API field. max_uses: 5, name: "web_search", type: "web_search_20250305", } ], }); const file = new AttachmentBuilder( buildMarkdownFile(prompt, response.content), { name: "research.md" }, ); await logger.metric("research_query", 1, { user: interaction.user.id }); await interaction.editReply({ content: "Here are your research results!", files: [ file ], }); } catch (error) { await logger.error("research command", error instanceof Error ? error : new Error(String(error))); await interaction.editReply({ content: "Something went wrong whilst running the research query.", }); } };