Skip to content

Commit

Permalink
✨ Try out SK planner with a chat interface (#71)
Browse files Browse the repository at this point in the history
* ✨ Try out SK planner with a chat interface

Improved prompt

Add Date range feature (week of timesheets at once)

* 🧵 Parrallelize some stuff

* Bug fixes for demo

* Get rid of warnings

* ⚡️ Use stringBuilder

* Move styling to css file

* Removed styles, updated package use fast GPT

* Add appInsights

* Fix some warnings
  • Loading branch information
bradystroud authored Nov 8, 2023
1 parent 72596ad commit b5770b1
Show file tree
Hide file tree
Showing 30 changed files with 740 additions and 241 deletions.
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")]
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
}
}

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

0 comments on commit b5770b1

Please sign in to comment.