OpenAI GPT に相談

2024年12月18日 (水) 20:34時点におけるYuko (トーク | 投稿記録)による版 (ページの作成:「== 概要 == OpenAIの生成AIである「GPT」を利用するためのMeryマクロです。 このマクロは、以下の流れで動作します: * ユーザーがエディタ内でテキストを選択する (未選択の場合、アクティブ行が対象になる) * 選択したテキストをプロンプトとしてOpenAI APIに送信する * APIからの応答を取得し、エディタに表示する * 応答内容は履歴として保持され、…」)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)

概要

OpenAIの生成AIである「GPT」を利用するためのMeryマクロです。

このマクロは、以下の流れで動作します:

  • ユーザーがエディタ内でテキストを選択する (未選択の場合、アクティブ行が対象になる)
  • 選択したテキストをプロンプトとしてOpenAI APIに送信する
  • APIからの応答を取得し、エディタに表示する
  • 応答内容は履歴として保持され、次回のリクエストに利用する

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

  • 本マクロではOpenAI APIを利用します。API利用に伴う料金発生やOpenAI社の情報取り扱い等によるユーザー不利益が発生しても、作者は一切の責任を負いません。
    • OpenAIでの支払いでは、誤って高額の課金にならないように上限を設けることができます。上限を設定しての利用を強くおすすめします。
    • 2024/12/18 時点、本マクロでは「gpt-4o-mini」モデルを利用しており、費用は従量課金ですが「$0.150/100万入力トークン、 $0.600/100万出力トークン」と低価格です。目安として、日本語1文字 1~2 トークン程で計上されますが、よほど大量の文字の入出力を繰り返さない限りは月間数ドルで利用できると思われます。
  • OpenAI APIでは受け取ったリクエストを学習に使わないと宣言していますが、くれぐれも機密情報やプライバシーに関わる情報を指定しないように注意してください。
  • OpenAI APIは、 OpenAI利用規約 に則ってご利用ください。

利用前提

  • Mery Ver 3.7.8 以上
  • OpenAI Platform で、OpenAI APIキーを取得していること
  • OpenAI APIを利用できるだけの課金残高があること

利用方法

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

ソースコード (gpt.js)

#title = "GPT に相談"
BeginUndoGroup();

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

// コンテキストメニュー番号
var EXECUTE_AI = 1;
var RESET_HISTORY = 2;

var ConsoleClass = function () {
    this.log = function (str) {
        outputBar.Writeln(str)
    }
}
var console = new ConsoleClass();

var doc = document;
var sel = document.selection;

main();

function main() {
    var selectionIsEmpty = sel.IsEmpty
    var beforeScrollY = ScrollY;

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

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

    // プロンプトが空の場合はエラー
    if (promptLength === 0) {
        alert("プロンプトを入力してください。");
        return;
    }
    // 文字数が多すぎる場合はエラー
    if (promptLength > 10000) {
        alert("プロンプトの文字数が多すぎます。10000 文字以下にしてください。");
        return;
    }
    // 読み取り専用の場合はエラー
    if (doc.ReadOnly) {
        alert("ファイルが読み取り専用のため実行できません。");
        return;
    }

    // カーソル位置移動
    sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical));
    sel.EndOfLine(false, mePosLogical);
    sel.NewLine();
    var activeY = sel.GetActivePointY(mePosLogical);

    // GPT API に問い合わせ
    doc.ReadOnly = true;
    var ret = callGPTAPI(promptText);
    doc.ReadOnly = false;

    // 結果を表示
    sel.Text = "\n---\n\n" + ret[0] + "\n\n" + ret[1] + "\n\n---\n";

    // 位置調整
    sel.SetActivePoint(
        mePosLogical,
        1,
        activeY,
        false
    )
    if (beforeScrollY + 2 < ScrollY) {
        ScrollY = ScrollY - 1;
    }
}

function createContextMenu(selectionIsEmpty) {
    var menu = CreatePopupMenu();
    menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0);
    menu.Add("", 0, meMenuSeparator);
    if (selectionIsEmpty) {
        menu.Add("アクティブ行の内容で相談 (&Q)", EXECUTE_AI);
    } else {
        menu.Add("選択範囲の内容で相談 (&Q)", EXECUTE_AI);
    }
    if (document.Tag.exists("gpt-text")) {
        var turn = JSON.parse(document.Tag("gpt-text")).length / 2;
        menu.Add("会話履歴をリセット (現在の会話数: " + turn + ") (&R)", RESET_HISTORY);
    }
    return menu;
}

