🍕

ASP.NET Core のサンプルにある Pizza 屋を OpenAI で DX していく

2023/04/09に公開

ASP.NET Core の Training ページにあるやつを改変していく

今回はまず ASP.NET Core を使用して Web UI を作成する に書かれている ASP.NET Core のアプリケーションを作成します。
Training の内容が若干不親切なのはおいておいて、一旦それが済んだということにします。
サンプルでは、ピザと金額などを入力するのですが、そのピザの説明については入力していません。
一般的な マルゲリータ とか ペパロニ とかを売ってるピザ屋を目指すのであれば、その説明をわざわざ人間が入力する必要はありません。
OpenAI の力を借りて、ピザの説明を自動生成し、DX を進めていきましょう。

Disclaimer

私は ASP.NET Core や .NET、C# の professional ではなく、実際のソースコードも ChatGPT や GitHub Copilot を利用して作成しています。
そのため、なんかイケてない部分がいっぱいあるんだとは思いますが、そのうち気が向いたら直しますので、ご指摘あればコメントまでお願いします。
そもそもこの記事を書いている途中で Blazor と Razor という、なんかスペル似てるけど違うんだなこれ、というのに気づいた程度の知識です。

前提条件

  • ASP.NET Core のアプリとしては Training にある Pizza 屋の在庫管理を利用します
  • OpenAI 側の準備としてはアカウントの作成と、課金情報の登録、そして API Key の取得が必要です

Models/Pizza.cs

さて、簡単な Model から編集していきます。

このファイルでは Description という field? を追加していきます。
もともと Name という field を持っていますので、それをもとにおいしそうなピザの表現を OpenAI に聞き出して、Description に保存します。
型については Name を見習って public string? としています。
Null 許容値型 というやつです、たしか。

using System.ComponentModel.DataAnnotations;

namespace RazorPagesPizza.Models;

public class Pizza
{
    public int Id { get; set; }

    [Required]
    public string? Name { get; set; }
    public PizzaSize Size { get; set; }
    public bool IsGlutenFree { get; set; }
    public string? Description { get; set; }

    [Range(0.01, 9999.99)]
    public decimal Price { get; set; }
}

public enum PizzaSize { Small, Medium, Large }

Pages/Pizza.cshtml

次は Pages の方を修正していきます。

.cshtml の方は table を持っていますので、Description も表示するように列を追加します。

<table class="table mt-5">
    <thead>
        <tr>
            <th scope="col">Name</th>
            <th scope="col">Price</th>
            <th scope="col">Size</th>
            <th scope="col">Gluten Free</th>
            <th scope="col">Description</th>
            <th scope="col">Delete</th>
        </tr>
    </thead>
    @foreach (var pizza in Model.pizzas)
    {
        <tr>
            <td>@pizza.Name</td>
            <td>@($"{pizza.Price:C}")</td>
            <td>@pizza.Size</td>
            <td>@Model.GlutenFreeText(pizza)</td>
            <td>@pizza.Description</td>
            <td>
                <form method="post" asp-page-handler="Delete" asp-route-id="@pizza.Id">
                    <button class="btn btn-danger">Delete</button>
                </form>
            </td>
        </tr>
    }
</table>

Pages/Pizza.cshtml.cs

次は Pages の中の .cshtml.cs の方を修正していきます。

いいか悪いかがわからないのですが、とりあえず動いてるのでよし、ということでこのような実装にしています。
Training の完了時点からの変更は OnPost()OnPostAsync() に変えています。
これは、OpenAI の API を叩くために async にしているので、それに合わせて OnPost()OnPostAsync() に変えています。
ただし、OpenAI の API の応答が返ってくるのが遅いため、await をあえて入れず、見た目上の画面遷移を速くしています。

namespace RazorPagesPizza.Pages
{
    public class PizzaModel : PageModel
    {
        public List<Pizza> pizzas = new();
        [BindProperty]
        public Pizza NewPizza { get; set; } = new();

        public void OnGet()
        {
            pizzas = PizzaService.GetAll();
        }

        public string GlutenFreeText(Pizza pizza)
        {
            return pizza.IsGlutenFree ? "Gluten Free" : "Not Gluten Free";
        }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            PizzaService.AddAsync(NewPizza);
            return RedirectToAction("Get");
        }

        public IActionResult OnPostDelete(int id)
        {
            PizzaService.Delete(id);
            return RedirectToAction("Get");
        }
    }
}

Services/PizzaService.cs

最後に一番苦労した Service です。

メインの変更点である GetDescriptionFromNameAsync() に関しては、ChatGPT API利用方法の簡単解説 という記事を参考にさせていただきました。
Authorization header を利用した curl のサンプルがあったので、それを ChatGPT にお願いして C# に書き換えた感じです。
本当は // から始まるコメントを書けば GitHub Copilot がいい感じに関数書いてくれないかなーと思ったのですが、まだ学習していなかったように思います。

若干工夫した点としては、synchronous な範囲でまずは pizza.Description を "生成中..." という文字列をはめて Pizzas.Add() しておきます。
そのあと、GetDescriptionFromNameAsync() で OpenAI の API を叩いて pizza.Description を更新しています。
これにより、画面には "生成中..." と表示されているので、ユーザーは待っていることがわかります。
そして、OpenAI の API からの応答が返ってきたら、その時点で画面には "生成中..." ではなく、OpenAI の API からの応答が表示されます。
ただし、ブラウザの画面側に自動的に更新するような処理は書いていないので、ユーザーは手動で画面を更新する必要があります。
最初は全体を Async/await の形にしていたのですが、OpenAI の応答が返り切るまで待つことになるため、数秒間ぐるぐるしているような感じになります。

