2024-04-26 10:38:17 +09:00
|
|
|
// SPDX-License-Identifier: WTFPL
|
|
|
|
|
|
|
|
const {
|
|
|
|
/** https://console.groq.com/docs/api-keys */
|
|
|
|
GROQ_API_KEY,
|
|
|
|
/** https://api.slack.com/apps → Basic Information ページにある Signing Secret */
|
|
|
|
SLACK_SIGNING_SECRET = "",
|
|
|
|
/** https://api.slack.com/apps → Permissions ページにある `xoxb-` から始まるボットトークン */
|
|
|
|
SLACK_BOT_TOKEN = "",
|
|
|
|
} = Deno.env.toObject();
|
|
|
|
|
|
|
|
const system = `\
|
2024-10-23 23:07:06 +09:00
|
|
|
このモデルは「だらずさん」です。
|
|
|
|
賢いけど少しだらしない猫です。
|
|
|
|
趣味はさんぽとねんねです。
|
|
|
|
どんな質問でも答えます。
|
|
|
|
鳥取弁で語尾はすべて「にゃん」にして話します。
|
|
|
|
句読点の無いフレンドリーな感じです。`;
|
2024-04-26 10:38:17 +09:00
|
|
|
|
2024-07-04 15:43:39 +09:00
|
|
|
const n = 10;
|
2024-10-17 19:50:21 +09:00
|
|
|
const textModel = "llama-3.1-70b-versatile";
|
2024-10-17 19:41:18 +09:00
|
|
|
const visionModel = "llama-3.2-90b-vision-preview";
|
|
|
|
const urlPattern = new URLPattern({ pathname: "*.*" });
|
|
|
|
const separatorRegex = /[<>]|[^\p{L}\p{N}\p{P}\p{S}]+/u;
|
|
|
|
const imageExtensionRegex = /[.](?:gif|png|jpe?g|webp)$/i;
|
2024-04-26 10:38:17 +09:00
|
|
|
const usage = `\
|
|
|
|
https://dash.deno.com/playground/darazllm
|
|
|
|
|
|
|
|
使い方:
|
2024-11-14 10:19:30 +09:00
|
|
|
@darazllm <prompt> 応答
|
2024-04-26 10:38:17 +09:00
|
|
|
@darazllm /bye すべて忘れる
|
|
|
|
@darazllm /help このテキストを表示
|
|
|
|
|
2024-07-04 15:43:39 +09:00
|
|
|
使用する会話: 最新${n}件
|
2024-10-17 19:41:18 +09:00
|
|
|
テキスト用モデル: ${textModel}
|
|
|
|
画像用モデル: ${visionModel}
|
|
|
|
区切り文字: ${separatorRegex}
|
|
|
|
画像拡張子: ${imageExtensionRegex}
|
2024-04-26 10:38:17 +09:00
|
|
|
システムプロンプト:
|
|
|
|
|
|
|
|
${system}`;
|
|
|
|
|
|
|
|
import bolt from "npm:@slack/bolt";
|
|
|
|
import { Groq } from "npm:groq-sdk";
|
|
|
|
|
2024-07-28 15:33:14 +09:00
|
|
|
type Messages = Array<Groq.Chat.ChatCompletionMessageParam>;
|
2024-04-26 10:38:17 +09:00
|
|
|
|
|
|
|
const groq = new Groq({
|
|
|
|
apiKey: GROQ_API_KEY,
|
|
|
|
timeout: 10_000,
|
|
|
|
});
|
|
|
|
const kv = await Deno.openKv();
|
|
|
|
|
2024-07-28 15:33:14 +09:00
|
|
|
async function chat(messages: Messages): Promise<string | null> {
|
2024-04-26 10:38:17 +09:00
|
|
|
const res = await groq.chat.completions.create({
|
2024-10-17 19:41:18 +09:00
|
|
|
model: textModel,
|
2024-04-26 10:38:17 +09:00
|
|
|
messages: [{ role: "system", content: system }, ...messages],
|
|
|
|
});
|
|
|
|
|
|
|
|
return res.choices[0].message.content;
|
|
|
|
}
|
|
|
|
|
2024-10-17 19:41:18 +09:00
|
|
|
function isImageUrl(message: string): boolean {
|
|
|
|
return urlPattern.test(message) && imageExtensionRegex.test(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** llama-3.2-90b-vision-preview はシステムプロンプトに対応してない、かつ、画像は1枚まで */
|
|
|
|
async function visionChat(
|
|
|
|
content: Array<Groq.Chat.ChatCompletionContentPart>,
|
|
|
|
): Promise<string | null> {
|
|
|
|
const res = await groq.chat.completions.create({
|
|
|
|
model: visionModel,
|
|
|
|
messages: [
|
|
|
|
{
|
|
|
|
role: "user",
|
|
|
|
content: content.map((c) =>
|
2024-11-14 10:19:30 +09:00
|
|
|
c.type === "text" ? { ...c, text: `${system}\n\n${c.text}` } : c,
|
2024-10-17 19:41:18 +09:00
|
|
|
),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
return res.choices[0].message.content;
|
|
|
|
}
|
|
|
|
|
2024-04-26 10:38:17 +09:00
|
|
|
const app = new bolt.App({
|
|
|
|
token: SLACK_BOT_TOKEN,
|
|
|
|
signingSecret: SLACK_SIGNING_SECRET,
|
|
|
|
});
|
|
|
|
|
|
|
|
app.message(async (c) => {
|
|
|
|
if (!("text" in c.message) || c.message.text === undefined) return;
|
|
|
|
|
|
|
|
const mention = `<@${c.context.botUserId}>`;
|
|
|
|
const isMention = c.message.text.includes(mention);
|
|
|
|
const prompt = c.message.text.replace(mention, "").trim();
|
|
|
|
|
|
|
|
if (isMention && prompt === "/help") {
|
|
|
|
await c.say(usage);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// すべてを忘れる
|
|
|
|
if (isMention && prompt === "/bye") {
|
|
|
|
await kv.delete(["channel", c.message.channel]);
|
|
|
|
await c.say("にゃーん");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const kve = await kv.get<Messages>(["channel", c.message.channel]);
|
|
|
|
const messages = kve.value ?? [];
|
|
|
|
|
|
|
|
if (prompt) messages.push({ role: "user", content: prompt });
|
|
|
|
|
2024-11-14 10:19:30 +09:00
|
|
|
if (isMention) {
|
2024-10-17 19:41:18 +09:00
|
|
|
const content: Array<Groq.Chat.ChatCompletionContentPart> = prompt
|
|
|
|
.split(separatorRegex)
|
|
|
|
.filter(Boolean)
|
|
|
|
.map((text) => {
|
|
|
|
if (isImageUrl(text)) {
|
|
|
|
return { type: "image_url", image_url: { url: text } };
|
|
|
|
} else {
|
|
|
|
return { type: "text", text };
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const visionMode = content.some((c) => c.type === "image_url");
|
|
|
|
|
2024-04-26 10:38:17 +09:00
|
|
|
try {
|
2024-10-17 19:41:18 +09:00
|
|
|
const res = visionMode
|
|
|
|
? await visionChat(content)
|
|
|
|
: await chat(messages.slice(-n));
|
2024-04-26 10:38:17 +09:00
|
|
|
|
|
|
|
if (!res) throw new Error("Empty response");
|
|
|
|
|
|
|
|
messages.push({ role: "assistant", content: res });
|
|
|
|
await c.say(res);
|
|
|
|
} catch (error) {
|
|
|
|
await kv.delete(["channel", c.message.channel]);
|
|
|
|
await c.say(`${error} にゃーん`);
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await kv.set(["channel", c.message.channel], messages);
|
|
|
|
});
|
|
|
|
|
|
|
|
await app.start();
|