Controlando os gastos de Natal com Kernel Semântico

· 12 min de leitura

## Introdução

À medida que as festas de fim de ano se aproximam, gerenciar despesas pode se tornar um desafio, principalmente com a enxurrada de compras e compras de presentes. Nesta postagem do blog, exploraremos como aproveitar a inteligência artificial para ajudar a controlar seus gastos de Natal usando tecnologias .NET. Ao analisar recibos com o poder do Kernel Semântico e da IA, podemos extrair com eficiência detalhes importantes, como nomes de lojas, datas, listas de itens e valores totais. Esta solução permite monitorar e gerenciar sem esforço seus gastos de Natal, garantindo que você mantenha o controle do seu orçamento sem o incômodo de revisar manualmente os recibos.

Calendário de Adviento de Inteligência Artificial 2024 em espanhol

Este projeto foi inspirado na minha participação no Calendario de Adviento de Inteligencia Artificial 2024 en Español, um evento online dedicado à IA. Você pode encontrar mais sobre o evento neste link Dev.to.

O projeto

Para este projeto, usaremos o Azure OpenAI, um serviço que nos permite utilizar modelos de IA poderosos, como o GPT-4, para processar e analisar imagens. O processo envolve várias etapas, desde a configuração do serviço API de back-end até a integração com um front-end do Blazor para uploads de imagens. Também usaremos o .NET Aspire, um componente que ajuda a conectar tudo perfeitamente.

Pré-requisitos

Antes de mergulharmos no código, certifique-se de ter os seguintes pré-requisitos:

-.NET 9

  • Acesso Azure OpenAI (chave API)
  • Visual Studio ou Código do Visual Studio
  • Conhecimento básico de Blazor, clientes HTTP e desenvolvimento de API

A solução Visual Studio

Acabaremos tendo algo assim, gosto de manter as coisas separadas e com nomes legais então é assim:

Mas vamos passo a passo criando coisas!

Passo 0: Os modelos

O núcleo do aplicativo Receipt Scanner depende de vários modelos principais que facilitam a interação entre os serviços front-end, API e IA. Abaixo estão os principais modelos utilizados neste projeto:

  • AnalyzeReceiptRequest
    Este modelo representa a estrutura de solicitação para análise de um recibo. Ele contém a propriedade ImageBytes, que contém a matriz de bytes da imagem do recibo que será processada.

    public class AnalyzeReceiptRequest
    {
        public byte[] ImageBytes { get; set; }
    }
    
  • ReceiptAnalyzeResult
    Este modelo captura o resultado após o processamento de um recibo. Contém os dados estruturados extraídos do recibo, como nome da loja, data, itens e valor total.

    public class ReceiptAnalyzeResult
    {
        public DateTime CreatedAt { get; set; }
        public ReceiptData Result { get; set; }
    }
    
  • Dados de Recibo
    Este é o modelo que contém os dados estruturados do recebimento. Inclui propriedades para nome da loja, data, lista de itens (com nome e preço de cada item) e valor total no recibo.

    public class ReceiptData
    {
        public string Store { get; set; }
        public DateTime? Date { get; set; }
        public List<ReceiptItem> Items { get; set; }
        public decimal? Total { get; set; }
    }
    
  • Item de recibo
    Cada item do recibo é representado por este modelo. Ele contém o nome do item e seu preço.

    public class ReceiptItem
    {
        public string Name { get; set; }
        public decimal? Price { get; set; }
    }
    ```Esses modelos servem de base para a passagem de dados entre o cliente e o servidor, garantindo um fluxo tranquilo de informações. A API recebe a imagem do recibo e, em troca, processa e retorna um objeto JSON estruturado que pode ser facilmente consumido pelo front-end.
    

Etapa 1: Configurando o serviço API de back-end

A primeira etapa na construção deste aplicativo é configurar um serviço API para analisar imagens de recibos. Usaremos a API Azure OpenAI para extrair informações das imagens de recibo. Aqui está um resumo de como tudo se encaixa:

Serviço de IA – Um mergulho profundo

O serviço de IA está no centro do nosso sistema de análise de recibos. É responsável pela comunicação com a API do Azure OpenAI para processar os dados da imagem e retornar insights significativos. A classe AiApiClient é o cliente que lidará com todas as interações com a API Azure OpenAI.

Implementação do cliente de IA

O AiApiClient é o principal componente responsável por enviar a imagem do recibo (em formato de matriz de bytes) para a API Azure OpenAI. Ele lida com a comunicação, registro de erros e análise de dados:

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;
        }
    }
}

Nesta parte do código, definimos o método AnalyzeAsync, que é responsável por:

  1. Envio da matriz de bytes da imagem para a API Azure OpenAI.
  2. Tratamento de quaisquer erros ou respostas malsucedidas da API.
  3. Analisar os dados JSON retornados em um resultado estruturado (ReceiptAnalyzeResult).

Os benefícios de separar esta funcionalidade num serviço dedicado (AiApiClient) incluem:

  • Tratamento de erros: Tratamento centralizado de erros, como problemas de rede ou respostas inválidas.
  • Registro: Registro adequado de solicitações e respostas para monitorar o comportamento do sistema.

Serviço API - Tratamento de solicitações e respostas

O Serviço de API atua como intermediário entre o aplicativo front-end Blazor e o serviço de IA. Este serviço é responsável por aceitar os dados da imagem, repassá-los ao serviço de IA e devolver os resultados da análise ao cliente.

Ponto final da API

Nesta etapa, definimos um endpoint de API simples para aceitar imagens de recebimento, encaminhá-las ao serviço de IA para processamento e retornar os resultados ao cliente:

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();

Este ponto final:

  1. Aceita a imagem do recibo como parte do corpo da solicitação.
  2. Chama o método endopint AiService para enviar a imagem ao Azure OpenAI para processamento.
  3. Devolve o resultado da análise ao cliente.

Etapa 2: Configurando o front-end do Blazor

Agora que configuramos o backend, vamos voltar nossa atenção para o frontend do Blazor. É aqui que os usuários podem fazer upload de imagens de recibos para análise e ver os resultados.

Implementação da página Blazor

A página Blazor fornece uma interface simples onde os usuários podem fazer upload de várias imagens de recibo e ver os resultados da análise exibidos em uma tabela. Aqui está o código da página:

@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;
        }
    }
}

```Esta página permite aos usuários fazer upload de recibos e mostra os resultados da análise em uma tabela com nomes de lojas, datas, valores totais e lista de itens.

