mirror of
https://github.com/syuilo/ai.git
synced 2025-03-25 21:12:56 +00:00
Merge pull request #6 from tetsuya-ki/enhance-aichat-add-geminipro1.5
Enhance aichat add geminipro1.5
This commit is contained in:
commit
dc6cba5ba0
8 changed files with 279 additions and 1 deletions
|
@ -9,7 +9,7 @@ RUN if [ $enable_mecab -ne 0 ]; then apt-get update \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt-get/lists/* \
|
&& rm -rf /var/lib/apt-get/lists/* \
|
||||||
&& cd /opt \
|
&& cd /opt \
|
||||||
&& git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git \
|
&& git clone --depth 1 https://github.com/yokomotod/mecab-ipadic-neologd.git \
|
||||||
&& cd /opt/mecab-ipadic-neologd \
|
&& cd /opt/mecab-ipadic-neologd \
|
||||||
&& ./bin/install-mecab-ipadic-neologd -n -y \
|
&& ./bin/install-mecab-ipadic-neologd -n -y \
|
||||||
&& rm -rf /opt/mecab-ipadic-neologd \
|
&& rm -rf /opt/mecab-ipadic-neologd \
|
||||||
|
|
|
@ -21,6 +21,8 @@ Misskey用の日本語Botです。
|
||||||
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
|
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||||
"checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)",
|
"checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||||
"checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)",
|
"checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)",
|
||||||
|
"geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は<https://ai.google.dev/pricing?hl=ja>"",
|
||||||
|
"prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。\n\n質問:」",
|
||||||
"mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)",
|
"mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)",
|
||||||
"mecabDic": "MeCab の辞書ファイルパス (オプション)",
|
"mecabDic": "MeCab の辞書ファイルパス (オプション)",
|
||||||
"memoryDir": "memory.jsonの保存先(オプション、デフォルトは'.'(レポジトリのルートです))"
|
"memoryDir": "memory.jsonの保存先(オプション、デフォルトは'.'(レポジトリのルートです))"
|
||||||
|
@ -44,6 +46,8 @@ Misskey用の日本語Botです。
|
||||||
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
|
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||||
"checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)",
|
"checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||||
"checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)",
|
"checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)",
|
||||||
|
"geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は<https://ai.google.dev/pricing?hl=ja>"",
|
||||||
|
"prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。\n\n質問:」",
|
||||||
"mecab": "/usr/bin/mecab",
|
"mecab": "/usr/bin/mecab",
|
||||||
"mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/",
|
"mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/",
|
||||||
"memoryDir": "data"
|
"memoryDir": "data"
|
||||||
|
|
|
@ -12,6 +12,9 @@ type Config = {
|
||||||
serverMonitoring: boolean;
|
serverMonitoring: boolean;
|
||||||
checkEmojisEnabled?: boolean;
|
checkEmojisEnabled?: boolean;
|
||||||
checkEmojisAtOnce?: boolean;
|
checkEmojisAtOnce?: boolean;
|
||||||
|
geminiProApiKey?: string;
|
||||||
|
pLaMoApiKey?: string;
|
||||||
|
prompt?: string;
|
||||||
mecab?: string;
|
mecab?: string;
|
||||||
mecabDic?: string;
|
mecabDic?: string;
|
||||||
memoryDir?: string;
|
memoryDir?: string;
|
||||||
|
|
|
@ -34,6 +34,7 @@ import NotingModule from './modules/noting/index.js';
|
||||||
import PollModule from './modules/poll/index.js';
|
import PollModule from './modules/poll/index.js';
|
||||||
import ReminderModule from './modules/reminder/index.js';
|
import ReminderModule from './modules/reminder/index.js';
|
||||||
import CheckCustomEmojisModule from './modules/check-custom-emojis/index.js';
|
import CheckCustomEmojisModule from './modules/check-custom-emojis/index.js';
|
||||||
|
import AiChatModule from './modules/aichat/index.js';
|
||||||
|
|
||||||
console.log(' __ ____ _____ ___ ');
|
console.log(' __ ____ _____ ___ ');
|
||||||
console.log(' /__\\ (_ _)( _ )/ __)');
|
console.log(' /__\\ (_ _)( _ )/ __)');
|
||||||
|
@ -96,6 +97,7 @@ promiseRetry(retry => {
|
||||||
new PollModule(),
|
new PollModule(),
|
||||||
new ReminderModule(),
|
new ReminderModule(),
|
||||||
new CheckCustomEmojisModule(),
|
new CheckCustomEmojisModule(),
|
||||||
|
new AiChatModule(),
|
||||||
]);
|
]);
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
log(chalk.red('Failed to fetch the account'));
|
log(chalk.red('Failed to fetch the account'));
|
||||||
|
|
241
src/modules/aichat/index.ts
Normal file
241
src/modules/aichat/index.ts
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
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 urlToBase64 from '@/utils/url2base64.js';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
|
type AiChat = {
|
||||||
|
question: string;
|
||||||
|
prompt: string;
|
||||||
|
api: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
type Base64Image = {
|
||||||
|
type: string;
|
||||||
|
base64: string;
|
||||||
|
};
|
||||||
|
const GEMINI_15_FLASH_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent';
|
||||||
|
const GEMINI_15_PRO_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent';
|
||||||
|
const PLAMO_API = 'https://platform.preferredai.jp/api/completion/v1/chat/completions';
|
||||||
|
|
||||||
|
export default class extends Module {
|
||||||
|
public readonly name = 'aichat';
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public install() {
|
||||||
|
return {
|
||||||
|
mentionHook: this.mentionHook
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async genTextByGemini(aiChat: AiChat, image:Base64Image|null) {
|
||||||
|
this.log('Generate Text By Gemini...');
|
||||||
|
let parts: ({ text: string; inline_data?: undefined; } | { inline_data: { mime_type: string; data: string; }; text?: undefined; })[];
|
||||||
|
if (image === null) {
|
||||||
|
// 画像がない場合、メッセージのみで問い合わせ
|
||||||
|
parts = [{text: aiChat.prompt + aiChat.question}];
|
||||||
|
} else {
|
||||||
|
// 画像が存在する場合、画像を添付して問い合わせ
|
||||||
|
parts = [
|
||||||
|
{ text: aiChat.prompt + aiChat.question },
|
||||||
|
{
|
||||||
|
inline_data: {
|
||||||
|
mime_type: image.type,
|
||||||
|
data: image.base64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
let options = {
|
||||||
|
url: aiChat.api,
|
||||||
|
searchParams: {
|
||||||
|
key: aiChat.key,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
contents: {parts: parts}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.log(JSON.stringify(options));
|
||||||
|
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}\n${err.stack}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async genTextByPLaMo(aiChat: AiChat) {
|
||||||
|
this.log('Generate Text By PLaMo...');
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
url: aiChat.api,
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + aiChat.key
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
model: 'plamo-beta',
|
||||||
|
messages: [
|
||||||
|
{role: 'system', content: aiChat.prompt},
|
||||||
|
{role: 'user', content: aiChat.question},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.log(JSON.stringify(options));
|
||||||
|
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('choices')) {
|
||||||
|
if (res_data.choices.length > 0) {
|
||||||
|
if (res_data.choices[0].hasOwnProperty('message')) {
|
||||||
|
if (res_data.choices[0].message.hasOwnProperty('content')) {
|
||||||
|
return res_data.choices[0].message.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.log('Error By Call PLaMo');
|
||||||
|
if (err instanceof Error) {
|
||||||
|
this.log(`${err.name}\n${err.message}\n${err.stack}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async note2base64Image(notesId: string) {
|
||||||
|
const noteData = await this.ai.api('notes/show', { noteId: notesId });
|
||||||
|
let fileType: string | undefined,thumbnailUrl: string | undefined;
|
||||||
|
if (noteData !== null && noteData.hasOwnProperty('files')) {
|
||||||
|
if (noteData.files.length > 0) {
|
||||||
|
if (noteData.files[0].hasOwnProperty('type')) {
|
||||||
|
fileType = noteData.files[0].type;
|
||||||
|
}
|
||||||
|
if (noteData.files[0].hasOwnProperty('thumbnailUrl')) {
|
||||||
|
thumbnailUrl = noteData.files[0].thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileType !== undefined && thumbnailUrl !== undefined) {
|
||||||
|
try {
|
||||||
|
const image = await urlToBase64(thumbnailUrl);
|
||||||
|
const base64Image:Base64Image = {type: fileType, base64: image};
|
||||||
|
return base64Image;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
this.log(`${err.name}\n${err.message}\n${err.stack}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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';
|
||||||
|
} else if (msg.includes([kigo + 'plamo'])) {
|
||||||
|
type = 'plamo';
|
||||||
|
}
|
||||||
|
const question = msg.extractedText
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(this.name, '')
|
||||||
|
.replace(kigo + type, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
let text:string, aiChat:AiChat;
|
||||||
|
let prompt:string = '';
|
||||||
|
if (config.prompt) {
|
||||||
|
prompt = config.prompt;
|
||||||
|
}
|
||||||
|
switch(type) {
|
||||||
|
case 'gemini':
|
||||||
|
// geminiの場合、APIキーが必須
|
||||||
|
if (!config.geminiProApiKey) {
|
||||||
|
msg.reply(serifs.aichat.nothing(type));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const base64Image:Base64Image|null = await this.note2base64Image(msg.id);
|
||||||
|
aiChat = {
|
||||||
|
question: question,
|
||||||
|
prompt: prompt,
|
||||||
|
api: GEMINI_15_PRO_API,
|
||||||
|
key: config.geminiProApiKey
|
||||||
|
};
|
||||||
|
if (msg.includes([kigo + 'gemini-flash'])) {
|
||||||
|
aiChat.api = GEMINI_15_FLASH_API;
|
||||||
|
}
|
||||||
|
text = await this.genTextByGemini(aiChat, base64Image);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PLaMo':
|
||||||
|
// PLaMoの場合、APIキーが必須
|
||||||
|
if (!config.pLaMoApiKey) {
|
||||||
|
msg.reply(serifs.aichat.nothing(type));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
aiChat = {
|
||||||
|
question: question,
|
||||||
|
prompt: prompt,
|
||||||
|
api: PLAMO_API,
|
||||||
|
key: config.pLaMoApiKey
|
||||||
|
};
|
||||||
|
text = await this.genTextByPLaMo(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.error(type));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('Replying...');
|
||||||
|
msg.reply(serifs.aichat.post(text, type));
|
||||||
|
|
||||||
|
return {
|
||||||
|
reaction: 'like'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -388,6 +388,12 @@ export default {
|
||||||
emojiOnce: emoji => `:${emoji}:(\`${emoji}\`)`
|
emojiOnce: emoji => `:${emoji}:(\`${emoji}\`)`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
aichat: {
|
||||||
|
nothing: type => `あぅ... ${type}のAPIキーが登録されてないみたいです`,
|
||||||
|
error: type => `うぇ...${type}でエラーが発生しちゃったみたいです。gemini-flashだと動くかも?`,
|
||||||
|
post: (text, type) => `${text} (${type}) #aichat`,
|
||||||
|
},
|
||||||
|
|
||||||
sleepReport: {
|
sleepReport: {
|
||||||
report: hours => `んぅ、${hours}時間くらい寝ちゃってたみたいです`,
|
report: hours => `んぅ、${hours}時間くらい寝ちゃってたみたいです`,
|
||||||
reportUtatane: 'ん... うたた寝しちゃってました',
|
reportUtatane: 'ん... うたた寝しちゃってました',
|
||||||
|
|
16
src/utils/url2base64.ts
Normal file
16
src/utils/url2base64.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import log from '@/utils/log.js';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
|
export default async function(url: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const buffer = await got(url).buffer();
|
||||||
|
const base64Image = buffer.toString('base64');
|
||||||
|
return base64Image;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
log('Error in url2base64');
|
||||||
|
if (err instanceof Error) {
|
||||||
|
log(`${err.name}\n${err.message}\n${err.stack}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,12 @@ PONGを返します。生存確認にどうぞ
|
||||||
### カスタム絵文字チェック
|
### カスタム絵文字チェック
|
||||||
1日に1回、カスタム絵文字の追加を監視してくれます。「カスタムえもじチェック」または「カスタムえもじを確認して」ですぐに確認してくれます。
|
1日に1回、カスタム絵文字の追加を監視してくれます。「カスタムえもじチェック」または「カスタムえもじを確認して」ですぐに確認してくれます。
|
||||||
|
|
||||||
|
### aichat
|
||||||
|
```
|
||||||
|
@ai aichat 部屋の片付けの手順を教えて
|
||||||
|
```
|
||||||
|
のようにメンションを飛ばすと、GoogleのGemini APIなどを使って返答してくれます(今のバージョンではGemini APIのみ対応)。利用するにはAPIキーの登録が必要です。
|
||||||
|
|
||||||
### その他反応するフレーズ (トークのみ)
|
### その他反応するフレーズ (トークのみ)
|
||||||
* かわいい
|
* かわいい
|
||||||
* なでなで
|
* なでなで
|
||||||
|
|
Loading…
Reference in a new issue