Skip to content

前回までにやってきたこと#

  • createPromptメソッドを追加してツール説明をプロンプトに含める機能を実装
  • handleToolResponseメソッドを追加してJSON形式のツール実行コマンドを処理
  • writeFileメソッドを実装してファイル作成機能を追加
  • LLMがファイルを作成できるツール機能が動作することを確認

今回やること#

  • readfileツールの追加
  • プロンプトにreadfileツールの説明を追加
  • ファイル読み込み機能の実装
  • 生成AIにファイルの内容を渡すようにする。

VSCode Extension AI Agent作成 - ファイル読み込みツール#

今回はエージェントを拡張し、

src/extension.ts (全体)#

前のファイルから最小限の変更でreadfile機能を追加します:

import * as vscode from 'vscode';

// ここから追加
interface FileInfo {
    path: string;
    content: string;
}
//ここまで追加
export function activate(context: vscode.ExtensionContext) {
    const provider = new MagiViewProvider();
    context.subscriptions.push(
        vscode.window.registerWebviewViewProvider(
            "main.view",
            provider,
        ),
    );
}
class MagiViewProvider implements vscode.WebviewViewProvider {
    private _readFiles: FileInfo[] = []; // この行を追加

    public async resolveWebviewView(webviewView: vscode.WebviewView) {
        webviewView.webview.options = {
            enableScripts: true,
        };

        webviewView.webview.onDidReceiveMessage(async (data) => {
            if (data.type === 'promptEntered') {
                webviewView.webview.postMessage({
                    type: 'addElement',
                    text: data.text
                });
                const models = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4.1' });
                const model = models[0];
//ここから追加

                let filesContext = '';
                if (this._readFiles.length > 0) {
                    filesContext = '\n\nこれまでに読み込んだファイル:\n';
                    this._readFiles.forEach((fileInfo, index) => {
                        filesContext += `【ファイル${index + 1}】パス: ${fileInfo.path}\n内容:\n${fileInfo.content}\n\n`;
                    });
                }
//ここまで追加
                const prompt = `ユーザーの依頼:${data.text}${filesContext}

ユーザーの依頼を実現するために、適切なアクションを決定してJSONで回答してください。

回答は必ず以下のJSON形式で返してください:
{"tool":"利用するツール","args":["ツールに渡すパラメータ1","ツールに渡すパラメータ2"]}

使用可能なツール:
- "readfile": ファイルを読み込む場合。argsは["ファイルパス"]
- "writefile": ファイルを作成・編集する場合。argsは["ファイルパス","ファイル内容"]
- "message": ユーザーにメッセージを返す場合。argsは["ユーザに見せたいメッセージ"]

ユーザーの依頼内容を分析し、ファイル操作が必要な場合は"writefile"または"readfile"、説明やメッセージが必要な場合は"message"を選択してください。
JSON以外の文字は一切含めず、純粋なJSONのみを返してください。`;
// ↑のプロンプトの最初に${filesContext}を追加、と、 使用可能なツール:にreadfileの行を追加。
                const messages = [vscode.LanguageModelChatMessage.User(prompt)];

                const response = await model.sendRequest(messages);

                let returnTextFromVscodeLm = '';
                for await (const fragment of response.text) {
                    returnTextFromVscodeLm += fragment;
                }

                try {
                    // LLMからの応答をJSONとしてパース
                    const returnJSON = JSON.parse(returnTextFromVscodeLm);

                    if (returnJSON.tool === 'message') {
                        // メッセージツールの場合:Webviewにメッセージを表示
                        webviewView.webview.postMessage({
                            type: 'addElement',
                            text: returnJSON.args[0]
                        });
//ここから挿入    
                    } else if (returnJSON.tool === 'readfile') {
                        // ファイル読み込みツールの場合:ファイルを読み込んでフィールドに保存
                        const filePath = returnJSON.args[0];

                        // 現在のワークスペースフォルダを取得
                        const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
                        if (workspaceFolder) {
                            try {
                                // ワークスペースフォルダ内のファイルパスを構築
                                const fullPath = vscode.Uri.joinPath(workspaceFolder.uri, filePath);

                                // ファイルを読み込み
                                const fileData = await vscode.workspace.fs.readFile(fullPath);
                                const fileContent = new TextDecoder().decode(fileData);

                                // すでに読み込み済みかチェック
                                const existingIndex = this._readFiles.findIndex(f => f.path === filePath);
                                if (existingIndex >= 0) {
                                    // 既存のエントリを更新
                                    this._readFiles[existingIndex].content = fileContent;
                                } else {
                                    // 新しいエントリを追加
                                    this._readFiles.push({ path: filePath, content: fileContent });
                                }

                                // 成功メッセージを表示
                                webviewView.webview.postMessage({
                                    type: 'addElement',
                                    text: `ファイル "${filePath}" を読み込みました!(現在${this._readFiles.length}個のファイルを保持中)`
                                });
                            } catch (error) {
                                webviewView.webview.postMessage({
                                    type: 'addElement',
                                    text: `ファイル "${filePath}" の読み込みに失敗しました: ${error}`
                                });
                            }
                        } else {
                            webviewView.webview.postMessage({
                                type: 'addElement',
                                text: 'ワークスペースが開かれていません。'
                            });
                        }
//ここまで挿入
                    } else if (returnJSON.tool === 'writefile') {
                        // ファイル書き込みツールの場合:ファイルを作成
                        const filePath = returnJSON.args[0];
                        const fileContent = returnJSON.args[1];

                        // 現在のワークスペースフォルダを取得
                        const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
                        if (workspaceFolder) {
                            // ワークスペースフォルダ内のファイルパスを構築
                            const fullPath = vscode.Uri.joinPath(workspaceFolder.uri, filePath);

                            // ファイルを作成
                            await vscode.workspace.fs.writeFile(fullPath, Buffer.from(fileContent, 'utf8'));

                            // 成功メッセージを表示
                            webviewView.webview.postMessage({
                                type: 'addElement',
                                text: `ファイル "${filePath}" を作成しました!`
                            });
                        } else {
                            webviewView.webview.postMessage({
                                type: 'addElement',
                                text: 'ワークスペースが開かれていません。'
                            });
                        }
                    } else {
                        // 未知のツールの場合
                        webviewView.webview.postMessage({
                            type: 'addElement',
                            text: `未知のツール: ${returnJSON.tool}`
                        });
                    }
                } catch (error) {
                    // JSONパースエラーの場合は元のテキストをそのまま表示
                    webviewView.webview.postMessage({
                        type: 'addElement',
                        text: `JSONパースエラー: ${returnTextFromVscodeLm}`
                    });
                }
            }
        });
        webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Agent</title>
</head>
<body>
    Hello World!
    <textarea id="input-textarea" data-testid="input-textarea" rows="4" style="width:100%" placeholder="Enter text and press Enter..."></textarea>
    <div id="output" data-testid="output"></div>

    <script>
        const vscode = acquireVsCodeApi();
        const textarea = document.getElementById('input-textarea');
        const output = document.getElementById('output');

        textarea.addEventListener('keydown', function(event) {
            // Enterキーが押された時
            if (event.key === 'Enter') {
                // IMEの変換中(composing状態)でない場合のみ処理
                if (!event.isComposing) {
                    event.preventDefault(); // デフォルトの改行を防ぐ

                    const text = textarea.value.trim();
                    if (text) {
                        // VS Codeにメッセージを送信
                        vscode.postMessage({
                            type: 'promptEntered',
                            text: text
                        });
                        textarea.value = '';
                    }
                }
            }
        });

        // VS Codeからのメッセージを受け取る
        window.addEventListener('message', event => {
            const message = event.data;
            if (message.type === 'addElement') {
                const newDiv = document.createElement('div');
                newDiv.textContent = message.text;
                output.appendChild(newDiv);
            }
        });

        // composition系のイベントも念のため処理
        let isComposing = false;
        textarea.addEventListener('compositionstart', function() {
            isComposing = true;
        });
        textarea.addEventListener('compositionend', function() {
            isComposing = false;
        });
    </script>
</body>
</html>`;
    }
}

動かしてみる。#

npm run compile

でプロジェクトをビルドしてから、 Visual Studio CodeのメニューのRun→Start Debuggingを押すと新しくVisuao Studio Codeが立ち上がります。 すでにExtensionを立ち上げている場合はソースを表示しているVisual Studio Codeの runbar で、再起動矢印を押すと反映されます。

Extensionが動いているVisual Studio Codeでいずれかのディレクトリを開いて、 textareaにファイルを作成するような命令をお願いしてみましょう。

占い結果を表示するJavaScriptのコードをuranai.jsに作成して。

で、まずは読み込むファイルを作成します。 次に、

uranai.jsを読み込んで。

で、uranai.jsの内容をプロンプトに入れられるようにします。 最後に、

uranai.jsの出力する占いの結果のバリエーションを増やして。

と入力してEnterキーを押すと、生成AIがuranai.jsを読み込んで占いのバリエーションを増やしてくれるはずです!きっと! これで、更にコーディング生成AIエージェントっぽくなりましたね!

コードの解説#

要点を解説していきます。

filesContext#

生成AIとのやり取りは、基本全てプロンプトで行います。 ファイルを読み込ませるのも、プロンプトに載せる方法を取ります。 そして、現在の生成AI(LLM)は基本、記憶領域を持ちません。なので、「読んで」といっても、その内容をりかいさせることはできません。(モデルに学習させれば可能ですが、多大なコストが掛かります。) よって、ファイルを読み込む際は、ファイルを読み込んだ命令以降のプロンプトにファイルの内容を追加することで行います。

まとめ#

今回は、ファイルの読み込み機能を作ってみました。

今回やったこと#

  • createPromptメソッドにreadfileツールの説明を追加
  • handleToolResponseメソッドにreadfile処理の分岐を追加
  • readfileメソッドを実装してファイル読み込み機能を追加
  • プロンプトにファイルを追加して、その内容を使ってファイルを出力できることを確認。

次やること#

今は一回の入力で一つのツール実行しかできません。 次回はいよいよ最終回。ユーザ入力内容の実現のために、複数回のツールを利用できるように反復実行機能を追加していきます。