23. Implementación de Routers Conversacionales Inteligentes

This entry is parte 23 de 25 in the series Introducción a Microsoft Semantic Kernel

Introducción a Microsoft Semantic Kernel

1. Introducción a Semantic Kernel con C#: Construyendo tu Primera Aplicación de IA

2.Creando Plugins Personalizados en Semantic Kernel

3. Clasificación de Intenciones con LLMs en .NET

4. Servicios de Chat Completion con Azure OpenAI y Semantic Kernel

5. Vector Embeddings y Búsqueda Semántica con .NET

6. Prompt Engineering: Mejores Prácticas para LLMs

7. Integración de Azure OpenAI en Aplicaciones .NET

8. Configuración de Temperatura y Tokens en Modelos LLM

9. Estrategias de Caché para Servicios de IA

10. Manejo de Errores en Aplicaciones de IA con .NET

11. Salida JSON Estructurada con LLMs

12. Testing de Servicios de IA en .NET

13. Inyección de Dependencias con Semantic Kernel

14. Comprensión de Consultas con Lenguaje Natural

15. Sistemas de Búsqueda Semántica en Producción

16. Configuración de HttpClient para Servicios de IA

17. Guardrails y Validación en Sistemas de IA

18. Workflows Multi-Paso con IA

19. Optimización de Costos en Aplicaciones de IA

20. Monitoreo y Observabilidad de Servicios de IA

21. Arquitectura de Microservicios con IA

22. Seguridad en Aplicaciones de IA

23. Implementación de Routers Conversacionales Inteligentes

24. Filtrado por Relevancia Semántica en Búsquedas

25. Normalización y Preprocesamiento de Datos para IA

Introducción

Los routers conversacionales son el cerebro de un chatbot: deciden cómo manejar cada mensaje del usuario. En este tutorial aprenderás a implementar un router conversacional robusto que gestiona estados, intenciones, y flujos complejos.

Arquitectura de un Router

Un router conversacional efectivo necesita:

  1. Gestión de Estado: Recordar el contexto de la conversación
  2. Resolución de Intenciones: Determinar qué quiere hacer el usuario
  3. Enrutamiento: Dirigir a los handlers apropiados
  4. Comandos Globales: Manejar comandos prioritarios (menú, ayuda)
  5. Fallbacks: Respuestas cuando no se entiende el mensaje

Modelo de Estado de Conversación

public class ConversationState
{
    public required string UserId { get; init; }
    public string? CurrentFlow { get; set; }
    public Dictionary<string, object> Context { get; set; } = new();
    public DateTime LastInteraction { get; set; } = DateTime.UtcNow;
    public string? PendingAction { get; set; }
    public int MessageCount { get; set; }
}

public interface IConversationStateStore
{
    Task<ConversationState> GetAsync(string userId, CancellationToken cancellationToken = default);
    Task SaveAsync(ConversationState state, CancellationToken cancellationToken = default);
    Task ClearAsync(string userId, CancellationToken cancellationToken = default);
}

Implementación del Router

using Microsoft.Extensions.Logging;

public class ConversationRouter
{
    private readonly IConversationStateStore _stateStore;
    private readonly IIntentClassificationService _intentClassifier;
    private readonly ILogger<ConversationRouter> _logger;
    private readonly Dictionary<string, Func<RouteContext, Task<string>>> _handlers;
    
    public ConversationRouter(
        IConversationStateStore stateStore,
        IIntentClassificationService intentClassifier,
        ILogger<ConversationRouter> logger)
    {
        _stateStore = stateStore;
        _intentClassifier = intentClassifier;
        _logger = logger;
        
        // Registrar handlers
        _handlers = new()
        {
            ["menu"] = HandleMenuAsync,
            ["search"] = HandleSearchAsync,
            ["help"] = HandleHelpAsync,
            ["buy"] = HandleBuyAsync,
            ["status"] = HandleStatusAsync
        };
    }
    
    public async Task<string> RouteAsync(
        string userId,
        string message,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Routing message for user {UserId}: {Message}", userId, message);
        
        // 1. Obtener estado
        var state = await _stateStore.GetAsync(userId, cancellationToken);
        
        // 2. Manejar comandos globales (prioridad máxima)
        var globalResponse = await TryHandleGlobalCommandAsync(message, state);
        if (globalResponse != null)
        {
            state.MessageCount++;
            await _stateStore.SaveAsync(state, cancellationToken);
            return globalResponse;
        }
        
        // 3. Manejar flujo activo
        if (!string.IsNullOrEmpty(state.CurrentFlow))
        {
            var flowResponse = await TryHandleActiveFlowAsync(message, state, cancellationToken);
            if (flowResponse != null)
            {
                state.MessageCount++;
                await _stateStore.SaveAsync(state, cancellationToken);
                return flowResponse;
            }
        }
        
        // 4. Clasificar intención
        var classification = await _intentClassifier.ClassifyAsync(message, cancellationToken);
        
        _logger.LogInformation(
            "Intent classified: {Intent} (confidence: {Confidence})",
            classification.Intent,
            classification.Confidence);
        
        // 5. Enrutar al handler apropiado
        if (_handlers.TryGetValue(classification.Intent, out var handler))
        {
            var context = new RouteContext
            {
                UserId = userId,
                Message = message,
                State = state,
                Classification = classification
            };
            
            var response = await handler(context);
            
            state.MessageCount++;
            await _stateStore.SaveAsync(state, cancellationToken);
            
            return response;
        }
        
        // 6. Fallback
        _logger.LogWarning("No handler found for intent: {Intent}", classification.Intent);
        return "No estoy seguro de entender. ¿Puedes reformular tu pregunta?";
    }
    
    private async Task<string?> TryHandleGlobalCommandAsync(
        string message,
        ConversationState state)
    {
        var normalized = message.ToLowerInvariant().Trim();
        
        // Comando de menú
        if (normalized is "menu" or "menú" or "ayuda" or "help" or "inicio")
        {
            state.CurrentFlow = null;
            state.Context.Clear();
            return BuildMenuMessage();
        }
        
        // Comando de cancelar
        if (normalized is "cancelar" or "cancel" or "salir" or "exit")
        {
            state.CurrentFlow = null;
            state.Context.Clear();
            return "Operación cancelada. ¿En qué puedo ayudarte?";
        }
        
        return null;
    }
    
    private async Task<string?> TryHandleActiveFlowAsync(
        string message,
        ConversationState state,
        CancellationToken cancellationToken)
    {
        // Manejar flujo activo basado en state.CurrentFlow
        return state.CurrentFlow switch
        {
            "purchase" => await HandlePurchaseFlowAsync(message, state, cancellationToken),
            "registration" => await HandleRegistrationFlowAsync(message, state, cancellationToken),
            "feedback" => await HandleFeedbackFlowAsync(message, state, cancellationToken),
            _ => null
        };
    }
    
    private async Task<string> HandleMenuAsync(RouteContext context)
    {
        context.State.CurrentFlow = null;
        context.State.Context.Clear();
        return BuildMenuMessage();
    }
    
    private async Task<string> HandleSearchAsync(RouteContext context)
    {
        var query = context.Classification.Entities?.ContainsKey("query") == true
            ? context.Classification.Entities["query"].ToString()
            : context.Message;
        
        _logger.LogInformation("Handling search for: {Query}", query);
        
        // Implementar búsqueda
        return $"Buscando: {query}...";
    }
    
    private async Task<string> HandleHelpAsync(RouteContext context)
    {
        return """
        Puedo ayudarte con:
        
        🔍 Buscar productos o servicios
        🛒 Realizar compras
        📦 Consultar el estado de pedidos
        📋 Ver el menú de opciones
        
        ¿Qué necesitas?
        """;
    }
    
    private async Task<string> HandleBuyAsync(RouteContext context)
    {
        // Iniciar flujo de compra
        context.State.CurrentFlow = "purchase";
        context.State.Context["step"] = "product_selection";
        
        return "¿Qué producto te gustaría comprar?";
    }
    
    private async Task<string> HandleStatusAsync(RouteContext context)
    {
        var orderId = context.Classification.Entities?.ContainsKey("order_id") == true
            ? context.Classification.Entities["order_id"].ToString()
            : null;
        
        if (orderId == null)
        {
            return "Por favor, proporciona tu número de pedido.";
        }
        
        return $"Consultando estado del pedido {orderId}...";
    }
    
    private async Task<string> HandlePurchaseFlowAsync(
        string message,
        ConversationState state,
        CancellationToken cancellationToken)
    {
        var step = state.Context.TryGetValue("step", out var stepObj)
            ? stepObj?.ToString()
            : "product_selection";
        
        return step switch
        {
            "product_selection" => await HandleProductSelectionAsync(message, state),
            "quantity_selection" => await HandleQuantitySelectionAsync(message, state),
            "confirmation" => await HandlePurchaseConfirmationAsync(message, state),
            _ => "Flujo de compra no válido."
        };
    }
    
    private async Task<string> HandleProductSelectionAsync(
        string message,
        ConversationState state)
    {
        state.Context["selected_product"] = message;
        state.Context["step"] = "quantity_selection";
        
        return $"Has seleccionado: {message}\n\n¿Cuántas unidades deseas?";
    }
    
