JavaScriptを有効にしてください

GitHub Copilot Extensions (Chat Extention?) を自作し隊

 ·   23 分で読めます  ·   Kento

Build 2024 でも発表があった GitHub Copilot Extensions を試してみました
題材としては Azure OpenAI を使って Mermaid を使った Azure 構成図を作り隊 – クラウドを勉強し隊 を使ってみます

先に結論を言うと
「Visual Studio Code 内の GitHub Copilot Chat でお願いする → Mermaid で記述された Azure 構成図が生成」
が実現できました

インフラ エンジニアでもやればできる!!!
NakayamaKento/github_copilot_extension_mermaid

GitHub Copilot Extensions とは

Copilot に GitHub Copilot Extensionsのご紹介:パートナーとのエコシステムで無限の可能性を引き出す - GitHubブログ の要約をお願いしました
詳細は元記事を見てください


GitHub Copilot Extensionsに関する主要なポイントは以下の通りです:

  • エコシステムの拡張: GitHub Copilot Extensionsは、開発者が IDE や GitHub.com を離れることなく、好みのツールやサービスを使用して自然言語でクラウドを構築し、デプロイできるようにします。
  • パートナーエコシステム: DataStax、Docker、LambdaTestなどのパートナーと共に、開発者がフローを維持し、スキルを向上させ、イノベーションを迅速に起こすことができます。
  • 自然言語プログラミング: GitHub Copilotは、自然言語でのプログラミングを通じて、ソフトウェア開発の参入障壁を下げ、開発者が技術スタックのあらゆるツールと完全に統合するインテリジェントなプラットフォームです。

わざわざチャットツールを起動したり、ブラウザを開いたりしなくても Visual Studio Code 内で完結できるのがいいですね

ざっくり手順

  1. 慣れるための勉強
  2. Visual Studio Code Insider のインストール
  3. GitHub Copilot Chat の pre-release を有効化
  4. Visual Studio Code 拡張機能のひな形作成
  5. GitHub の連携
  6. 全体の設計
  7. package.json の編集
  8. extension.ts の編集
  9. 画像の作成

0. 慣れるための勉強

0.1 Visual Studio Code の拡張機能の勉強

GitHub Copilot Extensions は Visual Studio Code の拡張機能として開発します
Visual Studio Code の拡張機能の開発経験はないので軽く勉強しました
VSCode Extensions(拡張機能) 自作入門 〜VSCodeにおみくじ機能を追加する〜 #VSCode - Qiita のサイトを参考に手を動かしました

extensions.ts と package.json を編集すればいいということがわかりました

0.2 GitHub Copilot Extensions の勉強

GitHub Copilot Extensions に関する記事もざっと目を通しました

Microsoft の GitHub リポジトリに Visual Studio Code の拡張機能のサンプルがあります
そのなかに GitHub Copilot Extensions に関するものがあるので、これをざっと確認しました
今回作成はこれをベースにカスタマイズしていきます
microsoft/vscode-extension-samples: Sample code illustrating the VS Code extension API.

一人では確認できないので、Copilot に 1 行ずつ解説してもらいながら読み解いていきました

vsc03
サンプルの読み解き:

1. Visual Studio Code Insider のインストール

執筆時点では Stable 版では使用できないので、Insider 版の Visual Studio Code をインストールします
Download Visual Studio Code Insiders

vsc01
Visual Studio Code Insider:

2. GitHub Copilot Chat の pre-release を有効化

執筆時点では GitHub Copilot Chat の pre-release 版を使う必要があるので有効化しておきます

vsc02
GitHub Copilot Chat の pre-release:

3. Visual Studio Code 拡張機能のひな形作成

VSCode Extensions(拡張機能) 自作入門 〜VSCodeにおみくじ機能を追加する〜 #VSCode - Qiita を参考にひな形を作成します

  1. Node.js — Run JavaScript Everywhere から Node.js のインストール
  2. 拡張機能に必要なコンポーネント yo と generator-code を npm install -g yo generator-code でインストール
  3. 作業フォルダを作成し、yo code を実行。色々聞かれるので下記の通りに選択しました
質問 回答
What type of extension do you want to create? New Extension (TypeScript)
What’s the name of your extension? mermaid-azure
What’s the identifier of your extension? (そのまま Enter)
What’s the description of your extension? Create Azure configuration diagrams using Mermaid
Initialize a git repository? y
Which bundler to use? webpack
Which package manager to use? npm

