Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions core/samples/CustomHosting/MyTeamsBotApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@
// Licensed under the MIT License.

using Microsoft.Teams.Apps;
using Microsoft.Teams.Apps.Api.Clients;
using Microsoft.Teams.Apps.Handlers;
using Microsoft.Teams.Core;
using Microsoft.Teams.Core.Hosting;

namespace CustomHosting;

public class MyTeamsBotApp : TeamsBotApplication
{
public MyTeamsBotApp(ConversationClient conversationClient, UserTokenClient userTokenClient, ApiClient teamsApiClient, IHttpContextAccessor httpContextAccessor, ILogger<TeamsBotApplication> logger, BotApplicationOptions? options = null) : base(conversationClient, userTokenClient, teamsApiClient, httpContextAccessor, logger, options)
public MyTeamsBotApp(TeamsBotApplicationDependencies deps) : base(deps)
{
Comment thread
MehakBindra marked this conversation as resolved.
this.OnMessage(async (ctx, ct) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,45 @@

namespace ExtAIBot;

// Bot activity handlers: incoming messages, clarification-card submits, and the
// custom-feedback fetch/submit pair. Each handler ultimately funnels a user-supplied
// string back through the Agent.
internal static class TeamsBotAppHandlers
// Teams bot subclass: wires the four activity handlers (message, clarification card
// submit, custom-feedback fetch/submit) in its constructor. Each handler funnels a
// user-supplied string back through the Agent.
internal class ExtAIBotApp : TeamsBotApplication
{
public static TeamsBotApplication RegisterHandlers(this TeamsBotApplication teamsApp, Agent agent, ILogger logger)
private readonly Agent _agent;
private readonly ILogger<ExtAIBotApp> _logger;

public ExtAIBotApp(
TeamsBotApplicationDependencies dependencies,
Agent agent,
Comment thread
MehakBindra marked this conversation as resolved.
Outdated
ILogger<ExtAIBotApp> logger)
: base(dependencies)
{
_agent = agent;
_logger = logger;

// Message handler.
teamsApp.OnMessage(async (context, cancellationToken) =>
this.OnMessage(async (context, cancellationToken) =>
{
string userText = context.Activity.TextWithoutMentions ?? "";
await RespondAsync(agent, context, userText, cancellationToken);
await RespondAsync(context, userText, cancellationToken);
});

// Clarification: adaptive card action.
// Triggered when the user submits the clarification card (Action.Execute, verb "clarification").
teamsApp.OnAdaptiveCardAction(async (context, cancellationToken) =>
this.OnAdaptiveCardAction(async (context, cancellationToken) =>
{
if (context.Activity.Value?.Action?.Verb == "clarification")
{
string choice = context.Activity.Value.Action.Data?["clarificationChoice"]?.ToString() ?? "";
await RespondAsync(agent, context, choice, cancellationToken);
await RespondAsync(context, choice, cancellationToken);
}
return InvokeResponse.Ok();
});

// Feedback: message fetch task.
// Triggered when the user clicks thumbs up or thumbs down on a bot reply.
teamsApp.OnMessageFetchTask((context, cancellationToken) =>
this.OnMessageFetchTask((context, cancellationToken) =>
{
string? reaction = context.Activity.Value?.Data?.ActionValue?.Reaction;

Expand All @@ -52,28 +62,26 @@ public static TeamsBotApplication RegisterHandlers(this TeamsBotApplication team
});

// Feedback: message submit action.
teamsApp.OnMessageSubmitFeedback((context, cancellationToken) =>
this.OnMessageSubmitFeedback((context, cancellationToken) =>
{
MessageSubmitFeedbackValue? feedback = context.Activity.Value;
logger.LogInformation("Feedback received — reaction: {Reaction}, feedback: {Feedback}",
_logger.LogInformation("Feedback received — reaction: {Reaction}, feedback: {Feedback}",
feedback?.Reaction, feedback?.Feedback);
return Task.FromResult(InvokeResponse.Ok());
});

return teamsApp;
}

// Runs the agent and streams a response back. Shared between the incoming-message
// handler and the clarification-card submit handler — both flows ultimately just
// feed a user-supplied string into the agent.
private static async Task RespondAsync<TActivity>(Agent agent, Context<TActivity> context, string userText, CancellationToken cancellationToken)
private async Task RespondAsync<TActivity>(Context<TActivity> context, string userText, CancellationToken cancellationToken)
where TActivity : TeamsActivity
{
_ = context.Activity.Conversation?.Id
?? throw new InvalidOperationException("Missing conversation ID.");

TeamsStreamingWriter writer = TeamsStreamingWriter.CreateFromContext(context);
RunResult result = await agent.RunAsync(context.Activity.Conversation!.Id, userText, writer, cancellationToken);
RunResult result = await _agent.RunAsync(context.Activity.Conversation!.Id, userText, writer, cancellationToken);

IList<Entity> entities = result.Citations.BuildEntities(result.FullText);

Expand Down
16 changes: 5 additions & 11 deletions core/samples/ExtAIBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@
using Microsoft.Extensions.AI;
using Microsoft.Teams.Apps;

// Wires up the Teams bot application and delegates AI execution to Agent.
// Handler registration lives in TeamsBotAppHandlers.cs.
// Wires up the Teams bot application. Handler registration lives in ExtAIBotApp.

WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddTeamsBotApplication();
builder.Services.AddTeamsBotApplication<ExtAIBotApp>();

builder.Services.AddSingleton<IChatClient>(sp =>
{
IConfiguration config = sp.GetRequiredService<IConfiguration>();
string endpoint = config["AzureOpenAI:Endpoint"] ?? throw new InvalidOperationException("AzureOpenAI:Endpoint is required.");
string apiKey = config["AzureOpenAI:ApiKey"] ?? throw new InvalidOperationException("AzureOpenAI:ApiKey is required.");
string modelId = config["AzureOpenAI:ModelId"] ?? throw new InvalidOperationException("AzureOpenAI:ModelId is required.");
string deployment = config["AzureOpenAI:Deployment"] ?? throw new InvalidOperationException("AzureOpenAI:Deployment is required.");

return new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey))
.GetChatClient(modelId)
.GetChatClient(deployment)
.AsIChatClient()
.AsBuilder()
.UseFunctionInvocation()
Expand All @@ -34,10 +33,5 @@
builder.Services.AddSingleton<Agent>();

WebApplication webApp = builder.Build();

Agent agent = webApp.Services.GetRequiredService<Agent>();
ILogger handlerLogger = webApp.Services.GetRequiredService<ILoggerFactory>().CreateLogger("ExtAIBot.TeamsBotAppHandlers");

webApp.UseTeamsBotApplication().RegisterHandlers(agent, handlerLogger);

webApp.UseTeamsBotApplication<ExtAIBotApp>();
webApp.Run();
2 changes: 1 addition & 1 deletion core/samples/ExtAIBot/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"AzureOpenAI": {
"Endpoint": "",
"ApiKey": "",
"ModelId": ""
"Deployment": ""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public static IServiceCollection AddTeamsBotApplication<TApp>(this IServiceColle

services.AddBotApplication<TApp>(botConfig);
services.AddBotClient<ApiClient>(nameof(ApiClient), botConfig);
services.AddSingleton<TeamsBotApplicationDependencies>();
return services;
}

Expand Down
56 changes: 33 additions & 23 deletions core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,36 +81,46 @@ public OAuthFlow GetOAuthFlow(string connectionName)
/// </remarks>
public ApiClient Api { get; }

/// <param name="conversationClient">The conversation client for sending and managing activities.</param>
/// <param name="userTokenClient">The user token client for OAuth operations.</param>
/// <param name="teamsApiClient">The Teams API client for Teams-specific operations.</param>
/// <param name="httpContextAccessor">The HTTP context accessor for reading invoke responses.</param>
/// <param name="logger">The logger instance.</param>
/// <param name="options">Options containing the application (client) ID, used for logging and diagnostics. Defaults to an empty instance if not provided.</param>
/// <param name="teamsOptions">Teams-specific options including OAuth flow configuration. Defaults to an empty instance if not provided.</param>
public TeamsBotApplication(
ConversationClient conversationClient,
UserTokenClient userTokenClient,
ApiClient teamsApiClient,
IHttpContextAccessor httpContextAccessor,
ILogger<TeamsBotApplication> logger,
BotApplicationOptions? options = null,
TeamsBotApplicationOptions? teamsOptions = null)
: base(conversationClient, userTokenClient, logger, options)
/// <summary>
/// Initializes a new instance using a bundled <see cref="TeamsBotApplicationDependencies"/>.
/// Designed for subclassing: derived types declare a single-parameter constructor that
/// forwards to this overload.
/// </summary>
/// <param name="dependencies">The bundled dependencies. Cannot be null.</param>
/// <example>
/// <code>
/// public class MyBot : TeamsBotApplication
/// {
/// public MyBot(TeamsBotApplicationDependencies deps) : base(deps)
/// {
/// this.OnMessage(async (ctx, ct) =>
/// await ctx.SendActivityAsync("Hello!", ct));
/// }
/// }
/// </code>
/// </example>
public TeamsBotApplication(TeamsBotApplicationDependencies dependencies)
Comment thread
MehakBindra marked this conversation as resolved.
Outdated
: base(
(dependencies ?? throw new ArgumentNullException(nameof(dependencies))).ConversationClient,
dependencies.UserTokenClient,
dependencies.Logger,
dependencies.Options)
{
_teamsApiClient = teamsApiClient;
Api = teamsApiClient;
Logger = logger;
Router = new Router(logger);
_teamsApiClient = dependencies.TeamsApiClient;
Api = dependencies.TeamsApiClient;
Logger = dependencies.Logger;
Router = new Router(dependencies.Logger);

// Auto-register OAuth flows from DI options
if (teamsOptions is not null)
if (dependencies.TeamsOptions is not null)
{
foreach (TeamsBotApplicationOptions.OAuthFlowDescriptor descriptor in teamsOptions.OAuthFlows)
foreach (TeamsBotApplicationOptions.OAuthFlowDescriptor descriptor in dependencies.TeamsOptions.OAuthFlows)
{
this.AddOAuthFlow(descriptor.Options);
}
}

IHttpContextAccessor httpContextAccessor = dependencies.HttpContextAccessor;
ILogger<TeamsBotApplication> logger = dependencies.Logger;
OnActivity = async (activity, cancellationToken) =>
{
logger.LogDebug("OnActivity invoked for activity: Id={Id}", activity.Id);
Expand Down
32 changes: 32 additions & 0 deletions core/src/Microsoft.Teams.Apps/TeamsBotApplicationDependencies.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Teams.Apps.Api.Clients;
using Microsoft.Teams.Core;
using Microsoft.Teams.Core.Hosting;

namespace Microsoft.Teams.Apps;

/// <summary>
/// Bundles the dependencies required to construct a <see cref="TeamsBotApplication"/>.
/// Pass a single <c>TeamsBotApplicationDependencies</c> to the base constructor when subclassing,
/// so derived types do not need to thread every dependency by hand.
/// </summary>
/// <example>
/// <code>
/// public class MyBot : TeamsBotApplication
/// {
/// public MyBot(TeamsBotApplicationDependencies deps) : base(deps) { }
/// }
/// </code>
/// </example>
public sealed record TeamsBotApplicationDependencies(
ConversationClient ConversationClient,
UserTokenClient UserTokenClient,
ApiClient TeamsApiClient,
IHttpContextAccessor HttpContextAccessor,
ILogger<TeamsBotApplication> Logger,
BotApplicationOptions? Options = null,
TeamsBotApplicationOptions? TeamsOptions = null);
4 changes: 2 additions & 2 deletions core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -426,13 +426,13 @@ private static TestHarness CreateHarness(params string[] connectionNames)
mockConversationClient.Object,
mockUserTokenClient.Object);

TeamsBotApplication app = new(
TeamsBotApplication app = new(new TeamsBotApplicationDependencies(
mockConversationClient.Object,
mockUserTokenClient.Object,
apiClient,
new HttpContextAccessor(),
NullLogger<TeamsBotApplication>.Instance,
new BotApplicationOptions { AppId = "test-app-id" });
new BotApplicationOptions { AppId = "test-app-id" }));

OAuthFlow? graphFlow = null;
OAuthFlow? githubFlow = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,13 @@ private static TestHarness CreateHarness()
mockConversationClient.Object,
mockUserTokenClient.Object);

TeamsBotApplication app = new(
TeamsBotApplication app = new(new TeamsBotApplicationDependencies(
mockConversationClient.Object,
mockUserTokenClient.Object,
apiClient,
new HttpContextAccessor(),
NullLogger<TeamsBotApplication>.Instance,
new BotApplicationOptions { AppId = "test-app-id" });
new BotApplicationOptions { AppId = "test-app-id" }));

return new TestHarness
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Teams.Apps.UnitTests;

public class TeamsBotApplicationHostingExtensionsTests
{
private static ServiceProvider BuildServiceProvider(Dictionary<string, string?> configData)
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

ServiceCollection services = new();
services.AddSingleton<IConfiguration>(configuration);
services.AddLogging();
services.AddTeamsBotApplication();

return services.BuildServiceProvider();
}

[Fact]
public void AddTeamsBotApplication_RegistersTeamsBotApplicationDependencies_WithAllFieldsPopulated()
{
Dictionary<string, string?> configData = new()
{
["AzureAd:ClientId"] = "teams-bundle-client-id",
["AzureAd:TenantId"] = "teams-bundle-tenant-id"
};

ServiceProvider serviceProvider = BuildServiceProvider(configData);
TeamsBotApplicationDependencies deps = serviceProvider.GetRequiredService<TeamsBotApplicationDependencies>();
Comment thread
MehakBindra marked this conversation as resolved.
Outdated

Assert.NotNull(deps.ConversationClient);
Assert.NotNull(deps.UserTokenClient);
Assert.NotNull(deps.TeamsApiClient);
Assert.NotNull(deps.HttpContextAccessor);
Assert.NotNull(deps.Logger);
Assert.NotNull(deps.Options);
Assert.Equal("teams-bundle-client-id", deps.Options!.AppId);
Assert.NotNull(deps.TeamsOptions);
}
Comment thread
MehakBindra marked this conversation as resolved.

[Fact]
public void AddTeamsBotApplication_WithCustomSubclass_ResolvesViaBundledCtor()
{
Dictionary<string, string?> configData = new()
{
["AzureAd:ClientId"] = "subclass-client-id",
["AzureAd:TenantId"] = "subclass-tenant-id"
};

IConfigurationRoot configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

ServiceCollection services = new();
services.AddSingleton<IConfiguration>(configuration);
services.AddLogging();
services.AddTeamsBotApplication<BundleSubclassBot>();

using ServiceProvider serviceProvider = services.BuildServiceProvider();
BundleSubclassBot bot = serviceProvider.GetRequiredService<BundleSubclassBot>();

Assert.True(bot.ConstructedViaBundle);
Assert.Equal("subclass-client-id", bot.AppId);
}

private sealed class BundleSubclassBot : TeamsBotApplication
{
public bool ConstructedViaBundle { get; }

public BundleSubclassBot(TeamsBotApplicationDependencies services) : base(services)
{
ConstructedViaBundle = true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ private static TeamsBotApplication CreateApp()
mockConversationClient.Object,
mockUserTokenClient.Object);

return new TeamsBotApplication(
return new TeamsBotApplication(new TeamsBotApplicationDependencies(
mockConversationClient.Object,
mockUserTokenClient.Object,
apiClient,
new HttpContextAccessor(),
NullLogger<TeamsBotApplication>.Instance,
new BotApplicationOptions { AppId = "test-app-id" });
new BotApplicationOptions { AppId = "test-app-id" }));
}
}
Loading