    private async Task<string> HandleQuantitySelectionAsync(
        string message,
        ConversationState state)
    {
        if (!int.TryParse(message, out var quantity) || quantity <= 0)
        {
            return "Por favor, indica una cantidad válida.";
        }
        
        state.Context["quantity"] = quantity;
        state.Context["step"] = "confirmation";
        
        var product = state.Context["selected_product"];
        
        return $"""
        Resumen de tu pedido:
        • Producto: {product}
        • Cantidad: {quantity}
        
        ¿Confirmas la compra? (sí/no)
        """;
    }
    
    private async Task<string> HandlePurchaseConfirmationAsync(
        string message,
        ConversationState state)
    {
        var normalized = message.ToLowerInvariant().Trim();
        
        if (normalized is "sí" or "si" or "s" or "yes" or "y")
        {
            var product = state.Context["selected_product"];
            var quantity = state.Context["quantity"];
            
            // Procesar pedido
            _logger.LogInformation(
                "Processing purchase: Product={Product}, Quantity={Quantity}",
                product,
                quantity);
            
            // Limpiar flujo
            state.CurrentFlow = null;
            state.Context.Clear();
            
            return $"✅ Pedido confirmado: {quantity}x {product}\n\nRecibirás un correo con los detalles.";
        }
        else
        {
            // Cancelar
            state.CurrentFlow = null;
            state.Context.Clear();
            
            return "Pedido cancelado. ¿Puedo ayudarte con algo más?";
        }
    }
    
    private async Task<string> HandleRegistrationFlowAsync(
        string message,
        ConversationState state,
        CancellationToken cancellationToken)
    {
        // Implementar flujo de registro
        return "Flujo de registro...";
    }
    
    private async Task<string> HandleFeedbackFlowAsync(
        string message,
        ConversationState state,
        CancellationToken cancellationToken)
    {
        // Implementar flujo de feedback
        return "Flujo de feedback...";
    }
    
    private string BuildMenuMessage()
    {
        return """
        🏠 Menú Principal
        
        Opciones disponibles:
        1️⃣ Buscar productos
        2️⃣ Ver mis pedidos
        3️⃣ Hacer una compra
        4️⃣ Ayuda
        
        Escribe el número o describe lo que necesitas.
        """;
    }
}

public class RouteContext
{
    public required string UserId { get; init; }
    public required string Message { get; init; }
    public required ConversationState State { get; init; }
    public required IntentClassificationResult Classification { get; init; }
}

Gestión de Estado con Redis

using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

public class RedisConversationStateStore : IConversationStateStore
{
    private readonly IDistributedCache _cache;
    private readonly TimeSpan _stateExpiration = TimeSpan.FromHours(24);
    
    public RedisConversationStateStore(IDistributedCache cache)
    {
        _cache = cache;
    }
    
    public async Task<ConversationState> GetAsync(
        string userId,
        CancellationToken cancellationToken = default)
    {
        var key = GetCacheKey(userId);
        var json = await _cache.GetStringAsync(key, cancellationToken);
        
        if (json == null)
        {
            return new ConversationState { UserId = userId };
        }
        
        return JsonSerializer.Deserialize<ConversationState>(json)
            ?? new ConversationState { UserId = userId };
    }
    
    public async Task SaveAsync(
        ConversationState state,
        CancellationToken cancellationToken = default)
    {
        var key = GetCacheKey(state.UserId);
        var json = JsonSerializer.Serialize(state);
        
        await _cache.SetStringAsync(
            key,
            json,
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = _stateExpiration
            },
            cancellationToken);
    }
    
    public async Task ClearAsync(
        string userId,
        CancellationToken cancellationToken = default)
    {
        var key = GetCacheKey(userId);
        await _cache.RemoveAsync(key, cancellationToken);
    }
    
    private string GetCacheKey(string userId)
    {
        return $"conversation:state:{userId}";
    }
}

Router con Middleware Pattern

public interface IRouterMiddleware
{
    Task<string?> ProcessAsync(
        RouteContext context,
        Func<RouteContext, Task<string>> next);
}

public class LoggingMiddleware : IRouterMiddleware
{
    private readonly ILogger<LoggingMiddleware> _logger;
    
    public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
    {
        _logger = logger;
    }
    
    public async Task<string?> ProcessAsync(
        RouteContext context,
        Func<RouteContext, Task<string>> next)
    {
        _logger.LogInformation(
            "Processing message from {UserId}: {Message}",
            context.UserId,
            context.Message);
        
        var stopwatch = Stopwatch.StartNew();
        var response = await next(context);
        stopwatch.Stop();
        
        _logger.LogInformation(
            "Response generated in {Elapsed}ms",
            stopwatch.ElapsedMilliseconds);
        
        return response;
    }
}

