Controlar el gasto navideño con Semantic Kernel
## Introducción
A medida que se acerca la temporada navideña, administrar los gastos puede convertirse en un desafío, especialmente con la avalancha de compras y regalos. En esta publicación de blog, exploraremos cómo aprovechar la inteligencia artificial para ayudarlo a realizar un seguimiento de sus gastos navideños utilizando tecnologías .NET. Al analizar los recibos con el poder del kernel semántico y la IA, podemos extraer de manera eficiente detalles clave como nombres de tiendas, fechas, listas de artículos y montos totales. Esta solución le permite monitorear y administrar sin esfuerzo sus gastos navideños, asegurándose de estar al tanto de su presupuesto sin la molestia de revisar los recibos manualmente.
Calendario de Adviento de Inteligencia Artificial 2024 en Español

Este proyecto se inspiró en mi participación en el Calendario de Adviento de Inteligencia Artificial 2024 en Español, un evento en línea dedicado a la IA. Puede encontrar más información sobre el evento en este enlace Dev.to.
El proyecto
Para este proyecto, usaremos Azure OpenAI, un servicio que nos permite utilizar potentes modelos de IA como GPT-4 para procesar y analizar imágenes. El proceso implica varios pasos, desde la configuración del servicio API de backend hasta la integración con una interfaz de Blazor para la carga de imágenes. También usaremos .NET Aspire, un componente que ayuda a conectar todo sin problemas.
Requisitos previos
Antes de profundizar en el código, asegúrese de tener los siguientes requisitos previos:
- .NET 9
- Acceso a Azure OpenAI (clave API)
- Visual Studio o Código de Visual Studio
- Conocimientos básicos de Blazor, clientes HTTP y desarrollo de API.
La solución de Visual Studio
Terminaremos teniendo algo como esto, me gusta mantener las cosas separadas y con nombres interesantes, así que así es como se ve:

¡Pero vayamos paso a paso creando cosas!
Paso 0: Los modelos
El núcleo de la aplicación Receipt Scanner se basa en varios modelos clave que facilitan la interacción entre los servicios front-end, API y AI. A continuación se detallan los principales modelos utilizados en este proyecto:
AnalizarSolicitud de recibo
Este modelo representa la estructura de solicitud para analizar un recibo. Contiene la propiedadImageBytes, que contiene la matriz de bytes de la imagen del recibo que se procesará.public class AnalyzeReceiptRequest { public byte[] ImageBytes { get; set; } }Resultado del análisis de recibo
Este modelo captura el resultado después de procesar un recibo. Contiene los datos estructurados extraídos del recibo, como el nombre de la tienda, la fecha, los artículos y el monto total.public class ReceiptAnalyzeResult { public DateTime CreatedAt { get; set; } public ReceiptData Result { get; set; } }Datos de recibo
Este es el modelo que contiene los datos de recibos estructurados. Incluye propiedades para el nombre de la tienda, la fecha, una lista de artículos (con el nombre y el precio de cada artículo) y el monto total en el 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; } }Artículo de recibo
Cada artículo del recibo está representado por este modelo. Contiene el nombre del artículo y su precio.public class ReceiptItem { public string Name { get; set; } public decimal? Price { get; set; } } ```Estos modelos sirven como base para pasar datos entre el cliente y el servidor, asegurando un flujo fluido de información. La API recibe la imagen del recibo y, a cambio, procesa y devuelve un objeto JSON estructurado que el front-end puede consumir fácilmente.
Paso 1: Configurar el servicio API de backend
El primer paso para crear esta aplicación es configurar un servicio API para analizar imágenes de recibos. Usaremos la API Azure OpenAI para extraer información de las imágenes de los recibos. Aquí hay un desglose de cómo encaja todo:
Servicio de IA: una inmersión profunda
El servicio de IA es el núcleo de nuestro sistema de análisis de recibos. Es responsable de comunicarse con la API de Azure OpenAI para procesar los datos de la imagen y devolver información significativa. La clase AiApiClient es el cliente que controlará todas las interacciones con la API de Azure OpenAI.
Implementación de cliente de IA
AiApiClient es el componente clave responsable de enviar la imagen del recibo (en formato de matriz de bytes) a la API de Azure OpenAI. Maneja la comunicación, el registro de errores y el análisis de datos:
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;
}
}
}
En esta parte del código definimos el método AnalyzeAsync, el cual es responsable de:
- Envío de la matriz de bytes de la imagen a la API de Azure OpenAI.
- Manejar cualquier error o respuesta fallida de la API.
- Analizar los datos JSON devueltos en un resultado estructurado (
ReceiptAnalyzeResult).
Los beneficios de separar esta funcionalidad en un servicio dedicado (AiApiClient) incluyen:
- Manejo de errores: Manejo centralizado de errores como problemas de red o respuestas no válidas.
- Registro: Registro adecuado de solicitudes y respuestas para monitorear el comportamiento del sistema.

Servicio API: manejo de solicitudes y respuestas
El Servicio API actúa como intermediario entre la aplicación frontal Blazor y el servicio de IA. Este servicio es responsable de aceptar los datos de la imagen, pasarlos al servicio de IA y devolver los resultados del análisis al cliente.
Punto final API
En este paso, definimos un punto final API simple para aceptar imágenes de recibos, reenviarlas al servicio de IA para su procesamiento y devolver los resultados al 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 punto final:
- Acepta la imagen del recibo como parte del cuerpo de la solicitud.
- Llama al método endopint
AiServicepara enviar la imagen a Azure OpenAI para su procesamiento. - Devuelve el resultado del análisis al cliente.

Paso 2: Configurar la interfaz de Blazor
Ahora que tenemos el backend configurado, dirijamos nuestra atención a la frontend de Blazor. Aquí es donde los usuarios pueden cargar las imágenes de sus recibos para analizarlas y ver los resultados.
Implementación de la página Blazor
La página Blazor proporciona una interfaz sencilla donde los usuarios pueden cargar varias imágenes de recibos y luego ver los resultados del análisis mostrados en una tabla. Aquí está el código de la 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 a los usuarios cargar recibos y muestra los resultados del análisis en una tabla con nombres de tiendas, fechas, montos totales y la lista de artículos.
<p align="centro">
<img src="https://imgur.com/BLswKhm.png">
</p>
## Paso 3: .NET Aspire
<p align="centro">
<img src="https://imgur.com/ja56RWN.png">
</p>
### ¿Qué es .NET Aspire?
.NET Aspire es un conjunto de potentes herramientas, plantillas y paquetes para crear aplicaciones observables y listas para producción. .NET Aspire se entrega a través de una colección de paquetes NuGet que manejan inquietudes específicas nativas de la nube. Las aplicaciones nativas de la nube a menudo constan de piezas o microservicios pequeños e interconectados en lugar de una base de código única y monolítica. Las aplicaciones nativas de la nube generalmente consumen una gran cantidad de servicios, como bases de datos, mensajería y almacenamiento en caché. Para obtener información sobre soporte, consulte la Política de soporte de .NET Aspire.
Una aplicación distribuida es aquella que utiliza recursos computacionales en múltiples nodos, como contenedores que se ejecutan en diferentes hosts. Dichos nodos deben comunicarse a través de los límites de la red para entregar respuestas a los usuarios. Una aplicación nativa de la nube es un tipo específico de aplicación distribuida que aprovecha al máximo la escalabilidad, la resiliencia y la capacidad de administración de las infraestructuras de la nube.
El uso de **.NET Aspire** para este proyecto proporciona varios beneficios que mejoran la calidad general del sistema, como por ejemplo:
### 1. **Registro centralizado**
.NET Aspire integra automáticamente el registro en toda la aplicación, lo que significa que no es necesario configurar manualmente el registro para cada servicio. Esto garantiza que los registros sean coherentes y se almacenen en una ubicación centralizada, lo que facilita mucho la depuración y el seguimiento.
Por ejemplo, la clase `AiApiClient` utiliza el registro para registrar los bytes de imagen enviados al servicio de IA, las respuestas de la API y cualquier error que ocurra durante el proceso de análisis.
```csharp
_logger.LogInformation("Sending analyze request with image bytes of length: {Length}",
imageBytes.Length);

