This entry is part 2 of 25 in the series Introducción a Microsoft Semantic Kernel

Introducción

Los plugins (anteriormente llamados Skills) son componentes reutilizables en Semantic Kernel que encapsulan funcionalidad específica. Aprenderás a crear plugins que pueden ser invocados por el kernel o por el propio LLM para realizar tareas específicas.

¿Qué son los Plugins?

Un plugin es una colección de funciones que:

  • Realizan tareas específicas (buscar datos, realizar cálculos, llamar APIs)
  • Pueden ser invocadas por el LLM automáticamente
  • Son reutilizables en diferentes contextos
  • Combinan código tradicional con capacidades de IA

Creando tu Primer Plugin

Plugin Simple: Calculadora

using System.ComponentModel;
using Microsoft.SemanticKernel;

public class CalculadoraPlugin
{
    [KernelFunction("sumar")]
    [Description("Suma dos números")]
    public double Sumar(
        [Description("Primer número")] double a,
        [Description("Segundo número")] double b)
    {
        return a + b;
    }

    [KernelFunction("multiplicar")]
    [Description("Multiplica dos números")]
    public double Multiplicar(
        [Description("Primer número")] double a,
        [Description("Segundo número")] double b)
    {
        return a * b;
    }

    [KernelFunction("calcular_porcentaje")]
    [Description("Calcula el porcentaje de un número")]
    public double CalcularPorcentaje(
        [Description("Número base")] double numero,
        [Description("Porcentaje a calcular")] double porcentaje)
    {
        return numero * (porcentaje / 100);
    }
}

Registrar el Plugin

var kernel = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey)
    .Build();

// Registrar plugin como objeto
kernel.Plugins.AddFromObject(new CalculadoraPlugin());

// Ahora el LLM puede usar estas funciones automáticamente
var response = await kernel.InvokePromptAsync(
    "Calcula el 15% de 200 y luego multiplícalo por 3");

Console.WriteLine(response);

Plugin Asíncrono: Búsqueda de Datos

using System.ComponentModel;
using Microsoft.SemanticKernel;

public class BuscadorPlugin
{
    private readonly HttpClient _httpClient;

    public BuscadorPlugin(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    [KernelFunction("buscar_informacion")]
    [Description("Busca información sobre un tema específico")]
    public async Task<string> BuscarInformacionAsync(
        [Description("Tema a buscar")] string tema,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // Simular búsqueda en una API
            var url = "https://api.ejemplo.com/search?q={Uri.EscapeDataString(tema)}";
  
            var response = await _httpClient.GetStringAsync(url, cancellationToken);  
            
            return response;  
    }  catch (Exception ex)  
    {  
       return "Error al buscar: {ex.Message}";
    }  
}  

    [KernelFunction("verificar_disponibilidad")]
    [Description("Verifica si un servicio está disponible")]  
    public async Task<bool> VerificarDisponibilidadAsync(  
        [Description("URL del servicio")] string url,  
        CancellationToken cancellationToken = default)  
    {  
        try  
        {  
           var response = await _httpClient.GetAsync(url, cancellationToken);  
           return response.IsSuccessStatusCode;  
        }  catch  
        {  
           return false;  
        }  
    } 
} 

Plugin con Dependencias: Acceso a Datos

using System.ComponentModel;
using Microsoft.SemanticKernel;
using Microsoft.Extensions.Logging;

public class RepositorioPlugin
{
    private readonly ILogger<RepositorioPlugin> _logger;
    private readonly IDatabaseService _database;

    public RepositorioPlugin(
        ILogger<RepositorioPlugin> logger,
        IDatabaseService database)
    {
        _logger = logger;
        _database = database;
    }

    [KernelFunction("obtener_usuario")]
    [Description("Obtiene información de un usuario por ID")]
    public async Task<string> ObtenerUsuarioAsync(
        [Description("ID del usuario")] string userId,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Buscando usuario {UserId}", userId);

        try
        {
            var usuario = await _database.GetUserAsync(userId, cancellationToken);

            if (usuario == null)
            {
                return "Usuario no encontrado";
            }

            return "Usuario: {usuario.Nombre}, Email: {usuario.Email}";  
        }  catch (Exception ex)  
        {  
           _logger.LogError(ex, "Error obteniendo usuario {UserId}", userId);  
          return "Error al obtener usuario";  
        }  
    }  

[KernelFunction("guardar_nota")]  
[Description("Guarda una nota en la base de datos")]  
public async Task<string> GuardarNotaAsync(
        [Description("Contenido de la nota")] string contenido,  
        [Description("Categoría de la nota")] string categoria,  
        CancellationToken cancellationToken = default)  
    {  
        _logger.LogInformation("Guardando nota en categoría {Categoria}", categoria);
  
        try  {  
            var nota = new Nota  {  Contenido = contenido,  Categoria = categoria,  FechaCreacion = DateTime.UtcNow};  

await _database.SaveNoteAsync(nota, cancellationToken);  

return "Nota guardada exitosamente con ID: {nota.Id}";  

        }  catch (Exception ex)  
        {  
           _logger.LogError(ex, "Error guardando nota");  

           return "Error al guardar la nota";  

        }  
    } 
} 

