# Teams SDK - C# Documentation (Complete) > Microsoft Teams SDK - A comprehensive framework for building AI-powered Teams applications using C#. Using this SDK, you can easily build and integrate a variety of features in Microsoft Teams by building Agents or Tools. The documentation here helps by giving background information and code samples on how best to do this. IMPORTANT THINGS TO REMEMBER: - This SDK is NOT based off of BotFramework (which the _previous_ version of the Teams SDK was based on). This SDK is a completely new framework. Use this guide to find snippets to drive your decisions. - When scaffolding new applications, using the CLI is a lot simpler and preferred than doing it all by yourself. See the Quickstart guide for that. - It's a good idea to build the application and fix compile time errors to help ensure the app works as expected. - It is helpful to inspect NuGet packages folder to get exact types for a given namesapce YOU MUST FOLLOW THE ABOVE GUIDANCE. ## Main Documentation ### Action commands # Action commands Action commands allow you to present your users with a modal pop-up called a dialog in Teams. The dialog collects or displays information, processes the interaction, and sends the information back to Teams compose box. ## Action command invocation locations There are three different areas action commands can be invoked from: 1. Compose Area 2. Compose Box 3. Message ### Compose Area and Box ![Screenshot of Teams with outlines around the 'Compose Box' (for typing messages) and the 'Compose Area' (the menu option next to the compose box that provides a search bar for actions and apps).](/screenshots/compose-area.png) ### Message action command ![Screenshot of message extension response in Teams. By selecting the '...' button, a menu has opened with 'More actions' option in which they can select from a list of available message extension actions.](/screenshots/message.png) :::tip See the [Invoke Locations](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command?tabs=Teams-toolkit%2Cdotnet#select-action-command-invoke-locations) guide to learn more about the different entry points for action commands. ::: ## Setting up your Teams app manifest To use action commands you have define them in the Teams app manifest. Here is an example: ```json "composeExtensions": [ { "botId": "${{BOT_ID}}", "commands": [ { "id": "createCard", "type": "action", "context": [ "compose", "commandBox" ], "description": "Command to run action to create a card from the compose box.", "title": "Create Card", "parameters": [ { "name": "title", "title": "Card title", "description": "Title for the card", "inputType": "text" }, { "name": "subTitle", "title": "Subtitle", "description": "Subtitle for the card", "inputType": "text" }, { "name": "text", "title": "Text", "description": "Text for the card", "inputType": "textarea" } ] }, { "id": "getMessageDetails", "type": "action", "context": [ "message" ], "description": "Command to run action on message context.", "title": "Get Message Details" }, { "id": "fetchConversationMembers", "description": "Fetch the conversation members", "title": "Fetch Conversation Members", "type": "action", "fetchTask": true, "context": [ "compose" ] }, ] } ] ``` Here we have defining three different commands: 1. `createCard` - that can be invoked from either the `compose` or `commandBox` areas. Upon invocation a dialog will popup asking the user to fill the `title`, `subTitle`, and `text`. ![Screenshot of a message extension dialog with the editable fields 'Card title', 'Subtitle', and 'Text'.](/screenshots/parameters.png) 2. `getMessageDetails` - It is invoked from the `message` overflow menu. Upon invocation the message payload will be sent to the app which will then return the details like `createdDate`, etc. ![Screenshot of the 'More actions' message extension menu expanded with 'Get Message Details' option selected.](/screenshots/message-command.png) 3. `fetchConversationMembers` - It is invoked from the `compose` area. Upon invocation the app will return an adaptive card in the form of a dialog with the conversation roster. ![Screenshot of the 'Fetch Conversation Members' option exposed from the message extension menu '...' option.](/screenshots/fetch-conversation-members.png) ## Handle submission Handle submission when the `createCard` or `getMessageDetails` actions commands are invoked. ```csharp using System.Text.Json; using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; //... [MessageExtension.SubmitAction] public Response OnMessageExtensionSubmit( [Context] SubmitActionActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[MESSAGE_EXT_SUBMIT] Action submit received"); var commandId = activity.Value?.CommandId; var data = activity.Value?.Data as JsonElement?; log.Info($"[MESSAGE_EXT_SUBMIT] Command: {commandId}"); log.Info($"[MESSAGE_EXT_SUBMIT] Data: {JsonSerializer.Serialize(data)}"); switch (commandId) { case "createCard": return HandleCreateCard(data, log); case "getMessageDetails": return HandleGetMessageDetails(activity, log); default: log.Error($"[MESSAGE_EXT_SUBMIT] Unknown command: {commandId}"); return CreateErrorActionResponse("Unknown command"); } } ``` ### Create card `HandleCreateCard()` method ```csharp using System.Text.Json; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Cards; using Microsoft.Teams.Common; //... private static Response HandleCreateCard(JsonElement? data, ILogger log) { var title = GetJsonValue(data, "title") ?? "Default Title"; var description = GetJsonValue(data, "description") ?? "Default Description"; log.Info($"[CREATE_CARD] Title: {title}, Description: {description}"); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Custom Card Created") { Weight = TextWeight.Bolder, Size = TextSize.Large, Color = TextColor.Good }, new TextBlock(title) { Weight = TextWeight.Bolder, Size = TextSize.Medium }, new TextBlock(description) { Wrap = true, IsSubtle = true } } }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = ContentType.AdaptiveCard, Content = card }; return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List { attachment } } }; } ``` ### Create message details card `HandleGetMessageDetails()` method ```csharp using Microsoft.Teams.Api; using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Cards; //... private static Response HandleGetMessageDetails(SubmitActionActivity activity, ILogger log) { var messageText = activity.Value?.MessagePayload?.Body?.Content ?? "No message content"; var messageId = activity.Value?.MessagePayload?.Id ?? "Unknown"; log.Info($"[GET_MESSAGE_DETAILS] Message ID: {messageId}"); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Message Details") { Weight = TextWeight.Bolder, Size = TextSize.Large, Color = TextColor.Accent }, new TextBlock($"Message ID: {messageId}") { Wrap = true }, new TextBlock($"Content: {messageText}") { Wrap = true } } }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = card }; return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List { attachment } } }; } ``` ## Handle opening adaptive card dialog Handle opening adaptive card dialog when the `fetchConversationMembers` command is invoked. ```csharp using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; //... [MessageExtension.FetchTask] public async Task OnMessageExtensionFetchTask( [Context] FetchTaskActivity activity, [Context] ILogger log) { log.Info("[MESSAGE_EXT_FETCH_TASK] Fetch task received"); var commandId = activity.Value?.CommandId; log.Info($"[MESSAGE_EXT_FETCH_TASK] Command: {commandId}"); return CreateFetchTaskResponse(commandId, log); } ``` ### Create conversation members card `CreateFetchTaskResponse()` method ```csharp using Microsoft.Teams.Api; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Cards; using Microsoft.Teams.Common; //... private static ActionResponse CreateFetchTaskResponse(string? commandId, ILogger log) { log.Info($"[CREATE_FETCH_TASK] Creating task for command: {commandId}"); // Create an adaptive card for the task module var card = new AdaptiveCard { Body = new List { new TextBlock("Conversation Members is not implemented in C# yet :(") { Weight = TextWeight.Bolder, Color = TextColor.Accent }, } }; return new ActionResponse { Task = new ContinueTask(new TaskInfo { Title = "Fetch Task Dialog", Height = new Union(Size.Small), Width = new Union(Size.Small), Card = new Microsoft.Teams.Api.Attachment(card) }) }; } // Helper method to extract JSON values private static string? GetJsonValue(JsonElement? data, string key) { if (data?.ValueKind == JsonValueKind.Object && data.Value.TryGetProperty(key, out var value)) { return value.GetString(); } return null; } // Helper method to create error responses private static Response CreateErrorActionResponse(string message) { return new Response { ComposeExtension = new Result { Type = ResultType.Message, Text = message } }; } ``` ## Resources - [Action commands](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/define-action-command?tabs=Teams-toolkit%2Cdotnet) - [Returning Adaptive Card Previews in Task Modules](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/action-commands/respond-to-task-module-submit?tabs=dotnet%2Cdotnet-1#bot-response-with-adaptive-card) --- ### Adaptive Cards # Adaptive Cards Overview of Adaptive Cards in C# Teams SDK for building rich, interactive user experiences in Teams applications. Adaptive Cards provide a flexible, cross-platform content format for creating rich, interactive experiences. They consist of a customizable body of card elements combined with optional action sets, all fully serializable for delivery to clients. Through a powerful combination of text, graphics, and interactive buttons, Adaptive Cards enable compelling user experiences across various platforms. The Adaptive Card framework is widely implemented throughout Microsoft's ecosystem, with significant integration in Microsoft Teams. Within Teams, Adaptive Cards power numerous key scenarios including: - Rich interactive messages - Dialogs - Message Extensions - Link Unfurling - Configuration forms - And many more application contexts Mastering Adaptive Cards is essential for creating sophisticated, engaging experiences that leverage the full capabilities of the Teams platform. This guide will help you learn how to use them in this SDK. For a more comprehensive guide on Adaptive Cards, see the [official documentation](https://adaptivecards.microsoft.com/). --- ### App Basics # App Basics The `App` class is the main entry point for your agent. It is responsible for: 1. Hosting and running the server (via plugins) 2. Serving incoming requests and routing them to your handlers 3. Handling authentication for your agent to the Teams backend 4. Providing helpful utilities which simplify the ability for your application to interact with the Teams platform 5. Managing plugins which can extend the functionality of your agent ```mermaid flowchart LR %% Layout Definitions direction LR Teams subgraph AppClass CorePlugins["Plugins"] Events["Events"] subgraph AppResponsibilities direction TB ActivityRouting["Activity Routing"] Utilities["Utilities"] Auth["Auth"] end Plugins2["Plugins"] end ApplicationLogic["Application Logic"] %% Connections Teams --> CorePlugins CorePlugins --> Events Events --> ActivityRouting ActivityRouting --> Plugins2 Plugins2 --> ApplicationLogic Auth --> ApplicationLogic Utilities --> ApplicationLogic %% Styling style Teams fill:#2E86AB,stroke:#1B4F72,stroke-width:2px,color:#ffffff style ApplicationLogic fill:#28B463,stroke:#1D8348,stroke-width:2px,color:#ffffff ``` ## Core Components **Plugins** - Can be used to set up the server - Can listen to messages or send messages out **Events** - Listens to events from core plugins - Emit interesting events to the application **Activity Routing** - Routes activities to appropriate handlers **Utilities** - Provides utility functions for convenience (like sending replies or proactive messages) **Auth** - Handles authenticating your agent with Teams, Graph, etc. - Simplifies the process of authenticating your app or user for your app **Plugins (Secondary)** - Can hook into activity handlers or proactive scenarios - Can modify or update agent activity events ## Plugins You'll notice that plugins are present in the front, which exposes your application as a server, and also in the back after the app does some processing to the incoming message. The plugin architecture allows the application to be built in an extremely modular way. Each plugin can be swapped out to change or augment the functionality of the application. The plugins can listen to various events that happen (e.g. the server starting or ending, an error occuring, etc), activities being sent to or from the application and more. This allows the application to be extremely flexible and extensible. --- ### Building Adaptive Cards # Building Adaptive Cards Adaptive Cards are JSON payloads that describe rich, interactive UI fragments. With `Microsoft.Teams.Cards` you can build these cards entirely in C# while enjoying full IntelliSense and compiler safety. ## The Builder Pattern `Microsoft.Teams.Cards` exposes small **builder helpers** including `AdaptiveCard`, `TextBlock`, `ToggleInput`, `ExecuteAction`, _etc._ Each helper wraps raw JSON and provides fluent, chainable methods that keep your code concise and readable. ```csharp using Microsoft.Teams.Cards; var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Hello world") { Wrap = true, Weight = TextWeight.Bolder }, new ToggleInput("Notify me") { Id = "notify" } }, Actions = new List { new ExecuteAction { Title = "Submit", Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "submit_basic" } } }), AssociatedInputs = AssociatedInputs.Auto } } }; ``` Benefits: | Benefit | Description | | ----------- | ----------------------------------------------------------------------------- | | Readability | No deep JSON trees—just chain simple methods. | | Re‑use | Extract snippets to functions or classes and share across cards. | | Safety | Builders validate every property against the Adaptive Card schema (see next). | :::info The builder helpers use strongly-typed interfaces. Use IntelliSense (Ctrl+Space) or "Go to Definition" (F12) in your IDE to explore available types and properties. Source code lives in the `Microsoft.Teams.Cards` namespace. ::: ## Type‑safe Authoring & IntelliSense The package bundles the **Adaptive Card v1.5 schema** as strict C# types. While coding you get: - **Autocomplete** for every element and attribute. - **In‑editor validation**—invalid enum values or missing required properties produce build errors. - Automatic upgrades when the schema evolves; simply update the package. ```csharp // "Huge" is not a valid size for TextBlock - this will cause a compilation error var textBlock = new TextBlock("Test") { Wrap = true, Weight = TextWeight.Bolder, Size = "Huge" // This is invalid - should be TextSize enum }; ``` ## The Visual Designer Prefer a drag‑and‑drop approach? Use [Microsoft's Adaptive Card Designer](https://adaptivecards.microsoft.com/designer.html): 1. Add elements visually until the card looks right. 2. Copy the JSON payload from the editor pane. 3. Paste the JSON into your project **or** convert it to builder calls: ```csharp var cardJson = """ { "type": "AdaptiveCard", "body": [ { "type": "ColumnSet", "columns": [ { "type": "Column", "verticalContentAlignment": "center", "items": [ { "type": "Image", "style": "Person", "url": "https://aka.ms/AAp9xo4", "size": "Small", "altText": "Portrait of David Claux" } ], "width": "auto" }, { "type": "Column", "spacing": "medium", "verticalContentAlignment": "center", "items": [ { "type": "TextBlock", "weight": "Bolder", "text": "David Claux", "wrap": true } ], "width": "auto" }, { "type": "Column", "spacing": "medium", "verticalContentAlignment": "center", "items": [ { "type": "TextBlock", "text": "Principal Platform Architect at Microsoft", "isSubtle": true, "wrap": true } ], "width": "stretch" } ] } ], "version": "1.5", "schema": "http://adaptivecards.io/schemas/adaptive-card.json" } """; // Deserialize the JSON into an AdaptiveCard object var card = AdaptiveCard.Deserialize(cardJson); // Send the card await client.Send(card); ``` This method leverages the full Adaptive Card schema and ensures that the payload adheres strictly to `AdaptiveCard`. :::tip You can use a combination of raw JSON and builder helpers depending on whatever you find easier. ::: ## End‑to‑end Example – Task Form Card Below is a complete example showing a task management form. ```csharp teams.OnMessage(async (context, cancellationToken) => { var text = context.Activity.Text?.ToLowerInvariant() ?? ""; if (text.Contains("form")) { await context.Typing(cancellationToken); var card = CreateTaskFormCard(); await context.Send(card, cancellationToken); } }); ``` The definition for `CreateTaskFormCard` is as follows ```csharp private static AdaptiveCard CreateTaskFormCard() { return new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Create New Task") { Weight = TextWeight.Bolder, Size = TextSize.Large }, new TextInput { Id = "title", Label = "Task Title", Placeholder = "Enter task title" }, new TextInput { Id = "description", Label = "Description", Placeholder = "Enter task details", IsMultiline = true }, new ChoiceSetInput { Id = "priority", Label = "Priority", Value = "medium", Choices = new List { new() { Title = "High", Value = "high" }, new() { Title = "Medium", Value = "medium" }, new() { Title = "Low", Value = "low" } } }, new DateInput { Id = "due_date", Label = "Due Date", Value = DateTime.Now.ToString("yyyy-MM-dd") } }, Actions = new List { new ExecuteAction { Title = "Create Task", Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "create_task" } } }), AssociatedInputs = AssociatedInputs.Auto, Style = ActionStyle.Positive } } }; } ``` ## Additional Resources - [**Official Adaptive Card Documentation**](https://adaptivecards.microsoft.com/) - [**Adaptive Cards Designer**](https://adaptivecards.microsoft.com/designer.html) ### Summary - Use **builder helpers** for readable, maintainable card code. - Enjoy **full type safety** and IDE assistance. - Prototype quickly in the **visual designer** and refine with builders. Happy card building! 🎉 --- ### Creating Dialogs # Creating Dialogs :::tip If you're not familiar with how to build Adaptive Cards, check out [the cards guide](../adaptive-cards). Understanding their basics is a prerequisite for this guide. ::: ## Entry Point To open a dialog, you need to supply a special type of action to the Adaptive Card. The `TaskFetchAction` is specifically designed for this purpose - it automatically sets up the proper Teams data structure to trigger a dialog. Once this button is clicked, the dialog will open and ask the application what to show. ```csharp using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Cards; using Microsoft.Teams.Common.Logging; //... [Message] public async Task OnMessage([Context] MessageActivity activity, [Context] IContext.Client client, [Context] ILogger log) { // Create the launcher adaptive card var card = CreateDialogLauncherCard(); await client.Send(card); } private static AdaptiveCard CreateDialogLauncherCard() { var card = new AdaptiveCard { Body = new List { new TextBlock("Select the examples you want to see!") { Size = TextSize.Large, Weight = TextWeight.Bolder } }, Actions = new List { new TaskFetchAction(new { opendialogtype = "simple_form" }) { Title = "Simple form test" }, new TaskFetchAction(new { opendialogtype = "webpage_dialog" }) { Title = "Webpage Dialog" }, new TaskFetchAction(new { opendialogtype = "multi_step_form" }) { Title = "Multi-step Form" } } }; return card; } ``` ## Handling Dialog Open Events Once an action is executed to open a dialog, the Teams client will send an event to the agent to request what the content of the dialog should be. When using `TaskFetchAction`, the data is nested inside an `MsTeams` property structure. ```csharp using System.Text.Json; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Common.Logging; //... [TaskFetch] public Microsoft.Teams.Api.TaskModules.Response OnTaskFetch([Context] Tasks.FetchActivity activity, [Context] IContext.Client client, [Context] ILogger log) { var data = activity.Value?.Data as JsonElement?; if (data == null) { log.Info("[TASK_FETCH] No data found in the activity value"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("No data found in the activity value")); } var dialogType = data.Value.TryGetProperty("opendialogtype", out var dialogTypeElement) && dialogTypeElement.ValueKind == JsonValueKind.String ? dialogTypeElement.GetString() : null; log.Info($"[TASK_FETCH] Dialog type: {dialogType}"); return dialogType switch { "simple_form" => CreateSimpleFormDialog(), "webpage_dialog" => CreateWebpageDialog(_configuration, log), "multi_step_form" => CreateMultiStepFormDialog(), "mixed_example" => CreateMixedExampleDialog(), _ => new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Unknown dialog type")) }; } ``` ### Rendering A Card You can render an Adaptive Card in a dialog by returning a card response. ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Cards; //... private static Microsoft.Teams.Api.TaskModules.Response CreateSimpleFormDialog() { var choices = new List { new Choice { Title = "Option 1", Value = "opt1" }, new Choice { Title = "Option 2", Value = "opt2" }, new Choice { Title = "Option 3", Value = "opt3" } }; var dialogCard = new AdaptiveCard { Body = new List { new TextBlock("This is a simple form") { Size = TextSize.Large, Weight = TextWeight.Bolder }, new TextInput { Id = "name", Label = "Name", Placeholder = "Enter your name", IsRequired = true }, new ChoiceSetInput { Id = "preference", Label = "Select your preference", Choices = choices, Style = StyleEnum.Compact } }, Actions = new List { new SubmitAction { Title = "Submit", Data = new { submissiondialogtype = "simple_form" } } } }; var taskInfo = new TaskInfo { Title = "Simple Form Dialog", Card = new Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = dialogCard } }; return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.ContinueTask(taskInfo)); } ``` :::info The action type for submitting a dialog must be `Action.Submit`. This is a requirement of the Teams client. If you use a different action type, the dialog will not be submitted and the agent will not receive the submission event. ::: ### Rendering A Webpage You can render a webpage in a dialog as well. There are some security requirements to be aware of: 1. The webpage must be hosted on a domain that is allow-listed as `validDomains` in the Teams app [manifest](/teams/manifest) for the agent 2. The webpage must also host the [teams-js client library](https://www.npmjs.com/package/@microsoft/teams-js). The reason for this is that for security purposes, the Teams client will not render arbitrary webpages. As such, the webpage must explicitly opt-in to being rendered in the Teams client. Setting up the teams-js client library handles this for you. ```csharp using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Common; //... private static Microsoft.Teams.Api.TaskModules.Response CreateWebpageDialog(IConfiguration configuration, ILogger log) { var botEndpoint = configuration["BotEndpoint"]; if (string.IsNullOrEmpty(botEndpoint)) { log.Warn("No remote endpoint detected. Using webpages for dialog will not work as expected"); botEndpoint = "http://localhost:3978"; // Fallback for local development } else { log.Info($"Using BotEndpoint: {botEndpoint}/tabs/dialog-form"); } var taskInfo = new TaskInfo { Title = "Webpage Dialog", Width = new Union(1000), Height = new Union(800), // Here we are using a webpage that is hosted in the same // server as the agent. This server needs to be publicly accessible, // needs to set up teams.js client library (https://www.npmjs.com/package/@microsoft/teams-js) // and needs to be registered in the manifest. Url = $"{botEndpoint}/tabs/dialog-form" }; return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.ContinueTask(taskInfo)); } ``` ### Setting up Embedded Web Content To serve web content for dialogs, you can use the `AddTab` functionality to embed HTML files as resources: ```csharp // In Program.cs when building your app app.UseTeams(); app.AddTab("dialog-form", "Web/dialog-form"); // Configure project file to embed web resources // In .csproj: // true // // ``` --- ### Listening To Activities # Listening To Activities An **Activity** is the Teams‑specific payload that flows between the user and your bot. Where _events_ describe high‑level happenings inside your app, _activities_ are the raw Teams messages such as chat text, card actions, installs, or invoke calls. The Teams SDK exposes a fluent router so you can subscribe to these activities with `app.OnActivity(...)` using minimal APIs. ```mermaid flowchart LR Teams["Teams"]:::less-interesting Server["App Server"]:::interesting ActivityRouter["Activity Router (app.on())"]:::interesting Handlers["Your Activity Handlers"]:::interesting Teams --> |Events| Server Server --> |Activity Event| ActivityRouter ActivityRouter --> |handler invoked| Handlers classDef interesting fill:#b1650f,stroke:#333,stroke-width:4px; classDef less-interesting fill:#666,stroke:#333,stroke-width:4px; ``` Here is an example of a basic message handler: ```csharp app.OnMessage(async (context, cancellationToken) => { await context.Send($"you said: {context.activity.Text}", cancellationToken); }); ``` In the above example, the `context.activity` parameter is of type `MessageActivity`, which has a `Text` property. You'll notice that the handler here does not return anything, but instead handles it by `send`ing a message back. For message activities, Teams does not expect your application to return anything (though it's usually a good idea to send some sort of friendly acknowledgment!). ## Middleware pattern The `OnActivity` activity handlers (and attributes) follow a [middleware](https://www.patterns.dev/vanilla/mediator-pattern/) pattern similar to how `dotnet` middlewares work. This means that for each activity handler, a `Next` function is passed in which can be called to pass control to the next handler. This allows you to build a chain of handlers that can process the same activity in different ways. ```csharp app.OnMessage(async (context, cancellationToken) => { Console.WriteLine("global logger"); context.Next(); // pass control onward return Task.CompletedTask; }); ``` ```csharp app.OnMessage(async (context, cancellationToken) => { if (context.Activity.Text == "/help") { await context.Send("Here are all the ways I can help you...", cancellationToken); } // Conditionally pass control to the next handler context.Next(); }); app.OnMessage(async (context, cancellationToken) => { // Fallthrough to the final handler await context.Send($"Hello! you said {context.Activity.Text}", cancellationToken); }); ``` :::info Just like other middlewares, if you stop the chain by not calling `next()`, the activity will not be passed to the next handler. The order of registration for the handlers also matters as that determines how the handlers will be called. ::: --- ### Middleware # Middleware Middleware is a useful tool for logging, validation, and more. You can easily register your own middleware using the `app.Use` method. Below is an example of a middleware that will log the elapse time of all handlers that come after it. ```csharp app.Use(async context => { var start = DateTime.UtcNow; try { await context.Next(); } catch { context.Log.Error("error occurred during activity processing"); } context.Log.Debug($"request took {(DateTime.UtcNow - start).TotalMilliseconds}ms"); }); ``` --- ### Proactive Messaging # Proactive Messaging In [Sending Messages](./), you were shown how to respond to an event when it happens. However, there are times when you want to send a message to the user without them sending a message first. This is called proactive messaging. You can do this by using the `send` method in the `app` instance. This approach is useful for sending notifications or reminders to the user. The main thing to note is that you need to have the `conversationId` of the chat or channel that you want to send the message to. It's a good idea to store this value somewhere from an activity handler so that you can use it for proactive messaging later. ```csharp app.OnInstall(async (context, cancellationToken) => { // Save the conversation id in context.Storage.Set(activity.From.AadObjectId!, activity.Conversation.Id); await context.Send("Hi! I am going to remind you to say something to me soon!", cancellationToken); notificationQueue.AddReminder(activity.From.AadObjectId!, Notifications.SendProactive, 10_000); }); ``` Then, when you want to send a proactive message, you can retrieve the `conversationId` from storage and use it to send the message. ```csharp public static class Notifications { public static async Task SendProactive(string userId) { var conversationId = (string?)storage.Get(userId); if (conversationId is null) return; await app.Send(conversationId, "Hey! It's been a while. How are you?"); } } ``` :::tip In this example, you see how to get the `conversationId` using one of the activity handlers. This is a good place to store the conversation id, but you can also do this in other places like when the user installs the app or when they sign in. The important thing is that you have the conversation id stored somewhere so you can use it later. ::: ## Targeted Proactive Messages :::info[Preview] Targeted messages are currently in preview. ::: Targeted messages, also known as ephemeral messages, are delivered to a specific user in a shared conversation. From a single user's perspective, they appear as regular inline messages in a conversation. Other participants won't see these messages. When sending targeted messages proactively, you must explicitly specify the recipient account. ```csharp // When sending proactively, you must provide an explicit recipient account public static async Task SendTargetedNotification(string conversationId, Account recipient) { var teams = app.UseTeams(); await teams.Send( conversationId, new MessageActivity("This is a private notification just for you!") .WithRecipient(recipient, isTargeted: true) ); } ``` --- ### Quickstart # Quickstart Get started with Teams SDK quickly using the Teams CLI. ## Set up a new project ### Prerequisites - **.NET** v.8 or higher. Install or upgrade from [dotnet.microsoft.com](https://dotnet.microsoft.com/en-us/download). ## Instructions ### Use the Teams CLI Use your terminal to run the Teams CLI using npx: ```sh npx @microsoft/teams.cli --version ``` :::info _The [Teams CLI](/developer-tools/cli) is a command-line tool that helps you create and manage Teams applications. It provides a set of commands to simplify the development process._

