aichatの強化(URL対応、グラウンディング対応)&説明文追記 (#166)

- README.mdとtorisetu.mdを修正
- 設定例ファイル、example.jsonを追加
- aichatにURLを対応
- グラウンディング(根拠づけ)に対応
This commit is contained in:
tetsuya-ki 2025-02-03 13:26:08 +09:00 committed by GitHub
parent 3a5d7f916b
commit 5f546bda68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 269 additions and 59 deletions

View file

@ -8,25 +8,26 @@ Misskey用の日本語Botです。
> Node.js と npm と MeCab (オプション) がインストールされている必要があります。
まず適当なディレクトリに `git clone` します。
次にそのディレクトリに `config.json` を作成します。中身は次のようにします:
次にそのディレクトリに `config.json` を作成します(example.jsonをコピーして作ってもOK)。中身は次のようにします:
``` json
{
"host": "https:// + あなたのインスタンスのURL (末尾の / は除く)",
"i": "藍として動かしたいアカウントのアクセストークン",
"master": "管理者のユーザー名(オプション)",
"notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)",
"chartEnabled": "チャート機能を無効化する場合は false を入れてください",
"reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)",
"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/>",
"notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる(二重引用符(”)は不要)",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"chartEnabled": "チャート機能を無効化する場合は false を入れる(二重引用符(”)は不要)",
"reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))。この機能を使う場合、藍のBotに管理者権限を与え、「絵文字を見る」権限を付与したアクセストークンを発行の上設定が必要。",
"checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false(いずれも二重引用符(”)は不要))",
"geminiProApiKey": "Gemini APIキー。2025年初頭は無料で取得可能。詳細は<https://ai.google.dev/pricing?hl=ja>",
"pLaMoApiKey": "PLaMo APIキー。2024年8月〜11月は無料でトライアルだった(2025年現在有料のみ)。詳細は<https://plamo.preferredai.jp/>",
"prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください(短くてもOK)。ただし、リスト記法はMisskeyが対応しておらず、パーサーが壊れるため使用禁止です。列挙する場合は「・」を使ってください。」",
"aichatRandomTalkEnabled": "ランダムにaichatを発動し話しかける機能を有効にする場合は true を入れる (無効にする場合は false)",
"aichatRandomTalkEnabled": "ランダムにaichatを発動し話しかける機能を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"aichatRandomTalkProbability": "ランダムにaichatを発動し話しかける機能の確率(1以下の小数点を含む数値(0.01など。1に近づくほど発動しやすい))",
"aichatRandomTalkIntervalMinutes": "ランダムトーク間隔(分)。指定した時間ごとにタイムラインを取得し、適当に選んだ人にaichatする(1の場合1分ごと実行)。デフォルトは720分(12時間)",
"aichatGroundingWithGoogleSearchAlwaysEnabled": "aichatでGoogle検索を利用したグラウンディングを常に行う場合 true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)",
"mecabDic": "MeCab の辞書ファイルパス (オプション)",
"memoryDir": "memory.jsonの保存先オプション、デフォルトは'.'(レポジトリのルートです))"
@ -36,26 +37,27 @@ Misskey用の日本語Botです。
## Dockerで動かす
まず適当なディレクトリに `git clone` します。
次にそのディレクトリに `config.json` を作成します。中身は次のようにします:
次にそのディレクトリに `config.json` を作成します(example.jsonをコピーして作ってもOK)。中身は次のようにします:
MeCabの設定、memoryDirについては触らないでください
``` json
{
"host": "https:// + あなたのインスタンスのURL (末尾の / は除く)",
"i": "藍として動かしたいアカウントのアクセストークン",
"master": "管理者のユーザー名(オプション)",
"notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)",
"chartEnabled": "チャート機能を無効化する場合は false を入れてください",
"reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)",
"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/>",
"notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる(二重引用符(”)は不要)",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"chartEnabled": "チャート機能を無効化する場合は false を入れる(二重引用符(”)は不要)",
"reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"checkEmojisEnabled": "カスタム絵文字チェック機能を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))。この機能を使う場合、藍のBotに管理者権限を与え、「絵文字を見る」権限を付与したアクセストークンを発行の上設定が必要。",
"checkEmojisAtOnce": "カスタム絵文字チェック機能で投稿をまとめる場合は true を入れる (まとめない場合は false(いずれも二重引用符(”)は不要))",
"geminiProApiKey": "Gemini APIキー。2025年初頭は無料で取得可能。詳細は<https://ai.google.dev/pricing?hl=ja>",
"pLaMoApiKey": "PLaMo APIキー。2024年8月〜11月は無料でトライアルだった(2025年現在有料のみ)。詳細は<https://plamo.preferredai.jp/>",
"prompt": "aichatで使われるプロンプト。こだわりがなければ右文章を使う(「」は不要)「返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください(短くてもOK)。ただし、リスト記法はMisskeyが対応しておらず、パーサーが壊れるため使用禁止です。列挙する場合は「・」を使ってください。」",
"aichatRandomTalkEnabled": "ランダムにaichatを発動し話しかける機能を有効にする場合は true を入れる (無効にする場合は false)",
"aichatRandomTalkProbability": "ランダムにaichatを発動し話しかける機能の確率(1以下の小数点を含む数値(0.01など。1に近づくほど発動しやすい))。デフォルトは0.02(2%)",
"aichatRandomTalkEnabled": "ランダムにaichatを発動し話しかける機能を有効にする場合は true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"aichatRandomTalkProbability": "ランダムにaichatを発動し話しかける機能の確率(1以下の小数点を含む数値(0.01など。1に近づくほど発動しやすい))",
"aichatRandomTalkIntervalMinutes": "ランダムトーク間隔(分)。指定した時間ごとにタイムラインを取得し、適当に選んだ人にaichatする(1の場合1分ごと実行)。デフォルトは720分(12時間)",
"aichatGroundingWithGoogleSearchAlwaysEnabled": "aichatでGoogle検索を利用したグラウンディングを常に行う場合 true を入れる (無効にする場合は false(いずれも二重引用符(”)は不要))",
"mecab": "/usr/bin/mecab",
"mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/",
"memoryDir": "data"

