2.Creando Plugins Personalizados en Semantic Kernel

Aprende a crear plugins (Skills) en Semantic Kernel con C#: KernelFunction, registro con DI, ejemplos async (calculadora, búsqueda, texto) y buenas prácticas.

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.