これでフォルダと必要なファイル一式が作成されます
最後に Do you want to open the new folder with Visual Studio Code? と聞かれるので Open with code-insiders を選択します

terminal01
ひな形の作成01:
terminal02
ひな形の作成02:

4. GitHub の連携

ローカル Git を有効化したので、GitHub のリポジトリを作って連携しておきます
適当にリポジトリを作成し、push しておきます

git remote add origin https://github.com/XXXXXXXX
git branch -M main
git push -u origin main
vsc04
GitHub 連携:

5. 全体の設計

簡単に1つのコマンドだけを実装してみます

flowchart TD
    A[@mermaid をメンション] --> C{コマンド}
    C -->|azure_figure| D[構成図を生成]
    C -->|else| E[何もしない]

例外処理とかはよくわからんので、基本はサンプルをコピペしていく方針です

6. package.json の編集

ここでは Chat の中でメンションする名前や、コマンドを記述しておきます
詳細は Chat extensions | Visual Studio Code Extension API を見てください

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
  "name": "mermaid-azure",
  "displayName": "mermaid-azure",
  "description": "Create Azure configuration diagrams using Mermaid",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.90.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "mermaid-azure.helloWorld",
        "title": "Hello World"
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "npm run package",
    "compile": "webpack",
    "watch": "webpack --watch",
    "package": "webpack --mode production --devtool hidden-source-map",
    "compile-tests": "tsc -p . --outDir out",
    "watch-tests": "tsc -p . -w --outDir out",
    "pretest": "npm run compile-tests && npm run compile && npm run lint",
    "lint": "eslint src --ext ts",
    "test": "vscode-test"
  },
  "devDependencies": {
    "@types/vscode": "^1.90.0",
    "@types/mocha": "^10.0.6",
    "@types/node": "18.x",
    "@typescript-eslint/eslint-plugin": "^7.11.0",
    "@typescript-eslint/parser": "^7.11.0",
    "eslint": "^8.57.0",
    "typescript": "^5.4.5",
    "ts-loader": "^9.5.1",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "@vscode/test-cli": "^0.0.9",
    "@vscode/test-electron": "^2.4.0"
  }
}

サンプルにこれが書かれていたので追加します

1
2
3
  "enabledApiProposals": [
    "chatVariableResolver"
  ],

ハイライトされた部分は、Node.jsプロジェクトでプロジェクトのプロパティを定義するために使用されるマニフェストファイルであるpackage.jsonからのものです。このファイルはnpm(Node Package Manager)によってプロジェクトを識別し、その依存関係を処理するために使用されます。

enabledApiProposalsプロパティは、Visual Studio Code(VS Code)拡張機能に特有のものです。これは、拡張機能が使用したい提案中のAPIをリストアップする配列です。VS Codeの提案中のAPIは、まだ安定していない新しいAPIです。これらは提案段階にあり、テストが行われており、VS Codeの将来のアップデートで変更されたり、削除されたりする可能性があります。

この場合、“chatVariableResolver"は、拡張機能が選択している提案中のAPIです。このAPIは、拡張機能内のチャットコンテキストで変数を解決するためのものと思われます。enabledApiProposals配列に含めることで、拡張機能はこの提案中のAPIを使用したいと示しています。

ただし、提案中のAPIを使用すると、APIが変更されたり削除されたりすると、拡張機能が予期しない動作をする可能性があるため、拡張機能も不安定になる可能性があります。そのため、提案中のAPIは開発やテスト中にのみ使用し、APIの状態を変更するためには注意が必要です。

vscode/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts at main · microsoft/vscode

cotributes のところに以下の内容を貼り付けます
chat でメンションする相手(参加者)やコマンドを登録しておきます

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  "contributes": {
    "chatParticipants": [
      {
        "id": "mermaid-azure.chat",
        "fullName": "Mermaid",
        "name": "mermaid",
        "description": "図を書いたるで",
        "isSticky": true,
        "commands": [
          {
            "name": "azure_figure",
            "description": "Azureの構成図を教えてや"
          }
        ]
      }
    ]
  }

description で記載する内容はこのように見えるので、ヘルプ的な内容を書いてるといいかもしれません

vsc05
description の表示例01:
vsc06
description の表示例02:

完成したのがこれです

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
  "name": "mermaid-azure",
  "displayName": "mermaid-azure",
  "description": "Create Azure configuration diagrams using Mermaid",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.90.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [],
  "enabledApiProposals": [
    "chatVariableResolver"
  ],
  "main": "./dist/extension.js",
  "contributes": {
    "chatParticipants": [
      {
        "id": "mermaid-azure.chat",
        "fullName": "Mermaid",
        "name": "mermaid",
        "description": "図を書いたるで",
        "isSticky": true,
        "commands": [
          {
            "name": "azure_figure",
            "description": "Azureの構成図を教えてや"
          }
        ]
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "npm run package",
    "compile": "webpack",
    "watch": "webpack --watch",
    "package": "webpack --mode production --devtool hidden-source-map",
    "compile-tests": "tsc -p . --outDir out",
    "watch-tests": "tsc -p . -w --outDir out",
    "pretest": "npm run compile-tests && npm run compile && npm run lint",
    "lint": "eslint src --ext ts",
    "test": "vscode-test"
  },
  "devDependencies": {
    "@types/vscode": "^1.90.0",
    "@types/mocha": "^10.0.6",
    "@types/node": "18.x",
    "@typescript-eslint/eslint-plugin": "^7.11.0",
    "@typescript-eslint/parser": "^7.11.0",
    "eslint": "^8.57.0",
    "typescript": "^5.4.5",
    "ts-loader": "^9.5.1",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "@vscode/test-cli": "^0.0.9",
    "@vscode/test-electron": "^2.4.0"
  }
}

7. extension.ts の編集

続いて処理を記述する extension.ts を編集します

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';

// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {

	// Use the console to output diagnostic information (console.log) and errors (console.error)
	// This line of code will only be executed once when your extension is activated
	console.log('Congratulations, your extension "mermaid-azure" is now active!');

	// The command has been defined in the package.json file
	// Now provide the implementation of the command with registerCommand
	// The commandId parameter must match the command field in package.json
	const disposable = vscode.commands.registerCommand('mermaid-azure.helloWorld', () => {
		// The code you place here will be executed every time your command is executed
		// Display a message box to the user
		vscode.window.showInformationMessage('Hello World from mermaid-azure!');
	});

	context.subscriptions.push(disposable);
}

// This method is called when your extension is deactivated
export function deactivate() {}

変数の定義

package.json で記載した ID、使用するモデル、システム メッセージに相当するメッセージを変数として用意しておきます
モデルはサンプルでは gpt-3.5-turbo を指定していましたが、 gpt-4 でも動作することを確認しました

const CAT_PARTICIPANT_ID = 'mermaid-azure.chat';
const MODEL_SELECTOR: vscode.LanguageModelChatSelector = { vendor: 'copilot', family: 'gpt-4' };

const system_pronpt = `\
あなたは Mermaid を使って Azure の構成図を書くことが得意です。
サブスクリプションや、リソースグループ、Vnet など図の中で箱として扱うものは subgraph として表現します。
(以下省略)
`;

インターフェースの定義

次に以下の内容を追記します

1
2
3
4
5
interface ICatChatResult extends vscode.ChatResult {
    metadata: {
        command: string;
    }
}

ハイライトされているコードは、ICatChatResultというインターフェースを定義しています。このインターフェースは、vscode.ChatResultを拡張しており、metadataというプロパティを追加しています。metadataプロパティは、commandという文字列を持つオブジェクトです。

このインターフェースは、チャットの結果を表現するために使用されます。特に、metadataプロパティは、チャットの結果に関連するコマンドを保持するために使用されます。これは、チャットの結果を処理する際に、特定のコマンドを実行するための情報を提供します。

このインターフェースは、extension.tsファイルで定義されています

メイン処理

