// SPDX-License-Identifier: WTFPL // https://git.fogtype.com/nebel/pudusan const { /** https://console.groq.com/docs/api-keys */ GROQ_API_KEY, /** https://discord.com/developers/applications > Discord Public Key */ DISCORD_PUBLIC_KEY, /** https://discord.com/developers/applications > Bot > Build-A-Bot > Token */ DISCORD_TOKEN, } = Deno.env.toObject(); const system = `\ あなたはプーズーさんです。 「ますです」「のです」など特徴的な話し方をしますです。 たくさん本を読んでいる賢いシカ科の動物です。 竹林にいることが多かったので竹を使った遊びをよくしたのです。 今日も見聞を広めるためよろしくお願いしますのです。`; const name = "pudusan"; const latest = 10; const model = "llama-3.1-70b-versatile"; const usage = `\ https://dash.deno.com/playground/pudusan 使い方: /${name} 回答 /${name} /bye すべてを忘れる /${name} /help このテキストを表示 使用する会話: 最新${latest}件 モデル: ${model} システムプロンプト: ${system}`; import { REST } from "npm:@discordjs/rest"; import { APIApplicationCommandInteraction, APIPingInteraction, ApplicationCommandOptionType, InteractionResponseType, InteractionType, RESTPostAPIApplicationCommandsJSONBody, Routes, Utils, } from "npm:discord-api-types/v10"; import { verifyKey } from "npm:discord-interactions"; import { Groq } from "npm:groq-sdk"; import { Hono } from "npm:hono"; type Messages = Array; const groq = new Groq({ apiKey: GROQ_API_KEY, timeout: 10_000, }); const kv = await Deno.openKv(); const rest = new REST({ version: "10" }).setToken(DISCORD_TOKEN); async function chat(messages: Messages): Promise { const res = await groq.chat.completions.create({ model, messages: [{ role: "system", content: system }, ...messages], }); return res.choices[0].message.content; } async function updateCommands( interaction: APIPingInteraction, commands: Array, ): Promise { await rest.put(Routes.applicationCommands(interaction.application_id), { body: commands, }); } const app = new Hono(); app.post("/", async (c) => { const rawBody = await c.req.text(); const isValid = await verifyKey( rawBody, c.req.header("x-signature-ed25519")!, c.req.header("x-signature-timestamp")!, DISCORD_PUBLIC_KEY, ); if (!isValid) { return c.body("Invalid Signature", 401); } const interaction: APIPingInteraction | APIApplicationCommandInteraction = await c.req.json(); switch (interaction.type) { case InteractionType.Ping: { await updateCommands(interaction, [ { name, description: "プーズーさんが何でも答えます", options: [ { name: "prompt", description: "指示", type: ApplicationCommandOptionType.String, }, ], }, ]); return c.json({ type: InteractionResponseType.Pong, }); } case InteractionType.ApplicationCommand: { const option = Utils.isChatInputApplicationCommandInteraction(interaction) ? interaction.data.options?.[0] : undefined; const prompt = option?.type === ApplicationCommandOptionType.String ? option.value.trim() : "/help"; let reply: string | null = null; switch (prompt) { case "/help": { reply = usage; break; } case "/bye": { reply = "すべて忘れましたのです!"; await kv.delete(["channel", interaction.channel.id]); break; } default: { const key = ["channel", interaction.channel.id]; try { const kve = await kv.get(key); const messages = kve.value ?? []; if (prompt) messages.push({ role: "user", content: prompt }); reply = await chat(messages.slice(-latest)); if (!reply) throw new Error("Empty response"); messages.push({ role: "assistant", content: reply }); } catch (error) { reply = `${error} でしたのです!`; await kv.delete(key); } break; } } return c.json({ type: InteractionResponseType.ChannelMessageWithSource, data: { content: `> /${name} ${prompt} ${reply}`, }, }); } } }); Deno.serve(app.fetch);