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

✨ Try out SK planner with a chat interface #71

Merged
merged 10 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/TimesheetGPT.Core/ConfigureServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public static class ConfigureServices
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped<IAiService, SemKerAiService>();
// services.AddScoped<IAIService, LangChainAIService>(); //TODO: Try langchain out
services.AddScoped<IGraphService, GraphService>();

return services;
}
Expand Down
5 changes: 4 additions & 1 deletion src/TimesheetGPT.Core/Interfaces/IAiService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using TimesheetGPT.Core.Models;

namespace TimesheetGPT.Core.Interfaces;

public interface IAiService
{
public Task<string> GetSummary(string text, string extraPrompts, string additionalNotes);
public Task<string?> ChatWithGraphApi(string ask);
public Task<string> GetSummaryBoring(IList<Email> emails, IEnumerable<Meeting> meetings, string extraPrompts, CancellationToken cancellationToken, string additionalNotes = "");
}
5 changes: 3 additions & 2 deletions src/TimesheetGPT.Core/Interfaces/IGraphService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace TimesheetGPT.Core.Interfaces;

public interface IGraphService
{
public Task<List<string>> GetEmailSubjects(DateTime date);
public Task<List<Meeting>> GetMeetings(DateTime date);
public Task<List<Email>> GetSentEmails(DateTime date, CancellationToken cancellationToken);
public Task<List<Meeting>> GetMeetings(DateTime date, CancellationToken cancellationToken);
public Task<List<TeamsCall>> GetTeamsCalls(DateTime date);
Task<Email> GetEmailBody(string subject, CancellationToken ct);
}
9 changes: 9 additions & 0 deletions src/TimesheetGPT.Core/Models/Email.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace TimesheetGPT.Core.Models;

public class Email
{
public string? Subject { get; set; }
public string? Body { get; set; }
public string? To { get; set; }
public string? Id { get; set; }
}
10 changes: 5 additions & 5 deletions src/TimesheetGPT.Core/Models/Summary.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace TimesheetGPT.Core.Models;

public class SummaryWithRaw
public class Summary
{
public List<string> Emails { get; set; }
public List<Meeting> Meetings { get; set; }
public string Summary { get; set; }
public string ModelUsed { get; set; }
public List<Email> Emails { get; set; } = [];
public List<Meeting> Meetings { get; set; } = [];
public string? Text { get; set; }
public string? ModelUsed { get; set; }
}
39 changes: 39 additions & 0 deletions src/TimesheetGPT.Core/Plugins.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.SemanticKernel;
using System.ComponentModel;
using System.Globalization;
using TimesheetGPT.Core.Interfaces;
using System.Text.Json;


namespace TimesheetGPT.Core;

public class GraphPlugins(IGraphService graphService)
{
[SKFunction, Description("Get email body from Id")]
bradystroud marked this conversation as resolved.
Show resolved Hide resolved
public async Task<string?> GetEmailBody(string id)
{
return (await graphService.GetEmailBody(id, new CancellationToken())).Body;
}

[SKFunction, Description("Get sent emails (subject, to, Id) for a date)")]
public async Task<string> GetSentEmails(DateTime dateTime)
{
var emails = await graphService.GetSentEmails(dateTime, new CancellationToken());
return JsonSerializer.Serialize(emails);
}

[SKFunction, Description("Get meetings for a date")]
public async Task<string> GetMeetings(DateTime dateTime)
{
var meetings = await graphService.GetMeetings(dateTime, new CancellationToken());
return JsonSerializer.Serialize(meetings);
}

[SKFunction, Description("Get todays date")]
public string GetTodaysDate(DateTime dateTime)
{
//TODO: Use browser datetime
return DateTime.Today.ToString(CultureInfo.InvariantCulture);

}
}
86 changes: 86 additions & 0 deletions src/TimesheetGPT.Core/PromptConfigs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Microsoft.SemanticKernel.AI;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.TemplateEngine;

namespace TimesheetGPT.Core;

