microsoft/teams.net
Publicmirrored from https://github.com/microsoft/teams.netAvailable
core/samples/A2ABot/A2A/A2AServer.cs
85lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | using System.Text.Json; |
| 5 | using A2A; |
| 6 | using Microsoft.Extensions.AI; |
| 7 | using Microsoft.Extensions.Logging; |
| 8 | using Microsoft.Teams.Apps.Schema; |
| 9 | using Microsoft.Teams.Core; |
| 10 | using Microsoft.Teams.Core.Schema; |
| 11 | |
| 12 | namespace A2ABot.A2A; |
| 13 | |
| 14 | // Inbound A2A. Parses the DataPart into a HandoffMessage, creates a 1:1 |
| 15 | // Teams conversation with the user, asks Agent to seed that conversation's |
| 16 | // session with the handoff context + greeting, then sends the greeting as |
| 17 | // a proactive message. |
| 18 | sealed class A2AServer( |
| 19 | Config config, |
| 20 | Agent agent, |
| 21 | ConversationClient conversations, |
| 22 | ILogger<A2AServer> logger) : IAgentHandler |
| 23 | { |
| 24 | private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true }; |
| 25 | |
| 26 | public async Task ExecuteAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken ct) |
| 27 | { |
| 28 | MessageResponder responder = new(eventQueue, context.ContextId); |
| 29 | |
| 30 | Part? dataPart = context.Message?.Parts?.FirstOrDefault(p => p.ContentCase == PartContentCase.Data); |
| 31 | if (dataPart?.Data is not { } data) |
| 32 | { |
| 33 | await responder.ReplyAsync("Expected a DataPart in the message.", ct); |
| 34 | return; |
| 35 | } |
| 36 | |
| 37 | HandoffMessage? handoff = data.Deserialize<HandoffMessage>(JsonOpts); |
| 38 | logger.LogInformation( |
| 39 | "[{Bot}/A2A] received handoff: from={From} user={User} aadId={AadId} tenant={TenantId} serviceUrl={ServiceUrl}", |
| 40 | config.Name, handoff?.From, handoff?.UserName, handoff?.AadObjectId, handoff?.TenantId, handoff?.ServiceUrl); |
| 41 | |
| 42 | if (handoff is null |
| 43 | || handoff.Kind != "handoff" |
| 44 | || string.IsNullOrEmpty(handoff.AadObjectId) |
| 45 | || string.IsNullOrEmpty(handoff.TenantId) |
| 46 | || string.IsNullOrEmpty(handoff.ServiceUrl)) |
| 47 | { |
| 48 | await responder.ReplyAsync("Unsupported or incomplete handoff message.", ct); |
| 49 | return; |
| 50 | } |
| 51 | |
| 52 | Uri serviceUrl = new(handoff.ServiceUrl); |
| 53 | |
| 54 | CreateConversationResponse conv = await conversations.CreateConversationAsync( |
| 55 | new ConversationParameters |
| 56 | { |
| 57 | IsGroup = false, |
| 58 | TenantId = handoff.TenantId, |
| 59 | Members = [new TeamsConversationAccount { Id = handoff.AadObjectId }], |
| 60 | }, |
| 61 | serviceUrl, |
| 62 | cancellationToken: ct); |
| 63 | |
| 64 | string newConvId = conv.Id |
| 65 | ?? throw new InvalidOperationException("CreateConversation returned no Id."); |
| 66 | |
| 67 | // Run the LLM with the handoff context so the greeting actually |
| 68 | // answers the question that came in the summary. The LLM's turn is |
| 69 | // stored in the thread, so subsequent user replies continue naturally. |
| 70 | string greeting = await agent.GreetWithHandoffAsync( |
| 71 | newConvId, handoff.From, handoff.UserName, handoff.Summary, ct); |
| 72 | |
| 73 | TeamsActivity proactive = TeamsActivity.CreateBuilder() |
| 74 | .WithType(TeamsActivityType.Message) |
| 75 | .WithText(greeting) |
| 76 | .WithServiceUrl(serviceUrl) |
| 77 | .WithConversation(new TeamsConversation { Id = newConvId }) |
| 78 | .Build(); |
| 79 | SendActivityResponse? sent = await conversations.SendActivityAsync(proactive, cancellationToken: ct); |
| 80 | logger.LogInformation("[{Bot}/A2A] proactive greeting sent (conv={ConvId}, activityId={ActivityId})", |
| 81 | config.Name, newConvId, sent?.Id ?? "<none>"); |
| 82 | |
| 83 | await responder.ReplyAsync($"Handoff received and {handoff.UserName} contacted directly.", ct); |
| 84 | } |
| 85 | } |
| 86 | |