Make meeting lifecycle stateful

This commit is contained in:
2026-05-27 12:55:17 +02:00
parent d607b957bb
commit e85274829a
91 changed files with 4076 additions and 479 deletions
@@ -1,6 +1,5 @@
using MeetingAssistant.MeetingNotes;
using Microsoft.Extensions.Options;
using YamlDotNet.Serialization;
namespace MeetingAssistant.Summary;
@@ -21,25 +20,31 @@ public sealed class MeetingSummaryInstructionBuilder : IMeetingSummaryInstructio
Keep the output grounded in the source material and explicitly say when a section has no known items.
""";
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.Build();
private readonly MeetingAssistantOptions options;
private readonly BoundMeetingProjectResolver projectResolver;
public MeetingSummaryInstructionBuilder(IOptions<MeetingAssistantOptions> options)
{
this.options = options.Value;
projectResolver = new BoundMeetingProjectResolver(this.options);
}
public async Task<string> BuildAsync(
MeetingSessionArtifacts artifacts,
CancellationToken cancellationToken)
{
return await BuildAsync(artifacts, options, cancellationToken);
}
public async Task<string> BuildAsync(
MeetingSessionArtifacts artifacts,
MeetingAssistantOptions options,
CancellationToken cancellationToken)
{
var instructions = string.IsNullOrWhiteSpace(options.Agent.InitialPrompt)
? DefaultInitialPrompt
: options.Agent.InitialPrompt.Trim();
var projectInstructions = await BuildProjectInstructionsAsync(artifacts, cancellationToken);
var projectInstructions = await BuildProjectInstructionsAsync(artifacts, options, cancellationToken);
return string.IsNullOrWhiteSpace(projectInstructions)
? instructions
: instructions.TrimEnd() + "\n\n" + projectInstructions;
@@ -47,9 +52,10 @@ public sealed class MeetingSummaryInstructionBuilder : IMeetingSummaryInstructio
private async Task<string> BuildProjectInstructionsAsync(
MeetingSessionArtifacts artifacts,
MeetingAssistantOptions options,
CancellationToken cancellationToken)
{
var projects = await GetBoundProjectsWithInstructionsAsync(artifacts, cancellationToken);
var projects = await GetBoundProjectsWithInstructionsAsync(artifacts, options, cancellationToken);
if (projects.Count == 0)
{
return "";
@@ -62,30 +68,16 @@ public sealed class MeetingSummaryInstructionBuilder : IMeetingSummaryInstructio
private async Task<List<ProjectInstructions>> GetBoundProjectsWithInstructionsAsync(
MeetingSessionArtifacts artifacts,
MeetingAssistantOptions options,
CancellationToken cancellationToken)
{
var projectNames = await ReadMeetingProjectNamesAsync(artifacts.MeetingNotePath, cancellationToken);
if (projectNames.Count == 0)
{
return [];
}
var projectsRoot = VaultPath.Resolve(options.Vault, options.Vault.ProjectsFolder);
if (!Directory.Exists(projectsRoot))
{
return [];
}
var resolver = ReferenceEquals(options, this.options)
? projectResolver
: new BoundMeetingProjectResolver(options);
var projects = new List<ProjectInstructions>();
foreach (var projectDirectory in Directory.EnumerateDirectories(projectsRoot).Order(StringComparer.OrdinalIgnoreCase))
foreach (var project in await resolver.GetBoundProjectsAsync(artifacts, cancellationToken))
{
var projectName = Path.GetFileName(projectDirectory);
if (!projectNames.Contains(projectName))
{
continue;
}
var agentsPath = Path.Combine(projectDirectory, "AGENTS.md");
var agentsPath = Path.Combine(project.Path, "AGENTS.md");
if (!File.Exists(agentsPath))
{
continue;
@@ -94,42 +86,12 @@ public sealed class MeetingSummaryInstructionBuilder : IMeetingSummaryInstructio
var content = await File.ReadAllTextAsync(agentsPath, cancellationToken);
if (!string.IsNullOrWhiteSpace(content))
{
projects.Add(new ProjectInstructions(projectName, content));
projects.Add(new ProjectInstructions(project.Name, content));
}
}
return projects;
}
private static async Task<HashSet<string>> ReadMeetingProjectNamesAsync(
string meetingNotePath,
CancellationToken cancellationToken)
{
if (!File.Exists(meetingNotePath))
{
return [];
}
var content = await File.ReadAllTextAsync(meetingNotePath, cancellationToken);
var document = MarkdownDocumentParser.SplitOptional(content);
if (!document.HasFrontmatter)
{
return [];
}
var frontmatter = YamlDeserializer.Deserialize<ProjectFrontmatter>(document.Frontmatter)
?? new ProjectFrontmatter();
return (frontmatter.Projects ?? [])
.Where(project => !string.IsNullOrWhiteSpace(project))
.Select(project => project.Trim())
.ToHashSet(StringComparer.OrdinalIgnoreCase);
}
private sealed record ProjectInstructions(string Name, string Instructions);
private sealed class ProjectFrontmatter
{
[YamlMember(Alias = "projects")]
public List<string>? Projects { get; set; }
}
}