OpenAI に渡す script については「あなたはプロのピザ職人です。」から始まるパターンを採用させていただいています。

using RazorPagesPizza.Models;
using System.Text;
using System.Text.Json;

namespace RazorPagesPizza.Services;
public static class PizzaService
{
    private static readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
    private static readonly ILogger _logger = _loggerFactory.CreateLogger(nameof(PizzaService));

    private static readonly HttpClient _client;

    static List<Pizza> Pizzas { get; }
    static int nextId = 3;

    // add initial pizzas
    static PizzaService()
    {
        Pizzas = new List<Pizza>
        {
            new Pizza { Id = 1, Name = "Classic Italian", Price=20.00M, Size=PizzaSize.Large, IsGlutenFree = false },
            new Pizza { Id = 2, Name = "Veggie", Price=15.00M, Size=PizzaSize.Small, IsGlutenFree = true }
        };
        _client = new HttpClient();
    }

    public static List<Pizza> GetAll() => Pizzas;

    public static Pizza? Get(int id) => Pizzas.FirstOrDefault(p => p.Id == id);

    public static async Task AddAsync(Pizza pizza)
    {
        if (pizza.Name is null)
            return;

        pizza.Id = nextId++;
        pizza.Description = "生成中...";
        Pizzas.Add(pizza);

        string response = await GetDescriptionFromNameAsync(name: pizza.Name);
        using (JsonDocument document = JsonDocument.Parse(response))
        {
            JsonElement root = document.RootElement;
            JsonElement choices = root.GetProperty("choices");
            JsonElement message = choices[0].GetProperty("message");
            JsonElement content = message.GetProperty("content");
            pizza.Description = content.GetString();
        }
        Update(pizza);
    }

    public static void Delete(int id)
    {
        var pizza = Get(id);
        if (pizza is null)
            return;

        Pizzas.Remove(pizza);
    }

    public static void Update(Pizza pizza)
    {
        var index = Pizzas.FindIndex(p => p.Id == pizza.Id);
        if (index == -1)
            return;

        Pizzas[index] = pizza;
    }

    // get detailed description from its name by OpenAI
    public static async Task<string> GetDescriptionFromNameAsync(string name)
    {
        // get openAI key from environment variable
        var openAIKey = Environment.GetEnvironmentVariable("OPENAI_KEY");
        var url = "https://api.openai.com/v1/chat/completions";
        //var client = new HttpClient();

        var request = new HttpRequestMessage(HttpMethod.Post, url);
        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", openAIKey);

        var requestBody = $@"{{
                ""model"": ""gpt-3.5-turbo"",
                ""messages"": [
                    {{""role"": ""user"", ""content"": ""あなたはプロのピザ職人です。{name} というピザについて説明してください。表現力たっぷりになるべくおいしそうな感じが伝わるような表現を使ってください。""}}
                ]
            }}";
        _logger.LogInformation(requestBody);

        request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");

        var response = await _client.SendAsync(request);
        var responseContent = await response.Content.ReadAsStringAsync();

        return responseContent;
    }
}

環境変数

これもどこから設定するのか ChatGPT に聞いたのですが、ベストプラクティスなのかはよくわからないです。
一応動いてるのはそうなんですが、なんかいい感じの設定がある気がする。

  1. Visual Studio で、プロジェクトを開きます。
  2. [プロジェクト] メニューから [プロパティ] を選択します。
  3. [デバッグ] タブを選択します。
  4. [環境変数] セクションで、新しい環境変数を追加します。
  5. 追加した環境変数の値を設定します。
  6. プログラムを実行します。

visual-studio-envvar

DX 完了後の Pizza 屋をご笑覧ください

今回は私の大好きな「クアトロフォルマッジ」をこの Pizza 屋のメニューに追加してみます。
名前以外は適当に入れます。

aspnetcore-pizza-before

Add ボタンを押した直後、ページはぐるぐるせずにすぐに更新されます。
この際には Description はまだ生成されていないので、"生成中..." と表示されます。

aspnetcore-pizza-processing

しばらくしたのちに画面を F5 で更新すると、Description が生成されています。
「表現力たっぷりになるべくおいしそうな感じが伝わるような表現を使ってください。」と prompt で与えたとおり、非常においしそうに感じる説明が生成されました。

aspnetcore-pizza-after

おいしそうな説明がいっぱい生成されるので、おなかがすきますね。。。

aspnetcore-pizza-variety

まとめ

今回は、ASP.NET Core で Razor Pages を使って、Pizza 屋のメニューを管理するアプリをさらに DX してみました。
OpenAI の GPT-3 を使って、Pizza の名前から説明を生成する機能を追加しました。
結構 OpenAI の API を叩いた気がするのですが、コストは $0.02 などと表示されており、誤差みたいなもんですね。
いい使い方なのかどうかはさておき、「なんにでも OpenAI を注ぐ女」の気持ちでこれからもおもちゃにしていきたいと思います。

参考

https://qiita.com/mikito/items/b69f38c54b362c20e9e6

https://qiita.com/kotattsu3/items/d6533adc785ee8509e2c

Microsoft (有志)

Discussion