メインとなる export function activate(context: vscode.ExtensionContext) の中身を編集します
詳細は Chat extensions | Visual Studio Code Extension API を見てもらうとして、実装と Copilot による解説を載せておきます

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
export function activate(context: vscode.ExtensionContext) {

	// Chat の処理を行う関数 
	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 == 'azure_figure') {	// コマンドが azure_figureコマンドの場合
			stream.progress('Mermaid の図を考えています...');	// プログレスバーを表示
			try {
				const [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR);	// モデルを取得
				if (model) {	// モデルが取得できた場合
					const messages = [	// メッセージを作成
						vscode.LanguageModelChatMessage.User(system_pronpt),	// システムプロンプトを追加
						vscode.LanguageModelChatMessage.User(request.prompt)	// ユーザーの入力を追加
					];

					const chatResponse = await model.sendRequest(messages, {}, token);	// モデルにリクエストを送信

					stream.markdown('```\n');	// マークダウンのコードブロックを開始
					for await (const fragment of chatResponse.text) {	// レスポンスのテキストを取得
						stream.markdown(fragment);		// マークダウンに追加
					}
					stream.markdown('\n```');	// マークダウンのコードブロックを終了
				}
			} catch (err) {	// エラーが発生した場合
				handleError(err, stream);
			}

			return { metadata: { command: 'azure_figure' } };	// メタデータを返す
		} else {	// それ以外の場合
			try {
				const [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR);	// モデルを取得
				if (model) {	// モデルが取得できた場合
					const messages = [	// メッセージを作成
						vscode.LanguageModelChatMessage.User(`あなたは Mermaid を使って図を書くことが得意です。`),
						vscode.LanguageModelChatMessage.User(request.prompt)
					];

					const chatResponse = await model.sendRequest(messages, {}, token);	// モデルにリクエストを送信
					for await (const fragment of chatResponse.text) {	// レスポンスのテキストを取得
						// Process the output from the language model
						stream.markdown(fragment);
					}
				}
			} catch (err) {
				handleError(err, stream);	// エラーが発生した場合
			}

			return { metadata: { command: '' } };	// メタデータを返す
		}
	};

	// Chat participants appear as top-level options in the chat input
	// when you type `@`, and can contribute sub-commands in the chat input
	// that appear when you type `/`.
	const mermaid = vscode.chat.createChatParticipant(CHAT_PARTICIPANT_ID, handler);	// ChatParticipantを作成
	mermaid.iconPath = vscode.Uri.joinPath(context.extensionUri, 'azure_mermaid.jpg');	// アイコンを設定
	mermaid.followupProvider = {	// フォローアッププロバイダーを設定
		provideFollowups(result: ICatChatResult, context: vscode.ChatContext, token: vscode.CancellationToken) {	// フォローアップを提供
			return [{	// フォローアップを返す
				prompt: 'create a figure',	// プロンプトを設定
				label: vscode.l10n.t('Create the figure'),	// ラベルを設定
				command: 'azure_figure'	// コマンドを設定
			} satisfies vscode.ChatFollowup];	// ChatFollowupを返す
		}
	};

	// よくわからないので ほとんどコピペ
	context.subscriptions.push(	// ChatParticipantを登録
		mermaid,	// ChatParticipantを登録
		// 
		vscode.commands.registerTextEditorCommand(MERMAID_NAMES_COMMAND_ID, async (textEditor: vscode.TextEditor) => {	// テキストエディタコマンドを登錻
			// Get the text from the editor
			const text = textEditor.document.getText();

			let chatResponse: vscode.LanguageModelChatResponse | undefined;	// ChatResponseを初期化
			try {
				const [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4' });	// モデルを取得
				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) {	// エラーが LanguageModelError の場合
					console.log(err.message, err.code, err.cause)
				} else {	// それ以外の場合
					throw err;
				}
				return;
			}

			// Clear the editor content before inserting new content
			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));
			});

			// 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);
				});
			}
		}),
	);
}

このコードは、VS Codeの拡張機能を作成するためのものです。主な機能は以下の通りです:

ChatRequestHandlerの作成:ユーザーからのチャットリクエストを処理する関数を定義しています。特定のコマンド(‘azure_figure’)がリクエストされた場合とそれ以外の場合で処理を分けています。各ケースで、言語モデルからの応答を取得し、その結果をマークダウン形式でストリームに出力します。

ChatParticipantの作成:ChatRequestHandlerを使用して、チャット参加者を作成します。この参加者は、ユーザーがチャット入力で@を入力したときにトップレベルのオプションとして表示されます。また、/を入力したときにサブコマンドを提供することもできます。

FollowupProviderの設定:ユーザーが特定のアクションを完了した後に提供されるフォローアップを定義します。この例では、‘azure_figure’コマンドの実行後に’create a figure’というプロンプトを提供します。

