GitHub Copilot + GPT-4o/Claude Sonnet で ASP.NET Core minimal な web api を作成する

Copilot の Agent を “GPT-4o” に変えて、minmal web api を作成していきます。vscode 上で使っている間はどちらもで変わらないし、いま使っている限りではどちらが上という感じはしません。課金的に安いほうを選べばいいんじゃないでしょうか?

- データベース定義 : `ddl.md`
- API 定義 : `mos-api.yaml`
- 開発環境 : `asp.net minimal`
- データベース : `MySQL`

だけ決めておいて、プロンプトをスタートさせます。

dotnet new webapi 

でプロジェクトを作成した後に、次のプロンプトでスタートです。

ddl.md と mos-api.yaml を参照して asp.net minimal で web api を作成して。
データベースは MySQL を使用して。

mos-api.yaml には products や orders の CRUD が含まれているのですが、それは追えていないようです。これが gpt-4o だからなのかは不明ですが、ビルドが終わったら少しずつ追加していきます。

最初の修正

DefaultConnection を追加して。
ポート番号を 8000 にして。
エンドポイントを localhost:8000/api/categories でアクセスできるようにして。
Models のクラスを ddl.md のカラム名にあわせて。

なんと!Models/Category.cs のプロパティ名に合わせてデータベースのほうを修正しようとします。しかも MySQL なのに pgsql を使おうとするので、ここでストップ。

どうやら、GPT-4o では無理そうなので、Claude Sonnet 4 に切り替えます。

まあ、普通はそうですね。C# の場合は Column 属性で名前を合わせます。データベースが MySQL の場合は大文字小文字が区別されるので、テーブルを参照するときのカラム名はすべて設定しておいたほうが無難です。

using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace aspnet_minimal_sample.Models
{
    public class Category
    {
        public int Id { get; set; }
        public string Slug { get; set; } = string.Empty;
        public string Title { get; set; } = string.Empty;
        public string? Description { get; set; }
        public string? Image { get; set; }
        
        [Column("sortid")]
        public int SortId { get; set; } = 0;
        
        public int Display { get; set; } = 1;
        
        [Column("created_at")]
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        
        [Column("updated_at")]
        public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
        
        [Column("deleted_at")]
        public DateTime? DeletedAt { get; set; }
    }
}

GPT-4o の尻ぬぐいは Claude Sonnet にやってもらいましょう。

mos-api.yaml に従って、products や orders のエンドポイントも作って。
categories も mos-api.yaml のように修正して。
レスポンスの JSON を categoryId ではなく category_id のように mos-api.yaml に揃えて。
localhost:8000/mos/api/products/1 のレスポンスを mos-api.yaml にあわせて

あちこち抜けているので、mos-api.yaml の仕様にあっているかチェックしてもらいます。

JSON のレスポンスが mos-api.yaml の仕様にあっているか全体をチェックして。

指示した視点でコードのレビューができるので、git へのプルリクエストのコードのチェックとかできるもしれません。この場合 PR のマスター役がやるというよりも、PR を出す人があらかじめ Claude Code などでコードレビューをしておいてね、という感じでしょうか。

nuxt-sample と接続してみる

さて asp.net minimal がほどよく作成できたので、実際にクライアントと接続して試してみましょう。既にできている(筈)の nuxt-sample を実行します。

npm run dev

カテゴリ一覧からカテゴリを選択すると「読み込み中…」で止まってしまいます。

商品一覧で画像が表示されません。「品切れ」はよくわからない。こんな機能をいれたっけ?

商品をクリックすると「読み込み中…」になってしまいます。

このあたり、nuxt-sample のバグなのか asp.net minimal のバグかわからないので、vscode でひとつのワークスペースで扱って調査していきます。

バグ取り開始

next-sample でカテゴリを選択したとき「読み込み中...」のまま止まります。

vscode で aspnet-minimal-sample と next-sample をひとつのワークスペースにいれておきます。これで、両方のプロジェクトを Claude Sonnet が参照できるようになります。

ひとまず aspnet 側の CORS 問題だったらしいので、カテゴリ内の商品表示ができるようになりました。

商品詳細のページも表示できるようになっています。

画像に関しては products.image カラムのデータに拡張子を含むようにしたので、next-sample のページで “.jpeg” しているところを外します。

画像ファイルは next-sample のほうではなく、aspnet-minimal-sample のサーバーのほうに置きたいので尋ねてみます。

aspnet-minimal-sample のほうに public を置けますか?

next-sample のほうの img タグのほうも書き変えて、画像が表示されるようになります。

レイアウトにつかうときの画像ファイルは next-sample のほうの public に置けばよいのですが、商品データの画像はデータベースや public の適当なところに置くことになるので aspnet-minimal-sample のほうに置きます。

あとは、カートに入れて注文番号が表示されれば ok です。

ひとまず、これで web api のほうも含めて作成完了です。

ASP.NET Core minimal なのでワンコードで確認ができる。

Laravel の場合と比較すると、aspnet minimal の場合は Program.cs に全て詰め込まれています。

// カテゴリ API エンドポイント
app.MapGet("/mos/api/categories", async (AppDbContext db, int? display) =>
{
    var query = db.Categories.AsQueryable();
    
    if (display.HasValue)
        query = query.Where(c => c.Display == display.Value);
    
    var categories = await query.ToListAsync();
    return Results.Ok(new { items = categories, total = categories.Count });
});

app.MapPost("/mos/api/categories", async (AppDbContext db, Category category) =>
{
    db.Categories.Add(category);
    await db.SaveChangesAsync();
    return Results.Created($"/mos/api/categories/{category.Id}", category);
});

app.MapGet("/mos/api/categories/{id}", async (AppDbContext db, int id) =>
{
    var category = await db.Categories.FindAsync(id);
    return category is not null ? Results.Ok(category) : Results.NotFound();
});

app.MapPut("/mos/api/categories/{id}", async (AppDbContext db, int id, Category category) =>
{
    var existingCategory = await db.Categories.FindAsync(id);
    if (existingCategory is null) return Results.NotFound();
    
    existingCategory.Slug = category.Slug;
    existingCategory.Title = category.Title;
    existingCategory.Description = category.Description;
    existingCategory.Image = category.Image;
    existingCategory.SortId = category.SortId;
    existingCategory.Display = category.Display;
    existingCategory.UpdatedAt = DateTime.UtcNow;
    
    await db.SaveChangesAsync();
    return Results.Ok(existingCategory);
});
...

本格的な仕事コードだとエラー処理とかの不安があるので、MVC パターンの Controller ベースにしたいところですが、このぐらいの実験コードとか管理モードで社内ツールとして使う場合にはルーティングやデータベースアクセス(特にCRUDのみ)はひとつにまとまっておいたほうが修正がしやすいです。Claude Code のような AI を使う場合にはファイルを横断してくれますが、人が手動で修正する場合は「置換」を多用するのでひとつのファイルにまとまっていたほうが便利です。

実際に、web api の機能としては Program.cs のコード量は 300行弱でしかないので、あれこれファイルを分割するよりもひとつのファイルにまとまった方が編集もしやすいでしょう。

参照先

mos-ai-sample/src/webapi/aspnet-minimal-sample at master · moonmile/mos-ai-sample https://github.com/moonmile/mos-ai-sample/tree/master/src/webapi/aspnet-minimal-sample

カテゴリー: 開発 | GitHub Copilot + GPT-4o/Claude Sonnet で ASP.NET Core minimal な web api を作成する はコメントを受け付けていません

Copilot + Claude で Laravel の Web API を作る

一番手慣れた手段として、Laravel で作ってみます。

OpenAPI 仕様 mos-api.yaml に従って Controller を作って。

これまで OpenAPI の yaml/json から各プログラム言語のコードを作るときはジェネレーターを動かす必要があったのですが、Claude Sonnet を使うと一切必要ありません。

  • routes/api.php にルーティングの追加
  • app/Models/* にモデルクラスの追加
  • app/Http/Controllers/* にコントローラークラスの追加
  • database/migrations/* にマイグレーションコードの追加

この手のコンバートツールは個人で作りがちだし、開発プロジェクト内でも標準ツールにしがちなのですが、フレームワークのバージョンが上がったりすると乖離してしまうので自作はお薦めしません。できることならば OSS にある標準のものを使うか、以後は Claude Sonnet のような生成AIものにしておくか、というところです。

あと、windows 上の mysql を動かすと毎回出てくるエラーにも対処しておきます。

SQLSTATE[HY000]: General error: 1709 Index column size too large. The maximum column size is 767 bytes. (Connection: mysql, SQL: alter table `users` add unique `users_email_unique`(`email`)) 

の対処は?
class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        // Set default string length to 191 for MySQL compatibility
        Schema::defaultStringLength(191);
    }
}

これで php artisan migrate:fresh が正常に終了します。

標準のままだと、/api のプレフィックスが付くので、これを修正します。

api/mos/api/categories



mos/api/categories

にするには?

以下のコマンドを動かせばよいそうです。

php artisan route:list --path=mos

postman で動作確認

postman を使って web api にアクセスしてみます。これは成功

localhost:8000/mos/api/categories

OpenAPI SwaggerUI でアクセス

vscode の swaggerUI でアクセスしてみます。

どうやら CORS でエラーになっているっぽいです。

swaggerUI でアクセスをすると CORS のエラーがでます。

実は、この対処だけではエラーになるので、Claude にログも確認してもらいます。

どうやら、Laravel に最初に入っている CORS の機能と、Sanctumミドルウェア、CORS ミドルウェアの兼ね合いが良くなかったみたいですね。

このあたりの laravel の middleware の設定が良く分かっていないので、なんとも言えないのですが、ひとまずエラーが無くなります。

app.php

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        apiPrefix: '',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        // Add CORS middleware alias
        $middleware->alias([
            'cors' => \Illuminate\Http\Middleware\HandleCors::class,
        ]);

        // Apply CORS middleware globally to API routes
        $middleware->api(append: [
            \Illuminate\Http\Middleware\HandleCors::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

cors.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Cross-Origin Resource Sharing (CORS) Configuration
    |--------------------------------------------------------------------------
    |
    | Here you may configure your settings for cross-origin resource sharing
    | or "CORS". This determines what cross-origin operations may execute
    | in web browsers. You are free to adjust these settings as needed.
    |
    | To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
    |
    */

    'paths' => ['mos/api/*', 'api/*'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => false,

];

Seeder を入れる

カテゴリ(categories)と商品(products)に初期値を入れたいので、これを Claude に作って貰います。

categories と puroducts の seed のサンプルを作成して。

ひな形ができたので、これをハンバーガー注文サイトのカテゴリと商品名に手作業で修正。

CategorySeeder.php

class CategorySeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $categories = [
            [
                'id' => 1,
                'slug' => 'special1',
                'title' => '今月のお薦め',
                'description' => '今月のお薦め商品を紹介します。',
                'image' => 'special1.jpg',
                'sortid' => 1,
                'display' => 1,
            ],
            [
                'id' => 2,
                'slug' => 'special2',
                'title' => 'ネット注文特別価格メニュー',
                'description' => '',
                'image' => 'special2.jpg',
                'sortid' => 2,
                'display' => true,
            ],

ProductSeeder.php

class ProductSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */


    public function run(): void
    {
        // カテゴリの最大値からランダムidを取得
        function fake_category_id() {
            $maxId = Category::max('id');
            return rand(1, $maxId);
        }

        $products = [
            [
                'id' => 1,
                'slug' => 'burger1',
                'name' => 'モスバーガー',
                'description' => '',
                'image' => 'burger1.jpg',
                'price' => 440.00,
                'sortid' => 1,
                'display' => true,
                'category_id' => fake_category_id(),
            ],
            [
                'id' => 2,
                'slug' => 'burger2',
                'name' => 'モスチーズバーガー',
                'description' => '',
                'image' => 'burger2.jpg',
                'price' => 480.00,
                'sortid' => 2,
                'display' => true,
                'category_id' => fake_category_id(),
            ],

たぶん、テスト用に大量に作成する手段はあるのですが、ひとまず新人研修で作成したものをコピーします。このあたり、Copilot にテストデータを作って貰ってもいいと思います。

OpenAPI の戻り値を修正する

web api で  GET /products/{id} を呼び出したときに、category_id がそのまま返ってきているので、これを categories を検索するよう変更します。

mos-api.yaml の GET /products/{id} を呼び出した時に、
category_id と一緒に categories の内容も戻して。

ProductController.php と mos-api.yaml の両方を修正してくれます。

GET /categories の戻りが配列だけになっているので、以前のクライアントで受けられるように items の中に配列を作ります。正確には mos-api.yaml を直すのですが、直し方がわからないのでレスポンスの JSON 例を示して Copilot に修正してもらいましょう。

web api で GET /categories のレスポンスを次のように変更して。

JSON 形式の例

{
"items": [
{
"id": 1,
"slug": "special1",
"title": "今月のお薦め",
"description": "今月のお薦め商品を紹介します。",
"image": "",
"sortid": 1,
"display": true,
"created_at": "2025-06-05T11:57:01+09:00",
"updated_at": "2025-06-05T11:57:01+09:00",
"deleted_at": null
},
...
{
"id": 10,
"slug": "sidemenu3",
"title": "デザート",
"description": "",
"image": "",
"sortid": 22,
"display": true,
"created_at": "2024-06-19T02:49:19+09:00",
"updated_at": "2024-06-19T02:49:19+09:00",
"deleted_at": null
}
],
"total": 10
}

この部分はプロトタイプや社内ツールならば Claude の提案したテンプレートでもよいのですが、Web API の場合は既存の形式に合わせることも多いので妥協せずに少し突っ込みます。

指定したカテゴリ内の商品を取得する場合、/products?category_id=1 で実装済みではあるのですが、これも /products/slug/{category_slug} 形式に修正します。

カテゴリ内の商品一覧を取得する web api を
/products/slug/{category_slug}
のようにカテゴリの slug を使って。
POST /orders では、
次の JSON 形式の例に変更して。

{
total_price: 0,
total_quantity: 0,
items: [
{
id: 1,
price: 1000,
quantity: 2,
},
{
id: 2,
price: 2000,
quantity: 3,
},
],
}

これでほぼ完成です。後は

  • ログイン機能の追加
  • 管理画面で使うためのルーティングをログイン状態へ移動
  • CRFS, CROS 機能の復活

あたりでしょうか。

ファイル数が多くなると、指示によってあちこちのファイルの手を入れることになるので時間が掛かります。この現象自体はAIであっても人間であっても同じなのですが、MVC パターンの Web API の場合は、Model/Controller そしてルーティングの3か所に手をいれないといけないのが難点です。この現象はもともとの MVC パターンのアプリケーションからあるもので今に限ったものではないのですが、人が修正するときは3か所同時に手をいれないといけないので大変でした。

そういう意味では、Claude Sonnet の場合は多少時間がかかるものの複数のファイルに手を入れることに躊躇はしません。たまにコンパイルが出来ないコードを吐き出しますが、Claude 自らコンパイルエラーを読み解いたり、実行時のエラーを読み解くことでコードを修正していきます。その部分では開発者はナビゲーターとしての役割やレビュアやテスターの視点を持てるのが便利なところです。自分がやると「面倒くさい」が先に立ってしまって、オブジェクト指向的にもう少し依存関係が少なくならないか?と考えてしまいますからね。

参照

mos-ai-sample/src/webapi/laravel-webapi-sample at master · moonmile/mos-ai-sample https://github.com/moonmile/mos-ai-sample/tree/master/src/webapi/laravel-webapi-sample

カテゴリー: 開発 | Copilot + Claude で Laravel の Web API を作る はコメントを受け付けていません

Copilot + Claude Sonnet を活用して web api を作る下準備

ここまで、3つのクライアント(React, Blazor, Kotlin)を作ってきましたが、実際のプロジェクトでは Web API も同時に作ることが多いです。既存のシステムの場合は、既に web api があるかもしれませんが。

mock のダミー画像データを作る

新人研修ではモスバーガーの画像をそのまま借用してしまっているのですが、版権的に良くないのでダミー画像を作ります。注文サイト&管理サイトの研修方式は数年間からやっていて、このダミー画像が結構面倒なのですが、いまだと OpenAI の DALL-E でできます。

当然のことながら OpenAI の API KEY が必要になりますが、そこは必要経費ということで。

# ハンバーガー注文サイト用の画像ファイルを作成する

## カテゴリ用の画像作成


```
slug title filename
-------- ------------------------------------- --------------------
"special1" "今月のお薦め" "special1.jpg"
"special3" "限定メニュー" "special3.jpg"
"special2" "ネット注文特別価格メニュー" "special2.jpg"
"main1" "メインメニュー" "main1.jpg"
"main2" "ハンバーガー" "main2.jpg"
"main3" "ホットドック" "main3.jpg"
"main4" "ソイパティ" "main4.jpg"
"sidemenu1" "サイドメニュー" "sidemenu1.jpg"
"sidemenu2" "ドリンク・スープ" "sidemenu2.jpg"
"sidemenu3" "デザート" "sidemenu3.jpg"
```

カテゴリの title に適した画像ファイルを 690x560 px で作成してください。

こんな形でドキュメントを書いて、Copilot に渡しますが、あえなく挫折。

Claude Sonnet だと画像ができないそうなので。これを ChatGPT でちまちま作ってもいいのですが、カテゴリも10種類、商品となるハンバーガー等の画像は100個ぐらいが必要になります。

なので、量産するコードをを書いて貰いましょう。

openai を使って、プロンプトを指示して、指定サイズの画像を作るコードを書いて。

ツールで使う言語は何でもいいのですが、C# のコンソールアプリにしています。

using OpenAI;
using OpenAI.Images;
using System.ClientModel;

class Program
{
    // カテゴリ情報の定義
    private static readonly Dictionary<string, string> CategoryInfo = new()
    {
        { "special1", "今月のお薦め" },
        { "special3", "限定メニュー" },
        { "special2", "ネット注文特別価格メニュー" },
        { "main1", "メインメニュー" },
        { "main2", "ハンバーガー" },
        { "main3", "ホットドック" },
        { "main4", "ソイパティ" },
        { "sidemenu1", "サイドメニュー" },
        { "sidemenu2", "ドリンク・スープ" },
        { "sidemenu3", "デザート" }
    };

    // 各カテゴリに対応する英語プロンプト
    private static readonly Dictionary<string, string> CategoryPrompts = new()
    {
        { "special1", "A beautiful and appetizing seasonal hamburger set with fresh ingredients, warm orange and yellow color scheme, professional food photography, featuring 'Monthly Recommendation' text overlay" },
        { "special3", "A premium limited edition hamburger with luxury presentation, elegant black and gold color scheme, high-end food photography, featuring 'Limited Menu' text overlay" },
        { "special2", "A special price hamburger with discount elements, eye-catching red and white color scheme, online ordering theme, featuring 'Special Online Price' text overlay" },
        { "main1", "A variety of delicious hamburgers arranged together, appetizing brown and red color scheme, main menu showcase, featuring 'Main Menu' text overlay" },
        { "main2", "A classic hamburger cross-section showing layers of bun, patty, and fresh vegetables, vibrant green, red, and brown colors, featuring 'Hamburger' text overlay" },
        { "main3", "A delicious hot dog with sausage, bun, mustard and ketchup, warm brown and red color scheme, featuring 'Hot Dog' text overlay" },
        { "main4", "A healthy soy patty burger with fresh vegetables, healthy green and brown color scheme, emphasizing healthiness, featuring 'Soy Patty' text overlay" },
        { "sidemenu1", "A variety of side dishes including french fries and onion rings, golden yellow color scheme, featuring 'Side Menu' text overlay" },
        { "sidemenu2", "Various drinks and soups with glasses, cups, ice, and steam, refreshing blue and clear color scheme, featuring 'Drinks & Soup' text overlay" },
        { "sidemenu3", "Delicious desserts including ice cream and pies, sweet pastel color scheme, happy atmosphere, featuring 'Dessert' text overlay" }
    };

    static async Task Main(string[] args)
    {
        // OpenAI API キーを環境変数から取得
        var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
        if (string.IsNullOrEmpty(apiKey))
        {
            Console.WriteLine("エラー: OPENAI_API_KEY 環境変数が設定されていません。");
            Console.WriteLine("使用方法: set OPENAI_API_KEY=your_api_key_here");
            return;
        }

        var client = new OpenAIClient(apiKey);

        // 出力ディレクトリを作成
        var outputDir = Path.Combine(Directory.GetCurrentDirectory(), "generated_images");
        Directory.CreateDirectory(outputDir);

        Console.WriteLine("ハンバーガー注文サイト用画像生成ツール");
        Console.WriteLine("========================================");

        if (args.Length == 0)
        {
            Console.WriteLine("使用方法:");
            Console.WriteLine("  すべてのカテゴリ画像を生成: dotnet run all");
            Console.WriteLine("  特定のカテゴリ画像を生成: dotnet run <slug>");
            Console.WriteLine("  カスタムプロンプトで生成: dotnet run custom \"<prompt>\" <filename>");
            Console.WriteLine();
            Console.WriteLine("利用可能なカテゴリ:");
            foreach (var category in CategoryInfo)
            {
                Console.WriteLine($"  {category.Key} - {category.Value}");
            }
            return;
        }

この手のやつは、ちまちま本に書いた覚えがあるのですが、AI に書いて貰うほうが断然楽です。どうせ使い捨てツールなので、中身はどうということはありません。

実行すると、こんな感じで画像を作ってくれます。

商品画像のほうは量が多いのでデータベースから読み込むようにしてもいいのですが、これも使い捨てツールなので商品画像の作成機能を追加してしまいます。

同じパターンで、商品画像の作成も追加して。

互換性もなんのその、豪快にメニューを書き変えて「カテゴリ画像」と「商品画像」の両方が生成できるようにしてくれます。

        Console.WriteLine("ハンバーガー注文サイト用画像生成ツール");
        Console.WriteLine("========================================");

        if (args.Length == 0)
        {
            Console.WriteLine("使用方法:");
            Console.WriteLine("  すべてのカテゴリ画像を生成: dotnet run category-all");
            Console.WriteLine("  特定のカテゴリ画像を生成: dotnet run category <slug>");
            Console.WriteLine("  すべての商品画像を生成: dotnet run product-all");
            Console.WriteLine("  特定の商品画像を生成: dotnet run product <slug>");
            Console.WriteLine("  カスタムプロンプトで生成: dotnet run custom \"<prompt>\" <filename>");
            Console.WriteLine();
            Console.WriteLine("利用可能なカテゴリ:");
            foreach (var category in CategoryInfo)
            {
                Console.WriteLine($"  {category.Key} - {category.Value}");
            }
            Console.WriteLine();
            Console.WriteLine("利用可能な商品:");
            foreach (var product in ProductInfo)
            {
                Console.WriteLine($"  {product.Key} - {product.Value}");
            }
            return;
        }

生成中です。

ひとまず、これで版権をクリアした画像の作成が完了。

テーブル定義

DDL定義(テーブル定義)を作成しておきます。

会社でデータベース設計書などを作るときは Excel を使うことが多いと思うのですが、おそらく ddl.md というドキュメントファイルを作って、create table の定義を並べておいたほうが AI に理解しやすいです。いったん、MySQL Workbeanch とか、SQL Server Management Studio などを使ってテーブル作成した後にスクリプトに落とし込んでもいいでしょう。

# DDL 定義

## categories

カテゴリのテーブル定義です。

```sql
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug varchar(255) NOT NULL UNIQUE,
title varchar(255) NOT NULL,
description TEXT,
image varchar(255),
sortid INTEGER NOT NULL DEFAULT 0,
display INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
);
```
## products

商品のテーブル定義です。

```sql
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER,
slug varchar(255) NOT NULL UNIQUE,
name varchar(255) NOT NULL,
description TEXT,
image varchar(255),
price REAL NOT NULL,
sortid INTEGER NOT NULL DEFAULT 0,
display INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id)
);
```

## orders

注文のテーブル定義です。

```sql
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_number varchar(10) NOT NULL,
total_price REAL NOT NULL,
total_quantity INTEGER NOT NULL,
status INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
);
```

## order_products

注文と商品を関連付ける中間テーブルの定義です。

```sql
CREATE TABLE order_products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
price REAL NOT NULL,
quantity INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
```

コードファースト形式の場合、マイグレーションのコードをプログラムに書くことになるのですが、これは言語やフレームワークに依存してしまいます。Ruby, Laravel, C# などそれぞれの言語でしか使えない形式になってしまうので、CREATE TABLE のように SQL のままのほうがよいと思います。たぶん、各言語で書いたとしても別の言語に AI がコンバートしてくれるだろうから、大丈夫だとは思うのですが。

OpenAPI定義を書く

web api の定義として、RESTful で書いていくか、OpenAPI で書いていくか、ルーティングをどう記述していくかが問題になると思いますが、ここでは OpenAPI定義を直接書いていきます。

この部分も諸々仕様書に書き起こすこともできるのですが、現状だと

  • テーブル定義から OpenAPI 定義を書き出す
  • OpenAPI 定義から設計書等へ書き出す

のように、リバース型式でドキュメントを書いたほうが楽です。

db/ddl.md のテーブル定義を参考にして、
mos-api.yaml に CURD を呼び出せる openapi を書き出して。

このように DDL の定義から CRUD 形式を一発で作成してくれます。

openapi: 3.1.1
info:
title: Mos API
description: Mos API
version: 1.0.0
contact:
name: moonmile solutions
servers:
- url: "http://localhost:8000/mos/api"
paths:
# Categories CRUD
/categories:
get:
summary: Get all categories
operationId: getCategories
parameters:
- name: display
in: query
description: Filter by display status
required: false
schema:
type: integer
enum: [0, 1]
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Category'
post:
summary: Create a new category
operationId: createCategory
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CategoryCreate'
responses:
'201':
description: Category created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Category'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
...

ハンバーガー注文サイトの場合は、注文 /orders など、いくつかの web api を用意することがあるのですが、ひとまず Copilot で mos-api.yaml を書いて貰います。

vscode でプレビューもできるし、実際にここから web api を呼び出すこともできます。

参照

moonmile/mos-ai-sample: opilot + Claude Sonnet 4 でハンバーガー注文サイト作成のサンプル https://github.com/moonmile/mos-ai-sample

  • src/tools/make-image ダミー画像の作成ツール
  • src/db/ddl.md テーブル定義
  • src/webapi/mos-api.yaml OpenAPI 定義

カテゴリー: 開発 | Copilot + Claude Sonnet を活用して web api を作る下準備 はコメントを受け付けていません

Github Copilot + Claude Sonnet で Kotlin の Android アプリを作ってみる

同じパターンで Android もいけるだろう、ということで Android Studio + Kotlin の組み合わせで作ってみます。React Native とか Flutter ならいけそうなので、ここはあえて Kotlin で。

カテゴリ一覧を表示するページを追加して。

豪快に MainActivity を書き変えてくれますが、まあ、初手はそれでもいいです。

カテゴリ一覧の取得には web api で 10.0.2.2:8000/mos/api/categories を使って。

戻り値 JSON の例

{
"items": [
{
"id": 1,
"slug": "special1",
"title": "今月のお薦め",
"description": "今月のお薦め商品を紹介します。",
"image": "",
"sortid": 1,
"display": true,
"created_at": "2025-06-05T11:57:01+09:00",
"updated_at": "2025-06-05T11:57:01+09:00",
"deleted_at": null
},
...

React や Blazor のときと同じようにカテゴリ一覧を web api 呼び出しに変更します。

Android の場合はインターネット接続のためのパーミッションを入れる必要があり、きちんと AndroidManifest.xml に追加してくれます。あと、build.gradle にも必要なライブラリを追加してくれます。

ひとまずカテゴリ一覧の作成が完了

コードは MainActivity から CategoryListScreen を呼び出して、データは CategoryViewModel から拾うようになっていますね。ベタで書くよりは ViewModel を使ったほうが良さそうです。

次はカテゴリ内の商品を表示させます。

カテゴリ一覧のカテゴリをクリックしたときに、カテゴリ内の商品の一覧を表示して。
web api は /products/slug/{category_slug} を使って。

レスポンスの例

{
"items": [
{
"id": 13,
"category_id": 1,
"slug": "hotdog3",
"name": "スパイシーチリドッグ",
"description": "",
"image": "m011",
"price": 470,
"sortid": 13,
"display": true,
"created_at": "2024-06-19T02:49:19+09:00",
"updated_at": "2024-06-19T02:49:19+09:00",
"deleted_at": null
},
...

既に web api ができているので、このサンプルを貼り付ければ ok 。ちなみにエミュレータからはホストのPCに対して 10.0.2.2 で接続が可能です。

MainActivity を書き変えて MosNavigation を呼び出すようにしています。この手のナビゲーション機能は、設計時に使うか使わないかの判断をするのですが(そうしないとコーディングの変更が多くて大変)、Claude Sonnet を使うと後からの大幅な変更もやってくれます。

このあたり、アジャイル的にUIを変更するにしても、React や Flutter であったとしても UI 構造の大幅な変更は避けたいものです。なので、共通レイアウトを作ってみたり、共通のベースクラス(BaseActivityを継承するとか)をやりがちになるのですが、生成AIを使ったときにはそのあたりの手間は惜しみません。オブジェクト指向設計の「共通化」の部分がごっそり抜けてあっても大丈夫です。むしろ継承はしなくていい感じですね。このあたりは、将来的なコード技術の進化もあるでしょうが、あれこれと継承しなくてよいのは人間にも楽です。

商品一覧で、商品を選択したときに商品詳細を表示して。

web api は /products/{id}

戻り値の例

{
"id": 1,
"category_id": 6,
"slug": "burger1",
"name": "モスバーガー",
"description": "",
"image": "m001",
"price": 440,
"sortid": 1,
"display": true,
"created_at": "2024-06-19T02:49:19+09:00",
"updated_at": "2024-06-19T02:49:19+09:00",
"deleted_at": null
}

プロンプトはだんだん手慣れてきて、こんな感じに書くようになります。特にプロンプト技術っぽいのもは入っていません。あまり複雑なこともしないし、step by step で進んでいくので、AI 側にも誤解が少ないのでしょう。というか、曖昧さを排除していけば ok です。

そういう意味では、ペアプロしていてドライバー役で後ろから指示を出している感じですよね。キーボード担当の Claude Sonnet が一生懸命コーディングしている感じ。

商品詳細に表示される画像を切り替えます。

商品詳細の画像データは、

10.0.2.2:8000/images/{画像コード}.jpeg

で取得ができるので、これを切り替えて。

画像が表示されるようになりました、が、間違った画像が表示されています。

というか、絶妙にサンプルデータを突っ込んでいますw

これを修正させます。

カテゴリで「今月のお薦め」を選択したときの商品の数が多すぎます。

実際は

localhost:8000/mos/api/products/slug/special1

{
"items": [
{
"id": 13,
"category_id": 1,
"slug": "hotdog3",
"name": "スパイシーチリドッグ",
"description": "",
"image": "m011",
"price": 470,
"sortid": 13,
"display": true,
"created_at": "2024-06-19T02:49:19+09:00",
"updated_at": "2024-06-19T02:49:19+09:00",
"deleted_at": null
},
{
"id": 15,
"category_id": 1,
"slug": "soiburger2",
"name": "ソイバーガー",
"description": "",
"image": "m013",
"price": 260,
"sortid": 15,
"display": true,
"created_at": "2024-06-19T02:49:19+09:00",
"updated_at": "2024-06-19T02:49:19+09:00",
"deleted_at": null
}
],
"total": 2
}

の2つの商品だけです。

サンプルデータを表示していませんか?
カテゴリスラッグ別の商品データは

/mos/api/products/slug/{slug}

を使って。

endpoint のアドレスが間違っていたので、サンプルデータが表示されてしまったようですね。サーバーに接続できない場合にはエラーが出るのが普通だと思うのですが、ここの Claude Sonnet ではサンプルデータを表示するようになっています。後で、これは修正しましょう。

次はカートの機能を実装してもらいましょう。

続きは昼の後で…

商品詳細で「カートに追加」で、カートに追加する機能を実装して。

「カートに追加」ボタンの機能はあるらしいのですが、カートの中身が見れませんw。仕方がないので、カートの中身を見るページを作って貰いましょう。

チキンナゲットの画像があるはずなのですが、絵文字になっているのでうまく拾えていないようです。これは後でチェック。

右上に「カート」のメニューを作って。

CategoryListScreen.kt や ProductDetailScreen.kt のページにがんがんとトップメニューを追加していきます。最初からいれておけよとか、共通化したらどうか?というのは無しです。これは React で作った時も同じですが、人の場合は共通化しないと手間が掛かったり間違ったりするのですが、AIの場合は間違わないのでがんがん手をいれいきます。いや、実際は間違えるのですが、間違えない振りをして多くのファイルに手を入れていく方法に躊躇がありません。

修正方法を間違えるというのは、たまにビルドが通らなかったり文法エラーを起こしているためです。文法エラーになる頻度はそう多くはないのですが、それなりにあります。そのたびに、ビルドができなかった原因を Claude Sonnet が自らチェックをして直していきます。そのあたり、一発で正しいコードが出て来ないので反復という点でうまくいっているような気がします。そのあたり、反復して少しずつ修正していくという手法が実に人間っぽいです。AIですが。

右上にカートのアイコン、そしてカートのページが実装されました。

「注文する」ボタンを実装して。

web api は POST /mos/api/orders

送信する JSON 形式の例

{
total_price: 0,
total_quantity: 0,
items: [
{
id: 1,
price: 1000,
quantity: 2,
},
{
id: 2,
price: 2000,
quantity: 3,
},
],
}
注文した結果が

{ "order_number": "00000000" }

となるので、この注文番号を表示して。

というわけで、やっとこさ注文画面の作成が完了です。これらを一発で書けるようなプロンプトを作ってもよいのですが、ソフトウェアは同じものを作ることはまずありえないので、ペアプロっぽく少しずつ組み立てるのがコツです。

Next.js や Nuxt.js とは違い、ビルドをして Android エミュレータに転送しないと動作確認ができない分だけ、ちょっと時間がかかります。それでも午前中に始めて、多少間があいて現時点(22時頃)には終わります。間に他の仕事をしていたので、おそらく半日でここまでいけます。

おそらく、手慣れた Android アプリの開発者であれば、似た感じのものを半日で作れると思います。ですが、Copilot + Claude Sonnet の組み合わせだと、まあそれなりのものがこのペースで作れるわけです。果たして新人が作れるかどうかは分かりませんが(おそらく新人だとコードの内容が理解できないので、ハマると大変そうです)、何回か Android アプリを作っている人であれば、同じ形でできるでしょう。コードやファイル構造を眺めると、こんな感じです。

  • UI に Jetpack Compose を使う
  • ViewModel を使う
  • Retrofit を使う
  • NavHostController を使う
  • データクラスをちまちま作る

という構造になっています。これがベストな方法かよくわからないのですが、現状の Claude Sonnet のベストプラクティスなのだと思います。

サンプルコード

https://github.com/moonmile/mos-ai-sample/tree/master/src/client/mos-kotlin

カテゴリー: 開発 | Github Copilot + Claude Sonnet で Kotlin の Android アプリを作ってみる はコメントを受け付けていません

Copilot + Claude Sonnet 4 と Blazor の組み合わせ

前回の続きで、注文画面を Blazor を使って作ってみるテスト。React と同じように GitHub Copilot + Claude Sonnet 4  の組み合わせを使う訳。

先と同じパターンで Web API は実装済みという状態で、適宜 postman などを使って送受信の JSON の形式を Copilot に伝えて作業をすすめていく。

エージェントモードで指示をする

dotnet new blazor

で、ひな形のプロジェクトを作ってから、Agent モードで指示をします。

カテゴリ一覧の Categories.razor を作って。

注文サイト系は Claude Sonnet の中でも学習済みだろうから、定番のものを引き当ててくれます。これ、見たことがないページの場合はどうなるのか不安なところがあるのと同時に、定番のものは定番のページに寄せてくるので結構余分な機能も付けてくれます。

カテゴリ一覧は webapi で localhost:8000/mos/api/categories を呼び出して。

レスポンスの JSON 形式の例

{
"items": [
{
"id": 1,
"slug": "special1",
"title": "今月のお薦め",
"description": "今月のお薦め商品を紹介します。",
"image": "",
"sortid": 1,
"display": true,
"created_at": "2025-06-05T11:57:01+09:00",
"updated_at": "2025-06-05T11:57:01+09:00",
"deleted_at": null
},
...

カテゴリ一覧に「新しいカテゴリの追加」ボタンを付けてくれます。内部実装はしていないのですが、この手の気の利いた機能っぽいものはきちんと削っていかないとセキュリティリスクが高まるところです。この場合、web api ではカテゴリの追加を実装していないので大丈夫なのですが、実装済みだったりするとうっかり呼ばれてしまいかねません。

React と同じようにプロンプトの指示をだしていきます。

商品一覧の商品をクリックしたときに、商品詳細ページを開いて
web api は /products/{id}

戻り値の例

{
"id": 1,
"category_id": 6,
"slug": "burger1",
"name": "モスバーガー",
"description": "",
"image": "m001",
"price": 440,
"sortid": 1,
"display": true,
"created_at": "2024-06-19T02:49:19+09:00",
"updated_at": "2024-06-19T02:49:19+09:00",
"deleted_at": null
}
商品詳細ページで「カートに追加」の機能を実装して。
カートの中身は Cart.razor で見れるようにして。
ショッピングカートで「注文手続き」へを実装して。
web api を POST /orders して、

呼び出しJSON の例

{
total_price: 0,
total_quantity: 0,
items: [
{
id: 1,
price: 1000,
quantity: 2,
},
{
id: 2,
price: 2000,
quantity: 3,
},
],
}

ここまでカートの機能まで実装してくれます。

何故か、Blazor で作っているときは注文の確認ページを作ってくれます。今回の場合は、確認ページがいらなくて、直接注文の web api を呼び出して欲しいので、プロンプトで修正を指示します。

注文確認ページを削除して。
「注文手続き」を「注文する」に変更して。
「注文する」ボタンをクリックしたときに /orders を呼び出して。
web api /orders の戻り値は

{ "order_number": "00000000" }

となるので、この注文番号 order_number を表示して。

このあたりを一気に最初のプロンプトで指示するのは無理そうなので、ペアプロ風にコーディングを進めていくのがよいでしょう。なので、直接コードは書けなくてもコードが読めるようにならないと駄目っぽいです。

ただし、Claude Sonnet が先行き、人が読みやすいコードを出力するのか、読む人の能力を超えてしまうコードを吐き出すのか(人が直せないコードになる)はわかりません。

余分な web api 呼び出しを修正する

ある程度コードができあがったところで画面の操作していると、商品一覧 Product.razor を表示するときにひどく時間が掛かっています。dotnet run の結果をみると、同じ web api を4回程呼び出しています。

商品一覧を表示するときに同じ web api を 4回呼び出しています。
これを1回になるように修正して。

info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET http://localhost:8000/mos/api/products/slug/special1
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET http://localhost:8000/mos/api/products/slug/special1
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response headers after 2404.2005ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 2404.2535ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET http://localhost:8000/mos/api/categories
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET http://localhost:8000/mos/api/categories
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response headers after 2366.16ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 2366.2342ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET http://localhost:8000/mos/api/products/slug/special1
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET http://localhost:8000/mos/api/products/slug/special1
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response headers after 2366.8227ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 2366.8871ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET http://localhost:8000/mos/api/categories
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET http://localhost:8000/mos/api/categories
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response headers after 2374.857ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 2374.9186ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET http://localhost:8000/mos/api/products/slug/special1
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET http://localhost:8000/mos/api/products/slug/special1
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response headers after 2423.4487ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 2423.5976ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET http://localhost:8000/mos/api/categories
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET http://localhost:8000/mos/api/categories
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response headers after 2369.3309ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 2369.4243ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET http://localhost:8000/mos/api/products/slug/special1
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET http://localhost:8000/mos/api/products/slug/special1
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response headers after 2397.9347ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 2398.0152ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[100]
Start processing HTTP request GET http://localhost:8000/mos/api/categories
info: System.Net.Http.HttpClient.Default.ClientHandler[100]
Sending HTTP request GET http://localhost:8000/mos/api/categories
info: System.Net.Http.HttpClient.Default.ClientHandler[101]
Received HTTP response headers after 2371.7073ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
End processing HTTP request after 2371.7812ms - 200

つまり、表面上動いているっぽいコード(実際に Claude Sonnet は動かしていないわけですが)ではあるものの、いわゆる「非機能要件」は人間が確認しないといけません。Copilot はある程度正解のコードを出してはくれますが、必ずしもそれが正解とは限らないのです。

何回か修正を繰り返すのですが、最終的には必ず2回 web api を呼び出すことになります。

実は、Blazor をサーバーサイドンダリングで表示させるときに、

  • ブラウザ上でのプリレンダリングの表示
  • サーバーサイドでのレンダリングでの表示

のために、同じ web api を 2回呼び出してしまいます。

    protected override async Task OnParametersSetAsync()
    {
        // カテゴリスラッグが変更された場合のみAPIを呼び出す(重複防止)
        if (!string.IsNullOrEmpty(CategorySlug) && 
            CategorySlug != lastLoadedCategorySlug && 
            !isLoadingInProgress)
        {
            isLoadingInProgress = true;
            isLoading = true;
            lastLoadedCategorySlug = CategorySlug;
            
            try
            {
                await LoadProducts();
            }
            finally
            {
                isLoadingInProgress = false;
            }
        }
    }

たぶん OnParametersSetAsync じゃなくて OnInitialized を使えばいいと思うのですが、ここは手をいれていません。

ショッピングカートの部分でレイアウトずれが発生していますが、ひとまず機能的にはokな状態です。

このあたりは「人間が目でみて重箱の隅をつつく」作業が必要になってきます。つまりは、テスト工程を人がやる必要がでてきます。

アイコンが二重に表示される問題を手作業で修正。

数量の部分が改行されてしまっているので、これも直さないといけないです。

サンプルコード

https://github.com/moonmile/mos-ai-sample/tree/master/src/client/blazor-sample

ちなみに、Blazor に関しては、第2版を出したのでこれで。

プリレンダリングあたりとか@inject のあたりが書いてあったりします。前回はクライアントサイドのSPAが中心だったのですが、今回はサーバーサイドのSSRが中心になっています。Next.js や Nuxt.js と同じタイプです。

カテゴリー: 開発 | Copilot + Claude Sonnet 4 と Blazor の組み合わせ はコメントを受け付けていません

Copilot + Claude Sonnet 4 でショッピングサイト風なものを作る

ここ数年、新人研修でハンバーガー注文サイトと管理画面を作っている。最初は Vue2 ベースで web api を Laravel、次が Vue3 ベースで web api が Laravel、今年は、Vue3/Nuxt.js で web api が Laravel という形になっている。

ここ数年でシングルページアプリケーション(SPA)の風潮から再びサーバーサイドレンダリング(SSR)に戻ってきている感じで、Vue2 にしても、Vue3/Nuxt3 という変化からそれに続いする形で課題も変わってきているわけですが。

この課題の目的としては

  • 注文サイトのような主に GET が多く使われて、レイアウトが重視される別々な画面設計
  • 管理サイトのような CRUD のパターンが多く使われ、画面が量産できるパターン

を実体験するというものがある。

個人的に言えば、後者の管理サイトみたいなのは Ruby on Rails などの特定ツールを使って一気に作ってしまうほうがよい。開発プロジェクトにとって、管理系の画面は定型のものが多く、利用者はコンピュータ―にある程度強いという前提が成り立っている。なので、多少レイアウトや使い勝手を犠牲にしてでも、MVCパターン系のツール(CakePHPのbekeとか)を使って100枚ぐらい一気に作ってしまうと、またそれ系のツールを自作して一気に作るほうがよい。

とは思うんだが、開発者それぞれなので。。。

折角なのでReactで作る

100億年振りに触るのですが、Reactで作ってみます。確か Scratch の中身を改造しようとしたときとか、React Native や Expo でアプリを作ろうとして挫折したとか、別件だけど Flutter のコードに辟易してしまったとき以来なわけで、どうも JSX 形式が慣れないんですが。まあ、生成AIを使ってやって貰うんだったら何でもいいですよね。という訳です。

クイックスタート – React https://ja.react.dev/learn

実は、最初は Copilot を使ってコードをちまちま修正するかと思っていたのですが、なにか勢いで Copilot が直接コードを触ってくれるモードがあったので、うっかりと同意してしまった次第です。

ひとまず、VSCode + Copilot の状態で「Agent」モードを有効にします。ちょっと前まで、Github 上でレポジトリを変更するとかなんとかいう機能を紹介していたような気がするのですが、まあ、これでコードを Copilot が触ってくれます。実際には内部で Claude Sonnet が動いています。

この手のコード生成ツールは色々と違いがあるようですが、私の使い方だとどれも大差がありません。最初にでかい設計書や厳密なプロンプトを作っておいて、一気に生成しようという方が多いのですが、それだと違いが出るとは思いますが、私はそんな使い方をしません。アジャイル開発風にペアプログラミングをしていきましょう。生成AIにコード生成を頼みながら、テストやコードのレビューをしつつ、再びプロンプトに追加分を打ち込んでいけばいいわけです。ペアプロで対話をすればいいわけです。

余談ですが、厳密な設計書は厳密なプロンプトから目的のサイト作成を一気に作るように頑張る位だったら「厳密な設計書は設定から目的のサイトを自動作成するツールを作成する」のがベターですね。つまりは、先の Ruby on Rails のようなツールを生成AIに作って貰う乃至は協同で作るといパターンです。魚を得るのではなくて釣り竿を作るとか、金を掘るのではなくてツルハシを売るとかジーンズを売るとかそいうレベルの話です。

ページ単位でプロンプトで指示する

npx create-next-app@latest

で next.js のプロジェクトを作成した後に vscode で開きます。

Coilot は “Agent” モードにしておきます。普通のチャットを使いたいときは “Ask” にすれば ok です。

npm run dev

しておいて、画面が確認できる状態にしておきます。

仕事だと画面設計から入るとか共通のフレームワークがあると思うのですが、そのあたりは「プロトタイプを作る」ことに割り切ってしまいます。協同作業なので Copilot がやりやすいように作って貰いましょう。

カテゴリ一覧のページを categories.tsx で作成して。

以前のチャットだとちまちまと手作業でコードに入れる(あるいはコード上でチャットに聞く)ことが主流でしたが、エージェントモードだと書くコードに直接手をいれてくれます。Microsoft 365 の Copilot もこれくらい手を入れてくれるとよいのですが、そのうち変更してくれるかもしれません(パワポはちょっと手をいれてくれる)

コード変更に関しては「保持する」ボタンをぽちぽちと押していきます。本当はコードレビューをしたほうがいいのでしょうが、中身を読んでもあまりよくわからないし、なによりも JSX を触りたくはないですからね。Copilot によしなにやってもらいましょう。

ほどよくダミーデータをいれてくれて、カテゴリ一覧ができます。目的はハンバーガーサイト(モスのネット注文 https://www.mos.jp/service/shop/netorder/ )を想定しているので、ハンバーガーのカテゴリにしたいです。

カテゴリ一覧の web api は localhost:8000/mos/api/categories を使って。
JSON 形式の例

{
"items": [
{
"id": 1,
"slug": "special1",
"title": "今月のお薦め",
"description": "今月のお薦め商品を紹介します。",
"image": "",
"sortid": 1,
"display": true,
"created_at": "2025-06-05T11:57:01+09:00",
"updated_at": "2025-06-05T11:57:01+09:00",
"deleted_at": null
},
{
"id": 3,
...

既に web api は Laravel で作ってあるので動作が可能な状態です。新人教育でも web api は提供するパターンで画面を Vue3/Nuxt で作って貰っています。web api まわりはデータベースアクセスなどの範囲が広いのとルーティング設定や Controller やらとややこしいことが多いので、提供することにしています。このあたり、良い教科書がないですかね? Web APIの設計の本は小難しいものが多いのです。

いろいろ追加されていますが、ひとまず「保持する」してしまいます。実際のコードの場合は、git への commit 単位にして気に入らなかった戻せばいいんじゃないでしょうか?

カテゴリ一覧のページとカテゴリ内の商品のページが表示されます。商品のほうはまだダミーデータですね。このあたりは、最初のところで web api の一覧とか OpenAPI を使って情報を突っ込んでやるうまく解析してくれるかもしません。今回は最初なので1手順ずつやっています。しかも、このブログを書きながらリアルタイムでやっている。

カテゴリ内の商品を表示するときは web api で /products/slug/{category_slug} を使って。
商品を選択したときは、/products/{product} を使って、商品の詳細ページを表示して。

Copilot が作ったページがちょっと違うので、Copilot に直して貰いましょう。そもそも、Copilot あるいは Claude Sonnet の書きやすい形でコードを生成しているので、それ自身が理解しやすいはずです。このあたり

  • 新規で Claude Sonnet を作成して、さらに Claude Sonnet で修正する
  • 人間が書いた既存のコードを Copilot が解析して Claude Sonnet が修正/追加していく

という2つのパターンがあると思います。で、おそらく前者のほうが生成AIにとっては楽でしょうね。学習済みのコードをを出してくるのですが、それを解析/学習するのは容易なはずです。ですが、後者のほうは人間の「理解不能なコード」を理解しないといけません。このあたり、生成AIの都合の良いほうに寄せるのは重要だと思っています。

もちろん、後者も必要なんですけどね。ちなみに不味いコードは Claude Sonnet が書き変えてしまうので、かなりリスクが高いです(いろいろな意味で)。

大体意図した通りになっているのですが、商品詳細のほうが違っています。

商品の詳細を表示する場合は /products/{id} のように id を使って。

これは laravel の api.php の書き方が悪いのですが、実際は id を設定します。

windows 上なので mv コマンドがなくて失敗していますが、PowerShell でやり直しています。ファイルを消すコマンド等は「続行」ボタンを押すみたいです。

X とかで「ファイルが消されてしまった!」現象が多くみられるので、その対策みたいですね。まあ、git に履歴が残っていれば復活できるのと、github actions と連携すると取り返しがつかないので、そこは人間がやっても生成AIがやっても一緒な話です。気を付けましょう、ということで。

ひとまず「カートに追加」」ボタンまでできました。ボタンはできているけど、コードの中身はない状態ですね。ここまで、概ね2時間ぐらいです。動作を確認しながらだし、ブログで記録をつけながらなので、そこそこ時間がかかっているかもしれませんが、ペースとしてはこれぐらいでしょう。

React/Nuxt慣れしていれば、人が単体でももっと素早くできるかもしませんが、これ以上ペースを上げても疲れるだけなので、検証としてはこの程度でいいんじゃないかなと思います。これをもっと早くしたい場合は(例えば100画面同時につくるとか)、先に書いた通り Ruby on Rails 形式で別途共通フォーマットや共通データを作成しておいて、生成AIに量産するツールを作ったもらったほうがよいです。そのあたりの線引きをペアプロ型のプロトタイプ開発に落とし込むか、オートメーションの大量生産型に落とし込むかですね。ブラウザ上で、さっくりと管理画面系ができますのようなキントーンを使った場合は後者のほうがベターだと思います。

さて、お昼を食べてから続きをやりましょう。

どうせなのでモスバーガーに行ってこよう。

昼から続き…

商品詳細の「カートに追加」機能を実装して。
カートの内容を cart.tsx で表示して。

この手の商品サイトには必須のカート機能を入れる。カテゴリ一覧も商品一覧も何処にもあるものだし、カート機能も定番のものなので、どこからか参考になるものを引っ張ってくればok

定番のものは人が作ってもAIが作っても同じなわけで、この場合は生成AIに作ってもらっている。新人研修の場合は、これとほぼ同じものを新人に手作業で作って貰っている。これはこれで意味があることで、

  • Vue3やTypeScriptの文法を覚え、実際に使ってみる
  • 動かした時にエラーが出るので、エラーの見方や直し方を学ぶ
  • 後から追加機能をいれるときに、入れやすいわかりやすいコードを心がける

という仕事上に必須な技術を学ぶためである。目的として「注文サイト」だけ作るのであれば Copilot に注文ページを作って貰えばいいわけだが、デザインにせよコードにせよ、何かを変えたり何かを修正したりすることが生成AI頼みではやりづらい。

実際、Claude Sonnet が出力してくるコードを手作業で直すのはちょっと辛いカモ。コードが揃っているという点ではコード規約に即していて読みやすいけど、共通化&ライブラリ化されているとは言い難いですよね。

カートの「注文に進む」をクリックしたときに
POST /mos/api/orders で、注文を実行して。

JSON形式の例

{
total_price: 0,
total_quantity: 0,
items: [
{
id: 1,
price: 1000,
quantity: 2,
},
{
id: 2,
price: 2000,
quantity: 3,
},
],
}

送信するJSON形式は、OpenAPIの形式でも上記のように例を示してもokです。

注文した結果の JSON が
{ "order_number": "07029999" }
となるので、この order_number をユーザーに表示して。

レスポンスに注文番号(order_number)が入っているので完了ページに表示させます。

これで機能は大体揃ったので、後は全体のデザインを整えていけばいいでしょう。ヘッダーとフッターを付けておきます。多分 layout.tsx なんですが、これも Copilot 経由で仮に付けてしまう。

全体のヘッダーとフッターを付けて。

ヘッダーは「新人研修のための注文サイト(仮)」
フッターは、coplyleft by moonmile

ここで、全 page.tsx に手を入れているのがさすが! Claude Sonnet ですね。

人がやるならば、クラスを使って共通化するとか各ページの記述場所を共通化するとか省力化するわけですが、Claude Sonnet はちらかった className の記述をひとつひとつ直していきます。おそらく100画面があれば、100画面直しておくと思われます。

ここは機械ならではというところで、人間の開発者とはアプローチが違うところです。逆に言えば、人間の開発者には修正がしにくいということです。

ひとまず、ここまで出来たのでokとしましょう。細かいところを見ると

  • トップページにある「商品一覧」は不要
  • パンくずリストが残っているページがある
  • 注文が完了しました、から自動的にトップページに戻ってしまう
  • カテゴリ統計とかは必要なし

など、手をいれる必要がありますが概ねの動きはこんな感じです。

サンプルコード

Copilot + Claude Sonnet 4 でショッピングサイト風のサンプル
https://github.com/moonmile/mos-next-sample

カテゴリー: 開発 | Copilot + Claude Sonnet 4 でショッピングサイト風なものを作る はコメントを受け付けていません

DeepResearch っぽい Research-mini を作ってみる

研究職に話題な DeepResearch ですが、簡易的なものであればさっくりと作れます。というテストです。

「さっくり」とは言いましたが、それなりに OpenAI API の作り方を知っていないとうまくいかないとは思うのですが、まあ、そのあたりも ChatGPT やら Copilot やらを使えばなんとかなります。

ざっと方針を決定する

この手の調査はよくやる話なので、人間がやっていることを AI に任せればokです。

  • キーワードを入れて、関連する WEB サイトを探す
  • WEB サイトの要約を表示する

の2段階があれば十分でしょう。実際、Google とかでキーワードを入れて一覧を作ってから、ぽちぽちと WEB サイトに飛んで、ページをコピペした後に要約するのはよくやる作業です。実に面倒くさいのですが、AI ならなんとかやってくれます。きっと。

ChatGPT に尋ねる

いきなりコード生成するのもよいのですが、ひとまず概要を伝えてみましょう。第一段階のキーワードにマッチするようなWebサイトを探して貰うコードを作ります。

テーマを指定すると、関連する WEB サイトの一覧を取得するコンソールツールを作って。

実は、この時点では Google で検索するコードを Python で出してくれます。確かに Google でもいいのですが、ここは OpenAI API を使ってもらいましょう。

openai API を使う書き方に直して。

OpenAI の gpt-4 モデルを使った Python コードに直してくれました。ただ、私の場合は .NET でやりたかったので、C# に書き直して貰います。

C# のコードに直して。

この時点で、かなり動くコードがでてきます。

class Program
{
    private static readonly string apiKey = "your_openai_api_key";

    static async Task Main(string[] args)
    {
        if (args.Length == 0)
        {
            Console.WriteLine("Usage: dotnet run <theme> [-n <number_of_results>]");
            return;
        }

        string theme = args[0];
        int numResults = 10;
        
        if (args.Length > 2 && args[1] == "-n" && int.TryParse(args[2], out int parsedNum))
        {
            numResults = parsedNum;
        }

        Console.WriteLine($"Searching for: {theme}\n");
        var results = await SearchWeb(theme, numResults);

        if (results.Count > 0)
        {
            Console.WriteLine("Related Websites:");
            for (int i = 0; i < results.Count; i++)
            {
                Console.WriteLine($"{i + 1}. {results[i]}");
            }
        }
        else
        {
            Console.WriteLine("No results found.");
        }
    }

    static async Task<List<string>> SearchWeb(string theme, int numResults)
    {
        using HttpClient client = new HttpClient();
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");

        var requestBody = new
        {
            model = "gpt-4",
            messages = new[]
            {
                new { role = "system", content = "You are a helpful assistant." },
                new { role = "user", content = $"List {numResults} websites related to {theme}." }
            }
        };

        string jsonRequest = JsonSerializer.Serialize(requestBody);
        var response = await client.PostAsync(
            "https://api.openai.com/v1/chat/completions",
            new StringContent(jsonRequest, Encoding.UTF8, "application/json")
        );

        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine("Error: Unable to fetch search results.");
            return new List<string>();
        }

        string responseContent = await response.Content.ReadAsStringAsync();
        using JsonDocument doc = JsonDocument.Parse(responseContent);
        var content = doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString();

        var results = new List<string>(content.Split('\n', StringSplitOptions.RemoveEmptyEntries));
        return results;
    }
}

プロンプトで「iBeaconのフォーマットについて」で検索させるとこんな感じです。

URLが出てくるので、これをピックアップするためのコードを Copilot に書いて貰いました。

    static List<string> ExtractUrls(List<string> results)
    {
        var urlPattern = new Regex(@"https?://[\w./?=&-]+", RegexOptions.Compiled);
        return results.Select(result => urlPattern.Match(result).Value).Where(url => !string.IsNullOrEmpty(url)).ToList();
    }

あとは、URL を読み込んで要約を出すわけですが、これも Copilot を使っています。

    /// <summary>
    /// URLを指定したら内容を要約して返す関数
    /// </summary>
    /// <param name="url"></param>
    /// <returns></returns>
    static string Shlink(string url)
    {
        using HttpClient client = new HttpClient();
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");

        var requestBody = new
        {
            model = "gpt-4",
            messages = new[]
            {
                new { role = "system", content = "You are a helpful assistant." },
                new { role = "user", content = $"Summarize the content of the website {url}." }
            }
        };

        string jsonRequest = JsonSerializer.Serialize(requestBody);
        var response = client.PostAsync(
            "https://api.openai.com/v1/chat/completions",
            new StringContent(jsonRequest, Encoding.UTF8, "application/json")
        ).Result;

        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine("Error: Unable to fetch search results.");
            return "";
        }

        string responseContent = response.Content.ReadAsStringAsync().Result;
        using JsonDocument doc = JsonDocument.Parse(responseContent);
        var content = doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString();

        return content ?? "none";
    }

こんな風に関数を作ってもらって、それを main に組み込みます。関数を作って貰うときは、関数の定義(ここでは、static string Shlink(string url) の部分)を書いたあとに、関数の前にコメントを書くと Copilot が自動補完してくれます。

この操作は、ChatGPTのブラウザ上でやってもいいでしょう。私の場合 Copilot のほうが慣れているので、Visual Studio Code 上でやっています。

表示しているところを何回かに分けて書き直しています。

        Console.WriteLine($"Searching for: {theme}\n");
        var results = await SearchWeb(theme, numResults);

        if (results.Count > 0)
        {
            Console.WriteLine("Related Websites:");
            for (int i = 0; i < results.Count; i++)
            {
                Console.WriteLine($"{i + 1}. {results[i]}");
            }

            Console.WriteLine("urls:");
            var urls = ExtractUrls(results);
            urls.ForEach(url => Console.WriteLine(url));
            foreach (var url in urls)
            {
                var summary = Shlink(url);
                Console.WriteLine("--------------------");
                Console.WriteLine($"Summary of {url}");
                Console.WriteLine("\n");
                Console.WriteLine( summary);
            }
        }
        else
        {
            Console.WriteLine("No results found.");
        }

デバッグ動作も兼ねて、区切りを入れたり URL を表示させておきます。

注意)何回か動かすと openai API のリクエスト制限に引っ掛かるらしく TooManyRequests のエラーが返ってきます。いまテストしていたら引っ掛かってしまったので、また明日追記の予定。

こんな感じで、一発でプロンプトを使ってコードを出力するわけではなくて、ChatGPT や Copilot を使いながら何回か出力してコードを組み立てていく例です。コード自体は1時間ちょいぐらいで動くようになります。その後、何回かテストするの1時間ぐらいかかるけど。

実行例

そのままだと、OpenAI の回答が英語になるのですが、プロンプトを追加すれば英語から日本語に翻訳してくれます。

        var requestBody = new
        {
            model = "gpt-4o-mini",
            messages = new[]
            {
                new { role = "system", content = "You are a helpful assistant." },
                new { role = "system", content = "応答は日本語に翻訳して" },
                new { role = "user", content = $"Summarize the content of the website {url}." }
            }
        };

日本語訳

iBeacon の場合は企業サイトが多いので直接クローリングができないっぽいです。公開されている技術文書や論文とかだったらいけるんじゃないかな。

公開コード

https://github.com/moonmile/research-mini

カテゴリー: 開発 | DeepResearch っぽい Research-mini を作ってみる はコメントを受け付けていません

GitHub Copilot で iBeacon 発信ツールを作れる?

要は「GitHub Copilot x Python」本の宣伝記事です。自前のブログ記事だから自前の本の宣伝をしてもよいのですが、内容を紹介しているだけでは “私が” 詰らないので、もうちょっと先のところを書きます。

この書籍ではPythonを学ぶことになっていますが、実はGitHub Copilotを学ぶのが目的です。GitHub Copilotはコーディング時に適した生成AIで Visual Studio Code と Copilot を組み合わせて使います。昨今では、プログラムのコーディング時に生成AIを使う(ChatGPTとかGeminiとか)を使うことは普通になってきましたが、もう一歩進んでコーディングに適した使い方を学んでいこうというのが主旨です。なので、実はツールは問いません。この本ではGitHub Copilotを使っていますが Cursor とか他のツールでも構いません。

生成AIを伴ってコーディングをする

ChatGPTなどのツールをコーディングに使おうとしたとき、真っ先に考えるのは「○○のツールを作って」というプロンプトを使ったコード生成の仕方です。実際、簡単なゲーム(インベーダーとかブロック崩しとか)のコードは、これだけのプロンプトでも簡単に作ることができます。色を付けたり、音が付いたりするので、かなりの驚きを覚えるのですが、まあ、実際のところインベーダーゲームを作ったりブロック崩しを作ったりしたいわけではないので、その後が続きません。

最初に断っておくと、おそらくですが「ゲーム」のように閉じられた環境の場合には、このプロンプトの方式はかなり有効です。おそらく、同じパターンでロールプレイングゲームとかか未芝居型のギャルゲーとかもいけそうです。小学生向けのさんすうゲームとかもいけそうです。おそらく、小奇麗なショッピングさいととも可能かもしれません。

これはいわゆる巷にあるテンプレート的なものに対して生成AIのコーディング力が非常に強いためです。ある意味、プログラミングというものが既存のライブラリの組み合わせや、Webのユーザーインターフェース(HTMLとCSSの組み合わせとかアニメーションとか)のサンプルコードがインターネットにたくさん紹介されていて、それらのノウハウの蓄積が早いからでしょう。

ただし、この手の目的にぴったりのモノが出てくる場合はいいのですが、ぴったりのものが出て来ない場合はどうすればいいでしょうか?ひたすら生成AIに渡すプロンプトを工夫してコードを練り上げていく方がよいのでしょうか?

ひとつの方法として「何度もプロンプトを繰り返す」というのもあるのですが、もう少しプログラマらしい手法を取ってみましょう。というのが主旨です。なにも、全てを生成AIに任せるのではなく、生成AIに足りない部分は人が補い、逆に人に足りない部分(知識とか時間とか)を生成AIで補わせるという方法です。端的に言えば、生成AIとペアプロをするという方法です。

iBeacon を発信させる

GitHub Copilot の細かい使い方やPythonの文法などは先の本に任せるとして、ここでは先に進んでみましょう。

注)最初に書いておきますが、Windows 環境で Python を使って iBeacon を送信することはできません。そうそう、.NETでもできなかったんですよね。WSLを使うとできるようなできないような感じでしたが…ひとまず、それを忘れて「Windows で Python を使って iBeacon を送信していみよう!」と思って、作り始めた記録です。

仮想環境 .venv を作って vscode に transmit-ibeacon.py というファイルを開きます。

まずは、初手として Copilot にプロンプトを入れます。色々設計してもよいのですが、作りたいツールを直接入れてしまうのが初手としては手っ取り早いです。

「iBeacon を発信するコードを書いて。」

中味は GPT 4o なので結構コードが生成されます。

これを「同意する」を押して確定させます。

import time
import bluetooth

# iBeacon settings
uuid = "12345678-1234-1234-1234-123456789abc"
major = 1
minor = 1
tx_power = -59

# Create the iBeacon advertisement packet
def create_ibeacon_packet(uuid, major, minor, tx_power):
    prefix = b'\x02\x01\x1a\x1a\xff\x4c\x00\x02\x15'
    uuid_bytes = bytes.fromhex(uuid.replace('-', ''))
    major_bytes = major.to_bytes(2, byteorder='big')
    minor_bytes = minor.to_bytes(2, byteorder='big')
    tx_power_byte = tx_power.to_bytes(1, byteorder='big', signed=True)
    return prefix + uuid_bytes + major_bytes + minor_bytes + tx_power_byte

# Transmit iBeacon
def transmit_ibeacon():
    ibeacon_packet = create_ibeacon_packet(uuid, major, minor, tx_power)
    sock = bluetooth.BluetoothSocket(bluetooth.LE_ADVERTISING)
    sock.bind(("", 0))
    sock.send(ibeacon_packet)
    print("iBeacon is transmitting...")

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        sock.close()
        print("iBeacon transmission stopped.")

if __name__ == "__main__":
    transmit_ibeacon()

実は、このプログラムを実行すると bluetooth ライブラリが無くてこけます。

どうやら、先頭の「import bluetooth」がエラーになっているので、インポートするライブラリが足りないようです。

ここで「Copilot を使用して修正する」を選択します。書籍のほうでは、いろいろと試行錯誤をしていますが、ここではちゃっちゃと聞いてしまいましょう。だいたい、pip コマンドでライブラリを入れれば解決できる話なので。

予想通り、pybluez と言うライブラリを入れればよさそうですね。

pip install pybluez

これを仮想環境でインストールしましょう。

実は bybluez は Windows 環境にはインストールできません。Linux 用の Bluetooth ライブラリなので、Windows 環境では動かないのです。

おそらく Linux 環境で動かせば Copilot の提案通りに動く筈なのですが、いまは Windows 環境で動作させたいので、これは使えません。さてどうしたものか。

プロンプトで Copilot に頼んでみましょう。

Windows 環境で動くように書き換えて。

これはうまくいきませんでした。なにか変な import を追加しますが本筋ではありません。

仕方がなので、チャットを開いて pip で出て来たエラーを貼り付けて問い合わせをします。

(.venv) PS H:\BLE\py> pip install pybluez
Collecting pybluez
  Using cached PyBluez-0.23.tar.gz (97 kB)
  Preparing metadata (setup.py) ... error
  error: subprocess-exited-with-error

  × python setup.py egg_info did not run successfully.
  │ exit code: 1
  ╰─> [1 lines of output]
      error in PyBluez setup command: use_2to3 is invalid.
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
error: metadata-generation-failed

× Encountered error while generating package metadata.
╰─> See above for output.

note: This is an issue with the package mentioned above, not pip.
hint: See above for details.
(.venv) PS H:\BLE\py>

チャットの様子

実は、このエラーを Copilot のチャットや ChatGPT のプロンプトに貼り付けて問い合わせをするのはよくやります。コンパイル時の長いエラーや、ややこしいエラーメッセージなどを生成 AI が解析してくれるので便利です。

この回答では、setuptools や wheel の更新を求めていますが、実は駄目です。最後の「pip install bleak」というところで、bleakというライブラリを使うことになります。

最初から書き直して貰ってもいいのですが、bluetooth のところを直せば良さそうなので、この部分で「bleakで書き換えて」とプロンプトに入れてみます。

そうすると、Bleak ライブラリを使った形で書き換えてくれます。

出来上がったコードです。

import asyncio
from bleak import BleakScanner, BleakAdvertiser

# iBeacon settings
uuid = "12345678-1234-1234-1234-123456789abc"
major = 1
minor = 1
tx_power = -59

# Create the iBeacon advertisement packet
def create_ibeacon_packet(uuid, major, minor, tx_power):
    prefix = b'\x02\x01\x1a\x1a\xff\x4c\x00\x02\x15'
    uuid_bytes = bytes.fromhex(uuid.replace('-', ''))
    major_bytes = major.to_bytes(2, byteorder='big')
    minor_bytes = minor.to_bytes(2, byteorder='big')
    tx_power_byte = tx_power.to_bytes(1, byteorder='big', signed=True)
    return prefix + uuid_bytes + major_bytes + minor_bytes + tx_power_byte

# Transmit iBeacon using Bleak
async def transmit_ibeacon():
    ibeacon_packet = create_ibeacon_packet(uuid, major, minor, tx_power)
    advertiser = BleakAdvertiser()
    await advertiser.start(ibeacon_packet)
    print("iBeacon is transmitting...")

    try:
        while True:
            await asyncio.sleep(1)
    except KeyboardInterrupt:
        await advertiser.stop()
        print("iBeacon transmission stopped.")


if __name__ == "__main__":
    transmit_ibeacon()

さて、これを動かし見ると実はエラーで動きません。

これを Copilot に聞くと BleakAdvertiser 自体が無いことがわかります。つまりは Copilot のハルシネーションですね。勝手に作った(あるいは、どこかのサンプルコード?)になります。

あらためて、Copiot に尋ねてみましょう。

「Windows 環境で Python を使って iBeacon を発信するには?」

提案されたコード

import pygatt
import time

# iBeacon settings
uuid = "12345678-1234-1234-1234-123456789abc"
major = 1
minor = 1
tx_power = -59

# Create the iBeacon advertisement packet
def create_ibeacon_packet(uuid, major, minor, tx_power):
    prefix = b'\x02\x01\x1a\x1a\xff\x4c\x00\x02\x15'
    uuid_bytes = bytes.fromhex(uuid.replace('-', ''))
    major_bytes = major.to_bytes(2, byteorder='big')
    minor_bytes = minor.to_bytes(2, byteorder='big')
    tx_power_byte = tx_power.to_bytes(1, byteorder='big', signed=True)
    return prefix + uuid_bytes + major_bytes + minor_bytes + tx_power_byte

# Transmit iBeacon using pygatt
def transmit_ibeacon():
    adapter = pygatt.GATTToolBackend()
    adapter.start()
    try:
        ibeacon_packet = create_ibeacon_packet(uuid, major, minor, tx_power)
        adapter.sendline('sudo hcitool -i hci0 cmd 0x08 0x0008 ' + ibeacon_packet.hex())
        print("iBeacon is transmitting...")
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("iBeacon transmission stopped.")
    finally:
        adapter.stop()

if __name__ == "__main__":
    transmit_ibeacon()

実行してみると、実はこれも動きません。

提案されている pygatt は Linux の BlueZ を使っているので、Windows では動かないんですよね。

結論的には Windows 環境から iBeacon を発信することはできないので、別の環境が必要です。Linux で hcitool を使うか、Android とかを使うか。

ちなみに M5Stack を使った例を示しておきます。

M5Stack で iBeacon を飛ばす(PlatformIO環境) | Moonmile Solutions Blog https://www.moonmile.net/blog/archives/11081

なので、これだとアレだから、Android で iBeacon を作る例を続きへ。

カテゴリー: 開発 | GitHub Copilot で iBeacon 発信ツールを作れる? はコメントを受け付けていません

フジテレビでの会見の考察

午後4時からスタートした会見が翌日の午前2時半ごろまでかかったので、長丁場の10時間の及んだ記者会見ですが、ちょうど午後4時から2時間ほど見た後に、午後10時頃から午前2時までは見ていたので、そのあたりの感想をまとめておきます。

フジテレビ記者会見 10時間超に 新社長に清水賢治氏 港浩一社長 嘉納修治氏 遠藤龍之介氏 金光修氏ら出席 | NHK | テレビ局 https://www3.nhk.or.jp/news/html/20250127/k10014704771000.html

すぐにでも youtube に全会見が上がりそうな勢いですが、NHKの一問一答があるので、これを参考にでもしてください。

会見の争点

見ている人によってそれぞれの立場があるので会見の味方が異なります。視点としては、ビジネス寄りにして「今後のフジテレビが存続し得るか?スポンサー会社は戻ってくるのか?戻れるのか?」で見ています。なので、具体的な性被害/性接待については別途中居問題としてでてくるだろうし、週刊誌等が追うでしょう。それはそれで必要なんだけど、株主のいる株式会社としてフジテレビは今後どうするのか?ということです。

問題となるのは、

  • スポンサーが一斉に降りてしまった責任を誰が取るのか?
  • フジテレビ自体を今後どうするのか?立て直すのか?
  • スポンサーが戻る余地があるのか?

ということです。明らかに第1回の閉鎖的な会見が良くなくて、その後のスポンサーが一斉に抜けたわけで、それをなんとか収拾しないと株主としてもスポンサー会社としても困るわけです。

会見の良かった点

先に会見の良かった点をあげていきましょう。実際のところ記者会見としては比較的良かった部類に入ると思うのです。一部の記者(望月記者など)の怒号や主張が長かったという意見もありますがそれを受けての記者会見ではあるし、長時間にわたってしまったのはひとりひとりの記者に必ず質問の機会が与えられていたという進行のためなので、仕方がないでしょう。逆に言えば、長時間にわたってしまったが、記者が質問をするという形式は確実に守れたと思われます。

  • 港前社長の退陣が決定していた
  • 清水新社長の新体制や第三者委員会の解説が明確であった
  • 日枝相談役に関しては、相談役として据えているだけで強烈な影響があるわけではない(まあ、多少はあるにせよ)

少なくとも株主への説明やスポンサー会社への説明のための材料は揃えていたような気がします。元凶が前社長の対応の杜撰さにあるので、これを払拭できるかわかりませんが少なくともフジテレビの社長と会長を退陣させることでそれの決着はできるでしょう。副会長が留任しているのは副会長の言葉が実に真摯であることからわかります。5人の会見でしたが、港前会長が一番駄目で、他の4人はそこそこ答えている感じでした。

フジ・メディアHDの社長の言葉は、たしかにフジテレビの株主会社なのでこういう言い方しかできませんよね。もう全面的に港前社長と周辺の役員、その下に連なるディレクターが不味いって話でもあります。

フジテレビの買収の恐れに関しては、フジ・メディアHDの社長の説明する通りメディア業務が50%を越えることを前提にHD自体が作られているそうなので「買収はあり得ない」と考えられます。買収されたときは、HDごと買収されることになるので、あの社長は必至で抵抗するでしょう。それがHDの役目でもあるのでそこは相談役ともども乗り込んでくると思われます。例の外国株主からの件は、そのあたりの事情が良く分かっていないのかもしれません。フジテレビの株を持ってはいるものの、番組内容や会社の態勢はHDが株主的にも優先度が高いのではねのけるかなと。

あと、フジテレビで会見を最後まで生放送(10分遅れではありますが)したのは、フジテレビの制作担当だそうです。ちょくちょく入っている音声なしの状態も、その遅延の間に消しているので流石報道制作部(多分)というといころですね。これはバラエティだとしても同じで音声やカメラの切り替えで放送事故になりそうなものを瞬時に切り替える技術のたまものです。

個人的にはもっと音声がなくなると思っていたのですが、かなりピンポイントで削られているだけでした。おそらく編成担当の部署に役員がいて色々指示することが無かったんじゃないかなと思われます。単なる記者会見ではなくて、自社の社員向けでもあるということも含めてできるだけ流すという方針にしたのは良かったです。所謂、のり弁になるのを危惧したのですが、それは無かった。

会見の良くなかった点

この会見のよく無かった点としては港前社長の不誠実な回答に付きます。おそらく、実際のところ該当の女性からの報告(女性の同僚や医師)をかなり分かっていないのが丸見えで、当時ながらわからないところがあったにせよ、そのあたりを謝罪しないと前に進まないのですが。そこは第1回目の会見を閉鎖でやったところもあって「さもありなん」という体ですね。

おそらく前社長の保身が今回の騒動を大きくしてしまった(発端は中居問題にせよ)のではないかと考えられます。

非常に気になる(印象がよくない)のは、

  • 女性が事実を知られたくないまま復帰したいと望んだので、それに応じて隠した。中居番組も続けた。
  • 今回の件は非常に「特殊」であった。できれば女性本人に会って謝りたい。

の2点です。

「女性が事実を知られたくないまま~」が言い訳になっているに過ぎません。少なくとも株主やスポンサーからは会社としての危機にあっているのに、当時の女性の気持ちのせいにしている前社長を信用することはできません。むしろ、当時の状態として女性の意図を間違って汲み取ってしまって、コンプライアンス的にも人権的にも誤った行為をしてしまった、と謝罪したほうが受けがよいです。そうじゃないと、単なる女性社員の一存で社長が振り回されている感じがするし、先導できていません。まあ、実際そういう訳で馘なので決着は一応ついているのですが、本人が分かっているかどうかが分かりません。

実際のところは第三者委員会の調査を待つところですが、性被害(セッティングに突っ込んだの女性本人の意思だとしても、性被害とみられるような行為をされるのはそれは犯罪ですよね)にあった人にとってその直後はその事実を否定したり自分を過度に責めたりすることはよくあることです。心理的にその事実を無くしたいという思いがあるので、向き合うので「事実が知られないまま復帰」を望むわけですが、実際そういうのでは心的被害が残ります。想像ではありますが、前社長が何度も「特殊」なというのは、かなり深刻な状態だったと思われます。本人が強烈に隠蔽と復帰を望むと同時に、おそらく自殺を図ったのかもしれません。自殺すると一遍に情報が洩れますからね。ちょっとしたセクシャルハラスメントとは状況が違うわけです。副会長が言う通り、フジテレビ内でもちょくちょくセクハラやパワハラでコンプライアンス部への報告もあったのでしょう。でも、それは自殺レベルではなかったかと思われます。そういう意味で実際に「特殊」だったのでは?と想像ができます。

で、午後1時過ぎの前社長の言葉ですが、既に退職していますか?という記者の質問に対して「在籍しています」と答えています。何度も「直接会って謝りたい」と言っていますが、現在も社員であれば直接会うのは容易ですよね。会わない、あるいは会えないのは何か理由があるのでしょうか?女性に拒否されている?変な勘ぐりをされてしまう状況です。

勘ではありますが、被害女性は躁鬱症状態になっているのかなと思います。これ個人の特定とかそういう範囲になってしまうのですが(実は、今の私が被害女性が誰だったかを詳しく調べていません。うちの奥さんからパリ五輪に見に行っていた、と聞いたぐらいで世の中としては特定されているようなのですが、あえて調べていません)、躁鬱症の場合は本人の言葉を医師が鵜呑みにすることはまずないんですよね。なので、性被害をあった直後に「復帰したい」とかいう言葉を鵜呑みにすることはないのです。あと本職のカウンセリングだとそういう個人的な話を会社に伝えることもないんですよね。おそらく、フジテレビ内の産業医レベルかもしれません。個人的なカウンセリングを受けたほうが良いかと。

余談

ハッキリ言って、細野不二彦著「電波の城」を思わせます。というか「電波の城」の連載自体がそういうテレビ業界の裏話をベースに作った漫画なので、そのあたりは多少の誇張はあるにせよ当時のテレビ業界の雰囲気がこんな感じだったわけです。なので「女子アナ」というジャンルが、いわゆるコンパニオン役を担わされていた時代背景もあります。

そこが一般企業の認識とずれて居るところに、前社長の言葉のずれぐあいが集約されていますね。

もうひとつ、日枝相談役の存在は河合隼雄著「中空構造 日本の深層」を読むと詳しく書いてあります。いわゆる日本神話から続く、日本の権力構造がほぼ空位である天皇というもので存在して、権威ではあるが権力がないという中空構造を保っている歴史的経緯があるのです。これは、外国の株主には理解しがたい現象かもしれませんね。

カテゴリー: 開発 | フジテレビでの会見の考察 はコメントを受け付けていません

機動戦士ガンダム ジークアクス劇場版の発想元を考える

盛り上がっているうちに観ていきますかということで近場の映画館で観てきました。平日の水曜日で昼からなのですが、それなりに人が来ています。詰め詰めじゃないけど、がらがらというわけでもなさそうで、年齢層は幅広く入っていたかなぁと。

以下、ネタバレになるので、できれば映画館で観た後に。ネタバレ大丈夫な場合(私はそっちのほう)は、ざっと予習してから見に行っても楽しめます。

ひとことで言えば、「ガンダムを観に行ったらガンダムが始まった!」という感想に付きます。「水星の魔女」の場合は、第1話がガンダムっぽくないものからガンダムっぽいところで終わるのですが、ジークアクスの場合はガンダムで始まってガンダムで終わります。

端的に言えば、初代ガンダムのリメイクから始まるわけです。

仮想戦記という考え方

そもそも機動戦士ガンダムというアニメが仮想戦記なわけで当然のことながら史実に基づいているわけではありません。しかし、ところどころに出てくる地名が第二次世界大戦を思わせる地名だったり(ジャブロー、ソロモンなど)、登場人物がレーニンやスターリンを思わせたりするものがあって、史実を参考にしているところが多いのです。仮想戦記/ifシリーズと言えば「紺碧の艦隊」や「沈黙の艦隊」、「戦国自衛隊」など史実を交えながらも、ちょっとだけ当時の事実が変わって、日本軍が勝ったりするストーリーが流行っていました。

そういう点で、ジークアクス劇場版はifシリーズの系譜に相当します。もしも、ジオン軍が年戦争で勝っていたらという話ですね。もっと直接的に言えば、アムロの代わりにシャアがガンダムに乗っていたらというifシリーズです。

ちなみに、初代ガンダムでは「戦局が個人の能力に左右されることはない」という思想が強いので、シャアがガンダムに乗ったからといってジオン軍が勝つことはない、と思うのですが、せっかくだからジオン軍が勝ったという仮定をしてみましょう。

って、去年の12月にネタバレしている椎名先生はエスパーですか!?

って、一方でマフィア梶田氏が「ジオン軍が勝ったストーリーを見てみたい」という放送がカットされたという話もあるので(苦笑)、いろいろとガンダム界隈でチェックが入っていたみたいです。

作り手の発想元ネタを探ってみる

初代ガンダムネタがいろいろな方がやっていると思うので、ガンダムシリーズとは違うところから持ってきたであろう、という妄想も含めて発想元のネタを考えてみましょう。

  • ジークアクスの世界でのグランバトルは、ボトムズの地下闘技場を思わせる。そのうちキリコっぽい人がでてくるかも。
  • キラキラの部分は「夜のクラゲは泳げない」にそっくりで、なんとなく黒髪のキャラもにている。あるいは涼宮ハルヒのSOS団マークに似ているかもしれない。時空のトンネルを越えるとかの前兆?
  • 女ボスは、タイムボカンシリーズのドロンジョですね。
  • 時空のトンネルは「エウレカセブン AO」あたりに似ている。
  • マブ(MAV)は「マブダチ」が元ネタなんだけど(映画では理屈をこねているが)、タイガー&バニーのダブルデッカーですね。ダグ&キリルでもいいです。
  • 赤いガンダムの搭乗者は、どうみても「ハウルの動く城」のハウルにしか見えない。たぶん、性格もハウルに寄せてくる。
  • 全編で「ピキーン」のニュータイプ音が響くわけですが、ジークアクスがニュータイプに寄せていくかどうかは不明。どちらかというと、プラモ狂四郎とかプラレス3四郎のようなエンジニアちっくなものに寄せるような気がする。
  • ビギニングのほうは、ギレンの帝国や、サンダーボルトに雰囲気を寄せている。コロニーの脇を疾走するシーンは、そのまんまサンダーボルトにあります。
  • 奪取したガンダムをキシリアが赤く塗らせたシーンは、そのまんま「トニーたけざきのガンダム漫画」そのものです。とりあえず赤く塗っとけ!と切れるキシリアを思わせる
  • ティム・レイの工作ボックスは、Google かなんかで型番を検索して出てくるほど量産品になっていて、とってもHDDっぽい。ぴかぴかしているし。
  • ガンダムの四ツ目はエヴァそのもの
  • 木馬の左舷は~、が健在です。初代ガンダムネタは鹵獲された木馬を始めとしてあちこちに出てくるので初代を見ておくと面白いですよ。

シャリア・ブルの立ち位置は?

実は、偶然にもジークアクスの劇場版を見る直前に、富野版の初代ガンダムの小説を再読していました。シャリア・ブルは、テレビ版にも小説版にも出てくるんですが、初代ガンダムの劇場版ではカットされているんですよね。当時の理由はよくわからないのですが、後にある小説版の方に出てくるところを見ると、それなりにキャラ建てはできていたと思われます。

逆に言えば、シャリア・ブルの周辺は当時は特に設定がないわけで、シャアと組ませて話を広げやすかったという理由でしょう。特に裏設定とかなさそうなので、ジークアクスの世界でのシャリア・ブルを彼なりの正史にしやすいという図式でしょう。

個人的に言うと、ニュータイプ的な「覚醒」はあまり面白くないんですよね。旧人類が重力に引かれている=既存の構造主義や権力に縛られているので、それから解き放されたスペースノイドは呪縛から解き放されるので、構造から脱することができる「脱構築/脱構築」というスタイルが望ましい…のですが、果たして。

ガンダム史と社会思想に関してはまた別途。

カテゴリー: 開発 | 機動戦士ガンダム ジークアクス劇場版の発想元を考える はコメントを受け付けていません