mirror of
https://github.com/syuilo/ai.git
synced 2025-02-26 09:33:57 +00:00
aichatの強化(URL対応、グラウンディング対応)&説明文追記 (#166)
- README.mdとtorisetu.mdを修正 - 設定例ファイル、example.jsonを追加 - aichatにURLを対応 - グラウンディング(根拠づけ)に対応
This commit is contained in:
parent
3a5d7f916b
commit
5f546bda68
7 changed files with 269 additions and 59 deletions
48
README.md
48
README.md
|
@ -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
20
example.json
Normal 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"
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,35 +228,71 @@ 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) {
|
||||
this.log('Error By Call Gemini');
|
||||
|
@ -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...');
|
||||
|
|
|
@ -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
27
src/utils/url2json.ts
Normal 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;
|
||||
}
|
||||
}
|
19
torisetu.md
19
torisetu.md
|
@ -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でない場合のみ実行されます
|
||||
* 条件を変更したい場合はソース修正してください
|
||||
|
||||
### その他反応するフレーズ (トークのみ)
|
||||
* かわいい
|
||||
|
|
Loading…
Reference in a new issue