TextEditorCommandの登録:テキストエディタのコマンドを登録します。このコマンドは、エディタのテキストを取得し、それを言語モデルに送信します。モデルからの応答をエディタに挿入します。

以上の機能を提供する拡張機能を作成しています。ただし、このコードにはhandleError関数が未定義であるため、エラーが発生します。この関数を定義する必要があります

handleError 関数

Copilot にもバレていましたが、handleError をサンプルからコピペしてきます
メッセージだけちょっと修正

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function handleError(err: any, stream: vscode.ChatResponseStream): void {
    // making the chat request might fail because
    // - model does not exist
    // - user consent not given
    // - quote limits exceeded
    if (err instanceof vscode.LanguageModelError) {
        console.log(err.message, err.code, err.cause);
        if (err.cause instanceof Error && err.cause.message.includes('off_topic')) {
            stream.markdown(vscode.l10n.t('I\'m sorry, I can only create Azure diagrams.'));
        }
    } else {
        // re-throw other errors so they show up in the UI
        throw err;
    }
}

これで一通りできました

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';

const CHAT_PARTICIPANT_ID = 'mermaid-azure.chat';
const MERMAID_NAMES_COMMAND_ID = 'mermaid.namesInEditor';
const MODEL_SELECTOR: vscode.LanguageModelChatSelector = { vendor: 'copilot', family: 'gpt-4' };
const system_pronpt = `\
あなたは Mermaid を使って Azure の構成図を書くことが得意です。
サブスクリプションや、リソースグループ、Vnet など図の中で箱として扱うものは subgraph として表現します。
Vnet Peering や VPN, Private Link などは矢印で関係性を表現します。
色分けして見やすくするために以下のように表示します。
- サブスクリプション:fill:none,color:#345,stroke:#345
- ネットワーク関連:fill:none,color:#0a0,stroke:#0a0
- VM、Private Endpoint:fill:#e83,color:#fff,stroke:none
- PaaS:fill:#46d,color:#fff,stroke:#fff
ユーザーからのリクエスに対して 上記のルールに従って Mermaid のフローチャートで回答を出力してください。
リソースグループやサブスクリプションは指定された場合のみ回答に含めて、それ以外の場合は回答に含めないでください。
インデント以外の、余計な半角スペースは入れないでください。

以下に例を3つ示します
例1
入力:
ハブ&スポークの構成です。スポークは3つあります。
出力:
---
title: ハブ&スポークの構成
---
graph TB;

%%グループとサービス
subgraph hub_Vnet[hub-Vnet]
end

subgraph Spoke1["Spoke 1"]
end

subgraph Spoke2["Spoke 2"]
end

subgraph Spoke3["Spoke 3"]
end


%%サービス同士の関係
hub_Vnet <-- "ピアリング" --> Spoke1
hub_Vnet <-- "ピアリング" --> Spoke2
hub_Vnet <-- "ピアリング" --> Spoke3


%%サブグラフのスタイル
classDef subG fill:none,color:#345,stroke:#345
class hubsub,sub1,sub2,sub3 subG

classDef VnetG fill:none,color:#0a0,stroke:#0a0
class hub_Vnet,Spoke1,Spoke2,Spoke3 VnetG

例2
入力:
ハブ&スポークの構成です。
スポークは3つあります。
ハブとスポークはすべて異なるサブスクリプションにデプロイされています。
ハブには VM が1台あります。
1つのスポーク用サブスクリプションには Storage Account があります。
出力:
---
title: ハブ&スポークの例 | VM, PaaS, Subscription の追加
---
graph TB;

%%グループとサービス
subgraph hubsub["Hub Subscription"]
	subgraph hub_Vnet[hub-Vnet]
		VM1("VM")
	end
end

subgraph sub1["Spoke Subscription"]
	subgraph Spoke1["Spoke 1"]
	end
	ST1{{"fa:fa-folder Storage Account"}}
end

subgraph sub2["Spoke Subscription"]
	subgraph Spoke2["Spoke 2"]
	end
end

subgraph sub3["Spoke Subscription"]
	subgraph Spoke3["Spoke 3"]
	end
end


%%サービス同士の関係
hub_Vnet <-- "ピアリング" --> Spoke1
hub_Vnet <-- "ピアリング" --> Spoke2
hub_Vnet <-- "ピアリング" --> Spoke3


%%サブグラフのスタイル
classDef subG fill:none,color:#345,stroke:#345
class hubsub,sub1,sub2,sub3 subG

classDef VnetG fill:none,color:#0a0,stroke:#0a0
class hub_Vnet,Spoke1,Spoke2,Spoke3 VnetG

%%ノードのスタイル
classDef SCP fill:#e83,color:#fff,stroke:none
class VM1 SCP

classDef PaaSG fill:#46d,color:#fff,stroke:#fff
class ST1 PaaSG

例3
入力:
サブスクリプションとリソースグループの階層構成を出力してください。
サブスクリプションは1つ。リソースグループは3つです。
それぞれのリソースグループにはリソースが2つ含まれています。
出力:
---
title: サブスクリプション全体構成
---
graph TD
sub1[サブスクリプション] --> rg1[リソースグループ1]
sub1 --> rg2[リソースグループ2]
sub1 --> rg3[リソースグループ3]
rg1 --> r1[リソース1]
rg1 --> r2[リソース2]
rg2 --> r3[リソース3]
rg2 --> r4[リソース4]
rg3 --> r5[リソース5]
rg3 --> r6[リソース6]
`;