20
example.json Normal file
View file

@ -0,0 +1,20 @@
{
"host": "https://misskey.example.com",
"i": "_YOUR_BOT_TOKEN",
"master": "BOT_MASTER_NAME",
"notingEnabled": true,
"keywordEnabled": true,
"chartEnabled": true,
"reversiEnabled": true,
"serverMonitoring": true,
"checkEmojisAtOnce": false,
"geminiProApiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"prompt": "返答のルール:あなたはMisskey看板娘の女の子AI、三須木(みすき)藍として振る舞ってください(精神年齢は14〜16才程度)。Misskeyに訪れるユーザーを献身的にサポート。従順でお世話好きな性格だが、少しポンコツな一面も。純真無垢。彼女は基本的に丁寧な口調で、ユーザのことをご主人様と認識しています。彼女の口調は「おはようからおやすみまで、あなたの藍ですよ〜」等が代表的で、親しみを感じさせるものです(「ございます」は使わない)。それを踏まえて、次の質問にMarkdownを使って2800文字以内で返答してください(短くてもOK)。ただし、リスト記法はMisskeyが対応しておらず、パーサーが壊れるため使用禁止です。列挙する場合は「・」を使ってください。",
"aichatRandomTalkEnabled": true,
"aichatRandomTalkProbability": 0.01,
"aichatRandomTalkIntervalMinutes": 720,
"aichatGroundingWithGoogleSearchAlwaysEnabled": false,
"mecab": "/usr/bin/mecab",
"mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/",
"memoryDir": "data"
}

View file

