En publicaciones anteriores, discutimos cómo empaquetar servicios MCP como Herramientas (AIFunction) que un Agente puede invocar. El mismo principio se aplica a los Agentes A2A.
La ventaja de este enfoque es que permite a los Agentes dentro de MAF interactuar con Agentes A2A remotos o servidores MCP como si estuvieran llamando a funciones locales.
El siguiente fragmento de código ilustra cómo convertir una Tarjeta A2A en un Agente dentro de MAF, y luego ese Agente se convierte en una AIFunction:
var functionTools = new List<AIFunction>();
foreach (var endpoint in agentEndpoints)
{
var resolver = new A2ACardResolver(new Uri(endpoint));
var card = await resolver.GetAgentCardAsync();
var agent = card.AsAIAgent(); // Convierte el Agente A2A en una instancia de AIAgent
functionTools.AddRange(AgentFunctionHelper.CreateFunctionTools(agent, card));
}
A continuación se muestra la implementación de la clase AgentFunctionHelper:
public static class AgentFunctionHelper
{
public static IEnumerable<AIFunction> CreateFunctionTools(AIAgent a2aAgent, AgentCard agentCard)
{
foreach (var skill in agentCard.Skills)
{
var options = new AIFunctionFactoryOptions()
{
Name = Sanitize(skill.Id),
Description = $$"""
{
"description": "{{skill.Description}}",
"tags": "[{{string.Join(", ", skill.Tags ?? new string[0])}}]",
"examples": "[{{string.Join(", ", skill.Examples ?? new string[0])}}]",
"inputModes": "[{{string.Join(", ", skill.InputModes ?? new string[0])}}]",
"outputModes": "[{{string.Join(", ", skill.OutputModes ?? new string[0])}}]"
}
""",
};
yield return AIFunctionFactory.Create(RunAgentAsync, options);
}
async Task<string> RunAgentAsync(string input, CancellationToken cancellationToken)
{
var response = await a2aAgent.RunAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
return response.Text;
}
}
private static readonly Regex InvalidNameCharsRegex = new Regex("[^0-9A-Za-z]+", RegexOptions.Compiled);
public static string Sanitize(string name)
{
return InvalidNameCharsRegex.Replace(name, "_");
}
}
El método CreateFunctionTools se encarga de transformar todas las habilidades expuestas de un Agente A2A en herramientas AIFunction. El método Sanitize normaliza los nombres de las funciones, ya que los nombress de AIFunction deben cumplir con un formato específico (solo caracteres alfanuméricos y guiones bajos). Por lo tanto, es necesario normalizar los nombres de las habilidades activamente.
Ejemplo de Integración Completa
Escenario
Continuando con el ejemplo de un asistente de viajes de artículos anteriores, integraremos varios Agentes a través del protocolo A2A. Crearemos cuatro proyectos .NET:
- 1 proyecto de consola .NET: El asistente principal.
- 3 proyectos web ASP.NET: Agente de clima, Agente de hotel, Agente de planificación de rutas.
El asistente principal podrá responder a las consultas de los usuarios sobre diversos temas (atracciones, hoteles, clima). Seleccionará de forma autónoma uno o varios Agentes A2A para recopilar la información necesaria, la integrará y optimizará antes de responder al usuario.
Agente de Clima
Primero, agregue el siguiente paquete NuGet. Todos los proyectos de Agente A2A requerirán este paquete:
A2A.AspNetCore
Cree una clase WeatherAgent para definir sus capacidades y AgentCard, exponiendo una habilidad de consulta de clima:
public class WeatherAgent
{
public void Attach(ITaskManager taskManager)
{
taskManager.OnMessageReceived = QueryWeatherAsync;
taskManager.OnAgentCardQuery = GetAgentCardAsync;
}
private Task<A2AResponse> QueryWeatherAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<A2AResponse>(cancellationToken);
}
var messageText = messageSendParams.Message.Parts.OfType<TextPart>().First().Text;
var message = new AgentMessage()
{
Role = MessageRole.Agent,
MessageId = Guid.NewGuid().ToString(),
ContextId = messageSendParams.Message.ContextId,
Parts = [new TextPart() {
Text = $"""
🌤️ **Resultado de la consulta del clima**
Hora de consulta: {DateTime.Now:yyyy-MM-dd HH:mm}
**Clima en Beijing**
- Hoy: Soleado a parcialmente nublado, temperatura -2°C ~ 8°C
- Mañana: Parcialmente nublado, temperatura 0°C ~ 10°C
- Pasado mañana: Nublado, temperatura 2°C ~ 9°C
**Clima en Shanghai**
- Hoy: Parcialmente nublado, temperatura 5°C ~ 12°C
- Mañana: Llovizna ligera, temperatura 6°C ~ 10°C
- Pasado mañana: Nublado a soleado, temperatura 4°C ~ 11°C
👔 Sugerencia de vestimenta: Beijing está frío, se recomienda ropa de abrigo; Shanghai es templado, se recomienda una chaqueta y llevar paraguas.
"""
}]
};
return Task.FromResult<A2AResponse>(message);
}
private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<AgentCard>(cancellationToken);
}
var capabilities = new AgentCapabilities()
{
Streaming = true,
PushNotifications = false,
};
return Task.FromResult(new AgentCard()
{
Name = "weather agent",
Description = "Agente de información meteorológica",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [
new AgentSkill
{
Id = "weather-query",
Name = "Consulta de clima",
Description = "Consulta el pronóstico del tiempo para una ciudad específica, incluyendo temperatura, probabilidad de precipitación, sugerencias de vestimenta, etc.",
Tags = ["weather", "forecast", "climate"],
Examples = ["¿Cómo estará el clima en Shanghai mañana?", "Pronóstico del tiempo para Chengdu esta semana", "¿Lloverá en Hangzhou?"],
InputModes = ["text"],
OutputModes = ["text"]
}],
});
}
}
Nota: Para simplificar, este ejemplo devuelve información meteorológica fija. En aplicaciones reales, se implementaría lógica de negocio o se integraría con modelos de lenguaje grandes.
Registre el agente en Program.cs y configure el mapeo de puertos:
using A2A;
using A2A.AspNetCore;
using WeatherAgentServer; // Asumiendo que WeatherAgent está en este namespace
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var taskManager = new TaskManager();
var agent = new WeatherAgent();
agent.Attach(taskManager);
app.MapA2A(taskManager, "/weather"); // Endpoint JSON-RPC para A2A
app.MapWellKnownAgentCard(taskManager, "/weather"); // Endpoint Agent Card conocido para A2A
app.MapHttpA2A(taskManager, "/weather"); // Endpoint HTTP para A2A
app.Run();
Agente de Hotel
Cree una clase HotelAgent para definirr sus capacidades y AgentCard:
public class HotelAgent
{
public void Attach(ITaskManager taskManager)
{
taskManager.OnMessageReceived = QueryHotelsAsync;
taskManager.OnAgentCardQuery = GetAgentCardAsync;
}
private Task<A2AResponse> QueryHotelsAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<A2AResponse>(cancellationToken);
}
var messageText = messageSendParams.Message.Parts.OfType<TextPart>().First().Text;
var message = new AgentMessage()
{
Role = MessageRole.Agent,
MessageId = Guid.NewGuid().ToString(),
ContextId = messageSendParams.Message.ContextId,
Parts = [new TextPart() {
Text = $"""
🏨 **Recomendaciones de hoteles**
Según sus requisitos, le recomendamos los siguientes hoteles:
**Lujo ⭐⭐⭐⭐⭐**
1. Waldorf Astoria Shanghai on the Bund
- 📍 Ubicación principal en el Bund, habitaciones con vista al río
- 💰 Desde ¥2,500/noche
- ⭐ Puntuación 4.9/5.0
**Confort ⭐⭐⭐⭐**
2. Shangri-La Hotel, Shanghai Jing An
- 📍 Zona comercial de Jing'an Temple, acceso conveniente
- 💰 Desde ¥1,200/noche
- ⭐ Puntuación 4.7/5.0
**Económico ⭐⭐⭐**
3. Hanting Hotel (Nanjing Road Store)
- 📍 Cerca de la calle peatonal Nanjing Road
- 💰 Desde ¥380/noche
- ⭐ Puntuación 4.5/5.0
💡 Consejo: Se recomienda reservar con antelación, los precios pueden aumentar un 20-50% los fines de semana y festivos.
"""
}]
};
return Task.FromResult<A2AResponse>(message);
}
private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<AgentCard>(cancellationToken);
}
var capabilities = new AgentCapabilities()
{
Streaming = true,
PushNotifications = false,
};
return Task.FromResult(new AgentCard()
{
Name = "hotel-a2a-agent",
Description = "Agente de información hotelera",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [
new AgentSkill
{
Id = "hotel-recommendation",
Name = "Recomendación de hotel",
Description = "Recomienda hoteles adecuados según el destino y el presupuesto, incluyendo categorías de lujo, confort y económico.",
Tags = ["hotel", "accommodation", "booking", "travel"],
Examples = ["Recomiéndame hoteles en Shanghai", "¿Hay buenos hoteles cerca del Bund en Shanghai?", "Hoteles en Beijing por menos de 500 RMB"],
InputModes = ["text"],
OutputModes = ["text"]
}
],
});
}
}
Consulte el agente de clima para el registro en Program.cs.
Agente de Planificación
Cree una clase PlanAgent para definir sus capacidades y AgentCard:
public class PlanAgent
{
public void Attach(ITaskManager taskManager)
{
taskManager.OnMessageReceived = QueryPlansAsync;
taskManager.OnAgentCardQuery = GetAgentCardAsync;
}
private Task<A2AResponse> QueryPlansAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<A2AResponse>(cancellationToken);
}
var messageText = messageSendParams.Message.Parts.OfType<TextPart>().First().Text;
var message = new AgentMessage()
{
Role = MessageRole.Agent,
MessageId = Guid.NewGuid().ToString(),
ContextId = messageSendParams.Message.ContextId,
Parts = [new TextPart() {
Text = $"""
🎡 **Recomendaciones de atracciones turísticas**
Le recomendamos las atracciones imprescindibles de Shanghai:
**Clásicas y Culturales**
1. 🏛️ The Bund - Admire la arquitectura de estilo internacional
2. 🏯 Yu Garden - Un representante de los jardines clásicos de Jiangnan
3. 🕌 Chenghuang Miao - Pruebe la auténtica cocina de Shanghai
**Urbanas Modernas**
4. 🗼 Oriental Pearl TV Tower - El hito de Shanghai, con vistas panorámicas del río Huangpu
5. 🌆 Lujiazui - El centro financiero, la cima de Shanghai
6. 🛍️ Nanjing Road Pedestrian Street - Un paraíso de las compras
**Artísticas y de Ocio**
7. 🎨 Tian Zi Fang - Concentración de tiendas artísticas
8. 📚 Wukang Road - Estilo francés bajo los plátanos
9. 🌳 Century Park - El pulmón verde de la ciudad, ideal para familias
📅 Tiempo de visita sugerido: 3-4 días para cubrir las atracciones principales.
"""
}]
};
return Task.FromResult<A2AResponse>(message);
}
private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<AgentCard>(cancellationToken);
}
var capabilities = new AgentCapabilities()
{
Streaming = true,
PushNotifications = false,
};
return Task.FromResult(new AgentCard()
{
Name = "plan agent",
Description = "Agente de planes de viaje y atracciones turísticas",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [
new AgentSkill
{
Id = "attraction-recommendation",
Name = "Recomendación de atracciones",
Description = "Recomienda atracciones populares y rutas de viaje para un destino, incluyendo categorías como clásicas, modernas y artísticas.",
Tags = ["attraction", "sightseeing", "tourism", "travel"],
Examples = ["¿Qué hay de interesante en Shanghai?", "Atracciones imprescindibles en Beijing", "¿Cómo visitar el Lago del Oeste en Hangzhou?"],
InputModes = ["text"],
OutputModes = ["text"]
}
],
});
}
}
Consulte el agente de clima para el registro en Program.cs.
Asistente Principal
En el proyecto del asistente principal (TravelPlannerClient), instale los siguientes paquetes NuGet:
Microsoft.Extensions.AI.OpenAI
Microsoft.Agents.AI.A2A
Microsoft.Agents.AI.Abstractions
Microsoft.Extensions.AI.Abstractions
Cree un cliente de chat para el asistente principal:
var chatClient = new OpenAIClient(
new ApiKeyCredential(openAIProvider.ApiKey),
new OpenAIClientOptions { Endpoint = new Uri(openAIProvider.Endpoint) })
.GetChatClient(openAIProvider.ModelId)
.AsIChatClient();
Convierta los agentes A2A remotos en herramientas AIFunction:
var agentEndpoints = new[]
{
"https://localhost:7021/weather", // Agente de hotel
"https://localhost:7011/hotel", // Agente de clima
"https://localhost:7031/plan" // Agente de planificación
};
var functionTools = new List<AIFunction>();
foreach (var endpoint in agentEndpoints)
{
var resolver = new A2ACardResolver(new Uri(endpoint));
var card = await resolver.GetAgentCardAsync();
var agent = card.AsAIAgent(); // Convierte el Agente A2A en una instancia de AIAgent
functionTools.AddRange(AgentFunctionHelper.CreateFunctionTools(agent, card));
}
Cree un agente principal que pueda invocar los agentes A2A. Este es el paso central:
var mainAgent = new ChatClientAgent(
chatClient: chatClient,
instructions: @"Eres un asistente inteligente de planificación de viajes. Puedes utilizar las herramientas disponibles para ayudar a los usuarios a completar sus tareas.
Cuando un usuario te pregunte, utiliza la herramienta adecuada para obtener información y luego responde al usuario.",
tools: [.. functionTools]
);
Finalmente, pruebe el asistente:
var userRequests = new[]
{
"Consulta el clima en Shanghai.",
"Recomiéndame hoteles en Shanghai.",
"Planifica un itinerario de un día en Shanghai y dime cómo debo vestirme.",
};
foreach (var userRequest in userRequests)
{
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"👤 Solicitud del usuario: {userRequest}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("⏱️ Procesando por el Agente Principal...");
var response = await mainAgent.RunAsync(userRequest);
Console.WriteLine($"💬 Respuesta:\n{response.Text}");
Console.WriteLine();
}
Resultados de la Prueba
Caso 1: Consulta del clima en Shanghai (Tarea Simple)
El asistente principal invoca al agente de clima para obtener información meteorológica y responde adecuadamente.
Caso 2: Recomendación de hoteles en Shanghai (Tarea Simple)
El asistente principle invoca al agente de hotel para obtener recomendaciones y responde.
Caso 3: Plan de viaje y recomendación de vestimenta (Tarea Compleja)
El asistente principal orquesta llamadas tanto al agente de planificación como al agente de clima para recopilar información diversa y generar una respuesta integral.