「OpenAI GPT に相談」の版間の差分
編集の要約なし |
編集の要約なし |
||
| 3行目: | 3行目: | ||
OpenAIの生成AIである「GPT」を利用するためのMeryマクロです。 | OpenAIの生成AIである「GPT」を利用するためのMeryマクロです。 | ||
後述のソースコードではV8エンジンで動作するものと従来版スクリプトエンジンで動作するものの2種類を掲載していますが、従来版のものはアーカイブです。特段の理由がない限りはV8エンジン版をご利用ください。 | |||
【主な機能】 | |||
* OpenAI APIを利用して生成AIと対話できるMery用のマクロ | |||
* 選択したテキストやアクティブな行の内容について生成AIに質問・相談が可能 | |||
* 会話履歴を保持し、文脈を考慮した対話が可能 | |||
【特徴的な機能】 | |||
* | '''会話履歴機能''': | ||
* 事前の設定数に応じた会話履歴を保持 | |||
* 履歴のリセットが可能 | |||
これにより、会話の文脈を考慮した回答を得られます。 | |||
'''カスタムインストラクション''': | |||
* AIの応答方針を設定可能 | |||
* 一時的なカスタム指示の保存と変更が可能 | |||
'''プロンプトテンプレート''': | |||
* 誤字脱字チェック | |||
* 日本語翻訳 | |||
* コードの最適化、生成 | |||
など、定型的な質問を簡単に実行できます。 | |||
※テンプレート機能では回答を安定させるため、前述の会話履歴を文脈に含みません。 | |||
'''生成AIチャット風仕様''': | |||
* ストリーミングモードでリアルタイムに1文字ずつMeryに入力されていく | |||
* Shiftキー長押しで、テキスト生成を中断できる | * Shiftキー長押しで、テキスト生成を中断できる | ||
| 52行目: | 74行目: | ||
#title = "GPT に相談 (V8版)" | #title = "GPT に相談 (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 = 4; | const HISTORY_SIZE = 4; | ||
| 63行目: | 123行目: | ||
// 生成AI応答の入力毎の遅延時間 (ミリ秒) | // 生成AI応答の入力毎の遅延時間 (ミリ秒) | ||
// 入力をゆっくりにしたいときは数字を大きくしてください。 | |||
const INPUT_DELAY = 20; | const INPUT_DELAY = 20; | ||
// | // コンテキストメニュー番号の定義 | ||
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 | // タグ名の定義 | ||
const HISTORY_TAG = "gpt-history"; | |||
const TEMPORARY_CUSTOM_INSTRUCTION_TAG = "gpt-temporary-custom-instruction"; | |||
// console.log に出力する内容を Mery のアウトプットバーに表示する | |||
const console = { | |||
log: function (str) { | |||
outputBar.Writeln(str) | outputBar.Writeln(str) | ||
} | } | ||
} | } | ||
const doc = document; | const doc = document; | ||
| 94行目: | 161行目: | ||
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; | ||
} | } | ||
| 125行目: | 200行目: | ||
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; | |||
} | } | ||
| 142行目: | 223行目: | ||
sel.Text = "\n---\n\n" | sel.Text = "\n---\n\n" | ||
await Promise.all([ | await Promise.all([ | ||
callGPTAPI(apiKey, promptText), | callGPTAPI(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); | ||
| 159行目: | 244行目: | ||
menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0); | menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0); | ||
menu.Add("", 0, meMenuSeparator); | menu.Add("", 0, meMenuSeparator); | ||
let t = ""; | |||
if (selectionIsEmpty) { | if (selectionIsEmpty) { | ||
t = "アクティブ行の内容で相談 (&Q)"; | |||
} else { | } else { | ||
t = "選択範囲の内容で相談 (&Q)"; | |||
} | } | ||
menu.Add(t, EXECUTE_AI); | |||
const turn = JSON.parse(document.Tag[ | menu.Add((document.Tag.exists(HISTORY_TAG) ? "├" : "└") | ||
menu.Add("会話履歴をリセット (現在の会話数: " + turn + ") (&R)", RESET_HISTORY); | + " カスタムインストラクションを変更 (&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; | return menu; | ||
} | } | ||
| 173行目: | 283行目: | ||
// 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) { | ||
| 187行目: | 297行目: | ||
} | } | ||
async function callGPTAPI(apiKey, promptText) { | async function callGPTAPI(apiKey, promptText, isTemplate) { | ||
const controller = new AbortController(); | const controller = new AbortController(); | ||
const signal = controller.signal; | const signal = controller.signal; | ||
| 202行目: | 312行目: | ||
body: JSON.stringify({ | body: JSON.stringify({ | ||
model: 'gpt-4o-mini', | model: 'gpt-4o-mini', | ||
messages: createGPTRequest(promptText), | messages: createGPTRequest(promptText, isTemplate), | ||
stream: true, | stream: true, | ||
stream_options: { "include_usage": true }, | stream_options: { "include_usage": true }, | ||
temperature: 0. | // テンプレートの場合は応答を決定論的にする | ||
temperature: isTemplate ? 0.42 : 0.8, | |||
max_tokens: 4000, | max_tokens: 4000, | ||
}) | }) | ||
| 237行目: | 348行目: | ||
let inputText = ""; | let inputText = ""; | ||
for (const line of lines) { | for (const line of lines) { | ||
const json_text = line.replace(/^data:\s*/, ''); | // HACK 万が一空行が入ってきた場合に備えて trim() しておく | ||
const json_text = line.trim().replace(/^data:\s*/, ''); | |||
// データがない場合はスキップ | // データがない場合はスキップ | ||
if (!json_text) { | if (!json_text) { | ||
| 285行目: | 397行目: | ||
// 結果を会話履歴に追加 | // 結果を会話履歴に追加 | ||
if (HISTORY_SIZE > 0) { | if (HISTORY_SIZE > 0 && !isTemplate) { | ||
appendReturnTextToTag(promptText, responseMessage); | appendReturnTextToTag(promptText, responseMessage); | ||
} | } | ||
| 297行目: | 409行目: | ||
// document.Tag の内容を加味してGPTのリクエストを作成 | // document.Tag の内容を加味してGPTのリクエストを作成 | ||
function createGPTRequest(promptText) { | function createGPTRequest(promptText, isTemplate) { | ||
// テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない | |||
if (isTemplate) { | |||
return [ | |||
{ | |||
"role": "user", | |||
"content": [{ "type": "text", "text": promptText }] | |||
} | |||
]; | |||
} | |||
const tag = document.Tag; | const tag = document.Tag; | ||
// | const customInstructionText = buildCustomInstructionText(); | ||
if (!tag.exists( | |||
// 以前の会話が存在しない場合は新規リクエスト | |||
if (!tag.exists(HISTORY_TAG)) { | |||
const request = [{ | |||
"role": "user", | "role": "user", | ||
"content": promptText | "content": [{ "type": "text", "text": promptText }] | ||
}]; | }]; | ||
if (customInstructionText) { | |||
request.unshift({ | |||
"role": "system", | |||
"content": [{ "type": "text", "text": customInstructionText }] | |||
}) | |||
} | |||
return request; | |||
} | } | ||
const | // 会話履歴が存在する場合は、履歴を含めてリクエスト | ||
const request = JSON.parse(tag[HISTORY_TAG]); | |||
if (customInstructionText) { | |||
request.unshift({ | |||
"role": "system", | |||
"content": [{ "type": "text", "text": customInstructionText }] | |||
}); | |||
} | |||
request.push({ | |||
"role": "user", | "role": "user", | ||
"content": promptText | "content": promptText | ||
}); | }); | ||
return | return request; | ||
} | } | ||
| 318行目: | 456行目: | ||
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([ | ||
{ | { | ||
"role": "user", | "role": "user", | ||
| 330行目: | 468行目: | ||
]); | ]); | ||
} else { | } else { | ||
const gptText = JSON.parse(tag[ | const gptText = JSON.parse(tag[HISTORY_TAG]); | ||
gptText.push({ | gptText.push({ | ||
"role": "user", | "role": "user", | ||
| 343行目: | 481行目: | ||
gptText.shift(); | gptText.shift(); | ||
} | } | ||
tag[ | tag[HISTORY_TAG] = JSON.stringify(gptText); | ||
} | } | ||
} | } | ||
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"> | ||
スポンサーリンク