Skip to content

Commit

Permalink
CopilotChat: Enable MsGraph ToDos in CopilotChat (microsoft#781)
Browse files Browse the repository at this point in the history
### Motivation and Context
Enable MsGraph skills in CopilotChat.

### Description
- Added TaskListSkill to CopilotChat when MsGraph authentication is
available
- Reduced the webapp's scopes to just User.Read and Tasks.ReadWrite
- Added a "GetDefaultTasks" function with filter for not getting
completed tasks.
- Fixed importing Klarna from ChatGPT plugin URL.
- Removed unused methods from the OneDrive connector
- Removed unused semantic skills from CopilotChat
  • Loading branch information
adrianwyatt committed May 3, 2023
1 parent ce92d96 commit b7573a9
Show file tree
Hide file tree
Showing 22 changed files with 206 additions and 349 deletions.
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
12 changes: 0 additions & 12 deletions dotnet/src/Skills/Skills.MsGraph/Connectors/OneDriveConnector.cs
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
3 changes: 2 additions & 1 deletion dotnet/src/Skills/Skills.MsGraph/ITaskManagementConnector.cs
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

0 comments on commit b7573a9

Please sign in to comment.