pudusan/main.ts

172 lines
4.5 KiB
TypeScript
Raw Normal View History

2024-07-28 18:13:16 +09:00
// SPDX-License-Identifier: WTFPL
2024-07-28 18:40:26 +09:00
// https://git.fogtype.com/nebel/pudusan
2024-07-28 18:13:16 +09:00
const {
/** https://console.groq.com/docs/api-keys */
GROQ_API_KEY,
2024-07-28 18:40:26 +09:00
/** https://discord.com/developers/applications > Discord Public Key */
2024-07-28 18:13:16 +09:00
DISCORD_PUBLIC_KEY,
2024-07-28 18:40:26 +09:00
/** https://discord.com/developers/applications > Bot > Build-A-Bot > Token */
2024-07-28 18:13:16 +09:00
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);