diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 56eee02abc8e..5f0602cd5cd7 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -84,7 +84,7 @@ - + @@ -144,7 +144,7 @@ - + diff --git a/dotnet/samples/Concepts/Plugins/MsGraph_CalendarPlugin.cs b/dotnet/samples/Concepts/Plugins/MsGraph_CalendarPlugin.cs new file mode 100644 index 000000000000..810c92f48b52 --- /dev/null +++ b/dotnet/samples/Concepts/Plugins/MsGraph_CalendarPlugin.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Azure.Identity; +using Microsoft.Graph; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.MsGraph; +using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; + +namespace Plugins; + +/// +/// This example shows how to use Microsoft Graph Plugin +/// These examples require a valid Microsoft account and delegated/application access for the Microsoft Graph used resources. +/// +public class MsGraph_CalendarPlugin(ITestOutputHelper output) : BaseTest(output) +{ + private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true }; + + /// Shows how to use Microsoft Graph Calendar Plugin with AI Models. + [Fact] + public async Task UsingWithAIModel() + { + // Setup the Kernel + Kernel kernel = Kernel.CreateBuilder() + .AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) + .Build(); + + using var graphClient = GetGraphClient(); + + var calendarConnector = new OutlookCalendarConnector(graphClient); + + // Add the plugin to the Kernel + var graphPlugin = kernel.Plugins.AddFromObject(new CalendarPlugin(calendarConnector, jsonSerializerOptions: s_options)); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; + + string Prompt = $""" + 1. Show me the next 10 calendar events I have + 2. If I don't have any event named "Semantic Kernel", please create a new event named "Semantic Kernel" + starting at {DateTimeOffset.Now.AddHours(1)} with 1 hour of duration. + """; + + // Invoke the OneDrive plugin multiple times + var result = await kernel.InvokePromptAsync(Prompt, new(settings)); + + Console.WriteLine(result); + } + + private static GraphServiceClient GetGraphClient() + { + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions() + { + ClientId = TestConfiguration.MSGraph.ClientId, + TenantId = TestConfiguration.MSGraph.TenantId, + RedirectUri = TestConfiguration.MSGraph.RedirectUri, + }); + + return new GraphServiceClient(credential); + } +} diff --git a/dotnet/samples/Concepts/Plugins/MsGraph_EmailPlugin.cs b/dotnet/samples/Concepts/Plugins/MsGraph_EmailPlugin.cs new file mode 100644 index 000000000000..1d4b7bd77aa5 --- /dev/null +++ b/dotnet/samples/Concepts/Plugins/MsGraph_EmailPlugin.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.Identity; +using Microsoft.Graph; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; + +namespace Plugins; + +/// +/// This example shows how to use Microsoft Graph Plugin +/// These examples require a valid Microsoft account and delegated/application access for the used resources. +/// +public class MsGraph_EmailPlugin(ITestOutputHelper output) : BaseTest(output) +{ + /// Shows how to use Microsoft Graph Email Plugin with AI Models. + [Fact] + public async Task EmailPlugin_SendEmailToMyself() + { + // Setup the Kernel + Kernel kernel = Kernel.CreateBuilder() + .AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) + .Build(); + + using var graphClient = GetGraphClient(); + + var emailConnector = new OutlookMailConnector(graphClient); + + // Add the plugin to the Kernel + var graphPlugin = kernel.Plugins.AddFromObject(new Microsoft.SemanticKernel.Plugins.MsGraph.EmailPlugin(emailConnector)); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; + + const string Prompt = """ + Using the tools available, please do the following: + 1. Get my email address + 2. Send an email to myself with the subject "FYI" and content "This is a very important email" + 3. List 10 of my email messages + """; + + // Invoke the Graph plugin with a prompt + var result = await kernel.InvokePromptAsync(Prompt, new(settings)); + + Console.WriteLine(result); + } + + private static GraphServiceClient GetGraphClient() + { + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions() + { + ClientId = TestConfiguration.MSGraph.ClientId, + TenantId = TestConfiguration.MSGraph.TenantId, + RedirectUri = TestConfiguration.MSGraph.RedirectUri, + }); + + return new GraphServiceClient(credential); + } +} diff --git a/dotnet/samples/Concepts/Plugins/MsGraph_OneDrivePlugin.cs b/dotnet/samples/Concepts/Plugins/MsGraph_OneDrivePlugin.cs new file mode 100644 index 000000000000..7b53158cbabb --- /dev/null +++ b/dotnet/samples/Concepts/Plugins/MsGraph_OneDrivePlugin.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.Identity; +using Microsoft.Graph; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.MsGraph; +using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; + +namespace Plugins; + +/// +/// This example shows how to use Microsoft Graph Plugin +/// These examples require a valid Microsoft account and delegated/application access for the used resources. +/// +public class MsGraph_OneDrivePlugin(ITestOutputHelper output) : BaseTest(output) +{ + /// Shows how to use Microsoft Graph OneDrive Plugin with AI Models. + [Fact] + public async Task UsingWithAIModel() + { + // Setup the Kernel + Kernel kernel = Kernel.CreateBuilder() + .AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) + .Build(); + + using var graphClient = GetGraphClient(); + var connector = new OneDriveConnector(graphClient); + + // Add the plugin to the Kernel + var graphPlugin = kernel.Plugins.AddFromObject(new CloudDrivePlugin(connector)); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; + + const string Prompt = """ + I need you to do the following things with the tools available: + 1. Update the current file: "Resources/travelinfo.txt" to my OneDrive into the "Test" folder. + 2. Generate a OneDrive Link for sharing the file + 3. Summarize for me the contents of the uploaded file + 4. Show me the generated shared link. + """; + + // Invoke the OneDrive plugin multiple times + var result = await kernel.InvokePromptAsync(Prompt, new(settings)); + + Console.WriteLine($"Assistant: {result}"); + } + + private static GraphServiceClient GetGraphClient() + { + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions() + { + ClientId = TestConfiguration.MSGraph.ClientId, + TenantId = TestConfiguration.MSGraph.TenantId, + RedirectUri = TestConfiguration.MSGraph.RedirectUri, + }); + + return new GraphServiceClient(credential); + } +} diff --git a/dotnet/samples/Concepts/Plugins/MsGraph_OrganizationHierarchyPlugin.cs b/dotnet/samples/Concepts/Plugins/MsGraph_OrganizationHierarchyPlugin.cs new file mode 100644 index 000000000000..1e6b2f5b0fe7 --- /dev/null +++ b/dotnet/samples/Concepts/Plugins/MsGraph_OrganizationHierarchyPlugin.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Azure.Identity; +using Microsoft.Graph; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.MsGraph; +using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; + +namespace Plugins; + +/// +/// This example shows how to use Microsoft Graph Plugin +/// These examples require a valid Microsoft account and delegated/application access for the used resources. +/// +public class MsGraph_OrganizationHierarchyPlugin(ITestOutputHelper output) : BaseTest(output) +{ + private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true }; + + /// Shows how to use Microsoft Graph Organization Hierarchy Plugin with AI Models. + [Fact] + public async Task UsingWithAIModel() + { + // Setup the Kernel + Kernel kernel = Kernel.CreateBuilder() + .AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) + .Build(); + + using var graphClient = GetGraphClient(); + var connector = new OrganizationHierarchyConnector(graphClient); + + // Add the plugin to the Kernel + var graphPlugin = kernel.Plugins.AddFromObject(new OrganizationHierarchyPlugin(connector, s_options)); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; + + const string Prompt = "I need you to show my manager details as well as my direct reports using the tools available:"; + + // Invoke the OneDrive plugin multiple times + var result = await kernel.InvokePromptAsync(Prompt, new(settings)); + + Console.WriteLine($"Assistant: {result}"); + } + + private static GraphServiceClient GetGraphClient() + { + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions() + { + ClientId = TestConfiguration.MSGraph.ClientId, + TenantId = TestConfiguration.MSGraph.TenantId, + RedirectUri = TestConfiguration.MSGraph.RedirectUri, + }); + + return new GraphServiceClient(credential); + } +} diff --git a/dotnet/samples/Concepts/Plugins/MsGraph_TaskListPlugin.cs b/dotnet/samples/Concepts/Plugins/MsGraph_TaskListPlugin.cs new file mode 100644 index 000000000000..70ac5ea360d2 --- /dev/null +++ b/dotnet/samples/Concepts/Plugins/MsGraph_TaskListPlugin.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Azure.Identity; +using Microsoft.Graph; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.MsGraph; +using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; + +namespace Plugins; + +/// +/// This example shows how to use Microsoft Graph Plugin +/// These examples require a valid Microsoft account and delegated/application access for the used resources. +/// +public class MsGraph_TaskListPlugin(ITestOutputHelper output) : BaseTest(output) +{ + private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true }; + + /// Shows how to use Microsoft Graph To-Do Tasks Plugin with AI Models. + [Fact] + public async Task UsingWithAIModel() + { + // Setup the Kernel + Kernel kernel = Kernel.CreateBuilder() + .AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) + .Build(); + + using var graphClient = GetGraphClient(); + var connector = new MicrosoftToDoConnector(graphClient); + + // Add the plugin to the Kernel + var graphPlugin = kernel.Plugins.AddFromObject(new TaskListPlugin(connector, jsonSerializerOptions: s_options)); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; + + const string Prompt = """ + 1. Show me all the tasks I have + 3. If I don't have a task named "Semantic Kernel", please create one + """; + + // Invoke the OneDrive plugin multiple times + var result = await kernel.InvokePromptAsync(Prompt, new(settings)); + + Console.WriteLine($"Assistant: {result}"); + } + + private static GraphServiceClient GetGraphClient() + { + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions() + { + ClientId = TestConfiguration.MSGraph.ClientId, + TenantId = TestConfiguration.MSGraph.TenantId, + RedirectUri = TestConfiguration.MSGraph.RedirectUri, + }); + + return new GraphServiceClient(credential); + } +} diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md index d58d2d959fda..0facc49d7196 100644 --- a/dotnet/samples/Concepts/README.md +++ b/dotnet/samples/Concepts/README.md @@ -185,6 +185,11 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom - [DescribeAllPluginsAndFunctions](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/DescribeAllPluginsAndFunctions.cs) - [GroundednessChecks](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/GroundednessChecks.cs) - [ImportPluginFromGrpc](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/ImportPluginFromGrpc.cs) +- [MsGraph_CalendarPlugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/MsGraph_CalendarPlugin.cs) +- [MsGraph_EmailPlugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/MsGraph_EmailPlugin.cs) +- [MsGraph_ContactsPlugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/MsGraph_ContactsPlugin.cs) +- [MsGraph_DrivePlugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/MsGraph_DrivePlugin.cs) +- [MsGraph_TasksPlugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/MsGraph_TasksPlugin.cs) - [TransformPlugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/TransformPlugin.cs) - [CopilotAgentBasedPlugins](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CopilotAgentBasedPlugins.cs) - [WebPlugins](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/WebPlugins.cs) diff --git a/dotnet/src/Functions/Functions.Prompty/Functions.Prompty.csproj b/dotnet/src/Functions/Functions.Prompty/Functions.Prompty.csproj index 3b7fb3d4839c..3e59d45c3fb3 100644 --- a/dotnet/src/Functions/Functions.Prompty/Functions.Prompty.csproj +++ b/dotnet/src/Functions/Functions.Prompty/Functions.Prompty.csproj @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/dotnet/src/Plugins/Plugins.MsGraph/CalendarPlugin.cs b/dotnet/src/Plugins/Plugins.MsGraph/CalendarPlugin.cs index 9b62a1f3cd5c..4999432c4dfb 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/CalendarPlugin.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/CalendarPlugin.cs @@ -22,6 +22,7 @@ public sealed class CalendarPlugin { private readonly ICalendarConnector _connector; private readonly ILogger _logger; + private readonly JsonSerializerOptions? _jsonSerializerOptions; private static readonly JsonSerializerOptions s_options = new() { WriteIndented = false, @@ -34,10 +35,13 @@ public sealed class CalendarPlugin /// /// Calendar connector. /// The to use for logging. If null, no logging will be performed. - public CalendarPlugin(ICalendarConnector connector, ILoggerFactory? loggerFactory = null) + /// The to use for serialization. If null, default options will be used. + public CalendarPlugin(ICalendarConnector connector, ILoggerFactory? loggerFactory = null, JsonSerializerOptions? jsonSerializerOptions = null) { Ensure.NotNull(connector, nameof(connector)); + this._jsonSerializerOptions = jsonSerializerOptions ?? s_options; + this._connector = connector; this._logger = loggerFactory?.CreateLogger(typeof(CalendarPlugin)) ?? NullLogger.Instance; } @@ -87,13 +91,13 @@ public async Task GetCalendarEventsAsync( const string SelectString = "start,subject,organizer,location"; - IEnumerable events = await this._connector.GetEventsAsync( + IEnumerable? events = await this._connector.GetEventsAsync( top: maxResults, skip: skip, select: SelectString, cancellationToken ).ConfigureAwait(false); - return JsonSerializer.Serialize(value: events, options: s_options); + return JsonSerializer.Serialize(value: events, options: this._jsonSerializerOptions); } } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/CloudDrivePlugin.cs b/dotnet/src/Plugins/Plugins.MsGraph/CloudDrivePlugin.cs index 6c87c2736bb7..de8660092fe4 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/CloudDrivePlugin.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/CloudDrivePlugin.cs @@ -39,12 +39,18 @@ public CloudDrivePlugin(ICloudDriveConnector connector, ILoggerFactory? loggerFa /// A cancellation token to observe while waiting for the task to complete. /// A string containing the file content. [KernelFunction, Description("Get the contents of a file in a cloud drive.")] - public async Task GetFileContentAsync( + public async Task GetFileContentAsync( [Description("Path to file")] string filePath, CancellationToken cancellationToken = default) { this._logger.LogDebug("Getting file content for '{0}'", filePath); - Stream fileContentStream = await this._connector.GetFileContentStreamAsync(filePath, cancellationToken).ConfigureAwait(false); + Stream? fileContentStream = await this._connector.GetFileContentStreamAsync(filePath, cancellationToken).ConfigureAwait(false); + + if (fileContentStream is null) + { + this._logger.LogDebug("File content stream for '{0}' is null", filePath); + return null; + } using StreamReader sr = new(fileContentStream); return await sr.ReadToEndAsync( diff --git a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/MicrosoftGraphModelExtensions.cs b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/MicrosoftGraphModelExtensions.cs index 1c5280a4894f..b55ba8d9b1f7 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/MicrosoftGraphModelExtensions.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/MicrosoftGraphModelExtensions.cs @@ -2,8 +2,7 @@ using System; using System.Linq; -using Microsoft.Graph; -using Microsoft.Graph.Extensions; +using Microsoft.Graph.Models; using Microsoft.SemanticKernel.Plugins.MsGraph.Models; namespace Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; @@ -16,19 +15,19 @@ internal static class MicrosoftGraphModelExtensions /// /// Convert a Microsoft Graph message to an email message. /// - public static Models.EmailMessage ToEmailMessage(this Message graphMessage) + public static Models.EmailMessage ToEmailMessage(this Graph.Models.Message graphMessage) => new() { - BccRecipients = graphMessage.BccRecipients?.Select(r => r.EmailAddress.ToEmailAddress()), + BccRecipients = graphMessage.BccRecipients?.Select(r => r.EmailAddress!.ToEmailAddress()), Body = graphMessage.Body?.Content, #pragma warning disable CA1307 // Specify StringComparison for clarity - BodyPreview = graphMessage.BodyPreview.Replace("\u200C", ""), // BodyPreviews are sometimes filled with zero-width non-joiner characters - remove them. + BodyPreview = graphMessage.BodyPreview?.Replace("\u200C", ""), // BodyPreviews are sometimes filled with zero-width non-joiner characters - remove them. #pragma warning restore CA1307 - CcRecipients = graphMessage.CcRecipients?.Select(r => r.EmailAddress.ToEmailAddress()), + CcRecipients = graphMessage.CcRecipients?.Select(r => r.EmailAddress!.ToEmailAddress()), From = graphMessage.From?.EmailAddress?.ToEmailAddress(), IsRead = graphMessage.IsRead, ReceivedDateTime = graphMessage.ReceivedDateTime, - Recipients = graphMessage.ToRecipients?.Select(r => r.EmailAddress.ToEmailAddress()), + Recipients = graphMessage.ToRecipients?.Select(r => r.EmailAddress!.ToEmailAddress()), SentDateTime = graphMessage.SentDateTime, Subject = graphMessage.Subject }; @@ -36,7 +35,7 @@ public static Models.EmailMessage ToEmailMessage(this Message graphMessage) /// /// Convert a Microsoft Graph email address to an email address. /// - public static Models.EmailAddress ToEmailAddress(this Microsoft.Graph.EmailAddress graphEmailAddress) + public static Models.EmailAddress ToEmailAddress(this Microsoft.Graph.Models.EmailAddress graphEmailAddress) => new() { Address = graphEmailAddress.Address, @@ -46,25 +45,25 @@ public static Models.EmailAddress ToEmailAddress(this Microsoft.Graph.EmailAddre /// /// Convert a calendar event to a Microsoft Graph event. /// - public static Graph.Event ToGraphEvent(this CalendarEvent calendarEvent) + public static Graph.Models.Event ToGraphEvent(this CalendarEvent calendarEvent) => new() { Subject = calendarEvent.Subject, - Body = new ItemBody { Content = calendarEvent.Content, ContentType = BodyType.Html }, + Body = new Graph.Models.ItemBody { Content = calendarEvent.Content, ContentType = Microsoft.Graph.Models.BodyType.Html }, Start = calendarEvent.Start.HasValue - ? DateTimeTimeZone.FromDateTimeOffset(calendarEvent.Start.Value) - : DateTimeTimeZone.FromDateTime(System.DateTime.Now), + ? calendarEvent.Start.Value.ToDateTimeTimeZone() + : System.DateTime.Now.ToDateTimeTimeZone(), End = calendarEvent.End.HasValue - ? DateTimeTimeZone.FromDateTimeOffset(calendarEvent.End.Value) - : DateTimeTimeZone.FromDateTime(System.DateTime.Now + TimeSpan.FromHours(1)), - Location = new Location { DisplayName = calendarEvent.Location }, - Attendees = calendarEvent.Attendees?.Select(a => new Attendee { EmailAddress = new Microsoft.Graph.EmailAddress { Address = a } }) + ? calendarEvent.End.Value.ToDateTimeTimeZone() + : (System.DateTime.Now + TimeSpan.FromHours(1)).ToDateTimeTimeZone(), + Location = new Microsoft.Graph.Models.Location { DisplayName = calendarEvent.Location }, + Attendees = calendarEvent.Attendees?.Select(a => new Microsoft.Graph.Models.Attendee { EmailAddress = new Microsoft.Graph.Models.EmailAddress { Address = a } })?.ToList() }; /// /// Convert a Microsoft Graph event to a calendar event. /// - public static Models.CalendarEvent ToCalendarEvent(this Event msGraphEvent) + public static Models.CalendarEvent ToCalendarEvent(this Graph.Models.Event msGraphEvent) => new() { Subject = msGraphEvent.Subject, @@ -72,6 +71,6 @@ public static Models.CalendarEvent ToCalendarEvent(this Event msGraphEvent) Start = msGraphEvent.Start?.ToDateTimeOffset(), End = msGraphEvent.End?.ToDateTimeOffset(), Location = msGraphEvent.Location?.DisplayName, - Attendees = msGraphEvent.Attendees?.Select(a => a.EmailAddress.Address) + Attendees = msGraphEvent.Attendees?.Where(a => a.EmailAddress?.Address is not null).Select(a => a.EmailAddress!.Address!), }; } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/MicrosoftToDoConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/MicrosoftToDoConnector.cs index cfba57b21c2c..e738165d11d2 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/MicrosoftToDoConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/MicrosoftToDoConnector.cs @@ -7,9 +7,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Graph; +using Microsoft.Graph.Models; using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.Diagnostics; using Microsoft.SemanticKernel.Plugins.MsGraph.Models; -using TaskStatus = Microsoft.Graph.TaskStatus; +using TaskStatus = Microsoft.Graph.Models.TaskStatus; namespace Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; @@ -35,87 +36,128 @@ public MicrosoftToDoConnector(GraphServiceClient graphServiceClient) // .Filter("wellknownListName eq 'defaultList'") does not work as expected so we grab all the lists locally and filter them by name. // GH issue: https://github.com/microsoftgraph/microsoft-graph-docs/issues/17694 - ITodoListsCollectionPage lists = await this._graphServiceClient.Me - .Todo.Lists - .Request().GetAsync(cancellationToken).ConfigureAwait(false); + // Get the initial page (response won't be null if successful; exceptions are thrown on failure) + var initialPage = await this._graphServiceClient.Me.Todo.Lists.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - TodoTaskList? result = lists.SingleOrDefault(list => list.WellknownListName == WellknownListName.DefaultList); + TodoTaskList? result = null; - while (result is null && lists.Count != 0 && lists.NextPageRequest is not null) + if (initialPage is null) { - lists = await lists.NextPageRequest.GetAsync(cancellationToken).ConfigureAwait(false); - result = lists.SingleOrDefault(list => list.WellknownListName == WellknownListName.DefaultList); + return null; } + var pageIterator = PageIterator.CreatePageIterator( + this._graphServiceClient, + initialPage, + (list) => + { + if (list?.WellknownListName == WellknownListName.DefaultList) + { + result = list; + return false; // Stop iterating once found + } + return true; // Continue to next item/page + }); + + await pageIterator.IterateAsync(cancellationToken).ConfigureAwait(false); + if (result is null) { - throw new KernelException("Could not find default task list."); + return null; // No default list found } - return new TaskManagementTaskList(result.Id, result.DisplayName); - } + if (string.IsNullOrEmpty(result.Id)) + { + return null; // Ensure the ID is not null or empty + } + return new TaskManagementTaskList( + result.Id, // We've checked it's not null/empty + result.DisplayName ?? "Unnamed Default List" // Coalesce to a fallback if null + ); + } /// - public async Task> GetTaskListsAsync(CancellationToken cancellationToken = default) + public async Task?> GetTaskListsAsync(CancellationToken cancellationToken = default) { - ITodoListsCollectionPage lists = await this._graphServiceClient.Me - .Todo.Lists - .Request().GetAsync(cancellationToken).ConfigureAwait(false); - - List taskLists = [.. lists]; + // Get the initial page (response won't be null if successful; exceptions thrown on failure) + var response = await this._graphServiceClient.Me.Todo.Lists + .GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - while (lists.Count != 0 && lists.NextPageRequest is not null) + if (response?.Value == null) { - lists = await lists.NextPageRequest.GetAsync(cancellationToken).ConfigureAwait(false); - taskLists.AddRange(lists); + return null; } - return taskLists.Select(list => new TaskManagementTaskList( - id: list.Id, - name: list.DisplayName)); + List? taskLists = null; + + var pageIterator = PageIterator.CreatePageIterator( + this._graphServiceClient, + response, + (list) => + { + (taskLists = []).Add(list); + return true; // Continue to fetch all pages + }); + + await pageIterator.IterateAsync(cancellationToken).ConfigureAwait(false); + + return taskLists?.Select(list => new TaskManagementTaskList( + id: list?.Id, + name: list?.DisplayName)); } /// - public async Task> GetTasksAsync(string listId, bool includeCompleted, CancellationToken cancellationToken = default) + public async Task?> GetTasksAsync(string listId, bool includeCompleted, CancellationToken cancellationToken = default) { Ensure.NotNullOrWhitespace(listId, nameof(listId)); - string filterValue = string.Empty; - if (!includeCompleted) + // Get the initial page with optional filter + var response = await this._graphServiceClient.Me.Todo.Lists[listId].Tasks + .GetAsync(requestConfig => + { + if (!includeCompleted) + { + requestConfig.QueryParameters.Filter = "status ne 'completed'"; + } + }, cancellationToken).ConfigureAwait(false); + + if (response?.Value == null) { - filterValue = "status ne 'completed'"; + return Enumerable.Empty(); } - ITodoTaskListTasksCollectionPage tasksPage = await this._graphServiceClient.Me - .Todo.Lists[listId] - .Tasks.Request().Filter(filterValue).GetAsync(cancellationToken).ConfigureAwait(false); - - List tasks = [.. tasksPage]; - - while (tasksPage.Count != 0 && tasksPage.NextPageRequest is not null) - { - tasksPage = await tasksPage.NextPageRequest.GetAsync(cancellationToken).ConfigureAwait(false); - tasks.AddRange(tasksPage); - } - - return tasks.Select(task => new TaskManagementTask( - id: task.Id, - title: task.Title, - reminder: task.ReminderDateTime?.DateTime, - due: task.DueDateTime?.DateTime, - isCompleted: task.Status == TaskStatus.Completed)); + List? tasks = null; + + var pageIterator = PageIterator.CreatePageIterator( + this._graphServiceClient, + response, + (task) => + { + (tasks = []).Add(task); + return true; // Continue to fetch all pages + }); + + await pageIterator.IterateAsync(cancellationToken).ConfigureAwait(false); + + return tasks?.Select(task => new TaskManagementTask( + id: task?.Id, + title: task?.Title, + reminder: task?.ReminderDateTime?.DateTime, + due: task?.DueDateTime?.DateTime, + isCompleted: task?.Status == TaskStatus.Completed)); } /// - public async Task AddTaskAsync(string listId, TaskManagementTask task, CancellationToken cancellationToken = default) + public async Task AddTaskAsync(string listId, TaskManagementTask task, CancellationToken cancellationToken = default) { Ensure.NotNullOrWhitespace(listId, nameof(listId)); Ensure.NotNull(task, nameof(task)); - return ToTaskListTask(await this._graphServiceClient.Me - .Todo.Lists[listId] - .Tasks - .Request().AddAsync(FromTaskListTask(task), cancellationToken).ConfigureAwait(false)); + var createdTask = await this._graphServiceClient.Me.Todo.Lists[listId].Tasks + .PostAsync(FromTaskListTask(task), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return createdTask != null ? ToTaskListTask(createdTask) : null; } /// @@ -126,8 +168,7 @@ public Task DeleteTaskAsync(string listId, string taskId, CancellationToken canc return this._graphServiceClient.Me .Todo.Lists[listId] - .Tasks[taskId] - .Request().DeleteAsync(cancellationToken); + .Tasks[taskId].DeleteAsync(cancellationToken: cancellationToken); } private static TodoTask FromTaskListTask(TaskManagementTask task) @@ -139,10 +180,10 @@ private static TodoTask FromTaskListTask(TaskManagementTask task) Title = task.Title, ReminderDateTime = task.Reminder is null ? null - : DateTimeTimeZone.FromDateTimeOffset(DateTimeOffset.Parse(task.Reminder, CultureInfo.InvariantCulture.DateTimeFormat)), + : DateTimeOffset.Parse(task.Reminder, CultureInfo.InvariantCulture.DateTimeFormat).ToDateTimeTimeZone(), DueDateTime = task.Due is null ? null - : DateTimeTimeZone.FromDateTimeOffset(DateTimeOffset.Parse(task.Due, CultureInfo.InvariantCulture.DateTimeFormat)), + : DateTimeOffset.Parse(task.Due, CultureInfo.InvariantCulture.DateTimeFormat).ToDateTimeTimeZone(), Status = task.IsCompleted ? TaskStatus.Completed : TaskStatus.NotStarted }; } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OneDriveConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OneDriveConnector.cs index ff2b541807e1..78c93e472665 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OneDriveConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OneDriveConnector.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Graph; +using Microsoft.Graph.Models; using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.Diagnostics; namespace Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; @@ -28,14 +29,16 @@ public OneDriveConnector(GraphServiceClient graphServiceClient) } /// - public async Task GetFileContentStreamAsync(string filePath, CancellationToken cancellationToken = default) + public async Task GetFileContentStreamAsync(string filePath, CancellationToken cancellationToken = default) { Ensure.NotNullOrWhitespace(filePath, nameof(filePath)); - return await this._graphServiceClient.Me - .Drive.Root - .ItemWithPath(filePath).Content - .Request().GetAsync(cancellationToken).ConfigureAwait(false); + var myDrive = await this._graphServiceClient.Me.Drive.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + return await this._graphServiceClient + .Drives[myDrive!.Id].Root.ItemWithPath(filePath).Content + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); } /// @@ -50,9 +53,10 @@ public async Task FileExistsAsync(string filePath, CancellationToken cance try { - await this._graphServiceClient.Me - .Drive.Root - .ItemWithPath(filePath).Request().GetAsync(cancellationToken).ConfigureAwait(false); + var myDrive = await this._graphServiceClient.Me.Drive.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + await this._graphServiceClient + .Drives[myDrive!.Id].Root.ItemWithPath(filePath).GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); // If no exception is thrown, the file exists. return true; @@ -60,12 +64,12 @@ await this._graphServiceClient.Me catch (ServiceException ex) { // If the exception is a 404 Not Found, the file does not exist. - if (ex.StatusCode == HttpStatusCode.NotFound) + if (ex.ResponseStatusCode == (int)HttpStatusCode.NotFound) { return false; } - throw new HttpOperationException(ex.StatusCode, responseContent: null, ex.Message, ex); + throw new HttpOperationException((HttpStatusCode)ex.ResponseStatusCode, responseContent: null, ex.Message, ex); } } @@ -85,24 +89,27 @@ public async Task UploadSmallFileAsync(string filePath, string destinationPath, using FileStream fileContentStream = new(filePath, FileMode.Open, FileAccess.Read); - GraphResponse? response = null; + DriveItem? response = null; try { - response = await this._graphServiceClient.Me - .Drive.Root - .ItemWithPath(destinationPath).Content - .Request().PutResponseAsync(fileContentStream, cancellationToken, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false); + var myDrive = await this._graphServiceClient.Me.Drive.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - response.ToHttpResponseMessage().EnsureSuccessStatusCode(); + response = await this._graphServiceClient + .Drives[myDrive!.Id].Root + .ItemWithPath(destinationPath).Content.PutAsync(fileContentStream, cancellationToken: cancellationToken).ConfigureAwait(false); } catch (ServiceException ex) { - throw new HttpOperationException(ex.StatusCode, responseContent: null, ex.Message, ex); + throw new HttpOperationException((HttpStatusCode)ex.ResponseStatusCode, responseContent: null, ex.Message, ex); } catch (HttpRequestException ex) { - throw new HttpOperationException(response?.StatusCode, responseContent: null, ex.Message, ex); +#if NET8_0_OR_GREATER + throw new HttpOperationException(ex.StatusCode, responseContent: null, ex.Message, ex); +#else + throw new HttpOperationException(null, responseContent: null, ex.Message, ex); +#endif } } @@ -114,28 +121,32 @@ public async Task CreateShareLinkAsync(string filePath, string type = "v Ensure.NotNullOrWhitespace(type, nameof(type)); Ensure.NotNullOrWhitespace(scope, nameof(scope)); - GraphResponse? response = null; + Permission? response = null; try { - response = await this._graphServiceClient.Me - .Drive.Root - .ItemWithPath(filePath) - .CreateLink(type, scope) - .Request().PostResponseAsync(cancellationToken).ConfigureAwait(false); + var myDrive = await this._graphServiceClient.Me.Drive.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - response.ToHttpResponseMessage().EnsureSuccessStatusCode(); + response = await this._graphServiceClient + .Drives[myDrive!.Id].Root + .ItemWithPath(filePath) + .CreateLink.PostAsync(new() { Type = type, Scope = scope }, cancellationToken: cancellationToken) + .ConfigureAwait(false); } catch (ServiceException ex) { - throw new HttpOperationException(ex.StatusCode, responseContent: null, ex.Message, ex); + throw new HttpOperationException((HttpStatusCode)ex.ResponseStatusCode, responseContent: null, ex.Message, ex); } catch (HttpRequestException ex) { - throw new HttpOperationException(response?.StatusCode, responseContent: null, ex.Message, ex); +#if NET8_0_OR_GREATER + throw new HttpOperationException(ex.StatusCode, responseContent: null, ex.Message, ex); +#else + throw new HttpOperationException(null, responseContent: null, ex.Message, ex); +#endif } - string? result = (await response.GetResponseObjectAsync().ConfigureAwait(false)).Link?.WebUrl; + string? result = response?.Link?.WebUrl; if (string.IsNullOrWhiteSpace(result)) { throw new KernelException("Shareable file link was null or whitespace."); diff --git a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OrganizationHierarchyConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OrganizationHierarchyConnector.cs index 04893f4cf9ba..b28d4448c32c 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OrganizationHierarchyConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OrganizationHierarchyConnector.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Graph; +using Microsoft.Graph.Models; namespace Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; @@ -25,32 +26,32 @@ public OrganizationHierarchyConnector(GraphServiceClient graphServiceClient) } /// - public async Task GetManagerEmailAsync(CancellationToken cancellationToken = default) => - ((User)await this._graphServiceClient.Me - .Manager - .Request().GetAsync(cancellationToken).ConfigureAwait(false)).UserPrincipalName; + public async Task GetManagerEmailAsync(CancellationToken cancellationToken = default) => + ((User?)await this._graphServiceClient.Me + .Manager.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false))?.UserPrincipalName; /// - public async Task GetManagerNameAsync(CancellationToken cancellationToken = default) => - ((User)await this._graphServiceClient.Me - .Manager - .Request().GetAsync(cancellationToken).ConfigureAwait(false)).DisplayName; + public async Task GetManagerNameAsync(CancellationToken cancellationToken = default) => + ((User?)await this._graphServiceClient.Me + .Manager.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false))?.DisplayName; /// - public async Task> GetDirectReportsEmailAsync(CancellationToken cancellationToken = default) + public async Task?> GetDirectReportsEmailAsync(CancellationToken cancellationToken = default) { - IUserDirectReportsCollectionWithReferencesPage directsPage = await this._graphServiceClient.Me - .DirectReports - .Request().GetAsync(cancellationToken).ConfigureAwait(false); + DirectoryObjectCollectionResponse? directsPage = await this._graphServiceClient.Me + .DirectReports.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - List directs = directsPage.Cast().ToList(); + List? directs = directsPage?.Value?.Cast().ToList(); - while (directs.Count != 0 && directsPage.NextPageRequest is not null) + while (directs is { Count: > 0 } && directsPage!.OdataNextLink is not null) { - directsPage = await directsPage.NextPageRequest.GetAsync(cancellationToken).ConfigureAwait(false); - directs.AddRange(directsPage.Cast()); + directsPage = await this._graphServiceClient.Me.DirectReports.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + if (directsPage?.Value is not null) + { + directs.AddRange(directsPage!.Value.Cast()); + } } - return directs.Select(d => d.UserPrincipalName); + return directs?.Where(d => d.UserPrincipalName is not null)?.Select(d => d.UserPrincipalName!); } } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OutlookCalendarConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OutlookCalendarConnector.cs index bc856c8bbd4b..8fb4da2c50f7 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OutlookCalendarConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OutlookCalendarConnector.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Graph; +using Microsoft.Graph.Models; using Microsoft.SemanticKernel.Plugins.MsGraph.Models; namespace Microsoft.SemanticKernel.Plugins.MsGraph.Connectors; @@ -26,37 +27,26 @@ public OutlookCalendarConnector(GraphServiceClient graphServiceClient) } /// - public async Task AddEventAsync(CalendarEvent calendarEvent, CancellationToken cancellationToken = default) + public async Task AddEventAsync(CalendarEvent calendarEvent, CancellationToken cancellationToken = default) { - Event resultEvent = await this._graphServiceClient.Me.Events.Request() - .AddAsync(calendarEvent.ToGraphEvent(), cancellationToken).ConfigureAwait(false); - return resultEvent.ToCalendarEvent(); + Event? resultEvent = await this._graphServiceClient.Me.Events + .PostAsync(calendarEvent.ToGraphEvent(), cancellationToken: cancellationToken).ConfigureAwait(false); + + return resultEvent?.ToCalendarEvent(); } /// - public async Task> GetEventsAsync( + public async Task?> GetEventsAsync( int? top, int? skip, string? select, CancellationToken cancellationToken = default) { - ICalendarEventsCollectionRequest query = this._graphServiceClient.Me.Calendar.Events.Request(); - - if (top.HasValue) - { - query.Top(top.Value); - } - - if (skip.HasValue) + var result = await this._graphServiceClient.Me.Calendar.Events.GetAsync(config => { - query.Skip(skip.Value); - } - - if (!string.IsNullOrEmpty(select)) - { - query.Select(select); - } - - ICalendarEventsCollectionPage result = await query.GetAsync(cancellationToken).ConfigureAwait(false); + config.QueryParameters.Top = top; + config.QueryParameters.Skip = skip; + config.QueryParameters.Select = !string.IsNullOrEmpty(select) ? [select] : null; + }, cancellationToken: cancellationToken).ConfigureAwait(false); - IEnumerable events = result.Select(e => e.ToCalendarEvent()); + IEnumerable? events = result?.Value?.Select(e => e.ToCalendarEvent()); return events; } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OutlookMailConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OutlookMailConnector.cs index 78c484910bff..6428c08b7d14 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OutlookMailConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/Connectors/OutlookMailConnector.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Graph; +using Microsoft.Graph.Models; using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.Diagnostics; using Microsoft.SemanticKernel.Plugins.MsGraph.Models; @@ -27,8 +28,8 @@ public OutlookMailConnector(GraphServiceClient graphServiceClient) } /// - public async Task GetMyEmailAddressAsync(CancellationToken cancellationToken = default) - => (await this._graphServiceClient.Me.Request().GetAsync(cancellationToken).ConfigureAwait(false)).UserPrincipalName; + public async Task GetMyEmailAddressAsync(CancellationToken cancellationToken = default) + => (await this._graphServiceClient.Me.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false))?.UserPrincipalName; /// public async Task SendEmailAsync(string subject, string content, string[] recipients, CancellationToken cancellationToken = default) @@ -47,36 +48,24 @@ public async Task SendEmailAsync(string subject, string content, string[] recipi { Address = recipientAddress } - }) + }).ToList() }; - await this._graphServiceClient.Me.SendMail(message).Request().PostAsync(cancellationToken).ConfigureAwait(false); + await this._graphServiceClient.Me.SendMail.PostAsync(new() { Message = message }, cancellationToken: cancellationToken).ConfigureAwait(false); } /// - public async Task> GetMessagesAsync( + public async Task?> GetMessagesAsync( int? top, int? skip, string? select, CancellationToken cancellationToken = default) { - IUserMessagesCollectionRequest query = this._graphServiceClient.Me.Messages.Request(); - - if (top.HasValue) - { - query.Top(top.Value); - } - - if (skip.HasValue) + var result = await this._graphServiceClient.Me.Messages.GetAsync((config) => { - query.Skip(skip.Value); - } - - if (!string.IsNullOrEmpty(select)) - { - query.Select(select); - } - - IUserMessagesCollectionPage result = await query.GetAsync(cancellationToken).ConfigureAwait(false); + config.QueryParameters.Top = top; + config.QueryParameters.Skip = skip; + config.QueryParameters.Select = !string.IsNullOrEmpty(select) ? [select] : null; + }, cancellationToken: cancellationToken).ConfigureAwait(false); - IEnumerable messages = result.Select(m => m.ToEmailMessage()); + IEnumerable? messages = result?.Value?.Select(m => m.ToEmailMessage()); return messages; } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/EmailPlugin.cs b/dotnet/src/Plugins/Plugins.MsGraph/EmailPlugin.cs index d4aefd72d64b..bda0d312aa9a 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/EmailPlugin.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/EmailPlugin.cs @@ -21,6 +21,7 @@ public sealed class EmailPlugin { private readonly IEmailConnector _connector; private readonly ILogger _logger; + private readonly JsonSerializerOptions? _jsonSerializerOptions; private static readonly JsonSerializerOptions s_options = new() { WriteIndented = false, @@ -33,10 +34,12 @@ public sealed class EmailPlugin /// /// Email connector. /// The to use for logging. If null, no logging will be performed. - public EmailPlugin(IEmailConnector connector, ILoggerFactory? loggerFactory = null) + /// The to use for serialization. If null, default options will be used. + public EmailPlugin(IEmailConnector connector, ILoggerFactory? loggerFactory = null, JsonSerializerOptions? jsonSerializerOptions = null) { Ensure.NotNull(connector, nameof(connector)); + this._jsonSerializerOptions = jsonSerializerOptions ?? s_options; this._connector = connector; this._logger = loggerFactory?.CreateLogger(typeof(EmailPlugin)) ?? NullLogger.Instance; } @@ -45,7 +48,7 @@ public EmailPlugin(IEmailConnector connector, ILoggerFactory? loggerFactory = nu /// Get my email address. /// [KernelFunction, Description("Gets the email address for me.")] - public async Task GetMyEmailAddressAsync() + public async Task GetMyEmailAddressAsync() => await this._connector.GetMyEmailAddressAsync().ConfigureAwait(false); /// @@ -78,7 +81,7 @@ public async Task SendEmailAsync( /// Get email messages with specified optional clauses used to query for messages. /// [KernelFunction, Description("Get email messages.")] - public async Task GetEmailMessagesAsync( + public async Task GetEmailMessagesAsync( [Description("Optional limit of the number of message to retrieve.")] int? maxResults = 10, [Description("Optional number of message to skip before retrieving results.")] int? skip = 0, CancellationToken cancellationToken = default) @@ -87,13 +90,18 @@ public async Task GetEmailMessagesAsync( const string SelectString = "subject,receivedDateTime,bodyPreview"; - IEnumerable messages = await this._connector.GetMessagesAsync( + IEnumerable? messages = await this._connector.GetMessagesAsync( top: maxResults, skip: skip, select: SelectString, cancellationToken) .ConfigureAwait(false); - return JsonSerializer.Serialize(value: messages, options: s_options); + if (messages is null) + { + return null; + } + + return JsonSerializer.Serialize(value: messages, options: this._jsonSerializerOptions); } } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/ICalendarConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/ICalendarConnector.cs index e12f87cfda79..52fd2ec7d1b4 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/ICalendarConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/ICalendarConnector.cs @@ -18,7 +18,7 @@ public interface ICalendarConnector /// Event to add. /// The to monitor for cancellation requests. The default is . /// Event that was added. - Task AddEventAsync(CalendarEvent calendarEvent, CancellationToken cancellationToken = default); + Task AddEventAsync(CalendarEvent calendarEvent, CancellationToken cancellationToken = default); /// /// Get the user's calendar events. @@ -29,6 +29,6 @@ public interface ICalendarConnector /// Cancellation token /// The user's calendar events. #pragma warning disable CA1716 // Identifiers should not match keywords - Task> GetEventsAsync(int? top, int? skip, string? @select, CancellationToken cancellationToken = default); + Task?> GetEventsAsync(int? top, int? skip, string? @select, CancellationToken cancellationToken = default); #pragma warning restore CA1716 // Identifiers should not match keywords } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/ICloudDriveConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/ICloudDriveConnector.cs index f13fa7240c57..a54d46464ae1 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/ICloudDriveConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/ICloudDriveConnector.cs @@ -26,7 +26,7 @@ public interface ICloudDriveConnector /// /// Path to the remote file. /// The to monitor for cancellation requests. The default is . - Task GetFileContentStreamAsync(string filePath, CancellationToken cancellationToken = default); + Task GetFileContentStreamAsync(string filePath, CancellationToken cancellationToken = default); /// /// Upload a small file (less than 4MB). diff --git a/dotnet/src/Plugins/Plugins.MsGraph/IEmailConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/IEmailConnector.cs index 8faf50f973e2..8b25b2b21123 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/IEmailConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/IEmailConnector.cs @@ -17,7 +17,7 @@ public interface IEmailConnector /// /// The to monitor for cancellation requests. The default is . /// The user's email address. - Task GetMyEmailAddressAsync(CancellationToken cancellationToken = default); + Task GetMyEmailAddressAsync(CancellationToken cancellationToken = default); /// /// Send an email to the specified recipients. @@ -37,6 +37,6 @@ public interface IEmailConnector /// Cancellation token /// The user's email messages. #pragma warning disable CA1716 // Identifiers should not match keywords - Task> GetMessagesAsync(int? top, int? skip, string? @select, CancellationToken cancellationToken = default); + Task?> GetMessagesAsync(int? top, int? skip, string? @select, CancellationToken cancellationToken = default); #pragma warning restore CA1716 // Identifiers should not match keywords } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/IOrganizationHierarchyConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/IOrganizationHierarchyConnector.cs index 543cbf57cad6..48aa3661df27 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/IOrganizationHierarchyConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/IOrganizationHierarchyConnector.cs @@ -16,19 +16,19 @@ public interface IOrganizationHierarchyConnector /// /// The to monitor for cancellation requests. The default is . /// The user's direct reports' email addresses. - Task> GetDirectReportsEmailAsync(CancellationToken cancellationToken = default); + Task?> GetDirectReportsEmailAsync(CancellationToken cancellationToken = default); /// /// Get the user's manager's email address. /// /// The to monitor for cancellation requests. The default is . /// The user's manager's email address. - Task GetManagerEmailAsync(CancellationToken cancellationToken = default); + Task GetManagerEmailAsync(CancellationToken cancellationToken = default); /// /// Get the user's manager's name. /// /// The to monitor for cancellation requests. The default is . /// The user's manager's name. - Task GetManagerNameAsync(CancellationToken cancellationToken = default); + Task GetManagerNameAsync(CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/ITaskManagementConnector.cs b/dotnet/src/Plugins/Plugins.MsGraph/ITaskManagementConnector.cs index 56c6cf7d99a3..b1897aae1af9 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/ITaskManagementConnector.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/ITaskManagementConnector.cs @@ -19,7 +19,7 @@ public interface ITaskManagementConnector /// Task to add. /// The to monitor for cancellation requests. The default is . /// Added task definition. - Task AddTaskAsync(string listId, TaskManagementTask task, CancellationToken cancellationToken = default); + Task AddTaskAsync(string listId, TaskManagementTask task, CancellationToken cancellationToken = default); /// /// Delete a task from a task list. @@ -40,7 +40,7 @@ public interface ITaskManagementConnector /// /// The to monitor for cancellation requests. The default is . /// All of the user's task lists. - Task> GetTaskListsAsync(CancellationToken cancellationToken = default); + Task?> GetTaskListsAsync(CancellationToken cancellationToken = default); /// /// Get the all tasks in a task list. @@ -49,5 +49,5 @@ public interface ITaskManagementConnector /// Whether to include completed tasks. /// The to monitor for cancellation requests. The default is . /// All of the tasks in the specified task list. - Task> GetTasksAsync(string listId, bool includeCompleted, CancellationToken cancellationToken = default); + Task?> GetTasksAsync(string listId, bool includeCompleted, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/Models/TaskManagementTask.cs b/dotnet/src/Plugins/Plugins.MsGraph/Models/TaskManagementTask.cs index 1cdb5e79317b..1a5eb8bb52cd 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/Models/TaskManagementTask.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/Models/TaskManagementTask.cs @@ -10,12 +10,12 @@ public class TaskManagementTask /// /// ID of the task. /// - public string Id { get; set; } + public string? Id { get; set; } /// /// Title of the task. /// - public string Title { get; set; } + public string? Title { get; set; } /// /// Reminder date/time for the task. @@ -40,7 +40,7 @@ public class TaskManagementTask /// Reminder date/time for the task. /// Task's due date/time. /// True if the task is completed, otherwise false. - public TaskManagementTask(string id, string title, string? reminder = null, string? due = null, bool isCompleted = false) + public TaskManagementTask(string? id, string? title, string? reminder = null, string? due = null, bool isCompleted = false) { this.Id = id; this.Title = title; diff --git a/dotnet/src/Plugins/Plugins.MsGraph/Models/TaskManagementTaskList.cs b/dotnet/src/Plugins/Plugins.MsGraph/Models/TaskManagementTaskList.cs index e8cea6d8ead8..fb529eb32946 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/Models/TaskManagementTaskList.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/Models/TaskManagementTaskList.cs @@ -10,19 +10,19 @@ public class TaskManagementTaskList /// /// ID of the task list. /// - public string Id { get; set; } + public string? Id { get; set; } /// /// Name of the task list. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Initializes a new instance of the class. /// /// ID of the task list. /// Name of the task list. - public TaskManagementTaskList(string id, string name) + public TaskManagementTaskList(string? id, string? name) { this.Id = id; this.Name = name; diff --git a/dotnet/src/Plugins/Plugins.MsGraph/OrganizationHierarchyPlugin.cs b/dotnet/src/Plugins/Plugins.MsGraph/OrganizationHierarchyPlugin.cs index a38274d3bd29..05133c9369f6 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/OrganizationHierarchyPlugin.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/OrganizationHierarchyPlugin.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel.Plugins.MsGraph.Diagnostics; @@ -15,15 +16,23 @@ namespace Microsoft.SemanticKernel.Plugins.MsGraph; public sealed class OrganizationHierarchyPlugin { private readonly IOrganizationHierarchyConnector _connector; + private readonly JsonSerializerOptions? _jsonSerializerOptions; + private static readonly JsonSerializerOptions s_options = new() + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; /// /// Initializes a new instance of the class. /// /// The connector to be used for fetching organization hierarchy data. - public OrganizationHierarchyPlugin(IOrganizationHierarchyConnector connector) + /// The to use for serialization. If null, default options will be used. + public OrganizationHierarchyPlugin(IOrganizationHierarchyConnector connector, JsonSerializerOptions? jsonSerializerOptions = null) { Ensure.NotNull(connector, nameof(connector)); + this._jsonSerializerOptions = jsonSerializerOptions ?? s_options; this._connector = connector; } @@ -34,7 +43,7 @@ public OrganizationHierarchyPlugin(IOrganizationHierarchyConnector connector) /// A JSON string containing the email addresses of the direct reports of the current user. [KernelFunction, Description("Get my direct report's email addresses.")] public async Task GetMyDirectReportsEmailAsync(CancellationToken cancellationToken = default) - => JsonSerializer.Serialize(await this._connector.GetDirectReportsEmailAsync(cancellationToken).ConfigureAwait(false)); + => JsonSerializer.Serialize(await this._connector.GetDirectReportsEmailAsync(cancellationToken).ConfigureAwait(false), this._jsonSerializerOptions); /// /// Get the email of the manager of the current user. @@ -42,7 +51,7 @@ public async Task GetMyDirectReportsEmailAsync(CancellationToken cancell /// An optional to observe while waiting for the task to complete. /// A string containing the email address of the manager of the current user. [KernelFunction, Description("Get my manager's email address.")] - public async Task GetMyManagerEmailAsync(CancellationToken cancellationToken = default) + public async Task GetMyManagerEmailAsync(CancellationToken cancellationToken = default) => await this._connector.GetManagerEmailAsync(cancellationToken).ConfigureAwait(false); /// @@ -51,6 +60,6 @@ public async Task GetMyManagerEmailAsync(CancellationToken cancellationT /// An optional to observe while waiting for the task to complete. /// A string containing the name of the manager of the current user. [KernelFunction, Description("Get my manager's name.")] - public async Task GetMyManagerNameAsync(CancellationToken cancellationToken = default) + public async Task GetMyManagerNameAsync(CancellationToken cancellationToken = default) => await this._connector.GetManagerNameAsync(cancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/Plugins/Plugins.MsGraph/TaskListPlugin.cs b/dotnet/src/Plugins/Plugins.MsGraph/TaskListPlugin.cs index 6c0649721090..947f43220804 100644 --- a/dotnet/src/Plugins/Plugins.MsGraph/TaskListPlugin.cs +++ b/dotnet/src/Plugins/Plugins.MsGraph/TaskListPlugin.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -20,16 +21,24 @@ public sealed class TaskListPlugin { private readonly ITaskManagementConnector _connector; private readonly ILogger _logger; + private readonly JsonSerializerOptions? _jsonSerializerOptions; + private static readonly JsonSerializerOptions s_options = new() + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; /// /// Initializes a new instance of the class. /// /// Task list connector. /// The to use for logging. If null, no logging will be performed. - public TaskListPlugin(ITaskManagementConnector connector, ILoggerFactory? loggerFactory = null) + /// The to use for serialization. If null, default options will be used. + public TaskListPlugin(ITaskManagementConnector connector, ILoggerFactory? loggerFactory = null, JsonSerializerOptions? jsonSerializerOptions = null) { Ensure.NotNull(connector, nameof(connector)); + this._jsonSerializerOptions = jsonSerializerOptions ?? s_options; this._connector = connector; this._logger = loggerFactory?.CreateLogger(typeof(TaskListPlugin)) ?? NullLogger.Instance; } @@ -72,14 +81,14 @@ public async Task AddTaskAsync( // Sensitive data, logging as trace, disabled by default this._logger.LogTrace("Adding task '{0}' to task list '{1}'", task.Title, defaultTaskList.Name); - await this._connector.AddTaskAsync(defaultTaskList.Id, task, cancellationToken).ConfigureAwait(false); + await this._connector.AddTaskAsync(defaultTaskList.Id!, task, cancellationToken).ConfigureAwait(false); } /// /// Get tasks from the default task list. /// [KernelFunction, Description("Get tasks from the default task list.")] - public async Task GetDefaultTasksAsync( + public async Task GetDefaultTasksAsync( [Description("Whether to include completed tasks (optional)")] string includeCompleted = "false", CancellationToken cancellationToken = default) { @@ -91,7 +100,7 @@ public async Task GetDefaultTasksAsync( this._logger.LogWarning("Invalid value for '{0}' variable: '{1}'", nameof(includeCompleted), includeCompleted); } - IEnumerable tasks = await this._connector.GetTasksAsync(defaultTaskList.Id, includeCompletedValue, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Serialize(tasks); + IEnumerable? tasks = await this._connector.GetTasksAsync(defaultTaskList.Id!, includeCompletedValue, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Serialize(tasks, s_options); } } diff --git a/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/CloudDrivePluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/CloudDrivePluginTests.cs index 389c72663239..ee15a1a92725 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/CloudDrivePluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/CloudDrivePluginTests.cs @@ -68,7 +68,7 @@ public async Task GetFileContentAsyncSucceedsAsync() CloudDrivePlugin target = new(connectorMock.Object); // Act - string actual = await target.GetFileContentAsync(anyFilePath); + string? actual = await target.GetFileContentAsync(anyFilePath); // Assert Assert.Equal(expectedContent, actual); diff --git a/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/EmailPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/EmailPluginTests.cs index f2f27419b2ea..809dae82bd5d 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/EmailPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/EmailPluginTests.cs @@ -80,7 +80,7 @@ public async Task GetMyEmailAddressAsyncSucceedsAsync() EmailPlugin target = new(connectorMock.Object); // Act - string actual = await target.GetMyEmailAddressAsync(); + string? actual = await target.GetMyEmailAddressAsync(); // Assert Assert.Equal(anyEmailAddress, actual); diff --git a/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/OrganizationHierarchyPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/OrganizationHierarchyPluginTests.cs index eeaa18446803..c641cbbe7b90 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/OrganizationHierarchyPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/MsGraph/OrganizationHierarchyPluginTests.cs @@ -46,7 +46,7 @@ public async Task GetMyManagerEmailAsyncSucceedsAsync() OrganizationHierarchyPlugin target = new(connectorMock.Object); // Act - string actual = await target.GetMyManagerEmailAsync(); + string? actual = await target.GetMyManagerEmailAsync(); // Assert Assert.Equal(anyManagerEmail, actual); @@ -63,7 +63,7 @@ public async Task GetMyManagerNameAsyncSucceedsAsync() OrganizationHierarchyPlugin target = new(connectorMock.Object); // Act - string actual = await target.GetMyManagerNameAsync(); + string? actual = await target.GetMyManagerNameAsync(); // Assert Assert.Equal(anyManagerName, actual); diff --git a/dotnet/src/VectorData/Milvus/Milvus.csproj b/dotnet/src/VectorData/Milvus/Milvus.csproj index 07b6696cea8a..00e3d56491ef 100644 --- a/dotnet/src/VectorData/Milvus/Milvus.csproj +++ b/dotnet/src/VectorData/Milvus/Milvus.csproj @@ -23,6 +23,7 @@ +