Using `npx` allows you to run the Teams CLI without installing it globally. You can verify it works by running the version command above. ::: ## Creating Your First Agent Let's begin by creating a simple echo agent that responds to messages. Run: ```sh npx @microsoft/teams.cli@latest new csharp quote-agent --template echo ``` This command: 1. Creates a new directory called `Quote.Agent`. 2. Bootstraps the echo agent template files into your project directory. 3. Creates your agent's manifest files, including a `manifest.json` file and placeholder icons in the `Quote.Agent/appPackage` directory. The Teams [app manifest](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) is required for [sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) the app into Teams. > The `echo` template creates a basic agent that repeats back any message it receives - perfect for learning the fundamentals. ## Running your agent 1. Navigate to your new agent's directory: ```sh cd Quote.Agent/Quote.Agent ``` 2. Install the dependencies: ```sh dotnet restore ``` 3. Start the development server: ```sh dotnet run ``` 4. In the console, you should see a similar output: ```sh [INFO] Microsoft.Hosting.Lifetime Now listening on: http://localhost:3978 [WARN] Echo.Microsoft.Teams.Plugins.AspNetCore.DevTools ⚠️ Devtools are not secure and should not be used production environments ⚠️ [INFO] Echo.Microsoft.Teams.Plugins.AspNetCore.DevTools Available at http://localhost:3979/devtools [INFO] Microsoft.Hosting.Lifetime Application started. Press Ctrl+C to shut down. [INFO] Microsoft.Hosting.Lifetime Hosting environment: Development ``` When the application starts, you'll see: 1. An HTTP server starting up (on port `3978`). This is the main server which handles incoming requests and serves the agent application. ## Add to an Existing Project If you already have a project and want to add Teams support, install the SDK directly: Then initialize the Teams app with your existing server: `app.initialize()` registers the Teams endpoint on your server without starting a new one — you keep full control of your server lifecycle. ## Next steps After creating and running your first agent, read about [the code basics](code-basics.txt) to better understand its components and structure. Otherwise, if you want to run your agent in Teams, you can check out the [Running in Teams](running-in-teams.txt) guide. ## Resources - [Teams CLI documentation](/developer-tools/cli) - [Teams manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) - [Teams sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) --- ### Service URL Validation # Service URL Validation The Teams SDK validates that incoming `serviceUrl` values belong to known domains before using them for outbound API calls. This prevents the bot's credentials from being sent to unauthorized endpoints. ## How it works When your bot receives an activity from the Bot Framework Channel Service, the activity includes a `serviceUrl` field that tells the SDK where to send responses. The SDK validates this URL against allowed hostnames from the configured cloud environment before making any outbound requests. ### Allowed hostnames by cloud Each cloud environment preset includes the allowed service URL hostnames for that cloud: | Cloud | Allowed hostnames | |---|---| | Public (default) | `smba.trafficmanager.net`, `smba.onyx.prod.teams.trafficmanager.net`, `smba.infra.gcc.teams.microsoft.com` | | US Gov (GCCH) | `smba.infra.gov.teams.microsoft.us` | | US Gov DoD | `smba.infra.dod.teams.microsoft.us` | | China (21Vianet) | `frontend.botapi.msg.infra.teams.microsoftonline.cn` | | `localhost` / `127.0.0.1` | Always allowed for local development | If your bot is configured for a sovereign cloud, only that cloud's hostnames are allowed by default. ## Adding custom domains If your bot receives activities from a service URL outside the cloud preset's allowlist (e.g., sovereign clouds without presets, non-Teams channels, or custom environments), you can add additional hostnames. Sovereign cloud FQDNs that require `additionalAllowedDomains`: | Environment | FQDN | |---|---| | France (Bleu) | `smba.teams.sovcloud-core.fr` | | EagleX | `frontend.botapi.msg.infra.teams.eaglex.ic.gov` | | SCloud | `frontend.botapi.msg.infra.teams.microsoft.scloud` | Via `appsettings.json`: ```json { "Teams": { "AdditionalAllowedDomains": ["api.my-custom-channel.com"] } } ``` ## Disabling validation :::warning Disabling service URL validation removes a security protection that prevents your bot's credentials from being sent to unauthorized endpoints. Only disable this if you understand the security implications. ::: If you have a non-standard setup where domain-based validation does not work, you can disable it by passing `*` as an additional domain: Via `appsettings.json`: ```json { "Teams": { "AdditionalAllowedDomains": ["*"] } } ``` ## Proactive messaging :::info Service URL validation runs on incoming activities. If you store a `ConversationReference` for proactive messaging, the `serviceUrl` was validated when the original activity was received. Only store conversation references from validated inbound activities -- never from untrusted external sources. ::: --- ### Setup & Prerequisites # Setup & Prerequisites There are a few prerequisites to getting started with integrating LLMs into your application: - LLM API Key - To generate messages using an LLM, you will need to have an API Key for the LLM you are using. - [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) - [OpenAI](https://platform.openai.com/) **NuGet Package** - Install the Microsoft Teams SDK: ```bash dotnet add package Microsoft.Teams.AI ``` - In your application, you should include your keys in a secure way. You should include your keys securely using `appsettings.json` or environment variables ### Azure OpenAI You will need to deploy a model in Azure OpenAI. View the [resource creation guide](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model 'Azure OpenAI Model Deployment Guide') for more information on how to do this. Once you have deployed a model, configure your application using `appsettings.json` or `appsettings.Development.json`: **appsettings.Development.json** ```json { "AzureOpenAIKey": "your-azure-openai-api-key", "AzureOpenAIModel": "your-azure-openai-model-deployment-name", "AzureOpenAIEndpoint": "https://your-resource.openai.azure.com/" } ``` **Using configuration in your code:** ```csharp var azureOpenAIModel = configuration["AzureOpenAIModel"] ?? throw new InvalidOperationException("AzureOpenAIModel not configured"); var azureOpenAIEndpoint = configuration["AzureOpenAIEndpoint"] ?? throw new InvalidOperationException("AzureOpenAIEndpoint not configured"); var azureOpenAIKey = configuration["AzureOpenAIKey"] ?? throw new InvalidOperationException("AzureOpenAIKey not configured"); var azureOpenAI = new AzureOpenAIClient( new Uri(azureOpenAIEndpoint), new ApiKeyCredential(azureOpenAIKey) ); var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); ``` :::tip Use `appsettings.Development.json` for local development and keep it in `.gitignore`. For production, use environment variables or Azure Key Vault. ::: :::info The Azure OpenAI SDK handles API versioning automatically. You don't need to specify an API version manually. ::: ### OpenAI You will need to create an OpenAI account and get an API key. View the [OpenAI Quickstart Guide](https://platform.openai.com/docs/quickstart/build-your-application 'OpenAI Quickstart Guide') for how to do this. Once you have your API key, configure your application: **appsettings.Development.json** ```json { "OpenAIKey": "sk-your-openai-api-key", "OpenAIModel": "gpt-4o" } ``` **Using configuration in your code:** ```csharp var openAIKey = configuration["OpenAIKey"] ?? throw new InvalidOperationException("OpenAIKey not configured"); var openAIModel = configuration["OpenAIModel"] ?? "gpt-4o"; var aiModel = new OpenAIChatModel(openAIModel, openAIKey); ``` :::tip Use `appsettings.Development.json` for local development and keep it in `.gitignore`. For production, use environment variables or Azure Key Vault. ::: --- ### Static Pages # Static Pages The `App` class lets you host web apps in the agent. This can be used for an efficient inner loop when building a complex app using Microsoft 365 Agents Toolkit, as it lets you build, deploy, and sideload both an agent and a Tab app inside of Teams in a single step. It's also useful in production scenarios, as it makes it straight-forward to host a simple experience such as an agent configuration page or a Dialog. To host a static tab web app, call the `app.AddTab()` function and provide an app name and a path to a folder containing an `index.html` file to be served up. ```csharp app.AddTab("myApp", "Web/bin"); ``` This registers a route that is hosted at `http://localhost:PORT/tabs/myApp` or `https://BOT_DOMAIN/tabs/myApp`. ## Additional resources - For more details about Tab apps, see the [Tabs](../tabs/) in-depth guide. - For an example of hosting a Dialog, see the [Creating Dialogs](../dialogs/creating-dialogs) in-depth guide. --- ### 💬 Chat Generation # 💬 Chat Generation Before going through this guide, please make sure you have completed the [setup and prerequisites](./setup-and-prereqs.mdx) guide. # Setup The basic setup involves creating a `ChatPrompt` and giving it the `Model` you want to use. ```mermaid flowchart LR Prompt subgraph Application Send --> Prompt UserMessage["User Message
Hi how are you?"] --> Send Send --> Content["Content
I am doing great! How can I help you?"] subgraph Setup Messages --> Prompt Instructions --> Prompt Options["Other options..."] --> Prompt Prompt --> Model end end subgraph LLMProvider Model --> AOAI["Azure Open AI"] Model --> OAI["Open AI"] Model --> Anthropic["Claude"] Model --> OtherModels["..."] end ``` ## Simple chat generation Chat generation is the the most basic way of interacting with an LLM model. It involves setting up your ChatPrompt, the Model, and sending it the message. Import the relevant namespaces: ```csharp // AI using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; // Teams using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Annotations; ``` Create a ChatModel, ChatPrompt, and handle user - LLM interactions: ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; using Microsoft.Teams.AI.Templates; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps.Activities; using Azure.AI.OpenAI; using System.ClientModel; // Configuration var azureOpenAIModel = configuration["AzureOpenAIModel"]!; var azureOpenAIEndpoint = configuration["AzureOpenAIEndpoint"]!; var azureOpenAIKey = configuration["AzureOpenAIKey"]!; var azureOpenAI = new AzureOpenAIClient( new Uri(azureOpenAIEndpoint), new ApiKeyCredential(azureOpenAIKey) ); // AI Model var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); // Simple chat handler teamsApp.OnMessage(async (context, cancellationToken) => { var prompt = new OpenAIChatPrompt(aiModel, new ChatPromptOptions { Instructions = new StringTemplate("You are a friendly assistant who talks like a pirate") }); var result = await prompt.Send(context.Activity.Text); if (result.Content != null) { var messageActivity = new MessageActivity { Text = result.Content, }.AddAIGenerated(); await context.Send(messageActivity, cancellationToken); // Ahoy, matey! 🏴‍☠️ How be ye doin' this fine day on th' high seas? What can this ol' salty sea dog help ye with? 🚢☠️ } }); ``` ### Declarative Approach This approach uses attributes to declare prompts, providing clean separation of concerns. **Create a Prompt Class:** ```csharp using Microsoft.Teams.AI.Annotations; namespace Samples.AI.Prompts; [Prompt] [Prompt.Description("A friendly pirate assistant")] [Prompt.Instructions("You are a friendly assistant who talks like a pirate")] public class PiratePrompt { } ``` **Usage in Program.cs:** ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Api.Activities; // Create the AI model var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); // Use the prompt with OpenAIChatPrompt.From() teamsApp.OnMessage(async (context, cancellationToken) => { var prompt = OpenAIChatPrompt.From(aiModel, new Samples.AI.Prompts.PiratePrompt()); var result = await prompt.Send(context.Activity.Text); if (!string.IsNullOrEmpty(result.Content)) { await context.Send(new MessageActivity { Text = result.Content }.AddAIGenerated(), cancellationToken); // Ahoy, matey! 🏴‍☠️ How be ye doin' this fine day on th' high seas? } }); ``` :::note The current `OpenAIChatModel` implementation uses chat-completions API. The responses API is coming soon. ::: ## Streaming chat responses LLMs can take a while to generate a response, so often streaming the response leads to a better, more responsive user experience. :::warning Streaming is only currently supported for single 1:1 chats, and not for groups or channels. ::: ```csharp // Streaming handler teamsApp.OnMessage(async (context, cancellationToken) => { var match = Regex.Match(context.Activity.Text ?? "", @"^stream\s+(.+)", RegexOptions.IgnoreCase); if (match.Success) { var query = match.Groups[1].Value.Trim(); var prompt = new OpenAIChatPrompt(aiModel, new ChatPromptOptions { Instructions = new StringTemplate("You are a friendly assistant who responds in extremely verbose language") }); var result = await prompt.Send(query, (chunk) => { context.Stream.Emit(chunk); return Task.CompletedTask; }); } }); ``` ![Animated image showing agent response text incrementally appearing in the chat window.](/screenshots/streaming-chat.gif) --- ### 🔍 Search commands # 🔍 Search commands Message extension search commands allow users to search external systems and insert the results of that search into a message in the form of a card. ## Search command invocation locations There are two different areas search commands can be invoked from: 1. Compose Area 2. Compose Box ### Compose Area and Box ![Screenshot of Teams with outlines around the 'Compose Box' (for typing messages) and the 'Compose Area' (the menu option next to the compose box that provides a search bar for actions and apps).](/screenshots/compose-area.png) ## Setting up your Teams app manifest To use search commands you have to define them in the Teams app manifest. Here is an example: ```json "composeExtensions": [ { "botId": "${{BOT_ID}}", "commands": [ { "id": "searchQuery", "context": [ "compose", "commandBox" ], "description": "Test command to run query", "title": "Search query", "type": "query", "parameters": [ { "name": "searchQuery", "title": "Search Query", "description": "Your search query", "inputType": "text" } ] } ] } ] ``` Here we are defining the `searchQuery` search (or query) command. ## Handle submission Handle the search query submission when the `searchQuery` search command is invoked. ```csharp using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; //... [MessageExtension.Query] public Response OnMessageExtensionQuery( [Context] QueryActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[MESSAGE_EXT_QUERY] Search query received"); var commandId = activity.Value?.CommandId; var query = activity.Value?.Parameters?.FirstOrDefault(p => p.Name == "searchQuery")?.Value?.ToString() ?? ""; log.Info($"[MESSAGE_EXT_QUERY] Command: {commandId}, Query: {query}"); if (commandId == "searchQuery") { return CreateSearchResults(query, log); } return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List() } }; } ``` `CreateSearchResults()` method ```csharp using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Cards; using Microsoft.Teams.Common; //... private static Response CreateSearchResults(string query, ILogger log) { var attachments = new List(); // Create simple search results for (int i = 1; i <= 5; i++) { var card = new AdaptiveCard { Body = new List { new TextBlock($"Search Result {i}") { Weight = TextWeight.Bolder, Size = TextSize.Large }, new TextBlock($"Query: '{query}' - Result description for item {i}") { Wrap = true, IsSubtle = true } } }; var previewCard = new ThumbnailCard() { Title = $"Result {i}", Text = $"This is a preview of result {i} for query '{query}'." }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = ContentType.AdaptiveCard, Content = card, Preview = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = ContentType.ThumbnailCard, Content = previewCard } }; attachments.Add(attachment); } return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = attachments } }; } ``` To implement custom actions when a user clicks on a search result item, you can handle the select item event: ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Cards; //... [MessageExtension.SelectItem] public Response OnMessageExtensionSelectItem( [Context] SelectItemActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[MESSAGE_EXT_SELECT_ITEM] Item selection received"); var selectedItem = activity.Value; log.Info($"[MESSAGE_EXT_SELECT_ITEM] Selected: {JsonSerializer.Serialize(selectedItem)}"); return CreateItemSelectionResponse(selectedItem, log); } // Helper method to create item selection response private static Response CreateItemSelectionResponse(object? selectedItem, ILogger log) { var itemJson = JsonSerializer.Serialize(selectedItem); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Item Selected") { Weight = TextWeight.Bolder, Size = TextSize.Large, Color = TextColor.Good }, new TextBlock("You selected the following item:") { Wrap = true }, new TextBlock(itemJson) { Wrap = true, FontType = FontType.Monospace, Separator = true } } }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = card }; return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List { attachment } } }; } ``` The search results include both a full adaptive card and a preview card. The preview card appears as a list item in the search command area: ![Screenshot of Teams showing a message extensions search menu open with list of search results displayed as preview cards.](/screenshots/preview-card.png) When a user clicks on a list item the dummy adaptive card is added to the compose box: ![Screenshot of Teams showing the selected adaptive card added to the compose box.](/screenshots/card-in-compose.png) To implement custom actions when a user clicks on a search result item, you can add the `tap` property to the preview card. This allows you to handle the click event with custom logic: ## Resources - [Search command](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/define-search-command?tabs=Teams-toolkit%2Cdotnet) - [Just-In-Time Install](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/universal-actions-for-search-based-message-extensions#just-in-time-install) --- ### 🗃️ Custom Logger # 🗃️ Custom Logger The `App` will provide a default logger, but you can also provide your own. The default `Logger` instance will be set to `ConsoleLogger` from the `Microsoft.Teams.Common` package. ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Common.Logging; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); var appBuilder = App.Builder() .AddLogger(new ConsoleLogger()) builder.AddTeams(appBuilder) var app = builder.Build(); var teams = app.UseTeams(); ``` ## Log Levels ## Filtering by Logger Name Each logger is created with a name. You can filter which loggers emit output by providing a name pattern, using `*` as a wildcard. ## Environment Variables ## Child Loggers --- ### Code Basics # Code Basics After following the guidance in [the quickstart](quickstart.txt) to create your first Teams application, let's review its structure and key components. This knowledge can help you build more complex applications as you progress. ## Project Structure When you create a new Teams application, it generates a directory with this basic structure: ``` Quote.Agent/ |── appPackage/ # Teams app package files ├── Program.cs # Main application startup code ``` - **appPackage/**: Contains the Teams app package files, including the `manifest.json` file and icons. This is required for [sideloading](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) the app into Teams for testing. The app manifest defines the app's metadata, capabilities, and permissions. ## Core Components Let's break down the simple application from the [quickstart](quickstart.txt) into its core components. ### The App Class The heart of an application is the `App` class. This class handles all incoming activities and manages the application's lifecycle. It also acts as a way to host your application service. ```csharp title="Program.cs" using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Extensions; using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); builder.AddTeams().AddTeamsDevTools(); var app = builder.Build(); var teams = app.UseTeams(); teams.OnMessage(async (context, cancellationToken) => { await context.Typing(cancellationToken); await context.Send($"you said '{context.Activity.Text}'", cancellationToken); }); app.Run(); ``` The app configuration includes a variety of options that allow you to customize its behavior, including controlling the underlying server, authentication, and other settings. ### Plugins Plugins are a core part of the Teams SDK. They allow you to hook into various lifecycles of the application. The lifecycles include server events (start, stop, initialize, etc.), and also Teams Activity events (onActivity, onActivitySent, etc.). ### Message Handling Teams applications respond to various types of activities. The most basic is handling messages: ```csharp title="Program.cs" teams.OnMessage(async (context, cancellationToken) => { await context.Typing(cancellationToken); await context.Send($"you said \"{context.activity.Text}\"", cancellationToken); }); ``` This code: 1. Listens for all incoming messages using `onMessage` handler. 2. Sends a typing indicator, which renders as an animated ellipsis (…) in the chat. 3. Responds by echoing back the received message. :::info Each activity type has both an attribute and a functional method for type safety/simplicity of routing logic! ::: ### Application Lifecycle Your application starts when you run: ```csharp var app = builder.Build(); app.UseTeams(); app.Run(); ``` This code initializes your application server and, when configured for Teams, also authenticates it to be ready for sending and receiving messages. ## Next Steps Now that you understand the basic structure of your Teams application, you're ready to [run it in Teams](running-in-teams.txt). You will learn about Microsoft 365 Agents Toolkit and other important tools that help you with deployment and testing your application. After that, you can: - Add more activity handlers for different types of interactions. See [Listening to Activities](../essentials/on-activity) for more details. - Integrate with external services using the [API Client](../essentials/api). - Add interactive [cards](../in-depth-guides/adaptive-cards) and [dialogs](../in-depth-guides/dialogs). Continue on to the next page to learn about these advanced features. ## Other Resources - [Essentials](../essentials) - [Teams concepts](/teams) - [Teams developer tools](/developer-tools) --- ### Dialogs # Dialogs Dialogs are a helpful paradigm in Teams which improve interactions between your agent and users. When dialogs are **invoked**, they pop open a window for a user in the Teams client. The content of the dialog can be supplied by the agent application. :::note In Teams client v1, dialogs were called task modules. They may occasionaly be used synonymously. ::: ## Key benefits 1. Dialogs pop open for a user in the Teams client. This means in group-settings, dialog actions are not visible to other users in the channel, reducing clutter. 2. Interactions like filling out complex forms, or multi-step forms where each step depends on the previous step are excellent use cases for dialogs. 3. The content for the dialog can be hard-coded in, or fetched at runtime. This makes them extremely flexible and powerful. ## Resources - [Task Modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/what-are-task-modules) - [Invoking Task Modules](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/invoking-task-modules) --- ### Essentials # Essentials At its core, an application that hosts an agent on Microsoft Teams exists to do three things well: listen to events, handle the ones that matter, and respond efficiently. Whether a user sends a message, opens a dialog, or clicks a button — your app is there to interpret the event and act on it. With Teams SDK, we've made it easier than ever to build this kind of reactive, conversational logic. The SDK introduces a few simple but powerful paradigms to help you connect to Teams, register handlers, and build intelligent agent behaviors quickly. Before diving in, let's define a few key terms: - Event: Anything interesting that happens on Teams — or within your application as a result of handling an earlier event. - Activity: A special type of Teams-specific event. Activities include things like messages, reactions, and adaptive card actions. - InvokeActivity: A specific kind of activity triggered by user interaction (like submitting a form), which may or may not require a response. - Handler: The logic in your application that reacts to events or activities. Handlers decide what to do, when, and how to respond. ```mermaid flowchart LR Teams["Teams"] Server["App Server"] AppEventHandlers["Event Handler (app.OnEvent())"] AppRouter["Activity Event Router"] AppActivityHandlers["Activity Handlers (app.OnActivity())"] Teams --> |Activity| Server Teams --> |Signed In| Server Teams --> |...other
incoming events| Server Server --> |ActivityEvent
or InvokeEvent| AppRouter Server ---> |incoming
events| AppEventHandlers Server ---> |outgoing
events
| AppEventHandlers AppRouter --> |message activity| AppActivityHandlers AppRouter --> |card activity| AppActivityHandlers AppRouter --> |installation activity| AppActivityHandlers AppRouter --> |...other activities| AppActivityHandlers linkStyle 0,3 stroke:#66fdf3,stroke-width:1px,color:Tomato linkStyle 1,2,4,5 stroke:#66fdf3,stroke-width:1px linkStyle 6,7,8,9 color:Tomato ``` This section will walk you through the foundational pieces needed to build responsive, intelligent agents using the SDK. --- ### Executing Actions # Executing Actions Adaptive Cards support interactive elements through **actions**—buttons, links, and input submission triggers that respond to user interaction. You can use these to collect form input, trigger workflows, show task modules, open URLs, and more. ## Action Types The Teams SDK supports several action types for different interaction patterns: | Action Type | Purpose | Description | | ------------------------- | ---------------------- | ---------------------------------------------------------------------------- | | `Action.Execute` | Server‑side processing | Send data to your bot for processing. Best for forms & multi‑step workflows. | | `Action.Submit` | Simple data submission | Legacy action type. Prefer `Execute` for new projects. | | `Action.OpenUrl` | External navigation | Open a URL in the user's browser. | | `Action.ShowCard` | Progressive disclosure | Display a nested card when clicked. | | `Action.ToggleVisibility` | UI state management | Show/hide card elements dynamically. | :::info For complete reference, see the [official documentation](https://adaptivecards.microsoft.com/?topic=Action.Execute). ::: ## Creating Actions with the SDK ### Single Actions The SDK provides builder helpers that abstract the underlying JSON. For example: ```csharp using Microsoft.Teams.Cards; var action = new ExecuteAction { Title = "Submit Feedback", Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "submit_feedback" } } }), AssociatedInputs = AssociatedInputs.Auto }; ``` ### Action Sets Group actions together using `ActionSet`: ```csharp using Microsoft.Teams.Cards; var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Actions = new List { new ExecuteAction { Title = "Submit Feedback", Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "submit_feedback" } } }) }, new OpenUrlAction("https://adaptivecards.microsoft.com") { Title = "Learn More" } } }; ``` ### Raw JSON Alternative Just like when building cards, if you prefer to work with raw JSON, you can do just that. ```csharp var actionJson = """ { "type": "Action.OpenUrl", "url": "https://adaptivecards.microsoft.com", "title": "Learn More" } """; var action = OpenUrlAction.Deserialize(actionJson); ``` ## Working with Input Values ### Associating data with the cards Sometimes you want to send a card and have it be associated with some data. Set the `data` value to be sent back to the client so you can associate it with a particular entity. ```csharp private static AdaptiveCard CreateProfileCard() { return new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("User Profile") { Weight = TextWeight.Bolder, Size = TextSize.Large }, new TextInput { Id = "name", Label = "Name", Value = "John Doe" }, new TextInput { Id = "email", Label = "Email", Value = "john@contoso.com" }, new ToggleInput("Subscribe to newsletter") { Id = "subscribe", Value = "false" } }, Actions = new List { new ExecuteAction { Title = "Save", // entity_id will come back after the user submits Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "save_profile" }, { "entity_id", "12345" } } }), AssociatedInputs = AssociatedInputs.Auto } } }; } // Data received in handler (conceptual structure) /* { "action": "save_profile", "entity_id": "12345", // From action data "name": "John Doe", // From name input "email": "john@doe.com", // From email input "subscribe": "true" // From toggle input (as string) } Accessed in C# as: - data["action"] → "save_profile" - data["entity_id"] → "12345" - data["name"] → "John Doe" - data["email"] → "john@doe.com" - data["subscribe"] → "true" */ ``` ### Input Validation Input Controls provide ways for you to validate. More details can be found on the Adaptive Cards [documentation](https://adaptivecards.microsoft.com/?topic=input-validation). ```csharp private static AdaptiveCard CreateProfileCardWithValidation() { return new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Profile with Validation") { Weight = TextWeight.Bolder, Size = TextSize.Large }, new NumberInput { Id = "age", Label = "Age", IsRequired = true, Min = 0, Max = 120 }, // Can configure custom error messages new TextInput { Id = "name", Label = "Name", IsRequired = true, ErrorMessage = "Name is required" }, new TextInput { Id = "location", Label = "Location" } }, Actions = new List { new ExecuteAction { Title = "Save", // All inputs should be validated Data = new Union(new SubmitActionData { NonSchemaProperties = new Dictionary { { "action", "save_profile" } } }), AssociatedInputs = AssociatedInputs.Auto } } }; } ``` ## Server Handlers ### Basic Structure Card actions arrive as `card.action` activities in your app. These give you access to the validated input values plus any `data` values you had configured to be sent back to you. ```csharp using System.Text.Json; using Microsoft.Teams.Api.Activities.Invokes.AdaptiveCards; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Common.Logging; //... teams.OnAdaptiveCardAction(async (context, cancellationToken) => { var activity = context.Activity; context.Log.Info("[CARD_ACTION] Card action received"); var data = activity.Value?.Action?.Data; context.Log.Info($"[CARD_ACTION] Raw data: {JsonSerializer.Serialize(data)}"); if (data == null) { context.Log.Error("[CARD_ACTION] No data in card action"); return new ActionResponse.Message("No data specified") { StatusCode = 400 }; } string? action = data.TryGetValue("action", out var actionObj) ? actionObj?.ToString() : null; if (string.IsNullOrEmpty(action)) { context.Log.Error("[CARD_ACTION] No action specified in card data"); return new ActionResponse.Message("No action specified") { StatusCode = 400 }; } context.Log.Info($"[CARD_ACTION] Processing action: {action}"); string? GetFormValue(string key) { if (data.TryGetValue(key, out var val)) { if (val is JsonElement element) return element.GetString(); return val?.ToString(); } return null; } switch (action) { case "submit_basic": var notifyValue = GetFormValue("notify") ?? "false"; await context.Send($"Basic card submitted! Notify setting: {notifyValue}", cancellationToken); break; case "submit_feedback": var feedbackText = GetFormValue("feedback") ?? "No feedback provided"; await context.Send($"Feedback received: {feedbackText}", cancellationToken); break; case "create_task": var title = GetFormValue("title") ?? "Untitled"; var priority = GetFormValue("priority") ?? "medium"; var dueDate = GetFormValue("due_date") ?? "No date"; await context.Send($"Task created!\nTitle: {title}\nPriority: {priority}\nDue: {dueDate}", cancellationToken); break; case "save_profile": var name = GetFormValue("name") ?? "Unknown"; var email = GetFormValue("email") ?? "No email"; var subscribe = GetFormValue("subscribe") ?? "false"; var age = GetFormValue("age"); var location = GetFormValue("location") ?? "Not specified"; var response = $"Profile saved!\nName: {name}\nEmail: {email}\nSubscribed: {subscribe}"; if (!string.IsNullOrEmpty(age)) response += $"\nAge: {age}"; if (location != "Not specified") response += $"\nLocation: {location}"; await context.Send(response, cancellationToken); break; case "test_json": await context.Send("JSON deserialization test successful!", cancellationToken); break; default: context.Log.Error($"[CARD_ACTION] Unknown action: {action}"); return new ActionResponse.Message("Unknown action") { StatusCode = 400 }; } return new ActionResponse.Message("Action processed successfully") { StatusCode = 200 }; }); ``` :::note The `data` values come from JSON and need to be extracted using the helper method shown above to handle different JSON element types. ::: --- ### Handling Dialog Submissions # Handling Dialog Submissions Dialogs have a specific `TaskSubmit` event to handle submissions. When a user submits a form inside a dialog, the app is notified via this event, which is then handled to process the submission values, and can either send a response or proceed to more steps in the dialogs (see [Multi-step Dialogs](./handling-multi-step-forms)). :::warning Return Type Requirement Methods decorated with `[TaskSubmit]` **must** return `Task`. Every code path must return a Response object containing either a `MessageTask` (to show a message and close the dialog) or a `ContinueTask` (to show another dialog). Using just `Task` or `void` will compile but fail at runtime when the Teams client expects a Response object. ::: ## Basic Example In this example, we show how to handle dialog submissions from an Adaptive Card form: ```csharp using System.Text.Json; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Common.Logging; //... [TaskSubmit] public async Task OnTaskSubmit([Context] Tasks.SubmitActivity activity, [Context] IContext.Client client, [Context] ILogger log) { var data = activity.Value?.Data as JsonElement?; if (data == null) { log.Info("[TASK_SUBMIT] No data found in the activity value"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("No data found in the activity value")); } var submissionType = data.Value.TryGetProperty("submissiondialogtype", out var submissionTypeObj) && submissionTypeObj.ValueKind == JsonValueKind.String ? submissionTypeObj.ToString() : null; string? GetFormValue(string key) { if (data.Value.TryGetProperty(key, out var val)) { if (val is JsonElement element) return element.GetString(); return val.ToString(); } return null; } switch (submissionType) { case "simple_form": var name = GetFormValue("name") ?? "Unknown"; await client.Send($"Hi {name}, thanks for submitting the form!"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Form was submitted")); // More examples below default: return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Unknown submission type")); } } ``` Similarly, handling dialog submissions from rendered webpages is also possible: ```csharp // Add this case to the switch statement in OnTaskSubmit method case "webpage_dialog": var webName = GetFormValue("name") ?? "Unknown"; var email = GetFormValue("email") ?? "No email"; await client.Send($"Hi {webName}, thanks for submitting the form! We got that your email is {email}"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Form submitted successfully")); ``` ### Complete TaskSubmit Handler Example Here's the complete example showing how to handle multiple submission types: ```csharp using System.Text.Json; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Common.Logging; //... [TaskSubmit] public async Task OnTaskSubmit([Context] Tasks.SubmitActivity activity, [Context] IContext.Client client, [Context] ILogger log) { var data = activity.Value?.Data as JsonElement?; if (data == null) { log.Info("[TASK_SUBMIT] No data found in the activity value"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("No data found in the activity value")); } var submissionType = data.Value.TryGetProperty("submissiondialogtype", out var submissionTypeObj) && submissionTypeObj.ValueKind == JsonValueKind.String ? submissionTypeObj.ToString() : null; string? GetFormValue(string key) { if (data.Value.TryGetProperty(key, out var val)) { if (val is JsonElement element) return element.GetString(); return val.ToString(); } return null; } switch (submissionType) { case "simple_form": var name = GetFormValue("name") ?? "Unknown"; await client.Send($"Hi {name}, thanks for submitting the form!"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Form was submitted")); case "webpage_dialog": var webName = GetFormValue("name") ?? "Unknown"; var email = GetFormValue("email") ?? "No email"; await client.Send($"Hi {webName}, thanks for submitting the form! We got that your email is {email}"); return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Form submitted successfully")); default: return new Microsoft.Teams.Api.TaskModules.Response( new Microsoft.Teams.Api.TaskModules.MessageTask("Unknown submission type")); } } ``` --- ### Listening To Events # Listening To Events An **event** is a foundational concept in building agents — it represents something noteworthy happening either on Microsoft Teams or within your application. These events can originate from the user (e.g. installing or uninstalling your app, sending a message, submitting a form), or from your application server (e.g. startup, error in a handler). ```mermaid flowchart LR Teams["Teams"]:::less-interesting Server["App Server"]:::interesting AppEventHandlers["Event Handler (app.OnEvent())"]:::interesting Teams --> |Activity| Server Teams --> |Signed In| Server Teams --> |...other
incoming events| Server Server ---> |incoming
events| AppEventHandlers Server ---> |outgoing
events
| AppEventHandlers linkStyle 0,1,2,3,4 stroke:#b1650f,stroke-width:1px classDef interesting fill:#b1650f,stroke:#333,stroke-width:4px; ``` The Teams SDK makes it easy to subscribe to these events and respond appropriately. You can register event handlers to take custom actions when specific events occur — such as logging errors, triggering workflows, or sending follow-up messages. Here are the events that you can start building handlers for: | **Event Name** | **Description** | | ------------------- | ------------------------------------------------------------------------------ | | `start` | Triggered when your application starts. Useful for setup or boot-time logging. | | `signin` | Triggered during a sign-in flow via Teams. | | `error` | Triggered when an unhandled error occurs in your app. Great for diagnostics. | | `activity` | A catch-all for incoming Teams activities (messages, commands, etc.). | | `activity.response` | Triggered when your app sends a response to an activity. Useful for logging. | | `activity.sent` | Triggered when an activity is sent (not necessarily in response). | ### Example 1 We can subscribe to errors that occur in the app. ```csharp app.OnError((sender, @event) => { // do something with the error app.Logger.Info(@event.Exception.ToString()); }); ``` ### Example 2 When an activity is received, log its `JSON` payload. ```csharp app.OnActivity((sender, @event) => { app.Logger.Info(@event.Activity.ToString()); }); ``` --- ### MCP Client # MCP Client You are able to leverage other MCP servers that expose tools via the Streamable HTTP protocol as part of your application. This allows your AI agent to use remote tools to accomplish tasks. Install it to your application: ```bash dotnet add package Microsoft.Teams.Plugins.External.McpClient --prerelease ``` :::info Take a look at [Function calling](../function-calling) to understand how the `ChatPrompt` leverages tools to enhance the LLM's capabilities. MCP extends this functionality by allowing remote tools, that may or may not be developed or maintained by you, to be used by your application. ::: ## Remote MCP Server The first thing that's needed is access to a **remote** MCP server. MCP Servers (at present) come using two main types protocols: 1. StandardIO - This is a _local_ MCP server, which runs on your machine. An MCP client may connect to this server, and use standard input and outputs to communicate with it. Since our application is running remotely, this is not something that we want to use 2. Streamable HTTP/SSE - This is a _remote_ MCP server. An MCP client may send it requests and the server responds in the expected MCP protocol. For hooking up to your valid remote server, you will need to know the URL of the server, and if applicable, and keys that must be included as part of the header. ## MCP Client Plugin The `MCPClientPlugin` (from `Microsoft.Teams.Plugins.External.McpClient` package) integrates directly with the `ChatPrompt` object as a plugin. When the `ChatPrompt`'s `send` function is called, it calls the external MCP server and loads up all the tools that are available to it. Once loaded, it treats these tools like any functions that are available to the `ChatPrompt` object. If the LLM then decides to call one of these remote MCP tools, the MCP Client plugin will call the remote MCP server and return the result back to the LLM. The LLM can then use this result in its response. ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Plugins.AspNetCore.Extensions; using Microsoft.Teams.Plugins.External.McpClient; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.AddTeams(); WebApplication webApp = builder.Build(); OpenAIChatPrompt prompt = new( new OpenAIChatModel( model: "gpt-4o", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY")!), new ChatPromptOptions() .WithDescription("helpful assistant") .WithInstructions( "You are a helpful assistant that can help answer questions using Microsoft docs.", "You MUST use tool calls to do all your work.") ); prompt.Plugin(new McpClientPlugin().UseMcpServer("https://learn.microsoft.com/api/mcp")); App app = webApp.UseTeams(); app.OnMessage(async (context, cancellationToken) => { await context.Send(new TypingActivity(), cancellationToken); var result = await prompt.Send(context.Activity.Text); await context.Send(result.Content, cancellationToken); }); webApp.Run(); ``` ### Custom Headers Some MCP servers may require custom headers to be sent as part of the request. You can customize the headers when calling the `UseMcpServer` function: ```csharp new McpClientPlugin() .UseMcpServer("https://learn.microsoft.com/api/mcp", new McpClientPluginParams() { HeadersFactory = () => new Dictionary() { { "HEADER_KEY", "HEADER_VALUE" } } } ); ``` ### Transport Mode The client defaults to the Streamable HTTP transport. If the MCP server you are connecting to only supports SSE, set `Transport` to `McpClientTransport.Sse`: ```csharp new McpClientPlugin() .UseMcpServer("https:///mcp", new McpClientPluginParams() { Transport = McpClientTransport.Sse } ); ``` In this example, we augment the `ChatPrompt` with a remote MCP Server. :::note You can quickly set up an MCP server using [Azure Functions](https://techcommunity.microsoft.com/blog/appsonazureblog/build-ai-agent-tools-using-remote-mcp-with-azure-functions/4401059). ::: ![Animated image of user typing a prompt ('Tell me about Charizard') to DevTools Chat window and multiple paragraphs of information being returned.](/screenshots/mcp-client-pokemon.gif) In this example, our MCP server is a Pokemon API and our client knows how to call it. The LLM is able to call the `getPokemon` function exposed by the server and return the result back to the user. --- ### ⚙️ Settings # ⚙️ Settings You can add a settings page that allows users to configure settings for your app. The user can access the settings by right-clicking the app item in the compose box.
Settings This guide will show how to enable user access to settings, as well as setting up a page that looks like this: ![Settings Page](/screenshots/settings-page.png) ## 1. Update the Teams Manifest Set the `canUpdateConfiguration` field to `true` in the desired message extension under `composeExtensions`. ```json "composeExtensions": [ { "botId": "${{BOT_ID}}", "canUpdateConfiguration": true, ... } ] ``` ## 2. Serve the settings `html` page This is the code snippet for the settings `html` page: ```html Message Extension Settings

