microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
aamirj/ConversationalClient

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/src/Microsoft.Bot.Core/ConversationClient.cs

436lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Net.Http.Json;
5using System.Net.Mime;
6using System.Text;
7using System.Text.Json;
8using Microsoft.Bot.Core.Hosting;
9using Microsoft.Bot.Core.Schema;
10
11namespace Microsoft.Bot.Core;
12
13using CustomHeaders = Dictionary<string, string>;
14
15/// <summary>
16/// Provides methods for sending activities to a conversation endpoint using HTTP requests.
17/// </summary>
18/// <param name="httpClient">The HTTP client instance used to send requests to the conversation service. Must not be null.</param>
19public class ConversationClient(HttpClient httpClient)
20{
21 internal const string ConversationHttpClientName = "BotConversationClient";
22
23 /// <summary>
24 /// Gets the default custom headers that will be included in all requests.
25 /// </summary>
26 public CustomHeaders DefaultCustomHeaders { get; } = new();
27
28 /// <summary>
29 /// Sends the specified activity to the conversation endpoint asynchronously.
30 /// </summary>
31 /// <param name="activity">The activity to send. Cannot be null. The activity must contain valid conversation and service URL information.</param>
32 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
33 /// <param name="cancellationToken">A cancellation token that can be used to cancel the send operation.</param>
34 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the sent activity.</returns>
35 /// <exception cref="Exception">Thrown if the activity could not be sent successfully. The exception message includes the HTTP status code and
36 /// response content.</exception>
37 public async Task<SendActivityResponse> SendActivityAsync(CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
38 {
39 ArgumentNullException.ThrowIfNull(activity);
40 ArgumentNullException.ThrowIfNull(activity.Conversation);
41 ArgumentNullException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id);
42 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
43
44 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{activity.Conversation.Id}/activities/";
45
46 return await SendHttpRequestAsync<SendActivityResponse>(
47 HttpMethod.Post,
48 url,
49 activity.ToJson(),
50 activity.From.GetAgenticIdentity(),
51 "sending activity",
52 customHeaders,
53 cancellationToken).ConfigureAwait(false);
54 }
55
56 /// <summary>
57 /// Updates an existing activity in a conversation.
58 /// </summary>
59 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
60 /// <param name="activityId">The ID of the activity to update. Cannot be null or whitespace.</param>
61 /// <param name="activity">The updated activity data. Cannot be null.</param>
62 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
63 /// <param name="cancellationToken">A cancellation token that can be used to cancel the update operation.</param>
64 /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity.</returns>
65 /// <exception cref="HttpRequestException">Thrown if the activity could not be updated successfully.</exception>
66 public async Task<UpdateActivityResponse> UpdateActivityAsync(string conversationId, string activityId, CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
67 {
68 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
69 ArgumentNullException.ThrowIfNullOrWhiteSpace(activityId);
70 ArgumentNullException.ThrowIfNull(activity);
71 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
72
73 string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}";
74
75 return await SendHttpRequestAsync<UpdateActivityResponse>(
76 HttpMethod.Put,
77 url,
78 activity.ToJson(),
79 activity.From.GetAgenticIdentity(),
80 "updating activity",
81 customHeaders,
82 cancellationToken).ConfigureAwait(false);
83 }
84
85
86 /// <summary>
87 /// Deletes an existing activity from a conversation.
88 /// </summary>
89 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
90 /// <param name="activityId">The ID of the activity to delete. Cannot be null or whitespace.</param>
91 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
92 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
93 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
94 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
95 /// <returns>A task that represents the asynchronous operation.</returns>
96 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
97 public async Task DeleteActivityAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
98 {
99 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
100 ArgumentNullException.ThrowIfNullOrWhiteSpace(activityId);
101 ArgumentNullException.ThrowIfNull(serviceUrl);
102
103 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}";
104
105 await SendHttpRequestAsync<DeleteActivityResponse>(
106 HttpMethod.Delete,
107 url,
108 body: null,
109 agenticIdentity: agenticIdentity,
110 "deleting activity",
111 customHeaders,
112 cancellationToken).ConfigureAwait(false);
113 }
114
115 /// <summary>
116 /// Deletes an existing activity from a conversation using activity context.
117 /// </summary>
118 /// <param name="activity">The activity to delete. Must contain valid Id, Conversation.Id, and ServiceUrl. Cannot be null.</param>
119 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
120 /// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
121 /// <returns>A task that represents the asynchronous operation.</returns>
122 /// <exception cref="HttpRequestException">Thrown if the activity could not be deleted successfully.</exception>
123 public async Task DeleteActivityAsync(CoreActivity activity, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
124 {
125 ArgumentNullException.ThrowIfNull(activity);
126 ArgumentNullException.ThrowIfNullOrWhiteSpace(activity.Id);
127 ArgumentNullException.ThrowIfNull(activity.Conversation);
128 ArgumentNullException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id);
129 ArgumentNullException.ThrowIfNull(activity.ServiceUrl);
130
131 await DeleteActivityAsync(
132 activity.Conversation.Id,
133 activity.Id,
134 activity.ServiceUrl,
135 activity.From.GetAgenticIdentity(),
136 customHeaders,
137 cancellationToken).ConfigureAwait(false);
138 }
139
140 /// <summary>
141 /// Gets the members of a conversation.
142 /// </summary>
143 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
144 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
145 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
146 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
147 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
148 /// <returns>A task that represents the asynchronous operation. The task result contains a list of conversation members.</returns>
149 /// <exception cref="HttpRequestException">Thrown if the members could not be retrieved successfully.</exception>
150 public async Task<IList<ConversationAccount>> GetConversationMembersAsync(string conversationId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
151 {
152 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
153 ArgumentNullException.ThrowIfNull(serviceUrl);
154
155 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/members";
156
157 return await SendHttpRequestAsync<IList<ConversationAccount>>(
158 HttpMethod.Get,
159 url,
160 body: null,
161 agenticIdentity,
162 "getting conversation members",
163 customHeaders,
164 cancellationToken).ConfigureAwait(false);
165 }
166
167 /// <summary>
168 /// Gets the conversations in which the bot has participated.
169 /// </summary>
170 /// <param name="serviceUrl">The service URL for the bot. Cannot be null.</param>
171 /// <param name="continuationToken">Optional continuation token for pagination.</param>
172 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
173 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
174 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
175 /// <returns>A task that represents the asynchronous operation. The task result contains the conversations and an optional continuation token.</returns>
176 /// <exception cref="HttpRequestException">Thrown if the conversations could not be retrieved successfully.</exception>
177 public async Task<GetConversationsResponse> GetConversationsAsync(Uri serviceUrl, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
178 {
179 ArgumentNullException.ThrowIfNull(serviceUrl);
180
181 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations";
182 if (!string.IsNullOrWhiteSpace(continuationToken))
183 {
184 url += $"?continuationToken={Uri.EscapeDataString(continuationToken)}";
185 }
186
187 return await SendHttpRequestAsync<GetConversationsResponse>(
188 HttpMethod.Get,
189 url,
190 body: null,
191 agenticIdentity,
192 "getting conversations",
193 customHeaders,
194 cancellationToken).ConfigureAwait(false);
195 }
196
197 /// <summary>
198 /// Gets the members of a specific activity.
199 /// </summary>
200 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
201 /// <param name="activityId">The ID of the activity. Cannot be null or whitespace.</param>
202 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
203 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
204 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
205 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
206 /// <returns>A task that represents the asynchronous operation. The task result contains a list of members for the activity.</returns>
207 /// <exception cref="HttpRequestException">Thrown if the activity members could not be retrieved successfully.</exception>
208 public async Task<IList<ConversationAccount>> GetActivityMembersAsync(string conversationId, string activityId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
209 {
210 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
211 ArgumentNullException.ThrowIfNullOrWhiteSpace(activityId);
212 ArgumentNullException.ThrowIfNull(serviceUrl);
213
214 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}/members";
215
216 return await SendHttpRequestAsync<IList<ConversationAccount>>(
217 HttpMethod.Get,
218 url,
219 body: null,
220 agenticIdentity,
221 "getting activity members",
222 customHeaders,
223 cancellationToken).ConfigureAwait(false);
224 }
225
226 /// <summary>
227 /// Creates a new conversation.
228 /// </summary>
229 /// <param name="parameters">The parameters for creating the conversation. Cannot be null.</param>
230 /// <param name="serviceUrl">The service URL for the bot. Cannot be null.</param>
231 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
232 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
233 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
234 /// <returns>A task that represents the asynchronous operation. The task result contains the conversation resource response with the conversation ID.</returns>
235 /// <exception cref="HttpRequestException">Thrown if the conversation could not be created successfully.</exception>
236 public async Task<CreateConversationResponse> CreateConversationAsync(ConversationParameters parameters, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
237 {
238 ArgumentNullException.ThrowIfNull(parameters);
239 ArgumentNullException.ThrowIfNull(serviceUrl);
240
241 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations";
242
243 return await SendHttpRequestAsync<CreateConversationResponse>(
244 HttpMethod.Post,
245 url,
246 JsonSerializer.Serialize(parameters),
247 agenticIdentity,
248 "creating conversation",
249 customHeaders,
250 cancellationToken).ConfigureAwait(false);
251 }
252
253 /// <summary>
254 /// Gets the members of a conversation one page at a time.
255 /// </summary>
256 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
257 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
258 /// <param name="pageSize">Optional page size for the number of members to retrieve.</param>
259 /// <param name="continuationToken">Optional continuation token for pagination.</param>
260 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
261 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
262 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
263 /// <returns>A task that represents the asynchronous operation. The task result contains a page of members and an optional continuation token.</returns>
264 /// <exception cref="HttpRequestException">Thrown if the conversation members could not be retrieved successfully.</exception>
265 public async Task<PagedMembersResult> GetConversationPagedMembersAsync(string conversationId, Uri serviceUrl, int? pageSize = null, string? continuationToken = null, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
266 {
267 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
268 ArgumentNullException.ThrowIfNull(serviceUrl);
269
270 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/pagedmembers";
271
272 List<string> queryParams = new();
273 if (pageSize.HasValue)
274 {
275 queryParams.Add($"pageSize={pageSize.Value}");
276 }
277 if (!string.IsNullOrWhiteSpace(continuationToken))
278 {
279 queryParams.Add($"continuationToken={Uri.EscapeDataString(continuationToken)}");
280 }
281 if (queryParams.Count > 0)
282 {
283 url += $"?{string.Join("&", queryParams)}";
284 }
285
286 return await SendHttpRequestAsync<PagedMembersResult>(
287 HttpMethod.Get,
288 url,
289 body: null,
290 agenticIdentity,
291 "getting paged conversation members",
292 customHeaders,
293 cancellationToken).ConfigureAwait(false);
294 }
295
296 /// <summary>
297 /// Deletes a member from a conversation.
298 /// </summary>
299 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
300 /// <param name="memberId">The ID of the member to delete. Cannot be null or whitespace.</param>
301 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
302 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
303 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
304 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
305 /// <returns>A task that represents the asynchronous operation.</returns>
306 /// <exception cref="HttpRequestException">Thrown if the member could not be deleted successfully.</exception>
307 /// <remarks>If the deleted member was the last member of the conversation, the conversation is also deleted.</remarks>
308 public async Task DeleteConversationMemberAsync(string conversationId, string memberId, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
309 {
310 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
311 ArgumentNullException.ThrowIfNullOrWhiteSpace(memberId);
312 ArgumentNullException.ThrowIfNull(serviceUrl);
313
314 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/members/{memberId}";
315
316 await SendHttpRequestAsync<object>(
317 HttpMethod.Delete,
318 url,
319 body: null,
320 agenticIdentity,
321 "deleting conversation member",
322 customHeaders,
323 cancellationToken).ConfigureAwait(false);
324 }
325
326 /// <summary>
327 /// Uploads and sends historic activities to the conversation.
328 /// </summary>
329 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
330 /// <param name="transcript">The transcript containing the historic activities. Cannot be null.</param>
331 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
332 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
333 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
334 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
335 /// <returns>A task that represents the asynchronous operation. The task result contains the response with a resource ID.</returns>
336 /// <exception cref="HttpRequestException">Thrown if the history could not be sent successfully.</exception>
337 /// <remarks>Activities in the transcript must have unique IDs and appropriate timestamps for proper rendering.</remarks>
338 public async Task<SendConversationHistoryResponse> SendConversationHistoryAsync(string conversationId, Transcript transcript, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
339 {
340 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
341 ArgumentNullException.ThrowIfNull(transcript);
342 ArgumentNullException.ThrowIfNull(serviceUrl);
343
344 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/history";
345
346 return await SendHttpRequestAsync<SendConversationHistoryResponse>(
347 HttpMethod.Post,
348 url,
349 JsonSerializer.Serialize(transcript),
350 agenticIdentity,
351 "sending conversation history",
352 customHeaders,
353 cancellationToken).ConfigureAwait(false);
354 }
355
356 /// <summary>
357 /// Uploads an attachment to the channel's blob storage.
358 /// </summary>
359 /// <param name="conversationId">The ID of the conversation. Cannot be null or whitespace.</param>
360 /// <param name="attachmentData">The attachment data to upload. Cannot be null.</param>
361 /// <param name="serviceUrl">The service URL for the conversation. Cannot be null.</param>
362 /// <param name="agenticIdentity">Optional agentic identity for authentication.</param>
363 /// <param name="customHeaders">Optional custom headers to include in the request.</param>
364 /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
365 /// <returns>A task that represents the asynchronous operation. The task result contains the response with an attachment ID.</returns>
366 /// <exception cref="HttpRequestException">Thrown if the attachment could not be uploaded successfully.</exception>
367 /// <remarks>This is useful for storing data in a compliant store when dealing with enterprises.</remarks>
368 public async Task<UploadAttachmentResponse> UploadAttachmentAsync(string conversationId, AttachmentData attachmentData, Uri serviceUrl, AgenticIdentity? agenticIdentity = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default)
369 {
370 ArgumentNullException.ThrowIfNullOrWhiteSpace(conversationId);
371 ArgumentNullException.ThrowIfNull(attachmentData);
372 ArgumentNullException.ThrowIfNull(serviceUrl);
373
374 string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/attachments";
375
376 return await SendHttpRequestAsync<UploadAttachmentResponse>(
377 HttpMethod.Post,
378 url,
379 JsonSerializer.Serialize(attachmentData),
380 agenticIdentity,
381 "uploading attachment",
382 customHeaders,
383 cancellationToken).ConfigureAwait(false);
384 }
385
386 private async Task<T> SendHttpRequestAsync<T>(HttpMethod method, string url, string? body, AgenticIdentity? agenticIdentity, string operationDescription, CustomHeaders? customHeaders, CancellationToken cancellationToken)
387 {
388 using HttpRequestMessage request = new(method, url);
389
390 if (body is not null)
391 {
392 request.Content = new StringContent(body, Encoding.UTF8, MediaTypeNames.Application.Json);
393 }
394
395 if (agenticIdentity is not null)
396 {
397 request.Options.Set(BotAuthenticationHandler.AgenticIdentityKey, agenticIdentity);
398 }
399
400 // Apply default custom headers
401 foreach (var header in DefaultCustomHeaders)
402 {
403 request.Headers.TryAddWithoutValidation(header.Key, header.Value);
404 }
405
406 // Apply method-level custom headers (these override default headers if same key)
407 if (customHeaders is not null)
408 {
409 foreach (var header in customHeaders)
410 {
411 request.Headers.Remove(header.Key);
412 request.Headers.TryAddWithoutValidation(header.Key, header.Value);
413 }
414 }
415
416 using HttpResponseMessage resp = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
417
418 if (resp.IsSuccessStatusCode)
419 {
420 string responseString = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
421 if (responseString.Length > 2) // to handle empty response
422 {
423 T? result = JsonSerializer.Deserialize<T>(responseString);
424 return result ?? throw new InvalidOperationException($"Failed to deserialize response for {operationDescription}");
425 }
426 // Empty response - return default value (e.g., for DELETE operations)
427 return default!;
428 }
429 else
430 {
431 string errResponseString = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
432 throw new HttpRequestException($"Error {operationDescription} {resp.StatusCode}. {errResponseString}");
433 }
434 }
435
436}
437