interface ICatChatResult extends vscode.ChatResult {
    metadata: {
        command: string;
    }
}


// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {

	// Chat の処理を行う関数 
	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 == 'azure_figure') {	// コマンドが azure_figureコマンドの場合
			stream.progress('Mermaid の図を考えています...');	// プログレスバーを表示
			try {
				const [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR);	// モデルを取得
				if (model) {	// モデルが取得できた場合
					const messages = [	// メッセージを作成
						vscode.LanguageModelChatMessage.User(system_pronpt),	// システムプロンプトを追加
						vscode.LanguageModelChatMessage.User(request.prompt)	// ユーザーの入力を追加
					];

					const chatResponse = await model.sendRequest(messages, {}, token);	// モデルにリクエストを送信

					stream.markdown('```\n');	// マークダウンのコードブロックを開始
					for await (const fragment of chatResponse.text) {	// レスポンスのテキストを取得
						stream.markdown(fragment);		// マークダウンに追加
					}
					stream.markdown('\n```');	// マークダウンのコードブロックを終了
				}
			} catch (err) {	// エラーが発生した場合
				handleError(err, stream);
			}

			return { metadata: { command: 'azure_figure' } };	// メタデータを返す
		} else {	// それ以外の場合
			try {
				const [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR);	// モデルを取得
				if (model) {	// モデルが取得できた場合
					const messages = [	// メッセージを作成
						vscode.LanguageModelChatMessage.User(`あなたは Mermaid を使って図を書くことが得意です。`),
						vscode.LanguageModelChatMessage.User(request.prompt)
					];

					const chatResponse = await model.sendRequest(messages, {}, token);	// モデルにリクエストを送信
					for await (const fragment of chatResponse.text) {	// レスポンスのテキストを取得
						// Process the output from the language model
						stream.markdown(fragment);
					}
				}
			} catch (err) {
				handleError(err, stream);	// エラーが発生した場合
			}

			return { metadata: { command: '' } };	// メタデータを返す
		}
	};

	// Chat participants appear as top-level options in the chat input
	// when you type `@`, and can contribute sub-commands in the chat input
	// that appear when you type `/`.
	const mermaid = vscode.chat.createChatParticipant(CHAT_PARTICIPANT_ID, handler);	// ChatParticipantを作成
	mermaid.iconPath = vscode.Uri.joinPath(context.extensionUri, 'azure_mermaid.jpg');	// アイコンを設定
	mermaid.followupProvider = {	// フォローアッププロバイダーを設定
		provideFollowups(result: ICatChatResult, context: vscode.ChatContext, token: vscode.CancellationToken) {	// フォローアップを提供
			return [{	// フォローアップを返す
				prompt: 'create a figure',	// プロンプトを設定
				label: vscode.l10n.t('Create the figure'),	// ラベルを設定
				command: 'azure_figure'	// コマンドを設定
			} satisfies vscode.ChatFollowup];	// ChatFollowupを返す
		}
	};

	// よくわからないので ほとんどコピペ
	context.subscriptions.push(	// ChatParticipantを登録
		mermaid,	// ChatParticipantを登録
		// 
		vscode.commands.registerTextEditorCommand(MERMAID_NAMES_COMMAND_ID, async (textEditor: vscode.TextEditor) => {	// テキストエディタコマンドを登錻
			// Get the text from the editor
			const text = textEditor.document.getText();

			let chatResponse: vscode.LanguageModelChatResponse | undefined;	// ChatResponseを初期化
			try {
				const [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4' });	// モデルを取得
				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) {	// エラーが LanguageModelError の場合
					console.log(err.message, err.code, err.cause)
				} else {	// それ以外の場合
					throw err;
				}
				return;
			}

			// Clear the editor content before inserting new content
			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));
			});

			// 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);
				});
			}
		}),
	);
}