2. Recopilación automática de métricas
.NET Aspire también rastrea e informa automáticamente métricas importantes de la aplicación, como tiempos de respuesta, recuentos de solicitudes y tasas de error. Esto le ayuda a comprender el rendimiento de la aplicación y a detectar rápidamente cualquier cuello de botella o problema.

3. Rendimiento mejorado
.NET Aspire optimiza las llamadas HTTP, lo que ayuda a mantener bajos los tiempos de respuesta y reducir el consumo innecesario de recursos. Proporciona funciones como agrupación de conexiones, reintentos de solicitudes y enrutamiento inteligente.
4. Integración perfecta
.NET Aspire simplifica la integración de varios servicios (como los servicios de IA y API en este proyecto) y agiliza el proceso de implementación. No necesita preocuparse por las configuraciones de bajo nivel, ya que Aspire se encarga de las tareas relacionadas con la infraestructura por usted.

ConclusiónLa IA ya no es sólo una palabra de moda o algo que vemos en las películas de ciencia ficción. Actualmente está resolviendo activamente problemas del mundo real, como el que abordamos en este proyecto: extraer datos estructurados de los recibos. Con la ayuda de Azure OpenAI, .NET Aspire y Blazor, podemos automatizar lo que de otro modo sería una tarea manual que consumiría mucho tiempo y sería propensa a errores. La IA no solo chatea o responde a indicaciones como ChatGPT; interpreta imágenes, extrae información valiosa y nos brinda información útil en segundos.
Al utilizar Azure OpenAI para el análisis de recibos y .NET Aspire para una integración perfecta con registros y métricas, hemos creado una solución que es potente y escalable. El potencial de la IA para agilizar los procesos empresariales, automatizar tareas tediosas y mejorar la precisión es enorme, y este es sólo un ejemplo de cómo se puede aplicar.
Esta publicación es parte del Calendario de Adviento de Inteligencia Artificial 2024 en Español, un evento que muestra aplicaciones de IA del mundo real y educa a la comunidad tecnológica de habla hispana sobre las últimas tendencias. Si busca profundizar en la IA y sus posibilidades, este evento es un excelente lugar para comenzar.
La IA está transformando nuestra forma de trabajar y este proyecto es solo una muestra de lo que es posible. El verdadero poder de la IA reside en su capacidad para resolver problemas reales, ya sea procesando recibos, analizando imágenes o prediciendo tendencias. Sólo estamos arañando la superficie.
Código fuente
El código fuente completo de este proyecto está disponible en GitHub. No dude en descargarlo, explorar cómo funcionan juntos los servicios de IA y API y adaptarlo a sus propios casos de uso. Si tiene algún problema o si tiene ideas para mejorar, no dude en crear un problema o enviar una solicitud de extracción. ¡Las contribuciones siempre son bienvenidas y sus comentarios ayudarán a que este proyecto sea aún mejor!
¡Feliz codificación!