171 lines
4.5 KiB
TypeScript
171 lines
4.5 KiB
TypeScript
// 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} <prompt> 回答
|
|
/${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<Groq.Chat.ChatCompletionMessageParam>;
|
|
|
|
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<string | null> {
|
|
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<RESTPostAPIApplicationCommandsJSONBody>,
|
|
): Promise<void> {
|
|
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<Messages>(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);
|