mirror of
https://github.com/syuilo/ai.git
synced 2025-01-31 07:29:10 +00:00
feat:aichat機能(Gemini APIなどを使ってチャットする機能) (#153)
* カスタム絵文字チェックモジュールの追加 * カスタム絵文字チェックモジュールの修正 - README.mdやtorisetu.mdに記載 - カスタム絵文字投稿をまとめる設定を追加(checkEmojisAtOnce) * Feat: aichat機能 * 現時点ではGemini APIのみ対応 * enhance: aichat機能(画像対応) * fix #2 * fix #4 * fix #5 * README.mdにPLaMoについて記載 * aichatの文字列置換を正規表現に変更 * Feat: aichat機能 * 現時点ではGemini APIのみ対応 * enhance: aichat機能(画像対応) * fix #4 * fix #5 * README.mdにPLaMoについて記載 * aichatの文字列置換を正規表現に変更 * fix #7 - aichatで回答したものに返信すると、文脈を保持して回答されるように変更 * fix #9 - ランダムにaichatを発動し話しかける機能の追加 - 設定追加 - aichatRandomTalkEnabled - aichatRandomTalkProbability - aichatRandomTalkIntervalMinutes * torisetu.md更新(aichat)
This commit is contained in:
parent
830c9c2ecd
commit
81543029a8
8 changed files with 551 additions and 1 deletions
|
@ -9,7 +9,7 @@ RUN if [ $enable_mecab -ne 0 ]; then apt-get update \
|
|||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt-get/lists/* \
|
||||
&& 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 \
|
||||
&& ./bin/install-mecab-ipadic-neologd -n -y \
|
||||
&& rm -rf /opt/mecab-ipadic-neologd \
|
||||
|
|
12
README.md
12
README.md
|
@ -21,6 +21,12 @@ Misskey用の日本語Botです。
|
|||
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||
"checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||
"checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)",
|
||||
"geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は<https://ai.google.dev/pricing?hl=ja>",
|
||||
"pLaMoApiKey": "PLaMo APIキー。2024年8月〜10月(予定)は無料でトライアル可能。詳細は<https://plamo.preferredai.jp/>",
|
||||
"prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。」",
|
||||
"aichatRandomTalkEnabled": "ランダムにaichatを発動し話しかける機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||
"aichatRandomTalkProbability": "ランダムにaichatを発動し話しかける機能の確率(1以下の小数点を含む数値(0.01など。1に近づくほど発動しやすい))",
|
||||
"aichatRandomTalkIntervalMinutes": "ランダムトーク間隔(分)。指定した時間ごとにタイムラインを取得し、適当に選んだ人にaichatする(1の場合1分ごと実行)。デフォルトは720分(12時間)",
|
||||
"mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)",
|
||||
"mecabDic": "MeCab の辞書ファイルパス (オプション)",
|
||||
"memoryDir": "memory.jsonの保存先(オプション、デフォルトは'.'(レポジトリのルートです))"
|
||||
|
@ -44,6 +50,12 @@ Misskey用の日本語Botです。
|
|||
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||
"checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||
"checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false)",
|
||||
"geminiProApiKey": "Gemini APIキー。2024年初頭は無料で取得可能。詳細は<https://ai.google.dev/pricing?hl=ja>",
|
||||
"pLaMoApiKey": "PLaMo APIキー。2024年8月〜10月(予定)は無料でトライアル可能。詳細は<https://plamo.preferredai.jp/>",
|
||||
"prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください。」",
|
||||
"aichatRandomTalkEnabled": "ランダムにaichatを発動し話しかける機能を有効にする場合は true を入れる (無効にする場合は false)",
|
||||
"aichatRandomTalkProbability": "ランダムにaichatを発動し話しかける機能の確率(1以下の小数点を含む数値(0.01など。1に近づくほど発動しやすい))。デフォルトは0.02(2%)",
|
||||
"aichatRandomTalkIntervalMinutes": "ランダムトーク間隔(分)。指定した時間ごとにタイムラインを取得し、適当に選んだ人にaichatする(1の場合1分ごと実行)。デフォルトは720分(12時間)",
|
||||
"mecab": "/usr/bin/mecab",
|
||||
"mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/",
|
||||
"memoryDir": "data"
|
||||
|
|
|
@ -12,6 +12,12 @@ type Config = {
|
|||
serverMonitoring: boolean;
|
||||
checkEmojisEnabled?: boolean;
|
||||
checkEmojisAtOnce?: boolean;
|
||||
geminiProApiKey?: string;
|
||||
pLaMoApiKey?: string;
|
||||
prompt?: string;
|
||||
aichatRandomTalkEnabled?: string;
|
||||
aichatRandomTalkProbability?: string;
|
||||
aichatRandomTalkIntervalMinutes?: string;
|
||||
mecab?: string;
|
||||
mecabDic?: string;
|
||||
memoryDir?: string;
|
||||
|
|
|
@ -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'));
|
||||
|
|
501
src/modules/aichat/index.ts
Normal file
501
src/modules/aichat/index.ts
Normal file
|
@ -0,0 +1,501 @@
|
|||
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 Friend from '@/friend.js';
|
||||
import urlToBase64 from '@/utils/url2base64.js';
|
||||
import got from 'got';
|
||||
import loki from 'lokijs';
|
||||
|
||||
type AiChat = {
|
||||
question: string;
|
||||
prompt: string;
|
||||
api: string;
|
||||
key: string;
|
||||
history?: { role: string; content: string }[];
|
||||
};
|
||||
type Base64Image = {
|
||||
type: string;
|
||||
base64: string;
|
||||
};
|
||||
type AiChatHist = {
|
||||
postId: string;
|
||||
createdAt: number;
|
||||
type: string;
|
||||
api?: string;
|
||||
history?: {
|
||||
role: string;
|
||||
content: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const KIGO = '&';
|
||||
const TYPE_GEMINI = 'gemini';
|
||||
const TYPE_PLAMO = 'plamo';
|
||||
|
||||
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';
|
||||
|
||||
const RANDOMTALK_DEFAULT_PROBABILITY = 0.02;// デフォルトのrandomTalk確率
|
||||
const TIMEOUT_TIME = 1000 * 60 * 60 * 0.5;// aichatの返信を監視する時間
|
||||
const RANDOMTALK_DEFAULT_INTERVAL = 1000 * 60 * 60 * 12;// デフォルトのrandomTalk間隔
|
||||
|
||||
export default class extends Module {
|
||||
public readonly name = 'aichat';
|
||||
private aichatHist: loki.Collection<AiChatHist>;
|
||||
private randomTalkProbability: number = RANDOMTALK_DEFAULT_PROBABILITY;
|
||||
private randomTalkIntervalMinutes: number = RANDOMTALK_DEFAULT_INTERVAL;
|
||||
|
||||
@bindThis
|
||||
public install() {
|
||||
this.aichatHist = this.ai.getCollection('aichatHist', {
|
||||
indices: ['postId']
|
||||
});
|
||||
|
||||
// 確率は設定されていればそちらを採用(設定がなければデフォルトを採用)
|
||||
if (config.aichatRandomTalkProbability != undefined && !Number.isNaN(Number.parseFloat(config.aichatRandomTalkProbability))) {
|
||||
this.randomTalkProbability = Number.parseFloat(config.aichatRandomTalkProbability);
|
||||
}
|
||||
// ランダムトーク間隔(分)は設定されていればそちらを採用(設定がなければデフォルトを採用)
|
||||
if (config.aichatRandomTalkIntervalMinutes != undefined && !Number.isNaN(Number.parseInt(config.aichatRandomTalkIntervalMinutes))) {
|
||||
this.randomTalkIntervalMinutes = 1000 * 60 * Number.parseInt(config.aichatRandomTalkIntervalMinutes);
|
||||
}
|
||||
this.log('aichatRandomTalkEnabled:' + config.aichatRandomTalkEnabled);
|
||||
this.log('randomTalkProbability:' + this.randomTalkProbability);
|
||||
this.log('randomTalkIntervalMinutes:' + (this.randomTalkIntervalMinutes / (60 * 1000)));
|
||||
|
||||
// 定期的にデータを取得しaichatRandomTalkを行う
|
||||
if (config.aichatRandomTalkEnabled) {
|
||||
setInterval(this.aichatRandomTalk, this.randomTalkIntervalMinutes);
|
||||
}
|
||||
|
||||
return {
|
||||
mentionHook: this.mentionHook,
|
||||
contextHook: this.contextHook,
|
||||
timeoutCallback: this.timeoutCallback,
|
||||
};
|
||||
}
|
||||
|
||||
@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; })[];
|
||||
const systemInstruction : {role: string; parts: [{text: string}]} = {role: 'system', parts: [{text: aiChat.prompt}]};
|
||||
|
||||
if (!image) {
|
||||
// 画像がない場合、メッセージのみで問い合わせ
|
||||
parts = [{text: aiChat.question}];
|
||||
} else {
|
||||
// 画像が存在する場合、画像を添付して問い合わせ
|
||||
parts = [
|
||||
{ text: aiChat.question },
|
||||
{
|
||||
inline_data: {
|
||||
mime_type: image.type,
|
||||
data: image.base64,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 履歴を追加
|
||||
let contents: ({ role: string; parts: ({ text: string; inline_data?: undefined; } | { inline_data: { mime_type: string; data: string; }; text?: undefined; })[]}[]) = [];
|
||||
if (aiChat.history != null) {
|
||||
aiChat.history.forEach(entry => {
|
||||
contents.push({ role : entry.role, parts: [{text: entry.content}]});
|
||||
});
|
||||
}
|
||||
contents.push({role: 'user', parts: parts});
|
||||
|
||||
let options = {
|
||||
url: aiChat.api,
|
||||
searchParams: {
|
||||
key: aiChat.key,
|
||||
},
|
||||
json: {
|
||||
contents: contents,
|
||||
systemInstruction: systemInstruction,
|
||||
},
|
||||
};
|
||||
this.log(JSON.stringify(options));
|
||||
let res_data:any = null;
|
||||
try {
|
||||
res_data = await got.post(options,
|
||||
{parseJson: (res: string) => JSON.parse(res)}).json();
|
||||
{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')) {
|
||||
const responseText = res_data.candidates[0].content.parts[0].text;
|
||||
return responseText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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: string) => 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');
|
||||
}
|
||||
|
||||
// msg.idをもとにnotes/conversationを呼び出し、会話中のidかチェック
|
||||
const conversationData = await this.ai.api('notes/conversation', { noteId: msg.id });
|
||||
|
||||
// aichatHistに該当のポストが見つかった場合は会話中のためmentionHoonkでは対応しない
|
||||
let exist : AiChatHist | null = null;
|
||||
if (conversationData != undefined) {
|
||||
for (const message of conversationData) {
|
||||
exist = this.aichatHist.findOne({
|
||||
postId: message.id
|
||||
});
|
||||
// 見つかった場合はそれを利用
|
||||
if (exist != null) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// タイプを決定
|
||||
let type = TYPE_GEMINI;
|
||||
if (msg.includes([KIGO + TYPE_GEMINI])) {
|
||||
type = TYPE_GEMINI;
|
||||
} else if (msg.includes([KIGO + 'chatgpt4'])) {
|
||||
type = 'chatgpt4';
|
||||
} else if (msg.includes([KIGO + 'chatgpt'])) {
|
||||
type = 'chatgpt3.5';
|
||||
} else if (msg.includes([KIGO + TYPE_PLAMO])) {
|
||||
type = TYPE_PLAMO;
|
||||
}
|
||||
const current : AiChatHist = {
|
||||
postId: msg.id,
|
||||
createdAt: Date.now(),// 適当なもの
|
||||
type: type
|
||||
};
|
||||
// AIに問い合わせ
|
||||
const result = await this.handleAiChat(current, msg);
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
reaction: 'like'
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async contextHook(key: any, msg: Message) {
|
||||
this.log('contextHook...');
|
||||
if (msg.text == null) return false;
|
||||
|
||||
// msg.idをもとにnotes/conversationを呼び出し、該当のidかチェック
|
||||
const conversationData = await this.ai.api('notes/conversation', { noteId: msg.id });
|
||||
|
||||
// 結果がnullやサイズ0の場合は終了
|
||||
if (conversationData == null || conversationData.length == 0 ) {
|
||||
this.log('conversationData is nothing.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// aichatHistに該当のポストが見つからない場合は終了
|
||||
let exist : AiChatHist | null = null;
|
||||
for (const message of conversationData) {
|
||||
exist = this.aichatHist.findOne({
|
||||
postId: message.id
|
||||
});
|
||||
// 見つかった場合はそれを利用
|
||||
if (exist != null) break;
|
||||
}
|
||||
if (exist == null) {
|
||||
this.log('conversationData is not found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 見つかった場合はunsubscribe&removeし、回答。今回のでsubscribe,insert,timeout設定
|
||||
this.log('unsubscribeReply & remove.');
|
||||
this.log(exist.type + ':' + exist.postId);
|
||||
if (exist.history) {
|
||||
for (const his of exist.history) {
|
||||
this.log(his.role + ':' + his.content);
|
||||
}
|
||||
}
|
||||
this.unsubscribeReply(key);
|
||||
this.aichatHist.remove(exist);
|
||||
|
||||
// AIに問い合わせ
|
||||
const result = await this.handleAiChat(exist, msg);
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
reaction: 'like'
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async aichatRandomTalk() {
|
||||
this.log('AiChat(randomtalk) started');
|
||||
const tl = await this.ai.api('notes/local-timeline', {
|
||||
limit: 30
|
||||
});
|
||||
const interestedNotes = tl.filter(note =>
|
||||
note.userId !== this.ai.account.id &&
|
||||
note.text != null &&
|
||||
note.replyId == null &&
|
||||
note.renoteId == null &&
|
||||
note.cw == null &&
|
||||
!note.user.isBot
|
||||
);
|
||||
|
||||
// 対象が存在しない場合は処理終了
|
||||
if (interestedNotes == undefined || interestedNotes.length == 0) return false;
|
||||
|
||||
// ランダムに選択
|
||||
const choseNote = interestedNotes[Math.floor(Math.random() * interestedNotes.length)];
|
||||
|
||||
// msg.idをもとにnotes/conversationを呼び出し、会話中のidかチェック
|
||||
const conversationData = await this.ai.api('notes/conversation', { noteId: choseNote.id });
|
||||
|
||||
// aichatHistに該当のポストが見つかった場合は会話中のためaichatRandomTalkでは対応しない
|
||||
let exist : AiChatHist | null = null;
|
||||
if (conversationData != undefined) {
|
||||
for (const message of conversationData) {
|
||||
exist = this.aichatHist.findOne({
|
||||
postId: message.id
|
||||
});
|
||||
if (exist != null) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 確率をクリアし、親愛度が指定以上、かつ、Botでない場合のみ実行
|
||||
if (Math.random() < this.randomTalkProbability) {
|
||||
this.log('AiChat(randomtalk) targeted: ' + choseNote.id);
|
||||
} else {
|
||||
this.log('AiChat(randomtalk) is end.');
|
||||
return false;
|
||||
}
|
||||
const friend: Friend | null = this.ai.lookupFriend(choseNote.userId);
|
||||
if (friend == null || friend.love < 7) {
|
||||
this.log('AiChat(randomtalk) end.Because there was not enough affection.');
|
||||
return false;
|
||||
} else if (choseNote.user.isBot) {
|
||||
this.log('AiChat(randomtalk) end.Because message author is bot.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const current : AiChatHist = {
|
||||
postId: choseNote.id,
|
||||
createdAt: Date.now(),// 適当なもの
|
||||
type: TYPE_GEMINI
|
||||
};
|
||||
// AIに問い合わせ
|
||||
let targetedMessage = choseNote;
|
||||
if (choseNote.extractedText == undefined) {
|
||||
const data = await this.ai.api('notes/show', { noteId: choseNote.id });
|
||||
targetedMessage = new Message(this.ai, data);
|
||||
}
|
||||
const result = await this.handleAiChat(current, targetedMessage);
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
reaction: 'like'
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async handleAiChat(exist: AiChatHist, msg: Message) {
|
||||
let text: string, aiChat: AiChat;
|
||||
let prompt: string = '';
|
||||
if (config.prompt) {
|
||||
prompt = config.prompt;
|
||||
}
|
||||
const reName = RegExp(this.name, "i");
|
||||
const reKigoType = RegExp(KIGO + exist.type, "i");
|
||||
const extractedText = msg.extractedText;
|
||||
if (extractedText == undefined || extractedText.length == 0) return false;
|
||||
|
||||
const question = extractedText
|
||||
.replace(reName, '')
|
||||
.replace(reKigoType, '')
|
||||
.trim();
|
||||
switch (exist.type) {
|
||||
case TYPE_GEMINI:
|
||||
// geminiの場合、APIキーが必須
|
||||
if (!config.geminiProApiKey) {
|
||||
msg.reply(serifs.aichat.nothing(exist.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,
|
||||
history: exist.history
|
||||
};
|
||||
if (msg.includes([KIGO + 'gemini-flash']) || (exist.api && exist.api === GEMINI_15_FLASH_API)) {
|
||||
aiChat.api = GEMINI_15_FLASH_API;
|
||||
}
|
||||
text = await this.genTextByGemini(aiChat, base64Image);
|
||||
break;
|
||||
|
||||
case TYPE_PLAMO:
|
||||
// PLaMoの場合、APIキーが必須
|
||||
if (!config.pLaMoApiKey) {
|
||||
msg.reply(serifs.aichat.nothing(exist.type));
|
||||
return false;
|
||||
}
|
||||
aiChat = {
|
||||
question: msg.text,
|
||||
prompt: prompt,
|
||||
api: PLAMO_API,
|
||||
key: config.pLaMoApiKey,
|
||||
history: exist.history
|
||||
};
|
||||
text = await this.genTextByPLaMo(aiChat);
|
||||
break;
|
||||
|
||||
default:
|
||||
msg.reply(serifs.aichat.nothing(exist.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(exist.type));
|
||||
return false;
|
||||
}
|
||||
|
||||
this.log('Replying...');
|
||||
msg.reply(serifs.aichat.post(text, exist.type)).then(reply => {
|
||||
// 履歴に登録
|
||||
if (!exist.history) {
|
||||
exist.history = [];
|
||||
}
|
||||
exist.history.push({ role: 'user', content: question });
|
||||
exist.history.push({ role: 'model', content: text });
|
||||
// 履歴が10件を超えた場合、古いものを削除
|
||||
if (exist.history.length > 10) {
|
||||
exist.history.shift();
|
||||
}
|
||||
this.aichatHist.insertOne({
|
||||
postId: reply.id,
|
||||
createdAt: Date.now(),
|
||||
type: exist.type,
|
||||
api: aiChat.api,
|
||||
history: exist.history
|
||||
});
|
||||
|
||||
this.log('Subscribe&Set Timer...');
|
||||
|
||||
// メンションをsubscribe
|
||||
this.subscribeReply(reply.id, reply.id);
|
||||
|
||||
// タイマーセット
|
||||
this.setTimeoutWithPersistence(TIMEOUT_TIME, {
|
||||
id: reply.id
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async timeoutCallback({id}) {
|
||||
this.log('timeoutCallback...');
|
||||
const exist = this.aichatHist.findOne({
|
||||
postId: id
|
||||
});
|
||||
this.unsubscribeReply(id);
|
||||
if (exist != null) {
|
||||
this.aichatHist.remove(exist);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -388,6 +388,12 @@ export default {
|
|||
emojiOnce: emoji => `:${emoji}:(\`${emoji}\`)`
|
||||
},
|
||||
|
||||
aichat: {
|
||||
nothing: type => `あぅ... ${type}のAPIキーが登録されてないみたいです`,
|
||||
error: type => `うぇ...${type}でエラーが発生しちゃったみたいです。gemini-flashだと動くかも?`,
|
||||
post: (text, type) => `${text} (${type}) #aichat`,
|
||||
},
|
||||
|
||||
sleepReport: {
|
||||
report: hours => `んぅ、${hours}時間くらい寝ちゃってたみたいです`,
|
||||
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,13 @@ PONGを返します。生存確認にどうぞ
|
|||
### カスタム絵文字チェック
|
||||
1日に1回、カスタム絵文字の追加を監視してくれます。「カスタムえもじチェック」または「カスタムえもじを確認して」ですぐに確認してくれます。
|
||||
|
||||
### aichat
|
||||
```
|
||||
@ai aichat 部屋の片付けの手順を教えて
|
||||
```
|
||||
のようにメンションを飛ばすと、GoogleのGemini APIなどを使って返答してくれます(今のバージョンではGemini APIのみ対応)。利用するにはAPIキーの登録が必要です。藍ちゃんの返信に対し、返信するとさらに返信されます(指定時間以内のみ)。
|
||||
APIキーを登録の上、設定でaichatRandomTalkEnabledをtrueにすると、ランダムトーク(ランダムでaichatを発動)させることも可能です。ランダムトーク間隔、ランダムトーク確率を設定で指定可能です。
|
||||
|
||||
### その他反応するフレーズ (トークのみ)
|
||||
* かわいい
|
||||
* なでなで
|
||||
|
|
Loading…
Reference in a new issue