public class RateLimitMiddleware : IRouterMiddleware
{
    private readonly Dictionary<string, Queue<DateTime>> _userRequests = new();
    private readonly int _maxRequestsPerMinute = 10;
    
    public async Task<string?> ProcessAsync(
        RouteContext context,
        Func<RouteContext, Task<string>> next)
    {
        if (!_userRequests.ContainsKey(context.UserId))
        {
            _userRequests[context.UserId] = new Queue<DateTime>();
        }
        
        var userQueue = _userRequests[context.UserId];
        var now = DateTime.UtcNow;
        var oneMinuteAgo = now.AddMinutes(-1);
        
        // Limpiar requests antiguos
        while (userQueue.Count > 0 && userQueue.Peek() < oneMinuteAgo)
        {
            userQueue.Dequeue();
        }
        
        if (userQueue.Count >= _maxRequestsPerMinute)
        {
            return "Has enviado demasiados mensajes. Por favor, espera un momento.";
        }
        
        userQueue.Enqueue(now);
        
        return await next(context);
    }
}

Testing del Router

using Xunit;
using Moq;

public class ConversationRouterTests
{
    [Fact]
    public async Task RouteAsync_MenuCommand_ReturnsMenuMessage()
    {
        // Arrange
        var mockStateStore = new Mock<IConversationStateStore>();
        mockStateStore
            .Setup(x => x.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ConversationState { UserId = "test-user" });
        
        var mockClassifier = new Mock<IIntentClassificationService>();
        var logger = Mock.Of<ILogger<ConversationRouter>>();
        
        var router = new ConversationRouter(
            mockStateStore.Object,
            mockClassifier.Object,
            logger);
        
        // Act
        var response = await router.RouteAsync("test-user", "menu");
        
        // Assert
        Assert.Contains("Menú Principal", response);
    }
    
    [Fact]
    public async Task RouteAsync_PurchaseFlow_CompletesSuccessfully()
    {
        // Arrange
        var state = new ConversationState { UserId = "test-user" };
        var mockStateStore = new Mock<IConversationStateStore>();
        mockStateStore
            .Setup(x => x.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(state);
        
        var mockClassifier = new Mock<IIntentClassificationService>();
        mockClassifier
            .Setup(x => x.ClassifyAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new IntentClassificationResult
            {
                Intent = "buy",
                Confidence = 0.95
            });
        
        var logger = Mock.Of<ILogger<ConversationRouter>>();
        var router = new ConversationRouter(
            mockStateStore.Object,
            mockClassifier.Object,
            logger);
        
        // Act - Start purchase
        var response1 = await router.RouteAsync("test-user", "quiero comprar");
        
        // Act - Select product
        var response2 = await router.RouteAsync("test-user", "libro");
        
        // Act - Select quantity
        var response3 = await router.RouteAsync("test-user", "2");
        
        // Act - Confirm
        var response4 = await router.RouteAsync("test-user", "sí");
        
        // Assert
        Assert.Contains("producto", response1, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("unidades", response2, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("Resumen", response3, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("confirmado", response4, StringComparison.OrdinalIgnoreCase);
    }
}

Mejores Prácticas

1. Prioridad de Comandos

// Global > Flow > Intent > Fallback
private async Task<string> RouteWithPriorityAsync(string message, ConversationState state)
{
    return await TryHandleGlobalCommandAsync(message, state)
        ?? await TryHandleActiveFlowAsync(message, state)
        ?? await TryHandleIntentAsync(message, state)
        ?? GetFallbackMessage();
}

2. Timeouts para Flujos

public bool IsFlowExpired(ConversationState state)
{
    const int FlowTimeoutMinutes = 15;
    return state.LastInteraction.AddMinutes(FlowTimeoutMinutes) < DateTime.UtcNow;
}

3. Context Cleanup

public void CleanupExpiredState(ConversationState state)
{
    if (IsFlowExpired(state))
    {
        state.CurrentFlow = null;
        state.Context.Clear();
    }
}

Conclusión

Un router conversacional bien diseñado es esencial para chatbots robustos. Gestiona estados, enruta correctamente, maneja flujos complejos, y proporciona fallbacks apropiados. El uso de middleware permite agregar funcionalidades como logging y rate limiting de forma modular.


Palabras clave: conversational router, chatbot architecture, state management, intent routing, flow handling, middleware pattern

Share this content:

Introducción a Microsoft Semantic Kernel

22. Seguridad en Aplicaciones de IA 24. Filtrado por Relevancia Semántica en Búsquedas