// TODO: Refactor into a JSON file
// https://learn.microsoft.com/en-us/semantic-kernel/ai-orchestration/plugins/semantic-functions/serializing-semantic-functions
public static class PromptConfigs
{
public static readonly PromptTemplateConfig SummarizeEmailsAndCalendar = new()
{
Schema = 1,

Check warning on line 13 in src/TimesheetGPT.Core/PromptConfigs.cs

View workflow job for this annotation

GitHub Actions / build

'PromptTemplateConfig.Schema' is obsolete: 'Type property is no longer used. This will be removed in a future release.'
Description = "Summarises users emails and meetings.",
ModelSettings = new List<AIRequestSettings>
{
// Note: Token limit hurts things like additional notes. If you don't have enough, the prompt will suck
new OpenAIRequestSettings { MaxTokens = 1000, Temperature = 0, TopP = 0.5 }
},
Input =
{
Parameters = new List<PromptTemplateConfig.InputParameter>
{
new()
{
Name = PromptVariables.Meetings,
Description = "meetings",
DefaultValue = ""
},
new()
{
Name = PromptVariables.Emails,
Description = "emails",
DefaultValue = ""
},
new()
{
Name = PromptVariables.AdditionalNotes,
Description = "Additional Notes",
DefaultValue = ""
},
new()
{
Name = PromptVariables.ExtraPrompts,
Description = "extraPrompts",
DefaultValue = ""
}
}
}
};

public static readonly PromptTemplateConfig SummarizeEmailBody = new()
{
Schema = 1,

Check warning on line 54 in src/TimesheetGPT.Core/PromptConfigs.cs

View workflow job for this annotation

GitHub Actions / build

'PromptTemplateConfig.Schema' is obsolete: 'Type property is no longer used. This will be removed in a future release.'
Description = "Summarizes body of an email",
ModelSettings = new List<AIRequestSettings>
{
// Note: Token limit hurts things like additional notes. If you don't have enough, the prompt will suck
new OpenAIRequestSettings { MaxTokens = 1000, Temperature = 0, TopP = 0.5 }
},
Input =
{
Parameters = new List<PromptTemplateConfig.InputParameter>
{
new()
{
Name = PromptVariables.Recipients,
Description = nameof(PromptVariables.Recipients),
DefaultValue = ""
},
new()
{
Name = PromptVariables.Subject,
Description = nameof(PromptVariables.Subject),
DefaultValue = ""
},
new()
{
Name = PromptVariables.EmailBody,
Description = nameof(PromptVariables.EmailBody),
DefaultValue = ""
}
}
}
};
}
44 changes: 44 additions & 0 deletions src/TimesheetGPT.Core/PromptTemplates.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace TimesheetGPT.Core;

public static class PromptTemplates
{
// Doesn't hot reload
public static readonly string SummarizeEmailsAndCalendar = $"""
Generate a concise timesheet summary in chronological order from my meetings and emails.

For meetings, follow the format 'Meeting Name - Meeting Length'
Skip non-essential meetings like Daily Scrums.
Treat all-day (or 9-hour) meetings as bookings e.g. Brady was booked as the Bench Master.
Use email subjects to figure out what tasks were completed.
Note that emails starting with 'RE:' are replies, not new tasks.
An email titled 'Sprint X Review' means I led that Sprint review/retro.
Merge meetings and emails into one summary. If an item appears in both, mention it just once.
Ignore the day's meetings if an email is marked 'Sick Today.'
Appointments labeled 'Leave' should be omitted.
Only output the timesheet summary so i can copy it directly. Use a Markdown unordered list, keeping it lighthearted with a few emojis. 🌟

{PromptVarFormatter(PromptVariables.ExtraPrompts)}

Here is the data:

{PromptVarFormatter(PromptVariables.Emails)}

{PromptVarFormatter(PromptVariables.Meetings)}

Additional notes:

{PromptVarFormatter(PromptVariables.AdditionalNotes)}
""";

public static readonly string SummarizeEmailBody = $"""
Summarise this email body in 1-2 sentences. This summary will later be used to generate a timesheet summary.
Respond in this format: Recipients - Subject - Summary of body

Here is the data:
Recipients: {PromptVarFormatter(PromptVariables.Recipients)}
Subject: {PromptVarFormatter(PromptVariables.Subject)}
Body: {PromptVarFormatter(PromptVariables.Subject)}
""";

private static string PromptVarFormatter(string promptVar) => "{{$" + promptVar + "}}";
}
13 changes: 13 additions & 0 deletions src/TimesheetGPT.Core/PromptVariables.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace TimesheetGPT.Core;

public static class PromptVariables
{
public const string Emails = "emails";
public const string Meetings = "meetings";
public const string ExtraPrompts = "extraPrompts";
public const string AdditionalNotes = "additionalNotes";

public const string Recipients = "recipients";
public const string Subject = "subject";
public const string EmailBody = "emailBody";
}
43 changes: 0 additions & 43 deletions src/TimesheetGPT.Core/Prompts.cs