function callGPTAPI(promptText) {
    try {
        // APIキーの取得
        var gptApiKey = shell.getEnv("OPENAI_API_KEY");

        var http = new ActiveXObject("Msxml2.ServerXMLHTTP");
        var url = 'https://api.openai.com/v1/chat/completions';

        http.open('POST', url, true);
        http.setRequestHeader('Content-Type', 'application/json');
        http.setRequestHeader('Authorization', 'Bearer ' + gptApiKey);
        http.send('{'
            + '"model": "gpt-4o-mini",' // モデルの指定
            + '"temperature": 0.7,' // 生成の多様性 (0.0 ~ 1.0)
            + '"max_tokens": 4000,' // 生成トークン数の上限
            + '"messages":' + createGPTRequest(promptText)
            + '}');

        // 応答待機
        var timeout = 60;
        var retrySec = 0;
        var startX = sel.GetActivePointX(mePosLogical);
        var prevPos = sel.GetActivePos();
        while (http.readyState != 4) {
            if (retrySec > timeout) {
                break;
            }
            http.waitForResponse(1);
            retrySec += 1;
            inputProgressChar(prevPos);
            prevPos = sel.GetActivePos();
        }
        // 進捗表示文字を削除
        sel.SetActivePoint(
            mePosLogical,
            startX,
            sel.GetActivePointY(mePosLogical),
            true
        );
        sel.Delete();

        var responseMessage = "";
        var token = "";

        // 応答ステータスチェック
        if (http.readyState == 4 && http.status == 200) {
            // JSONをパースして必要なテキストを抽出
            var response = JSON.parse(http.responseText);
            if (response.choices && response.choices.length > 0) {
                // メッセージの内容を取得
                response.choices.forEach(function (choice) {
                    if (choice.message && choice.message.content) {
                        responseMessage += choice.message.content;
                    }
                });
                // トークン数を取得
                var usage = response.usage;
                token += "(使用トークン数: "
                    + "プロンプト=" + usage.prompt_tokens
                    + ", 応答=" + usage.completion_tokens
                    + ", 合計=" + usage.total_tokens
                    + ")";
            }
        } else {
            var responseMessage = "エラーが発生しました: " + http.status + " " + http.statusText
            http.abort();
            return responseMessage;
        }

        // 結果を会話履歴に追加
        if (HISTORY_SIZE > 0) {
            appendReturnTextToTag(promptText, responseMessage);
        }

        // 整形しつつ返す
        return [responseMessage.trim(), token];
    } catch (e) {
        console.log(e.stack);
        return ["エラーが発生しました: " + e.message, ""];
    }
}

// document.Tag の内容を加味してGPTのリクエストを作成
function createGPTRequest(promptText) {
    var tag = document.Tag;
    // 以前の会話が存在しない場合
    if (!tag.exists("gpt-text")) {
        return JSON.stringify([{ "role": "user", "content": promptText }]);
    }

    var gptText = JSON.parse(tag("gpt-text"));
    gptText.push({
        "role": "user",
        "content": promptText
    });
    return JSON.stringify(gptText);
}

// document.Tag にGPTの応答を追加
function appendReturnTextToTag(userText, modelText) {
    var tag = document.Tag;
    if (!tag.exists("gpt-text")) {
        tag("gpt-text") = JSON.stringify([
            {
                "role": "user",
                "content": userText
            },
            {
                "role": "assistant",
                "content": modelText
            }
        ]);
    } else {
        var gptText = JSON.parse(tag("gpt-text"));
        gptText.push({
            "role": "user",
            "content": userText
        });
        gptText.push({
            "role": "assistant",
            "content": modelText
        });
        // 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除
        while (gptText.length > HISTORY_SIZE * 2) {
            gptText.shift();
        }
        tag("gpt-text") = JSON.stringify(gptText);
    }
}

function resetTag() {
    if (document.Tag.exists("gpt-text")) {
        document.Tag.remove("gpt-text");
    }
}

function inputProgressChar(pos) {
    doc.ReadOnly = false;
    sel.SetActivePos(pos);
    sel.Text = ".";
    doc.ReadOnly = true;
}

更新履歴

  • 2024-12-18 初版
スポンサーリンク