Files
meeting-assistant/MeetingAssistant/MeetingNotes/MarkdownMeetingArtifactStore.cs
2026-05-27 12:55:18 +02:00

253 lines
9.6 KiB
C#

namespace MeetingAssistant.MeetingNotes;
using YamlDotNet.Serialization;
public sealed class MarkdownMeetingArtifactStore : IMeetingArtifactStore
{
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.Build();
private readonly ILogger<MarkdownMeetingArtifactStore> logger;
public MarkdownMeetingArtifactStore(ILogger<MarkdownMeetingArtifactStore> 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<AssistantContextFrontmatterYaml>(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"
};
}
}