Skip to content

Commit 0c55c36

Browse files
authored
Merge pull request #284 from microsoft/users/tracyboehrer/handlingattachments
Added HandlingAttachments
2 parents 2152d21 + cf3f90b commit 0c55c36

File tree

21 files changed

+438
-793
lines changed

21 files changed

+438
-793
lines changed

src/Microsoft.Agents.SDK.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FullAuthentication", "sampl
123123
EndProject
124124
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OBOAuthorization", "samples\Authorization\OBOAuthorization\OBOAuthorization.csproj", "{0E1394C7-045C-2BDC-65E8-D42B2F31EF46}"
125125
EndProject
126+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HandlingAttachments", "samples\HandlingAttachments\HandlingAttachments.csproj", "{D5202D4A-2F15-CE1B-F82C-2405C040EB14}"
127+
EndProject
126128
Global
127129
GlobalSection(SolutionConfigurationPlatforms) = preSolution
128130
Debug|Any CPU = Debug|Any CPU
@@ -305,6 +307,10 @@ Global
305307
{0E1394C7-045C-2BDC-65E8-D42B2F31EF46}.Debug|Any CPU.Build.0 = Debug|Any CPU
306308
{0E1394C7-045C-2BDC-65E8-D42B2F31EF46}.Release|Any CPU.ActiveCfg = Release|Any CPU
307309
{0E1394C7-045C-2BDC-65E8-D42B2F31EF46}.Release|Any CPU.Build.0 = Release|Any CPU
310+
{D5202D4A-2F15-CE1B-F82C-2405C040EB14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
311+
{D5202D4A-2F15-CE1B-F82C-2405C040EB14}.Debug|Any CPU.Build.0 = Debug|Any CPU
312+
{D5202D4A-2F15-CE1B-F82C-2405C040EB14}.Release|Any CPU.ActiveCfg = Release|Any CPU
313+
{D5202D4A-2F15-CE1B-F82C-2405C040EB14}.Release|Any CPU.Build.0 = Release|Any CPU
308314
EndGlobalSection
309315
GlobalSection(SolutionProperties) = preSolution
310316
HideSolutionNode = FALSE
@@ -367,6 +373,7 @@ Global
367373
{E951C602-E9ED-F77D-8FC9-5272DB90D1DD} = {674A812C-7287-4883-97F9-697D83750648}
368374
{A6785A2A-A4C2-8F38-E9BB-4C5FD229F1F9} = {674A812C-7287-4883-97F9-697D83750648}
369375
{0E1394C7-045C-2BDC-65E8-D42B2F31EF46} = {0F9D3F0D-C131-4D3B-A86F-59CC781E8A02}
376+
{D5202D4A-2F15-CE1B-F82C-2405C040EB14} = {674A812C-7287-4883-97F9-697D83750648}
370377
EndGlobalSection
371378
GlobalSection(ExtensibilityGlobals) = postSolution
372379
SolutionGuid = {F1E8E538-309A-46F8-9CE7-AEC6589FAE60}

src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/App/TeamsAttachmentDownloader.cs

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public async Task<IList<InputFile>> DownloadFilesAsync(ITurnContext turnContext,
6767
IEnumerable<Attachment>? attachments = turnContext.Activity.Attachments?.Where((a) => !a.ContentType.StartsWith("text/html"));
6868
if (attachments == null || !attachments.Any())
6969
{
70-
return new List<InputFile>();
70+
return [];
7171
}
7272

7373
string accessToken = "";
@@ -102,46 +102,43 @@ public async Task<IList<InputFile>> DownloadFilesAsync(ITurnContext turnContext,
102102
if (attachment.ContentUrl != null && (attachment.ContentUrl.StartsWith("https://") || attachment.ContentUrl.StartsWith("http://localhost")))
103103
{
104104
// Get downloadable content link
105+
string downloadUrl;
105106
var contentProperties = ProtocolJsonSerializer.ToJsonElements(attachment.Content);
106107
if (contentProperties == null || !contentProperties.TryGetValue("downloadUrl", out System.Text.Json.JsonElement value))
107108
{
108-
return null;
109+
downloadUrl = attachment.ContentUrl;
110+
}
111+
else
112+
{
113+
downloadUrl = value.ToString();
109114
}
110115

111-
string? downloadUrl = value.ToString();
112-
if (downloadUrl == null)
116+
using HttpRequestMessage request = new(HttpMethod.Get, downloadUrl);
117+
request.Headers.Add("Authorization", $"Bearer {accessToken}");
118+
119+
HttpResponseMessage response = await httpClient.SendAsync(request).ConfigureAwait(false);
120+
121+
// Failed to download file
122+
if (!response.IsSuccessStatusCode)
113123
{
114-
downloadUrl = attachment.ContentUrl;
124+
return null;
115125
}
116126

117-
using (HttpRequestMessage request = new(HttpMethod.Get, downloadUrl))
127+
// Convert to a buffer
128+
byte[] content = await response.Content.ReadAsByteArrayAsync();
129+
130+
// Fixup content type
131+
string contentType = response.Content.Headers.ContentType.MediaType;
132+
if (contentType.StartsWith("image/"))
118133
{
119-
request.Headers.Add("Authorization", $"Bearer {accessToken}");
120-
121-
HttpResponseMessage response = await httpClient.SendAsync(request).ConfigureAwait(false);
122-
123-
// Failed to download file
124-
if (!response.IsSuccessStatusCode)
125-
{
126-
return null;
127-
}
128-
129-
// Convert to a buffer
130-
byte[] content = await response.Content.ReadAsByteArrayAsync();
131-
132-
// Fixup content type
133-
string contentType = response.Content.Headers.ContentType.MediaType;
134-
if (contentType.StartsWith("image/"))
135-
{
136-
contentType = "image/png";
137-
}
138-
139-
return new InputFile(new BinaryData(content), contentType)
140-
{
141-
ContentUrl = attachment.ContentUrl,
142-
Filename = name
143-
};
134+
contentType = "image/png";
144135
}
136+
137+
return new InputFile(new BinaryData(content), contentType)
138+
{
139+
ContentUrl = attachment.ContentUrl,
140+
Filename = name
141+
};
145142
}
146143
else
147144
{

src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,19 +115,13 @@ public static IHostApplicationBuilder AddAgent<TAdapter>(this IHostApplicationBu
115115
/// <returns></returns>
116116
public static IHostApplicationBuilder AddAgentApplicationOptions(
117117
this IHostApplicationBuilder builder,
118-
IList<IInputFileDownloader> fileDownloaders = null,
119118
AutoSignInSelector autoSignIn = null)
120119
{
121120
if (autoSignIn != null)
122121
{
123122
builder.Services.AddSingleton<AutoSignInSelector>(sp => autoSignIn);
124123
}
125124

126-
if (fileDownloaders != null)
127-
{
128-
builder.Services.AddSingleton(sp => fileDownloaders);
129-
}
130-
131125
builder.Services.AddSingleton<AgentApplicationOptions>();
132126

133127
return builder;
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Agents.Builder;
5+
using Microsoft.Agents.Builder.App;
6+
using Microsoft.Agents.Builder.State;
7+
using Microsoft.Agents.Connector;
8+
using Microsoft.Agents.Connector.Types;
9+
using Microsoft.Agents.Core.Models;
10+
using System;
11+
using System.IO;
12+
using System.Linq;
13+
using System.Threading;
14+
using System.Threading.Tasks;
15+
16+
namespace HandlingAttachments;
17+
18+
public class AttachmentsAgent : AgentApplication
19+
{
20+
public AttachmentsAgent(AgentApplicationOptions options) : base(options)
21+
{
22+
OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeMessageAsync);
23+
OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last);
24+
}
25+
26+
private async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
27+
{
28+
foreach (var member in turnContext.Activity.MembersAdded)
29+
{
30+
if (member.Id != turnContext.Activity.Recipient.Id)
31+
{
32+
await turnContext.SendActivityAsync(
33+
$"Welcome to HandlingAttachment Agent." +
34+
$" This bot will introduce you to Attachments." +
35+
$" Please select an option",
36+
cancellationToken: cancellationToken);
37+
await DisplayOptionsAsync(turnContext, cancellationToken);
38+
}
39+
}
40+
}
41+
42+
private async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
43+
{
44+
var reply = await ProcessInput(turnContext, turnState, cancellationToken);
45+
if (reply != null)
46+
{
47+
// Respond to the user.
48+
await turnContext.SendActivityAsync(reply, cancellationToken);
49+
}
50+
await DisplayOptionsAsync(turnContext, cancellationToken);
51+
}
52+
53+
private static async Task DisplayOptionsAsync(ITurnContext turnContext, CancellationToken cancellationToken)
54+
{
55+
// Create a HeroCard with options for the user to interact with the bot.
56+
var card = new HeroCard
57+
{
58+
Text = "You can upload an image or select one of the following choices",
59+
Buttons =
60+
[
61+
// Note that some channels require different values to be used in order to get buttons to display text.
62+
// In this code the emulator is accounted for with the 'title' parameter, but in other channels you may
63+
// need to provide a value for other parameters like 'text' or 'displayText'.
64+
new CardAction(ActionTypes.ImBack, title: "1. Inline Attachment", value: "1"),
65+
new CardAction(ActionTypes.ImBack, title: "2. Internet Attachment", value: "2"),
66+
],
67+
};
68+
69+
if (!turnContext.Activity.ChannelId.Equals(Channels.Msteams, StringComparison.OrdinalIgnoreCase))
70+
{
71+
card.Buttons.Add(new CardAction(ActionTypes.ImBack, title: "3. Uploaded Attachment", value: "3"));
72+
}
73+
74+
var reply = MessageFactory.Attachment(card.ToAttachment());
75+
await turnContext.SendActivityAsync(reply, cancellationToken);
76+
}
77+
78+
// Given the input from the message, create the response.
79+
private static async Task<IActivity> ProcessInput(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
80+
{
81+
IActivity reply;
82+
83+
if (turnState.Temp.InputFiles.Any())
84+
{
85+
reply = MessageFactory.Text($"There are {turnState.Temp.InputFiles.Count} attachments.");
86+
87+
var imageData = Convert.ToBase64String(turnState.Temp.InputFiles[0].Content);
88+
reply.Attachments = [new Attachment() { Name = turnState.Temp.InputFiles[0].Filename, ContentType = "image/png", ContentUrl = $"data:image/png;base64,{imageData}" }];
89+
}
90+
else
91+
{
92+
// Send at attachment to the user.
93+
reply = await HandleOutgoingAttachment(turnContext, turnContext.Activity, cancellationToken);
94+
}
95+
96+
return reply;
97+
}
98+
99+
// Returns a reply with the requested Attachment
100+
private static async Task<IActivity> HandleOutgoingAttachment(ITurnContext turnContext, IActivity activity, CancellationToken cancellationToken)
101+
{
102+
// Look at the user input, and figure out what kind of attachment to send.
103+
104+
if (string.IsNullOrEmpty(activity.Text))
105+
{
106+
return null;
107+
}
108+
109+
IActivity reply = null;
110+
111+
if (activity.Text.StartsWith('1'))
112+
{
113+
reply = MessageFactory.Text("This is an inline attachment.");
114+
reply.Attachments = [GetInlineAttachment()];
115+
}
116+
else if (activity.Text.StartsWith('2'))
117+
{
118+
reply = MessageFactory.Text("This is an attachment from a HTTP URL.");
119+
reply.Attachments = [GetInternetAttachment()];
120+
}
121+
else if (activity.Text.StartsWith('3'))
122+
{
123+
reply = MessageFactory.Text("This is an uploaded attachment.");
124+
125+
// Get the uploaded attachment.
126+
var uploadedAttachment = await UploadAttachmentAsync(turnContext, activity.ServiceUrl, activity.Conversation.Id, cancellationToken);
127+
reply.Attachments = [uploadedAttachment];
128+
}
129+
130+
return reply;
131+
}
132+
133+
// Creates an inline attachment sent from the bot to the user using a base64 string.
134+
// Using a base64 string to send an attachment will not work on all channels.
135+
// Additionally, some channels will only allow certain file types to be sent this way.
136+
// For example a .png file may work but a .pdf file may not on some channels.
137+
// Please consult the channel documentation for specifics.
138+
private static Attachment GetInlineAttachment()
139+
{
140+
var imagePath = Path.Combine(Environment.CurrentDirectory, @"Resources", "build-agents.png");
141+
var imageData = Convert.ToBase64String(File.ReadAllBytes(imagePath));
142+
143+
return new Attachment
144+
{
145+
Name = @"Resources\build-agents.png",
146+
ContentType = "image/png",
147+
ContentUrl = $"data:image/png;base64,{imageData}",
148+
};
149+
}
150+
151+
// Creates an "Attachment" to be sent from the bot to the user from an uploaded file.
152+
private static async Task<Attachment> UploadAttachmentAsync(ITurnContext turnContext, string serviceUrl, string conversationId, CancellationToken cancellationToken)
153+
{
154+
if (string.IsNullOrWhiteSpace(serviceUrl))
155+
{
156+
throw new ArgumentNullException(nameof(serviceUrl));
157+
}
158+
159+
if (string.IsNullOrWhiteSpace(conversationId))
160+
{
161+
throw new ArgumentNullException(nameof(conversationId));
162+
}
163+
164+
var imagePath = Path.Combine(Environment.CurrentDirectory, @"Resources", "agents-sdk.png");
165+
166+
var connector = turnContext.Services.Get<IConnectorClient>();
167+
168+
// This only supports payloads smaller than 260k
169+
var response = await connector.Conversations.UploadAttachmentAsync(
170+
conversationId,
171+
new AttachmentData
172+
{
173+
Name = @"Resources\agents-sdk.png",
174+
OriginalBase64 = File.ReadAllBytes(imagePath),
175+
Type = "image/png",
176+
},
177+
cancellationToken);
178+
179+
var attachmentUri = connector.Attachments.GetAttachmentUri(response.Id);
180+
181+
return new Attachment
182+
{
183+
Name = @"Resources\agents-sdk.png",
184+
ContentType = "image/png",
185+
ContentUrl = attachmentUri,
186+
};
187+
}
188+
189+
// Creates an <see cref="Attachment"/> to be sent from the bot to the user from a HTTP URL.
190+
private static Attachment GetInternetAttachment()
191+
{
192+
// ContentUrl must be HTTPS.
193+
return new Attachment
194+
{
195+
Name = @"Resources\introducing-agents-sdk.png",
196+
ContentType = "image/png",
197+
ContentUrl = "https://devblogs.microsoft.com/microsoft365dev/wp-content/uploads/sites/73/2024/11/word-image-23435-1.png",
198+
};
199+
}
200+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\..\libraries\Authentication\Authentication.Msal\Microsoft.Agents.Authentication.Msal.csproj" />
10+
<ProjectReference Include="..\..\libraries\Extensions\Microsoft.Agents.Extensions.Teams\Microsoft.Agents.Extensions.Teams.csproj" />
11+
<ProjectReference Include="..\..\libraries\Hosting\AspNetCore\Microsoft.Agents.Hosting.AspNetCore.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<None Update="Resources\agents-sdk.png">
16+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
17+
</None>
18+
<None Update="Resources\introducing-agents-sdk.png">
19+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
20+
</None>
21+
<None Update="Resources\build-agents.png">
22+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
23+
</None>
24+
</ItemGroup>
25+
</Project>

0 commit comments

Comments
 (0)