function handleError(err: any, stream: vscode.ChatResponseStream): void {
    // making the chat request might fail because
    // - model does not exist
    // - user consent not given
    // - quote limits exceeded
    if (err instanceof vscode.LanguageModelError) {
        console.log(err.message, err.code, err.cause);
        if (err.cause instanceof Error && err.cause.message.includes('off_topic')) {
            stream.markdown(vscode.l10n.t('I\'m sorry, I can only explain computer science concepts.'));
        }
    } else {
        // re-throw other errors so they show up in the UI
        throw err;
    }
}

// This method is called when your extension is deactivated
export function deactivate() { }

8. 画像の作成

チャットのアイコンに使う画像を適当に生成しました
Azure の構成図を作成するマーメイドです

azure_mermaid.jpg
生成したアイコン:

使ってみる

F5 で実行してみます
なんか言われましたが、よくわからんので [Debug anyway] で突き進みます

vsc07
なんか表示された:

こんな風に実行できました

先ほどのは GPT-4 をモデルに利用していました
GPT-3.5 turbo を使用した場合はこちらです

生成はできていますが、色付けがうまくいっていない部分がありますね
1回ずつしか試していないので、これだけで精度の判断はできないですが、、、
勘違いでした。どちらのモデルでもうまく実行できていますね

おまけ

README を Copilot に考えてもらいました
ありがたい

配布してみる

F5 で実行して動くことが確認できました
自作した Copilot Extension を他の人に使ってもらえるようにしてみます

.vsix ファイルで配布

Visual Studio Code の拡張機能は .visx というファイルにして配布することができます
ファイルを作成する前に、package.json に GitHub のレポジトリの情報を追記しておきます
※Git のレポジトリ情報がなくても注意されるだけで .visx ファイルの作成は可能です

1
2
3
4
    "repository": {
        "type": "git",
        "url": "https://github.com/NakayamaKento/github_copilot_extension_mermaid"
    },

.visx ファイルの作成は、拡張機能を開発したディレクトリで下記のコマンドを実行するだけです

1
npx vsce package

実行結果はこちらです
途中、ライセンスに関する情報がなくて注意されていますが、気にせずそのまま進めました

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
PS C:\Users\kento\mermaid-azure> npx vsce package
Executing prepublish script 'npm run vscode:prepublish'...

> mermaid-azure@0.0.1 vscode:prepublish
> npm run package


> mermaid-azure@0.0.1 package
> webpack --mode production --devtool hidden-source-map

    [webpack-cli] Compiler starting... 
    [webpack-cli] Compiler is using config: 'C:\Users\kento\mermaid-azure\webpack.config.js'
    [webpack-cli] Compiler finished
asset extension.js 7.36 KiB [compared for emit] [minimized] (name: main) 1 related asset
./src/extension.ts 12.7 KiB [built] [code generated]
external "vscode" 42 bytes [built] [code generated]
webpack 5.92.0 compiled successfully in 1259 ms
 WARNING  LICENSE.md, LICENSE.txt or LICENSE not found
Do you want to continue? [y/N] y
 DONE  Packaged: C:\Users\kento\mermaid-azure\mermaid-azure-0.0.1.vsix (7 files, 167.65KB)

作成した .visx ファイルを拡張機能としてインポートしてみます
そのまえにインポート前の状態を確認してみます

vsc08
.vsix ファイルのインポート前:

スクリーンショットの通り、拡張機能のメニューの 3点リーダー> Install from VSIX を選択し、作成した .vsix ファイルを選択します
日本語の場合は「VSIX からのインストール」のような表現だと思います

vsc09
.vsix ファイルのインポート:

無事に拡張機能としてインポートができました

vsc10
.vsix ファイルのインポート後:

