セマンティック カーネルでクリスマスの支出をコントロールする

· 6分で読める

はじめに

ホリデーシーズンが近づくと、特にショッピングやギフトの購入が増えるため、出費の管理が課題になることがあります。このブログ投稿では、人工知能を活用して、.NET テクノロジを使用してクリスマスの支出を追跡する方法を検討します。セマンティック カーネルと AI の力でレシートを分析することで、店名、日付、商品リスト、合計金額などの重要な詳細を効率的に抽出できます。このソリューションを使用すると、クリスマスの支出を簡単に監視および管理できるため、領収書を手動で確認する手間をかけずに予算を確実に把握できます。

Calendario de Adviento de Inteligencia Artificial 2024 en Español

このプロジェクトは、AI に特化したオンライン イベントである Calendario de Adviento de Inteligencia Artificial 2024 en Español への参加からインスピレーションを受けました。イベントの詳細については、この Dev.to リンク でご覧いただけます。

プロジェクト

このプロジェクトでは、GPT-4 などの強力な AI モデルを利用して画像を処理および分析できるサービスである Azure OpenAI を使用します。このプロセスには、バックエンド API サービスのセットアップから画像アップロードのための Blazor フロントエンドとの統合まで、いくつかの手順が含まれます。また、すべてをシームレスに接続するのに役立つコンポーネントである .NET Aspire も使用します。

前提条件

コードに入る前に、次の前提条件を満たしていることを確認してください。

  • .NET9
  • Azure OpenAI アクセス (API キー)
  • Visual Studio または Visual Studio Code
  • Blazor、HTTP クライアント、API 開発の基本的な知識

Visual Studio ソリューション

最終的には次のようなものになります。私は物事を分けてクールな名前を付けるのが好きなので、次のようになります。

でも、ステップバイステップで物を作っていきましょう!

ステップ 0: モデル

