Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

CopilotChat: Enable MsGraph ToDos in CopilotChat #781

Merged
merged 7 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.CustomClient;
[SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "OpenAI users use strings")]
public abstract class OpenAIClientBase : IDisposable
{
protected readonly static HttpClientHandler DefaultHttpClientHandler = new() { CheckCertificateRevocationList = true };
protected static readonly HttpClientHandler DefaultHttpClientHandler = new() { CheckCertificateRevocationList = true };

/// <summary>
/// Logger
Expand All @@ -53,7 +53,7 @@ internal OpenAIClientBase(HttpClient? httpClient = null, ILogger? logger = null)
this.HTTPClient = httpClient;
}

this.HTTPClient.DefaultRequestHeaders.Add("User-Agent", HTTPUseragent);
this.HTTPClient.DefaultRequestHeaders.Add("User-Agent", HTTPUserAgent);
}

/// <summary>
Expand Down Expand Up @@ -186,7 +186,7 @@ protected virtual void Dispose(bool disposing)
#region private ================================================================================

// HTTP user agent sent to remote endpoints
private const string HTTPUseragent = "Microsoft Semantic Kernel";
private const string HTTPUserAgent = "Microsoft-Semantic-Kernel";

// Set to true to dispose of HttpClient when disposing. If HttpClient was passed in, then the caller can manage.
private readonly bool _disposeHttpClient = false;
Expand All @@ -198,12 +198,9 @@ private async Task<T> ExecutePostRequestAsync<T>(string url, string requestBody,
try
{
using HttpContent content = new StringContent(requestBody, Encoding.UTF8, "application/json");
HttpResponseMessage response = await this.HTTPClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);

if (response == null)
{
throw new AIException(AIException.ErrorCodes.NoResponse);
}
HttpResponseMessage response = await this.HTTPClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false)
?? throw new AIException(AIException.ErrorCodes.NoResponse);