Plugin Complejo: Procesamiento de Texto

using System.ComponentModel;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.SemanticKernel;

public class TextoPlugin
{
    [KernelFunction("contar_palabras")]
    [Description("Cuenta las palabras en un texto")]
    public int ContarPalabras([Description("Texto a analizar")] string texto)
    {
        if (string.IsNullOrWhiteSpace(texto))
            return 0;

        return texto.Split(new[] { ' ', '\t', '\n', '\r' }, 
            StringSplitOptions.RemoveEmptyEntries).Length;
    }

    [KernelFunction("extraer_emails")]
    [Description("Extrae todos los emails de un texto")]
    public List<string> ExtraerEmails([Description("Texto a procesar")] string texto)
    {
        if (string.IsNullOrWhiteSpace(texto))
            return new List<string>();

        var pattern = @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b";
        var matches = Regex.Matches(texto, pattern);

        return matches.Select(m => m.Value).Distinct().ToList();
    }

    [KernelFunction("formatear_texto")]
    [Description("Formatea un texto según reglas específicas")]
    public string FormatearTexto(
        [Description("Texto a formatear")] string texto,
        [Description("Tipo de formato: uppercase, lowercase, titlecase")] string formato)
    {
        if (string.IsNullOrWhiteSpace(texto))
            return string.Empty;

        return formato.ToLower() switch
        {
            "uppercase" => texto.ToUpper(),
            "lowercase" => texto.ToLower(),
            "titlecase" => System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(texto.ToLower()),
            _ => texto
        };
    }

    [KernelFunction("resumen_estadistico")]
    [Description("Proporciona estadísticas sobre un texto")]
    public string ResumenEstadistico([Description("Texto a analizar")] string texto)
    {
        if (string.IsNullOrWhiteSpace(texto))
            return "Texto vacío";

        var palabras = ContarPalabras(texto);
        var caracteres = texto.Length;
        var lineas = texto.Split('\n').Length;
        var emails = ExtraerEmails(texto).Count;

        var sb = new StringBuilder();

        sb.AppendLine("Palabras: {palabras}");  
        sb.AppendLine("Caracteres: {caracteres}");  
        sb.AppendLine("Líneas: {lineas}");  
        sb.AppendLine("Emails encontrados: {emails}");  
        return sb.ToString();  
    } 
} 

Registro de Plugins con Inyección de Dependencias

using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;

// Configurar servicios
var services = new ServiceCollection();
services.AddHttpClient();
services.AddLogging();
services.AddSingleton<IDatabaseService, DatabaseService>();

// Crear kernel con DI
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton(services.BuildServiceProvider());

builder.AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey);

var kernel = builder.Build();

// Registrar plugins con dependencias resueltas por DI
var serviceProvider = kernel.Services.GetRequiredService<IServiceProvider>();
var httpClient = serviceProvider.GetRequiredService<HttpClient>();
var database = serviceProvider.GetRequiredService<IDatabaseService>();
var logger = serviceProvider.GetRequiredService<ILogger<RepositorioPlugin>>();

kernel.Plugins.AddFromObject(new BuscadorPlugin(httpClient));
kernel.Plugins.AddFromObject(new RepositorioPlugin(logger, database));
kernel.Plugins.AddFromObject(new TextoPlugin());
kernel.Plugins.AddFromObject(new CalculadoraPlugin());

Invocación Manual de Funciones

// Invocar función específica directamente
var resultado = await kernel.InvokeAsync(
    "CalculadoraPlugin",
    "sumar",
    new KernelArguments
    {
        ["a"] = 10,
        ["b"] = 20
    });

Console.WriteLine("Resultado: {resultado}"); 

// Invocar con tipo fuerte 

var textoPlugin = new TextoPlugin();
kernel.Plugins.AddFromObject(textoPlugin, "TextoPlugin"); 
var estadisticas = await kernel.InvokeAsync(  "TextoPlugin",  "resumen_estadistico",  new KernelArguments  {  ["texto"] = "Este es un texto de ejemplo para analizar."  }); 

Console.WriteLine(estadisticas); 

Plugin con Estado: Sesión de Usuario

using System.ComponentModel;
using Microsoft.SemanticKernel;

public class SesionPlugin
{
    private readonly Dictionary<string, object> _sessionData = new();

    [KernelFunction("guardar_en_sesion")]
    [Description("Guarda un valor en la sesión del usuario")]
    public string GuardarEnSesion(
        [Description("Clave para el valor")] string clave,
        [Description("Valor a guardar")] string valor)
    {
        _sessionData[clave] = valor;
        
        return "Valor guardado con clave '{clave}'";  
    }  