Receipt Scanner アプリケーションの中核は、フロントエンド、API、AI サービス間の対話を促進するいくつかの主要なモデルに依存しています。このプロジェクトで使用される主なモデルは次のとおりです。

  • 受信リクエストの分析
    このモデルは、レシートを分析するためのリクエスト構造を表します。これには、処理されるレシート画像のバイト配列を保持する ImageBytes プロパティが含まれています。

    public class AnalyzeReceiptRequest
    {
        public byte[] ImageBytes { get; set; }
    }
    
  • 受信分析結果
    このモデルは、レシートを処理した後の結果をキャプチャします。レシートから抽出された店舗名、日付、品目、合計金額などの構造化データが保持されます。

    public class ReceiptAnalyzeResult
    {
        public DateTime CreatedAt { get; set; }
        public ReceiptData Result { get; set; }
    }
    
  • 領収書データ
    構造化されたレシートデータを保持するモデルです。これには、店名、日付、アイテムのリスト (各アイテムの名前と価格)、レシートの合計金額のプロパティが含まれます。

    public class ReceiptData
    {
        public string Store { get; set; }
        public DateTime? Date { get; set; }
        public List<ReceiptItem> Items { get; set; }
        public decimal? Total { get; set; }
    }
    
  • 受信アイテム
    領収書の各項目はこのモデルで表されます。アイテム名とその価格が保持されます。

    public class ReceiptItem
    {
        public string Name { get; set; }
        public decimal? Price { get; set; }
    }
    ```これらのモデルは、クライアントとサーバー間でデータを受け渡すための基盤として機能し、情報のスムーズな流れを保証します。 API はレシート画像を受け取り、その代わりに、フロントエンドで簡単に使用できる構造化された JSON オブジェクトを処理して返します。
    

ステップ 1: バックエンド API サービスのセットアップ

このアプリケーションを構築する最初のステップは、レシート画像を分析するための API サービスを設定することです。 Azure OpenAI API を使用して、レシート画像から情報を抽出します。すべてがどのように組み合わされるかは次のとおりです。

AI サービス - 詳しい説明

AIサービスはレシート分析システムの中核です。 Azure OpenAI の API と通信して画像データを処理し、意味のある洞察を返します。 AiApiClient クラスは、Azure OpenAI API とのすべての対話を処理するクライアントです。

AI クライアントの実装

AiApiClient は、レシート画像 (バイト配列形式) を Azure OpenAI API に送信する重要なコンポーネントです。通信、エラーログ、データ解析を処理します。

public class AiApiClient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<AiApiClient> _logger;

    public AiApiClient(HttpClient httpClient, ILogger<AiApiClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<ReceiptAnalyzeResult?> AnalyzeAsync(byte[] imageBytes, CancellationToken cancellationToken = default)
    {
        if (imageBytes == null || imageBytes.Length == 0)
        {
            _logger.LogWarning("ImageBytes is null or empty.");
            return null;
        }

        _logger.LogInformation("Sending analyze request with image bytes of length: {Length}", imageBytes.Length);

        var request = new AnalyzeReceiptRequest
        {
            ImageBytes = imageBytes
        };

        try
        {
            var response = await _httpClient.PostAsJsonAsync("/analyze-receipt", request, cancellationToken);

            if (!response.IsSuccessStatusCode)
            {
                _logger.LogWarning("Failed to analyze receipt. StatusCode: {StatusCode}", response.StatusCode);
                return null;
            }

            var analyzeResult = await response.Content.ReadFromJsonAsync<ReceiptAnalyzeResult>(cancellationToken: cancellationToken);

            if (analyzeResult == null)
            {
                _logger.LogWarning("No content received from AI API service.");
                return null;
            }

            _logger.LogInformation("Analysis result received: {AnalyzeResult}", analyzeResult);

            return analyzeResult;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while analyzing the receipt.");
            return null;
        }
    }
}

コードのこの部分では、次の役割を担う AnalyzeAsync メソッドを定義します。

  1. 画像のバイト配列を Azure OpenAI API に送信します。
  2. API からのエラーまたは失敗した応答の処理。
  3. 返された JSON データを構造化された結果 (ReceiptAnalyzeResult) に解析します。

この機能を専用サービス (AiApiClient) に分離することの利点は次のとおりです。

  • エラー処理: ネットワークの問題や無効な応答などのエラーを一元的に処理します。
  • ログ: システムの動作を監視するためのリクエストと応答の適切なログ。

API サービス - リクエストとレスポンスの処理

API サービス は、フロントエンド Blazor アプリケーションと AI サービスの間の仲介者として機能します。このサービスは、画像データを受け取り、AI サービスに渡し、分析結果をクライアントに返す役割を果たします。

API エンドポイント

このステップでは、レシート画像を受け入れ、それを AI サービスに転送して処理し、結果をクライアントに返すための単純な API エンドポイントを定義します。

using ReceiptScanner.Shared.Clients;
using ReceiptScanner.Shared.Models;

var builder = WebApplication.CreateBuilder(args);

// Add service defaults & Aspire client integrations.
builder.AddServiceDefaults();

// Add services to the container.
builder.Services.AddProblemDetails();

// Register AiApiClient with HttpClient
builder.Services.AddHttpClient<AiApiClient>(client =>
{
    client.BaseAddress = new Uri("https+http://aiservice");
});

// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseExceptionHandler();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

// POST endpoint to analyze receipt
app.MapPost("/api/analyze-receipt", async (AnalyzeReceiptRequest request, AiApiClient aiApiClient, ILogger<Program> logger) =>
{
    if (request.ImageBytes == null || request.ImageBytes.Length == 0)
    {
        logger.LogWarning("ImageBytes is null or empty.");
        return Results.BadRequest("ImageBytes is required.");
    }

    logger.LogInformation("Received analyze receipt request with image bytes of length: {Length}", request.ImageBytes.Length);

    try
    {
        var result = await aiApiClient.AnalyzeAsync(request.ImageBytes);

        if (result == null)
        {
            logger.LogWarning("Failed to analyze the receipt.");
            return Results.Problem("Unable to process the receipt at this time. Please try again later.");
        }

        logger.LogInformation("Analysis completed successfully. Result: {Result}", result);

        return Results.Ok(result);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An error occurred while processing the receipt.");
        return Results.Problem("An error occurred while processing the receipt. Please try again later.");
    }
});

app.MapDefaultEndpoints();

app.Run();

このエンドポイント:

  1. リクエスト本文の一部としてレシート画像を受け入れます。
  2. AiService endopint メソッドを呼び出して、処理のために画像を Azure OpenAI に送信します。
  3. 分析結果をクライアントに返します。

ステップ 2: Blazor フロントエンドのセットアップ

バックエンドのセットアップが完了したので、Blazor フロントエンドに注目してみましょう。ここで、ユーザーは分析のためにレシート画像をアップロードし、結果を確認できます。

Blazor ページの実装

Blazor ページには、ユーザーが複数のレシート画像をアップロードして、表に表示される分析結果を確認できるシンプルなインターフェイスが提供されます。ページのコードは次のとおりです。

@page "/analyzer"
@using ReceiptScanner.Shared.Clients
@using ReceiptScanner.Shared.Models
@using System.Globalization
@inject ApiServiceClient ApiClient
@inject ILogger<Program> Logger

@attribute [StreamRendering]
@rendermode InteractiveServer

<PageTitle>Receipt Analyzer</PageTitle>

<h1 class="text-center my-4">Receipt Analyzer</h1>

<div class="container">
    <p class="lead text-center mb-4">Upload receipt images below to extract their data.</p>

    <!-- File Upload Section -->
    <div class="card mb-4">
        <div class="card-body">
            <InputFile OnChange="HandleFileSelected" multiple class="form-control mb-3" />
            <button class="btn btn-primary w-100" @onclick="ProcessReceipts" disabled="@(!hasFiles)" type="button">
                <span class="@(!processing ? "d-none" : "") spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
                @if (processing)
                {
                    <span>Processing...</span>
                }
                else
                {
                    <span>Process Receipts</span>
                }
            </button>
        </div>
    </div>

    <!-- Uploaded Images Preview -->
    @if (fileBytesList.Any())
    {
        <div class="card mb-4">
            <div class="card-header">
                <h5 class="mb-0">Uploaded Receipt Images</h5>
            </div>
            <div class="card-body">
                <div class="row">
                    @foreach (var fileBytes in fileBytesList)
                    {
                        <div class="col-12 col-md-4 mb-3">
                            <img src="@($"data:image/jpeg;base64,{Convert.ToBase64String(fileBytes)}")" class="img-fluid rounded" alt="Uploaded receipt" />
                        </div>
                    }
                </div>
            </div>
        </div>
    }

    <!-- Processing Indicator -->
    @if (processing)
    {
        <div class="alert alert-info text-center" role="alert">
            <strong>Processing receipts...</strong> Please wait while we analyze the uploaded files.
        </div>
    }

    <!-- Analysis Results Section -->
    @if (analyzedReceipts != null && analyzedReceipts.Any())
    {
        <div class="card">
            <div class="card-header">
                <h5 class="mb-0">Analysis Results</h5>
            </div>
            <div class="card-body">
                <table class="table table-striped table-bordered">
                    <thead>
                        <tr>
                            <th>Store</th>
                            <th>Date</th>
                            <th>Total</th>
                            <th>Items</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach (var receipt in analyzedReceipts)
                        {
                            <tr>
                                <td>@(receipt.Result?.Store ?? "Unknown")</td>
                                <td>@(receipt.Result?.Date?.ToString() ?? "Unknown")</td>
                                <td>@(receipt.Result?.Total?.ToString("C", ci) ?? "Unknown")</td>
                                <td>
                                    <ul class="list-unstyled">
                                        @if (receipt.Result?.Items is not null)
                                        {
                                            @foreach (var item in receipt.Result?.Items!)
                                            {
                                                <li><strong>@item.Name</strong> - @item.Price?.ToString("C", ci)</li>
                                            }
                                        }
                                    </ul>
                                </td>
                            </tr>
                        }
                    </tbody>
                </table>
            </div>
        </div>
    }
    else if (processed && (analyzedReceipts == null || !analyzedReceipts.Any()))
    {
        <div class="alert alert-warning text-center" role="alert">
            <strong>No results found.</strong> Please try again with different images or ensure they are clear and legible.
        </div>
    }
</div>

@code {
    private bool hasFiles;
    private bool processing;
    private bool processed;
    private List<byte[]> fileBytesList = new();
    private List<ReceiptAnalyzeResult> analyzedReceipts = new();
    CultureInfo ci = new CultureInfo("es-es");

    private async Task HandleFileSelected(InputFileChangeEventArgs e)
    {
        try
        {
            fileBytesList.Clear();

            foreach (var file in e.GetMultipleFiles())
            {
                var memoryStream = new MemoryStream();
                await file.OpenReadStream().CopyToAsync(memoryStream);
                fileBytesList.Add(memoryStream.ToArray());
            }

            hasFiles = fileBytesList.Any();
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Error while handling file upload.");
        }
    }

    private async Task ProcessReceipts()
    {
        if (!hasFiles)
            return;

        processing = true;
        analyzedReceipts.Clear();

        try
        {
            foreach (var fileBytes in fileBytesList)
            {
                var result = await ApiClient.AnalyzeReceiptAsync(fileBytes);
                if (result != null)
                {
                    analyzedReceipts.Add(result);
                }
            }
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Error while processing receipts.");
        }
        finally
        {
            processing = false;
            processed = true;
        }
    }
}

```このページでは、レシートをアップロードし、分析結果を店名、日付、合計金額、商品リストの表で表示します。