This file was deleted.

50 changes: 38 additions & 12 deletions src/TimesheetGPT.Core/Services/GraphService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public GraphService(GraphServiceClient client)
}


public async Task<List<string>> GetEmailSubjects(DateTime date)
public async Task<List<Email>> GetSentEmails(DateTime date, CancellationToken cancellationToken)
{
var nextDay = date.AddDays(1);
var dateUtc = date.ToUniversalTime();
Expand All @@ -29,24 +29,29 @@ public async Task<List<string>> GetEmailSubjects(DateTime date)
{
rc.QueryParameters.Top = 999;
rc.QueryParameters.Select =
new[] { "subject" };
new[] { "subject", "bodyPreview", "toRecipients", "id" };
rc.QueryParameters.Filter =
$"sentDateTime ge {dateUtc:yyyy-MM-ddTHH:mm:ssZ} and sentDateTime lt {nextDayUtc:yyyy-MM-ddTHH:mm:ssZ}";
rc.QueryParameters.Orderby = new[] { "sentDateTime asc" };

});

}, cancellationToken);

if (messages is { Value.Count: > 1 })
{
return messages.Value.Select(m => m.Subject).ToList();
return new List<Email>(messages.Value.Select(m => new Email
{
Subject = m.Subject,
Body = m.BodyPreview ?? "",
To = string.Join(", ", m.ToRecipients?.Select(r => r.EmailAddress?.Name).ToList() ?? new List<string?>()),
Id = m.Id
}));
}

return new List<string>(); //slack
return new List<Email>(); //slack
}


public async Task<List<Meeting>> GetMeetings(DateTime date)
public async Task<List<Meeting>> GetMeetings(DateTime date, CancellationToken cancellationToken)
{
var nextDay = date.AddDays(1);
var dateUtc = date.ToUniversalTime();
Expand All @@ -59,14 +64,14 @@ public async Task<List<Meeting>> GetMeetings(DateTime date)
rc.QueryParameters.EndDateTime = nextDayUtc.ToString("o");
rc.QueryParameters.Orderby = new[] { "start/dateTime" };
rc.QueryParameters.Select = new[] { "subject", "start", "end", "occurrenceId" };
});
}, cancellationToken);

if (meetings is { Value.Count: > 1 })
{
return meetings.Value.Select(m => new Meeting
{
Name = m.Subject,
Length = DateTime.Parse(m.End.DateTime) - DateTime.Parse(m.Start.DateTime),
Name = m.Subject ?? "",
Length = DateTime.Parse(m.End?.DateTime ?? string.Empty) - DateTime.Parse(m.Start?.DateTime ?? string.Empty),
Repeating = m.Type == EventType.Occurrence,
// Sender = m.EmailAddress.Address TODO: Why is Organizer and attendees null? permissions?
}).ToList();
Expand Down Expand Up @@ -94,7 +99,7 @@ public async Task<List<TeamsCall>> GetTeamsCalls(DateTime date)
{
return calls.Value.Select(m => new TeamsCall
{
Attendees = m.Participants.Select(p => p.User.DisplayName).ToList(),
Attendees = m.Participants?.Select(p => p.User?.DisplayName).ToList() ?? new List<string?>(),
Length = m.EndDateTime - m.StartDateTime ?? TimeSpan.Zero,
}).ToList();
}
Expand All @@ -106,10 +111,31 @@ public async Task<List<TeamsCall>> GetTeamsCalls(DateTime date)

return new List<TeamsCall>();
}

public async Task<Email> GetEmailBody(string id, CancellationToken ct)
{
var message = await _client.Me.Messages[id]
.GetAsync(rc =>
{
rc.QueryParameters.Select =
new[] { "bodyPreview", "toRecipients" };
}, ct);

if (message != null)
{
return new Email
{
Body = message.BodyPreview,
To = string.Join(", ", (message.ToRecipients ?? new List<Recipient>()).Select(r => r.EmailAddress?.Name).ToList())
};
}

return new Email(); //slack
}
bradystroud marked this conversation as resolved.
Show resolved Hide resolved
}

public class TeamsCall
{
public List<string> Attendees { get; set; }
public List<string?>? Attendees { get; set; }
public TimeSpan Length { get; set; }
}
Loading
Loading