Files
meeting-assistant/MeetingAssistant/Summary/MeetingSummaryArtifactResolver.cs
T
codex 7777b349a5
PR and Push Build/Test / build-and-test (push) Successful in 6m37s
Archive summary refinements and profile switching
2026-05-27 23:11:21 +02:00

228 lines
7.3 KiB
C#

using MeetingAssistant.MeetingNotes;
using Microsoft.Extensions.Options;
using YamlDotNet.Serialization;
namespace MeetingAssistant.Summary;
public interface IMeetingSummaryArtifactResolver
{
Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
string summaryPath,
CancellationToken cancellationToken);
Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
string summaryPath,
MeetingAssistantOptions options,
CancellationToken cancellationToken);
}
public sealed class MeetingSummaryArtifactResolver : IMeetingSummaryArtifactResolver
{
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.Build();
private readonly MeetingAssistantOptions options;
private readonly IMeetingNoteStore meetingNoteStore;
public MeetingSummaryArtifactResolver(
IOptions<MeetingAssistantOptions> options,
IMeetingNoteStore meetingNoteStore)
{
this.options = options.Value;
this.meetingNoteStore = meetingNoteStore;
}
public async Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
string summaryPath,
CancellationToken cancellationToken)
{
return await ResolveBySummaryPathAsync(summaryPath, options, cancellationToken);
}
public async Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
string summaryPath,
MeetingAssistantOptions options,
CancellationToken cancellationToken)
{
var requestedSummaryPath = ResolveRequestedSummaryPath(summaryPath, options);
if (requestedSummaryPath is null)
{
return null;
}
var summaryArtifacts = await ResolveFromSummaryFrontmatterAsync(
requestedSummaryPath,
cancellationToken);
if (summaryArtifacts is not null)
{
return summaryArtifacts;
}
var meetingNotesFolder = VaultPath.Resolve(options.Vault, options.Vault.MeetingNotesFolder);
if (!Directory.Exists(meetingNotesFolder))
{
return null;
}
foreach (var meetingNotePath in Directory.EnumerateFiles(meetingNotesFolder, "*.md"))
{
var note = await meetingNoteStore.ReadAsync(meetingNotePath, cancellationToken);
var noteSummaryPath = ResolveNoteLink(note.Frontmatter.Summary, note.Path);
if (!PathsEqual(requestedSummaryPath, noteSummaryPath))
{
continue;
}
return TryCreateArtifactsFromLinks(
note.Path,
requestedSummaryPath,
note.Frontmatter.Transcript,
note.Frontmatter.AssistantContext);
}
return null;
}
private static async Task<MeetingSessionArtifacts?> ResolveFromSummaryFrontmatterAsync(
string requestedSummaryPath,
CancellationToken cancellationToken)
{
if (!File.Exists(requestedSummaryPath))
{
return null;
}
var content = await File.ReadAllTextAsync(requestedSummaryPath, cancellationToken);
var (frontmatter, _) = MeetingArtifactFrontmatterRenderer.Split(content);
if (string.IsNullOrWhiteSpace(frontmatter))
{
return null;
}
SummaryArtifactLinks? links;
try
{
links = YamlDeserializer.Deserialize<SummaryArtifactLinks>(frontmatter);
}
catch (YamlDotNet.Core.YamlException)
{
return null;
}
if (string.IsNullOrWhiteSpace(links?.Meeting) ||
string.IsNullOrWhiteSpace(links.Transcript) ||
string.IsNullOrWhiteSpace(links.AssistantContext))
{
return null;
}
return TryCreateArtifactsFromLinks(
requestedSummaryPath,
requestedSummaryPath,
links.Transcript,
links.AssistantContext,
links.Meeting);
}
private static MeetingSessionArtifacts? TryCreateArtifactsFromLinks(
string sourceNotePath,
string requestedSummaryPath,
string transcriptLink,
string assistantContextLink,
string? meetingLink = null)
{
var assistantContextPath = ResolveNoteLink(assistantContextLink, sourceNotePath);
if (string.IsNullOrWhiteSpace(assistantContextPath) || !File.Exists(assistantContextPath))
{
return null;
}
return new MeetingSessionArtifacts(
meetingLink is null ? sourceNotePath : ResolveNoteLink(meetingLink, sourceNotePath),
ResolveNoteLink(transcriptLink, sourceNotePath),
assistantContextPath,
requestedSummaryPath);
}
private static string? ResolveRequestedSummaryPath(
string summaryPath,
MeetingAssistantOptions options)
{
if (string.IsNullOrWhiteSpace(summaryPath))
{
return null;
}
var summariesFolder = VaultPath.Resolve(options.Vault, options.Vault.SummariesFolder);
var requested = Environment.ExpandEnvironmentVariables(summaryPath);
var fullPath = Path.IsPathRooted(requested)
? Path.GetFullPath(requested)
: Path.GetFullPath(Path.Combine(summariesFolder, requested));
return IsWithinDirectory(summariesFolder, fullPath) ? fullPath : null;
}
private static string ResolveNoteLink(string pathOrLink, string sourceNotePath)
{
if (string.IsNullOrWhiteSpace(pathOrLink))
{
return "";
}
var sourceFolder = Path.GetDirectoryName(sourceNotePath) ?? "";
var target = ExtractWikiLinkTarget(pathOrLink) ?? pathOrLink;
target = target.Replace('/', Path.DirectorySeparatorChar);
if (!Path.HasExtension(target))
{
target += ".md";
}
return Path.GetFullPath(Path.IsPathRooted(target)
? target
: Path.Combine(sourceFolder, target));
}
private static string? ExtractWikiLinkTarget(string value)
{
if (!value.StartsWith("[[", StringComparison.Ordinal) ||
!value.EndsWith("]]", StringComparison.Ordinal))
{
return null;
}
var inner = value[2..^2];
var labelSeparator = inner.IndexOf('|', StringComparison.Ordinal);
return labelSeparator < 0 ? inner : inner[..labelSeparator];
}
private static bool PathsEqual(string left, string right)
{
return string.Equals(
Path.GetFullPath(left),
Path.GetFullPath(right),
StringComparison.OrdinalIgnoreCase);
}
private static bool IsWithinDirectory(string directory, string path)
{
var normalizedDirectory = Path.GetFullPath(directory)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
var normalizedPath = Path.GetFullPath(path);
return normalizedPath.StartsWith(normalizedDirectory, StringComparison.OrdinalIgnoreCase);
}
private sealed class SummaryArtifactLinks
{
[YamlMember(Alias = "meeting")]
public string? Meeting { get; set; }
[YamlMember(Alias = "transcript")]
public string? Transcript { get; set; }
[YamlMember(Alias = "assistant_context")]
public string? AssistantContext { get; set; }
}
}