Compare commits

..

3 commits

2 changed files with 29 additions and 50 deletions

View file

@ -1,6 +1,6 @@
{ {
"imports": { "imports": {
"@slack/bolt": "npm:@slack/bolt@^3.22.0", "@openai/openai": "jsr:@openai/openai@^4.79.1",
"groq-sdk": "npm:groq-sdk@^0.7.0" "@slack/bolt": "npm:@slack/bolt@^4.2.0"
} }
} }

75
main.ts
View file

@ -1,8 +1,8 @@
// SPDX-License-Identifier: WTFPL // SPDX-License-Identifier: WTFPL
const { const {
/** https://console.groq.com/docs/api-keys */ /** https://openrouter.ai/settings/keys */
GROQ_API_KEY, OPENROUTER_API_KEY = "",
/** https://api.slack.com/apps → Basic Information ページにある Signing Secret */ /** https://api.slack.com/apps → Basic Information ページにある Signing Secret */
SLACK_SIGNING_SECRET = "", SLACK_SIGNING_SECRET = "",
/** https://api.slack.com/apps → Permissions ページにある `xoxb-` から始まるボットトークン */ /** https://api.slack.com/apps → Permissions ページにある `xoxb-` から始まるボットトークン */
@ -18,8 +18,8 @@ const system = `\
`; `;
const n = 10; const n = 10;
const textModel = "llama-3.3-70b-versatile"; const model = "google/gemini-2.0-flash-exp:free";
const visionModel = "llama-3.2-90b-vision-preview"; const baseURL = "https://openrouter.ai/api/v1";
const urlPattern = new URLPattern({ pathname: "*.*" }); const urlPattern = new URLPattern({ pathname: "*.*" });
const separatorRegex = /[<>]|[^\p{L}\p{N}\p{P}\p{S}]+/u; const separatorRegex = /[<>]|[^\p{L}\p{N}\p{P}\p{S}]+/u;
const imageExtensionRegex = /[.](?:gif|png|jpe?g|webp)$/i; const imageExtensionRegex = /[.](?:gif|png|jpe?g|webp)$/i;
@ -32,28 +32,30 @@ https://dash.deno.com/playground/darazllm
@darazllm /help @darazllm /help
使用する会話: 最新${n} 使用する会話: 最新${n}
テキスト用モデル: ${textModel} モデル: ${model}
画像用モデル: ${visionModel} ベースURL: ${baseURL}
区切り文字: ${separatorRegex} 区切り文字: ${separatorRegex}
画像拡張子: ${imageExtensionRegex} 画像拡張子: ${imageExtensionRegex}
: :
${system}`; ${system}`;
import OpenAI from "jsr:@openai/openai";
import bolt from "npm:@slack/bolt"; import bolt from "npm:@slack/bolt";
import { Groq } from "npm:groq-sdk";
type Messages = Array<Groq.Chat.ChatCompletionMessageParam>; type Messages = Array<OpenAI.Chat.ChatCompletionMessageParam>;
const groq = new Groq({ const openai = new OpenAI({
apiKey: GROQ_API_KEY, baseURL,
apiKey: OPENROUTER_API_KEY,
timeout: 10_000, timeout: 10_000,
}); });
const kv = await Deno.openKv(); const kv = await Deno.openKv();
async function chat(messages: Messages): Promise<string | null> { async function chat(messages: Messages): Promise<string | null> {
const res = await groq.chat.completions.create({ const res = await openai.chat.completions.create({
model: textModel, model,
messages: [{ role: "system", content: system }, ...messages], messages: [{ role: "system", content: system }, ...messages],
}); });
@ -64,25 +66,6 @@ function isImageUrl(message: string): boolean {
return urlPattern.test(message) && imageExtensionRegex.test(message); 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) =>
c.type === "text" ? { ...c, text: `${system}\n\n${c.text}` } : c,
),
},
],
});
return res.choices[0].message.content;
}
const app = new bolt.App({ const app = new bolt.App({
token: SLACK_BOT_TOKEN, token: SLACK_BOT_TOKEN,
signingSecret: SLACK_SIGNING_SECRET, signingSecret: SLACK_SIGNING_SECRET,
@ -110,26 +93,22 @@ app.message(async (c) => {
const kve = await kv.get<Messages>(["channel", c.message.channel]); const kve = await kv.get<Messages>(["channel", c.message.channel]);
const messages = kve.value ?? []; const messages = kve.value ?? [];
if (prompt) messages.push({ role: "user", content: prompt }); const content: Array<OpenAI.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 };
}
});
if (content.length > 0) messages.push({ role: "user", content });
if (isMention) { if (isMention) {
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");
try { try {
const res = visionMode const res = await chat(messages.slice(-n));
? await visionChat(content)
: await chat(messages.slice(-n));
if (!res) throw new Error("Empty response"); if (!res) throw new Error("Empty response");