From 47e17052c5f67c9b384002f7bf848e268492a7d7 Mon Sep 17 00:00:00 2001 From: tetsuya-ki <64536338+tetsuya-ki@users.noreply.github.com> Date: Sun, 25 Feb 2024 19:24:53 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=20aichat=E6=A9=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 現時点ではGemini APIのみ対応 --- README.md | 4 ++ src/config.ts | 2 + src/index.ts | 2 + src/modules/aichat/index.ts | 128 ++++++++++++++++++++++++++++++++++++ src/serifs.ts | 5 ++ torisetu.md | 6 ++ 6 files changed, 147 insertions(+) create mode 100644 src/modules/aichat/index.ts diff --git a/README.md b/README.md index 042e75e..cfc8bb9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Misskey用の日本語Botです。 "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)", + "geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は"", + "prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。\n\n質問:」", "mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)", "mecabDic": "MeCab の辞書ファイルパス (オプション)", "memoryDir": "memory.jsonの保存先(オプション、デフォルトは'.'(レポジトリのルートです))" @@ -44,6 +46,8 @@ Misskey用の日本語Botです。 "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)", "checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)", + "geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は"", + "prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。\n\n質問:」", "mecab": "/usr/bin/mecab", "mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/", "memoryDir": "data" diff --git a/src/config.ts b/src/config.ts index 9466b0c..119daa8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,6 +12,8 @@ type Config = { serverMonitoring: boolean; checkEmojisEnabled?: boolean; checkEmojisAtOnce?: boolean; + geminiProApiKey?: string; + prompt?: string; mecab?: string; mecabDic?: string; memoryDir?: string; diff --git a/src/index.ts b/src/index.ts index 2f85f93..991a6d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ import NotingModule from './modules/noting/index.js'; import PollModule from './modules/poll/index.js'; import ReminderModule from './modules/reminder/index.js'; import CheckCustomEmojisModule from './modules/check-custom-emojis/index.js'; +import AiChatModule from './modules/aichat/index.js'; console.log(' __ ____ _____ ___ '); console.log(' /__\\ (_ _)( _ )/ __)'); @@ -96,6 +97,7 @@ promiseRetry(retry => { new PollModule(), new ReminderModule(), new CheckCustomEmojisModule(), + new AiChatModule(), ]); }).catch(e => { log(chalk.red('Failed to fetch the account')); diff --git a/src/modules/aichat/index.ts b/src/modules/aichat/index.ts new file mode 100644 index 0000000..5c6dfbc --- /dev/null +++ b/src/modules/aichat/index.ts @@ -0,0 +1,128 @@ +import { bindThis } from '@/decorators.js'; +import Module from '@/module.js'; +import serifs from '@/serifs.js'; +import Message from '@/message.js'; +import config from '@/config.js'; +import got from 'got'; + +type AiChat = { + question: string; + prompt: string; + api: string; + key: string; +}; +const GEMINI_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; + +export default class extends Module { + public readonly name = 'aichat'; + + @bindThis + public install() { + return { + mentionHook: this.mentionHook + }; + } + + @bindThis + private async genTextByGemini(aiChat: AiChat) { + this.log('Generate Text By Gemini...'); + var options = { + url: aiChat.api, + searchParams: { + key: aiChat.key, + }, + json: { + contents: [{ + parts:[{ + text: aiChat.prompt + aiChat.question + }] + }] + }, + }; + let res_data:any = null; + try { + res_data = await got.post(options, + {parseJson: res => JSON.parse(res)}).json(); + this.log(JSON.stringify(res_data)); + if (res_data.hasOwnProperty('candidates')) { + if (res_data.candidates.length > 0) { + if (res_data.candidates[0].hasOwnProperty('content')) { + if (res_data.candidates[0].content.hasOwnProperty('parts')) { + if (res_data.candidates[0].content.parts.length > 0) { + if (res_data.candidates[0].content.parts[0].hasOwnProperty('text')) { + return res_data.candidates[0].content.parts[0].text; + } + } + } + } + } + } + } catch (err: unknown) { + this.log('Error By Call Gemini'); + if (err instanceof Error) { + this.log(`${err.name}\n${err.message}`); + } + } + return null; + } + + @bindThis + private async mentionHook(msg: Message) { + if (!msg.includes([this.name])) { + return false; + } else { + this.log('AiChat requested'); + } + + const kigo = '&'; + let type = 'gemini'; + if (msg.includes([kigo + 'gemini'])) { + type = 'gemini'; + } else if (msg.includes([kigo + 'chatgpt4'])) { + type = 'chatgpt4'; + } else if (msg.includes([kigo + 'chatgpt'])) { + type = 'chatgpt3.5'; + } + const question = msg.extractedText + .replace(this.name, '') + .replace(kigo + type, '') + .trim(); + + let text; + let prompt = ''; + if (config.prompt) { + prompt = config.prompt; + } + switch(type) { + case 'gemini': + if (!config.geminiProApiKey) { + msg.reply(serifs.aichat.nothing(type)); + return false; + } + const aiChat = { + question: question, + prompt: prompt, + api: GEMINI_API, + key: config.geminiProApiKey + }; + text = await this.genTextByGemini(aiChat); + break; + default: + msg.reply(serifs.aichat.nothing(type)); + return false; + } + + if (text == null) { + this.log('The result is invalid. It seems that tokens and other items need to be reviewed.') + msg.reply(serifs.aichat.nothing(type)); + return false; + } + + this.log('Replying...'); + msg.reply(serifs.aichat.post(text, type)); + + return { + reaction: 'like' + }; + } +} diff --git a/src/serifs.ts b/src/serifs.ts index 49fc8d4..2fe0c15 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -388,6 +388,11 @@ export default { emojiOnce: emoji => `:${emoji}:(\`${emoji}\`)` }, + aichat: { + nothing: type => `あぅ... ${type}のAPIキーが登録されてないみたいです`, + post: (text, type) => `${text} (${type}) #aichat`, + }, + sleepReport: { report: hours => `んぅ、${hours}時間くらい寝ちゃってたみたいです`, reportUtatane: 'ん... うたた寝しちゃってました', diff --git a/torisetu.md b/torisetu.md index 0d29985..4ab4b97 100644 --- a/torisetu.md +++ b/torisetu.md @@ -78,6 +78,12 @@ PONGを返します。生存確認にどうぞ ### カスタム絵文字チェック 1日に1回、カスタム絵文字の追加を監視してくれます。「カスタムえもじチェック」または「カスタムえもじを確認して」ですぐに確認してくれます。 +### aichat +``` +@ai aichat 部屋の片付けの手順を教えて +``` +のようにメンションを飛ばすと、GoogleのGemini APIなどを使って返答してくれます(今のバージョンではGemini APIのみ対応)。利用するにはAPIキーの登録が必要です。 + ### その他反応するフレーズ (トークのみ) * かわいい * なでなで