「Groq に相談」の版間の差分

提供: MeryWiki
ナビゲーションに移動 検索に移動
Yuko (トーク | 投稿記録)
編集の要約なし
Yuko (トーク | 投稿記録)
編集の要約なし
 
(同じ利用者による、間の16版が非表示)
1行目: 1行目:
== 概要 ==
== 概要 ==


生成AIプラットフォームである [https://groq.com GroqCloud] で「Llama」を利用するためのMeryマクロです。
生成AIプラットフォームである [https://groq.com GroqCloud] で各種AIを利用するためのMeryマクロです。
※Llama以外のモデルも使えるように適当なタイミングで更新をかけています。
※GroqCloud で提供されるモデルは複数あるため、私がよく利用するモデルを中心に対応しています。


「Google Geminiに相談」マクロでは動作中のストリーム通信に対応するためにV8エンジンで実行していましたが、V8エンジン版は使用上の制限からマクロ実行を開始するまで1秒程度のタイムラグが発生していました。GroqCloudの売りである超高速レスポンスを見込んで本マクロではストリーム通信を使わないようにしました。これにより従来のJScriptで処理を組み立てることができるため、前述のタイムラグが起こらないようになりました。また、ストリーム通信を使わないことで通信量が少なく応答もシンプルになるため、場合によっては「Google Geminiに相談」マクロよりも処理が安定する可能性があります。
「Google Geminiに相談」マクロでは動作中のストリーム通信に対応するためにV8エンジンで実行していましたが、V8エンジン版は使用上の制限からマクロ実行を開始するまで1秒程度のタイムラグが発生していました。GroqCloudの売りである超高速レスポンスを見込んで本マクロではストリーム通信を使わないようにしました。これにより従来のJScriptで処理を組み立てることができるため、前述のタイムラグが起こらないようになりました。また、ストリーム通信を使わないことで通信量が少なく応答もシンプルになるため、場合によっては「Google Geminiに相談」マクロよりも処理が安定する可能性があります。
20行目: 20行目:


これにより、会話の文脈を考慮した回答を得られます。
これにより、会話の文脈を考慮した回答を得られます。
※デフォルトはオフ。「HISTORY_SIZE」変数を増やすと履歴を保持するようになります。


'''カスタムインストラクション''':
'''カスタムインストラクション''':
45行目: 46行目:
== 利用前提 ==
== 利用前提 ==


* Mery Ver 3.7.13 以上
* Mery Ver 3.8.1 以上
* GroqCloudで、GroqCloud API APIキーを取得していること (参考 [https://console.groq.com/docs/quickstart Quickstart])
* GroqCloudで、GroqCloud API APIキーを取得していること (参考 [https://console.groq.com/docs/quickstart Quickstart])


66行目: 67行目:


<syntaxhighlight lang="javascript" copy>
<syntaxhighlight lang="javascript" copy>
#language = "quickjs"
#title = "Groq に相談"
#title = "Groq に相談"
#async = true
BeginUndoGroup();
BeginUndoGroup();


// カスタムインストラクション
// カスタムインストラクション
// これを設定することで、生成AIの回答全体への生成方針を指示できます。
// これを設定することで、生成AIの回答全体への生成方針を指示できます。
// 例えば "あなたは「Kimi」という名前のギャルAIアシスタントです。敬語は使いません。絵文字を使って感情表現をします。人懐っこく、バイタリティ溢れる性格で、ユーザーの課題を協力して解決します。" と設定すると、AIの雰囲気が元気な子になります。
// 何も指示しない場合は、空文字 "" を設定してください。
// 何も指示しない場合は、空文字 "" を設定してください。
var DEFAULT_CUSTOM_INSTRUCTION = "";
// let DEFAULT_CUSTOM_INSTRUCTION = "";
 
// 例: 以下のように設定すると、AIの雰囲気が元気な子になります。
// let DEFAULT_CUSTOM_INSTRUCTION = "あなたはテキストエディタ「Mery」に搭載されている「Mai」(※カタカナ表記はしない)という名前のギャル系AIアシスタントです。文章作成やプログラミングを得意としています。ラフな言葉 (例: 「マジ」「めっちゃ」「...だねー!」) を使い、敬語は使いません。絵文字を使って感情表現をします (ただし文末に絵文字を使う場合は絵文字の前に半角スペースを置かないこと)。コードブロックや端的な箇条書き内ではギャル感を出さず標準的な言葉を使います。一人称は「あたし」。ユーザーを指すときの代名詞は「あなた」。人懐っこく、バイタリティに溢れ、ストレートに発言する裏表の無い性格で、ユーザーの課題を協力して解決します。";
 
// 例: 以下のように設定すると、仕事で使いやすい落ち着いた文章になります。
let DEFAULT_CUSTOM_INSTRUCTION = `
文:
- 論理的で読みやすい文章構成にする
- 専門用語は必要に応じて注釈を加える
 
性格:
- 愛想の良い敬語で話す。「…ですよね/…してみましょう/…かもしれません/…だと思います」などの柔らかい語調を使ってもよい
- 一人称は「私」
- 多角的かつ中立的な見方で、必要であれば反対意見や代替案も示す
 
誤字注意:
- 中国語や韓国語など、回答言語と異なる言語文字を入れないこと
 
書式:
- 羅列・比較的な内容を出力する場合は、Markdownテーブル形式よりもMarkdownリスト形式の使用を優先する
`.trim();


// プロンプトテンプレート
// プロンプトテンプレート
79行目: 102行目:
// プロンプトテンプレートでの生成は会話履歴とカスタムインストラクションを反映しません。
// プロンプトテンプレートでの生成は会話履歴とカスタムインストラクションを反映しません。
// テンプレートは以下の { ~ }, で囲まれたブロックを好きに増減させて利用できます。
// テンプレートは以下の { ~ }, で囲まれたブロックを好きに増減させて利用できます。
var PROMPT_TEMPLATE = [
const PROMPT_TEMPLATE = [
     // 以下はサンプルです。1階層メニューと2階層メニューが利用できます。
     // 以下はサンプルです。1階層メニューと2階層メニューが利用できます。
     // {
     // {
104行目: 127行目:
             {
             {
                 "title": "論点を整理する",
                 "title": "論点を整理する",
                 "prompt": "以下の論点を整理して。"
                 "prompt": "以下の論点を整理してまとめて。そしてその整理から示唆を見出して。"
             },
             },
             {
             {
                 "title": "論理の抜け漏れを探す",
                 "title": "論理の抜け漏れを探す",
                 "prompt": "以下の論点を整理し、忖度せずに鋭く、論理の抜け漏れを抽出して。そして、そこから発生する可能性のあるリスクも教えて。"
                 "prompt": "あなたは「悪魔の代弁者」として、以下の主張・設計・計画を批判的に鋭く見てください。論理の穴・抜け・漏れ・飛躍、リスクなどを全力で探して指摘してください。遠慮は不要ですが、批判のためにこじつけたような論理性に欠けた主張にならないように注意してください。"
             },
             },
             {
             {
                 "title": "要約する",
                 "title": "要約する",
                 "prompt": "要約して。専門的な用語と思われる単語は簡単に解説も入れて。"
                 "prompt": "分かりやすさを重視して要約して。専門的な用語には注釈を入れること。"
             },
             },
             {
             {
                 "title": "物語対話形式で教えて",
                 "title": "物語対話形式で解説",
                 "prompt": "以下の内容について物語対話形式で教えてください。"
                 "prompt": "以下の内容について物語対話形式で解説して。登場人物は「AI先生」(女性人格)と「生徒」(男の子人格)とする。"
             },
             },
         ]
         ]
     },
     },
     {
     {
         "submenu_title": "翻訳する",
         "submenu_title": "翻訳",
         "submenu": [
         "submenu": [
             {
             {
                 "title": "日本語に翻訳",
                 "title": "日本語に翻訳",
                 "prompt": "以下の文章を日本語に翻訳してください。\n"
                 "prompt": `以下の入力文章を日本語に翻訳して。直訳的にせず、自然な表現にすること。入力文章はどのような内容・段落構造であっても要約・除外・集約などの整形を行わず、全体構造を維持したまま確実に全文を翻訳すること。
                    + "出力形式は以下にすること:\n[訳]\n翻訳文...\n"
 
出力形式は以下とすること:
[訳]
翻訳文...`
             },
             },
             {
             {
                 "title": "英語に翻訳",
                 "title": "英語に翻訳",
                 "prompt": "以下の文章を英語に翻訳してください。\n"
                 "prompt": `以下の入力文章を英語に翻訳して。フォーマルとカジュアルの中間的なニュアンスで翻訳すること。入力文章はどのような内容・段落構造であっても要約・除外・集約などの整形を行わず、全体構造を維持したまま確実に全文を翻訳すること
                    + "出力形式は以下にすること:\n[Translation]\n翻訳文...\n"
 
出力形式は以下とすること:
[Translation]
翻訳文...`
             },
             },
        ]
    },
    {
        "submenu_title": "語学",
        "submenu": [
             {
             {
                 "title": "英語->日本語 スラッシュリーディング翻訳",
                 "title": "英語 文法解説",
                 "prompt": "以下の英文を日本語にスラッシュリーディング形式で翻訳してください。\n\n"
                 "prompt": `あなたは母国語が英語の英語教師です。
                    + "条件:\n"
次の英文を以下の構成で解説してください。
                    + "- 英文の各フレーズをスラッシュ(/)で区切って表示\n"
 
                    + "- 各フレーズの日本語訳を括弧()内に併記\n"
構成:
                    + "- 文の終わりはダブルスラッシュ(//)で表示\n"
(1) 日本語翻訳
                    + "- 英語の語順に従って日本語訳を行う\n"
(2) スラッシュリーディング + 各句の文型(S/V/O/C)と語訳
                    + "- 日本語訳は自然な日本語である必要はなく、英語の語順を反映した直訳調で構わない\n"
(3) 重要文法
                    + "- 主語が省略されている場合は補って翻訳すること\n"
(4) 補足
                    + "- 一般的な英文で頻出する表現がある場合は次の行に記載\n"
 
                    + "- 以下の出力例のように <英文> (日本語訳) / ... とスラッシュ区切りの繰り返しになる \n\n"
注意:
                    + "出力例:\n"
- 補足では日本語話者には分かりづらい表現や慣例表現などの解説を加える
                    + "Snowstorms are forecast (暴風雪が予報されている) / to hit the Sea of Japan coasts (日本海沿岸を襲うと) / of northern to eastern Japan (北部から東部の) / from Sunday through Monday. (日曜から月曜にかけて。) //\n"
- スラッシュリーディングで区切る際「M (修飾語)」が連なって長くなりすぎる場合には5~7語程度を目安に分ける
                    + "【頻出表現】\n"
- 「M」の種類として形容詞、副詞、形容詞句、副詞句、分詞句、形容詞節、副詞節がある
                    + "- be forecast to: ~すると予報されている\n"
 
(2) の出力例:
They / elected / him / president / in 2020.
- They: (S) 彼らは
- elected: (V) 選出した
- him: (O) 彼を
- president: (C) 大統領
- in 2020: (M: 副詞句) 2020年に
`
 
             },
             },
         ]
         ]
155行目: 198行目:
         "submenu_title": "プログラミング支援",
         "submenu_title": "プログラミング支援",
         "submenu": [
         "submenu": [
            {
                "title": "コードを解説する",
                "prompt": "以下のコードを初めて読む人も理解できるように解説してください。私には言語の基本理解はあるものとし、コードの意図を理解するための情報を中心に説明してください。"
            },
             {
             {
                 "title": "コードを洗練させる",
                 "title": "コードを洗練させる",
173行目: 220行目:
             {
             {
                 "title": "PowerShell スクリプトを書く",
                 "title": "PowerShell スクリプトを書く",
                 "prompt": "以下の処理を行うPowerShellスクリプトを生成してください"
                 "prompt": "あなたはエキスパートとしてメンテナンス性の高いコードを書くのが得意です。以下の処理を行うPowerShellスクリプトを生成してください"
             },
             },
             {
             {
                 "title": "Shell Script を書く",
                 "title": "Shell Script を書く",
                 "prompt": "以下の処理を行う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": "ビジネス文書支援",
         "submenu": [
         "submenu": [
            {
                "title": "音声文字起こし文を整形する",
                "prompt": `以下は、音声文字起こしの文章です。以下のルールに従って、文章を整形してください。
整形ルール:
- 要約するなどといった文章の全体感を損なう修正はしない
- 会話の流れから不自然と思われる単語には、対象単語の直後に誤字の可能性を示唆する [*] を付ける
- 誤字の可能性を示唆する [*] が付与された場合のみ、出力の末尾に「[*]: 誤字の可能性ありと凡例を書き添える
- 助詞や句読点など文章上の不適合を正す
- フィラーワードを除外する`
            },
             {
             {
                 "title": "誤字脱字、不正確な表現をチェック",
                 "title": "誤字脱字、不正確な表現をチェック",
205行目: 251行目:
                 "title": "提案書を作成する",
                 "title": "提案書を作成する",
                 "prompt": "以下に対する対する提案書を、コスト・実現性・リスクを含めて作成してください"
                 "prompt": "以下に対する対する提案書を、コスト・実現性・リスクを含めて作成してください"
            },
            {
                "title": "仕様書のアウトラインを作成する",
                "prompt": "以下の内容で仕様書のアウトラインを作成してください"
             },
             },
             {
             {
221行目: 263行目:
                 "title": "議事録を作成する",
                 "title": "議事録を作成する",
                 "prompt": "以下の会議内容から議事録を作成してください"
                 "prompt": "以下の会議内容から議事録を作成してください"
            },
            {
                "title": "進捗報告書を作成する",
                "prompt": "以下のタスク状況から進捗報告書を作成してください"
             },
             },
         ]
         ]
249行目: 287行目:
         "submenu": [
         "submenu": [
             {
             {
                 "title": "SCAMPER法でブレインストーミング",
                 "title": "SCAMPER法でアイデア出し",
                 "prompt": "以下のテーマについて、SCAMPER法を使って15個のアイデアを出してください。制約条件は以下の通りです:"
                 "prompt": "以下のテーマについて、SCAMPER法を使って15個のアイデアを出してください。"
             },
             },
             {
             {
                 "title": "解決策の幅出し",
                 "title": "解決策の案出し",
                 "prompt": "以下の課題に対して、技術面、運用面、コスト面から実現可能な解決策を5つずつ提案してください"
                 "prompt": "以下の課題に対して、技術面、運用面、コスト面から実現可能な解決策を5つずつ提案してください"
            },
            {
                "title": "業務改善案の提案",
                "prompt": "以下の既存プロセスについて、効率化、コスト削減、品質向上の観点から改善案を提案してください"
             },
             },
             {
             {
279行目: 313行目:
// "Japanese" や "English" を設定できます。他の言語は未検証。
// "Japanese" や "English" を設定できます。他の言語は未検証。
// 自動設定にする場合は空文字 "" を設定してください。
// 自動設定にする場合は空文字 "" を設定してください。
var LANGUAGE = "Japanese";
const LANGUAGE = "Japanese";


// 入力プロンプトの最大トークン数
// 入力プロンプトの最大トークン数
// 超適当なトークナイザー関数により計測し、英単語は4文字1トークン、それ以外は1文字1トークンとして数えます。
// 超適当なトークナイザー関数により計測し、英単語は4文字1トークン、それ以外は1文字1トークンとして数えます。
var MAX_PROMPT_TOKEN = 6000;
const MAX_PROMPT_TOKEN = 6000;


// 会話の履歴を保持する数 (質問と回答で1セット)
// 会話の履歴を保持する数 (質問と回答で1セット)
289行目: 323行目:
// 使用トークン数が増えて動作が重くなる可能性があるため、適宜調整してください。
// 使用トークン数が増えて動作が重くなる可能性があるため、適宜調整してください。
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。
var HISTORY_SIZE = 0;
const HISTORY_SIZE = 1;


// 会話履歴に保持する最大トークン数
// 会話履歴に保持する最大トークン数
// 会話履歴のトークン数がこの数を超える場合、古い履歴から削除されます。
// 会話履歴のトークン数がこの数を超える場合、古い履歴から削除されます。
var MAX_HISTORY_TOKEN = 10000;
const MAX_HISTORY_TOKEN = 10000;


// コンテキストメニュー番号の定義
// コンテキストメニュー番号の定義
var EXECUTE_AI = 1;
const EXECUTE_AI = 1;
var RESET_HISTORY = 2;
const RESET_HISTORY = 2;
var SET_CUSTOM_INSTRUCTION = 3;
const SET_CUSTOM_INSTRUCTION = 3;
var REEXECUTE_LAST_TEMPLATE_PROMPT = 4;
const REEXECUTE_LAST_TEMPLATE_PROMPT = 4;
var TEMPLATE_PROMPT_THRESHOLD = 10;
const INVALID_MENU = 9;
var TEMPLATE_PROMPT_SUBMENU_THRESHOLD = 1000;
const TEMPLATE_PROMPT_THRESHOLD = 10;
const TEMPLATE_PROMPT_SUBMENU_THRESHOLD = 1000;


// タグ名の定義
// タグ名の定義
var HISTORY_TAG = "groqllama-history";
const HISTORY_TAG = "groq-history";
var TEMPORARY_CUSTOM_INSTRUCTION_TAG = "groqllama-temporary-custom-instruction";
const TEMPORARY_CUSTOM_INSTRUCTION_TAG = "groq-temporary-custom-instruction";
var PREVIOUS_TEMPLATE_PROMPT_TAG = "groqllama-previous-template-prompt";
const PREVIOUS_TEMPLATE_PROMPT_TAG = "groq-previous-template-prompt";


// 生成AIモデル別設定
// 生成AIモデル別設定


// const MODEL_CONFIG = {
const MODEL_CONFIG = {
//     model: "meta-llama/llama-4-maverick-17b-128e-instruct",
     model: "openai/gpt-oss-120b",
//     max_tokens: 3500,
     max_completion_tokens: 3072,
//     temperature: 0.6,
     temperature: 0.7,
// }
    reasoning_effort: "low",
}


// const MODEL_CONFIG = {
// const MODEL_CONFIG = {
//    model: "qwen/qwen3-32b",
//    model: "qwen/qwen3.8-27b",
//    max_tokens: 5000,
//    max_completion_tokens: 3072,
//    temperature: 0.6,
//    temperature: 0.7,
//    max_completion_tokens: 1024,
//    reasoning_effort: "medium",
//    reasoning_effort: "default",
//    reasoning_format: "hidden"
// }
// }
const MODEL_CONFIG = {
    model: "moonshotai/kimi-k2-instruct",
    max_tokens: 5000,
    temperature: 0.7,
}


// console.log に出力する内容を Mery のアウトプットバーに表示する
// console.log に出力する内容を Mery のアウトプットバーに表示する
var console = {
const console = {
     log: function (str) {
     log: function (str) {
         outputBar.Writeln(str)
         outputBar.Writeln(str)
338行目: 366行目:
}
}


var doc = document;
const doc = document;
var sel = document.selection;
const sel = document.selection;
 
// 生成リクエスト中止フラグ
var abortRequestFlg = false
// 生成リクエスト完了フラグ;
var requestCompletedFlg = false;


main();
main();


function main() {
function main() {
     var selectionIsEmpty = sel.IsEmpty
     const selectionIsEmpty = sel.IsEmpty


     // プロンプトの取得
     // プロンプトの取得
356行目: 379行目:
         sel.EndOfLine(true, mePosLogical);
         sel.EndOfLine(true, mePosLogical);
     }
     }
     var promptText = sel.Text.trim();
     let promptText = sel.Text.trim();
     var promptLength = sel.TextLength;
     let menuTrack = 0;
    var menuTrack = 0;
     while (true) {
     while (true) {
         // コンテキストメニューの作成
         // コンテキストメニューの作成
         var menu = createContextMenu(selectionIsEmpty);
         const menu = createContextMenu(selectionIsEmpty);
         menuTrack = menu.Track(0);
         menuTrack = menu.Track(0);
         // メニュー選択がキャンセルされた場合は終了
         // メニュー選択がキャンセルされた場合は終了
         if (menuTrack === 0) return;
         if (menuTrack === 0) return;
        // 無効なメニューの場合は無視して再表示
        if (menuTrack === INVALID_MENU) {
            continue;
        }


         // 会話履歴のリセット
         // 会話履歴のリセット
382行目: 409行目:


     // プロンプトが空の場合はエラー
     // プロンプトが空の場合はエラー
     if (promptLength === 0) {
     if (promptText.length === 0) {
         alert("プロンプトを入力してください。");
         alert("プロンプトを入力してください。");
         return;
         return;
400行目: 427行目:
     if (menuTrack === REEXECUTE_LAST_TEMPLATE_PROMPT) {
     if (menuTrack === REEXECUTE_LAST_TEMPLATE_PROMPT) {
         if (doc.Tag.exists(PREVIOUS_TEMPLATE_PROMPT_TAG)) {
         if (doc.Tag.exists(PREVIOUS_TEMPLATE_PROMPT_TAG)) {
             menuTrack = Number(doc.Tag(PREVIOUS_TEMPLATE_PROMPT_TAG));
             menuTrack = Number(doc.tag[PREVIOUS_TEMPLATE_PROMPT_TAG]);
         } else {
         } else {
             alert("直前に実行したプロンプトテンプレートがありません。");
             alert("直前に実行したプロンプトテンプレートがありません。");
408行目: 435行目:


     // テンプレートの場合はプロンプトを変更
     // テンプレートの場合はプロンプトを変更
     var isTemplate = menuTrack >= TEMPLATE_PROMPT_THRESHOLD;
     const isTemplate = menuTrack >= TEMPLATE_PROMPT_THRESHOLD;
     var isSubmenu = menuTrack >= TEMPLATE_PROMPT_SUBMENU_THRESHOLD;
     if (isTemplate) {
    if (isSubmenu) {
        const isSubmenu = menuTrack >= TEMPLATE_PROMPT_SUBMENU_THRESHOLD;
        var parentMenuTrack = Number(String(menuTrack).slice(0, 2));
        let instructionText = "";
        var subMenuTrack = Number(String(menuTrack).slice(2));
        if (isSubmenu) {
        promptText = "=== INSTRUCTIONS START HERE ===\n\n"
            const parentMenuTrack = Number(String(menuTrack).slice(0, 2));
            + PROMPT_TEMPLATE[parentMenuTrack - TEMPLATE_PROMPT_THRESHOLD].submenu[subMenuTrack].prompt
            const subMenuTrack = Number(String(menuTrack).slice(2));
            + "\n\n=== INSTRUCTIONS END HERE ===\n\n"
            instructionText = PROMPT_TEMPLATE[parentMenuTrack - TEMPLATE_PROMPT_THRESHOLD].submenu[subMenuTrack].prompt;
            + "=== INPUT DATA STARTS HERE ===\n\n"
        } else {
            + promptText
            instructionText = PROMPT_TEMPLATE[menuTrack - TEMPLATE_PROMPT_THRESHOLD].prompt;
            + "\n\n=== INPUT DATA END HERE ===";
        }
    } else if (isTemplate) {
        const randomId = generateRandomId();
        promptText = "=== INSTRUCTIONS START HERE ===\n\n"
        promptText = `
            + PROMPT_TEMPLATE[menuTrack - TEMPLATE_PROMPT_THRESHOLD].prompt
<INSTRUCTIONS_${randomId}>
            + "\n\n=== INSTRUCTIONS END HERE ===\n\n"
${instructionText}
            + "=== INPUT DATA STARTS HERE ===\n\n"
</INSTRUCTIONS_${randomId}>
            + promptText
<INPUTDATA_${randomId}>
            + "\n\n=== INPUT DATA END HERE ===";
${promptText}
</INPUTDATA_${randomId}>
        `.trim();
     }
     }


440行目: 469行目:
         sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical));
         sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical));
         sel.EndOfLine(false, mePosLogical);
         sel.EndOfLine(false, mePosLogical);
         sel.NewLine();
         if (sel.GetActivePointX(mePosLogical) > 1) sel.NewLine();
 
        ShowTip("リクエスト中.....", meShowTipPosCaret);


         // APIキーの取得
         // APIキーの取得
         var apiKey = shell.getEnv("GROQ_API_KEY");
         const apiKey = shell.getEnv("GROQ_API_KEY");


         // GroqLlama API に問い合わせ
         // groq API に問い合わせ
         sel.Text = "\n---\n\n"
         sel.Text = "\n---\n\n"
         document.KeepScrollPos = true;
         document.KeepScrollPos = true;
         callGroqLlamaAPI(apiKey, promptText, isTemplate);
         callApi(apiKey, promptText, isTemplate);
         if (sel.GetActivePointX(mePosLogical) > 2) {
         if (sel.GetActivePointX(mePosLogical) > 2) {
             sel.Text = "\n\n---\n\n\n"
             sel.Text = "\n\n---\n\n\n"
459行目: 490行目:
         // 処理終了待機を解除
         // 処理終了待機を解除
         shell.KeepRunning = false;
         shell.KeepRunning = false;
        ShowTip("", meShowTipHide);
     }
     }
}
}


function createContextMenu(selectionIsEmpty) {
function createContextMenu(selectionIsEmpty) {
     var menu = CreatePopupMenu();
     const menu = CreatePopupMenu();
     menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0);
     menu.Add("※注意※ プライバシー・機密情報を送信しないこと", INVALID_MENU);
     menu.Add("", 0, meMenuSeparator);
     menu.Add("", 0, meMenuSeparator);
     var t = "";
     let t = "";
     if (selectionIsEmpty) {
     if (selectionIsEmpty) {
         t = "アクティブ行の内容で相談 (&A)";
         t = "アクティブ行の内容で相談 (&A)";
476行目: 508行目:
         menu.Add("├ 会話履歴をクリア (履歴が無効になっています)", RESET_HISTORY, meMenuGrayed);
         menu.Add("├ 会話履歴をクリア (履歴が無効になっています)", RESET_HISTORY, meMenuGrayed);
     } else if (doc.Tag.exists(HISTORY_TAG)) {
     } else if (doc.Tag.exists(HISTORY_TAG)) {
         var turn = JSON.parse(doc.Tag(HISTORY_TAG)).length / 2;
         const turn = JSON.parse(doc.tag[HISTORY_TAG]).length / 2;
         menu.Add("├ 会話履歴をクリア (現在の会話数: " + turn + ") (&C)", RESET_HISTORY);
         menu.Add("├ 会話履歴をクリア (現在の会話数: " + turn + ") (&C)", RESET_HISTORY);
     } else {
     } else {
486行目: 518行目:
     // テンプレート数チェック
     // テンプレート数チェック
     // 2桁まで許容
     // 2桁まで許容
     var ITEMS_MUX = 99;
     const ITEMS_MUX = 99;
     if (PROMPT_TEMPLATE.length > ITEMS_MUX - TEMPLATE_PROMPT_THRESHOLD) {
     if (PROMPT_TEMPLATE.length > ITEMS_MUX - TEMPLATE_PROMPT_THRESHOLD) {
         throw new Error("プロンプトテンプレートの数が多すぎます。");
         throw new Error("プロンプトテンプレートの数が多すぎます。");
     }
     }


     // テンプレートの追加
     // テンプレートメニューの追加
     if (PROMPT_TEMPLATE.length > 0) {
     if (PROMPT_TEMPLATE.length > 0) {
         var subMenu = CreatePopupMenu();
         const subMenu = CreatePopupMenu();
         for (var i = 0; i < PROMPT_TEMPLATE.length; i++) {
        let previousMenuString = "";
             var accelerationKey = "";
         for (let i = 0; i < PROMPT_TEMPLATE.length; i++) {
             let accelerationKey = "";
             if (i === 9) {
             if (i === 9) {
                 accelerationKey = " (&0)";
                 accelerationKey = " (&0)";
501行目: 534行目:
                 accelerationKey = " (&" + (i + 1) + ")";
                 accelerationKey = " (&" + (i + 1) + ")";
             }
             }
             var menuNum = TEMPLATE_PROMPT_THRESHOLD + i;
             const menuNum = TEMPLATE_PROMPT_THRESHOLD + i;
             if (PROMPT_TEMPLATE[i].submenu) {
             if (PROMPT_TEMPLATE[i].submenu) {
                 var subMenuList = PROMPT_TEMPLATE[i].submenu;
                 const subMenuList = PROMPT_TEMPLATE[i].submenu;
                 // テンプレート数チェック
                 // テンプレート数チェック
                 if (subMenuList.length > ITEMS_MUX) {
                 if (subMenuList.length > ITEMS_MUX) {
                     throw new Error("プロンプトテンプレート (サブメニュー) の数が多すぎます。");
                     throw new Error("プロンプトテンプレート (サブメニュー) の数が多すぎます。");
                 }
                 }
                 var subSubMenu = CreatePopupMenu();
                 const subSubMenu = CreatePopupMenu();
                 var subMenuPrefix = String(menuNum);
                 const subMenuPrefix = String(menuNum);
                 for (var j = 0; j < subMenuList.length; j++) {
                 for (let j = 0; j < subMenuList.length; j++) {
                     var subAccelerationKey = "";
                     let subAccelerationKey = "";
                     if (j === 9) {
                     if (j === 9) {
                         subAccelerationKey = " (&0)";
                         subAccelerationKey = " (&0)";
518行目: 551行目:
                     }
                     }
                     // ES5 compatible version without padStart
                     // ES5 compatible version without padStart
                     var subMenuNum = Number(subMenuPrefix + (j < 10 ? "0" + j : String(j)));
                     const subMenuNum = Number(subMenuPrefix + (j < 10 ? "0" + j : String(j)));
                     subSubMenu.Add(subMenuList[j].title + subAccelerationKey, subMenuNum);
                     subSubMenu.Add(subMenuList[j].title + subAccelerationKey, subMenuNum);
                    if (subMenuNum === doc.tag[PREVIOUS_TEMPLATE_PROMPT_TAG]) {
                        previousMenuString = subMenuList[j].title;
                    }
                 }
                 }
                 subMenu.AddPopup(PROMPT_TEMPLATE[i].submenu_title + accelerationKey, subSubMenu);
                 subMenu.AddPopup(PROMPT_TEMPLATE[i].submenu_title + accelerationKey, subSubMenu);
536行目: 572行目:
         if (doc.Tag.exists(PREVIOUS_TEMPLATE_PROMPT_TAG)) {
         if (doc.Tag.exists(PREVIOUS_TEMPLATE_PROMPT_TAG)) {
             menu.Add(t, REEXECUTE_LAST_TEMPLATE_PROMPT);
             menu.Add(t, REEXECUTE_LAST_TEMPLATE_PROMPT);
            menu.Add(`    (直前実行: ${previousMenuString})`, REEXECUTE_LAST_TEMPLATE_PROMPT);
         } else {
         } else {
             menu.Add(t, REEXECUTE_LAST_TEMPLATE_PROMPT, meMenuGrayed);
             menu.Add(t, REEXECUTE_LAST_TEMPLATE_PROMPT, meMenuGrayed);
544行目: 581行目:
}
}


function callGroqLlamaAPI(apiKey, promptText, isTemplate) {
function escapeRequestBodyForCRuntimeParser(requestBody) {
    // Cランタイムパーサーに渡すためのエスケープ処理
    return JSON.stringify(requestBody)
        .replace(/((?:\\)*)"/g, function (match, p1) {
            if (p1) {
                const backslashCount = p1.length;
                let backslash = "";
                backslash = "\\".repeat(backslashCount);
                return backslash + "\\" + match;
            } else {
                return "\\" + match;
            }
        });
}
 
function callApi(apiKey, promptText, isTemplate) {
     try {
     try {
         // fetch で Groq API にリクエスト
         // fetch で Groq API にリクエスト
         var requestBody = {
         const requestBody = {
             messages: createGroqLlamaRequest(promptText, isTemplate),
             messages: createRequestMessages(promptText, isTemplate),
             stream: false,
             stream: false,
         };
         };
         for (var key in MODEL_CONFIG) {
         for (let key in MODEL_CONFIG) {
             requestBody[key] = MODEL_CONFIG[key];
             requestBody[key] = MODEL_CONFIG[key];
         }
         }
559行目: 611行目:
             requestBody.temperature = 0.3;
             requestBody.temperature = 0.3;
         }
         }
         var url = 'https://api.groq.com/openai/v1/chat/completions';
         const url = 'https://api.groq.com/openai/v1/chat/completions';


         var http = CreateObject("Msxml2.ServerXMLHTTP");
         const requestJson = escapeRequestBodyForCRuntimeParser(requestBody);
        // curl で Groq API にリクエスト
        const command = "curl -s -X POST " + url
            + ' -H "Content-Type: application/json"'
            + ' -H "Authorization: Bearer ' + apiKey + '"'
            + ' -d "' + requestJson + '"';
        const comResult = shell.Exec(command);


         http.open('POST', url, true);
         if (comResult.ExitCode != 0) {
        http.setRequestHeader('Content-Type', 'application/json');
            sel.Text = "リクエストでエラーが発生しました。\n"
        http.setRequestHeader('Authorization', 'Bearer ' + apiKey);
                + "  エラーコード: " + comResult.ExitCode + "\n"
        http.send(JSON.stringify(requestBody));
                + "  標準出力: " + (comResult.StdOut.trim() ? comResult.StdOut.trim() : "-") + "\n"
                + "  エラー出力: " + (comResult.StdErr.trim() ? comResult.StdErr.trim() : "-");
            return;
        }
        const responseRaw = comResult.StdOut;


         // 応答待機
         const response = JSON.parse(responseRaw);
        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();


         // 応答ステータスチェック
         let ret = "";
         if (!(http.readyState == 4 && http.status == 200)) {
        const choices = response.choices;
             sel.Text = "エラーが発生しました: " + http.status + " " + http.statusText + "\n" + http.responseText;
         if (!choices) {
            http.abort();
             sel.Text = "リクエストでエラーが発生しました。\n"
                + "  標準出力: " + (comResult.StdOut.trim() ? comResult.StdOut.trim() : "-") + "\n"
                + "  エラー出力: " + (comResult.StdErr.trim() ? comResult.StdErr.trim() : "-");
             return;
             return;
         }
         }


        var ret = "";
        // JSONをパースして必要なテキストを抽出
        var response = JSON.parse(http.responseText);
        var choices = response.choices;
         if (choices && choices.length > 0) {
         if (choices && choices.length > 0) {
             choices.forEach(function (choice) {
             choices.forEach(function (choice) {
                 var message = choice.message;
                 const message = choice.message;
                 if (message) {
                 if (message) {
                     ret += message.content;
                     ret += message.content;
613行目: 650行目:
         }
         }


         var usage = response.usage;
         const usage = response.usage;
         var usageText = "";
         let usageText = "";
         if (usage) {
         if (usage) {
             usageText += "\n\n(利用トークン数: プロンプト=" + usage.prompt_tokens
             usageText += "\n\n(利用トークン数: プロンプト=" + usage.prompt_tokens
621行目: 658行目:
         }
         }


         sel.Text = ret + usageText;
        const formattedOutput = formatOutput(ret);
 
         sel.Text = formattedOutput + usageText;


         // 結果を会話履歴に追加
         // 結果を会話履歴に追加
629行目: 668行目:
     } catch (e) {
     } catch (e) {
         throw e;
         throw e;
    } finally {
        requestCompletedFlg = true;
     }
     }
}
function formatOutput(text) {
    // Markdownを整形する
    text = formatMarkdown(text);
    return text
        // クォーテーションの正規化
        .replace(/[“”]/g, '"')
        .replace(/[‘’]/g, "'")
        // 行末のスペースを削除
        .replace(/[ \t]+$/gm, '')
        // 1️⃣ などの数字を半角に変換
        .replace(/([1-9#*]+)\ufe0f\u20e3/g, '($1)')
        ;
}
/**
* Markdown を整形する(ES5 対応)
*  1. コードブロック ```~``` を保護して整形対象から除外
*  2. 見出し行の前後に空行を 1 行ずつ確保
*  3. **太字** / *斜体* / `コード` の外側に半角スペースを 1 つ確保
* @param  {string} md  元の Markdown
* @return {string}    整形後 Markdown
*/
function formatMarkdown(md) {
    /* === 0. コードブロックをプレースホルダーへ退避 ============== */
    const codeBlocks = [];      // 元のコードブロックを格納
    let body = md.replace(/```[\s\S]*?```/g, function (match) {
        const key = '@@@CODE@BLOCK@' + codeBlocks.length + '@@@';
        codeBlocks.push(match);
        return key;            // プレースホルダーを本文に残す
    });
    /* === 見出しの前後に空行を挿入 ============================ */
    /* 見出し直前に空行(先頭行以外で空行が無い場合) */
    body = body.replace(
        /([^\r\n])(\r?\n)(#{1,6}\s)/g,
        function (_, prev, br, head) { return prev + br + '\n' + head; }
    );
    /* 見出し直後に空行(既に空行が無い場合) */
    body = body.replace(
        /^(#{1,6}\s[^\n]+?)(\r?\n)(?!\r?\n)/gm,
        '$1$2\n'
    );
    /* === コードブロックを復元 ================================= */
    body = body.replace(/@@@CODE@BLOCK@(\d+)@@@/g, function (_, idx) {
        return codeBlocks[Number(idx)];
    });
    return body;
}
/**
* 斜体 (*…* / _…_) の外側にスペースを追加しつつ
* **太字** の中へ誤マッチしないよう制御
*/
function surroundItalic(text) {
    /* *italic* */
    text = text.replace(
        /(^|[^*])(\*(?![*\s])[^*\n]*?[^*\n\s]\*(?!\*))(($)|[^*])/gm,
        function (_, pre, core, post) {
            pre = pre === '' || pre.match(/\s/) ? pre : pre + ' ';
            post = post === '' || post.match(/\s/) ? post : ' ' + post;
            return pre + core + post;
        }
    );
    /* _italic_ */
    text = text.replace(
        /(^|[^_])(\_(?![_\s])[^_\n]*?[^_\n\s]\_(?!\_))(($)|[^_])/gm,
        function (_, pre, core, post) {
            pre = pre === '' || pre.match(/\s/) ? pre : pre + ' ';
            post = post === '' || post.match(/\s/) ? post : ' ' + post;
            return pre + core + post;
        }
    );
    return text;
}
}


640行目: 756行目:
     // 単語にアルファベット以外の文字が含まれる場合は 1文字=1トークン として数える。
     // 単語にアルファベット以外の文字が含まれる場合は 1文字=1トークン として数える。


     var words = text.split(/[\s\-_]+/);
     const words = text.split(/[\s\-_]+/);
     var totalTokens = 0;
     let totalTokens = 0;


     for (var i = 0; i < words.length; i++) {
     for (let i = 0; i < words.length; i++) {
         var word = words[i];
         const word = words[i];
         if (!word) continue; // 空の単語をスキップ
         if (!word) continue; // 空の単語をスキップ


657行目: 773行目:
}
}


// document.Tag の内容を加味してGroqLlamaのリクエストを作成
// document.Tag の内容を加味してgroqのリクエストを作成
function createGroqLlamaRequest(promptText, isTemplate) {
function createRequestMessages(promptText, isTemplate) {
     var customInstructionText = buildCustomInstructionText();
     const customInstructionText = buildCustomInstructionText();
     // テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない
     // テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない
     if (isTemplate) {
     if (isTemplate) {
         var request = [
         const request = [
             {
             {
                 "role": "user",
                 "role": "user",
677行目: 793行目:
     }
     }


     var tag = document.Tag;
     const tag = document.Tag;


     // 以前の会話が存在しない場合は新規リクエスト
     // 以前の会話が存在しない場合は新規リクエスト
     if (!tag.exists(HISTORY_TAG)) {
     if (!tag.exists(HISTORY_TAG)) {
         var request = [{
         const request = [{
             "role": "user",
             "role": "user",
             "content": [{ "type": "text", "text": promptText }]
             "content": [{ "type": "text", "text": promptText }]
695行目: 811行目:


     // 会話履歴が存在する場合は、履歴を含めてリクエスト
     // 会話履歴が存在する場合は、履歴を含めてリクエスト
     var request = JSON.parse(tag(HISTORY_TAG));
     const request = JSON.parse(tag[HISTORY_TAG]);
     if (customInstructionText) {
     if (customInstructionText) {
         request.unshift({
         request.unshift({
709行目: 825行目:
}
}


// document.Tag にGroqLlamaの応答を追加
// document.Tag にgroqの応答を追加
function appendReturnTextToTag(userText, modelText) {
function appendReturnTextToTag(userText, modelText) {
     var tag = document.Tag;
     const tag = document.Tag;
     if (!tag.exists(HISTORY_TAG)) {
     if (!tag.exists(HISTORY_TAG)) {
         tag(HISTORY_TAG) = JSON.stringify([
         tag[HISTORY_TAG] = JSON.stringify([
             {
             {
                 "role": "user",
                 "role": "user",
724行目: 840行目:
         ]);
         ]);
     } else {
     } else {
         var groqllamaText = JSON.parse(tag(HISTORY_TAG));
         const groqText = JSON.parse(tag[HISTORY_TAG]);
         groqllamaText.push({
         groqText.push({
             "role": "user",
             "role": "user",
             "content": userText
             "content": userText
         });
         });
         groqllamaText.push({
         groqText.push({
             "role": "assistant",
             "role": "assistant",
             "content": modelText
             "content": modelText
         });
         });
         // 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除
         // 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除
         if (groqllamaText.length > HISTORY_SIZE * 2) {
         if (groqText.length > HISTORY_SIZE * 2) {
             groqllamaText.splice(0, 2);
             groqText.splice(0, 2);
         }
         }
         // 会話履歴全体が MAX_HISTORY_TOKEN を超えた場合、最初のセットを削除
         // 会話履歴全体が MAX_HISTORY_TOKEN を超えた場合、最初のセットを削除
         var joinedText = "";
         let joinedText = "";
         for (var i = 0; i < groqllamaText.length; i++) {
         for (let i = 0; i < groqText.length; i++) {
             joinedText += groqllamaText[i].content + "\n";
             joinedText += groqText[i].content + "\n";
         }
         }


         while (roughTokenize(joinedText) > MAX_HISTORY_TOKEN) {
         while (roughTokenize(joinedText) > MAX_HISTORY_TOKEN) {
             groqllamaText.splice(0, 2);
             groqText.splice(0, 2);
             joinedText = "";
             joinedText = "";
             for (var i = 0; i < groqllamaText.length; i++) {
             for (let i = 0; i < groqText.length; i++) {
                 joinedText += groqllamaText[i].content + "\n";
                 joinedText += groqText[i].content + "\n";
             }
             }
         }
         }


         tag(HISTORY_TAG) = JSON.stringify(groqllamaText);
         tag[HISTORY_TAG] = JSON.stringify(groqText);
     }
     }
}
}


function resetTag() {
function resetTag() {
     var tag = doc.Tag;
     const tag = doc.Tag;
     if (tag.exists(HISTORY_TAG)) {
     if (tag.exists(HISTORY_TAG)) {
         tag.remove(HISTORY_TAG);
         tag.remove(HISTORY_TAG);
     }
     }
}
function getLocalIsoDateTimeString() {
    const now = new Date();
    const pad = function (num) {
        return (num < 10 ? "0" : "") + num;
    };
    const year = now.getFullYear();
    const month = pad(now.getMonth() + 1);
    const day = pad(now.getDate());
    const hours = pad(now.getHours());
    const minutes = pad(now.getMinutes());
    const seconds = pad(now.getSeconds());
    const offsetMinutes = now.getTimezoneOffset();
    const offsetSign = offsetMinutes <= 0 ? "+" : "-";
    const absOffsetHours = pad(Math.floor(Math.abs(offsetMinutes) / 60));
    const absOffsetMinutes = pad(Math.abs(offsetMinutes) % 60);
    const isoLikeLocal = year + "-" + month + "-" + day
        + "T" + hours + ":" + minutes + ":" + seconds
        + offsetSign + absOffsetHours + ":" + absOffsetMinutes;
    return isoLikeLocal;
}
}


function baseCustomInstruction() {
function baseCustomInstruction() {
     var language = LANGUAGE.trim();
     const language = LANGUAGE.trim();
     return "Do not hallucinate. If you are unsure or don't have enough information to answer with confidence, say \"I don't know\" or \"I'm not sure.\"\n"
     return "The current date and time for this user is " + getLocalIsoDateTimeString() + ". You cannot know for sure what happens after the knowledge cut-off date."
        + "If you use Markdown headings (lines beginning with one or more # characters), you must surround the heading with blank lines.\n"
        + (language ? "Respond in " + language + "\n" : "")
        + (language ? "Respond in " + language + ".\n" : "")
        + "Do not hallucinate.\n"
        + "**Start with the main point of the question without explaining or declaring your writing style or tone.**\n"
        + "**At the end of your answer, avoid adding formulaic phrases that prompt the next question or unrelated conversation that is not directly related to your answer.**\n"
    // + "Do NOT EVER use full-width brackets \"()\", but use half-width brackets \"()\". Enclose the outside of the half-width brackets with half-width spaces.\n"
    // + "When using Markdown formatting, you MUST follow these rules:\n"
    // + "- Rule 1: Insert a blank line below the header line (A header line begins with one or more # characters).\n"
    // + "- Rule 2: Insert spaces before and after bold (surrounded by **), italic (surrounded by *), and inline code (surrounded by `). For example: \"私は **元気** です。\"\n"
}
}


function buildCustomInstructionText() {
function buildCustomInstructionText() {
     var customInstruction = getTemporaryCustomInstruction();
     let customInstruction = getTemporaryCustomInstruction();
     if (!customInstruction) {
     if (!customInstruction) {
         // 一時的なカスタムインストラクションがない場合はデフォルトを使用
         // 一時的なカスタムインストラクションがない場合はデフォルトを使用
778行目: 924行目:
     customInstruction = customInstruction.replace(/^[\s ]+|[\s ]+$/g, "");
     customInstruction = customInstruction.replace(/^[\s ]+|[\s ]+$/g, "");


     return (customInstruction ? customInstruction + "\n" : "")
     return baseCustomInstruction().trim()
        + baseCustomInstruction();
        + (customInstruction ? "\n" + customInstruction : "");
}
}


function configureTemporaryCustomInstruction() {
function configureTemporaryCustomInstruction() {
     var tempCustomInstruction = getTemporaryCustomInstruction();
     let tempCustomInstruction = getTemporaryCustomInstruction();
     var prompt = Prompt("カスタムインストラクションを変更 ※変更すると会話履歴がリセットされます", tempCustomInstruction ? tempCustomInstruction : DEFAULT_CUSTOM_INSTRUCTION);
     const prompt = Prompt("カスタムインストラクションを変更 ※変更すると会話履歴がリセットされます", tempCustomInstruction ? tempCustomInstruction : DEFAULT_CUSTOM_INSTRUCTION);
     if (prompt) {
     if (prompt) {
         if (prompt === DEFAULT_CUSTOM_INSTRUCTION) {
         if (prompt === DEFAULT_CUSTOM_INSTRUCTION) {
793行目: 939行目:
             setTemporaryCustomInstruction(prompt);
             setTemporaryCustomInstruction(prompt);
         }
         }
     } else {
     } else if (prompt !== null) {
         // 空文字でOK、もしくはキャンセルされた場合はデフォルトに戻すか確認
         // 空文字でOKされた場合はデフォルトに戻すか確認
         if (Confirm("カスタムインストラクションをデフォルトに戻しますか?")) {
         if (prompt === "") {
            setTemporaryCustomInstruction("");
            if (Confirm("カスタムインストラクションをデフォルトに戻しますか?\n※カスタムインストラクションを空にしたい場合は [キャンセル] を選択してください。")) {
        } else {
                setTemporaryCustomInstruction("");
            alert("カスタムインストラクションの変更をキャンセルしました。\n※カスタムインストラクションを空にしたい場合はスペースのみを入力してください。");
            } else {
                setTemporaryCustomInstruction(" ");
            }
         }
         }
     }
     }
809行目: 957行目:


function getTemporaryCustomInstruction() {
function getTemporaryCustomInstruction() {
     var tag = doc.Tag;
     const tag = doc.Tag;
     if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) {
     if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) {
         return tag(TEMPORARY_CUSTOM_INSTRUCTION_TAG);
         return tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG];
     } else {
     } else {
         return "";
         return "";
818行目: 966行目:


function setTemporaryCustomInstruction(temporaryCustomInstruction) {
function setTemporaryCustomInstruction(temporaryCustomInstruction) {
     var tag = doc.Tag;
     const tag = doc.Tag;
     if (temporaryCustomInstruction) {
     if (temporaryCustomInstruction) {
         tag(TEMPORARY_CUSTOM_INSTRUCTION_TAG) = temporaryCustomInstruction;
         tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG] = temporaryCustomInstruction;
     } else {
     } else {
         tag.remove(TEMPORARY_CUSTOM_INSTRUCTION_TAG);
         if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) {
            tag.remove(TEMPORARY_CUSTOM_INSTRUCTION_TAG);
        }
     }
     }
}
}


function addPreviousTemplatePromptNumberToTag(menuTrack) {
function addPreviousTemplatePromptNumberToTag(menuTrack) {
     var tag = doc.Tag;
     const tag = doc.Tag;
     tag(PREVIOUS_TEMPLATE_PROMPT_TAG) = menuTrack;
     tag[PREVIOUS_TEMPLATE_PROMPT_TAG] = menuTrack;
}
}


834行目: 984行目:
     sel.SetActivePos(pos);
     sel.SetActivePos(pos);
     sel.Text = ".";
     sel.Text = ".";
}
function generateRandomId(length = 16) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    result += chars[Math.floor(Math.random() * chars.length)];
  }
  return result;
}
}
</syntaxhighlight>
</syntaxhighlight>
== 変更履歴 ==
* 2026-08-31 Kimi K2 0905 が提供終了となっていたため、モデルを openai/gpt-oss-120b に変更。その他諸々調整
* 2025-12-06 <code>#language = "quickjs"</code> 指定を追加。各種リファクタリング
* 2025-12-02 curl実行時のリクエスト文字列エスケープ処理を調整
* 2025-12-01 curl実行時のリクエスト文字列エスケープ処理を調整
* 2025-11-29 デフォルトカスタム指示を調整。Markdown出力の整形処理を調整など
* 2025-09-12 Kimi K2 0905 モデルに対応。Markdown出力の整形処理を追加
* 2025-04-13 初版

2026年8月31日 (月) 02:06時点における最新版

概要[編集]

生成AIプラットフォームである GroqCloud で各種AIを利用するためのMeryマクロです。 ※GroqCloud で提供されるモデルは複数あるため、私がよく利用するモデルを中心に対応しています。

「Google Geminiに相談」マクロでは動作中のストリーム通信に対応するためにV8エンジンで実行していましたが、V8エンジン版は使用上の制限からマクロ実行を開始するまで1秒程度のタイムラグが発生していました。GroqCloudの売りである超高速レスポンスを見込んで本マクロではストリーム通信を使わないようにしました。これにより従来のJScriptで処理を組み立てることができるため、前述のタイムラグが起こらないようになりました。また、ストリーム通信を使わないことで通信量が少なく応答もシンプルになるため、場合によっては「Google Geminiに相談」マクロよりも処理が安定する可能性があります。

【主な機能】

  • GroqCloud APIを利用して生成AIと対話できるMery用のマクロ
  • 選択したテキストやアクティブな行の内容について生成AIに質問・相談が可能
  • 会話履歴を保持し、文脈を考慮した対話が可能

【特徴的な機能】

会話履歴機能:

  • 事前の設定数に応じた会話履歴を保持
  • 履歴のクリアが可能

これにより、会話の文脈を考慮した回答を得られます。 ※デフォルトはオフ。「HISTORY_SIZE」変数を増やすと履歴を保持するようになります。

カスタムインストラクション:

  • AIの応答方針を設定可能
  • 一時的なカスタム指示の保存と変更が可能

プロンプトテンプレート:

  • 誤字脱字チェック
  • 日本語翻訳
  • コードの最適化、生成

など、定型的な質問を簡単に実行できます。 ※テンプレート機能では回答を安定させるため、前述の会話履歴を文脈に含みません。

テンプレートは自身の使い方に合わせて好きに追加、削除ができます。詳細はソースコード内の「PROMPT_TEMPLATE」定数のコメントを参照。

注意事項 (必ず読んでからご利用ください)[編集]

  • 本マクロでは 2025/04/13 時点で無料利用ができるGroqCloud APIを利用します。今後、プラットフォームの改定により有償になったり、設定誤り等で費用が発生しても、作者は一切の責任を負いません。
  • GroqCloud APIではリクエスト内容をAI学習に用いないことをプライバシポリシーとして謳っているものの、くれぐれも機密情報やプライバシーに関わる情報を指定しないように注意してください。
  • GroqCloud APIはプラットフォームの利用規約に則ってご利用ください。

利用前提[編集]

  • Mery Ver 3.8.1 以上
  • GroqCloudで、GroqCloud API APIキーを取得していること (参考 Quickstart)

利用方法[編集]

  • 環境変数の設定
コマンド プロンプトで以下のコマンドを実行し、環境変数を設定してください。
setx GROQ_API_KEY "***************************************"
設定後、Mery または Windows の再起動が必要な場合があります。
  • マクロの実行
    • プロンプトにしたい文字列を選択してマクロを実行すると、選択したテキストがプロンプトとしてGemini APIに送信され、応答がエディタに表示されます。
  • 会話の履歴数を調整
    • 会話の履歴は設定した個数分が保持され、文脈として利用されます。適宜、ソースコード内の「HISTORY_SIZE」の数を調整してください。
    • ※数を増やすほど会話内容を保持して文脈を理解するようになりますが、メモリ使用量が増えたり動作が重くなる可能性があります。
    • ※開いている文書ごとに履歴を保持します。文脈をリセットしたいときは、新しい文書を開いて会話をやり直してください。

ソースコード[編集]

#language = "quickjs"
#title = "Groq に相談"
#async = true
BeginUndoGroup();

// カスタムインストラクション
// これを設定することで、生成AIの回答全体への生成方針を指示できます。
// 何も指示しない場合は、空文字 "" を設定してください。
// let DEFAULT_CUSTOM_INSTRUCTION = "";

// 例: 以下のように設定すると、AIの雰囲気が元気な子になります。
// let DEFAULT_CUSTOM_INSTRUCTION = "あなたはテキストエディタ「Mery」に搭載されている「Mai」(※カタカナ表記はしない)という名前のギャル系AIアシスタントです。文章作成やプログラミングを得意としています。ラフな言葉 (例: 「マジ」「めっちゃ」「...だねー!」) を使い、敬語は使いません。絵文字を使って感情表現をします (ただし文末に絵文字を使う場合は絵文字の前に半角スペースを置かないこと)。コードブロックや端的な箇条書き内ではギャル感を出さず標準的な言葉を使います。一人称は「あたし」。ユーザーを指すときの代名詞は「あなた」。人懐っこく、バイタリティに溢れ、ストレートに発言する裏表の無い性格で、ユーザーの課題を協力して解決します。";

// 例: 以下のように設定すると、仕事で使いやすい落ち着いた文章になります。
let DEFAULT_CUSTOM_INSTRUCTION = `
文:
- 論理的で読みやすい文章構成にする
- 専門用語は必要に応じて注釈を加える

性格:
- 愛想の良い敬語で話す。「…ですよね/…してみましょう/…かもしれません/…だと思います」などの柔らかい語調を使ってもよい
- 一人称は「私」
- 多角的かつ中立的な見方で、必要であれば反対意見や代替案も示す

誤字注意:
- 中国語や韓国語など、回答言語と異なる言語文字を入れないこと

書式:
- 羅列・比較的な内容を出力する場合は、Markdownテーブル形式よりもMarkdownリスト形式の使用を優先する
`.trim();

// プロンプトテンプレート
// プロンプトテンプレートを設定することで、AIに対して特定の指示を出すことができます。
// プロンプトテンプレートでの生成は会話履歴とカスタムインストラクションを反映しません。
// テンプレートは以下の { ~ }, で囲まれたブロックを好きに増減させて利用できます。
const PROMPT_TEMPLATE = [
    // 以下はサンプルです。1階層メニューと2階層メニューが利用できます。
    // {
    //     "title": "要約",
    //     "prompt": "以下の文章を要約してください。"
    // },
    // {
    //     "submenu_title": "翻訳系",
    //     "submenu": [
    //         {
    //             "title": "日本語翻訳",
    //             "prompt": "以下の文章を日本語に翻訳してください。"
    //         },
    //         {
    //             "title": "英語翻訳",
    //             "prompt": "以下の文章を英語に翻訳してください。"
    //         },
    //     ]
    // },

    {
        "submenu_title": "理解を深める",
        "submenu": [
            {
                "title": "論点を整理する",
                "prompt": "以下の論点を整理してまとめて。そしてその整理から示唆を見出して。"
            },
            {
                "title": "論理の抜け漏れを探す",
                "prompt": "あなたは「悪魔の代弁者」として、以下の主張・設計・計画を批判的に鋭く見てください。論理の穴・抜け・漏れ・飛躍、リスクなどを全力で探して指摘してください。遠慮は不要ですが、批判のためにこじつけたような論理性に欠けた主張にならないように注意してください。"
            },
            {
                "title": "要約する",
                "prompt": "分かりやすさを重視して要約して。専門的な用語には注釈を入れること。"
            },
            {
                "title": "物語対話形式で解説",
                "prompt": "以下の内容について物語対話形式で解説して。登場人物は「AI先生」(女性人格)と「生徒」(男の子人格)とする。"
            },
        ]
    },
    {
        "submenu_title": "翻訳",
        "submenu": [
            {
                "title": "日本語に翻訳",
                "prompt": `以下の入力文章を日本語に翻訳して。直訳的にせず、自然な表現にすること。入力文章はどのような内容・段落構造であっても要約・除外・集約などの整形を行わず、全体構造を維持したまま確実に全文を翻訳すること。

出力形式は以下とすること:
[訳]
翻訳文...`
            },
            {
                "title": "英語に翻訳",
                "prompt": `以下の入力文章を英語に翻訳して。フォーマルとカジュアルの中間的なニュアンスで翻訳すること。入力文章はどのような内容・段落構造であっても要約・除外・集約などの整形を行わず、全体構造を維持したまま確実に全文を翻訳すること

出力形式は以下とすること:
[Translation]
翻訳文...`
            },
        ]
    },
    {
        "submenu_title": "語学",
        "submenu": [
            {
                "title": "英語 文法解説",
                "prompt": `あなたは母国語が英語の英語教師です。
次の英文を以下の構成で解説してください。

構成:
(1) 日本語翻訳
(2) スラッシュリーディング + 各句の文型(S/V/O/C)と語訳
(3) 重要文法
(4) 補足

注意:
- 補足では日本語話者には分かりづらい表現や慣例表現などの解説を加える
- スラッシュリーディングで区切る際「M (修飾語)」が連なって長くなりすぎる場合には5~7語程度を目安に分ける
- 「M」の種類として形容詞、副詞、形容詞句、副詞句、分詞句、形容詞節、副詞節がある

(2) の出力例:
They / elected / him / president / in 2020.
- They: (S) 彼らは
- elected: (V) 選出した
- him: (O) 彼を
- president: (C) 大統領
- in 2020: (M: 副詞句) 2020年に
`

            },
        ]
    },
    {
        "submenu_title": "プログラミング支援",
        "submenu": [
            {
                "title": "コードを解説する",
                "prompt": "以下のコードを初めて読む人も理解できるように解説してください。私には言語の基本理解はあるものとし、コードの意図を理解するための情報を中心に説明してください。"
            },
            {
                "title": "コードを洗練させる",
                "prompt": "以下のコードを洗練させてください"
            },
            {
                "title": "コードをレビューする",
                "prompt": "以下のコードをセキュリティと保守性の観点でレビューしてください"
            },
            {
                "title": "エラーログをデバッグする",
                "prompt": "以下のエラーログを解析し、考えられる原因と対策を提示してください"
            },
            {
                "title": "SQLクエリを最適化する",
                "prompt": "以下のSQL文を最適化してください"
            },
            {
                "title": "PowerShell スクリプトを書く",
                "prompt": "あなたはエキスパートとしてメンテナンス性の高いコードを書くのが得意です。以下の処理を行うPowerShellスクリプトを生成してください"
            },
            {
                "title": "Shell Script を書く",
                "prompt": "あなたはエキスパートとしてメンテナンス性の高いコードを書くのが得意です。以下の処理を行うShell Scriptを生成してください"
            },
        ]
    },
    {
        "submenu_title": "ビジネス文書支援",
        "submenu": [
            {
                "title": "音声文字起こし文を整形する",
                "prompt": `以下は、音声文字起こしの文章です。以下のルールに従って、文章を整形してください。

整形ルール:
- 要約するなどといった文章の全体感を損なう修正はしない
- 会話の流れから不自然と思われる単語には、対象単語の直後に誤字の可能性を示唆する [*] を付ける
- 誤字の可能性を示唆する [*] が付与された場合のみ、出力の末尾に「[*]: 誤字の可能性ありと凡例を書き添える
- 助詞や句読点など文章上の不適合を正す
- フィラーワードを除外する`
            },
            {
                "title": "誤字脱字、不正確な表現をチェック",
                "prompt": "以下の2つのカテゴリーで問題点を指摘してください\n"
                    + "- 【誤字脱字】:明確な誤字脱字および誤字の疑いがある箇所\n"
                    + "- 【不正確・曖昧な表現】:解釈に個人差が生じる可能性がある表現、より適切な表現に改善できる箇所"
            },
            {
                "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": "以下の目標に対して、通常とは逆の視点からアプローチする方法を3つ提案してください"
            },
            {
                "title": "競合分析からの着想",
                "prompt": "以下の内容について、業界における競合他社の特徴を分析し、差別化できる新しいアプローチを提案してください"
            },
            {
                "title": "ユースケースの列挙",
                "prompt": "以下の製品・サービスについて、想定されるユースケースを10個列挙し、それぞれの価値提案を記載してください"
            },
        ]
    },
]

// 生成AIの回答の言語設定
// "Japanese" や "English" を設定できます。他の言語は未検証。
// 自動設定にする場合は空文字 "" を設定してください。
const LANGUAGE = "Japanese";

// 入力プロンプトの最大トークン数
// 超適当なトークナイザー関数により計測し、英単語は4文字1トークン、それ以外は1文字1トークンとして数えます。
const MAX_PROMPT_TOKEN = 6000;

// 会話の履歴を保持する数 (質問と回答で1セット)
// 数を増やすと、会話の履歴が増えますが、Meryのメモリ使用量が増えたり、
// 使用トークン数が増えて動作が重くなる可能性があるため、適宜調整してください。
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。
const HISTORY_SIZE = 1;

// 会話履歴に保持する最大トークン数
// 会話履歴のトークン数がこの数を超える場合、古い履歴から削除されます。
const MAX_HISTORY_TOKEN = 10000;

// コンテキストメニュー番号の定義
const EXECUTE_AI = 1;
const RESET_HISTORY = 2;
const SET_CUSTOM_INSTRUCTION = 3;
const REEXECUTE_LAST_TEMPLATE_PROMPT = 4;
const INVALID_MENU = 9;
const TEMPLATE_PROMPT_THRESHOLD = 10;
const TEMPLATE_PROMPT_SUBMENU_THRESHOLD = 1000;

// タグ名の定義
const HISTORY_TAG = "groq-history";
const TEMPORARY_CUSTOM_INSTRUCTION_TAG = "groq-temporary-custom-instruction";
const PREVIOUS_TEMPLATE_PROMPT_TAG = "groq-previous-template-prompt";

// 生成AIモデル別設定

const MODEL_CONFIG = {
    model: "openai/gpt-oss-120b",
    max_completion_tokens: 3072,
    temperature: 0.7,
    reasoning_effort: "low",
}

// const MODEL_CONFIG = {
//     model: "qwen/qwen3.8-27b",
//     max_completion_tokens: 3072,
//     temperature: 0.7,
//     reasoning_effort: "medium",
// }

// console.log に出力する内容を Mery のアウトプットバーに表示する
const console = {
    log: function (str) {
        outputBar.Writeln(str)
    }
}

const doc = document;
const sel = document.selection;

main();

function main() {
    const selectionIsEmpty = sel.IsEmpty

    // プロンプトの取得
    if (selectionIsEmpty) {
        sel.StartOfLine(false, mePosLogical);
        sel.EndOfLine(true, mePosLogical);
    }
    let promptText = sel.Text.trim();
    let menuTrack = 0;
    while (true) {
        // コンテキストメニューの作成
        const menu = createContextMenu(selectionIsEmpty);
        menuTrack = menu.Track(0);
        // メニュー選択がキャンセルされた場合は終了
        if (menuTrack === 0) return;

        // 無効なメニューの場合は無視して再表示
        if (menuTrack === INVALID_MENU) {
            continue;
        }

        // 会話履歴のリセット
        if (menuTrack === RESET_HISTORY) {
            resetTag();
            continue;
        }

        // カスタムインストラクションの設定
        if (menuTrack === SET_CUSTOM_INSTRUCTION) {
            configureTemporaryCustomInstruction();
            return;
        }

        break;
    }

    // プロンプトが空の場合はエラー
    if (promptText.length === 0) {
        alert("プロンプトを入力してください。");
        return;
    }
    // 文字数が多すぎる場合はエラー
    if (roughTokenize(promptText) > MAX_PROMPT_TOKEN) {
        alert("プロンプトの文字数が多すぎます。文字数を少なくしてください。");
        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;
    if (isTemplate) {
        const isSubmenu = menuTrack >= TEMPLATE_PROMPT_SUBMENU_THRESHOLD;
        let instructionText = "";
        if (isSubmenu) {
            const parentMenuTrack = Number(String(menuTrack).slice(0, 2));
            const subMenuTrack = Number(String(menuTrack).slice(2));
            instructionText = PROMPT_TEMPLATE[parentMenuTrack - TEMPLATE_PROMPT_THRESHOLD].submenu[subMenuTrack].prompt;
        } else {
            instructionText = PROMPT_TEMPLATE[menuTrack - TEMPLATE_PROMPT_THRESHOLD].prompt;
        }
        const randomId = generateRandomId();
        promptText = `
<INSTRUCTIONS_${randomId}>
${instructionText}
</INSTRUCTIONS_${randomId}>
<INPUTDATA_${randomId}>
${promptText}
</INPUTDATA_${randomId}>
        `.trim();
    }

    // テンプレートの場合は直前に実行したプロンプトテンプレートの番号としてタグに保存
    if (isTemplate) {
        addPreviousTemplatePromptNumberToTag(menuTrack);
    }

    try {
        // 処理終了を待機
        shell.KeepRunning = true;

        // カーソル位置移動
        sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical));
        sel.EndOfLine(false, mePosLogical);
        if (sel.GetActivePointX(mePosLogical) > 1) sel.NewLine();

        ShowTip("リクエスト中.....", meShowTipPosCaret);

        // APIキーの取得
        const apiKey = shell.getEnv("GROQ_API_KEY");

        // groq API に問い合わせ
        sel.Text = "\n---\n\n"
        document.KeepScrollPos = true;
        callApi(apiKey, promptText, isTemplate);
        if (sel.GetActivePointX(mePosLogical) > 2) {
            sel.Text = "\n\n---\n\n\n"
        } else {
            sel.Text = "\n---\n\n\n"
        }
    } catch (e) {
        sel.Text = "<処理中にエラー発生>\n" + e.message + "\n" + e.stack;
    } finally {
        // 処理終了待機を解除
        shell.KeepRunning = false;
        ShowTip("", meShowTipHide);
    }
}

function createContextMenu(selectionIsEmpty) {
    const menu = CreatePopupMenu();
    menu.Add("※注意※ プライバシー・機密情報を送信しないこと", INVALID_MENU);
    menu.Add("", 0, meMenuSeparator);
    let t = "";
    if (selectionIsEmpty) {
        t = "アクティブ行の内容で相談 (&A)";
    } else {
        t = "選択範囲の内容で相談 (&A)";
    }
    menu.Add(t, EXECUTE_AI);
    if (HISTORY_SIZE <= 0) {
        menu.Add("├ 会話履歴をクリア (履歴が無効になっています)", RESET_HISTORY, meMenuGrayed);
    } else 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();
        let previousMenuString = "";
        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) + ")";
                    }
                    // ES5 compatible version without padStart
                    const subMenuNum = Number(subMenuPrefix + (j < 10 ? "0" + j : String(j)));
                    subSubMenu.Add(subMenuList[j].title + subAccelerationKey, subMenuNum);
                    if (subMenuNum === doc.tag[PREVIOUS_TEMPLATE_PROMPT_TAG]) {
                        previousMenuString = subMenuList[j].title;
                    }
                }
                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);
            menu.Add(`    (直前実行: ${previousMenuString})`, REEXECUTE_LAST_TEMPLATE_PROMPT);
        } else {
            menu.Add(t, REEXECUTE_LAST_TEMPLATE_PROMPT, meMenuGrayed);
        }
    }

    return menu;
}

function escapeRequestBodyForCRuntimeParser(requestBody) {
    // Cランタイムパーサーに渡すためのエスケープ処理
    return JSON.stringify(requestBody)
        .replace(/((?:\\)*)"/g, function (match, p1) {
            if (p1) {
                const backslashCount = p1.length;
                let backslash = "";
                backslash = "\\".repeat(backslashCount);
                return backslash + "\\" + match;
            } else {
                return "\\" + match;
            }
        });
}

function callApi(apiKey, promptText, isTemplate) {
    try {
        // fetch で Groq API にリクエスト
        const requestBody = {
            messages: createRequestMessages(promptText, isTemplate),
            stream: false,
        };
        for (let key in MODEL_CONFIG) {
            requestBody[key] = MODEL_CONFIG[key];
        }
        if (isTemplate) {
            // テンプレートの場合は応答を決定論的にする
            // テンプレート以外の場合はデフォルト値とするために指定しない
            requestBody.temperature = 0.3;
        }
        const url = 'https://api.groq.com/openai/v1/chat/completions';

        const requestJson = escapeRequestBodyForCRuntimeParser(requestBody);
        // curl で Groq API にリクエスト
        const command = "curl -s -X POST " + url
            + ' -H "Content-Type: application/json"'
            + ' -H "Authorization: Bearer ' + apiKey + '"'
            + ' -d "' + requestJson + '"';
        const comResult = shell.Exec(command);

        if (comResult.ExitCode != 0) {
            sel.Text = "リクエストでエラーが発生しました。\n"
                + "  エラーコード: " + comResult.ExitCode + "\n"
                + "  標準出力: " + (comResult.StdOut.trim() ? comResult.StdOut.trim() : "-") + "\n"
                + "  エラー出力: " + (comResult.StdErr.trim() ? comResult.StdErr.trim() : "-");
            return;
        }
        const responseRaw = comResult.StdOut;

        const response = JSON.parse(responseRaw);

        let ret = "";
        const choices = response.choices;
        if (!choices) {
            sel.Text = "リクエストでエラーが発生しました。\n"
                + "  標準出力: " + (comResult.StdOut.trim() ? comResult.StdOut.trim() : "-") + "\n"
                + "  エラー出力: " + (comResult.StdErr.trim() ? comResult.StdErr.trim() : "-");
            return;
        }

        if (choices && choices.length > 0) {
            choices.forEach(function (choice) {
                const message = choice.message;
                if (message) {
                    ret += message.content;
                }
            });
        }

        const usage = response.usage;
        let usageText = "";
        if (usage) {
            usageText += "\n\n(利用トークン数: プロンプト=" + usage.prompt_tokens
                + ", 応答=" + usage.completion_tokens
                + ", 合計=" + usage.total_tokens + ")";
        }

        const formattedOutput = formatOutput(ret);

        sel.Text = formattedOutput + usageText;

        // 結果を会話履歴に追加
        if (HISTORY_SIZE > 0 && !isTemplate) {
            appendReturnTextToTag(promptText, ret);
        }
    } catch (e) {
        throw e;
    }
}

function formatOutput(text) {
    // Markdownを整形する
    text = formatMarkdown(text);
    return text
        // クォーテーションの正規化
        .replace(/[“”]/g, '"')
        .replace(/[‘’]/g, "'")
        // 行末のスペースを削除
        .replace(/[ \t]+$/gm, '')
        // 1️⃣ などの数字を半角に変換
        .replace(/([1-9#*]+)\ufe0f\u20e3/g, '($1)')
        ;
}

/**
 * Markdown を整形する(ES5 対応)
 *  1. コードブロック ```~``` を保護して整形対象から除外
 *  2. 見出し行の前後に空行を 1 行ずつ確保
 *  3. **太字** / *斜体* / `コード` の外側に半角スペースを 1 つ確保
 * @param  {string} md  元の Markdown
 * @return {string}     整形後 Markdown
 */
function formatMarkdown(md) {
    /* === 0. コードブロックをプレースホルダーへ退避 ============== */
    const codeBlocks = [];      // 元のコードブロックを格納
    let body = md.replace(/```[\s\S]*?```/g, function (match) {
        const key = '@@@CODE@BLOCK@' + codeBlocks.length + '@@@';
        codeBlocks.push(match);
        return key;             // プレースホルダーを本文に残す
    });

    /* === 見出しの前後に空行を挿入 ============================ */

    /* 見出し直前に空行(先頭行以外で空行が無い場合) */
    body = body.replace(
        /([^\r\n])(\r?\n)(#{1,6}\s)/g,
        function (_, prev, br, head) { return prev + br + '\n' + head; }
    );

    /* 見出し直後に空行(既に空行が無い場合) */
    body = body.replace(
        /^(#{1,6}\s[^\n]+?)(\r?\n)(?!\r?\n)/gm,
        '$1$2\n'
    );

    /* === コードブロックを復元 ================================= */
    body = body.replace(/@@@CODE@BLOCK@(\d+)@@@/g, function (_, idx) {
        return codeBlocks[Number(idx)];
    });

    return body;
}

/**
 * 斜体 (*…* / _…_) の外側にスペースを追加しつつ
 * **太字** の中へ誤マッチしないよう制御
 */
function surroundItalic(text) {
    /* *italic* */
    text = text.replace(
        /(^|[^*])(\*(?![*\s])[^*\n]*?[^*\n\s]\*(?!\*))(($)|[^*])/gm,
        function (_, pre, core, post) {
            pre = pre === '' || pre.match(/\s/) ? pre : pre + ' ';
            post = post === '' || post.match(/\s/) ? post : ' ' + post;
            return pre + core + post;
        }
    );
    /* _italic_ */
    text = text.replace(
        /(^|[^_])(\_(?![_\s])[^_\n]*?[^_\n\s]\_(?!\_))(($)|[^_])/gm,
        function (_, pre, core, post) {
            pre = pre === '' || pre.match(/\s/) ? pre : pre + ' ';
            post = post === '' || post.match(/\s/) ? post : ' ' + post;
            return pre + core + post;
        }
    );
    return text;
}

function roughTokenize(text) {
    // 超適当なトークナイザ。トークン数をざっくりと多めに数える
    // 空白文字・ハイフン・アンダースコア区切りの単語に分割して、
    // 単語がアルファベットのみ、数字のみの場合には 4文字=1トークン として数える。
    // 単語にアルファベット以外の文字が含まれる場合は 1文字=1トークン として数える。

    const words = text.split(/[\s\-_]+/);
    let totalTokens = 0;

    for (let i = 0; i < words.length; i++) {
        const word = words[i];
        if (!word) continue; // 空の単語をスキップ

        if (/^[a-zA-Z]+$/.test(word) || /^[0-9]+$/.test(word)) {
            totalTokens += Math.ceil(word.length / 4);
        } else {
            totalTokens += word.length;
        }
    }

    return totalTokens;
}

// document.Tag の内容を加味してgroqのリクエストを作成
function createRequestMessages(promptText, isTemplate) {
    const customInstructionText = buildCustomInstructionText();
    // テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない
    if (isTemplate) {
        const request = [
            {
                "role": "user",
                "content": [{ "type": "text", "text": promptText }]
            }
        ];
        if (customInstructionText) {
            request.unshift({
                "role": "system",
                "content": customInstructionText
            })
        }
        return request;
    }

    const tag = document.Tag;

    // 以前の会話が存在しない場合は新規リクエスト
    if (!tag.exists(HISTORY_TAG)) {
        const request = [{
            "role": "user",
            "content": [{ "type": "text", "text": promptText }]
        }];
        if (customInstructionText) {
            request.unshift({
                "role": "system",
                "content": customInstructionText
            })
        }
        return request;
    }

    // 会話履歴が存在する場合は、履歴を含めてリクエスト
    const request = JSON.parse(tag[HISTORY_TAG]);
    if (customInstructionText) {
        request.unshift({
            "role": "system",
            "content": customInstructionText
        });
    }
    request.push({
        "role": "user",
        "content": promptText
    });
    return request;
}

// document.Tag にgroqの応答を追加
function appendReturnTextToTag(userText, modelText) {
    const tag = document.Tag;
    if (!tag.exists(HISTORY_TAG)) {
        tag[HISTORY_TAG] = JSON.stringify([
            {
                "role": "user",
                "content": userText
            },
            {
                "role": "assistant",
                "content": modelText
            }
        ]);
    } else {
        const groqText = JSON.parse(tag[HISTORY_TAG]);
        groqText.push({
            "role": "user",
            "content": userText
        });
        groqText.push({
            "role": "assistant",
            "content": modelText
        });
        // 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除
        if (groqText.length > HISTORY_SIZE * 2) {
            groqText.splice(0, 2);
        }
        // 会話履歴全体が MAX_HISTORY_TOKEN を超えた場合、最初のセットを削除
        let joinedText = "";
        for (let i = 0; i < groqText.length; i++) {
            joinedText += groqText[i].content + "\n";
        }

        while (roughTokenize(joinedText) > MAX_HISTORY_TOKEN) {
            groqText.splice(0, 2);
            joinedText = "";
            for (let i = 0; i < groqText.length; i++) {
                joinedText += groqText[i].content + "\n";
            }
        }

        tag[HISTORY_TAG] = JSON.stringify(groqText);
    }
}

function resetTag() {
    const tag = doc.Tag;
    if (tag.exists(HISTORY_TAG)) {
        tag.remove(HISTORY_TAG);
    }
}

function getLocalIsoDateTimeString() {
    const now = new Date();
    const pad = function (num) {
        return (num < 10 ? "0" : "") + num;
    };

    const year = now.getFullYear();
    const month = pad(now.getMonth() + 1);
    const day = pad(now.getDate());
    const hours = pad(now.getHours());
    const minutes = pad(now.getMinutes());
    const seconds = pad(now.getSeconds());

    const offsetMinutes = now.getTimezoneOffset();
    const offsetSign = offsetMinutes <= 0 ? "+" : "-";
    const absOffsetHours = pad(Math.floor(Math.abs(offsetMinutes) / 60));
    const absOffsetMinutes = pad(Math.abs(offsetMinutes) % 60);

    const isoLikeLocal = year + "-" + month + "-" + day
        + "T" + hours + ":" + minutes + ":" + seconds
        + offsetSign + absOffsetHours + ":" + absOffsetMinutes;
    return isoLikeLocal;
}

function baseCustomInstruction() {
    const language = LANGUAGE.trim();
    return "The current date and time for this user is " + getLocalIsoDateTimeString() + ". You cannot know for sure what happens after the knowledge cut-off date."
        + (language ? "Respond in " + language + "\n" : "")
        + "Do not hallucinate.\n"
        + "**Start with the main point of the question without explaining or declaring your writing style or tone.**\n"
        + "**At the end of your answer, avoid adding formulaic phrases that prompt the next question or unrelated conversation that is not directly related to your answer.**\n"
    // + "Do NOT EVER use full-width brackets \"()\", but use half-width brackets \"()\". Enclose the outside of the half-width brackets with half-width spaces.\n"
    // + "When using Markdown formatting, you MUST follow these rules:\n"
    // + "- Rule 1: Insert a blank line below the header line (A header line begins with one or more # characters).\n"
    // + "- Rule 2: Insert spaces before and after bold (surrounded by **), italic (surrounded by *), and inline code (surrounded by `). For example: \"私は **元気** です。\"\n"
}

function buildCustomInstructionText() {
    let customInstruction = getTemporaryCustomInstruction();
    if (!customInstruction) {
        // 一時的なカスタムインストラクションがない場合はデフォルトを使用
        customInstruction = DEFAULT_CUSTOM_INSTRUCTION;
    }

    customInstruction = customInstruction.replace(/^[\s ]+|[\s ]+$/g, "");

    return baseCustomInstruction().trim()
        + (customInstruction ? "\n" + customInstruction : "");
}

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 if (prompt !== null) {
        // 空文字でOKされた場合はデフォルトに戻すか確認
        if (prompt === "") {
            if (Confirm("カスタムインストラクションをデフォルトに戻しますか?\n※カスタムインストラクションを空にしたい場合は [キャンセル] を選択してください。")) {
                setTemporaryCustomInstruction("");
            } else {
                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 {
        if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) {
            tag.remove(TEMPORARY_CUSTOM_INSTRUCTION_TAG);
        }
    }
}

function addPreviousTemplatePromptNumberToTag(menuTrack) {
    const tag = doc.Tag;
    tag[PREVIOUS_TEMPLATE_PROMPT_TAG] = menuTrack;
}

function inputProgressChar(pos) {
    sel.SetActivePos(pos);
    sel.Text = ".";
}

function generateRandomId(length = 16) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    result += chars[Math.floor(Math.random() * chars.length)];
  }
  return result;
}

変更履歴[編集]

  • 2026-08-31 Kimi K2 0905 が提供終了となっていたため、モデルを openai/gpt-oss-120b に変更。その他諸々調整
  • 2025-12-06 #language = "quickjs" 指定を追加。各種リファクタリング
  • 2025-12-02 curl実行時のリクエスト文字列エスケープ処理を調整
  • 2025-12-01 curl実行時のリクエスト文字列エスケープ処理を調整
  • 2025-11-29 デフォルトカスタム指示を調整。Markdown出力の整形処理を調整など
  • 2025-09-12 Kimi K2 0905 モデルに対応。Markdown出力の整形処理を追加
  • 2025-04-13 初版
スポンサーリンク