<p align="center">
<img src="https://imgur.com/BLswKhm.png">
</p>

## ステップ 3: .NET Aspire

<p align="center">
<img src="https://imgur.com/ja56RWN.png">
</p>

### .NET Aspire とは何ですか?

.NET Aspire は、監視可能な実稼働対応アプリを構築するための強力なツール、テンプレート、パッケージのセットです。 .NET Aspire は、特定のクラウドネイティブの問題を処理する NuGet パッケージのコレクションを通じて提供されます。クラウド ネイティブ アプリは、多くの場合、単一のモノリシック コード ベースではなく、相互接続された小さな部分またはマイクロサービスで構成されます。クラウドネイティブ アプリは通常、データベース、メッセージング、キャッシュなどの多数のサービスを消費します。サポートの詳細については、.NET Aspire サポート ポリシーを参照してください。

分散アプリケーションは、異なるホスト上で実行されるコンテナなど、複数のノードにわたって計算リソースを使用するアプリケーションです。このようなノードは、ユーザーに応答を配信するためにネットワーク境界を越えて通信する必要があります。クラウド ネイティブ アプリは、クラウド インフラストラクチャのスケーラビリティ、復元力、管理容易性を最大限に活用する、特定の種類の分散アプリです。

このプロジェクトで **.NET Aspire** を使用すると、システム全体の品質が向上する次のようないくつかの利点が得られます。

