Public Access
Implement meeting assistant v1
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace MeetingAssistant.MeetingNotes;
|
||||
|
||||
public sealed class MarkdownMeetingNoteStore : IMeetingNoteStore
|
||||
{
|
||||
private readonly MeetingAssistantOptions options;
|
||||
private readonly ILogger<MarkdownMeetingNoteStore> logger;
|
||||
private readonly IDeserializer yamlDeserializer;
|
||||
|
||||
public MarkdownMeetingNoteStore(
|
||||
IOptions<MeetingAssistantOptions> options,
|
||||
ILogger<MarkdownMeetingNoteStore> logger)
|
||||
{
|
||||
this.options = options.Value;
|
||||
this.logger = logger;
|
||||
yamlDeserializer = new DeserializerBuilder()
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
}
|
||||
|
||||
public async Task<MeetingNote> SaveAsync(MeetingNote note, CancellationToken cancellationToken)
|
||||
{
|
||||
var folder = VaultPath.Resolve(options.Vault.MeetingNotesFolder);
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
var path = string.IsNullOrWhiteSpace(note.Path)
|
||||
? Path.Combine(folder, $"{DateTimeOffset.Now:yyyyMMdd-HHmmss}-{Slugify(note.Frontmatter.Title)}.md")
|
||||
: note.Path;
|
||||
var frontmatter = PrepareFrontmatter(note.Frontmatter, path);
|
||||
var content = Render(frontmatter, note.UserNotes);
|
||||
|
||||
await File.WriteAllTextAsync(path, content, cancellationToken);
|
||||
logger.LogInformation("Saved meeting note {MeetingNotePath}", path);
|
||||
|
||||
return new MeetingNote(path, frontmatter, note.UserNotes);
|
||||
}
|
||||
|
||||
public async Task<MeetingNote> ReadAsync(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(path, cancellationToken);
|
||||
var document = MarkdownDocumentParser.SplitRequired(
|
||||
content,
|
||||
"Meeting note does not start with YAML frontmatter.",
|
||||
"Meeting note frontmatter is not closed.");
|
||||
var yaml = yamlDeserializer.Deserialize<MeetingNoteYaml>(document.Frontmatter) ?? new MeetingNoteYaml();
|
||||
var frontmatter = new MeetingNoteFrontmatter
|
||||
{
|
||||
Title = yaml.Title ?? "",
|
||||
StartTime = ParseDateTime(yaml.StartTime),
|
||||
EndTime = ParseDateTime(yaml.EndTime),
|
||||
Attendees = yaml.Attendees ?? [],
|
||||
Projects = yaml.Projects ?? [],
|
||||
Transcript = yaml.Transcript ?? "",
|
||||
AssistantContext = yaml.AssistantContext ?? "",
|
||||
Summary = yaml.Summary ?? ""
|
||||
};
|
||||
|
||||
return new MeetingNote(path, frontmatter, document.Body);
|
||||
}
|
||||
|
||||
private MeetingNoteFrontmatter PrepareFrontmatter(MeetingNoteFrontmatter frontmatter, string notePath)
|
||||
{
|
||||
return new MeetingNoteFrontmatter
|
||||
{
|
||||
Title = frontmatter.Title,
|
||||
StartTime = frontmatter.StartTime,
|
||||
EndTime = frontmatter.EndTime,
|
||||
Attendees = frontmatter.Attendees,
|
||||
Projects = frontmatter.Projects,
|
||||
Transcript = ObsidianLink.ToWikiLink(frontmatter.Transcript, notePath, "Transcript"),
|
||||
AssistantContext = ObsidianLink.ToWikiLink(frontmatter.AssistantContext, notePath, "Assistant Context"),
|
||||
Summary = ObsidianLink.ToWikiLink(frontmatter.Summary, notePath, "Summary")
|
||||
};
|
||||
}
|
||||
|
||||
private static string Render(MeetingNoteFrontmatter frontmatter, string userNotes)
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
"---",
|
||||
$"title: {EscapeScalar(frontmatter.Title)}",
|
||||
$"start_time: {EscapeNullableDateTime(frontmatter.StartTime)}",
|
||||
$"end_time: {EscapeNullableDateTime(frontmatter.EndTime)}",
|
||||
"attendees:"
|
||||
};
|
||||
|
||||
lines.AddRange(frontmatter.Attendees.Select(attendee => $"- {EscapeListItem(attendee)}"));
|
||||
lines.Add("projects:");
|
||||
lines.AddRange(frontmatter.Projects.Select(project => $"- {EscapeListItem(project)}"));
|
||||
lines.Add($"transcript: {EscapeQuoted(frontmatter.Transcript)}");
|
||||
lines.Add($"assistant_context: {EscapeQuoted(frontmatter.AssistantContext)}");
|
||||
lines.Add($"summary: {EscapeQuoted(frontmatter.Summary)}");
|
||||
lines.Add("---");
|
||||
lines.Add("");
|
||||
lines.Add(userNotes);
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
private static string Slugify(string value)
|
||||
{
|
||||
var slug = new string(value
|
||||
.ToLowerInvariant()
|
||||
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
|
||||
.ToArray());
|
||||
slug = string.Join("-", slug.Split('-', StringSplitOptions.RemoveEmptyEntries));
|
||||
return string.IsNullOrWhiteSpace(slug) ? "meeting" : slug;
|
||||
}
|
||||
|
||||
private static string EscapeScalar(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "\"\"" : EscapeListItem(value);
|
||||
}
|
||||
|
||||
private static string EscapeQuoted(string value)
|
||||
{
|
||||
return $"\"{value.Replace("\"", "\\\"", StringComparison.Ordinal)}\"";
|
||||
}
|
||||
|
||||
private static string EscapeNullableDateTime(DateTimeOffset? value)
|
||||
{
|
||||
return value is null ? "\"\"" : EscapeQuoted(value.Value.ToString("O"));
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseDateTime(string? value)
|
||||
{
|
||||
return DateTimeOffset.TryParse(value, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string EscapeListItem(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
if (value.Contains(':', StringComparison.Ordinal) || value.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
return EscapeQuoted(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private sealed class MeetingNoteYaml
|
||||
{
|
||||
[YamlMember(Alias = "title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[YamlMember(Alias = "start_time")]
|
||||
public string? StartTime { get; set; }
|
||||
|
||||
[YamlMember(Alias = "end_time")]
|
||||
public string? EndTime { get; set; }
|
||||
|
||||
[YamlMember(Alias = "attendees")]
|
||||
public List<string>? Attendees { get; set; }
|
||||
|
||||
[YamlMember(Alias = "projects")]
|
||||
public List<string>? Projects { get; set; }
|
||||
|
||||
[YamlMember(Alias = "transcript")]
|
||||
public string? Transcript { get; set; }
|
||||
|
||||
[YamlMember(Alias = "assistant_context")]
|
||||
public string? AssistantContext { get; set; }
|
||||
|
||||
[YamlMember(Alias = "summary")]
|
||||
public string? Summary { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user