Google Gemini に相談
概要
Googleの生成AIである「Gemini」を利用するためのMeryマクロです。
後述のソースコードではV8エンジンで動作するものと従来版スクリプトエンジンで動作するものの2種類を掲載していますが、従来版のものはアーカイブです。特段の理由がない限りはV8エンジン版をご利用ください。
【主な機能】
- Google Gemini APIを利用して生成AIと対話できるMery用のマクロ
- 選択したテキストやアクティブな行の内容について生成AIに質問・相談が可能
- 会話履歴を保持し、文脈を考慮した対話が可能
【特徴的な機能】
会話履歴機能:
- 事前の設定数に応じた会話履歴を保持
- 履歴のリセットが可能
これにより、会話の文脈を考慮した回答を得られます。
カスタムインストラクション:
- AIの応答方針を設定可能
- 一時的なカスタム指示の保存と変更が可能
プロンプトテンプレート:
- 誤字脱字チェック
- 日本語翻訳
- コードの最適化、生成
など、定型的な質問を簡単に実行できます。 ※テンプレート機能では回答を安定させるため、前述の会話履歴を文脈に含みません。
テンプレートは自身の使い方に合わせて好きに追加、削除ができます。詳細はソースコード内の「PROMPT_TEMPLATE」定数のコメントを参照。
生成AIチャット風仕様:
- ストリーミングモードでリアルタイムに1文字ずつMeryに入力されていく
- Shiftキー長押しで、テキスト生成を中断できる
注意事項 (必ず読んでからご利用ください)
- 本マクロでは 2024/11/17 時点で無料利用ができるGemini APIを利用します。今後、Googleの改定により有償になったり、設定誤り等で費用が発生しても、作者は一切の責任を負いません。
- Gemini APIではその特性上、プロンプトとして送信した文字列がAIの学習に使われるものと思われます。くれぐれも機密情報やプライバシーに関わる情報を指定しないように注意してください。
- Gemini APIは、 Gemini API利用規約 に則ってご利用ください。
利用前提
- Mery Ver 3.7.8 以上
- Google AI Studioで、Gemini APIキーを取得していること (参考 API キーを取得する | Gemini API | Google AI for Developers)
利用方法
- 環境変数の設定
- コマンド プロンプトで以下のコマンドを実行し、環境変数を設定してください。
setx GOOGLE_API_KEY "***************************************"
- 設定後、Mery または Windows の再起動が必要な場合があります。
- マクロの実行
- プロンプトにしたい文字列を選択してマクロを実行すると、選択したテキストがプロンプトとしてGemini APIに送信され、応答がエディタに表示されます。
- 会話の履歴数を調整
- 会話の履歴は設定した個数分が保持され、文脈として利用されます。適宜、ソースコード内の「HISTORY_SIZE」の数を調整してください。
- ※数を増やすほど会話内容を保持して文脈を理解するようになりますが、メモリ使用量が増えたり動作が重くなる可能性があります。
- ※開いている文書ごとに履歴を保持します。文脈をリセットしたいときは、新しい文書を開いて会話をやり直してください。
ソースコード (V8エンジン版)
#language = "v8"
#title = "Gemini に相談 (V8版)"
BeginUndoGroup();
// カスタムインストラクション
// これを設定することで、生成AIの回答全体への生成方針を指示できます。
// 例えば "回答をござる口調にしてください" と設定すると、AIの回答がござる口調になります。
// 何も指示しない場合は、空文字 "" を設定してください。
const DEFAULT_CUSTOM_INSTRUCTION = "";
// プロンプトテンプレート
// プロンプトテンプレートを設定することで、AIに対して特定の指示を出すことができます。
// プロンプトテンプレートでの生成は会話履歴とカスタムインストラクションを反映しません。
// テンプレートは以下の { ~ }, で囲まれたブロックを好きに増減させて利用できます。
const PROMPT_TEMPLATE = [
{
"title": "要約する",
"prompt": "要約して。専門的な用語と思われる単語は簡単に解説も入れて。"
},
{
"title": "誤字脱字、不正確な表現をチェック",
"prompt": "以下の2つのカテゴリーで問題点を指摘してください\n"
+ "- 【誤字脱字】:明確な誤字脱字および誤字の疑いがある箇所\n"
+ "- 【不正確・曖昧な表現】:解釈に個人差が生じる可能性がある表現、より適切な表現に改善できる箇所"
},
{
"submenu_title": "翻訳",
"submenu": [
{
"title": "日本語に翻訳",
"prompt": "以下の文章を日本語に翻訳してください。原文の長さに応じて出力方式を変えてください\n"
+ "- 短文の場合: 日本語訳のみ出力すること\n"
+ "- 長文の場合: 1段落毎に「[原文]: ...\\n[日本語訳]: ...」の形式で出力すること"
},
{
"title": "英語に翻訳",
"prompt": "以下の文章を英語に翻訳してください。"
},
{
"title": "英語->日本語 スラッシュリーディング翻訳",
"prompt": "以下の英文を日本語にスラッシュリーディング形式で翻訳してください。\n\n"
+ "条件:\n"
+ "- 英文の各フレーズをスラッシュ(/)で区切って表示\n"
+ "- 各フレーズの日本語訳を括弧()内に併記\n"
+ "- 文の終わりはダブルスラッシュ(//)で表示\n"
+ "- 英語の語順に従って日本語訳を行う\n"
+ "- 日本語訳は自然な日本語である必要はなく、英語の語順を反映した直訳調で構わない\n"
+ "- 主語が省略されている場合は補って翻訳すること\n"
+ "- 一般的な英文で頻出する表現がある場合は次の行に記載\n"
+ "- 以下の出力例のように <英文> (日本語訳) / ... とスラッシュ区切りの繰り返しになる \n\n"
+ "出力例:\n"
+ "Snowstorms are forecast (暴風雪が予報されている) / to hit the Sea of Japan coasts (日本海沿岸を襲うと) / of northern to eastern Japan (北部から東部の) / from Sunday through Monday. (日曜から月曜にかけて。) //\n"
+ "【頻出表現】\n"
+ "- be forecast to: ~すると予報されている\n"
},
]
},
{
"submenu_title": "プログラミング支援",
"submenu": [
{
"title": "コードを洗練させる",
"prompt": "以下のコードを洗練させてください"
},
{
"title": "コードをレビューする",
"prompt": "以下のコードをセキュリティと保守性の観点でレビューしてください"
},
{
"title": "エラーログをデバッグする",
"prompt": "以下のエラーログを解析し、考えられる原因と対策を提示してください"
},
{
"title": "SQLクエリを最適化する",
"prompt": "以下のSQL文を最適化してください"
},
{
"title": "PowerShell スクリプトを書く",
"prompt": "以下の処理を行うPowerShellスクリプトを生成してください"
},
{
"title": "Shell Script を書く",
"prompt": "以下の処理を行うShell Scriptを生成してください"
},
{
"title": "Linux コマンドを提案",
"prompt": "あなたはLinuxエキスパートとして、以下のLinuxコマンドを提案してください。\n\n"
+ "制約条件:\n"
+ "- 多数のディストリビューションで採用されているメジャーなコマンドを使用し、ポータブルな実装にすること\n"
+ "- bashでの実行を前提にすること\n"
+ "- 可能な限りシンプルな実装にすること\n\n"
+ "期待する出力:\n"
+ "- コマンドの具体的な実装方法\n"
+ "- 各コマンドの動作説明\n"
+ "- 制限事項\n"
},
]
},
{
"submenu_title": "ビジネスドキュメント作成支援",
"submenu": [
{
"title": "提案書を作成する",
"prompt": "以下に対する対する提案書を、コスト・実現性・リスクを含めて作成してください"
},
{
"title": "仕様書のアウトラインを作成する",
"prompt": "以下の内容で仕様書のアウトラインを作成してください"
},
{
"title": "手順書のアウトラインを作成する",
"prompt": "以下の作業を行う手順書を、前提条件と注意事項を含めてアウトラインを作成してください"
},
{
"title": "ユーザーマニュアルのアウトラインを作成する",
"prompt": "以下の機能内容から、ユーザー向けマニュアルのアウトラインを作成してください"
},
{
"title": "議事録を作成する",
"prompt": "以下の会議内容から議事録を作成してください"
},
{
"title": "進捗報告書を作成する",
"prompt": "以下のタスク状況から進捗報告書を作成してください"
},
]
},
{
"submenu_title": "分析支援",
"submenu": [
{
"title": "問題を分析し、課題を抽出する",
"prompt": "以下の状況から問題や潜在的リスクを分析し、課題を抽出してください"
},
{
"title": "対応策を提案する",
"prompt": "以下の問題・課題に対する対応策を複数提案してください"
},
{
"title": "傾向を見つける",
"prompt": "以下の内容から、数値や挙動上の傾向を見つけてください"
},
]
},
{
"submenu_title": "アイデア出し",
"submenu": [
{
"title": "SCAMPER法でブレインストーミング",
"prompt": "以下のテーマについて、SCAMPER法を使って15個のアイデアを出してください。制約条件は以下の通りです:"
},
{
"title": "解決策の幅出し",
"prompt": "以下の課題に対して、技術面、運用面、コスト面から実現可能な解決策を5つずつ提案してください"
},
{
"title": "業務改善案の提案",
"prompt": "以下の既存プロセスについて、効率化、コスト削減、品質向上の観点から改善案を提案してください"
},
{
"title": "逆転の発想",
"prompt": "以下の目標に対して、通常とは逆の視点からアプローチする方法を3つ提案してください"
},
{
"title": "競合分析からの着想",
"prompt": "以下の内容について、業界における競合他社の特徴を分析し、差別化できる新しいアプローチを提案してください"
},
{
"title": "ユースケースの列挙",
"prompt": "以下の製品・サービスについて、想定されるユースケースを10個列挙し、それぞれの価値提案を記載してください"
},
]
},
]
// 生成AIの回答の言語設定
// "Japanese" や "English" を設定できます。他の言語は未検証。
// 自動設定にする場合は空文字 "" を設定してください。
const LANGUAGE = "Japanese";
// 会話の履歴を保持する数 (質問と回答で1セット)
// 数を増やすと、会話の履歴が増えますが、Meryのメモリ使用量が増えたり、
// 使用トークン数が増えて動作が重くなる可能性があるため、適宜調整してください。
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。
const HISTORY_SIZE = 5;
// 生成AI応答の入力毎の遅延時間 (ミリ秒)
// 入力をゆっくりにしたいときは数字を大きくしてください。
const INPUT_DELAY = 20;
// 1回の入力での文字数
const INPUT_LENGTH = 3;
// 入力プロンプトの最大文字数
const MAX_PROMPT_LENGTH = 15000;
// コンテキストメニュー番号の定義
const EXECUTE_AI = 1;
const RESET_HISTORY = 2;
const SET_CUSTOM_INSTRUCTION = 3;
const REEXECUTE_LAST_TEMPLATE_PROMPT = 4;
const TEMPLATE_PROMPT_THRESHOLD = 10;
const TEMPLATE_PROMPT_SUBMENU_THRESHOLD = 1000;
// タグ名の定義
const HISTORY_TAG = "gemini-history";
const TEMPORARY_CUSTOM_INSTRUCTION_TAG = "gemini-temporary-custom-instruction";
const PREVIOUS_TEMPLATE_PROMPT_TAG = "gemini-previous-template-prompt";
// console.log に出力する内容を Mery のアウトプットバーに表示する
const console = {
log: function (str) {
outputBar.Writeln(str)
}
}
const doc = document;
const sel = document.selection;
// 生成リクエスト中止フラグ
let abortRequestFlg = false
// 生成リクエスト完了フラグ;
let requestCompletedFlg = false;
main();
async function main() {
const selectionIsEmpty = sel.IsEmpty
// プロンプトの取得
if (selectionIsEmpty) {
sel.StartOfLine(false, mePosLogical);
sel.EndOfLine(true, mePosLogical);
}
let promptText = sel.Text.trim();
const promptLength = sel.TextLength;
let menuTrack = 0
while (true) {
// コンテキストメニューの作成
const menu = createContextMenu(selectionIsEmpty);
menuTrack = menu.Track(0);
// メニュー選択がキャンセルされた場合は終了
if (menuTrack === 0) return;
// 会話履歴のリセット
if (menuTrack === RESET_HISTORY) {
resetTag();
continue;
}
// カスタムインストラクションの設定
if (menuTrack === SET_CUSTOM_INSTRUCTION) {
configureTemporaryCustomInstruction();
return;
}
break;
}
// プロンプトが空の場合はエラー
if (promptLength === 0) {
alert("プロンプトを入力してください。");
return;
}
// 文字数が多すぎる場合はエラー
if (promptLength > MAX_PROMPT_LENGTH) {
alert(`プロンプトの文字数が多すぎます。${MAX_PROMPT_LENGTH} 文字以内で入力してください。`);
return;
}
// 読み取り専用の場合はエラー
if (doc.ReadOnly) {
alert("ファイルが読み取り専用のため実行できません。");
return;
}
// 直前に実行したプロンプトテンプレートの再実行の場合、menuTrack を書き換え
if (menuTrack === REEXECUTE_LAST_TEMPLATE_PROMPT) {
if (doc.Tag.exists(PREVIOUS_TEMPLATE_PROMPT_TAG)) {
menuTrack = Number(doc.Tag[PREVIOUS_TEMPLATE_PROMPT_TAG]);
} else {
alert("直前に実行したプロンプトテンプレートがありません。");
return;
}
}
// テンプレートの場合はプロンプトを変更
const isTemplate = menuTrack >= TEMPLATE_PROMPT_THRESHOLD;
const isSubmenu = menuTrack >= TEMPLATE_PROMPT_SUBMENU_THRESHOLD;
if (isSubmenu) {
const parentMenuTrack = Number(String(menuTrack).slice(0, 2));
const subMenuTrack = Number(String(menuTrack).slice(2));
promptText = PROMPT_TEMPLATE[parentMenuTrack - TEMPLATE_PROMPT_THRESHOLD].submenu[subMenuTrack].prompt
+ "\n\n---\n\n"
+ promptText;
} else if (isTemplate) {
promptText = PROMPT_TEMPLATE[menuTrack - TEMPLATE_PROMPT_THRESHOLD].prompt
+ "\n\n---\n\n"
+ promptText;
}
// テンプレートの場合は直前に実行したプロンプトテンプレートの番号としてタグに保存
if (isTemplate) {
addPreviousTemplatePromptNumberToTag(menuTrack);
}
try {
// 処理終了を待機
shell.KeepRunning = true;
// カーソル位置移動
sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical));
sel.EndOfLine(false, mePosLogical);
sel.NewLine();
// APIキーの取得
const apiKey = shell.getEnv("GOOGLE_API_KEY");
// Gemini API に問い合わせ
sel.Text = "\n---\n\n"
await Promise.all([
callGeminiAPI(apiKey, promptText, isTemplate),
abortRequest()
]);
if (sel.GetActivePointX(mePosLogical) > 2) {
sel.Text = "\n\n---\n\n\n"
} else {
sel.Text = "\n---\n\n\n"
}
} catch (e) {
console.log("エラーが発生しました: " + e.message + "\n" + e.stack);
sel.Text = "<処理中にエラー発生>"
} finally {
// 処理終了待機を解除
shell.KeepRunning = false;
}
}
function createContextMenu(selectionIsEmpty) {
const menu = CreatePopupMenu();
menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0);
menu.Add("", 0, meMenuSeparator);
let t = "";
if (selectionIsEmpty) {
t = "アクティブ行の内容で相談 (&A)";
} else {
t = "選択範囲の内容で相談 (&A)";
}
menu.Add(t, EXECUTE_AI);
if (doc.Tag.exists(HISTORY_TAG)) {
const turn = JSON.parse(doc.Tag[HISTORY_TAG]).length / 2;
menu.Add("├ 会話履歴をクリア (現在の会話数: " + turn + ") (&C)", RESET_HISTORY);
} else {
menu.Add("├ 会話履歴をクリア (現在の会話数: 0) (&C)", RESET_HISTORY, meMenuGrayed);
}
menu.Add("└ カスタムインストラクションを変更 (&I)", SET_CUSTOM_INSTRUCTION);
// テンプレート数チェック
// 2桁まで許容
const ITEMS_MUX = 99;
if (PROMPT_TEMPLATE.length > ITEMS_MUX - TEMPLATE_PROMPT_THRESHOLD) {
throw new Error("プロンプトテンプレートの数が多すぎます。");
}
// テンプレートの追加
if (PROMPT_TEMPLATE.length > 0) {
const subMenu = CreatePopupMenu();
for (let i = 0; i < PROMPT_TEMPLATE.length; i++) {
let accelerationKey = "";
if (i === 9) {
accelerationKey = " (&0)";
} else if (i < 10) {
accelerationKey = ` (&${i + 1})`;
}
const menuNum = TEMPLATE_PROMPT_THRESHOLD + i;
if (PROMPT_TEMPLATE[i].submenu) {
const subMenuList = PROMPT_TEMPLATE[i].submenu;
// テンプレート数チェック
if (subMenuList.length > ITEMS_MUX) {
throw new Error("プロンプトテンプレート (サブメニュー) の数が多すぎます。");
}
const subSubMenu = CreatePopupMenu();
const subMenuPrefix = String(menuNum);
for (let j = 0; j < subMenuList.length; j++) {
let subAccelerationKey = "";
if (j === 9) {
subAccelerationKey = " (&0)";
} else if (j < 10) {
subAccelerationKey = ` (&${j + 1})`;
}
const subMenuNum = Number(subMenuPrefix + String(j).padStart(2, '0'));
subSubMenu.Add(subMenuList[j].title + subAccelerationKey, subMenuNum);
}
subMenu.AddPopup(PROMPT_TEMPLATE[i].submenu_title + accelerationKey, subSubMenu);
} else {
subMenu.Add(PROMPT_TEMPLATE[i].title + accelerationKey, menuNum);
}
}
if (selectionIsEmpty) {
t = "プロンプト テンプレート (対象: アクティブ行) (&T)";
} else {
t = "プロンプト テンプレート (対象: 選択範囲) (&T)";
}
menu.AddPopup(t, subMenu);
t = "└ 直前に実行したプロンプト テンプレートを再実行 (&R)";
if (doc.Tag.exists(PREVIOUS_TEMPLATE_PROMPT_TAG)) {
menu.Add(t, REEXECUTE_LAST_TEMPLATE_PROMPT);
} else {
menu.Add(t, REEXECUTE_LAST_TEMPLATE_PROMPT, meMenuGrayed);
}
}
return menu;
}
// Shift キーが長押しされた場合はリクエスト中止フラグを立てる
async function abortRequest() {
const VIRTUAL_KEY_CODE_SHIFT = 0x10; // Shift キーの仮想キーコード
let pressCount = 0;
while (!requestCompletedFlg) {
await new Promise(resolve => setTimeout(resolve, 300));
if (shell.GetKeyState(VIRTUAL_KEY_CODE_SHIFT) < 0) {
pressCount++;
if (pressCount > 2) {
abortRequestFlg = true;
return;
}
}
}
}
async function callGeminiAPI(apiKey, promptText, isTemplate) {
const controller = new AbortController();
const signal = controller.signal;
try {
// fetch で Gemini API にリクエスト
// stream: true, stream_options: { "include_usage": true } で利用トークン数を取得
const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:streamGenerateContent?alt=sse&key=' + apiKey, {
signal: signal,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
generationConfig: {
// テンプレートの場合は応答を決定論的にする
temperature: isTemplate ? 0.55 : 1.0,
},
contents: createGeminiRequest(promptText, isTemplate),
}),
});
let responseMessage = "";
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
// 受信したバイナリデータ (value) をテキストに変換する
const text = decoder.decode(value);
// テキストには複数のメッセージが含まれている可能性があるため、
// 改行 (LF) で分離して、1 行ずつ処理する
const lines = text.trim().split(/\n+/);
for (const line of lines) {
// HACK 空行が渡ってくることがあるので trim() を入れる
const json_text = line.trim().replace(/^data:\s*/, '');
// データがない場合はスキップ
if (!json_text) {
continue;
}
const data = JSON.parse(json_text);
const candidate = data.candidates[0];
// まとまった文字数で入力する
const inputText = formatGeminiResponse(candidate.content.parts[0].text);
if (inputText) {
// HACK 絵文字などの文字幅が2以上の文字を考慮して配列変換を行う
const inputArray = Array.from(inputText);
for (let i = 0; i < inputArray.length; i += INPUT_LENGTH) {
// リクエスト中止フラグが立っている場合は中断
if (abortRequestFlg) {
controller.abort();
sel.Text = "<処理を中止>";
return;
}
// 入力
sel.Text = inputArray.slice(i, i + INPUT_LENGTH).join(''); // 配列をスライスして文字列に戻す
await new Promise(resolve => setTimeout(resolve, INPUT_DELAY));
}
responseMessage += inputText;
}
// 応答が終了した場合はループを抜ける
if (candidate.finishReason === "STOP") {
break;
}
}
}
// 結果を会話履歴に追加
if (HISTORY_SIZE > 0 && !isTemplate) {
appendReturnTextToTag(promptText, responseMessage);
}
} catch (e) {
controller.abort();
throw e;
} finally {
requestCompletedFlg = true;
}
}
// Geminiの応答には半角スペースが連続して含まれることがあるため、整形する
function formatGeminiResponse(response) {
return response
.replace(/([。!?]) +/g, "$1")
.replace(/([*:.]) {2,}/g, "$1 ");
}
// document.Tag の内容を加味してGeminiのリクエストを作成
function createGeminiRequest(promptText, isTemplate) {
// テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない
if (isTemplate) {
return [createRoleAndParts("user", promptText)];
}
const tag = doc.Tag;
const customInstructionText = buildCustomInstructionText()
// 以前の会話が存在しない場合は新規リクエスト
if (!tag.exists(HISTORY_TAG)) {
const roleAndParts = [createRoleAndParts("user", promptText)];
if (customInstructionText) {
// カスタムインストラクションがある場合は先頭に追加
roleAndParts.unshift(createRoleAndParts("user", customInstructionText));
}
return roleAndParts
}
// 会話履歴が存在する場合は、履歴を含めてリクエスト
const roleAndParts = JSON.parse(tag[HISTORY_TAG]);
if (customInstructionText) {
// カスタムインストラクションがある場合は先頭に追加
roleAndParts.unshift(createRoleAndParts("user", customInstructionText));
}
roleAndParts.push(createRoleAndParts("user", promptText));
return roleAndParts;
}
function createRoleAndParts(role, text) {
return {
"role": role,
"parts": [
{
"text": text
}
]
};
}
// document.Tag にGeminiの応答を追加
function appendReturnTextToTag(userText, modelText) {
const tag = doc.Tag;
if (!tag.exists(HISTORY_TAG)) {
tag[HISTORY_TAG] = JSON.stringify([
createRoleAndParts("user", userText),
createRoleAndParts("model", modelText)
]);
} else {
const geminiText = JSON.parse(tag[HISTORY_TAG]);
geminiText.push(createRoleAndParts("user", userText));
geminiText.push(createRoleAndParts("model", modelText));
// 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除
while (geminiText.length > HISTORY_SIZE * 2) {
geminiText.shift();
}
tag[HISTORY_TAG] = JSON.stringify(geminiText);
}
}
function resetTag() {
const tag = doc.Tag;
if (tag.exists(HISTORY_TAG)) {
tag.remove(HISTORY_TAG);
}
}
function buildCustomInstructionText() {
let customInstruction = getTemporaryCustomInstruction();
if (!customInstruction) {
// カスタムインストラクションがない場合はデフォルトを使用
customInstruction = DEFAULT_CUSTOM_INSTRUCTION.trim();
}
const language = LANGUAGE.trim();
if (!customInstruction && !language) {
return "";
}
return "--- CUSTOM INSTRUCTION START ---\n"
+ "Treat this section as metadata for this conversation. Do not respond directly to the instructions here.\n"
+ (customInstruction ? `${customInstruction}\n` : "")
+ (language ? `Please make sure to answer in ${language}.\n` : "")
+ "--- CUSTOM INSTRUCTION END ---";
}
function configureTemporaryCustomInstruction() {
let tempCustomInstruction = getTemporaryCustomInstruction();
const prompt = Prompt("カスタムインストラクションを変更 ※変更すると会話履歴がリセットされます", tempCustomInstruction ? tempCustomInstruction : DEFAULT_CUSTOM_INSTRUCTION);
if (prompt) {
if (prompt === DEFAULT_CUSTOM_INSTRUCTION) {
// デフォルトと同じ値の場合はタグを削除
setTemporaryCustomInstruction("");
} else if (prompt !== tempCustomInstruction) {
// デフォルト以外の値が設定された場合は保存
setTemporaryCustomInstruction(prompt);
}
} else {
// 空文字でOK、もしくはキャンセルされた場合はデフォルトに戻すか確認
// デフォルトカスタムインストラクションが空の場合は無条件でタグ削除
if (!DEFAULT_CUSTOM_INSTRUCTION || Confirm("カスタムインストラクションをデフォルトに戻しますか?")) {
setTemporaryCustomInstruction("");
}
}
// 変更が加わっている場合は会話履歴をリセット
if (tempCustomInstruction !== getTemporaryCustomInstruction()) {
resetTag();
}
}
function getTemporaryCustomInstruction() {
const tag = doc.Tag;
if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) {
return tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG];
} else {
return "";
}
}
function setTemporaryCustomInstruction(temporaryCustomInstruction) {
const tag = doc.Tag;
if (temporaryCustomInstruction) {
tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG] = temporaryCustomInstruction;
} else {
tag.remove(TEMPORARY_CUSTOM_INSTRUCTION_TAG);
}
}
function addPreviousTemplatePromptNumberToTag(menuTrack) {
const tag = doc.Tag;
tag[PREVIOUS_TEMPLATE_PROMPT_TAG] = menuTrack;
}
【アーカイブ】ソースコード (従来エンジン版)
#title = "Gemini に相談"
BeginUndoGroup();
// 会話の履歴を保持する数 (質問と回答で1セット)
// 数を増やすと、会話の履歴が増えますが、Meryのメモリ使用量が増えたり、
// 動作が重くなる可能性があるため、適宜調整してください。
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。
var HISTORY_SIZE = 5;
// コンテキストメニュー番号
var EXECUTE_AI = 1;
var RESET_HISTORY = 2;
var ConsoleClass = function () {
this.log = function (str) {
outputBar.Writeln(str)
}
}
var console = new ConsoleClass();
var doc = document;
var sel = document.selection;
main();
function main() {
var selectionIsEmpty = sel.IsEmpty
var beforeScrollY = ScrollY;
// プロンプトの取得
var promptText = "";
var promptLength = 0;
if (selectionIsEmpty) {
sel.StartOfLine(false, mePosLogical);
sel.EndOfLine(true, mePosLogical);
}
promptText = sel.Text.trim();
promptLength = sel.TextLength;
// コンテキストメニューの作成
var menu = createContextMenu(selectionIsEmpty);
var menuTrack = menu.Track(0);
// メニュー選択がキャンセルされた場合は終了
if (menuTrack === 0) return;
// 会話履歴のリセット
if (menuTrack === 2) {
resetTag();
return;
}
// プロンプトが空の場合はエラー
if (promptLength === 0) {
alert("プロンプトを入力してください。");
return;
}
// 文字数が多すぎる場合はエラー
if (promptLength > 10000) {
alert("プロンプトの文字数が多すぎます。10000 文字以下にしてください。");
return;
}
// 読み取り専用の場合はエラー
if (doc.ReadOnly) {
alert("ファイルが読み取り専用のため実行できません。");
return;
}
// カーソル位置移動
sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical));
sel.EndOfLine(false, mePosLogical);
sel.NewLine();
var activeY = sel.GetActivePointY(mePosLogical);
// Gemini API に問い合わせ
doc.ReadOnly = true;
var ret = callGeminiAPI(promptText);
doc.ReadOnly = false;
// 結果を表示
sel.Text = "\n---\n\n" + ret + "\n\n---\n";
// 位置調整
sel.SetActivePoint(
mePosLogical,
1,
activeY,
false
)
if (beforeScrollY + 2 < ScrollY) {
ScrollY = ScrollY - 1;
}
}
function createContextMenu(selectionIsEmpty) {
var menu = CreatePopupMenu();
menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0);
menu.Add("", 0, meMenuSeparator);
if (selectionIsEmpty) {
menu.Add("アクティブ行の内容で相談 (&Q)", EXECUTE_AI);
} else {
menu.Add("選択範囲の内容で相談 (&Q)", EXECUTE_AI);
}
if (document.Tag.exists("gemini-text")) {
var turn = JSON.parse(document.Tag("gemini-text")).length / 2;
menu.Add("会話履歴をリセット (現在の会話数: " + turn + ") (&R)", RESET_HISTORY);
}
return menu;
}
function callGeminiAPI(promptText) {
try {
// APIキーの取得
var gemini_api_key = shell.getEnv("GOOGLE_API_KEY");
var http = new ActiveXObject("Msxml2.ServerXMLHTTP");
var url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=' + gemini_api_key;
http.open('POST', url, true);
http.setRequestHeader('Content-Type', 'application/json');
http.send('{"contents":' + createGeminiRequest(promptText) + '}');
// 応答待機
var timeout = 60;
var retrySec = 0;
var startX = sel.GetActivePointX(mePosLogical);
var prevPos = sel.GetActivePos();
while (http.readyState != 4) {
if (retrySec > timeout) {
break;
}
http.waitForResponse(1);
retrySec += 1;
inputProgressChar(prevPos);
prevPos = sel.GetActivePos();
}
// 進捗表示文字を削除
sel.SetActivePoint(
mePosLogical,
startX,
sel.GetActivePointY(mePosLogical),
true
);
sel.Delete();
var ret = "";
// 応答ステータスチェック
if (http.readyState == 4 && http.status == 200) {
// JSONをパースして必要なテキストを抽出
var response = JSON.parse(http.responseText);
if (response.candidates && response.candidates.length > 0) {
response.candidates.forEach(function (candidate) {
if (candidate.content && candidate.content.parts) {
candidate.content.parts.forEach(function (part) {
if (part.text) {
ret += part.text;
}
});
}
});
}
} else {
var ret = "エラーが発生しました: " + http.status + " " + http.statusText
http.abort();
return ret;
}
// 結果を会話履歴に追加
if (HISTORY_SIZE > 0) {
appendReturnTextToTag(promptText, ret);
}
// 整形しつつ返す
return ret
.trim()
.replace(/([。!?]) +/g, "$1")
.replace(/\* +/g, "* ");
} catch (e) {
console.log(e.stack);
return "エラーが発生しました: " + e.message;
}
}
// document.Tag の内容を加味してGeminiのリクエストを作成
function createGeminiRequest(promptText) {
var tag = document.Tag;
// 以前の会話が存在しない場合
if (!tag.exists("gemini-text")) {
return JSON.stringify([{ "parts": [{ "text": promptText }] }]);
}
var geminiText = JSON.parse(tag("gemini-text"));
geminiText.push({
"role": "user",
"parts": [{ "text": promptText }]
});
return JSON.stringify(geminiText);
}
// document.Tag にGeminiの応答を追加
function appendReturnTextToTag(userText, modelText) {
var tag = document.Tag;
if (!tag.exists("gemini-text")) {
tag("gemini-text") = JSON.stringify([
{
"role": "user",
"parts": [{ "text": userText }]
},
{
"role": "model",
"parts": [{ "text": modelText }]
}
]);
} else {
var geminiText = JSON.parse(tag("gemini-text"));
geminiText.push({
"role": "user",
"parts": [{ "text": userText }]
});
geminiText.push({
"role": "model",
"parts": [{ "text": modelText }]
});
// 会話セット (user, model 1往復) が5セットを超えた場合、最初のセットを削除
while (geminiText.length > HISTORY_SIZE * 2) {
geminiText.shift();
}
tag("gemini-text") = JSON.stringify(geminiText);
}
}
function resetTag() {
if (document.Tag.exists("gemini-text")) {
document.Tag.remove("gemini-text");
}
}
function inputProgressChar(pos) {
doc.ReadOnly = false;
sel.SetActivePos(pos);
sel.Text = ".";
doc.ReadOnly = true;
}
更新履歴
- 2024-12-22 V8エンジン版を追加。それに伴い従来エンジン版はアーカイブ化
- 2024-12-18 会話履歴の保持方式をJSON文字列に変更。リクエスト確認のポップアップ表示をコンテキストメニュー表示に変更。同メニューに会話履歴リセット機能を追加
- 2024-11-28 文書内で初回実行する際、「"」が含まれるとエラーになってしまう事象に対処
- 2024-11-27 サンプルマクロ Google Gemini で執筆支援 の仕様に合わせ、環境変数「GOOGLE_API_KEY」からAPIキーを取得するように変更。これに合わせてMeryの動作バージョンを Ver 3.7.8 以上に変更
- 2024-11-17 初版
スポンサーリンク