Skip to content

Added HandlingAttachments #284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Microsoft.Agents.SDK.sln
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FullAuthentication", "sampl
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OBOAuthorization", "samples\Authorization\OBOAuthorization\OBOAuthorization.csproj", "{0E1394C7-045C-2BDC-65E8-D42B2F31EF46}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HandlingAttachments", "samples\HandlingAttachments\HandlingAttachments.csproj", "{D5202D4A-2F15-CE1B-F82C-2405C040EB14}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -305,6 +307,10 @@ Global
{0E1394C7-045C-2BDC-65E8-D42B2F31EF46}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E1394C7-045C-2BDC-65E8-D42B2F31EF46}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E1394C7-045C-2BDC-65E8-D42B2F31EF46}.Release|Any CPU.Build.0 = Release|Any CPU
{D5202D4A-2F15-CE1B-F82C-2405C040EB14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D5202D4A-2F15-CE1B-F82C-2405C040EB14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D5202D4A-2F15-CE1B-F82C-2405C040EB14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D5202D4A-2F15-CE1B-F82C-2405C040EB14}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -367,6 +373,7 @@ Global
{E951C602-E9ED-F77D-8FC9-5272DB90D1DD} = {674A812C-7287-4883-97F9-697D83750648}
{A6785A2A-A4C2-8F38-E9BB-4C5FD229F1F9} = {674A812C-7287-4883-97F9-697D83750648}
{0E1394C7-045C-2BDC-65E8-D42B2F31EF46} = {0F9D3F0D-C131-4D3B-A86F-59CC781E8A02}
{D5202D4A-2F15-CE1B-F82C-2405C040EB14} = {674A812C-7287-4883-97F9-697D83750648}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F1E8E538-309A-46F8-9CE7-AEC6589FAE60}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,13 @@ public static IHostApplicationBuilder AddAgent<TAdapter>(this IHostApplicationBu
/// <returns></returns>
public static IHostApplicationBuilder AddAgentApplicationOptions(
this IHostApplicationBuilder builder,
IList<IInputFileDownloader> fileDownloaders = null,
AutoSignInSelector autoSignIn = null)
{
if (autoSignIn != null)
{
builder.Services.AddSingleton<AutoSignInSelector>(sp => autoSignIn);
}

if (fileDownloaders != null)
{
builder.Services.AddSingleton(sp => fileDownloaders);
}

builder.Services.AddSingleton<AgentApplicationOptions>();

return builder;
Expand Down
192 changes: 192 additions & 0 deletions src/samples/HandlingAttachments/AttachmentsAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App;
using Microsoft.Agents.Builder.State;
using Microsoft.Agents.Connector;
using Microsoft.Agents.Connector.Types;
using Microsoft.Agents.Core.Models;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace HandlingAttachments;

public class AttachmentsAgent : AgentApplication
{
public AttachmentsAgent(AgentApplicationOptions options) : base(options)
{
OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeMessageAsync);
OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last);
}

private async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
foreach (var member in turnContext.Activity.MembersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(
$"Welcome to HandlingAttachment Agent." +
$" This bot will introduce you to Attachments." +
$" Please select an option",
cancellationToken: cancellationToken);
await DisplayOptionsAsync(turnContext, cancellationToken);
}
}
}

private async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
var reply = await ProcessInput(turnContext, turnState, cancellationToken);

// Respond to the user.
await turnContext.SendActivityAsync(reply, cancellationToken);
await DisplayOptionsAsync(turnContext, cancellationToken);
}

private static async Task DisplayOptionsAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
// Create a HeroCard with options for the user to interact with the bot.
var card = new HeroCard
{
Text = "You can upload an image or select one of the following choices",
Buttons =
[
// Note that some channels require different values to be used in order to get buttons to display text.
// In this code the emulator is accounted for with the 'title' parameter, but in other channels you may
// need to provide a value for other parameters like 'text' or 'displayText'.
new CardAction(ActionTypes.ImBack, title: "1. Inline Attachment", value: "1"),
new CardAction(ActionTypes.ImBack, title: "2. Internet Attachment", value: "2"),
new CardAction(ActionTypes.ImBack, title: "3. Uploaded Attachment", value: "3"),
],
};

var reply = MessageFactory.Attachment(card.ToAttachment());
await turnContext.SendActivityAsync(reply, cancellationToken);
}

