Semantic Kernel을 사용하여 크리스마스 지출 제어

· 10분 읽기

## 소개

연휴 시즌이 다가옴에 따라, 특히 쇼핑과 선물 구매가 붐비는 상황에서 비용 관리가 어려워질 수 있습니다. 이 블로그 게시물에서는 인공 지능을 활용하여 .NET 기술을 사용하여 크리스마스 지출을 추적하는 방법을 살펴보겠습니다. Semantic Kernel과 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를 사용할 것입니다.

전제 조건

코드를 살펴보기 전에 다음 전제 조건이 충족되었는지 확인하세요.

  • .NET 9
  • Azure OpenAI 액세스(API 키)
  • 비주얼 스튜디오 또는 비주얼 스튜디오 코드
  • Blazor, HTTP 클라이언트 및 API 개발에 대한 기본 지식

Visual Studio 솔루션

우리는 결국 다음과 같은 결과를 얻게 될 것입니다. 저는 항목을 분리하고 멋진 이름을 사용하는 것을 좋아하므로 다음과 같습니다.

하지만 단계별로 물건을 만들어 봅시다!

0단계: 모델

영수증 스캐너 애플리케이션의 핵심은 프런트 엔드, API 및 AI 서비스 간의 상호 작용을 촉진하는 여러 주요 모델에 의존합니다. 이 프로젝트에 사용된 주요 모델은 다음과 같습니다.

  • AnalyzeReceiptRequest
    이 모델은 영수증 분석을 위한 요청 구조를 나타냅니다. 여기에는 처리될 영수증 이미지의 바이트 배열을 보유하는 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 정렬="중앙">
<img src="https://imgur.com/BLswKhm.png">
</p>

## 3단계: .NET Aspire

<p 정렬="중앙">
<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는 더 이상 공상과학 영화에서나 볼 수 있는 유행어가 아닙니다. 영수증에서 구조화된 데이터를 추출하여 이 프로젝트에서 다룬 문제와 같이 오늘날 실제 문제를 적극적으로 해결하고 있습니다. Azure OpenAI, .NET AspireBlazor의 도움으로 시간이 많이 걸리고 오류가 발생하기 쉬운 수동 작업을 자동화할 수 있습니다. AI는 단순히 ChatGPT와 같은 메시지에 채팅하거나 응답하는 것이 아닙니다. 이미지를 해석하고, 귀중한 정보를 추출하고, 몇 초 만에 실행 가능한 통찰력을 제공합니다.

영수증 분석에 Azure OpenAI를 사용하고 로깅 및 메트릭과의 원활한 통합을 위해 .NET Aspire를 사용하여 강력하고 확장 가능한 솔루션을 만들었습니다. 비즈니스 프로세스를 간소화하고, 지루한 작업을 자동화하고, 정확성을 향상시키는 AI의 잠재력은 엄청나며, 이는 AI가 어떻게 적용될 수 있는지 보여주는 한 가지 예일 뿐입니다.

이 게시물은 실제 AI 애플리케이션을 선보이고 스페인어를 사용하는 기술 커뮤니티에 최신 트렌드를 교육하는 이벤트인 Calendario de Adviento de Inteligencia Artificial 2024 en Español의 일부입니다. AI와 그 가능성에 대해 더 깊이 알아보고 싶다면 이 이벤트를 시작하는 것이 좋습니다.

AI는 우리가 일하는 방식을 변화시키고 있으며, 이 프로젝트는 무엇이 가능한지 보여줍니다. AI의 진정한 힘은 영수증 처리, 이미지 분석, 추세 예측 등 실제 문제를 해결하는 능력에 있습니다. 우리는 단지 표면만 긁고 있을 뿐입니다.

소스 코드

이 프로젝트의 전체 소스 코드는 GitHub에서 확인할 수 있습니다. 자유롭게 다운로드하여 AI와 API 서비스가 어떻게 함께 작동하는지 살펴보고 자신의 사용 사례에 맞게 조정하세요. 문제가 발생하거나 개선을 위한 아이디어가 있는 경우 주저하지 말고 문제를 생성하거나 끌어오기 요청을 제출하세요. 기여는 언제나 환영하며 귀하의 피드백은 이 프로젝트를 더욱 개선하는 데 도움이 됩니다!

즐거운 코딩하세요!