Open7

VSCode v1.91で安定版になったLanguage Model APIを見てみる

sho-hatasho-hata

https://code.visualstudio.com/api/extension-guides/language-model

ざっくりまとめると、LLMによる自然言語処理を拡張機能から簡単に呼び出せるようになるというもの。
今まではOpenAI APIをはじめとしたWeb APIを呼び出すためにAPIクライアントを用意してゴニョゴニョ、という形だったが、シンプルにLLMにアクセスできるようになった。

同時に発表されたChat Extentionと組み合わせて、GItHub Copilotを固有のコンテキストで拡張できる。実際の例ではPostgresやStripeなどがコード例やドキュメントの提示などサービスに特化した返答を返すことができる拡張機能が紹介されていた。

これにより、たとえばWeb APIを組み合わせてRAGを組み込み、チャット機能を強化することもできるようになった。
ただ、チャット機能が代表的な例として紹介されているが、VSCodeの各種APIと組み合わせてエディターのあらゆる部分にLLMが入り込んで、開発者のできることを支援することが本命のようだ。

独自の拡張機能で、GitHub Copilotによって提供されるLLMに直接アクセスしていろんなことに活用できる構想自体はMS Build 2024で発表されており、Insider版でベータリリースされていた。

sho-hatasho-hata

デモ機能1: 簡単な応答機能

@cat /teachというようにすると、ランダムに技術トピックについて猫の口調(?)で説明する簡単な機能がデモとして用意されている。

まずはスラッシュコマンドが呼び出された時に処理されるハンドラ関数を見ていく。

コマンドを捌くハンドラ

const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<ICatChatResult> => {
        // To talk to an LLM in your subcommand handler implementation, your
        // extension can use VS Code's `requestChatAccess` API to access the Copilot API.
        // The GitHub Copilot Chat extension implements this provider.
        if (request.command === 'teach') {
            stream.progress('Picking the right topic to teach...');
            const topic = getTopic(context.history);
            try {
                const [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR);
                if (model) {
                    const messages = [
                        vscode.LanguageModelChatMessage.User('You are a cat! Your job is to explain computer science concepts in the funny manner of a cat. Always start your response by stating what concept you are explaining. Always include code samples.'),
                        vscode.LanguageModelChatMessage.User(topic)
                    ];

                    const chatResponse = await model.sendRequest(messages, {}, token);
                    for await (const fragment of chatResponse.text) {
                        stream.markdown(fragment);
                    }
                }
            } catch(err) {
                handleError(err, stream);
            }

            stream.button({
                command: CAT_NAMES_COMMAND_ID,
                title: vscode.l10n.t('Use Cat Names in Editor')
            });

            return { metadata: { command: 'teach' } };

コード量も少ないので読んでいく。

言語モデルの選択

const MODEL_SELECTOR: vscode.LanguageModelChatSelector = { vendor: 'copil1ot', family: 'gpt-3.5-turbo' };
const [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR);

使用する言語モデルを選択する。
2024年7月4日現在

  • vendorはcopilot固定
  • familyはgpt-3.5-turbogpt4から選択できる。phi2llamaも選択できるようだ

使用できるモデルは変わるため、最新の情報は書きリファレンスを参考とすること
https://code.visualstudio.com/api/references/vscode-api#LanguageModelChat

プロンプトの作成

const topic = getTopic(context.history);
const messages = [
    vscode.LanguageModelChatMessage.User('You are a cat! Your job is to explain computer science concepts in the funny manner of a cat. Always start your response by stating what concept you are explaining. Always include code samples.'),
    vscode.LanguageModelChatMessage.User(topic)
];

vscode.LanguageModelChatMessageを使用してプロンプトを組み立てる。2種類のプロンプト追加方法があり、役割が分かれている。

  • User: 指示やユーザーのリクエストを提供するために使用する
  • Assistant: 以前の言語モデルの応答履歴をプロンプトのコンテキストとして追加するために使用する

ここでは、最初のプロンプトとして役割などの前提条件を指定している。また、この処理が属しているhandler関数の引数vscode.ChatContextからhistoryを取得しており、チャット履歴をプロンプトとして追加している。

chat participantの作成

    const cat = vscode.chat.createChatParticipant(CAT_PARTICIPANT_ID, handler);
    cat.iconPath = vscode.Uri.joinPath(context.extensionUri, 'cat.jpeg');

先ほどのハンドラ関数をIDと共に登録し、ChatParticipantを作成。すると、チャットUI上で@catを指定することができ、登録したコマンド/teachが呼び出せるようになる。

sho-hatasho-hata

デモ機能2: 関数名や変数、処理を猫に関係するものに書き換える機能

コマンドパレットから「cat.namesInEditor」を呼び出すと、選択中のソースコードをLLMで処理して書き換える機能を見ていく。


やっていることは至極単純。

  1. 選択したファイルの中身をとってくるVSCodeのAPIを叩いてソースコードを受け取り、猫に関係するコードに書き換えるためのプロンプトともにLLMに問い合わせる
  2. LLMの解答を、VSCodeのAPIを叩いて選択したファイルの中身を書き換える

コマンドの登録

デモ機能1とやっていることは大きく変わらない。処理を呼び出すコンテキストが変わった(chat内 -> コマンドパレット)だけ。vscode.commands.RedisterTextEditorCommandの第二引数の関数内に処理を書いていく。

context.subscriptions.push(
        cat,
        // Register the command handler for the /meow followup
        vscode.commands.registerTextEditorCommand(CAT_NAMES_COMMAND_ID, async (textEditor: vscode.TextEditor) => {
            // Replace all variables in active editor with cat names and words
            const text = textEditor.document.getText();

            let chatResponse: vscode.LanguageModelChatResponse | undefined;
            try {
                const [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-3.5-turbo' });
                if (!model) {
                    console.log('Model not found. Please make sure the GitHub Copilot Chat extension is installed and enabled.');
                    return;
                }

                const messages = [
                    vscode.LanguageModelChatMessage.User(`You are a cat! Think carefully and step by step like a cat would.
                    Your job is to replace all variable names in the following code with funny cat variable names. Be creative. IMPORTANT respond just with code. Do not use markdown!`),
                    vscode.LanguageModelChatMessage.User(text)
                ];
                chatResponse = await model.sendRequest(messages, {}, new vscode.CancellationTokenSource().token);

            } catch (err) {
                if (err instanceof vscode.LanguageModelError) {
                    console.log(err.message, err.code, err.cause);
                } else {
                    throw err;
                }
                return;
            }
        }),
    );

プロンプトの作成

const text = textEditor.document.getText();
                const messages = [
                    vscode.LanguageModelChatMessage.User(`You are a cat! Think carefully and step by step like a cat would.
                    Your job is to replace all variable names in the following code with funny cat variable names. Be creative. IMPORTANT respond just with code. Do not use markdown!`),
                    vscode.LanguageModelChatMessage.User(text)
                ];
                chatResponse = await model.sendRequest(messages, {}, new vscode.CancellationTokenSource().token);

ここではプロンプトとして前提条件と、textEditor.document.getText()で取得したソースコードをLLMに渡している。

ファイル中身書き換え

await textEditor.edit(edit => {
                const start = new vscode.Position(0, 0);
                const end = new vscode.Position(textEditor.document.lineCount - 1, textEditor.document.lineAt(textEditor.document.lineCount - 1).text.length);
                edit.delete(new vscode.Range(start, end));
            });

ファイルの最初から末尾までの位置を取得し、その範囲を一括削除。

その後、LLMの解答(chatResponse)から回答を取得する。結果はAsyncIterable<string>になっているため、一瞬複数行の結果で書き換えるという挙動ではなく、取得した結果ループで回して一行づつInsertしていくという形になる。

// Stream the code into the editor as it is coming in from the Language Model
            try {
                for await (const fragment of chatResponse.text) {
                    await textEditor.edit(edit => {
                        const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
                        const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
                        edit.insert(position, fragment);
                    });
                }
            } catch (err) {
                // async response stream may fail, e.g network interruption or server side error
                await textEditor.edit(edit => {
                    const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
                    const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length);
                    edit.insert(position, (<Error>err).message);
                });
            }
        }),
