create project
This commit is contained in:
parent
8ef892e7a7
commit
c2ef90443e
3 changed files with 194 additions and 0 deletions
7
README.md
Normal file
7
README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# プーズーさん
|
||||
|
||||
Discord Bot
|
||||
|
||||
## ライセンス
|
||||
|
||||
WTFPL
|
9
deno.json
Normal file
9
deno.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"imports": {
|
||||
"@discordjs/rest": "npm:@discordjs/rest@^2.3.0",
|
||||
"discord-api-types": "npm:discord-api-types@^0.37.93",
|
||||
"discord-interactions": "npm:discord-interactions@^4.0.0",
|
||||
"groq-sdk": "npm:groq-sdk@^0.5.0",
|
||||
"hono": "npm:hono@^4.5.2"
|
||||
}
|
||||
}
|
178
main.ts
Normal file
178
main.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
// SPDX-License-Identifier: WTFPL
|
||||
|
||||
const {
|
||||
/** https://console.groq.com/docs/api-keys */
|
||||
GROQ_API_KEY,
|
||||
/**
|
||||
* 設定方法
|
||||
* 1. Discord アプリを作成 https://discord.com/developers/applications
|
||||
* 2. Public Key をコピー
|
||||
* 3. Bot トークンを生成 (Bot > Build-A-Bot > Rest Token > Copy)
|
||||
*
|
||||
* デプロイ後
|
||||
* 4. Interactions Endpoint URL に https://pudusan.deno.dev を指定
|
||||
* 5. Install Link にアクセス (Installation > Install Link > サーバーに追加)
|
||||
*/
|
||||
DISCORD_PUBLIC_KEY,
|
||||
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);
|
Loading…
Add table
Reference in a new issue