Visual Studio Code Insider での動作確認もできました

インポートした .vsix のアンインストールは画像の通りです

vsc11
.vsix ファイルのアンインストール:

MarketPlace で配布

せっかく人生初 自作 拡張機能なので自慢したいです
MarketPlace で公開してみます
公式ページ Publishing Extensions | Visual Studio Code Extension API と日本語解説記事 VisualStudio Code ExtensionのMarketplaceへの公開方法 - Hello Ys world ? を参考にしました

vsce のインストール

vsce という拡張機能を管理するツールをインストールします

npm install -g @vscode/vsce

Azure DevOps の準備

MarketPlace 公開用に Azure DevOps のアカウント作成、Organization の作成、トークン取得を行います
アカウントは既に持っていたので作成は割愛します
せっかくなので専用の Organization を作成します

azdev01
Organization の作成:

スクリーンショットのように [Personal Access Tokens] を選択します

azdev03
Personal Access Tokens の選択:

[New Token] を選択し、以下の内容でトークンを作成します

項目 入力内容
Name (任意) mermaid-azure merketplace
Organization All accessible organizations
Expiration (任意) デフォルトのままにしました
Scopes Custom defined
Marketplace (画面下部 Show All Scopes を選択すると表示) Manage
azdev04
トークンの作成01:
azdev05
トークンの作成02:

作成すると個人用トークンが表示されるので忘れずにメモしておきます

azdev06
トークンの作成03:

Publisher の作成

拡張機能の発行元になる Publisher を作成します
トークンを取得したのと同じアカウントで Create Publisher | Visual Studio Marketplace にアクセスします

必須の Name と ID を入力します
これらは拡張機能の発行元情報として表示されます (公式から画像を拝借)

copy01
ID と Name の使われるところ:

自分はこのように入力しました
画面に映っていないですが、アイコン画像のアップロードもしました

azdev07
Publisher の作成:

作成した Publisher を確認するために vsce login <publisher id> を実行します
自分の場合は vsce login kenakay です
途中で Personal Access Token を聞かれるので、メモした内容を入力します

1
2
3
4
5
PS C:\Users\kento\mermaid-azure> vsce login kenakay
https://marketplace.visualstudio.com/manage/publishers/
Personal Access Token for publisher 'kenakay': ****************************************************

The Personal Access Token verification succeeded for the publisher 'kenakay'.

package.json の更新

package.json に Publisher の情報を追記しておきます

1
  "publisher": "kenakay",

拡張機能の公開

拡張機能の公開は GUI と CLI の2種類あります
参考にした VisualStudio Code ExtensionのMarketplaceへの公開方法 - Hello Ys world ? では CLI だったので、あえて GUI でやってみます

GUI でアップロードするための .vsix を作成するため vsce package を実行します
Visual Studio Marketplace publisher management page にアクセスし、[+New Extension] > [Visual Studio Code] を選択します
先ほど作成した .vsix をアップロードします

アップロードが完了しましたが、検証にエラーが出てしまいました

azdev08
検証エラー:

エラー内容を見ても具体的に何がダメなのかわからなかったので CLI での方法に切り替えてみました
vsce publish を実行します
すると以下のエラーが表示されました
Copilot Extenstion で使用する API がプレビューのため弾かれていたようです
エラーで記載のあるパラメータを入力しても、アップロード後の検証でエラーになるため、ダメでした

1
2
PS C:\Users\kento\mermaid-azure> vsce publish
 ERROR  Extensions using unallowed proposed API (enabledApiProposals: [chatVariableResolver], allowed: []) can't be published to the Marketplace. Use --allow-proposed-apis <APIS...> or --allow-all-proposed-apis to bypass. https://code.visualstudio.com/api/advanced-topics/using-proposed-api

Marketplace への公開はしばらくは できそうにないです
残念

まとめ

インフラ エンジニアが GitHub Copilot Extensions の開発をしてみました!

Visual Studio Code の拡張機能 開発も初めて、TypeScript も初めて、右も左もよくわからん状態でした
Copilot に1行ずつ説明してもらうのは、初学者にはめちゃくちゃいい学び方だなーと思いました

NakayamaKento/github_copilot_extension_mermaid

参考

共有

Kento
著者
Kento
2020年に新卒で IT 企業に入社. インフラエンジニア(主にクラウド)として活動中