使用语义内核控制圣诞节支出

· 6 分钟阅读

## 简介

随着假期的临近,管理开支可能成为一项挑战,尤其是在购物和礼品购买热潮的情况下。在这篇博文中,我们将探讨如何利用人工智能使用 .NET 技术来帮助跟踪您的圣诞节支出。通过利用语义内核和人工智能的力量分析收据,我们可以有效地提取关键详细信息,例如商店名称、日期、商品列表和总额。该解决方案可让您轻松监控和管理您的圣诞节支出,确保您控制在预算范围内,而无需手动查看收据。

2024 年人工智能Adviento Adviento 西班牙语日历

<p对齐=“中心”>

这个项目的灵感来自于我参加 Calendario de Adviento de Inteligencia Artificial 2024 en Español,这是一个专门针对人工智能的在线活动。您可以在 此 Dev.to 链接 上找到有关该活动的更多信息。

项目

对于这个项目,我们将使用 Azure OpenAI,该服务允许我们利用强大的 AI 模型(例如 GPT-4)来处理和分析图像。该过程涉及多个步骤,从设置后端 API 服务到与 Blazor 前端集成以进行图像上传。我们还将使用**.NET Aspire**,这是一个有助于无缝连接一切的组件。

先决条件

在我们深入研究代码之前,请确保您满足以下先决条件:

  • .NET 9
  • Azure OpenAI 访问(API 密钥)
  • Visual Studio 或 Visual Studio Code
  • Blazor、HTTP 客户端和 API 开发的基本知识

Visual Studio 解决方案

我们最终会得到这样的东西,我喜欢把东西分开并用很酷的名字,所以它看起来是这样的:

<p对齐=“中心”>

但让我们一步一步来创造东西吧!

步骤 0:模型

收据扫描仪应用程序的核心依赖于几个关键模型,这些模型促进前端、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 服务 - 深入探讨

人工智能服务是我们收据分析系统的核心。它负责与 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) 的好处包括:

  • 错误处理: 集中处理网络问题或无效响应等错误。
  • 日志记录: 正确记录请求和响应以监视系统行为。

<p对齐=“中心”>

API 服务 - 处理请求和响应

API 服务 充当前端 Blazor 应用程序和 AI 服务之间的中介。该服务负责接受图像数据,将其传递给AI服务,并将分析结果返回给客户端。

API 端点

在此步骤中,我们定义一个简单的 API 端点来接受收据图像,将其转发给 AI 服务进行处理,并将结果返回给客户端:

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. 将分析结果返回给客户端。

<p对齐=“中心”>

步骤 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);

<p对齐=“中心”>

2. 自动指标收集

.NET Aspire 还自动跟踪和报告重要的应用程序指标,例如响应时间、请求计数和错误率。这可以帮助您了解应用程序的执行情况并快速检测任何瓶颈或问题。

<p对齐=“中心”>

3. 提高性能

.NET Aspire 优化了 HTTP 调用,这有助于缩短响应时间并减少不必要的资源消耗。它提供连接池、请求重试和智能路由等功能。

4. 无缝集成

.NET Aspire 简化了各种服务(如本项目中的 AI 和 API 服务)的集成并简化了部署过程。您无需担心低级配置,因为 Aspire 会为您处理与基础设施相关的任务。

<p对齐=“中心”>

结论人工智能不再只是一个流行词或我们在科幻电影中看到的东西。它正在积极解决当今的现实世界问题,就像我们在这个项目中解决的问题一样——从收据中提取结构化数据。在 Azure OpenAI.NET AspireBlazor 的帮助下,我们可以自动化原本耗时且容易出错的手动任务。 AI 不仅仅像 ChatGPT 那样聊天或响应提示;它可以解释图像,提取有价值的信息,并在几秒钟内为我们提供可操作的见解。

通过使用 Azure OpenAI 进行收据分析,并使用 .NET Aspire 与日志记录和指标无缝集成,我们创建了一个功能强大且可扩展的解决方案。人工智能在简化业务流程、自动化繁琐任务和提高准确性方面的潜力是巨大的,这只是其应用的一个例子。

这篇文章是 Calendario de Adviento de Inteligencia Artificial 2024 en Español 的一部分,该活动展示了现实世界的人工智能应用,并向西班牙语技术社区介绍最新趋势。如果您想更深入地了解人工智能及其可能性,那么本次活动是一个很好的起点。

人工智能正在改变我们的工作方式,这个项目只是对可能性的一瞥。人工智能的真正力量在于它解决实际问题的能力——无论是处理收据、分析图像还是预测趋势。我们只是触及了表面。

源代码

该项目的完整源代码可在 GitHub 上找到。请随意下载它,探索 AI 和 API 服务如何协同工作,并根据您自己的用例进行调整。如果您遇到任何问题,或者您有改进的想法,请立即创建问题或提交拉取请求。我们始终欢迎您的贡献,您的反馈将有助于使这个项目变得更好!

快乐编码!