    [KernelFunction("obtener_de_sesion")]  
    [Description("Obtiene un valor de la sesión del usuario")]  
    public string ObtenerDeSesion([Description("Clave del valor")] string clave)  
    {  
        if (_sessionData.TryGetValue(clave, out var valor))  
        {  
            return valor?.ToString() ?? "Valor nulo";  
        }  
        
        return "No se encontró valor para la clave '{clave}'";  
    }  

    [KernelFunction("limpiar_sesion")]  
    [Description("Limpia todos los datos de la sesión")]  
    public string LimpiarSesion()  
    {  
        var count = _sessionData.Count;  
        _sessionData.Clear();  
        return "Sesión limpiada. Se eliminaron {count} valores.";  
    } 
} 

Mejores Prácticas

1. Descripciones Claras

Las descripciones ayudan al LLM a decidir cuándo usar cada función:

[KernelFunction("buscar_productos")]
[Description("Busca productos en el catálogo que coincidan con los criterios especificados")]
public async Task<string> BuscarProductosAsync(
    [Description("Término de búsqueda, puede ser nombre, categoría o descripción")] string busqueda,
    [Description("Precio máximo en euros, opcional")] double? precioMax = null)
{
    // Implementación
}

2. Manejo de Errores Robusto

[KernelFunction("procesar_pedido")]
[Description("Procesa un pedido de cliente")]
public async Task<string> ProcesarPedidoAsync(
    string pedidoId,
    CancellationToken cancellationToken)
{
    try
    {
        // Validación
        if (string.IsNullOrWhiteSpace(pedidoId))
            return "Error: ID de pedido inválido";

        // Procesamiento
        var resultado = await _service.ProcessOrderAsync(pedidoId, cancellationToken);

        return "Pedido {pedidoId} procesado exitosamente";  
    }  catch (OperationCanceledException)      
    {  
        return "Operación cancelada por el usuario";  
    }  catch (Exception ex)  
    {  
        _logger.LogError(ex, "Error procesando pedido {PedidoId}", pedidoId);  
        return "Error al procesar el pedido: {ex.Message}";  
    } 
} 

3. Logging para Debugging

[KernelFunction("analizar_sentimiento")]
[Description("Analiza el sentimiento de un texto")]
public async Task<string> AnalizarSentimientoAsync(
    string texto,
    CancellationToken cancellationToken)
{
    _logger.LogInformation("Analizando sentimiento de texto con {Length} caracteres", texto?.Length ?? 0);

    var resultado = await _sentimentService.AnalyzeAsync(texto, cancellationToken);

    _logger.LogInformation("Sentimiento detectado: {Sentiment} (confianza: {Confidence})", 
        resultado.Sentiment, resultado.Confidence);

    return resultado.ToString();
}

4. Parámetros Opcionales

[KernelFunction("buscar_articulos")]
[Description("Busca artículos en el blog")]
public async Task<string> BuscarArticulosAsync(
    [Description("Término de búsqueda")] string termino,
    [Description("Categoría opcional para filtrar")] string? categoria = null,
    [Description("Número máximo de resultados, default 10")] int limite = 10,
    CancellationToken cancellationToken = default)
{
    // Implementación con parámetros opcionales
}

Conclusión

Los plugins son fundamentales para extender Semantic Kernel con funcionalidad personalizada. Te permiten combinar la inteligencia de los LLMs con lógica de negocio específica, acceso a datos y servicios externos. Con los conceptos aprendidos puedes crear plugins robustos y reutilizables para tus aplicaciones de IA.

Share this content:

Introducción a Microsoft Semantic Kernel

. Introducción a Semantic Kernel con C#: Construyendo tu Primera Aplicación de IA . Clasificación de Intenciones con LLMs en .NET

por David Cantón Nadales

David Cantón Nadales, ingeniero de software de Sevilla, España, es autor del bestseller Build Your own Metaverse with Unity. Reconocido como Microsoft MVP y Top Voices en Aplicaciones Móviles de LinkedIn. Con más de 20 años de experiencia, David ha liderado cientos proyectos a lo largo de su carrera, incluyendo videojuegos y aplicaciones de realidad virtual y aumentada con Oculus, Hololens, HTC Vive, DayDream y LeapMotion. Ha trabajado como Tech Lead en importantes multinacionales como Grupo Viajes El Corte Inglés y actualmente en SCRM Lidl del Grupo Schwarz. Fue embajador de la comunidad Samsung Dev Spain y organizador del Google Developers Group Sevilla. Durante el confinamiento por COVID-19, destacó como emprendedor social con la creación de Grita, una red social que facilitaba el apoyo psicológico entre personas. En 2022, ganó los Samsung Top Developers Awards.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.