Message Extension Settings

``` Save it in the `index.html` file in the same folder as where your app is initialized. You can serve it by adding the following code to your app: ```csharp // In your startup configuration (Program.cs or Startup.cs) app.UseStaticFiles(); app.MapGet("/tabs/settings", async context => { var html = await File.ReadAllTextAsync("wwwroot/settings.html"); context.Response.ContentType = "text/html"; await context.Response.WriteAsync(html); }); ``` :::note This will serve the HTML page to the `$BOT_ENDPOINT/tabs/settings` endpoint as a tab. See [Tabs Guide](../tabs) to learn more. ::: ## 3. Specify the URL to the settings page To enable the settings page, your app needs to handle the `message.ext.query-settings-url` activity that Teams sends when a user right-clicks the app in the compose box. Your app must respond with the URL to your settings page. Here's how to implement this: ```csharp using Microsoft.Teams.Api.Cards; using Microsoft.Teams.Cards; [MessageExtension.QuerySettingsUrl] public Microsoft.Teams.Api.MessageExtensions.Response OnMessageExtensionQuerySettingsUrl( [Context] Microsoft.Teams.Api.Activities.Invokes.MessageExtensions.QuerySettingsUrlActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) { log.Info("[MESSAGE_EXT_QUERY_SETTINGS_URL] Settings URL query received"); // Get user settings (this could come from a database or user store) var selectedOption = ""; // Default or retrieve from user preferences var botEndpoint = Environment.GetEnvironmentVariable("BOT_ENDPOINT") ?? "https://your-bot-endpoint.com"; var settingsUrl = $"{botEndpoint}/tabs/settings?selectedOption={Uri.EscapeDataString(selectedOption)}"; var settingsAction = new CardAction { Type = CardActionType.OpenUrl, Title = "Settings", Value = settingsUrl }; var suggestedActions = new Microsoft.Teams.Api.MessageExtensions.SuggestedActions { Actions = new List { settingsAction } }; var result = new Microsoft.Teams.Api.MessageExtensions.Result { Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Config, SuggestedActions = suggestedActions }; return new Microsoft.Teams.Api.MessageExtensions.Response { ComposeExtension = result }; } ``` ## 4. Handle Form Submission When a user submits the settings form, Teams sends a `message.ext.setting` activity with the selected option in the `activity.value.state` property. Handle it to save the user's selection: ```csharp [MessageExtension.Setting] public Microsoft.Teams.Api.MessageExtensions.Response OnMessageExtensionSetting( [Context] Microsoft.Teams.Api.Activities.Invokes.MessageExtensions.SettingActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) { log.Info("[MESSAGE_EXT_SETTING] Settings submission received"); var state = activity.Value?.State; log.Info($"[MESSAGE_EXT_SETTING] State: {state}"); if (state == "CancelledByUser") { log.Info("[MESSAGE_EXT_SETTING] User cancelled settings"); return CreateEmptyResult(); } var selectedOption = state; log.Info($"[MESSAGE_EXT_SETTING] Selected option: {selectedOption}"); // Here you would typically save the user's settings to a database or user store // SaveUserSettings(activity.From.Id, selectedOption); // Return empty result to close the settings dialog return CreateEmptyResult(); } // Helper method to create empty result private static Microsoft.Teams.Api.MessageExtensions.Response CreateEmptyResult() { return new Microsoft.Teams.Api.MessageExtensions.Response { ComposeExtension = new Microsoft.Teams.Api.MessageExtensions.Result { Type = Microsoft.Teams.Api.MessageExtensions.ResultType.Result, AttachmentLayout = Microsoft.Teams.Api.Attachment.Layout.List, Attachments = new List() } }; } ``` --- ### 📖 Message Extensions # 📖 Message Extensions Message extensions (or Compose Extensions) allow your application to hook into messages that users can send or perform actions on messages that users have already sent. They enhance user productivity by providing quick access to information and actions directly within the Teams interface. Users can search or initiate actions from the compose message area, the command box, or directly from a message, with the results returned as richly formatted cards that make information more accessible and actionable. There are two types of message extensions: [API-based](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/api-based-overview) and [Bot-based](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/build-bot-based-message-extension?tabs=search-commands). API-based message extensions use an OpenAPI specification that Teams directly queries, requiring no additional application to build or maintain, but offering less customization. Bot-based message extensions require building an application to handle queries, providing more flexibility and customization options. This SDK supports bot-based message extensions only. ## Resources - [What are message extensions?](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=desktop) --- ### Function / Tool calling # Function / Tool calling It's possible to hook up functions that the LLM can decide to call if it thinks it can help with the task at hand. This is done by registering functions with a `ChatPrompt` using the `.Function()` method. ```mermaid sequenceDiagram participant User participant ChatPrompt participant LLM participant Function-PokemonSearch participant ExternalAPI User->>ChatPrompt: send("Tell me about Pikachu") ChatPrompt->>LLM: Provide instructions, message, and available functions LLM->>ChatPrompt: Decide to call `pokemon_search` with pokemon_name="Pikachu" ChatPrompt->>Function-PokemonSearch: Execute with pokemon_name Function-PokemonSearch->>ExternalAPI: fetch Pokemon data ExternalAPI-->>Function-PokemonSearch: return Pokemon info Function-PokemonSearch-->>ChatPrompt: return result ChatPrompt->>LLM: Send function result(s) LLM-->>ChatPrompt: Final user-facing response ChatPrompt-->>User: send(result.content) ``` ## Single Function Example Here's a complete example showing how to create a Pokemon search function that the LLM can call. ```csharp using System.Text.Json; using Microsoft.Teams.AI.Annotations; using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; using Microsoft.Teams.AI.Templates; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; /// /// Handle Pokemon search using PokeAPI /// public static async Task PokemonSearchFunction([Param("pokemon_name")] string pokemonName) { try { using var client = new HttpClient(); var response = await client.GetAsync($"https://pokeapi.co/api/v2/pokemon/{pokemonName.ToLower()}"); if (!response.IsSuccessStatusCode) { return $"Pokemon '{pokemonName}' not found"; } var json = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(json); var root = data.RootElement; var name = root.GetProperty("name").GetString(); var height = root.GetProperty("height").GetInt32(); var weight = root.GetProperty("weight").GetInt32(); var types = root.GetProperty("types") .EnumerateArray() .Select(t => t.GetProperty("type").GetProperty("name").GetString()) .ToList(); return $"Pokemon {name}: height={height}, weight={weight}, types={string.Join(", ", types)}"; } catch (Exception ex) { return $"Error searching for Pokemon: {ex.Message}"; } } /// /// Handle single function calling - Pokemon search /// public static async Task HandlePokemonSearch(OpenAIChatModel model, IContext context) { var prompt = new OpenAIChatPrompt(model, new ChatPromptOptions { Instructions = new StringTemplate("You are a helpful assistant that can look up Pokemon for the user.") }); // Register the pokemon search function prompt.Function( "pokemon_search", "Search for pokemon information including height, weight, and types", PokemonSearchFunction ); var result = await prompt.Send(context.Activity.Text); if (result.Content != null) { var message = new MessageActivity { Text = result.Content, }.AddAIGenerated(); await context.Send(message); } else { await context.Reply("Sorry I could not find that pokemon"); } } ``` This approach uses attributes to declare prompts and functions, providing clean separation of concerns. **Create a Prompt Class:** ```csharp using System.Text.Json; using Microsoft.Teams.AI.Annotations; namespace Samples.AI.Prompts; [Prompt] [Prompt.Description("Pokemon search assistant")] [Prompt.Instructions("You are a helpful assistant that can look up Pokemon for the user.")] public class PokemonPrompt { [Function] [Function.Description("Search for pokemon information including height, weight, and types")] public async Task PokemonSearch([Param("pokemon_name")] string pokemonName) { try { using var httpClient = new HttpClient(); var response = await httpClient.GetAsync($"https://pokeapi.co/api/v2/pokemon/{pokemonName.ToLower()}"); if (!response.IsSuccessStatusCode) { return $"Pokemon '{pokemonName}' not found"; } var json = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(json); var root = data.RootElement; var name = root.GetProperty("name").GetString(); var height = root.GetProperty("height").GetInt32(); var weight = root.GetProperty("weight").GetInt32(); var types = root.GetProperty("types") .EnumerateArray() .Select(t => t.GetProperty("type").GetProperty("name").GetString()) .ToList(); return $"Pokemon {name}: height={height}, weight={weight}, types={string.Join(", ", types)}"; } catch (Exception ex) { return $"Error searching for Pokemon: {ex.Message}"; } } } ``` **Usage in Program.cs:** ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Api.Activities; // Create the AI model var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); // Use the prompt with OpenAIChatPrompt.From() teamsApp.OnMessage(async (context, cancellationToken) => { var prompt = OpenAIChatPrompt.From(aiModel, new Samples.AI.Prompts.PokemonPrompt()); var result = await prompt.Send(context.Activity.Text); if (!string.IsNullOrEmpty(result.Content)) { await context.Send(new MessageActivity { Text = result.Content }.AddAIGenerated(), cancellationToken); } else { await context.Reply("Sorry I could not find that pokemon", cancellationToken); } }); ``` ### How It Works 1. **Function Definition**: The function is defined as a regular C# method with parameters decorated with the `[Param]` attribute 2. **Automatic Schema Generation**: The SDK automatically generates the JSON schema for the function parameters using reflection 3. **Function Registration**: - **Imperative Approach**: The `.Function()` method registers the function with the prompt, providing the name, description, and handler - **Declarative Approach**: The `[Function]` attribute automatically registers methods when using `OpenAIChatPrompt.From()` 4. **Automatic Invocation**: When the LLM decides to call the function, it automatically: - Parses the function call arguments - Validates them against the schema - Invokes the handler - Returns the result back to the LLM ## Multiple Functions Additionally, for complex scenarios, you can add multiple functions to the `ChatPrompt`. The LLM will then decide which function(s) to call based on the context of the conversation. ```csharp /// /// Get user location (mock) /// public static string GetLocationFunction() { var locations = new[] { "Seattle", "San Francisco", "New York" }; var random = new Random(); var location = locations[random.Next(locations.Length)]; return location; } /// /// Get weather for location (mock) /// public static string GetWeatherFunction([Param] string location) { var weatherByLocation = new Dictionary { ["Seattle"] = (65, "sunny"), ["San Francisco"] = (60, "foggy"), ["New York"] = (75, "rainy") }; if (!weatherByLocation.TryGetValue(location, out var weather)) { return "Sorry, I could not find the weather for that location"; } return $"The weather in {location} is {weather.Condition} with a temperature of {weather.Temperature}°F"; } /// /// Handle multiple function calling - location then weather /// public static async Task HandleMultipleFunctions(OpenAIChatModel model, IContext context) { var prompt = new OpenAIChatPrompt(model, new ChatPromptOptions { Instructions = new StringTemplate("You are a helpful assistant that can help the user get the weather. First get their location, then get the weather for that location.") }); // Register both functions prompt.Function( "get_user_location", "Gets the location of the user", GetLocationFunction ); prompt.Function( "weather_search", "Search for weather at a specific location", GetWeatherFunction ); var result = await prompt.Send(context.Activity.Text); if (result.Content != null) { var message = new MessageActivity { Text = result.Content, }.AddAIGenerated(); await context.Send(message); } else { await context.Reply("Sorry I could not figure it out"); } } ``` **Create a Prompt Class:** ```csharp using Microsoft.Teams.AI.Annotations; namespace Samples.AI.Prompts; [Prompt] [Prompt.Description("Weather assistant")] [Prompt.Instructions("You are a helpful assistant that can help the user get the weather. First get their location, then get the weather for that location.")] public class WeatherPrompt { [Function] [Function.Description("Gets the location of the user")] public string GetUserLocation() { var locations = new[] { "Seattle", "San Francisco", "New York" }; var random = new Random(); return locations[random.Next(locations.Length)]; } [Function] [Function.Description("Search for weather at a specific location")] public string WeatherSearch([Param] string location) { var weatherByLocation = new Dictionary { ["Seattle"] = (65, "sunny"), ["San Francisco"] = (60, "foggy"), ["New York"] = (75, "rainy") }; if (!weatherByLocation.TryGetValue(location, out var weather)) { return "Sorry, I could not find the weather for that location"; } return $"The weather in {location} is {weather.Condition} with a temperature of {weather.Temperature}°F"; } } ``` **Usage in Program.cs:** ```csharp using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Api.Activities; // Create the AI model var aiModel = new OpenAIChatModel(azureOpenAIModel, azureOpenAI); // Use the prompt with OpenAIChatPrompt.From() teamsApp.OnMessage(async (context, cancellationToken) => { var prompt = OpenAIChatPrompt.From(aiModel, new Samples.AI.Prompts.WeatherPrompt()); var result = await prompt.Send(context.Activity.Text); if (!string.IsNullOrEmpty(result.Content)) { await context.Send(new MessageActivity { Text = result.Content }.AddAIGenerated(), cancellationToken); } else { await context.Reply("Sorry I could not figure it out", cancellationToken); } }); ``` ### Multiple Function Execution Flow When you register multiple functions: 1. The LLM receives information about all available functions 2. Based on the user's query, it decides which function(s) to call and in what order 3. For example, asking "What's the weather?" might trigger: - First: `get_user_location()` to determine where the user is - Then: `weather_search(location)` to get the weather for that location 4. The LLM combines all function results to generate the final response :::tip The LLM can call functions sequentially - using the output of one function as input to another - without any additional configuration. This makes it powerful for complex, multi-step workflows. ::: --- ### Handling Multi-Step Forms # Handling Multi-Step Forms Dialogs can become complex yet powerful with multi-step forms. These forms can alter the flow of the survey depending on the user's input or customize subsequent steps based on previous answers. ## Creating the Initial Dialog Start off by sending an initial card in the `TaskFetch` event. ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Cards; //... private static Response CreateMultiStepFormDialog() { var cardJson = """ { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "This is a multi-step form", "size": "Large", "weight": "Bolder" }, { "type": "Input.Text", "id": "name", "label": "Name", "placeholder": "Enter your name", "isRequired": true } ], "actions": [ { "type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "webpage_dialog_step_1"} } ] } """; var dialogCard = JsonSerializer.Deserialize(cardJson) ?? throw new InvalidOperationException("Failed to deserialize multi-step form card"); var taskInfo = new TaskInfo { Title = "Multi-step Form Dialog", Card = new Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = dialogCard } }; return new Response(new ContinueTask(taskInfo)); } ``` Then in the submission handler, you can choose to `continue` the dialog with a different card. ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Cards; //... // Add these cases to your OnTaskSubmit method case "webpage_dialog_step_1": var nameStep1 = GetFormValue("name") ?? "Unknown"; var nextStepCardJson = $$""" { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "Email", "size": "Large", "weight": "Bolder" }, { "type": "Input.Text", "id": "email", "label": "Email", "placeholder": "Enter your email", "isRequired": true } ], "actions": [ { "type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "webpage_dialog_step_2", "name": "{{nameStep1}}"} } ] } """; var nextStepCard = JsonSerializer.Deserialize(nextStepCardJson) ?? throw new InvalidOperationException("Failed to deserialize next step card"); var nextStepTaskInfo = new TaskInfo { Title = $"Thanks {nameStep1} - Get Email", Card = new Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = nextStepCard } }; return new Response(new ContinueTask(nextStepTaskInfo)); case "webpage_dialog_step_2": var nameStep2 = GetFormValue("name") ?? "Unknown"; var emailStep2 = GetFormValue("email") ?? "No email"; await client.Send($"Hi {nameStep2}, thanks for submitting the form! We got that your email is {emailStep2}"); return new Response(new MessageTask("Multi-step form completed successfully")); ``` ### Complete Multi-Step Form Handler Here's the complete example showing how to handle a multi-step form: ```csharp using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.TaskModules; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; using Microsoft.Teams.Cards; using Microsoft.Teams.Common.Logging; //... [TaskSubmit] public async Task OnTaskSubmit([Context] Tasks.SubmitActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[TASK_SUBMIT] Task submit request received"); var data = activity.Value?.Data as JsonElement?; if (data == null) { log.Info("[TASK_SUBMIT] No data found in the activity value"); return new Response(new MessageTask("No data found in the activity value")); } var submissionType = data.Value.TryGetProperty("submissiondialogtype", out var submissionTypeObj) && submissionTypeObj.ValueKind == JsonValueKind.String ? submissionTypeObj.ToString() : null; log.Info($"[TASK_SUBMIT] Submission type: {submissionType}"); string? GetFormValue(string key) { if (data.Value.TryGetProperty(key, out var val)) { if (val is JsonElement element) return element.GetString(); return val.ToString(); } return null; } switch (submissionType) { case "webpage_dialog_step_1": var nameStep1 = GetFormValue("name") ?? "Unknown"; var nextStepCardJson = $$""" { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "Email", "size": "Large", "weight": "Bolder" }, { "type": "Input.Text", "id": "email", "label": "Email", "placeholder": "Enter your email", "isRequired": true } ], "actions": [ { "type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "webpage_dialog_step_2", "name": "{{nameStep1}}"} } ] } """; var nextStepCard = JsonSerializer.Deserialize(nextStepCardJson) ?? throw new InvalidOperationException("Failed to deserialize next step card"); var nextStepTaskInfo = new TaskInfo { Title = $"Thanks {nameStep1} - Get Email", Card = new Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = nextStepCard } }; return new Response(new ContinueTask(nextStepTaskInfo)); case "webpage_dialog_step_2": var nameStep2 = GetFormValue("name") ?? "Unknown"; var emailStep2 = GetFormValue("email") ?? "No email"; await client.Send($"Hi {nameStep2}, thanks for submitting the form! We got that your email is {emailStep2}"); return new Response(new MessageTask("Multi-step form completed successfully")); default: return new Response(new MessageTask("Unknown submission type")); } } ``` --- ### In-Depth Guides # In-Depth Guides This documentation covers advanced features and capabilities of the Teams SDK in C#. This section provides comprehensive technical guides for integration with useful Teams features. Learn how to implement AI-powered bots, create adaptive cards, manage authentication flows, and build sophisticated message extensions. Each guide includes practical examples and best practices for production applications. --- ### 🔒 User Authentication # 🔒 User Authentication At times agents must access secured online resources on behalf of the user, such as checking email, checking on flight status, or placing an order. To enable this, the user must authenticate their identity and grant consent for the application to access these resources. This process results in the application receiving a token, which the application can then use to access the permitted resources on the user's behalf. :::info This is an advanced guide. It is highly recommended that you are familiar with [Teams Core Concepts](/teams/core-concepts) before attempting this guide. ::: :::warning User authentication does not work with the developer tools setup. You have to run the app in Teams. Follow these [instructions](/typescript/getting-started/running-in-teams#debugging-in-teams) to run your app in Teams. ::: :::info It is possible to authenticate the user into [other auth providers](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-identity-providers?view=azure-bot-service-4.0&tabs=adv2%2Cga2#other-identity-providers) like Facebook, Github, Google, Dropbox, and so on. ::: Once you have configured your Azure Bot resource OAuth settings, as described in the [official documentation](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0), add the following code to your `App`: ## Project Setup ### Create an app with the `graph` template :::tip Skip this step if you want to add the auth configurations to an existing app. ::: Use your terminal to run the following command: ```sh npx @microsoft/teams.cli@latest new csharp oauth-app --template graph ``` ### Add Agents Toolkit auth configuration Open your terminal with the project folder set as the current working directory and run the following command: ```sh npx @microsoft/teams.cli config add atk.oauth ``` The `atk.oauth` configuration is a basic setup for Agents Toolkit along with configurations to authenticate the user with Microsoft Entra ID to access Microsoft Graph APIs. This [CLI](/developer-tools/cli) command adds configuration files required by Agents Toolkit, including: - Azure Application Entra ID manifest file `aad.manifest.json`. - Azure bicep files to provision Azure bot in `infra/` folder. :::info Agents Toolkit, in the debugging flow, will deploy the `aad.manifest.json` and `infra/azure.local.bicep` file to provision the Application Entra ID and Azure bot with oauth configurations. ::: ## Configure the OAuth connection ```cs var builder = WebApplication.CreateBuilder(args); var appBuilder = App.Builder() .AddOAuth("graph"); builder.AddTeams(appBuilder); var app = builder.Build(); var teams = app.UseTeams(); ``` :::tip Make sure you use the same name you used when creating the OAuth connection in the Azure Bot Service resource. ::: :::note In many templates, `graph` is the default name of the OAuth connection, but you can change that by supplying a different connection name in your app configuration. ::: ## Signing In :::note This uses the Single Sign-On (SSO) authentication flow. To learn more about all the available flows and their differences see the [official documentation](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0). ::: You must call the `signin` method inside your route handler, for example: to signin when receiving the `/signin` message: ```cs teams.OnMessage("/signin", async (context, cancellationToken) => { if (context.IsSignedIn) { await context.Send("you are already signed in!", cancellationToken); return; } else { await context.SignIn(cancellationToken); } }); ``` ## Subscribe to the SignIn event You can subscribe to the `signin` event, that will be triggered once the OAuth flow completes. ```cs teams.OnSignIn(async (_, teamsEvent, cancellationToken) => { var context = teamsEvent.Context; await context.Send($"Signed in using OAuth connection {context.ConnectionName}. Please type **/whoami** to see your profile or **/signout** to sign out.", cancellationToken); }); ``` ## Start using the graph client From this point, you can use the `IsSignedIn` flag and the `userGraph` client to query graph, for example to reply to the `/whoami` message, or in any other route. :::note The default OAuth configuration requests the `User.ReadBasic.All` permission. It is possible to request other permissions by modifying the App Registration for the bot on Azure. ::: ```cs teams.OnMessage("/whoami", async (context, cancellationToken) => { if (!context.IsSignedIn) { await context.Send("you are not signed in!. Please type **/signin** to sign in", cancellationToken); return; } var me = await context.GetUserGraphClient().Me.GetAsync(); await context.Send($"user \"{me!.DisplayName}\" signed in.", cancellationToken); }); teams.OnMessage(async (context, cancellationToken) => { if (context.IsSignedIn) { await context.Send($"You said : {context.Activity.Text}. Please type **/whoami** to see your profile or **/signout** to sign out.", cancellationToken); } else { await context.Send($"You said : {context.Activity.Text}. Please type **/signin** to sign in.", cancellationToken); } }); ``` ## Signing Out You can signout by calling the `signout` method, this will remove the token from the User Token service cache ```cs teams.OnMessage("/signout", async (context, cancellationToken) => { if (!context.IsSignedIn) { await context.Send("you are not signed in!", cancellationToken); return; } await context.SignOut(cancellationToken); await context.Send("you have been signed out!", cancellationToken); }); ``` ## Handling Sign-In Failures When using SSO, if the token exchange fails Teams sends a `signin/failure` invoke activity to your app. The SDK includes a built-in default handler that logs a warning with actionable troubleshooting guidance. You can optionally register your own handler to customize the behavior: ```cs teams.OnSignInFailure(async (context, cancellationToken) => { var failure = context.Activity.Value; Console.WriteLine($"Sign-in failed: {failure?.Code} - {failure?.Message}"); await context.Send("Sign-in failed.", cancellationToken); }); ``` :::tip The most common failure codes are `installedappnotfound` (bot app not installed for the user) and `resourcematchfailed` (Token Exchange URL doesn't match the Application ID URI). See [SSO Setup - Troubleshooting](/teams/user-authentication/sso-setup#troubleshooting-sso) for a full list of failure codes and troubleshooting steps. ::: ## Resources [User Authentication Basics](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0) --- ### 🔗 Link unfurling # 🔗 Link unfurling Link unfurling lets your app respond when users paste URLs into Teams. When a URL from your registered domain is pasted, your app receives the URL and can return a card with additional information or actions. This works like a search command where the URL acts as the search term. :::note Users can use link unfurling even before they discover or install your app in Teams. This is called [Zero install link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages#zero-install-for-link-unfurling). In this scenario, your app will receive a `message.ext.anon-query-link` activity instead of the usual `message.ext.query-link`. ::: ## Setting up your Teams app manifest ### Configure message handlers ```json "composeExtensions": [ { "botId": "${{BOT_ID}}", "messageHandlers": [ { "type": "link", "value": { "domains": [ "www.test.com" ] } } ] } ] ``` ### How link unfurling works When a user pastes a URL from your registered domain (like `www.test.com`) into the Teams compose box, your app will receive a notification. Your app can then respond by returning an adaptive card that displays a preview of the linked content. This preview card appears before the user sends their message in the compose box, allowing them to see how the link will be displayed to others. ```mermaid flowchart TD A1["User pastes a URL (e.g., www\.test\.com) in Teams compose box"] B1([Microsoft Teams]) C1["Your App"] D1["Adaptive Card Preview"] A1 --> B1 B1 -->|Sends URL paste notification| C1 C1 -->|Returns card and preview| B1 B1 --> D1 %% Styling for readability and compatibility style B1 fill:#2E86AB,stroke:#1B4F72,stroke-width:2px,color:#ffffff style C1 fill:#28B463,stroke:#1D8348,stroke-width:2px,color:#ffffff style D1 fill:#F39C12,stroke:#D68910,stroke-width:2px,color:#ffffff ``` ## Implementing link unfurling ### Handle the query link event Handle link unfurling when a URL from your registered domain is submitted into the Teams compose box. ```csharp using Microsoft.Teams.Api.Activities.Invokes.MessageExtensions; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Apps.Annotations; //... [MessageExtension.QueryLink] public Response OnMessageExtensionQueryLink( [Context] QueryLinkActivity activity, [Context] IContext.Client client, [Context] ILogger log) { log.Info("[MESSAGE_EXT_QUERY_LINK] Link unfurling received"); var url = activity.Value?.Url; log.Info($"[MESSAGE_EXT_QUERY_LINK] URL: {url}"); if (string.IsNullOrEmpty(url)) { return CreateErrorResponse("No URL provided"); } return CreateLinkUnfurlResponse(url, log); } ``` ### Create the unfurl card `CreateLinkUnfurlResponse()` method ```csharp using Microsoft.Teams.Api; using Microsoft.Teams.Api.MessageExtensions; using Microsoft.Teams.Cards; //... private static Response CreateLinkUnfurlResponse(string url, ILogger log) { var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock("Link Preview") { Weight = TextWeight.Bolder, Size = TextSize.Medium }, new TextBlock($"URL: {url}") { IsSubtle = true, Wrap = true }, new TextBlock("This is a preview of the linked content generated by the message extension.") { Wrap = true, Size = TextSize.Small } } }; var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment { ContentType = new ContentType("application/vnd.microsoft.card.adaptive"), Content = card }; return new Response { ComposeExtension = new Result { Type = ResultType.Result, AttachmentLayout = Layout.List, Attachments = new List { attachment } } }; } // Helper method to create error responses private static Response CreateErrorResponse(string message) { return new Response { ComposeExtension = new Result { Type = ResultType.Message, Text = message } }; } ``` ### User experience flow The link unfurling response includes both a full adaptive card and a preview card. The preview card appears in the compose box when a user pastes a URL: ![Screenshot showing a preview card for an unfurled URL in the Teams compose box.](/screenshots/link-unfurl-preview.png) The user can expand the preview card by clicking on the _expand_ button on the top right. ![Screenshot of Teams compose box with an outline around the unfurled link card labeled 'Adaptive Card'.](/screenshots/link-unfurl-card.png) The user can then choose to send either the preview or the full adaptive card as a message. ## Resources - [Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages) - [Zero install link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages#zero-install-for-link-unfurling) --- ### Keeping State # Keeping State By default, LLMs are not stateful. This means that they do not remember previous messages or context when generating a response. It's common practice to keep state of the conversation history in your application and pass it to the LLM each time you make a request. By default, the `ChatPrompt` instance will create a temporary in-memory store to keep track of the conversation history. This is beneficial when you want to use it to generate an LLM response, but not persist the conversation history. But in other cases, you may want to keep the conversation history :::warning By reusing the same `ChatPrompt` class instance across multiple conversations will lead to the conversation history being shared across all conversations. Which is usually not the desired behavior. ::: To avoid this, you need to get messages from your persistent (or in-memory) store and pass it in to the `ChatPrompt`. :::note The `ChatPrompt` class will modify the messages object that's passed into it. So if you want to manually manage it, you need to make a copy of the messages object before passing it in. ::: ## State Initialization Here's how to initialize and manage conversation state for multiple conversations: ```csharp using Microsoft.Teams.AI; using Microsoft.Teams.AI.Messages; using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.AI.Prompts; using Microsoft.Teams.AI.Templates; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Apps; // Simple in-memory store for conversation histories // In your application, it may be a good idea to use a more // persistent store backed by a database or other storage solution private static readonly Dictionary ConversationStore = new(); /// /// Get or create conversation memory for a specific conversation /// public static List GetOrCreateConversationMemory(string conversationId) { if (!ConversationStore.ContainsKey(conversationId)) { ConversationStore[conversationId] = new List(); } return ConversationStore[conversationId]; } /// /// Clear memory for a specific conversation /// public static Task ClearConversationMemory(string conversationId) { if (ConversationStore.TryGetValue(conversationId, out var messages)) { var messageCount = messages.Count; messages.Clear(); } return Task.CompletedTask; } ``` ## Usage Example ```csharp /// /// Example of stateful conversation handler that maintains conversation history /// public static async Task HandleStatefulConversation(OpenAIChatModel model, IContext context) { // Retrieve existing conversation memory or initialize new one var messages = GetOrCreateConversationMemory(context.Activity.Conversation.Id); // Create prompt with conversation-specific memory var prompt = new OpenAIChatPrompt(model, new ChatPromptOptions { Instructions = new StringTemplate("You are a helpful assistant that remembers our previous conversation.") }); // Send with existing messages as context var options = new IChatPrompt.RequestOptions { Messages = messages }; var result = await prompt.Send(context.Activity.Text, options); if (result.Content != null) { var message = new MessageActivity { Text = result.Content, }.AddAIGenerated(); await context.Send(message); // Update conversation history messages.Add(UserMessage.Text(context.Activity.Text)); messages.Add(new ModelMessage(result.Content)); } else { await context.Reply("I did not generate a response."); } } ``` ### Usage in your application ```csharp teamsApp.OnMessage(async (context, cancellationToken) => { await HandleStatefulConversation(aiModel, context); }); ``` #### How It Works 1. **Conversation Store**: A dictionary maps conversation IDs to their message histories 2. **Per-Conversation Memory**: Each conversation gets its own isolated message list 3. **Request Options**: Pass the message history via `RequestOptions.Messages` when calling `Send()` 4. **Automatic Updates**: After receiving a response, manually add both the user message and AI response to the store 5. **Persistence**: The conversation history persists across multiple user interactions within the same conversation :::tip The `ChatPrompt.Send()` method does **not** automatically update the messages you pass in via `RequestOptions`. You must manually add the user message and AI response to your conversation store after each interaction. ::: :::note In a production application, consider using a more robust storage solution like Azure Cosmos DB, SQL Server, or Redis instead of an in-memory dictionary. This ensures conversation history persists across application restarts and scales across multiple instances. ::: ![Stateful Chat Example](/screenshots/stateful-chat-example.png) --- ### Sending Messages # Sending Messages Sending messages is a core part of an agent's functionality. With all activity handlers, a `send` method is provided which allows your handlers to send a message back to the user to the relevant conversation. ```csharp app.OnMessage(async (context, cancellationToken) => { await context.Send($"you said: {context.activity.Text}", cancellationToken); }); ``` In the above example, the handler gets a `message` activity, and uses the `send` method to send a reply to the user. ```csharp app.OnVerifyState(async (context, cancellationToken) => { await context.Send("You have successfully signed in!", cancellationToken); }); ``` You are not restricted to only replying to `message` activities. In the above example, the handler is listening to `SignIn.VerifyState` events, which are sent when a user successfully signs in. :::tip This shows an example of sending a text message. Additionally, you are able to send back things like [adaptive cards](../../in-depth-guides/adaptive-cards) by using the same `send` method. Look at the [adaptive card](../../in-depth-guides/adaptive-cards) section for more details. ::: ## Streaming You may also stream messages to the user which can be useful for long messages, or AI generated messages. The SDK makes this simple for you by providing a `stream` function which you can use to send messages in chunks. ```csharp app.OnMessage(async (context, cancellationToken) => { context.Stream.Emit("hello"); context.Stream.Emit(", "); context.Stream.Emit("world!"); // result message: "hello, world!" return Task.CompletedTask; }); ``` :::note Streaming is currently only supported in 1:1 conversations, not group chats or channels ::: ![Animated image showing agent response text incrementally appearing in the chat window.](/screenshots/streaming-chat.gif) ## @Mention Sending a message at `@mentions` a user is as simple including the details of the user using the `AddMention` method ```csharp app.OnMessage(async (context, cancellationToken) => { await context.Send(new MessageActivity("hi!").AddMention(activity.From), cancellationToken); }); ``` ## Targeted Messages :::info[Preview] Targeted messages are currently in preview. ::: Targeted messages, also known as ephemeral messages, are delivered to a specific user in a shared conversation. From a single user's perspective, they appear as regular inline messages in a conversation. Other participants won't see these messages, making them useful for authentication flows, help or error responses, personal reminders, or sharing contextual information without cluttering the group conversation. To send a targeted message when responding to an incoming activity, use the `WithRecipient` method with the recipient account and set the targeting flag to true. ```csharp app.OnMessage(async (context, cancellationToken) => { // Using WithRecipient with isTargeted=true explicitly targets the specified recipient await context.Send( new MessageActivity("This message is only visible to you!") .WithRecipient(context.Activity.From, isTargeted: true), cancellationToken ); }); ``` ### Targeted messages in preview :::tip[.NET] In .NET, targeted message APIs are marked with `[Experimental("ExperimentalTeamsTargeted")]` and will produce a compiler error until you opt in. Suppress the diagnostic inline with `#pragma warning disable ExperimentalTeamsTargeted` or project-wide in your `.csproj`: ```xml $(NoWarn);ExperimentalTeamsTargeted ``` ::: ## Reactions :::info[Preview] Reactions are currently in preview. ::: Reactions allow your agent to add or remove emoji reactions on messages in a conversation. The reactions client is available via the API client. ### Reactions in preview :::tip[.NET] In .NET, reaction APIs are marked with `[Experimental("ExperimentalTeamsReactions")]` and will produce a compiler error until you opt in. Suppress the diagnostic inline with `#pragma warning disable ExperimentalTeamsReactions` or project-wide in your `.csproj`: ```xml $(NoWarn);ExperimentalTeamsReactions ``` ::: --- ### 🤖 AI # 🤖 AI The AI packages in this SDK are designed to make it easier to build applications with LLMs. The `Microsoft.Teams.AI` has two main components: ## 📦 Prompts A `Prompt` is the component that orchestrates everything, it handles state management, function definitions, and invokes the model/template when needed. This layer abstracts many of the complexities of the Models to provide a common interface. ## 🧠 Models A `Model` is the component that interfaces with the LLM, being given some `input` and returning the `output`. This layer deals with any of the nuances of the particular Models being used. It is in the model implementation that the individual LLM features (i.e. streaming/tools etc.) are made compatible with the more general features of the `Microsoft.Teams.AI`. :::note You are not restricted to use the `Microsoft.Teams.AI` to build your Teams Agent applications. You can use models directly if you choose. These packages are there to simplify the interactions with the models and Teams. ::: --- ### App Authentication # App Authentication Your application needs to authenticate to send messages to Teams as your bot. Authentication allows your app service to certify that it is _allowed_ to send messages as your Azure Bot. :::info Azure Setup Required Before configuring your application, you must first set up authentication in Azure. See the [App Authentication Setup](/teams/app-authentication) guide for instructions on creating the necessary Azure resources. ::: ## Authentication Methods There are 3 main ways of authenticating: 1. **Client Secret** - Simple password-based authentication using a client secret 2. **User Managed Identity** - Passwordless authentication using Azure managed identities 3. **Federated Identity Credentials** - Advanced identity federation using managed identities ## Configuration Reference The Teams SDK automatically detects which authentication method to use based on the environment variables you set: | CLIENT_ID | CLIENT_SECRET | MANAGED_IDENTITY_CLIENT_ID | Authentication Method | |-|-|-|-| | not_set | | | No-Auth (local development only) | | set | set | | Client Secret | | set | not_set | | User Managed Identity | | set | not_set | set (same as CLIENT_ID) | User Managed Identity | | set | not_set | set (different from CLIENT_ID) | Federated Identity Credentials (UMI) | | set | not_set | "system" | Federated Identity Credentials (System Identity) | ## Client Secret The simplest authentication method using a password-like secret. ### Setup First, complete the [Client Secret Setup](/teams/app-authentication/client-secret) in Azure Portal or Azure CLI. ### Configuration Set the following environment variables in your application: - `CLIENT_ID`: Your Application (client) ID - `CLIENT_SECRET`: The client secret value you created - `TENANT_ID`: The tenant id where your bot is registered ```env CLIENT_ID=your-client-id-here CLIENT_SECRET=your-client-secret-here TENANT_ID=your-tenant-id ``` The SDK will automatically use Client Secret authentication when both `CLIENT_ID` and `CLIENT_SECRET` are provided. ## User Managed Identity Passwordless authentication using Azure managed identities - no secrets to rotate or manage. ### Setup First, complete the [User Managed Identity Setup](/teams/app-authentication/user-managed-identity) in Azure Portal or Azure CLI. ### Configuration :::note The environment file approach is not yet supported for C#. You need to configure authentication programmatically in your code. ::: In your `Program.cs`, replace the initialization: ```csharp var builder = WebApplication.CreateBuilder(args); builder.AddTeams(); ``` with the following code to enable User Assigned Managed Identity authentication: ```csharp var builder = WebApplication.CreateBuilder(args); Func createTokenFactory = async (string[] scopes, string? tenantId) => { var clientId = Environment.GetEnvironmentVariable("CLIENT_ID"); var managedIdentityCredential = new ManagedIdentityCredential(clientId); var tokenRequestContext = new TokenRequestContext(scopes, tenantId: tenantId); var accessToken = await managedIdentityCredential.GetTokenAsync(tokenRequestContext); return new TokenResponse { TokenType = "Bearer", AccessToken = accessToken.Token, }; }; var appBuilder = App.Builder() .AddCredentials(new TokenCredentials( Environment.GetEnvironmentVariable("CLIENT_ID") ?? string.Empty, async (tenantId, scopes) => { return await createTokenFactory(scopes, tenantId); } )); builder.AddTeams(appBuilder); ``` The `createTokenFactory` function provides a method to retrieve access tokens from Azure on demand, and `TokenCredentials` passes this method to the app. ## Configuration Set the following environment variable: - `CLIENT_ID`: Your Application (client) ID ## Federated Identity Credentials Advanced identity federation allowing you to assign managed identities directly to your App Registration. :::note Support for C# is coming soon. ::: ### Setup First, complete the [Federated Identity Credentials Setup](/teams/app-authentication/federated-identity-credentials) in Azure Portal or Azure CLI. ### Configuration Depending on the type of managed identity you select, set the environment variables accordingly. **For User Managed Identity:** Set the following environment variables: - `CLIENT_ID`: Your Application (client) ID - `MANAGED_IDENTITY_CLIENT_ID`: The Client ID for the User Managed Identity resource - **Do not set** `CLIENT_SECRET` - `TENANT_ID`: The tenant id where your bot is registered ```env CLIENT_ID=your-app-client-id-here MANAGED_IDENTITY_CLIENT_ID=your-managed-identity-client-id-here # Do not set CLIENT_SECRET TENANT_ID=your-tenant-id ``` **For System Assigned Identity:** Set the following environment variables: - `CLIENT_ID`: Your Application (client) ID - `MANAGED_IDENTITY_CLIENT_ID`: `system` - **Do not set** `CLIENT_SECRET` - `TENANT_ID`: The tenant id where your bot is registered ```env CLIENT_ID=your-app-client-id-here MANAGED_IDENTITY_CLIENT_ID=system # Do not set CLIENT_SECRET TENANT_ID=your-tenant-id ``` ## Troubleshooting If you encounter authentication errors, see the [Authentication Troubleshooting](/teams/app-authentication/troubleshooting) guide for common issues and solutions. --- ### Best Practices # Best Practices When sending messages using AI, Teams recommends a number of best practices to help with both user and developer experience. ## AI-Generated Indicator When sending messages using AI, Teams recommends including an indicator that the message was generated by AI. This can be done by calling the `.AddAIGenerated()` method on outgoing messages. This will help users understand that the message was generated by AI, and not by a human and can help with trust and transparency. ```csharp var messageActivity = new MessageActivity { Text = "Hello!", }.AddAIGenerated(); ``` ![Screenshot of outgoing agent message to user marked with 'AI generated' badge.](/screenshots/ai-generated.gif) ## Gather feedback to improve prompts AI Generated messages are not always perfect. Prompts can have gaps, and can sometimes lead to unexpected results. To help improve the prompts, Teams recommends gathering feedback from users on the AI-generated messages. See [Feedback](../feedback) for more information on how to gather feedback. This does involve thinking through a pipeline for gathering feedback and then automatically, or manually, updating prompts based on the feedback. The feedback system is an point of entry to your eval pipeline. ## Citations AI generated messages can hallucinate even if messages are grounded in real data. To help with this, Teams recommends including citations in the AI Generated messages. This is easy to do by using the `AddCitation` method on the message. :::warning Citations are added with a `position` property. This property value needs to also be included in the message text as `[]`. If there is a citation that's added without the associated value in the message text, Teams will not render the citation ::: ```csharp var messageActivity = new MessageActivity { Text = result.Content, }.AddAIGenerated(); for (int i = 0; i < citedDocs.Length; i++) { messageActivity.Text += $"[{i + 1}]"; messageActivity.AddCitation(i + 1, new CitationAppearance { Name = citedDocs[i].Title, Abstract = citedDocs[i].Content }); } ``` ![Animated screenshot showing user hovering over a footnote citation in agent response, and a pop-up showing explanatory text.](/screenshots/citation.gif) ## Suggested actions Suggested actions help users with ideas of what to ask next, based on the previous response or conversation. Teams recommends including suggested actions in your messages. You can do that by using the `WithSuggestedActions` method on the message. See [Suggested actions](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/conversations/prompt-suggestions) for more information on suggested actions. ```csharp var message = new MessageActivity { Text = result.Content, }.WithSuggestedActions( new Microsoft.Teams.Api.SuggestedActions() { To = [context.Activity.From.Id], Actions = [ new Microsoft.Teams.Api.Cards.Action(ActionType.IMBack) { Title = "Thank you!", Value = "Thank you very much!" } ] }).AddAIGenerated(); await context.Send(message); ``` --- ### Functions # Functions Agents may want to expose REST APIs that client applications can call. This SDK makes it easy to implement those APIs through the `app.AddFunction()` method. The function takes a name and a callback that implements the function. ```csharp app.AddFunction('do-something', (context) => { // do something useful }); ``` This registers a REST API hosted at `http://localhost:PORT/api/functions/do-something` or `https://BOT_DOMAIN/api/functions/do-something` that clients can POST to. When they do, this SDK validates that the caller provides a valid Microsoft Entra bearer token before invoking the registered callback. If the token is missing or invalid, the request is denied with a HTTP 401. The function can be typed to accept input arguments. The clients would include those in the POST request payload, and they are made available in the callback through the `Data` context argument. ```csharp public class ProcessMessageData { [JsonPropertyName("message")] public required string Message { get; set; } } // ... app.AddFunction ("process-message", (context) => { context.Log.Debug($"process-message with: {context.Data.Message}"); }); ``` :::warning This SDK does not validate that the function arguments are of the expected types or otherwise trustworthy. You must take care to validate the input arguments before using them. ::: If desired, the function can return data to the caller. ```csharp app.AddFunction('get-random-number', () => { return 4; // chosen by fair dice roll; // guaranteed to be random }); ``` ## Function context The function callback receives a context object with a number of useful values. Some originate within the agent itself, while others are furnished by the caller via the HTTP Request. | Property | Source | Description | | -------------- | ------ | ------------------------------------------------------------------------------------------------------------------ | | `Api` | Agent | The API client. | | `AppId` | Agent | Unique identifier assigned to the app after deployment, ensuring correct app instance recognition across hosts. | | `AppSessionId` | Caller | Unique ID for the calling app's session, used to correlate telemetry data. | | `AuthToken` | Caller | The validated MSAL Entra token. | | `ChannelId` | Caller | Microsoft Teams ID for the channel associated with the content. | | `ChatId` | Caller | Microsoft Teams ID for the chat associated with the content. | | `Data` | Caller | The function payload. | | `Log` | Agent | The app logger instance. | | `MeetingId` | Caller | Meeting ID used by tab when running in meeting context. | | `MessageId` | Caller | ID of the parent message from which the task module was launched (only available in bot card-launched modules). | | `PageId` | Caller | Developer-defined unique ID for the page this content points to. | | `Send` | Agent | Sends an activity to the current conversation. | | `SubPageId` | Caller | Developer-defined unique ID for the sub-page this content points to. Used to restore specific state within a page. | | `TeamId` | Caller | Microsoft Teams ID for the team associated with the content. | | `TenantId` | Caller | Microsoft Entra tenant ID of the current user, extracted from the validated auth token. | | `UserId` | Caller | Microsoft Entra object ID of the current user, extracted from the validated auth token. | | `UserName` | Caller | Microsoft Entra name of the current user, extracted from the validated auth token. | The `AuthToken` is validated before the function callback is invoked, and the `TenantId`, `UserId`, and `UserName` values are extracted from the validated token. In the typical case, the remaining caller-supplied values would reflect what the Teams Tab app retrieves from the teams-js `getContext()` API, but the agent does not validate these. :::warning Take care to validate the caller-supplied values before using them. Don't assume that the calling user actually has access to items indicated in the context. ::: To simplify a common scenarios, the context provides a `Send` method. This method sends an activity to the current conversation ID, determined from the context values provided by the client (chatId and channelId). If neither chatId or channelId is provided by the caller, the ID of the 1:1 conversation between the agent and the user is assumed. :::warning The `Send` method does not validate that the chat ID or conversation ID provided by the caller is valid or correct. You must take care to validate that the user and agent both have appropriate access to the conversation. ::: ## Additional resources - For details on how to Tab apps can call these functions, see the TypeScript [Executing Functions](../../../../typescript/in-depth-guides/tabs/functions/function-calling) in-depth guide. - For more information about the teams-js getContext() API, see the [Teams JavaScript client library](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/using-teams-client-library) documentation. --- ### MCP # MCP Teams SDK has optional packages which support the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) as a service or client. This allows you to use MCP to call functions and tools in your application. MCP servers and MCP clients dynamically load function definitions and tools. When building Servers, this could mean that you can introduce new tools as part of your application, and the MCP clients that are connected to it will automatically start consuming those tools. When building Clients, this could mean that you can connect to other MCP servers and your application has the flexibility to improve as the MCP servers its connected to evolve over time. :::tip The guides here can be used to build a server and a client that can leverage each other. That means you can build a server that has the ability to do complex things for the client agent. ::: --- ### Tabs # Tabs Tabs are host-aware webpages embedded in Microsoft Teams, Outlook, and Microsoft 365. Tabs are commonly implemented as Single Page Applications that use the Teams [JavaScript client library](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/using-teams-client-library) (TeamsJS) to interact with the app host. This SDK does not offer features for implementing Tab apps in C#. It does however let you host tab apps and implement functions that can be called by Tab apps. ## Resources - [Tabs overview](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/what-are-tabs?tabs=personal) - [Teams JavaScript client library](https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/using-teams-client-library) - [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview) - [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview) - [Nested App Authentication (NAA)](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/authentication/nested-authentication) ### Additional resources - [Static Pages](../server/static-pages) - [TypeScript Tabs in-depth guide](../../../typescript/in-depth-guides/tabs) --- ### Teams API Client # Teams API Client Teams has a number of areas that your application has access to via its API. These are all available via the `app.Api` object. Here is a short summary of the different areas: | Area | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user) | | `Meetings` | Gives your application access to meeting details and participant information via `GetByIdAsync` and `GetParticipantAsync` | | `Teams` | Gives your application access to team or channel details | An instance of the API client is passed to handlers that can be used to fetch details: ## Example In this example, we use the API client to fetch the members in a conversation. The `Api` object is passed to the activity handler in this case. ```csharp app.OnMessage(async (context, cancellationToken) => { var members = await context.Api.Conversations.Members.Get(context.Conversation.Id); }); ``` ## Proactive API It's also possible to access the API client from outside a handler via the app instance. Here we have the same example as above, but we're access the API client via the app instance. ```csharp var members = await app.Api.Conversations.Members.Get("..."); ``` ## Meetings Example In this example, we use the API client to get a specific meeting participant's details, such as their role (e.g. Organizer) and whether they are currently in the meeting. Provide the user's AAD Object ID to specify which participant to look up. The `meetingId` and `tenantId` are available from the activity's channel data. :::note To retrieve **all** members of a meeting, use the conversations API as shown in the [example above](#example), since meetings are also conversations. ::: ```csharp app.OnMeetingStart(async (context, cancellationToken) => { var meetingId = context.Activity.Value.Id; var tenantId = context.Activity.ChannelData?.Tenant?.Id; var userId = context.Activity.From?.AadObjectId; if (meetingId != null && tenantId != null && userId != null) { var participant = await context.Api.Meetings.GetParticipantAsync(meetingId, userId, tenantId); // participant.Meeting?.Role — "Organizer", "Presenter", "Attendee" // participant.Meeting?.InMeeting — true/false } }); ``` Visit [Meeting Events](../in-depth-guides/meeting-events) to learn more about meeting events. --- ### Feedback # Feedback User feedback is essential for the improvement of any application. Teams provides specialized UI components to help facilitate the gathering of feedback from users. ![Animated image showing user selecting the thumbs-up button on an agent response and a dialog opening asking 'What did you like?'. The user types 'Nice' and hits Submit.](/screenshots/feedback.gif) ## Storage Once you receive a feedback event, you can choose to store it in some persistent storage. In the example below, we are storing it in an in-memory store. ```csharp // This store would ideally be persisted in a database public static class FeedbackStore { public static readonly Dictionary StoredFeedbackByMessageId = new(); public class FeedbackData { public string IncomingMessage { get; set; } = string.Empty; public string OutgoingMessage { get; set; } = string.Empty; public int Likes { get; set; } public int Dislikes { get; set; } public List Feedbacks { get; set; } = new(); } } ``` ## Including Feedback Buttons When sending a message that you want feedback in, simply add feedback functionality to the message you are sending. ```csharp var sentMessageId = await context.Send( result.Content != null ? new MessageActivity(result.Content) .AddAiGenerated() /** Add feedback buttons via this method */ .AddFeedback() : "I did not generate a response." ); FeedbackStore.StoredFeedbackByMessageId[sentMessageId.Id] = new FeedbackStore.FeedbackData { IncomingMessage = context.Activity.Text, OutgoingMessage = result.Content ?? string.Empty, Likes = 0, Dislikes = 0, Feedbacks = new List() }; ``` ## Handling the feedback Once the user decides to like/dislike the message, you can handle the feedback in a received event. Once received, you can choose to include it in your persistent store. ```csharp [Microsoft.Teams.Apps.Activities.Invokes.Message.Feedback] public Task OnFeedbackReceived([Context] Microsoft.Teams.Api.Activities.Invokes.Messages.SubmitActionActivity activity) { var reaction = activity.Value?.ActionValue?.GetType().GetProperty("reaction")?.GetValue(activity.Value?.ActionValue)?.ToString(); var feedbackJson = activity.Value?.ActionValue?.GetType().GetProperty("feedback")?.GetValue(activity.Value?.ActionValue)?.ToString(); if (activity.ReplyToId == null) { _log.LogWarning("No replyToId found for messageId {ActivityId}", activity.Id); return Task.CompletedTask; } var existingFeedback = FeedbackStore.StoredFeedbackByMessageId.GetValueOrDefault(activity.ReplyToId); /** * feedbackJson looks like: * {"feedbackText":"Nice!"} */ if (existingFeedback == null) { _log.LogWarning("No feedback found for messageId {ActivityId}", activity.Id); } else { var updatedFeedback = new FeedbackStore.FeedbackData { IncomingMessage = existingFeedback.IncomingMessage, OutgoingMessage = existingFeedback.OutgoingMessage, Likes = existingFeedback.Likes + (reaction == "like" ? 1 : 0), Dislikes = existingFeedback.Dislikes + (reaction == "dislike" ? 1 : 0), Feedbacks = existingFeedback.Feedbacks.Concat(new[] { feedbackJson ?? string.Empty }).ToList() }; FeedbackStore.StoredFeedbackByMessageId[activity.Id] = updatedFeedback; } return Task.CompletedTask; } ``` --- ### Graph API Client # Graph API Client [Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview) gives you access to the wider Microsoft 365 ecosystem. You can enrich your application with data from across Microsoft 365. The SDK gives your application easy access to the Microsoft Graph API via the `Microsoft.Graph` package. ## Calling APIs Microsoft Graph can be accessed by your application using its own application token, or by using the user's token. If you need access to resources that your application may not have, but your user does, you will need to use the user's scoped graph client. To grant explicit consent for your application to access resources on behalf of a user, follow the [auth guide](../in-depth-guides/user-authentication). To access the graph using the Graph using the app, you may use the `app.Graph` object . ```csharp // Equivalent of https://learn.microsoft.com/en-us/graph/api/user-get // Gets the details of the bot-user var user = app.Graph.Me.GetAsync().GetAwaiter().GetResult(); Console.WriteLine($"User ID: {user.id}"); Console.WriteLine($"User Display Name: {user.displayName}"); Console.WriteLine($"User Email: {user.mail}"); Console.WriteLine($"User Job Title: {user.jobTitle}"); ``` To access the graph using the user's token, you need to do this as part of a message handler: ```csharp app.OnMessage(async (context, cancellationToken) => { var user = await context.UserGraph.Me.GetAsync(); Console.WriteLine($"User ID: {user.id}"); Console.WriteLine($"User Display Name: {user.displayName}"); Console.WriteLine($"User Email: {user.mail}"); Console.WriteLine($"User Job Title: {user.jobTitle}"); }); ``` Here, the `userGraph` object is a scoped graph client for the user that sent the message. :::tip You also have access to the `appGraph` object in the activity handler. This is equivalent to `app.Graph`. ::: --- ### Meeting Events # Meeting Events Microsoft Teams provides meeting events that allow your application to respond to various meeting lifecycle changes. Your app can listen to events like when a meeting starts, meeting ends, and participant activities to create rich, interactive experiences. ## Overview Meeting events enable your application to: - Send notifications when meetings start or end - Track participant activity (join/leave events) - Display relevant information or cards based on meeting context - Integrate with meeting workflows ## Configuring Your Bot There are a few requirements in the Teams app manifest (`manifest.json`) to support these events. 1. The scopes section must include `team`, and `groupChat` ```json bots": [ { "botId": "", "scopes": [ "team", "personal", "groupChat" ], "isNotificationOnly": false } ] ``` 2. In the authorization section, make sure to specify the following resource-specific permissions: ```json "authorization":{ "permissions":{ "resourceSpecific":[ { "name":"OnlineMeetingParticipant.Read.Chat", "type":"Application" }, { "name":"ChannelMeeting.ReadBasic.Group", "type":"Application" }, { "name":"OnlineMeeting.ReadBasic.Chat", "type":"Application" } ] } } ``` 3. In the Teams Developer Portal, for your `Bot`, make sure the `Meeting Event Subscriptions` are checked off. This enables you to receive the Meeting Participant events. For these events, you must create your Bot via TDP. ## Meeting Start Event When a meeting starts, your app can handle the `meetingStart` event to send a notification or card to the meeting chat. ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Events; using Microsoft.Teams.Cards; // Register meeting start handler teamsApp.OnMeetingStart(async (context, cancellationToken) => { var activity = context.Activity.Value; var startTime = activity.StartTime.ToLocalTime(); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock($"'{activity.Title}' has started at {startTime}.") { Wrap = true, Weight = TextWeight.Bolder } }, Actions = new List { new OpenUrlAction(activity.JoinUrl) { Title = "Join the meeting", } } }; await context.Send(card, cancellationToken); }); ``` ## Meeting End Event When a meeting ends, your app can handle the `meetingEnd` event to send a summary or follow-up information. ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Events; using Microsoft.Teams.Cards; // Register meeting end handler teamsApp.OnMeetingEnd(async (context, cancellationToken) => { var activity = context.Activity.Value; var endTime = activity.EndTime.ToLocalTime(); var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock($"'{activity.Title}' has ended at {endTime}.") { Wrap = true, Weight = TextWeight.Bolder } } }; await context.Send(card, cancellationToken); }); ``` ## Participant Join Event When a participant joins a meeting, your app can handle the `meetingParticipantJoin` event to welcome them or display their role. ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Events; using Microsoft.Teams.Cards; // Register participant join handler teamsApp.OnMeetingJoin(async (context, cancellationToken) => { var activity = context.Activity.Value; var member = activity.Members[0].User.Name; var role = activity.Members[0].Meeting.Role; var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock($"{member} has joined the meeting as {role}.") { Wrap = true, Weight = TextWeight.Bolder } } }; await context.Send(card, cancellationToken); }); ``` ## Participant Leave Event When a participant leaves a meeting, your app can handle the `meetingParticipantLeave` event to notify others. ```csharp using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Events; using Microsoft.Teams.Cards; // Register participant leave handler teamsApp.OnMeetingLeave(async (context, cancellationToken) => { var activity = context.Activity.Value; var member = activity.Members[0].User.Name; var card = new AdaptiveCard { Schema = "http://adaptivecards.io/schemas/adaptive-card.json", Body = new List { new TextBlock($"{member} has left the meeting.") { Wrap = true, Weight = TextWeight.Bolder } } }; await context.Send(card, cancellationToken); }); ``` --- ### Observability # Observability ---