this.Log.LogTrace("HTTP response: {0} {1}", (int)response.StatusCode, response.StatusCode.ToString("G"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Microsoft.SemanticKernel.Connectors.HuggingFace.TextCompletion;
/// </summary>
public sealed class HuggingFaceTextCompletion : ITextCompletion, IDisposable
{
private const string HttpUserAgent = "Microsoft Semantic Kernel";
private const string HttpUserAgent = "Microsoft-Semantic-Kernel";
private const string HuggingFaceApiEndpoint = "https://api-inference.huggingface.co/models";

private readonly string _model;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private void LogHttpMessage(HttpHeaders headers, Uri uri, string prefix)
{
if (this._logger.IsEnabled(LogLevel.Debug))
{
StringBuilder message = new StringBuilder();
StringBuilder message = new();
message.AppendLine($"{prefix} {uri}");
foreach (string headerName in this._headerNamesToLog)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,18 @@ public async Task<IEnumerable<TaskManagementTaskList>> GetTaskListsAsync(Cancell
}

/// <inheritdoc/>
public async Task<IEnumerable<TaskManagementTask>> GetTasksAsync(string listId, CancellationToken cancellationToken = default)
public async Task<IEnumerable<TaskManagementTask>> GetTasksAsync(string listId, bool includeCompleted, CancellationToken cancellationToken = default)
{
Ensure.NotNullOrWhitespace(listId, nameof(listId));

string filterValue = string.Empty;
if (!includeCompleted)
{
filterValue = "status ne 'completed'";
}
ITodoTaskListTasksCollectionPage tasksPage = await this._graphServiceClient.Me
.Todo.Lists[listId]
.Tasks.Request().GetAsync(cancellationToken).ConfigureAwait(false);
.Tasks.Request().Filter(filterValue).GetAsync(cancellationToken).ConfigureAwait(false);

List<TodoTask> tasks = tasksPage.ToList();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,6 @@ public async Task<Stream> GetFileContentStreamAsync(string filePath, Cancellatio
.Request().GetAsync(cancellationToken).ConfigureAwait(false);
}

/// <exception cref="NotImplementedException">This method is not yet supported for <see cref="OneDriveConnector"/>.</exception>
public Task<Stream> GetWriteableFileStreamAsync(string filePath, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

/// <exception cref="NotImplementedException">This method is not yet supported for <see cref="OneDriveConnector"/>.</exception>
public Task<Stream> CreateFileAsync(string filePath, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public async Task<bool> FileExistsAsync(string filePath, CancellationToken cancellationToken = default)
{
Ensure.NotNullOrWhitespace(filePath, nameof(filePath));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public interface ITaskManagementConnector
/// Get the all tasks in a task list.
/// </summary>
/// <param name="listId">ID of the list from which to get the tasks.</param>
/// <param name="includeCompleted">Whether to include completed tasks.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>All of the tasks in the specified task list.</returns>
Task<IEnumerable<TaskManagementTask>> GetTasksAsync(string listId, CancellationToken cancellationToken = default);
Task<IEnumerable<TaskManagementTask>> GetTasksAsync(string listId, bool includeCompleted, CancellationToken cancellationToken = default);
}
42 changes: 40 additions & 2 deletions dotnet/src/Skills/Skills.MsGraph/TaskListSkill.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -25,6 +27,11 @@ public static class Parameters
/// Task reminder as DateTimeOffset.
/// </summary>
public const string Reminder = "reminder";

/// <summary>
/// Whether to include completed tasks.
/// </summary>
public const string IncludeCompleted = "includeCompleted";
}

private readonly ITaskManagementConnector _connector;
Expand All @@ -48,7 +55,7 @@ public TaskListSkill(ITaskManagementConnector connector, ILogger<TaskListSkill>?
/// </summary>
public static DateTimeOffset GetNextDayOfWeek(DayOfWeek dayOfWeek, TimeSpan timeOfDay)
{
DateTimeOffset today = new DateTimeOffset(DateTime.Today);
DateTimeOffset today = new(DateTime.Today);
int nextDayOfWeekOffset = dayOfWeek - today.DayOfWeek;
if (nextDayOfWeekOffset <= 0)
{
Expand Down Expand Up @@ -76,7 +83,7 @@ public async Task AddTaskAsync(string title, SKContext context)
return;
}

TaskManagementTask task = new TaskManagementTask(
TaskManagementTask task = new(
id: Guid.NewGuid().ToString(),
title: title);

Expand All @@ -88,4 +95,35 @@ public async Task AddTaskAsync(string title, SKContext context)
this._logger.LogInformation("Adding task '{0}' to task list '{1}'", task.Title, defaultTaskList.Name);
await this._connector.AddTaskAsync(defaultTaskList.Id, task, context.CancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Get tasks from the default task list.
/// </summary>
[SKFunction("Get tasks from the default task list.")]
[SKFunctionContextParameter(Name = Parameters.IncludeCompleted, Description = "Whether to include completed tasks (optional)", DefaultValue = "false")]
public async Task<string> GetDefaultTasksAsync(SKContext context)
{
TaskManagementTaskList? defaultTaskList = await this._connector.GetDefaultTaskListAsync(context.CancellationToken)
.ConfigureAwait(false);

if (defaultTaskList == null)
{
context.Fail("No default task list found.");
return string.Empty;
}

bool includeCompleted = false;
if (context.Variables.Get(Parameters.IncludeCompleted, out string includeCompletedString))
{
if (!bool.TryParse(includeCompletedString, out includeCompleted))
{
this._logger.LogWarning("Invalid value for '{0}' variable: '{1}'", Parameters.IncludeCompleted, includeCompletedString);
}
}

IEnumerable<TaskManagementTask> tasks = await this._connector.GetTasksAsync(defaultTaskList.Id, includeCompleted, context.CancellationToken)
.ConfigureAwait(false);

return JsonSerializer.Serialize(tasks);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,73 +31,50 @@ public static class KernelChatGptPluginExtensions
/// <param name="kernel">Semantic Kernel instance.</param>
/// <param name="skillName">Skill name.</param>
/// <param name="url">Url to in which to retrieve the ChatGPT plugin.</param>
/// <param name="httpClient">Optional HttpClient to use for the request.</param>
/// <param name="httpClient">HttpClient to use for the request.</param>
/// <param name="authCallback">Optional callback for adding auth data to the API requests.</param>
/// <param name="userAgent">Optional user agent header value.</param>
/// <param name="retryConfiguration">Optional retry configuration.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of all the semantic functions representing the skill.</returns>
public static async Task<IDictionary<string, ISKFunction>> ImportChatGptPluginSkillFromUrlAsync(
this IKernel kernel,
string skillName,
Uri url,
HttpClient? httpClient = null,
HttpClient httpClient,
AuthenticateRequestAsyncCallback? authCallback = null,
string? userAgent = "Microsoft-Semantic-Kernel",
HttpRetryConfig? retryConfiguration = null,
CancellationToken cancellationToken = default)
{
Verify.ValidSkillName(skillName);

HttpResponseMessage? response = null;
try
{
if (httpClient == null)
{
// TODO Fix this: throwing "The inner handler has not been assigned"
//using DefaultHttpRetryHandler retryHandler = new DefaultHttpRetryHandler(
// config: new HttpRetryConfig() { MaxRetryCount = 3 },
// log: null);

//using HttpClient client = new HttpClient(retryHandler, false);
using HttpClient client = new();
using HttpResponseMessage response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();

response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false);
}
else
{
response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
}

response.EnsureSuccessStatusCode();

string gptPluginJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
string? openApiUrl = ParseOpenApiUrl(gptPluginJson);
string gptPluginJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
string? openApiUrl = ParseOpenApiUrl(gptPluginJson);

return await kernel.ImportOpenApiSkillFromUrlAsync(skillName, new Uri(openApiUrl), httpClient, authCallback, retryConfiguration, cancellationToken).ConfigureAwait(false);
}
finally
{
if (response != null)
{
response.Dispose();
}
}
return await kernel.ImportOpenApiSkillFromUrlAsync(skillName, new Uri(openApiUrl), httpClient, authCallback, userAgent, retryConfiguration, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Imports ChatGPT plugin from assembly resource.
/// </summary>
/// <param name="kernel">Semantic Kernel instance.</param>
/// <param name="skillName">Skill name.</param>
/// <param name="httpClient">Optional HttpClient to use for the request.</param>
/// <param name="httpClient">HttpClient to use for the request.</param>
/// <param name="authCallback">Optional callback for adding auth data to the API requests.</param>
/// <param name="userAgent">Optional user agent header value.</param>
/// <param name="retryConfiguration">Optional retry configuration.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of all the semantic functions representing the skill.</returns>
public static async Task<IDictionary<string, ISKFunction>> ImportChatGptPluginSkillFromResourceAsync(
this IKernel kernel,
string skillName,
HttpClient? httpClient = null,
HttpClient httpClient,
AuthenticateRequestAsyncCallback? authCallback = null,
string? userAgent = "Microsoft-Semantic-Kernel",
HttpRetryConfig? retryConfiguration = null,
CancellationToken cancellationToken = default)
{
Expand All @@ -107,18 +84,15 @@ public static class KernelChatGptPluginExtensions

var resourceName = $"{skillName}.ai-plugin.json";

var stream = type.Assembly.GetManifestResourceStream(type, resourceName);
if (stream == null)
{
throw new MissingManifestResourceException($"Unable to load OpenApi skill from assembly resource '{resourceName}'.");
}
var stream = type.Assembly.GetManifestResourceStream(type, resourceName)
?? throw new MissingManifestResourceException($"Unable to load OpenApi skill from assembly resource '{resourceName}'.");

using StreamReader reader = new StreamReader(stream);
using StreamReader reader = new(stream);
string gptPluginJson = await reader.ReadToEndAsync().ConfigureAwait(false);

string? openApiUrl = ParseOpenApiUrl(gptPluginJson);

return await kernel.ImportOpenApiSkillFromUrlAsync(skillName, new Uri(openApiUrl), httpClient, authCallback, retryConfiguration, cancellationToken).ConfigureAwait(false);
return await kernel.ImportOpenApiSkillFromUrlAsync(skillName, new Uri(openApiUrl), httpClient, authCallback, userAgent, retryConfiguration, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -132,7 +106,7 @@ public static class KernelChatGptPluginExtensions
/// <param name="retryConfiguration">Optional retry configuration.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of all the semantic functions representing the skill.</returns>
public async static Task<IDictionary<string, ISKFunction>> ImportChatGptPluginSkillSkillFromDirectoryAsync(
public static async Task<IDictionary<string, ISKFunction>> ImportChatGptPluginSkillSkillFromDirectoryAsync(
this IKernel kernel,
string parentDirectory,
string skillDirectoryName,
Expand Down Expand Up @@ -171,7 +145,7 @@ public static class KernelChatGptPluginExtensions
/// <param name="retryConfiguration">Optional retry configuration.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of all the semantic functions representing the skill.</returns>
public async static Task<IDictionary<string, ISKFunction>> ImportChatGptPluginSkillSkillFromFileAsync(
public static async Task<IDictionary<string, ISKFunction>> ImportChatGptPluginSkillSkillFromFileAsync(
this IKernel kernel,
string skillName,
string filePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,56 +35,34 @@ public static class KernelOpenApiExtensions
/// <param name="kernel">Semantic Kernel instance.</param>
/// <param name="skillName">Skill name.</param>
/// <param name="url">Url to in which to retrieve the OpenAPI definition.</param>
/// <param name="httpClient">Optional HttpClient to use for the request.</param>
/// <param name="httpClient">HttpClient to use for the request.</param>
/// <param name="authCallback">Optional callback for adding auth data to the API requests.</param>
/// <param name="userAgent">Optional user agent header value.</param>
/// <param name="retryConfiguration">Optional retry configuration.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of all the semantic functions representing the skill.</returns>
public static async Task<IDictionary<string, ISKFunction>> ImportOpenApiSkillFromUrlAsync(
this IKernel kernel,
string skillName,
Uri url,
HttpClient? httpClient = null,
HttpClient httpClient,
AuthenticateRequestAsyncCallback? authCallback = null,
string? userAgent = "Microsoft-Semantic-Kernel",
HttpRetryConfig? retryConfiguration = null,
CancellationToken cancellationToken = default)
{
Verify.ValidSkillName(skillName);

HttpResponseMessage? response = null;
try
{
if (httpClient == null)
{
// TODO Fix this: throwing "The inner handler has not been assigned"
//using DefaultHttpRetryHandler retryHandler = new DefaultHttpRetryHandler(
// config: new HttpRetryConfig() { MaxRetryCount = 3 },
// log: null);

//using HttpClient client = new HttpClient(retryHandler, false);
using HttpClient client = new HttpClient();

response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
}
else
{
response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
}
using HttpResponseMessage response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();

response.EnsureSuccessStatusCode();

Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
if (stream == null)
{
throw new MissingManifestResourceException($"Unable to load OpenApi skill from url '{url}'.");
}

return await kernel.RegisterOpenApiSkillAsync(stream, skillName, authCallback, retryConfiguration, cancellationToken: cancellationToken).ConfigureAwait(false);
}
finally
Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
if (stream == null)
{
response?.Dispose();
throw new MissingManifestResourceException($"Unable to load OpenApi skill from url '{url}'.");
}

return await kernel.RegisterOpenApiSkillAsync(stream, skillName, authCallback, retryConfiguration, userAgent, cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand Down