### 1. **集中ログ**

.NET Aspire は、アプリケーション全体のログを自動的に統合します。つまり、サービスごとにログを手動で構成する必要はありません。これにより、ログの一貫性が確保され、一元的な場所に保存されるため、デバッグと監視がはるかに簡単になります。

たとえば、`AiApiClient` クラスはログを使用して、AI サービスに送信された画像バイト、API 応答、分析プロセス中に発生したエラーを記録します。

```csharp
_logger.LogInformation("Sending analyze request with image bytes of length: {Length}",

 imageBytes.Length);

2. 自動メトリクス収集

.NET Aspire は、応答時間、リクエスト数、エラー率などの重要なアプリケーション メトリックも自動的に追跡し、レポートします。これは、アプリケーションのパフォーマンスを理解し、ボトルネックや問題を迅速に検出するのに役立ちます。

3. パフォーマンスの向上

.NET Aspire は HTTP 呼び出しを最適化するため、応答時間を短縮し、不必要なリソースの消費を削減します。接続プーリング、リクエストの再試行、インテリジェントなルーティングなどの機能を提供します。

4. シームレスな統合

.NET Aspire は、さまざまなサービス (このプロジェクトの AI サービスや API サービスなど) の統合を簡素化し、展開プロセスを合理化します。 Aspire がインフラストラクチャ関連のタスクを処理してくれるため、低レベルの構成について心配する必要はありません。

結論AI はもはや単なるバズワードや SF 映画で見られるものではありません。このプロジェクトで私たちが取り組んだ問題、領収書から構造化データを抽出する問題など、今日の現実世界の問題を積極的に解決しています。 Azure OpenAI.NET AspireBlazor の助けを借りて、時間がかかりエラーが発生しやすい手動タスクを自動化できます。 AI は単にチャットしたり、ChatGPT のようなプロンプトに応答したりするだけではありません。画像を解釈し、貴重な情報を抽出し、実用的な洞察を数秒で提供します。

領収書分析には Azure OpenAI、ログ記録とメトリクスとのシームレスな統合には .NET Aspire を使用することで、強力でスケーラブルなソリューションを作成しました。ビジネス プロセスを合理化し、退屈なタスクを自動化し、精度を向上させる AI の可能性は非常に大きく、これはその応用例の一例にすぎません。

この投稿は、Calendario de Adviento de Inteligencia Artificial 2024 en Español の一部です。このイベントは、現実世界の AI アプリケーションを紹介し、スペイン語圏のテクノロジー コミュニティに最新のトレンドを啓蒙するイベントです。 AI とその可能性についてさらに詳しく知りたい場合は、このイベントから始めるのが最適です。

AI は私たちの働き方を変革しつつあり、このプロジェクトは可能性を垣間見るだけです。 AI の真の力は、レシートの処理、画像の分析、傾向の予測など、実際の問題を解決する能力にあります。私たちは表面をなぞっただけです。

ソースコード

このプロジェクトの完全なソース コードは GitHub で入手できます。自由にダウンロードして、AI サービスと API サービスがどのように連携するかを調べて、独自のユースケースに合わせて調整してください。問題が発生した場合、または改善のアイデアがある場合は、ためらわずに問題を作成するか、プル リクエストを送信してください。貢献はいつでも歓迎されており、あなたのフィードバックはこのプロジェクトをさらに改善するのに役立ちます。

コーディングを楽しんでください!