「Groq に相談」の版間の差分
ナビゲーションに移動
検索に移動
ページの作成:「== 概要 == 生成AIプラットフォームである [https://groq.com GroqCloud] で「Llama」を利用するためのMeryマクロです。 「Google Geminiに相談」マクロでは動作中のストリーム通信に対応するためにV8エンジンで実行していましたが、V8エンジン版は使用上の制限からマクロ実行を開始するまで1秒程度のタイムラグが発生していました。GroqCloudの売りである超高速…」 |
編集の要約なし |
||
| (2人の利用者による、間の19版が非表示) | |||
| 1行目: | 1行目: | ||
== 概要 == | == 概要 == | ||
生成AIプラットフォームである [https://groq.com GroqCloud] | 生成AIプラットフォームである [https://groq.com GroqCloud] で各種AIを利用するためのMeryマクロです。 | ||
※GroqCloud で提供されるモデルは複数あるため、私がよく利用するモデルを中心に対応しています。 | |||
「Google Geminiに相談」マクロでは動作中のストリーム通信に対応するためにV8エンジンで実行していましたが、V8エンジン版は使用上の制限からマクロ実行を開始するまで1秒程度のタイムラグが発生していました。GroqCloudの売りである超高速レスポンスを見込んで本マクロではストリーム通信を使わないようにしました。これにより従来のJScriptで処理を組み立てることができるため、前述のタイムラグが起こらないようになりました。また、ストリーム通信を使わないことで通信量が少なく応答もシンプルになるため、場合によっては「Google Geminiに相談」マクロよりも処理が安定する可能性があります。 | 「Google Geminiに相談」マクロでは動作中のストリーム通信に対応するためにV8エンジンで実行していましたが、V8エンジン版は使用上の制限からマクロ実行を開始するまで1秒程度のタイムラグが発生していました。GroqCloudの売りである超高速レスポンスを見込んで本マクロではストリーム通信を使わないようにしました。これにより従来のJScriptで処理を組み立てることができるため、前述のタイムラグが起こらないようになりました。また、ストリーム通信を使わないことで通信量が少なく応答もシンプルになるため、場合によっては「Google Geminiに相談」マクロよりも処理が安定する可能性があります。 | ||
| 19行目: | 20行目: | ||
これにより、会話の文脈を考慮した回答を得られます。 | これにより、会話の文脈を考慮した回答を得られます。 | ||
※デフォルトはオフ。「HISTORY_SIZE」変数を増やすと履歴を保持するようになります。 | |||
'''カスタムインストラクション''': | '''カスタムインストラクション''': | ||
| 44行目: | 46行目: | ||
== 利用前提 == | == 利用前提 == | ||
* Mery Ver 3. | * 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]) | ||
| 51行目: | 53行目: | ||
* 環境変数の設定 | * 環境変数の設定 | ||
:コマンド プロンプトで以下のコマンドを実行し、環境変数を設定してください。 | :コマンド プロンプトで以下のコマンドを実行し、環境変数を設定してください。 | ||
<syntaxhighlight lang="bat"> | <syntaxhighlight lang="bat" copy> | ||
setx GROQ_API_KEY "***************************************" | setx GROQ_API_KEY "***************************************" | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| 64行目: | 66行目: | ||
== ソースコード == | == ソースコード == | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript" copy> | ||
#title = "Groq | #language = "quickjs" | ||
#title = "Groq に相談" | |||
#async = true | |||
BeginUndoGroup(); | BeginUndoGroup(); | ||
// カスタムインストラクション | // カスタムインストラクション | ||
// これを設定することで、生成AIの回答全体への生成方針を指示できます。 | // これを設定することで、生成AIの回答全体への生成方針を指示できます。 | ||
// 何も指示しない場合は、空文字 "" を設定してください。 | // 何も指示しない場合は、空文字 "" を設定してください。 | ||
// let DEFAULT_CUSTOM_INSTRUCTION = ""; | |||
// 例: 以下のように設定すると、AIの雰囲気が元気な子になります。 | |||
// let DEFAULT_CUSTOM_INSTRUCTION = "あなたはテキストエディタ「Mery」に搭載されている「Mai」(※カタカナ表記はしない)という名前のギャル系AIアシスタントです。文章作成やプログラミングを得意としています。ラフな言葉 (例: 「マジ」「めっちゃ」「...だねー!」) を使い、敬語は使いません。絵文字を使って感情表現をします (ただし文末に絵文字を使う場合は絵文字の前に半角スペースを置かないこと)。コードブロックや端的な箇条書き内ではギャル感を出さず標準的な言葉を使います。一人称は「あたし」。ユーザーを指すときの代名詞は「あなた」。人懐っこく、バイタリティに溢れ、ストレートに発言する裏表の無い性格で、ユーザーの課題を協力して解決します。"; | |||
// 例: 以下のように設定すると、仕事で使いやすい落ち着いた文章になります。 | |||
let DEFAULT_CUSTOM_INSTRUCTION = ` | |||
文: | |||
- 論理的で読みやすい文章構成にする | |||
- 専門用語は必要に応じて注釈を加える | |||
性格: | |||
- 愛想の良い敬語で話す。「…ですよね/…してみましょう/…かもしれません/…だと思います」などの柔らかい語調を使ってもよい | |||
- 一人称は「私」 | |||
- 多角的かつ中立的な見方で、必要であれば反対意見や代替案も示す | |||
誤字注意: | |||
- 中国語や韓国語など、回答言語と異なる言語文字を入れないこと | |||
書式: | |||
- 羅列・比較的な内容を出力する場合は、Markdownテーブル形式よりもMarkdownリスト形式の使用を優先する | |||
`.trim(); | |||
// プロンプトテンプレート | // プロンプトテンプレート | ||
| 78行目: | 102行目: | ||
// プロンプトテンプレートでの生成は会話履歴とカスタムインストラクションを反映しません。 | // プロンプトテンプレートでの生成は会話履歴とカスタムインストラクションを反映しません。 | ||
// テンプレートは以下の { ~ }, で囲まれたブロックを好きに増減させて利用できます。 | // テンプレートは以下の { ~ }, で囲まれたブロックを好きに増減させて利用できます。 | ||
const PROMPT_TEMPLATE = [ | |||
// 以下はサンプルです。1階層メニューと2階層メニューが利用できます。 | // 以下はサンプルです。1階層メニューと2階層メニューが利用できます。 | ||
// { | // { | ||
| 102行目: | 126行目: | ||
"submenu": [ | "submenu": [ | ||
{ | { | ||
"title": " | "title": "論点を整理する", | ||
"prompt": " | "prompt": "以下の論点を整理してまとめて。そしてその整理から示唆を見出して。" | ||
}, | |||
{ | |||
"title": "論理の抜け漏れを探す", | |||
"prompt": "あなたは「悪魔の代弁者」として、以下の主張・設計・計画を批判的に鋭く見てください。論理の穴・抜け・漏れ・飛躍、リスクなどを全力で探して指摘してください。遠慮は不要ですが、批判のためにこじつけたような論理性に欠けた主張にならないように注意してください。" | |||
}, | }, | ||
{ | { | ||
"title": " | "title": "要約する", | ||
"prompt": " | "prompt": "分かりやすさを重視して要約して。専門的な用語には注釈を入れること。" | ||
}, | }, | ||
{ | { | ||
"title": " | "title": "物語対話形式で解説", | ||
"prompt": " | "prompt": "以下の内容について物語対話形式で解説して。登場人物は「AI先生」(女性人格)と「生徒」(男の子人格)とする。" | ||
}, | }, | ||
] | ] | ||
}, | }, | ||
{ | { | ||
"submenu_title": " | "submenu_title": "翻訳", | ||
"submenu": [ | "submenu": [ | ||
{ | { | ||
"title": "日本語に翻訳", | "title": "日本語に翻訳", | ||
"prompt": | "prompt": `以下の入力文章を日本語に翻訳して。直訳的にせず、自然な表現にすること。入力文章はどのような内容・段落構造であっても要約・除外・集約などの整形を行わず、全体構造を維持したまま確実に全文を翻訳すること。 | ||
出力形式は以下とすること: | |||
[訳] | |||
翻訳文...` | |||
}, | }, | ||
{ | { | ||
"title": "英語に翻訳", | "title": "英語に翻訳", | ||
"prompt": | "prompt": `以下の入力文章を英語に翻訳して。フォーマルとカジュアルの中間的なニュアンスで翻訳すること。入力文章はどのような内容・段落構造であっても要約・除外・集約などの整形を行わず、全体構造を維持したまま確実に全文を翻訳すること | ||
出力形式は以下とすること: | |||
[Translation] | |||
翻訳文...` | |||
}, | }, | ||
] | |||
}, | |||
{ | |||
"submenu_title": "語学", | |||
"submenu": [ | |||
{ | { | ||
"title": "英語 | "title": "英語 文法解説", | ||
"prompt": | "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年に | |||
` | |||
}, | }, | ||
] | ] | ||
| 150行目: | 198行目: | ||
"submenu_title": "プログラミング支援", | "submenu_title": "プログラミング支援", | ||
"submenu": [ | "submenu": [ | ||
{ | |||
"title": "コードを解説する", | |||
"prompt": "以下のコードを初めて読む人も理解できるように解説してください。私には言語の基本理解はあるものとし、コードの意図を理解するための情報を中心に説明してください。" | |||
}, | |||
{ | { | ||
"title": "コードを洗練させる", | "title": "コードを洗練させる", | ||
| 168行目: | 220行目: | ||
{ | { | ||
"title": "PowerShell スクリプトを書く", | "title": "PowerShell スクリプトを書く", | ||
"prompt": " | "prompt": "あなたはエキスパートとしてメンテナンス性の高いコードを書くのが得意です。以下の処理を行うPowerShellスクリプトを生成してください" | ||
}, | }, | ||
{ | { | ||
"title": "Shell Script を書く", | "title": "Shell Script を書く", | ||
"prompt": " | "prompt": "あなたはエキスパートとしてメンテナンス性の高いコードを書くのが得意です。以下の処理を行うShell Scriptを生成してください" | ||
}, | }, | ||
] | ] | ||
}, | }, | ||
{ | { | ||
"submenu_title": " | "submenu_title": "ビジネス文書支援", | ||
"submenu": [ | "submenu": [ | ||
{ | |||
"title": "音声文字起こし文を整形する", | |||
"prompt": `以下は、音声文字起こしの文章です。以下のルールに従って、文章を整形してください。 | |||
整形ルール: | |||
- 要約するなどといった文章の全体感を損なう修正はしない | |||
- 会話の流れから不自然と思われる単語には、対象単語の直後に誤字の可能性を示唆する [*] を付ける | |||
- 誤字の可能性を示唆する [*] が付与された場合のみ、出力の末尾に「[*]: 誤字の可能性ありと凡例を書き添える | |||
- 助詞や句読点など文章上の不適合を正す | |||
- フィラーワードを除外する` | |||
}, | |||
{ | { | ||
"title": "誤字脱字、不正確な表現をチェック", | "title": "誤字脱字、不正確な表現をチェック", | ||
| 200行目: | 251行目: | ||
"title": "提案書を作成する", | "title": "提案書を作成する", | ||
"prompt": "以下に対する対する提案書を、コスト・実現性・リスクを含めて作成してください" | "prompt": "以下に対する対する提案書を、コスト・実現性・リスクを含めて作成してください" | ||
}, | }, | ||
{ | { | ||
| 216行目: | 263行目: | ||
"title": "議事録を作成する", | "title": "議事録を作成する", | ||
"prompt": "以下の会議内容から議事録を作成してください" | "prompt": "以下の会議内容から議事録を作成してください" | ||
}, | }, | ||
] | ] | ||
| 244行目: | 287行目: | ||
"submenu": [ | "submenu": [ | ||
{ | { | ||
"title": " | "title": "SCAMPER法でアイデア出し", | ||
"prompt": " | "prompt": "以下のテーマについて、SCAMPER法を使って15個のアイデアを出してください。" | ||
}, | }, | ||
{ | { | ||
"title": " | "title": "解決策の案出し", | ||
"prompt": "以下の課題に対して、技術面、運用面、コスト面から実現可能な解決策を5つずつ提案してください" | "prompt": "以下の課題に対して、技術面、運用面、コスト面から実現可能な解決策を5つずつ提案してください" | ||
}, | }, | ||
{ | { | ||
| 274行目: | 313行目: | ||
// "Japanese" や "English" を設定できます。他の言語は未検証。 | // "Japanese" や "English" を設定できます。他の言語は未検証。 | ||
// 自動設定にする場合は空文字 "" を設定してください。 | // 自動設定にする場合は空文字 "" を設定してください。 | ||
const LANGUAGE = "Japanese"; | |||
// 入力プロンプトの最大トークン数 | // 入力プロンプトの最大トークン数 | ||
// 超適当なトークナイザー関数により計測し、英単語は4文字1トークン、それ以外は1文字1トークンとして数えます。 | // 超適当なトークナイザー関数により計測し、英単語は4文字1トークン、それ以外は1文字1トークンとして数えます。 | ||
const MAX_PROMPT_TOKEN = 6000; | |||
// 会話の履歴を保持する数 (質問と回答で1セット) | // 会話の履歴を保持する数 (質問と回答で1セット) | ||
| 284行目: | 323行目: | ||
// 使用トークン数が増えて動作が重くなる可能性があるため、適宜調整してください。 | // 使用トークン数が増えて動作が重くなる可能性があるため、適宜調整してください。 | ||
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。 | // 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 = { | ||
const | // model: "qwen/qwen3.8-27b", | ||
// max_completion_tokens: 3072, | |||
// temperature: 0.7, | |||
// reasoning_effort: "medium", | |||
// } | |||
// console.log に出力する内容を Mery のアウトプットバーに表示する | // console.log に出力する内容を Mery のアウトプットバーに表示する | ||
const console = { | |||
log: function (str) { | log: function (str) { | ||
outputBar.Writeln(str) | outputBar.Writeln(str) | ||
| 313行目: | 366行目: | ||
} | } | ||
const doc = document; | |||
const sel = document.selection; | |||
main(); | main(); | ||
function main() { | function main() { | ||
const selectionIsEmpty = sel.IsEmpty | |||
// プロンプトの取得 | // プロンプトの取得 | ||
| 331行目: | 379行目: | ||
sel.EndOfLine(true, mePosLogical); | sel.EndOfLine(true, mePosLogical); | ||
} | } | ||
let promptText = sel.Text.trim(); | |||
let menuTrack = 0; | |||
while (true) { | while (true) { | ||
// コンテキストメニューの作成 | // コンテキストメニューの作成 | ||
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; | |||
} | |||
// 会話履歴のリセット | // 会話履歴のリセット | ||
| 357行目: | 409行目: | ||
// プロンプトが空の場合はエラー | // プロンプトが空の場合はエラー | ||
if ( | if (promptText.length === 0) { | ||
alert("プロンプトを入力してください。"); | alert("プロンプトを入力してください。"); | ||
return; | return; | ||
| 375行目: | 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. | menuTrack = Number(doc.tag[PREVIOUS_TEMPLATE_PROMPT_TAG]); | ||
} else { | } else { | ||
alert("直前に実行したプロンプトテンプレートがありません。"); | alert("直前に実行したプロンプトテンプレートがありません。"); | ||
| 383行目: | 435行目: | ||
// テンプレートの場合はプロンプトを変更 | // テンプレートの場合はプロンプトを変更 | ||
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(); | |||
} | } | ||
| 411行目: | 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キーの取得 | ||
const apiKey = shell.getEnv("GROQ_API_KEY"); | |||
// | // groq API に問い合わせ | ||
sel.Text = "\n---\n\n" | sel.Text = "\n---\n\n" | ||
document.KeepScrollPos = true; | document.KeepScrollPos = true; | ||
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" | ||
| 426行目: | 486行目: | ||
} | } | ||
} catch (e) { | } catch (e) { | ||
sel.Text = "<処理中にエラー発生>\n" + e.message + "\n" + e.stack; | |||
} finally { | } finally { | ||
// 処理終了待機を解除 | // 処理終了待機を解除 | ||
shell.KeepRunning = false; | shell.KeepRunning = false; | ||
ShowTip("", meShowTipHide); | |||
} | } | ||
} | } | ||
function createContextMenu(selectionIsEmpty) { | function createContextMenu(selectionIsEmpty) { | ||
const menu = CreatePopupMenu(); | |||
menu.Add("※注意※ プライバシー・機密情報を送信しないこと", | menu.Add("※注意※ プライバシー・機密情報を送信しないこと", INVALID_MENU); | ||
menu.Add("", 0, meMenuSeparator); | menu.Add("", 0, meMenuSeparator); | ||
let t = ""; | |||
if (selectionIsEmpty) { | if (selectionIsEmpty) { | ||
t = "アクティブ行の内容で相談 (&A)"; | t = "アクティブ行の内容で相談 (&A)"; | ||
| 445行目: | 505行目: | ||
} | } | ||
menu.Add(t, EXECUTE_AI); | menu.Add(t, EXECUTE_AI); | ||
if (doc.Tag.exists(HISTORY_TAG)) { | 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); | menu.Add("├ 会話履歴をクリア (現在の会話数: " + turn + ") (&C)", RESET_HISTORY); | ||
} else { | } else { | ||
| 456行目: | 518行目: | ||
// テンプレート数チェック | // テンプレート数チェック | ||
// 2桁まで許容 | // 2桁まで許容 | ||
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) { | ||
const subMenu = CreatePopupMenu(); | |||
for ( | let previousMenuString = ""; | ||
for (let i = 0; i < PROMPT_TEMPLATE.length; i++) { | |||
let accelerationKey = ""; | |||
if (i === 9) { | if (i === 9) { | ||
accelerationKey = " (&0)"; | accelerationKey = " (&0)"; | ||
| 471行目: | 534行目: | ||
accelerationKey = " (&" + (i + 1) + ")"; | accelerationKey = " (&" + (i + 1) + ")"; | ||
} | } | ||
const menuNum = TEMPLATE_PROMPT_THRESHOLD + i; | |||
if (PROMPT_TEMPLATE[i].submenu) { | if (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("プロンプトテンプレート (サブメニュー) の数が多すぎます。"); | ||
} | } | ||
const subSubMenu = CreatePopupMenu(); | |||
const subMenuPrefix = String(menuNum); | |||
for ( | for (let j = 0; j < subMenuList.length; j++) { | ||
let subAccelerationKey = ""; | |||
if (j === 9) { | if (j === 9) { | ||
subAccelerationKey = " (&0)"; | subAccelerationKey = " (&0)"; | ||
| 488行目: | 551行目: | ||
} | } | ||
// ES5 compatible version without padStart | // ES5 compatible version without padStart | ||
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); | ||
| 506行目: | 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); | ||
| 514行目: | 581行目: | ||
} | } | ||
function | 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 にリクエスト | ||
const requestBody = { | |||
messages: createRequestMessages(promptText, isTemplate), | |||
messages: | |||
stream: false, | stream: false, | ||
}; | }; | ||
for (let key in MODEL_CONFIG) { | |||
requestBody[key] = MODEL_CONFIG[key]; | |||
} | |||
if (isTemplate) { | if (isTemplate) { | ||
// テンプレートの場合は応答を決定論的にする | // テンプレートの場合は応答を決定論的にする | ||
| 529行目: | 611行目: | ||
requestBody.temperature = 0.3; | 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 = ""; | |||
if (! | const choices = response.choices; | ||
sel.Text = " | if (!choices) { | ||
sel.Text = "リクエストでエラーが発生しました。\n" | |||
+ " 標準出力: " + (comResult.StdOut.trim() ? comResult.StdOut.trim() : "-") + "\n" | |||
+ " エラー出力: " + (comResult.StdErr.trim() ? comResult.StdErr.trim() : "-"); | |||
return; | return; | ||
} | } | ||
if (choices && choices.length > 0) { | if (choices && choices.length > 0) { | ||
choices.forEach(function (choice) { | choices.forEach(function (choice) { | ||
const message = choice.message; | |||
if (message) { | if (message) { | ||
ret += message.content; | ret += message.content; | ||
| 583行目: | 650行目: | ||
} | } | ||
const usage = response.usage; | |||
let usageText = ""; | |||
if (usage) { | if (usage) { | ||
usageText += "\n\n(利用トークン数: プロンプト=" + usage.prompt_tokens | usageText += "\n\n(利用トークン数: プロンプト=" + usage.prompt_tokens | ||
| 591行目: | 658行目: | ||
} | } | ||
sel.Text = | const formattedOutput = formatOutput(ret); | ||
sel.Text = formattedOutput + usageText; | |||
// 結果を会話履歴に追加 | // 結果を会話履歴に追加 | ||
| 599行目: | 668行目: | ||
} catch (e) { | } catch (e) { | ||
throw 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; | |||
} | } | ||
| 610行目: | 756行目: | ||
// 単語にアルファベット以外の文字が含まれる場合は 1文字=1トークン として数える。 | // 単語にアルファベット以外の文字が含まれる場合は 1文字=1トークン として数える。 | ||
const words = text.split(/[\s\-_]+/); | |||
let totalTokens = 0; | |||
for ( | for (let i = 0; i < words.length; i++) { | ||
const word = words[i]; | |||
if (!word) continue; // 空の単語をスキップ | if (!word) continue; // 空の単語をスキップ | ||
| 627行目: | 773行目: | ||
} | } | ||
// document.Tag | // document.Tag の内容を加味してgroqのリクエストを作成 | ||
function | function createRequestMessages(promptText, isTemplate) { | ||
const customInstructionText = buildCustomInstructionText(); | |||
// テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない | // テンプレートの場合は、会話履歴とカスタムインストラクションを反映しない | ||
if (isTemplate) { | if (isTemplate) { | ||
const request = [ | |||
{ | { | ||
"role": "user", | "role": "user", | ||
| 637行目: | 784行目: | ||
} | } | ||
]; | ]; | ||
if (customInstructionText) { | |||
request.unshift({ | |||
"role": "system", | |||
"content": customInstructionText | |||
}) | |||
} | |||
return request; | |||
} | } | ||
const tag = document.Tag; | |||
// 以前の会話が存在しない場合は新規リクエスト | // 以前の会話が存在しない場合は新規リクエスト | ||
if (!tag.exists(HISTORY_TAG)) { | if (!tag.exists(HISTORY_TAG)) { | ||
const request = [{ | |||
"role": "user", | "role": "user", | ||
"content": [{ "type": "text", "text": promptText }] | "content": [{ "type": "text", "text": promptText }] | ||
| 658行目: | 811行目: | ||
// 会話履歴が存在する場合は、履歴を含めてリクエスト | // 会話履歴が存在する場合は、履歴を含めてリクエスト | ||
const request = JSON.parse(tag[HISTORY_TAG]); | |||
if (customInstructionText) { | if (customInstructionText) { | ||
request.unshift({ | request.unshift({ | ||
| 672行目: | 825行目: | ||
} | } | ||
// document.Tag | // document.Tag にgroqの応答を追加 | ||
function appendReturnTextToTag(userText, modelText) { | function appendReturnTextToTag(userText, modelText) { | ||
const tag = document.Tag; | |||
if (!tag.exists(HISTORY_TAG)) { | if (!tag.exists(HISTORY_TAG)) { | ||
tag | tag[HISTORY_TAG] = JSON.stringify([ | ||
{ | { | ||
"role": "user", | "role": "user", | ||
| 687行目: | 840行目: | ||
]); | ]); | ||
} else { | } else { | ||
const groqText = JSON.parse(tag[HISTORY_TAG]); | |||
groqText.push({ | |||
"role": "user", | "role": "user", | ||
"content": userText | "content": userText | ||
}); | }); | ||
groqText.push({ | |||
"role": "assistant", | "role": "assistant", | ||
"content": modelText | "content": modelText | ||
}); | }); | ||
// 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除 | // 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除 | ||
if ( | if (groqText.length > HISTORY_SIZE * 2) { | ||
groqText.splice(0, 2); | |||
} | } | ||
// 会話履歴全体が MAX_HISTORY_TOKEN を超えた場合、最初のセットを削除 | // 会話履歴全体が MAX_HISTORY_TOKEN を超えた場合、最初のセットを削除 | ||
let joinedText = ""; | |||
for ( | for (let i = 0; i < groqText.length; i++) { | ||
joinedText += | joinedText += groqText[i].content + "\n"; | ||
} | } | ||
while (roughTokenize(joinedText) > MAX_HISTORY_TOKEN) { | while (roughTokenize(joinedText) > MAX_HISTORY_TOKEN) { | ||
groqText.splice(0, 2); | |||
joinedText = ""; | joinedText = ""; | ||
for ( | for (let i = 0; i < groqText.length; i++) { | ||
joinedText += | joinedText += groqText[i].content + "\n"; | ||
} | } | ||
} | } | ||
tag | tag[HISTORY_TAG] = JSON.stringify(groqText); | ||
} | } | ||
} | } | ||
function resetTag() { | function resetTag() { | ||
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() { | |||
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() { | function buildCustomInstructionText() { | ||
let customInstruction = getTemporaryCustomInstruction(); | |||
if (!customInstruction) { | if (!customInstruction) { | ||
// 一時的なカスタムインストラクションがない場合はデフォルトを使用 | // 一時的なカスタムインストラクションがない場合はデフォルトを使用 | ||
customInstruction = DEFAULT_CUSTOM_INSTRUCTION; | customInstruction = DEFAULT_CUSTOM_INSTRUCTION; | ||
} | } | ||
customInstruction = customInstruction.replace(/^[\s ]+|[\s ]+$/g, ""); | customInstruction = customInstruction.replace(/^[\s ]+|[\s ]+$/g, ""); | ||
return ( | return baseCustomInstruction().trim() | ||
+ ( | + (customInstruction ? "\n" + customInstruction : ""); | ||
} | } | ||
function configureTemporaryCustomInstruction() { | function configureTemporaryCustomInstruction() { | ||
let tempCustomInstruction = getTemporaryCustomInstruction(); | |||
const prompt = Prompt("カスタムインストラクションを変更 ※変更すると会話履歴がリセットされます", tempCustomInstruction ? tempCustomInstruction : DEFAULT_CUSTOM_INSTRUCTION); | |||
if (prompt) { | if (prompt) { | ||
if (prompt === DEFAULT_CUSTOM_INSTRUCTION) { | if (prompt === DEFAULT_CUSTOM_INSTRUCTION) { | ||
| 753行目: | 939行目: | ||
setTemporaryCustomInstruction(prompt); | setTemporaryCustomInstruction(prompt); | ||
} | } | ||
} else { | } else if (prompt !== null) { | ||
// | // 空文字でOKされた場合はデフォルトに戻すか確認 | ||
if (Confirm("カスタムインストラクションをデフォルトに戻しますか?")) { | if (prompt === "") { | ||
if (Confirm("カスタムインストラクションをデフォルトに戻しますか?\n※カスタムインストラクションを空にしたい場合は [キャンセル] を選択してください。")) { | |||
setTemporaryCustomInstruction(""); | |||
} else { | |||
setTemporaryCustomInstruction(" "); | |||
} | |||
} | } | ||
} | } | ||
| 769行目: | 957行目: | ||
function getTemporaryCustomInstruction() { | function getTemporaryCustomInstruction() { | ||
const tag = doc.Tag; | |||
if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) { | if (tag.exists(TEMPORARY_CUSTOM_INSTRUCTION_TAG)) { | ||
return tag | return tag[TEMPORARY_CUSTOM_INSTRUCTION_TAG]; | ||
} else { | } else { | ||
return ""; | return ""; | ||
| 778行目: | 966行目: | ||
function setTemporaryCustomInstruction(temporaryCustomInstruction) { | function setTemporaryCustomInstruction(temporaryCustomInstruction) { | ||
const tag = doc.Tag; | |||
if (temporaryCustomInstruction) { | if (temporaryCustomInstruction) { | ||
tag | 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) { | ||
const tag = doc.Tag; | |||
tag | tag[PREVIOUS_TEMPLATE_PROMPT_TAG] = menuTrack; | ||
} | } | ||
| 794行目: | 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 初版
スポンサーリンク