「Google Gemini に相談」の版間の差分
ナビゲーションに移動
検索に移動
編集の要約なし |
|||
| 3行目: | 3行目: | ||
Googleの生成AIである「Gemini」を利用するためのMeryマクロです。 | Googleの生成AIである「Gemini」を利用するためのMeryマクロです。 | ||
後述のソースコードではV8エンジンで動作するものと従来版スクリプトエンジンで動作するものの2種類を掲載していますが、従来版のものはアーカイブです。特段の理由がない限りはV8エンジン版をご利用ください。 | |||
【主な機能】 | |||
* Google Gemini APIを利用して生成AIと対話できるMery用のマクロ | |||
* 選択したテキストやアクティブな行の内容について生成AIに質問・相談が可能 | |||
* 会話履歴を保持し、文脈を考慮した対話が可能 | |||
【特徴的な機能】 | |||
* | '''会話履歴機能''': | ||
* 事前の設定数に応じた会話履歴を保持 | |||
* 履歴のリセットが可能 | |||
これにより、会話の文脈を考慮した回答を得られます。 | |||
'''カスタムインストラクション''': | |||
* AIの応答方針を設定可能 | |||
* 一時的なカスタム指示の保存と変更が可能 | |||
'''プロンプトテンプレート''': | |||
* 誤字脱字チェック | |||
* 日本語翻訳 | |||
* コードの最適化、生成 | |||
など、定型的な質問を簡単に実行できます。 | |||
※テンプレート機能では回答を安定させるため、前述の会話履歴を文脈に含みません。 | |||
テンプレートは自身の使い方に合わせて好きに追加、削除ができます。詳細はソースコード内の「PROMPT_TEMPLATE」定数のコメントを参照。 | |||
'''生成AIチャット風仕様''': | |||
* ストリーミングモードでリアルタイムに1文字ずつMeryに入力されていく | |||
* Shiftキー長押しで、テキスト生成を中断できる | * Shiftキー長押しで、テキスト生成を中断できる | ||
| 49行目: | 73行目: | ||
#title = "Gemini に相談 (V8版)" | #title = "Gemini に相談 (V8版)" | ||
BeginUndoGroup(); | BeginUndoGroup(); | ||
// カスタムインストラクション | |||
// これを設定することで、生成AIの回答全体への生成方針を指示できます。 | |||
// 例えば "回答をござる口調にしてください" と設定すると、AIの回答がござる口調になります。 | |||
// 何も指示しない場合は、空文字 "" を設定してください。 | |||
const DEFAULT_CUSTOM_INSTRUCTION = ""; | |||
// プロンプトテンプレート | |||
// プロンプトテンプレートを設定することで、AIに対して特定の指示を出すことができます。 | |||
// プロンプトテンプレートでの生成は会話履歴とカスタムインストラクションを反映しません。 | |||
// テンプレートは以下の { ~ }, で囲まれたブロックを好きに増減させて利用できます。 | |||
const PROMPT_TEMPLATE = [ | |||
{ | |||
"title": "誤字脱字、不正確な表現をチェック", | |||
"prompt": "以下の文章の誤字脱字 (被疑も含む) と不正確な表現を見つけて箇条書きで示してください。特に誤字脱字は漏れなく注視してください。【誤字脱字】【不正確な表現】セクションに分けて出力してください。" | |||
}, | |||
{ | |||
"title": "日本語に翻訳", | |||
"prompt": "以下の文章を日本語に翻訳してください。1段落毎に「[原文]: ...\n[日本語訳]: ...」の形式で出力すること。" | |||
}, | |||
{ | |||
"title": "コードを洗練させる", | |||
"prompt": "以下のコードを洗練させてください" | |||
}, | |||
{ | |||
"title": "PowerShell スクリプトを書く", | |||
"prompt": "以下の処理を行うPowerShellスクリプトを生成してください" | |||
}, | |||
{ | |||
"title": "Shell Script を書く", | |||
"prompt": "以下の処理を行うShell Scriptを生成してください" | |||
}, | |||
] | |||
// 生成AIの回答の言語設定 | |||
// "Japanese" や "English" を設定できます。他の言語は未検証。 | |||
// 自動設定にする場合は空文字 "" を設定してください。 | |||
const LANGUAGE = "Japanese"; | |||
// 会話の履歴を保持する数 (質問と回答で1セット) | // 会話の履歴を保持する数 (質問と回答で1セット) | ||
// 数を増やすと、会話の履歴が増えますが、Meryのメモリ使用量が増えたり、 | // 数を増やすと、会話の履歴が増えますが、Meryのメモリ使用量が増えたり、 | ||
// | // 使用トークン数が増えて動作が重くなる可能性があるため、適宜調整してください。 | ||
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。 | // document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。 | ||
const HISTORY_SIZE = 5; | const HISTORY_SIZE = 5; | ||
// 生成AI応答の入力毎の遅延時間 (ミリ秒) | // 生成AI応答の入力毎の遅延時間 (ミリ秒) | ||
// 入力をゆっくりにしたいときは数字を大きくしてください。 | |||
const INPUT_DELAY = 20; | const INPUT_DELAY = 20; | ||
| 62行目: | 125行目: | ||
const INPUT_LENGTH = 3; | const INPUT_LENGTH = 3; | ||
// | // コンテキストメニュー番号の定義 | ||
const EXECUTE_AI = 1; | const EXECUTE_AI = 1; | ||
const RESET_HISTORY = 2; | const RESET_HISTORY = 2; | ||
const SET_CUSTOM_INSTRUCTION = 3; | |||
const TEMPLATE_PROMPT_THRESHOLD = 10; | |||
// タグ名の定義 | |||
const HISTORY_TAG = "gemini-history"; | |||
const TEMPORARY_CUSTOM_INSTRUCTION_TAG = "gemini-temporary-custom-instruction"; | |||
const | // console.log に出力する内容を Mery のアウトプットバーに表示する | ||
const console = { | |||
log: function (str) { | |||
outputBar.Writeln(str) | outputBar.Writeln(str) | ||
} | } | ||
} | } | ||
const doc = document; | const doc = document; | ||
| 91行目: | 160行目: | ||
sel.EndOfLine(true, mePosLogical); | sel.EndOfLine(true, mePosLogical); | ||
} | } | ||
let promptText = sel.Text.trim(); | |||
const promptLength = sel.TextLength; | const promptLength = sel.TextLength; | ||
let menuTrack = 0 | |||
while (true) { | while (true) { | ||
// コンテキストメニューの作成 | // コンテキストメニューの作成 | ||
const menu = createContextMenu(selectionIsEmpty); | const menu = createContextMenu(selectionIsEmpty); | ||
menuTrack = menu.Track(0); | |||
// メニュー選択がキャンセルされた場合は終了 | // メニュー選択がキャンセルされた場合は終了 | ||
if (menuTrack === 0) return; | if (menuTrack === 0) return; | ||
// 会話履歴のリセット | // 会話履歴のリセット | ||
if (menuTrack === | if (menuTrack === RESET_HISTORY) { | ||
resetTag(); | resetTag(); | ||
continue | continue; | ||
} | } | ||
// カスタムインストラクションの設定 | |||
if (menuTrack === SET_CUSTOM_INSTRUCTION) { | |||
configureTemporaryCustomInstruction(); | |||
return; | |||
} | |||
break; | break; | ||
} | } | ||
| 122行目: | 199行目: | ||
alert("ファイルが読み取り専用のため実行できません。"); | alert("ファイルが読み取り専用のため実行できません。"); | ||
return; | return; | ||
} | |||
// テンプレートの場合はプロンプトを変更 | |||
const isTemplate = menuTrack >= TEMPLATE_PROMPT_THRESHOLD; | |||
if (isTemplate) { | |||
promptText = PROMPT_TEMPLATE[menuTrack - TEMPLATE_PROMPT_THRESHOLD].prompt + "\n\n---\n\n" + promptText; | |||
} | } | ||
| 139行目: | 222行目: | ||
sel.Text = "\n---\n\n" | sel.Text = "\n---\n\n" | ||
await Promise.all([ | await Promise.all([ | ||
callGeminiAPI(apiKey, promptText), | callGeminiAPI(apiKey, promptText, isTemplate), | ||
abortRequest() | abortRequest() | ||
]); | ]); | ||
sel.Text = "\n\n---\n\n\n" | if (sel.GetActivePointX(mePosLogical) > 2) { | ||
sel.Text = "\n\n---\n\n\n" | |||
} else { | |||
sel.Text = "\n---\n\n\n" | |||
} | |||
} catch (e) { | } catch (e) { | ||
console.log("エラーが発生しました: " + e.message + "\n" + e.stack); | console.log("エラーが発生しました: " + e.message + "\n" + e.stack); | ||
| 156行目: | 243行目: | ||
menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0); | menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0); | ||
menu.Add("", 0, meMenuSeparator); | menu.Add("", 0, meMenuSeparator); | ||
let t = ""; | |||
if (selectionIsEmpty) { | if (selectionIsEmpty) { | ||
t = "アクティブ行の内容で相談 (&Q)"; | |||
} else { | } else { | ||
menu.Add(" | t = "選択範囲の内容で相談 (&Q)"; | ||
} | |||
menu.Add(t, EXECUTE_AI); | |||
menu.Add((document.Tag.exists(HISTORY_TAG) ? "├" : "└") | |||
+ " カスタムインストラクションを変更 (&C)", SET_CUSTOM_INSTRUCTION); | |||
if (document.Tag.exists(HISTORY_TAG)) { | |||
const turn = JSON.parse(document.Tag[HISTORY_TAG]).length / 2; | |||
menu.Add("└ 会話履歴をリセット (現在の会話数: " + turn + ") (&R)", RESET_HISTORY); | |||
} | } | ||
if ( | |||
// テンプレートの追加 | |||
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})`; | |||
} | |||
subMenu.Add(PROMPT_TEMPLATE[i].title + accelerationKey, TEMPLATE_PROMPT_THRESHOLD + i); | |||
} | |||
if (selectionIsEmpty) { | |||
t = "プロンプト テンプレート (対象: アクティブ行) (&T)"; | |||
} else { | |||
t = "プロンプト テンプレート (対象: 選択範囲) (&T)"; | |||
} | |||
menu.AddPopup(t, subMenu); | |||
} | } | ||
return menu; | return menu; | ||
} | } | ||
| 170行目: | 282行目: | ||
// Shift キーが長押しされた場合はリクエスト中止フラグを立てる | // Shift キーが長押しされた場合はリクエスト中止フラグを立てる | ||
async function abortRequest() { | async function abortRequest() { | ||
const VIRTUAL_KEY_CODE_SHIFT = 0x10; | const VIRTUAL_KEY_CODE_SHIFT = 0x10; // Shift キーの仮想キーコード | ||
let pressCount = 0; | let pressCount = 0; | ||
while (!requestCompletedFlg) { | while (!requestCompletedFlg) { | ||
| 184行目: | 296行目: | ||
} | } | ||
async function callGeminiAPI(apiKey, promptText) { | async function callGeminiAPI(apiKey, promptText, isTemplate) { | ||
const controller = new AbortController(); | const controller = new AbortController(); | ||
const signal = controller.signal; | const signal = controller.signal; | ||
| 196行目: | 308行目: | ||
'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||
}, | }, | ||
body: JSON.stringify(createGeminiRequest(promptText)), | body: JSON.stringify({ | ||
generationConfig: { | |||
// テンプレートの場合は応答を決定論的にする | |||
temperature: isTemplate ? 0.55 : 1.0, | |||
}, | |||
contents: createGeminiRequest(promptText, isTemplate), | |||
}), | |||
}); | }); | ||
| 254行目: | 372行目: | ||
// 結果を会話履歴に追加 | // 結果を会話履歴に追加 | ||
if (HISTORY_SIZE > 0) { | if (HISTORY_SIZE > 0 && !isTemplate) { | ||
appendReturnTextToTag(promptText, responseMessage); | appendReturnTextToTag(promptText, responseMessage); | ||
} | } | ||
| 269行目: | 387行目: | ||
return response | return response | ||
.replace(/([。!?]) +/g, "$1") | .replace(/([。!?]) +/g, "$1") | ||
.replace(/([*:]) | .replace(/([*:.]) {2,}/g, "$1 "); | ||
} | } | ||
// document.Tag の内容を加味してGeminiのリクエストを作成 | // document.Tag の内容を加味してGeminiのリクエストを作成 | ||
function createGeminiRequest(promptText) { | function createGeminiRequest(promptText, isTemplate) { | ||
// テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない | |||
if (isTemplate) { | |||
return [createRoleAndParts("user", promptText)]; | |||
} | |||
const tag = document.Tag; | const tag = document.Tag; | ||
// | const customInstructionText = buildCustomInstructionText() | ||
if (!tag.exists( | |||
// 以前の会話が存在しない場合は新規リクエスト | |||
if (!tag.exists(HISTORY_TAG)) { | |||
const roleAndParts = [createRoleAndParts("user", promptText)]; | |||
if (customInstructionText) { | |||
// カスタムインストラクションがある場合は先頭に追加 | |||
roleAndParts.unshift(createRoleAndParts("user", customInstructionText)); | |||
} | |||
return roleAndParts | |||
} | } | ||
const | // 会話履歴が存在する場合は、履歴を含めてリクエスト | ||
const roleAndParts = JSON.parse(tag[HISTORY_TAG]); | |||
return | if (customInstructionText) { | ||
// カスタムインストラクションがある場合は先頭に追加 | |||
roleAndParts.unshift(createRoleAndParts("user", customInstructionText)); | |||
} | |||
roleAndParts.push(createRoleAndParts("user", promptText)); | |||
return roleAndParts; | |||
} | } | ||
| 299行目: | 434行目: | ||
function appendReturnTextToTag(userText, modelText) { | function appendReturnTextToTag(userText, modelText) { | ||
const tag = document.Tag; | const tag = document.Tag; | ||
if (!tag.exists( | if (!tag.exists(HISTORY_TAG)) { | ||
tag[ | tag[HISTORY_TAG] = JSON.stringify([ | ||
createRoleAndParts("user", userText), | createRoleAndParts("user", userText), | ||
createRoleAndParts("model", modelText) | createRoleAndParts("model", modelText) | ||
]); | ]); | ||
} else { | } else { | ||
const geminiText = JSON.parse(tag[ | const geminiText = JSON.parse(tag[HISTORY_TAG]); | ||
geminiText.push(createRoleAndParts("user", userText)); | geminiText.push(createRoleAndParts("user", userText)); | ||
geminiText.push(createRoleAndParts("model", modelText)); | geminiText.push(createRoleAndParts("model", modelText)); | ||
| 312行目: | 447行目: | ||
geminiText.shift(); | geminiText.shift(); | ||
} | } | ||
tag[ | tag[HISTORY_TAG] = JSON.stringify(geminiText); | ||
} | } | ||
} | } | ||
function resetTag() { | function resetTag() { | ||
const tag = document.Tag; | |||
document.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 === DEFAULT_CUSTOM_INSTRUCTION) { | |||
// デフォルトと同じ値の場合はタグを削除 | |||
setTemporaryCustomInstruction(""); | |||
} else if (prompt) { | |||
// デフォルト以外の値が設定された場合は保存 | |||
setTemporaryCustomInstruction(prompt); | |||
} else { | |||
// 空文字でOK、もしくはキャンセルされた場合はデフォルトに戻すか確認 | |||
if (Confirm("カスタムインストラクションをデフォルトに戻しますか?")) { | |||
setTemporaryCustomInstruction(""); | |||
} | |||
} | |||
// 変更が加わっている場合は会話履歴をリセット | |||
if (tempCustomInstruction !== getTemporaryCustomInstruction()) { | |||
resetTag(); | |||
} | |||
} | |||
function getTemporaryCustomInstruction() { | |||
const tag = document.Tag; | |||
if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) { | |||
return tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG]; | |||
} else { | |||
return ""; | |||
} | |||
} | |||
function setTemporaryCustomInstruction(temporaryCustomInstruction) { | |||
const tag = document.Tag; | |||
if (temporaryCustomInstruction) { | |||
tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG] = temporaryCustomInstruction; | |||
} else { | |||
tag.remove(TEMPORARY_CUSTOM_INSTRUCTION_TAG); | |||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=== | === 【アーカイブ】ソースコード (従来エンジン版) === | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
| 572行目: | 764行目: | ||
== 更新履歴 == | == 更新履歴 == | ||
* 2024-12-22 V8エンジン版を追加。それに伴い従来エンジン版はアーカイブ化 | |||
* 2024-12-18 会話履歴の保持方式をJSON文字列に変更。リクエスト確認のポップアップ表示をコンテキストメニュー表示に変更。同メニューに会話履歴リセット機能を追加 | * 2024-12-18 会話履歴の保持方式をJSON文字列に変更。リクエスト確認のポップアップ表示をコンテキストメニュー表示に変更。同メニューに会話履歴リセット機能を追加 | ||
* 2024-11-28 文書内で初回実行する際、「"」が含まれるとエラーになってしまう事象に対処 | * 2024-11-28 文書内で初回実行する際、「"」が含まれるとエラーになってしまう事象に対処 | ||
* 2024-11-27 サンプルマクロ [https://www.haijin-boys.com/wiki/Google_Gemini_%E3%81%A7%E5%9F%B7%E7%AD%86%E6%94%AF%E6%8F%B4 Google Gemini で執筆支援] の仕様に合わせ、環境変数「GOOGLE_API_KEY」からAPIキーを取得するように変更。これに合わせてMeryの動作バージョンを Ver 3.7.8 以上に変更 | * 2024-11-27 サンプルマクロ [https://www.haijin-boys.com/wiki/Google_Gemini_%E3%81%A7%E5%9F%B7%E7%AD%86%E6%94%AF%E6%8F%B4 Google Gemini で執筆支援] の仕様に合わせ、環境変数「GOOGLE_API_KEY」からAPIキーを取得するように変更。これに合わせてMeryの動作バージョンを Ver 3.7.8 以上に変更 | ||
* 2024-11-17 初版 | * 2024-11-17 初版 | ||
2024年12月22日 (日) 17:59時点における版
概要
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": "以下の文章を日本語に翻訳してください。1段落毎に「[原文]: ...\n[日本語訳]: ...」の形式で出力すること。"
},
{
"title": "コードを洗練させる",
"prompt": "以下のコードを洗練させてください"
},
{
"title": "PowerShell スクリプトを書く",
"prompt": "以下の処理を行うPowerShellスクリプトを生成してください"
},
{
"title": "Shell Script を書く",
"prompt": "以下の処理を行うShell Scriptを生成してください"
},
]
// 生成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 EXECUTE_AI = 1;
const RESET_HISTORY = 2;
const SET_CUSTOM_INSTRUCTION = 3;
const TEMPLATE_PROMPT_THRESHOLD = 10;
// タグ名の定義
const HISTORY_TAG = "gemini-history";
const TEMPORARY_CUSTOM_INSTRUCTION_TAG = "gemini-temporary-custom-instruction";
// 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 > 10000) {
alert("プロンプトの文字数が多すぎます。10000 文字以下にしてください。");
return;
}
// 読み取り専用の場合はエラー
if (doc.ReadOnly) {
alert("ファイルが読み取り専用のため実行できません。");
return;
}
// テンプレートの場合はプロンプトを変更
const isTemplate = menuTrack >= TEMPLATE_PROMPT_THRESHOLD;
if (isTemplate) {
promptText = PROMPT_TEMPLATE[menuTrack - TEMPLATE_PROMPT_THRESHOLD].prompt + "\n\n---\n\n" + promptText;
}
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 = "アクティブ行の内容で相談 (&Q)";
} else {
t = "選択範囲の内容で相談 (&Q)";
}
menu.Add(t, EXECUTE_AI);
menu.Add((document.Tag.exists(HISTORY_TAG) ? "├" : "└")
+ " カスタムインストラクションを変更 (&C)", SET_CUSTOM_INSTRUCTION);
if (document.Tag.exists(HISTORY_TAG)) {
const turn = JSON.parse(document.Tag[HISTORY_TAG]).length / 2;
menu.Add("└ 会話履歴をリセット (現在の会話数: " + turn + ") (&R)", RESET_HISTORY);
}
// テンプレートの追加
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})`;
}
subMenu.Add(PROMPT_TEMPLATE[i].title + accelerationKey, TEMPLATE_PROMPT_THRESHOLD + i);
}
if (selectionIsEmpty) {
t = "プロンプト テンプレート (対象: アクティブ行) (&T)";
} else {
t = "プロンプト テンプレート (対象: 選択範囲) (&T)";
}
menu.AddPopup(t, subMenu);
}
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 = document.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 = document.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 = document.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 === DEFAULT_CUSTOM_INSTRUCTION) {
// デフォルトと同じ値の場合はタグを削除
setTemporaryCustomInstruction("");
} else if (prompt) {
// デフォルト以外の値が設定された場合は保存
setTemporaryCustomInstruction(prompt);
} else {
// 空文字でOK、もしくはキャンセルされた場合はデフォルトに戻すか確認
if (Confirm("カスタムインストラクションをデフォルトに戻しますか?")) {
setTemporaryCustomInstruction("");
}
}
// 変更が加わっている場合は会話履歴をリセット
if (tempCustomInstruction !== getTemporaryCustomInstruction()) {
resetTag();
}
}
function getTemporaryCustomInstruction() {
const tag = document.Tag;
if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) {
return tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG];
} else {
return "";
}
}
function setTemporaryCustomInstruction(temporaryCustomInstruction) {
const tag = document.Tag;
if (temporaryCustomInstruction) {
tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG] = temporaryCustomInstruction;
} else {
tag.remove(TEMPORARY_CUSTOM_INSTRUCTION_TAG);
}
}
【アーカイブ】ソースコード (従来エンジン版)
#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 初版
スポンサーリンク