<p alinhar="centro">
<img src="https://imgur.com/BLswKhm.png">
</p>

## Etapa 3: .NET Aspire

<p alinhar="centro">
<img src="https://imgur.com/ja56RWN.png">
</p>

### O que é o .NET Aspire?

O .NET Aspire é um conjunto de ferramentas, modelos e pacotes poderosos para criar aplicativos observáveis ​​e prontos para produção. O .NET Aspire é fornecido por meio de uma coleção de pacotes NuGet que tratam de questões específicas nativas da nuvem. Os aplicativos nativos da nuvem geralmente consistem em peças ou microsserviços pequenos e interconectados, em vez de uma base de código única e monolítica. Os aplicativos nativos da nuvem geralmente consomem um grande número de serviços, como bancos de dados, mensagens e cache. Para obter informações sobre suporte, consulte a Política de Suporte do .NET Aspire.

Um aplicativo distribuído é aquele que utiliza recursos computacionais em vários nós, como contêineres executados em hosts diferentes. Esses nós devem comunicar-se através dos limites da rede para entregar respostas aos usuários. Um aplicativo nativo da nuvem é um tipo específico de aplicativo distribuído que aproveita ao máximo a escalabilidade, a resiliência e a capacidade de gerenciamento das infraestruturas em nuvem.

Usar o **.NET Aspire** para este projeto oferece vários benefícios que melhoram a qualidade geral do sistema, como:

### 1. **Registro centralizado**

O .NET Aspire integra automaticamente o log em todo o aplicativo, o que significa que você não precisa configurar manualmente o log para cada serviço. Isso garante que os logs sejam consistentes e armazenados em um local centralizado, facilitando muito a depuração e o monitoramento.

Por exemplo, a classe `AiApiClient` usa log para registrar os bytes da imagem enviados ao serviço de IA, as respostas da API e quaisquer erros que ocorram durante o processo de análise.

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

 imageBytes.Length);

2. Coleta automática de métricas

O .NET Aspire também rastreia e relata automaticamente métricas importantes do aplicativo, como tempos de resposta, contagens de solicitações e taxas de erro. Isso ajuda você a entender o desempenho do aplicativo e detectar rapidamente quaisquer gargalos ou problemas.

3. Desempenho melhorado

O .NET Aspire otimiza chamadas HTTP, o que ajuda a manter baixos os tempos de resposta e a reduzir o consumo desnecessário de recursos. Ele fornece recursos como pool de conexões, novas tentativas de solicitação e roteamento inteligente.

4. Integração Perfeita

O .NET Aspire simplifica a integração de vários serviços (como os serviços de IA e API neste projeto) e agiliza o processo de implantação. Você não precisa se preocupar com configurações de baixo nível, pois o Aspire cuida das tarefas relacionadas à infraestrutura para você.

ConclusãoIA não é mais apenas uma palavra da moda ou algo que vemos em filmes de ficção científica. Atualmente, ele está resolvendo ativamente problemas do mundo real, como aquele que abordamos neste projeto: extrair dados estruturados de recibos. Com a ajuda do Azure OpenAI, do .NET Aspire e do Blazor, podemos automatizar o que de outra forma seria uma tarefa manual demorada e propensa a erros. A IA não apenas conversa ou responde a solicitações como o ChatGPT; ele interpreta imagens, extrai informações valiosas e nos fornece insights acionáveis ​​em segundos.

Ao usar o Azure OpenAI para análise de recibos e o .NET Aspire para integração perfeita com registros e métricas, criamos uma solução poderosa e escalonável. O potencial da IA ​​para agilizar processos de negócios, automatizar tarefas tediosas e melhorar a precisão é enorme, e este é apenas um exemplo de como pode ser aplicada.

Esta postagem faz parte do Calendario de Adviento de Inteligencia Artificial 2024 en Español, um evento que apresenta aplicações de IA do mundo real e educa a comunidade tecnológica de língua espanhola sobre as últimas tendências. Se você deseja se aprofundar na IA e em suas possibilidades, este evento é um ótimo lugar para começar.

A IA está transformando a forma como trabalhamos e este projeto é apenas um vislumbre do que é possível. O verdadeiro poder da IA ​​está na sua capacidade de resolver problemas reais, seja processando recibos, analisando imagens ou prevendo tendências. Estamos apenas arranhando a superfície.

Código Fonte

O código-fonte completo deste projeto está disponível no GitHub. Sinta-se à vontade para baixá-lo, explorar como os serviços de IA e API funcionam juntos e adaptá-lo para seus próprios casos de uso. Se você encontrar algum problema ou tiver ideias para melhorias, não hesite em criar um problema ou enviar uma solicitação pull. Contribuições são sempre bem-vindas e seus comentários ajudarão a tornar este projeto ainda melhor!

Boa codificação!