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, string agenda, DateTimeOffset? scheduledEnd, CancellationToken cancellationToken) { Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); var content = Render( artifacts, AssistantContextState.Transcribing, 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 agenda = existingFrontmatter.Agenda ?? ""; var scheduledEnd = ParseDateTime(existingFrontmatter.ScheduledEnd); await File.WriteAllTextAsync( artifacts.AssistantContextPath, Render(artifacts, state, agenda, scheduledEnd, body), cancellationToken); logger.LogInformation( "Updated assistant context note {AssistantContextPath} state to {State}", artifacts.AssistantContextPath, ToYamlState(state)); } private static string Render( MeetingSessionArtifacts artifacts, AssistantContextState state, string? agenda, DateTimeOffset? scheduledEnd, string body) { var frontmatter = new MeetingArtifactFrontmatter { Meeting = ObsidianLink.ToWikiLink(artifacts.MeetingNotePath, artifacts.AssistantContextPath, "Meeting Note"), Transcript = ObsidianLink.ToWikiLink(artifacts.TranscriptPath, artifacts.AssistantContextPath, "Transcript"), Summary = ObsidianLink.ToWikiLink(artifacts.SummaryPath, artifacts.AssistantContextPath, "Summary"), State = ToYamlState(state), Agenda = agenda ?? "", 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 = "agenda")] public string? Agenda { get; set; } [YamlMember(Alias = "scheduled_end")] public string? ScheduledEnd { get; set; } } private static string ToYamlState(AssistantContextState state) { return state switch { AssistantContextState.Transcribing => "transcribing", AssistantContextState.SpeakerRecognition => "speaker recognition", AssistantContextState.Summarizing => "summarizing", AssistantContextState.Finished => "finished", AssistantContextState.Error => "error", _ => "error" }; } }