GitHub Copilot Extension を .NET で作成する
ごきげんよう!mihohoi です。
GitHub Copilot Extension を作りたいなと思って早数か月…
ふと .NET チャンネルで GitHub Copilot Extension を作る動画がアップロードされていることに気づき「これはキタ!!」ということで、.NET 初心者が動画を見ながら作ってみました。
Build Your First GitHub Copilot Extension - YouTube
余談ですが、この間参加した Microsoft Ignite 2024 で、この方のセッション参加したことを思い出しました。お茶目な素敵な方でした。(プレゼンが激ツヨ)
GitHub Copilot Extension をつくるために必要準備しておくこと
そもそも、 GitHub Copilot Extensions とは何者か?というところですが、私の理解では「AI を使ったコード提案や生成などの機能を独自に構築することができる拡張機能」です。GitHub Copilot を自分好みの拡張機能に出来る、と言えば簡潔でしょうか。
その GitHub Copilot Extension を作るために必要な用語として以下があります。
Copilot agent
Copilot エージェントについて - GitHub Enterprise Cloud Docs
抜粋します。
Copilot agents は、Copilot Extensions に埋め込まれたカスタム ツールです。 これらは Copilot Chat と統合され、特定のニーズに合わせて調整された追加機能が提供されます。
GitHub Apps
作成したアプリケーションを GitHub 上で使えるように登録するものです。
GitHub App の使用について - GitHub Docs
抜粋します。
GitHub Apps は、GitHub の機能を拡張するツールです。 GitHub Apps を使うと、GitHub で、issue を開く、pull request にコメントする、プロジェクトを管理する、といったことを行うことができます。 また、GitHub で発生するイベントに基づいて、GitHub の外部で何かを行うこともできます。 たとえば、GitHub で issue が開かれたときに、GitHub App で Slack に投稿できます。
GitHub Copilot Agent を作成し、それを GitHub Apps として登録することで、GitHub Copilot Extension に統合できる機能を提供することができます。Copilot Agent は、Extension を強化するためのバックエンドモジュールのような役割を果たし、ユーザーが必要とする高度なタスクを実行する仕組みを追加します。
実際に作ってみよう
元ネタは、冒頭で紹介した URL になります。動画の長さは 30 分ほどなので、見ながら書いても小1時間もあれば大丈夫だと思いますが、備忘録と理解もかねて書いていきます。ちなみに私は1時間半かかりました、2回見たので。
紹介した動画はとってもわかりやすいので、元の動画もぜひ見て欲しいです。
最初の準備
Visual Studio 2022 を開き、Create Project で "ASP.NET Core Web API" にしてプロジェクトを作成します。
使わない機能はオフにしておきます。
プロジェクトが出来上がったら、 Programs.cs を更地にします。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/info", () => "Hello Copilot!");
app.Run();
launchSettings.json
の https を /info
で開きたいため、以下のように変更します。
"https": {
~略~
"launchUrl": "info",
~略~
},
この状態でまずは稼働することを確認します。
ブラウザが立ち上がり、"Hello Copilot!" と表示されればOKです。
この後、プロジェクトの開始設定を「Dev Tunnels」>「Create」して、Access を Public にします。
再度 Start して、localhost ではなく 限定的な URL が付与されたのを確認して、表示されている URL をメモしておきます。
書いていく(語彙力)
ベースができたので、ここから書いていきます。
まずは必要なものを入れていきます。Nuget〜
dotnet add package Octokit --version 13.0.1
using を記載します。
using Microsoft.AspNetCore.Mvc;
using Octokit;
using System.Diagnostics;
using System.Net.Http.Headers;
Callback を簡易的に入れる。
app.MapGet("/callback", () => "Hello Copilot!");
Copilot と Web サービスの間でメッセージをやり取りする事になるため、2つの追加のサポート タイプが必要になるので用意。app.Run();
配下にクラスを作成します。
必要なクラス
- Message: 役割とコンテンツで、Copilot との会話の一部として使用するもの
- Payload: GitHub システムから Copilot , Web サービスに渡されるもの
internal class Message
{
public required string Role { get; set; }
public required string Content { get; set; }
}
internal class Payload
{
public bool Stream { get; set; }
public List<Message> Messages { get; set; }
}
実際の処理を書いていきます。一旦以下で仮置き。
app.MapPost(
"/", async ([FromHeader(Name = "X-GitHub-Token")] string githubToken,
[FromBody] Payload payload) =>
{
// 処理を書いていく
});
ユーザーが誰であるかを検証する必要があるので、ユーザーに関する情報を取得します。
var octokitClient =
new GitHubClient(new Octokit.ProductHeaderValue(appName))
{
// Token を取得してユーザー情報を Get する
Credentials = new Credentials(githubToken)
};
var user = await octokitClient.User.Current();
Console.WriteLine($"User: {user.Login}");
その下に、Copilot にお仕事をさせる追加のコンテキストを書いていきます。
// Insert special pirate-y system messages in the message list.
payload.Messages.Insert(0, new Message
{
Role = "system",
Content = $"Start every response with the user's name, which is @{user.Login}"
});
payload.Messages.Insert(0, new Message
{
Role = "system",
Content = "// ここに何か書く"
});
私は大好きなキャラクター…いやキャラクターではない、神ですね、彼を降臨させるためプロンプトを書きました。
その後ろに、書いたメッセージを取得する必要があります。ユーザーが聞いた内容を GitHub Copilot に渡して HTTP クライアントをインスタンス化してアクセストークンを提供します。
最後に payload が準備出来たらメッセージを送信します。
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", githubToken);
payload.Stream = true;
で、ユーザーにメッセージを投稿するコードを書きます。
var copilotLLMResponse = await httpClient.PostAsJsonAsync(
"https://api.githubcopilot.com/chat/completions", payload);
// Stream the response straight back to the user.
var responseStream = await copilotLLMResponse.Content.ReadAsStreamAsync();
return Results.Stream(responseStream, "application/json");
これで実装は完了です。
GitHub App への登録
コードが完成したら、GitHub App へ登録します。
Copilot 拡張機能用の GitHub アプリの作成 - GitHub Enterprise Cloud Docs
登録が完了したら、メニューの「Copilot」> 「URL」に Dev Tunnels で発行された URL を記載します。リリース範囲も自分だけにしておきましょう。
実際に試す
それでは、GitHub 画面を使って実際に試していきます。
@
を入力すると、作成した GitHub Copilot Extension が出てきます。
それでは実際に聞いてみましょう。
無事に返答が返ってきました!いい...!!!
まとめ
簡単な応答をためすことが出来ました。これを発展すれば、自分専用にカスタマイズしたものを作ることができます。
プロジェクトで使用する場合は、コードの規約を書いておけば、出力する内容をそれにそった形で回答させることも出来そうです。(コツがいりそう)
おまけ(使用したコード)
Program.cs
のコードを置いておきます。
using Microsoft.AspNetCore.Mvc;
using Octokit;
using System.Diagnostics;
using System.Net.Http.Headers;
var appName = "name";
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/info", () => "Hello Copilot!");
app.MapGet("/callback", () => "Hello Copilot!");
app.MapPost(
"/", async ([FromHeader(Name = "X-GitHub-Token")] string githubToken,
[FromBody] Payload payload) =>
{
var octokitClient =
new GitHubClient(new Octokit.ProductHeaderValue(appName))
{
Credentials = new Credentials(githubToken)
};
var user = await octokitClient.User.Current();
Console.WriteLine($"User: {user.Login}");
// Insert special pirate-y system messages in the message list.
payload.Messages.Insert(0, new Message
{
Role = "system",
Content = $"Start every response with the user's name, which is @{user.Login}"
});
payload.Messages.Insert(0, new Message
{
Role = "system",
Content = "<何か書く>"
});
// Use Copilot's LLM to generate a response to the user's messages.
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", githubToken);
payload.Stream = true;
var copilotLLMResponse = await httpClient.PostAsJsonAsync(
"https://api.githubcopilot.com/chat/completions", payload);
// Stream the response straight back to the user.
var responseStream = await copilotLLMResponse.Content.ReadAsStreamAsync();
return Results.Stream(responseStream, "application/json");
});
app.Run();
internal class Message
{
public required string Role { get; set; }
public required string Content { get; set; }
}
internal class Payload
{
public bool Stream { get; set; }
public List<Message> Messages { get; set; }
}
Discussion