// Given the input from the message, create the response.
private static async Task<IActivity> ProcessInput(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
IActivity reply;

if (turnState.Temp.InputFiles.Any())
{
reply = MessageFactory.Text($"There are {turnState.Temp.InputFiles.Count} attachments.");

var imageData = Convert.ToBase64String(turnState.Temp.InputFiles[0].Content);
reply.Attachments = [new Attachment() { Name = turnState.Temp.InputFiles[0].Filename, ContentType = "image/png", ContentUrl = $"data:image/png;base64,{imageData}" }];
}
else
{
// Send at attachment to the user.
reply = await HandleOutgoingAttachment(turnContext, turnContext.Activity, cancellationToken);
}

return reply;
}

// Returns a reply with the requested Attachment
private static async Task<IActivity> HandleOutgoingAttachment(ITurnContext turnContext, IActivity activity, CancellationToken cancellationToken)
{
// Look at the user input, and figure out what kind of attachment to send.
IActivity reply;
if (activity.Text.StartsWith('1'))
{
reply = MessageFactory.Text("This is an inline attachment.");
reply.Attachments = [GetInlineAttachment()];
}
else if (activity.Text.StartsWith('2'))
{
reply = MessageFactory.Text("This is an attachment from a HTTP URL.");
reply.Attachments = [GetInternetAttachment()];
}
else if (activity.Text.StartsWith('3'))
{
reply = MessageFactory.Text("This is an uploaded attachment.");

// Get the uploaded attachment.
var uploadedAttachment = await UploadAttachmentAsync(turnContext, activity.ServiceUrl, activity.Conversation.Id, cancellationToken);
reply.Attachments = [uploadedAttachment];
}
else
{
// The user did not enter input that this bot was built to handle.
reply = MessageFactory.Text("Your input was not recognized please try again.");
}

return reply;
}

// Creates an inline attachment sent from the bot to the user using a base64 string.
// Using a base64 string to send an attachment will not work on all channels.
// Additionally, some channels will only allow certain file types to be sent this way.
// For example a .png file may work but a .pdf file may not on some channels.
// Please consult the channel documentation for specifics.
private static Attachment GetInlineAttachment()
{
var imagePath = Path.Combine(Environment.CurrentDirectory, @"Resources", "build-agents.png");
var imageData = Convert.ToBase64String(File.ReadAllBytes(imagePath));

return new Attachment
{
Name = @"Resources\build-agents.png",
ContentType = "image/png",
ContentUrl = $"data:image/png;base64,{imageData}",
};
}

// Creates an "Attachment" to be sent from the bot to the user from an uploaded file.
private static async Task<Attachment> UploadAttachmentAsync(ITurnContext turnContext, string serviceUrl, string conversationId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(serviceUrl))
{
throw new ArgumentNullException(nameof(serviceUrl));
}

if (string.IsNullOrWhiteSpace(conversationId))
{
throw new ArgumentNullException(nameof(conversationId));
}

var imagePath = Path.Combine(Environment.CurrentDirectory, @"Resources", "agents-sdk.png");

var connector = turnContext.Services.Get<IConnectorClient>();

// This only supports payloads smaller than 260k
var response = await connector.Conversations.UploadAttachmentAsync(
conversationId,
new AttachmentData
{
Name = @"Resources\agents-sdk.png",
OriginalBase64 = File.ReadAllBytes(imagePath),
Type = "image/png",
},
cancellationToken);

var attachmentUri = connector.Attachments.GetAttachmentUri(response.Id);

return new Attachment
{
Name = @"Resources\agents-sdk.png",
ContentType = "image/png",
ContentUrl = attachmentUri,
};
}

// Creates an <see cref="Attachment"/> to be sent from the bot to the user from a HTTP URL.
private static Attachment GetInternetAttachment()
{
// ContentUrl must be HTTPS.
return new Attachment
{
Name = @"Resources\introducing-agents-sdk.png",
ContentType = "image/png",
ContentUrl = "https://devblogs.microsoft.com/microsoft365dev/wp-content/uploads/sites/73/2024/11/word-image-23435-1.png",
};
}
}
24 changes: 24 additions & 0 deletions src/samples/HandlingAttachments/HandlingAttachments.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\libraries\Authentication\Authentication.Msal\Microsoft.Agents.Authentication.Msal.csproj" />
<ProjectReference Include="..\..\libraries\Hosting\AspNetCore\Microsoft.Agents.Hosting.AspNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Resources\agents-sdk.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\introducing-agents-sdk.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\build-agents.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
53 changes: 53 additions & 0 deletions src/samples/HandlingAttachments/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using HandlingAttachments;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App;
using Microsoft.Agents.Hosting.AspNetCore;
using Microsoft.Agents.Storage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddHttpClient();
builder.Logging.AddConsole();

// Register FileDownloaders
builder.Services.AddSingleton<IList<IInputFileDownloader>>(sp => [new AttachmentDownloader(sp.GetService<IHttpClientFactory>())]);

// Add ApplicationOptions
builder.AddAgentApplicationOptions();

// Add the bot (which is transient)
builder.AddAgent<AttachmentsAgent>();

// Register IStorage. For development, MemoryStorage is suitable.
// For production Agents, persisted storage should be used so
// that state survives Agent restarts, and operate correctly
// in a cluster of Agent instances.
builder.Services.AddSingleton<IStorage, MemoryStorage>();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.MapGet("/", () => "Microsoft Agents SDK Sample");
app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) =>
{
await adapter.ProcessAsync(request, response, agent, cancellationToken);
})
.AllowAnonymous();

// Hardcoded for brevity and ease of testing.
// In production, this should be set in configuration.
app.Urls.Add($"http://localhost:3978");

app.Run();
73 changes: 73 additions & 0 deletions src/samples/HandlingAttachments/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# HandleAttachments Sample

This is a sample of a simple Agent that is hosted on an Asp.net core web service. This Agent is configured to automatically download Activity Attachments.

This Agent Sample is intended to introduce you the basic operation of the Microsoft 365 Agents SDK messaging loop. It can also be used as a the base for a custom Agent that you choose to develop.

## Prerequisites

- [.Net](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) version 8.0
- [dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows)

## Running this sample

**To run the sample connected to Azure Bot Service, the following additional tools are required:**

- Access to an Azure Subscription with access to preform the following tasks:
- Create and configure Entra ID Application Identities
- Create and configure an [Azure Bot Service](https://aka.ms/AgentsSDK-CreateBot) for your Azure Bot.
- Create and configure an [Azure App Service](https://learn.microsoft.com/azure/app-service/) to deploy your Agent to.
- A tunneling tool to allow for local development and debugging should you wish to do local development whilst connected to a external client such as Microsoft Teams.

## Getting Started with HandleAttachments Sample

Read more about [Running an Agent](../../../docs/HowTo/running-an-agent.md)

### QuickStart using WebChat

1. [Create an Azure Bot](https://aka.ms/AgentsSDK-CreateBot)
- Record the Application ID, the Tenant ID, and the Client Secret for use below

1. Configuring the token connection in the Agent settings
> The instructions for this sample are for a SingleTenant Azure Bot using ClientSecrets. The token connection configuration will vary if a different type of Azure Bot was configured. For more information see [DotNet MSAL Authentication provider](https://aka.ms/AgentsSDK-DotNetMSALAuth)

1. Open the `appsettings.json` file in the root of the sample project.

1. Find the section labeled `Connections`, it should appear similar to this:

```json
"Connections": {
"ServiceConnection": {
"Settings": {
"AuthType": "ClientSecret", // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. The default is ClientSecret.
"AuthorityEndpoint": "https://login.microsoftonline.com/{{TenantId}}",
"ClientId": "{{ClientId}}", // this is the Client ID used for the connection.
"ClientSecret": "00000000-0000-0000-0000-000000000000", // this is the Client Secret used for the connection.
"Scopes": [
"https://api.botframework.com/.default"
]
}
}
},
```

1. Replace all **{{ClientId}}** with the AppId of the Azure Bot.
1. Replace all **{{TenantId}}** with the Tenant Id where your application is registered.
1. Set the **ClientSecret** to the Secret that was created for your identity.

> Storing sensitive values in appsettings is not recommend. Follow [AspNet Configuration](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-9.0) for best practices.

1. Run `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below:

```bash
devtunnel host -p 3978 --allow-anonymous
```

1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `{tunnel-url}/api/messages`

1. Start the Agent in Visual Studio

1. Select **Test in WebChat** on the Azure Bot

## Further reading
To learn more about building Agents, see our [Microsoft 365 Agents SDK](https://github.com/microsoft/agents) repo.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading