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 email read and calendar read plugins #801

Merged
merged 12 commits into from
May 5, 2023
72 changes: 68 additions & 4 deletions dotnet/src/Skills/Skills.MsGraph/CalendarSkill.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand Down Expand Up @@ -46,6 +49,16 @@ public static class Parameters
/// Event's attendees, separated by ',' or ';'.
/// </summary>
public const string Attendees = "attendees";

/// <summary>
/// The name of the top parameter used to limit the number of results returned in the response.
/// </summary>
public const string MaxResults = "maxResults";

/// <summary>
/// The name of the skip parameter used to skip a certain number of results in the response.
/// </summary>
public const string Skip = "skip";
}

private readonly ICalendarConnector _connector;
Expand Down Expand Up @@ -96,10 +109,12 @@ public async Task AddEventAsync(string subject, SKContext context)
return;
}

CalendarEvent calendarEvent = new CalendarEvent(
memory.Input,
DateTimeOffset.Parse(start, CultureInfo.InvariantCulture.DateTimeFormat),
DateTimeOffset.Parse(end, CultureInfo.InvariantCulture.DateTimeFormat));
CalendarEvent calendarEvent = new()
{
Subject = memory.Input,
Start = DateTimeOffset.Parse(start, CultureInfo.InvariantCulture.DateTimeFormat),
End = DateTimeOffset.Parse(end, CultureInfo.InvariantCulture.DateTimeFormat)
};

if (memory.Get(Parameters.Location, out string location))
{
Expand All @@ -119,4 +134,53 @@ public async Task AddEventAsync(string subject, SKContext context)
this._logger.LogInformation("Adding calendar event '{0}'", calendarEvent.Subject);
await this._connector.AddEventAsync(calendarEvent).ConfigureAwait(false);
}

/// <summary>
/// Get calendar events with specified optional clauses used to query for messages.
/// </summary>
[SKFunction("Get calendar events.")]
[SKFunctionContextParameter(Name = Parameters.MaxResults, Description = "Optional limit of the number of events to retrieve.")]
[SKFunctionContextParameter(Name = Parameters.Skip, Description = "Optional number of events to skip before retrieving results.")]
public async Task<string> GetCalendarEventsAsync(SKContext context)
{
this._logger.LogInformation("Getting calendar events with query options top: '{0}', skip:'{1}'.",
context.Variables.Get(Parameters.MaxResults, out string maxResultsString),
adrianwyatt marked this conversation as resolved.
Show resolved Hide resolved
context.Variables.Get(Parameters.Skip, out string skipString)
adrianwyatt marked this conversation as resolved.
Show resolved Hide resolved
);

string selectString = "start,subject,organizer,location";

int? top = null;
if (!string.IsNullOrWhiteSpace(maxResultsString))
{
if (int.TryParse(maxResultsString, out int topValue))
{
top = topValue;
}
}

int? skip = null;
if (!string.IsNullOrWhiteSpace(skipString))
{
if (int.TryParse(skipString, out int skipValue))
{
skip = skipValue;
}
}

IEnumerable<CalendarEvent> events = await this._connector.GetEventsAsync(
top: top,
skip: skip,
select: selectString,
context.CancellationToken)
.ConfigureAwait(false);
adrianwyatt marked this conversation as resolved.
Show resolved Hide resolved

return JsonSerializer.Serialize(
value: events,
options: new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Linq;
using Microsoft.Graph;
using Microsoft.Graph.Extensions;
using Microsoft.SemanticKernel.Skills.MsGraph.Models;

namespace Microsoft.SemanticKernel.Skills.MsGraph.Connectors;

/// <summary>
/// Extensions for converting between Microsoft Graph models and skill models.
/// </summary>
internal static class MicrosoftGraphModelExtensions
{
/// <summary>
/// Convert a Microsoft Graph message to an email message.
/// </summary>
public static Models.EmailMessage ToEmailMessage(this Message graphMessage)
adrianwyatt marked this conversation as resolved.
Show resolved Hide resolved
=> new()
{
BccRecipients = graphMessage.BccRecipients?.Select(r => r.EmailAddress.ToEmailAddress()),
Body = graphMessage.Body?.Content,
BodyPreview = graphMessage.BodyPreview.Replace("\u200C", ""), // BodyPreviews are sometimes filled with zero-width non-joiner characters - remove them.
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()),
SentDateTime = graphMessage.SentDateTime,
Subject = graphMessage.Subject
};

/// <summary>
/// Convert a Microsoft Graph email address to an email address.
/// </summary>
public static Models.EmailAddress ToEmailAddress(this Microsoft.Graph.EmailAddress graphEmailAddress)
=> new()
{
Address = graphEmailAddress.Address,
Name = graphEmailAddress.Name
};

/// <summary>
/// Convert a calendar event to a Microsoft Graph event.
/// </summary>
public static Event ToGraphEvent(this CalendarEvent calendarEvent)
=> new()
{
Subject = calendarEvent.Subject,
Body = new ItemBody { Content = calendarEvent.Content, ContentType = BodyType.Html },
Start = calendarEvent.Start.HasValue ? DateTimeTimeZone.FromDateTimeOffset(calendarEvent.Start.Value) : DateTimeTimeZone.FromDateTime(System.DateTime.Now),
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 } })
};

/// <summary>
/// Convert a Microsoft Graph event to a calendar event.
/// </summary>
public static CalendarEvent ToCalendarEvent(this Event msGraphEvent)
=> new()
{
Subject = msGraphEvent.Subject,
Content = msGraphEvent.Body?.Content,
Start = msGraphEvent.Start?.ToDateTimeOffset(),
End = msGraphEvent.End?.ToDateTimeOffset(),
Location = msGraphEvent.Location?.DisplayName,
Attendees = msGraphEvent.Attendees?.Select(a => a.EmailAddress.Address)
};
}

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.Graph.Extensions;
using Microsoft.SemanticKernel.Skills.MsGraph.Models;

namespace Microsoft.SemanticKernel.Skills.MsGraph.Connectors;
Expand All @@ -28,27 +28,36 @@ public OutlookCalendarConnector(GraphServiceClient graphServiceClient)
/// <inheritdoc/>
public async Task<CalendarEvent> AddEventAsync(CalendarEvent calendarEvent, CancellationToken cancellationToken = default)
{
Event resultEvent = await this._graphServiceClient.Me.Events.Request().AddAsync(ToGraphEvent(calendarEvent), cancellationToken).ConfigureAwait(false);
return ToCalendarEvent(resultEvent);
Event resultEvent = await this._graphServiceClient.Me.Events.Request()
.AddAsync(calendarEvent.ToGraphEvent(), cancellationToken).ConfigureAwait(false);
return resultEvent.ToCalendarEvent();
}

private static Event ToGraphEvent(CalendarEvent calendarEvent)
=> new Event()
/// <inheritdoc/>
public async Task<IEnumerable<CalendarEvent>> 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)
{
Subject = calendarEvent.Subject,
Body = new ItemBody { Content = calendarEvent.Content, ContentType = BodyType.Html },
Start = DateTimeTimeZone.FromDateTimeOffset(calendarEvent.Start),
End = DateTimeTimeZone.FromDateTimeOffset(calendarEvent.End),
Location = new Location { DisplayName = calendarEvent.Location },
Attendees = calendarEvent.Attendees?.Select(a => new Attendee { EmailAddress = new EmailAddress { Address = a } })
};

private static CalendarEvent ToCalendarEvent(Event msGraphEvent)
=> new CalendarEvent(msGraphEvent.Subject, msGraphEvent.Start.ToDateTimeOffset(), msGraphEvent.End.ToDateTimeOffset())
query.Skip(skip.Value);
}

if (!string.IsNullOrEmpty(select))
{
Id = msGraphEvent.Id,
Content = msGraphEvent.Body?.Content,
Location = msGraphEvent.Location?.DisplayName,
Attendees = msGraphEvent.Attendees?.Select(a => a.EmailAddress.Address)
};
query.Select(select);
}

ICalendarEventsCollectionPage result = await query.GetAsync(cancellationToken).ConfigureAwait(false);

IEnumerable<CalendarEvent> events = result.Select(e => e.ToCalendarEvent());

return events;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.SemanticKernel.Skills.MsGraph.Connectors.Diagnostics;
using Microsoft.SemanticKernel.Skills.MsGraph.Models;

namespace Microsoft.SemanticKernel.Skills.MsGraph.Connectors;

Expand Down Expand Up @@ -35,13 +37,13 @@ public async Task SendEmailAsync(string subject, string content, string[] recipi
Ensure.NotNullOrWhitespace(content, nameof(content));
Ensure.NotNull(recipients, nameof(recipients));

Message message = new Message
Message message = new()
{
Subject = subject,
Body = new ItemBody { ContentType = BodyType.Text, Content = content },
ToRecipients = recipients.Select(recipientAddress => new Recipient
{
EmailAddress = new EmailAddress
EmailAddress = new()
{
Address = recipientAddress
}
Expand All @@ -50,4 +52,33 @@ public async Task SendEmailAsync(string subject, string content, string[] recipi

await this._graphServiceClient.Me.SendMail(message).Request().PostAsync(cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public async Task<IEnumerable<Models.EmailMessage>> 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)
{
query.Skip(skip.Value);
}

if (!string.IsNullOrEmpty(select))
{
query.Select(select);
}

IUserMessagesCollectionPage result = await query.GetAsync(cancellationToken).ConfigureAwait(false);

IEnumerable<EmailMessage> messages = result.Select(m => m.ToEmailMessage());

return messages;
}
}

65 changes: 65 additions & 0 deletions dotnet/src/Skills/Skills.MsGraph/EmailSkill.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Graph;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.Skills.MsGraph.Diagnostics;
Expand All @@ -29,6 +33,16 @@ public static class Parameters
/// Email subject.
/// </summary>
public const string Subject = "subject";

/// <summary>
/// The name of the top parameter used to limit the number of results returned in the response.
/// </summary>
public const string MaxResults = "maxResults";

/// <summary>
/// The name of the skip parameter used to skip a certain number of results in the response.
/// </summary>
public const string Skip = "skip";
}

private readonly IEmailConnector _connector;
Expand Down Expand Up @@ -79,4 +93,55 @@ public async Task SendEmailAsync(string content, SKContext context)
string[] recipientList = recipients.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
await this._connector.SendEmailAsync(subject, content, recipientList).ConfigureAwait(false);
}

/// <summary>
/// Get email messages with specified optional clauses used to query for messages.
/// </summary>
[SKFunction("Get email messages.")]
[SKFunctionContextParameter(Name = Parameters.MaxResults, Description = "Optional limit of the number of message to retrieve.",
DefaultValue = "10")]
glahaye marked this conversation as resolved.
Show resolved Hide resolved
[SKFunctionContextParameter(Name = Parameters.Skip, Description = "Optional number of message to skip before retrieving results.",
DefaultValue = "0")]
public async Task<string> GetEmailMessagesAsync(SKContext context)
{
this._logger.LogInformation("Getting email messages with query options top: '{0}', skip:'{1}'.",
context.Variables.Get(Parameters.MaxResults, out string maxResultsString),
context.Variables.Get(Parameters.Skip, out string skipString)
adrianwyatt marked this conversation as resolved.
Show resolved Hide resolved
);

string selectString = "subject,receivedDateTime,bodyPreview";

int? top = null;
if (!string.IsNullOrWhiteSpace(maxResultsString))
{
if (int.TryParse(maxResultsString, out int topValue))
{
top = topValue;
}
}

int? skip = null;
if (!string.IsNullOrWhiteSpace(skipString))
{
if (int.TryParse(skipString, out int skipValue))
{
skip = skipValue;
}
}

IEnumerable<Models.EmailMessage> messages = await this._connector.GetMessagesAsync(
top: top,
skip: skip,
select: selectString,
context.CancellationToken)
.ConfigureAwait(false);

return JsonSerializer.Serialize(
value: messages,
options: new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
});
}
}