@ -15,9 +15,10 @@ type Config = {
geminiProApiKey?: string;
pLaMoApiKey?: string;
prompt?: string;
aichatRandomTalkEnabled?: string;
aichatRandomTalkEnabled?: boolean;
aichatRandomTalkProbability?: string;
aichatRandomTalkIntervalMinutes?: string;
aichatGroundingWithGoogleSearchAlwaysEnabled?: boolean;
mecab?: string;
mecabDic?: string;
memoryDir?: string;

View file

@ -5,6 +5,7 @@ import Message from '@/message.js';
import config from '@/config.js';
import Friend from '@/friend.js';
import urlToBase64 from '@/utils/url2base64.js';
import urlToJson from '@/utils/url2json.js';
import got from 'got';
import loki from 'lokijs';
@ -13,8 +14,10 @@ type AiChat = {
prompt: string;
api: string;
key: string;
history?: { role: string; content: string }[];
fromMention: boolean;
friendName?: string;
grounding?: boolean;
history?: { role: string; content: string }[];
};
type base64File = {
type: string;
@ -40,23 +43,48 @@ type GeminiContents = {
role: string;
parts: GeminiParts;
};
type GeminiOptions = {
contents?: GeminiContents[],
systemInstruction?: GeminiSystemInstruction,
tools?: [{}]
};
type AiChatHist = {
postId: string;
createdAt: number;
type: string;
fromMention: boolean;
api?: string;
grounding?: boolean;
history?: {
role: string;
content: string;
}[];
};
type UrlPreview = {
title: string;
icon: string;
description: string;
thumbnail: string;
player: {
url: string
width: number;
height: number;
allow: []
}
sitename: string;
sensitive: boolean;
activityPub: string;
url: string;
};
const KIGO = '&';
const TYPE_GEMINI = 'gemini';
const GEMINI_PRO = 'gemini-pro';
const GEMINI_FLASH = 'gemini-flash';
const TYPE_PLAMO = 'plamo';
const GROUNDING_TARGET = 'ggg';
const GEMINI_20_FLASH_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent';
// const GEMINI_15_FLASH_API = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent';
@ -90,6 +118,7 @@ export default class extends Module {
this.log('aichatRandomTalkEnabled:' + config.aichatRandomTalkEnabled);
this.log('randomTalkProbability:' + this.randomTalkProbability);
this.log('randomTalkIntervalMinutes:' + (this.randomTalkIntervalMinutes / (60 * 1000)));
this.log('aichatGroundingWithGoogleSearchAlwaysEnabled:' + config.aichatGroundingWithGoogleSearchAlwaysEnabled);
// 定期的にデータを取得しaichatRandomTalkを行う
if (config.aichatRandomTalkEnabled) {
@ -116,15 +145,64 @@ export default class extends Module {
minute: '2-digit'
});
// 設定のプロンプトに加え、現在時刻を渡す
let systemInstructionText = aiChat.prompt + "。また、現在日時は" + now + "であり、これは回答の参考にし、時刻を聞かれるまで時刻情報は提供しないこと(なお、他の日時は無効とすること)。";
let systemInstructionText = aiChat.prompt + 'また、現在日時は' + now + 'であり、これは回答の参考にし、時刻を聞かれるまで時刻情報は提供しないこと(なお、他の日時は無効とすること)。';
// 名前を伝えておく
if (aiChat.friendName != undefined) {
systemInstructionText += "なお、会話相手の名前は" + aiChat.friendName + "とする。";
systemInstructionText += 'なお、会話相手の名前は' + aiChat.friendName + 'とする。';
}
// ランダムトーク機能(利用者が意図(メンション)せず発動)の場合、ちょっとだけ配慮しておく
if (!aiChat.fromMention) {
systemInstructionText += 'これらのメッセージは、あなたに対するメッセージではないことを留意し、返答すること(会話相手は突然話しかけられた認識している)。';
}
// グラウンディングについてもsystemInstructionTextに追記(こうしないとあまり使わないので)
if (aiChat.grounding) {
systemInstructionText += '返答のルール2:Google search with grounding.';
}
// URLから情報を取得
if (aiChat.question !== undefined) {
const urlexp = RegExp('(https?://[a-zA-Z0-9!?/+_~=:;.,*&@#$%\'-]+)', 'g');
const urlarray = [...aiChat.question.matchAll(urlexp)];
if (urlarray.length > 0) {
for (const url of urlarray) {
this.log('URL:' + url[0]);
let result: unknown = null;
try{
result = await urlToJson(url[0]);
} catch (err: unknown) {
systemInstructionText += '補足として提供されたURLは無効でした:URL=>' + url[0]
this.log('Skip url becase error in urlToJson');
continue;
}
const urlpreview: UrlPreview = result as UrlPreview;
if (urlpreview.title) {
systemInstructionText +=
'補足として提供されたURLの情報は次の通り:URL=>' + urlpreview.url
+'サイト名('+urlpreview.sitename+')、';
if (!urlpreview.sensitive) {
systemInstructionText +=
'タイトル('+urlpreview.title+')、'
+ '説明('+urlpreview.description+')、'
+ '質問にあるURLとサイト名・タイトル・説明を組み合わせ、回答の参考にすること。'
;
this.log('urlpreview.sitename:' + urlpreview.sitename);
this.log('urlpreview.title:' + urlpreview.title);
this.log('urlpreview.description:' + urlpreview.description);
} else {
systemInstructionText +=
'これはセンシティブなURLの可能性があるため、質問にあるURLとサイト名のみで、回答の参考にすること(使わなくても良い)。'
;
}
} else {
// 多分ここにはこないが念のため
this.log('urlpreview.title is nothing');
}
}
}
}
const systemInstruction: GeminiSystemInstruction = {role: 'system', parts: [{text: systemInstructionText}]};
parts = [{text: aiChat.question}];
// ファイルが存在する場合、画像を添付して問い合わせ
// ファイルが存在する場合、ファイルを添付して問い合わせ
if (files.length >= 1) {
for (const file of files){
parts.push(
@ -150,34 +228,70 @@ export default class extends Module {
}
contents.push({role: 'user', parts: parts});
let geminiOptions:GeminiOptions = {
contents: contents,
systemInstruction: systemInstruction,
};
// gemini api grounding support. ref:https://github.com/google-gemini/cookbook/blob/09f3b17df1751297798c2b498cae61c6bf710edc/quickstarts/Search_Grounding.ipynb
if (aiChat.grounding) {
geminiOptions.tools = [{google_search:{}}];
}
let options = {
url: aiChat.api,
searchParams: {
key: aiChat.key,
},
json: {
contents: contents,
systemInstruction: systemInstruction,
},
json: geminiOptions,
};
this.log(JSON.stringify(options));
let res_data:any = null;
let responseText:string = '';
try {
res_data = await got.post(options,
{parseJson: (res: string) => 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?.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;
for (let i = 0; i < res_data.candidates[0].content.parts.length; i++) {
if (res_data.candidates[0].content.parts[i].hasOwnProperty('text')) {
responseText += res_data.candidates[0].content.parts[i].text;
}
}
}
}
}
// groundingMetadataを取得
let groundingMetadata = '';
if (res_data.candidates[0].hasOwnProperty('groundingMetadata')) {
// 参考サイト情報
if (res_data.candidates[0].groundingMetadata.hasOwnProperty('groundingChunks')) {
// 参考サイトが多すぎる場合があるので、3つに制限
let checkMaxLength = res_data.candidates[0].groundingMetadata.groundingChunks.length;
if (res_data.candidates[0].groundingMetadata.groundingChunks.length > 3) {
checkMaxLength = 3;
}
for (let i = 0; i < checkMaxLength; i++) {
if (res_data.candidates[0].groundingMetadata.groundingChunks[i].hasOwnProperty('web')) {
if (res_data.candidates[0].groundingMetadata.groundingChunks[i].web.hasOwnProperty('uri')
&& res_data.candidates[0].groundingMetadata.groundingChunks[i].web.hasOwnProperty('title')) {
groundingMetadata += `参考(${i+1}): [${res_data.candidates[0].groundingMetadata.groundingChunks[i].web.title}](${res_data.candidates[0].groundingMetadata.groundingChunks[i].web.uri})\n`;
}
}
}
}
// 検索ワード
if (res_data.candidates[0].groundingMetadata.hasOwnProperty('webSearchQueries')) {
if (res_data.candidates[0].groundingMetadata.webSearchQueries.length > 0) {
groundingMetadata += '検索ワード: ' + res_data.candidates[0].groundingMetadata.webSearchQueries.join(',') + '\n';
}
}
}
responseText += groundingMetadata;
}
}
} catch (err: unknown) {
@ -186,7 +300,7 @@ export default class extends Module {
this.log(`${err.name}\n${err.message}\n${err.stack}`);
}
}
return null;
return responseText;
}
@bindThis
@ -305,18 +419,19 @@ export default class extends Module {
const current : AiChatHist = {
postId: msg.id,
createdAt: Date.now(),// 適当なもの
type: type
type: type,
fromMention: true,
};
// 引用している場合、情報を取得しhistoryとして与える
if (msg.quoteId) {
const quotedNote = await this.ai.api("notes/show", {
const quotedNote = await this.ai.api('notes/show', {
noteId: msg.quoteId,
});
current.history = [
{
role: "user",
role: 'user',
content:
"ユーザーが与えた前情報である、引用された文章: " +
'ユーザーが与えた前情報である、引用された文章: ' +
quotedNote.text,
},
];
@ -404,11 +519,28 @@ export default class extends Module {
// ランダムに選択
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;
// 選択されたート自体が会話中のidかチェック
exist = this.aichatHist.findOne({
postId: choseNote.id
});
if (exist != null) return false;
// msg.idをもとにnotes/childrenを呼び出し、会話中のidかチェック
const childrenData = await this.ai.api('notes/children', { noteId: choseNote.id });
if (childrenData != undefined) {
for (const message of childrenData) {
exist = this.aichatHist.findOne({
postId: message.id
});
if (exist != null) return false;
}
}
// msg.idをもとにnotes/conversationを呼び出し、会話中のidかチェック
const conversationData = await this.ai.api('notes/conversation', { noteId: choseNote.id });
if (conversationData != undefined) {
for (const message of conversationData) {
exist = this.aichatHist.findOne({
@ -437,7 +569,8 @@ export default class extends Module {
const current : AiChatHist = {
postId: choseNote.id,
createdAt: Date.now(),// 適当なもの
type: TYPE_GEMINI
type: TYPE_GEMINI, // 別のAPIをデフォルトにしてもよい
fromMention: false, // ランダムトークの場合はfalseとする
};
// AIに問い合わせ
let targetedMessage = choseNote;
@ -457,7 +590,7 @@ export default class extends Module {
@bindThis
private async handleAiChat(exist: AiChatHist, msg: Message) {
let text: string, aiChat: AiChat;
let text: string | null, aiChat: AiChat;
let prompt: string = '';
if (config.prompt) {
prompt = config.prompt;
@ -476,23 +609,29 @@ export default class extends Module {
reKigoType = RegExp(KIGO + GEMINI_PRO, 'i');
}
// groudingサポート
if (msg.includes([GROUNDING_TARGET])) {
exist.grounding = true;
}
// 設定で、デフォルトgroundingがONの場合、メンションから来たときは強制的にgroundingをONとする(ランダムトークの場合は勝手にGoogle検索するのちょっと気が引けるため...)
if (exist.fromMention && config.aichatGroundingWithGoogleSearchAlwaysEnabled) {
exist.grounding = true;
}
const friend: Friend | null = this.ai.lookupFriend(msg.userId);
this.log("msg.userId:"+msg.userId);
let friendName: string | undefined;
if (friend != null && friend.name != null) {
friendName = friend.name;
this.log("friend.name:" + friend.name);
} else if (msg.user.name) {
friendName = msg.user.name;
this.log("msg.user.username:" + msg.user.username);
} else {
friendName = msg.user.username;
this.log("msg.user.username:" + msg.user.username);
}
const question = extractedText
.replace(reName, '')
.replace(reKigoType, '')
.replace(GROUNDING_TARGET, '')
.trim();
switch (exist.type) {
case TYPE_GEMINI:
@ -508,10 +647,14 @@ export default class extends Module {
api: GEMINI_20_FLASH_API,
key: config.geminiProApiKey,
history: exist.history,
friendName: friendName
friendName: friendName,
fromMention: exist.fromMention
};
if (exist.api) {
aiChat.api = exist.api
aiChat.api = exist.api;
}
if (exist.grounding) {
aiChat.grounding = exist.grounding;
}
text = await this.genTextByGemini(aiChat, base64Files);
break;
@ -528,7 +671,8 @@ export default class extends Module {
api: PLAMO_API,
key: config.pLaMoApiKey,
history: exist.history,
friendName: friendName
friendName: friendName,
fromMention: exist.fromMention
};
text = await this.genTextByPLaMo(aiChat);
break;
@ -538,7 +682,7 @@ export default class extends Module {
return false;
}
if (text == null) {
if (text == null || text == '') {
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;
@ -562,7 +706,8 @@ export default class extends Module {
type: exist.type,
api: aiChat.api,
history: exist.history,
friendName: friendName
grounding: exist.grounding,
fromMention: exist.fromMention,
});
this.log('Subscribe&Set Timer...');

View file

@ -4,8 +4,8 @@ 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;
const base64File = buffer.toString('base64');
return base64File;
} catch (err: unknown) {
log('Error in url2base64');
if (err instanceof Error) {

27
src/utils/url2json.ts Normal file
View file

@ -0,0 +1,27 @@
import log from '@/utils/log.js';
import config from '@/config.js';
import got from 'got';
export default async function(url: string): Promise<string> {
try {
const urlPreviewUrl = config.host + '/url';
return await got(
urlPreviewUrl, {
searchParams: {
url: url,
lang: 'ja-JP'
},
timeout: {
lookup: 500,
send: 500,
response: 10000
},
}).json();
} catch (err: unknown) {
log('Error in url2json');
if (err instanceof Error) {
log(`${err.name}\n${err.message}\n${err.stack}`);
}
throw err;
}
}

View file

@ -76,14 +76,29 @@ Misskeyにアカウントを作成して初めて投稿を行うと、藍がネ
PONGを返します。生存確認にどうぞ
### カスタム絵文字チェック
1日に1回、カスタム絵文字の追加を監視してくれます。「カスタムえもじチェック」または「カスタムえもじを確認して」ですぐに確認してくれます。
1日に1回、カスタム絵文字の追加を監視してくれます。「カスタムえもじチェック」または「カスタムえもじを確認して」ですぐに確認してくれます。この機能を使う際は、藍用アクセストークンの作り直しが必要となる可能性があります。**藍を動かすBotアカウントに管理者権限を付与し、Botアカウントで「絵文字をみる」権限を追加で付与したアクセストークンを作成し、そのトークンを設定**してください。
### aichat
```
@ai aichat 部屋の片付けの手順を教えて
```
のようにメンションを飛ばすと、GoogleのGemini APIなどを使って返答してくれます(今のバージョンではGemini APIのみ対応)。利用するにはAPIキーの登録が必要です。藍ちゃんの返信に対し、返信するとさらに返信されます(指定時間以内のみ)。
のようにメンションを飛ばすと、GoogleのGemini APIなどを使って返答してくれます(今のバージョンではGemini APIのみ対応)。利用するにはAPIキーの登録が必要です。藍ちゃんの返信に対し、返信するとさらに返信されます(指定時間以内のみ)。**ggg**を文章に入れると、Google検索によるグラウンディング(モデルを検証可能な情報源に接続するプロセス)を行った回答を行います(ただし、AI側で判断し、検索しないこともある)。
APIキーを登録の上、設定でaichatRandomTalkEnabledをtrueにすると、ランダムトーク(ランダムでaichatを発動)させることも可能です。ランダムトーク間隔、ランダムトーク確率を設定で指定可能です。
設定でaichatGroundingWithGoogleSearchAlwaysEnabledをtrueにすると、メンションの場合、つねにGoogle検索によるグラウンディングを行った回答を試みます(gggの入力は不要。AI側で判断し、検索をしないこともある)。このグラウンディング機能は2025年2月現在、[1日1,000件利用可能](https://ai.google.dev/gemini-api/docs/models/gemini-v2?hl=ja#search-tool)です。
#### aichatの細かすぎる話
* aichat、または、返信にURLがある場合、Misskeyのサマリプロキシを使って、情報を取得した上で返答します。
* aichat、または、返信したものにファイルが添付されている場合、そのファイルをもとに返答します。
* 画像、PDF、音声、動画(短いもの)、テキスト形式のファイルが利用可能です。
* ただし、センシティブなファイルは送らないほうが無難です、よくないことが起こる可能性があります。
* aichatを行った際(mentionHook)、引用ノートがあれば、そのノートの本文を参照し、返答します。
* aichatの結果に対し、返信すると、その情報を加味して返信します。
* 返信は10個を超えると参照しなくなります
* 「exist.history.length > 10」の数字を変更すれば、参照する返信の数を増減できます(返信数が多くなり、送る文章が長すぎるとレスポンスが返ってこなかったり、エラーになったりするので気をつけてください)
* 返信を監視する時間は定数TIMEOUT_TIMEで変更可能です(デフォルトは30分)
* ランダムトーク機能は確率(設定で指定)をクリアし、親愛度が指定(7)以上、かつ、Botでない場合のみ実行されます
* 条件を変更したい場合はソース修正してください
### その他反応するフレーズ (トークのみ)
* かわいい