- 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:
- Gestión de Estado: Recordar el contexto de la conversación
- Resolución de Intenciones: Determinar qué quiere hacer el usuario
- Enrutamiento: Dirigir a los handlers apropiados
- Comandos Globales: Manejar comandos prioritarios (menú, ayuda)
- 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: