Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 7 additions & 3 deletions core/samples/CustomHosting/MyTeamsBotApp.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Http;
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(
ApiClient api,
IHttpContextAccessor accessor,
ILogger<MyTeamsBotApp> logger,
TeamsBotApplicationOptions? options = null)
: base(api, accessor, logger, options)
{
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
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Http;
using Microsoft.Teams.Apps;
using Microsoft.Teams.Apps.Api.Clients;
using Microsoft.Teams.Apps.Handlers;
using Microsoft.Teams.Apps.Handlers.TaskModules;
using Microsoft.Teams.Apps.Schema;
Expand All @@ -10,35 +12,47 @@

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(
Agent agent,
ApiClient api,
IHttpContextAccessor accessor,
ILogger<ExtAIBotApp> logger,
TeamsBotApplicationOptions? options = null)
: base(api, accessor, logger, options)
{
_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 +66,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": ""
}
}
24 changes: 13 additions & 11 deletions core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ namespace Microsoft.Teams.Apps.Api.Clients;
public class ApiClient
{
private readonly BotHttpClient _http;
private readonly CoreConversationClient _conversationClient;
private readonly CoreUserTokenClient _userTokenClient;

internal CoreConversationClient ConversationClient { get; }

internal CoreUserTokenClient UserTokenClient { get; }

/// <summary>
/// The service URL used by this client.
Expand Down Expand Up @@ -78,8 +80,8 @@ public ApiClient(HttpClient httpClient, CoreConversationClient conversationClien
ArgumentNullException.ThrowIfNull(userTokenClient);

_http = new BotHttpClient(httpClient, logger);
_conversationClient = conversationClient;
_userTokenClient = userTokenClient;
ConversationClient = conversationClient;
UserTokenClient = userTokenClient;
Bots = new BotClient(userTokenClient);
Users = new UserClient(userTokenClient);

Expand All @@ -106,8 +108,8 @@ public ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient c
ArgumentNullException.ThrowIfNull(userTokenClient);

_http = new BotHttpClient(httpClient, logger);
_conversationClient = conversationClient;
_userTokenClient = userTokenClient;
ConversationClient = conversationClient;
UserTokenClient = userTokenClient;
ServiceUrl = serviceUrl;
Bots = new BotClient(userTokenClient);
Conversations = new ConversationApiClient(serviceUrl, conversationClient);
Expand All @@ -125,8 +127,8 @@ public ApiClient(ApiClient client)

ServiceUrl = client.ServiceUrl;
_http = client._http;
_conversationClient = client._conversationClient;
_userTokenClient = client._userTokenClient;
ConversationClient = client.ConversationClient;
UserTokenClient = client.UserTokenClient;
Bots = client.Bots;
Conversations = client.Conversations;
Users = client.Users;
Expand All @@ -138,8 +140,8 @@ public ApiClient(ApiClient client)
private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, Uri serviceUrl)
{
_http = http;
_conversationClient = conversationClient;
_userTokenClient = userTokenClient;
ConversationClient = conversationClient;
UserTokenClient = userTokenClient;
ServiceUrl = serviceUrl;
Bots = new BotClient(userTokenClient);
Conversations = new ConversationApiClient(serviceUrl, conversationClient);
Expand All @@ -157,6 +159,6 @@ private ApiClient(BotHttpClient http, CoreConversationClient conversationClient,
public virtual ApiClient ForServiceUrl(Uri serviceUrl)
{
ArgumentNullException.ThrowIfNull(serviceUrl);
return new ApiClient(_http, _conversationClient, _userTokenClient, serviceUrl);
return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public static IServiceCollection AddTeamsBotApplication<TApp>(this IServiceColle
BotConfig botConfig = BotConfig.Resolve(services, sectionName);

// Register TeamsBotApplicationOptions
TeamsBotApplicationOptions teamsOptions = new();
TeamsBotApplicationOptions teamsOptions = new() { AppId = botConfig.ClientId };
configure?.Invoke(teamsOptions);
services.AddSingleton(teamsOptions);

Expand Down
44 changes: 29 additions & 15 deletions core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,36 +81,50 @@ 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>
/// <summary>
/// Initializes a new <see cref="TeamsBotApplication"/>.
/// </summary>
/// <param name="teamsApiClient">The Teams API facade. Also carries the underlying Core conversation and user-token clients.</param>
/// <param name="httpContextAccessor">Accessor used to write invoke responses back to the current HTTP request.</param>
/// <param name="logger">Logger used by the bot and exposed as <see cref="Context{TActivity}.Log"/>.</param>
/// <param name="options">Optional Teams bot options (AppId, OAuth flows, etc.).</param>
/// <example>
/// <code>
/// public class MyBot : TeamsBotApplication
/// {
/// public MyBot(ApiClient api, IHttpContextAccessor accessor, ILogger&lt;MyBot&gt; logger, TeamsBotApplicationOptions? options = null)
/// : base(api, accessor, logger, options)
/// {
/// this.OnMessage(async (ctx, ct) =>
/// await ctx.SendActivityAsync("Hello!", ct));
/// }
/// }
/// </code>
/// </example>
public TeamsBotApplication(
ConversationClient conversationClient,
UserTokenClient userTokenClient,
ApiClient teamsApiClient,
IHttpContextAccessor httpContextAccessor,
ILogger<TeamsBotApplication> logger,
BotApplicationOptions? options = null,
TeamsBotApplicationOptions? teamsOptions = null)
: base(conversationClient, userTokenClient, logger, options)
TeamsBotApplicationOptions? options = null)
Comment thread
MehakBindra marked this conversation as resolved.
: base(
(teamsApiClient ?? throw new ArgumentNullException(nameof(teamsApiClient))).ConversationClient,
teamsApiClient.UserTokenClient,
logger,
options)
Comment thread
MehakBindra marked this conversation as resolved.
{
_teamsApiClient = teamsApiClient;
Api = teamsApiClient;
Logger = logger;
Router = new Router(logger);
Comment thread
MehakBindra marked this conversation as resolved.

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

OnActivity = async (activity, cancellationToken) =>
{
logger.LogDebug("OnActivity invoked for activity: Id={Id}", activity.Id);
Expand Down
4 changes: 3 additions & 1 deletion core/src/Microsoft.Teams.Apps/TeamsBotApplicationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// Licensed under the MIT License.

using Microsoft.Teams.Apps.OAuth;
using Microsoft.Teams.Core.Hosting;

namespace Microsoft.Teams.Apps;

/// <summary>
/// Options for configuring a <see cref="TeamsBotApplication"/>.
/// Inherits <see cref="BotApplicationOptions"/> so a single options object covers both Core and Teams settings.
/// </summary>
public sealed class TeamsBotApplicationOptions
public sealed class TeamsBotApplicationOptions : BotApplicationOptions
{
internal List<OAuthFlowDescriptor> OAuthFlows { get; } = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Microsoft.Teams.Core.Hosting;
/// <summary>
/// Options for configuring a bot application instance.
/// </summary>
public sealed class BotApplicationOptions
public class BotApplicationOptions
{
/// <summary>
/// Gets or sets the application (client) ID, used for logging and diagnostics.
Expand Down
Loading
Loading