[A2A协议与实现-06]如何将一个MAF Agent发布为A2A服务

[A2A协议与实现-06]如何将一个MAF Agent发布为A2A服务 这个系列之前的文章对A2A协议本身提供了系统介绍而且还提供了针对LangChain的A2A服务构建方式。借助于A2A SDKMAF针对A2A提供了很好的支持这篇文章通过几个简单的例子演示如何在一个ASP.NET Core应用构建A2A Server。1. 构建A2A服务器我们常见一个简单ASP.NET Core应用并添加针对如下NuGet包的引用。我们依然使用基于OpenAI的GPT模型所以将相关的设置写入.env文件中并利用DotNetEnv写入环境变量。DotNetEnvMicrosoft.Agents.AIMicrosoft.Agents.AI.A2AMicrosoft.Agents.AI.Hosting.A2A.AspNetCoreMicrosoft.Agents.AI.OpenAI我们定义了如下这个StartA2aServerAsync静态来完成所有的工作它的参数分别表示指定的服务器监听地址和针对A2A Server的设置。如代码所示我们创建了一个AIAgent对象和一个描述它的AgentCard。我们调用WebApplication的MapWellKnownAgentCard扩展方法注册了一个GET终结点发布AgentCard调用MapA2AJsonRpc扩展方法注册了POST终结点以JSON-PRC协议调用指定的AIAgent。在这之前我们调用了IServiceCollection的AddA2AServer和AddAIAgent扩展方法注册了依赖的服务。staticasyncTaskStartA2aServerAsync(Uriurl,ActionA2AServerRegistrationOptions?configureOptionsnull){Env.Load();varapiKeyEnvironment.GetEnvironmentVariable(OPENAI_API_KEY)!;varendpointEnvironment.GetEnvironmentVariable(OPENAI_BASE_URL)!;varagentnewOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{EndpointnewUri(endpoint)}).GetChatClient(model:gpt-5.4-mini).AsIChatClient().AsAIAgent(name:a2a-agent);varagentCardnewAgentCard{Namea2a-agent,DescriptionAgent for A2A testing.,Version1.0.0,DefaultInputModes[text,text/plain],DefaultOutputModes[text,text/plain],SupportedInterfaces[newAgentInterface{ProtocolBindingJSONRPC,ProtocolVersion2.0,Url${url}/json-rpc}],CapabilitiesnewAgentCapabilities{Streamingtrue,PushNotificationstrue},Skills[newA2A.AgentSkill{Idskill-1,Name1st skill,Descriptionthis is 1st skill for testing,Tags[t1,t2,T3]}],};varbuidlerWebApplication.CreateBuilder();buidler.Services.AddA2AServer(agent.Name,configureOptions).AddAIAgent(agent.Name,(services,anme)agent);buidler.WebHost.UseUrls(url.ToString());varappbuidler.Build();app.MapWellKnownAgentCard(agentCard);app.MapA2AJsonRpc(agent,/json-rpc);awaitapp.StartAsync();}2. 读取AgentCard信息MapWellKnownAgentCard方法调用会以标准的路径.well-known/agent-card.json来发布AgentCard的内容。所以我们可以直接请求此路径便可得到承载Agent基本信息的AgentCard。在如下的演示程序中我们调用上面定义的这个StartA2aServerAsync方法以指定的URL构建并启动A2A Server。然后直接利用创建的HttpClient对目标地址发送一个GET请求并将读取的内容反序列化成AgentCard对象。 我们最后调用WriteJsonAsync方法以格式化JSON的形式将其输出。varurlnewUri(http://localhost:3721/);awaitStartA2aServerAsync(url);usingvarhttpClientnewHttpClient{BaseAddressurl};varagentCardawaithttpClient.GetFromJsonAsyncAgentCard(.well-known/agent-card.json);WriteJsonAsync(agentCard);asyncTaskWriteJsonAsync(objectmessage){Console.WriteLine(JsonSerializer.Serialize(message,newJsonSerializerOptions{EncoderJavaScriptEncoder.UnsafeRelaxedJsonEscaping,WriteIndentedtrue,}));}输出{Name:a2a-agent,Description:Agent for A2A testing.,Version:1.0.0,DocumentationUrl:null,IconUrl:null,SupportedInterfaces:[{Url:http://localhost:3721//json-rpc,ProtocolBinding:JSONRPC,Tenant:null,ProtocolVersion:2.0}],Capabilities:{Streaming:true,PushNotifications:true,Extensions:null,ExtendedAgentCard:null},Provider:null,Skills:[{Id:skill-1,Name:1st skill,Description:this is 1st skill for testing,Tags:[t1,t2,T3],Examples:null,InputModes:null,OutputModes:null,SecurityRequirements:null}],DefaultInputModes:[text,text/plain],DefaultOutputModes:[text,text/plain],SecuritySchemes:null,SecurityRequirements:null,Signatures:null}3. 阻塞式调用AgentA2A针对Agent的调用提供了阻塞式和流式两种方式。在如下针对阻塞式调用的演示程序中我们创建了一个SendMessageRequest对象作为请求相当于是构建了一个角色为User的ChatMessage。并将其作为输入调用我们定义的InvokeAsync方法来以A2A的方式远程调用Agent。由于采用JSON-RPC协议所以每个请求都需要携带对应的Method。根据前面针对A2A协议的介绍我们知道阻塞式Agent调用采用的JSON-RPC Method是就是SendMessage。varurlnewUri(http://localhost:3721/);awaitStartA2aServerAsync(url);usingvarhttpClientnewHttpClient{BaseAddressurl};varrequestnewSendMessageRequest{MessagenewMessage{RoleRole.User,Parts[Part.FromText(给出战国四公子的名字)]}};varresponseawaitInvokeAsyncSendMessageResponse(SendMessage,request);foreach(varpartinresponse.Message.Parts){Console.Write(part.Text);}输出战国四公子是 1. **孟尝君**——田文齐国 2. **平原君**——赵胜赵国 3. **信陵君**——魏无忌魏国 4. **春申君**——黄歇楚国调用InvokeAsync方法以SendMessageResponse对象的形式返回Agent的响应。SendMessageResponse的Message属性返回一个类似于ChatMessage的Message对象正如ChatMessage利用Contents属性返回承载的内容一样这里的消息将内容置于Parts属性中。我们将每个Part的文本内容依次输出来。InvokeAsync方法定义如下泛型类型TResponse表示作为调用结果的类型。如代码所示我们将指定的参数作为Method和Params创建了作为JSON-RPC请求的JsonRpcRequest对象并将其作为输入调用HttpClient的PostAsJsonAsync方法并得到作为响应的JsonRpcResponse对象。我们最后将JsonRpcResponse的Result反序列化成TResponse对象并返回。asyncTaskTResponseInvokeAsyncTResponse(stringmethod,objectrequest){varrpcRequestnewJsonRpcRequest{Methodmethod,IdnewJsonRpcId(Guid.NewGuid().ToString()),ParamsnewJsonElement?(JsonSerializer.SerializeToElement(request,A2AJsonUtilities.DefaultOptions))};varresponseawaithttpClient.PostAsJsonAsync(json-rpc,rpcRequest);response.EnsureSuccessStatusCode();varrpcResponseawaitresponse.Content.ReadFromJsonAsyncJsonRpcResponse();returnrpcResponse.Result.DeserializeTResponse(A2AJsonUtilities.DefaultOptions);}4. 流式调用Agent由于流式调用Agent可以实时输出生成的内容在应用上的体现主要表示为逐字输出实时返回的内容能够带来更好的用户体验。在如下所示的演示程序中我们创建相同的方式创建了SendMessageRequest对象并将它作为输入调用StreamAsyncStreamResponse方法以流的形式远程调用Agent。由于流式调用对应的JSON-RPC方法名称为SendStreamingMessage我们将它作为调用StreamAsyncStreamResponse方法的第一个参数。该方法返回一个IAsyncEnumerableStreamResponse对象我们异步迭代每个StreamResponse并将它的消息携带的所有文本内容输出来。varurlnewUri(http://localhost:3721/);awaitStartA2aServerAsync(url);usingvarhttpClientnewHttpClient{BaseAddressurl};varrequestnewSendMessageRequest{MessagenewMessage{RoleRole.User,Parts[Part.FromText(给出战国四公子的名字)]}};awaitforeach(varresponseinStreamAsyncStreamResponse(SendStreamingMessage,request)){foreach(varpartinresponse.Message.Parts){Console.Write(part.Text);}}输出战国四公子是 1. **齐国孟尝君** —— 田文 2. **赵国平原君** —— 赵胜 3. **魏国信陵君** —— 魏无忌 4. **楚国春申君** —— 黄歇 如果你需要我也可以顺便给你讲他们各自的典故。如下所示的是StreamAsync方法的定义。我们采用与InvokeAsync类似的方式将原始的输入对象封装成JsonRpcRequest。由于我们构建的A2A Server会采用SSE协议实现流式输出所以整个过程就是一个采用SseParser解析每个SSE数据项并反序列化生成并输出结果的过程。asyncIAsyncEnumerableTResultStreamAsyncTResult(stringmethod,objectrequest,){varrpcRequestnewJsonRpcRequest{Methodmethod,IdnewJsonRpcId(Guid.NewGuid().ToString()),ParamsnewJsonElement?(JsonSerializer.SerializeToElement(request,A2AJsonUtilities.DefaultOptions))};usingvarcontentnewJsonRpcContent(rpcRequest);usingvarrequestMessagenewHttpRequestMessage(HttpMethod.Post,json-rpc){Contentcontent,};requestMessage.Headers.TryAddWithoutValidation(A2A-Version,1.0);requestMessage.Headers.Accept.Add(newSystem.Net.Http.Headers.MediaTypeWithQualityHeaderValue(text/event-stream));varresponseawaithttpClient.SendAsync(requestMessage,HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);response.EnsureSuccessStatusCode();varstreamawaitresponse.Content.ReadAsStreamAsync().ConfigureAwait(false);using(response)using(stream){awaitforeach(varsseIteminSseParser.Create(stream).EnumerateAsync()){varrpcResponseJsonSerializer.DeserializeJsonRpcResponse(sseItem.Data,A2AJsonUtilities.DefaultOptions);varresultrpcResponse.Result.DeserializeTResult(A2AJsonUtilities.DefaultOptions);yieldreturnresult;}}}