sho-hatasho-hata

作ってみた:ソースコードの選択範囲のgit historyについて質問する機能

https://twitter.com/sho_hata_/status/1810693128040689996

ということでLanguage Model APIを使ってちょっとした拡張を作ってみた。
仕組みは簡単で、ソースコードの選択した範囲はvscode.TextEditor.selectionから取得できる。取得した選択範囲の開始位置と終了位置をgit logのLオプションに渡し、選択範囲のgit historyを取得する。

async (textEditor: vscode.TextEditor) => {
			const workspaceFolder =
				vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
			const filePath = textEditor.document.fileName;
			const selection = textEditor.selection;
			const startLine = selection.start.line + 1;
			const endLine = selection.end.line + 1;

あとはgit log -Lの結果をユーザーが入力したプロトコルとともにLanguage Modelに渡し、結果をエディタに出力しているだけ。

雑に実装したコードは以下のリポジトリに置いてある。
https://github.com/sho-hata/git-voyage

sho-hatasho-hata

Copilot chat valiables API

↑で作ったツールは、コマンドパレットから起動し、結果をエディタに出力している。

これをもう少し実用的にしたい。イメージとしてはcopilot chat画面に拡張機能を登場させて、copilotで使える#selectionなどのコンテキスト変数をツールに渡すイメージ。

#selection @git-voyage /ask-history

みたいな。

だが、自作関数からコンテキスト変数へのアクセスができるようになるcopilot chat valiables apiはまだプロポーザル段階のようだ。これが使えるようになると任意のコンテクストを拡張機能に渡すことができ、かなり自由度が上がるので注目しておきたい。
https://code.visualstudio.com/api/extension-guides/chat#variables