namespace MeetingAssistant.MeetingNotes; using YamlDotNet.Serialization; public sealed class MarkdownMeetingArtifactStore : IMeetingArtifactStore { private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() .Build(); private readonly ILogger logger; public MarkdownMeetingArtifactStore(ILogger logger) { this.logger = logger; } public async Task CreateAssistantContextAsync( MeetingSessionArtifacts artifacts, MeetingNote meetingNote, string agenda, DateTimeOffset? scheduledEnd, CancellationToken cancellationToken) { Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); var content = Render( artifacts, meetingNote, AssistantContextState.CollectingMetadata, agenda, scheduledEnd, "# Assistant Context" + Environment.NewLine + Environment.NewLine + "## Live Context" + Environment.NewLine); await File.WriteAllTextAsync(artifacts.AssistantContextPath, content, cancellationToken); logger.LogInformation("Created assistant context note {AssistantContextPath}", artifacts.AssistantContextPath); } public async Task UpdateAssistantContextStateAsync( MeetingSessionArtifacts artifacts, AssistantContextState state, CancellationToken cancellationToken) { Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); var existing = File.Exists(artifacts.AssistantContextPath) ? MarkdownDocumentParser.SplitOptional(await File.ReadAllTextAsync(artifacts.AssistantContextPath, cancellationToken)) : new MarkdownDocument(false, "", "## Live Context" + Environment.NewLine); var body = existing.Body; var existingFrontmatter = ReadFrontmatter(existing.Frontmatter); var metadata = existingFrontmatter.ToMeetingArtifactMetadata(); var agenda = existingFrontmatter.Agenda ?? ""; var scheduledEnd = ParseDateTime(existingFrontmatter.ScheduledEnd); await File.WriteAllTextAsync( artifacts.AssistantContextPath, Render(artifacts, metadata, state, agenda, scheduledEnd, body), cancellationToken); logger.LogInformation( "Updated assistant context note {AssistantContextPath} state to {State}", artifacts.AssistantContextPath, ToYamlState(state)); } public async Task UpdateAssistantContextMetadataAsync( MeetingSessionArtifacts artifacts, MeetingNote meetingNote, string agenda, DateTimeOffset? scheduledEnd, CancellationToken cancellationToken) { Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); var existing = File.Exists(artifacts.AssistantContextPath) ? MarkdownDocumentParser.SplitOptional(await File.ReadAllTextAsync(artifacts.AssistantContextPath, cancellationToken)) : new MarkdownDocument(false, "", "## Live Context" + Environment.NewLine); var existingFrontmatter = ReadFrontmatter(existing.Frontmatter); var state = ParseState(existingFrontmatter.State); await File.WriteAllTextAsync( artifacts.AssistantContextPath, Render(artifacts, meetingNote, state, agenda, scheduledEnd, existing.Body), cancellationToken); logger.LogInformation( "Updated assistant context note {AssistantContextPath} calendar metadata", artifacts.AssistantContextPath); } public async Task UpdateAssistantContextMeetingAsync( MeetingSessionArtifacts artifacts, MeetingNote meetingNote, CancellationToken cancellationToken) { Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); var existing = File.Exists(artifacts.AssistantContextPath) ? MarkdownDocumentParser.SplitOptional(await File.ReadAllTextAsync(artifacts.AssistantContextPath, cancellationToken)) : new MarkdownDocument(false, "", "## Live Context" + Environment.NewLine); var existingFrontmatter = ReadFrontmatter(existing.Frontmatter); var state = ParseState(existingFrontmatter.State); var agenda = existingFrontmatter.Agenda ?? ""; var scheduledEnd = ParseDateTime(existingFrontmatter.ScheduledEnd); await File.WriteAllTextAsync( artifacts.AssistantContextPath, Render(artifacts, meetingNote, state, agenda, scheduledEnd, existing.Body), cancellationToken); logger.LogInformation( "Updated assistant context note {AssistantContextPath} meeting metadata", artifacts.AssistantContextPath); } public async Task AppendAssistantContextAsync( MeetingSessionArtifacts artifacts, string content, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(content)) { return; } Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); var prefix = File.Exists(artifacts.AssistantContextPath) && new FileInfo(artifacts.AssistantContextPath).Length > 0 ? Environment.NewLine : ""; await File.AppendAllTextAsync( artifacts.AssistantContextPath, prefix + content.TrimEnd() + Environment.NewLine, cancellationToken); logger.LogInformation( "Appended automation context to assistant context note {AssistantContextPath}", artifacts.AssistantContextPath); } private static string Render( MeetingSessionArtifacts artifacts, MeetingNote meetingNote, AssistantContextState state, string? agenda, DateTimeOffset? scheduledEnd, string body) { var metadata = new AssistantContextMeetingMetadata( meetingNote.Frontmatter.Title, meetingNote.Frontmatter.StartTime, meetingNote.Frontmatter.EndTime); return Render(artifacts, metadata, state, agenda, scheduledEnd, body); } private static string Render( MeetingSessionArtifacts artifacts, AssistantContextMeetingMetadata metadata, AssistantContextState state, string? agenda, DateTimeOffset? scheduledEnd, string body) { var frontmatter = MeetingArtifactFrontmatterRenderer.Create( artifacts, metadata.Title, metadata.StartTime, metadata.EndTime, artifacts.AssistantContextPath, ToYamlState(state)); frontmatter.Agenda = agenda ?? ""; frontmatter.ScheduledEnd = scheduledEnd; return MeetingArtifactFrontmatterRenderer.Render( frontmatter, body); } private static AssistantContextFrontmatterYaml ReadFrontmatter(string frontmatter) { if (string.IsNullOrWhiteSpace(frontmatter)) { return new AssistantContextFrontmatterYaml(); } return YamlDeserializer.Deserialize(frontmatter) ?? new AssistantContextFrontmatterYaml(); } private static DateTimeOffset? ParseDateTime(string? value) { return DateTimeOffset.TryParse(value, out var parsed) ? parsed : null; } private sealed class AssistantContextFrontmatterYaml { [YamlMember(Alias = "state")] public string? State { get; set; } [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 = "agenda")] public string? Agenda { get; set; } [YamlMember(Alias = "scheduled_end")] public string? ScheduledEnd { get; set; } public AssistantContextMeetingMetadata ToMeetingArtifactMetadata() { return new AssistantContextMeetingMetadata( Title ?? "", ParseDateTime(StartTime), ParseDateTime(EndTime)); } } private sealed record AssistantContextMeetingMetadata( string Title, DateTimeOffset? StartTime, DateTimeOffset? EndTime); private static AssistantContextState ParseState(string? value) { return value?.Trim().ToLowerInvariant() switch { "transcribing" => AssistantContextState.Transcribing, "collecting metadata" => AssistantContextState.CollectingMetadata, "speaker recognition" => AssistantContextState.SpeakerRecognition, "summarizing" => AssistantContextState.Summarizing, "finished" => AssistantContextState.Finished, "error" => AssistantContextState.Error, _ => AssistantContextState.CollectingMetadata }; } private static string ToYamlState(AssistantContextState state) { return state switch { AssistantContextState.CollectingMetadata => "collecting metadata", AssistantContextState.Transcribing => "transcribing", AssistantContextState.SpeakerRecognition => "speaker recognition", AssistantContextState.Summarizing => "summarizing", AssistantContextState.Finished => "finished", AssistantContextState.Error => "error", _ => "error" }; } }