Public Access
Archive summary refinements and profile switching
PR and Push Build/Test / build-and-test (push) Successful in 6m37s
PR and Push Build/Test / build-and-test (push) Successful in 6m37s
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
using MeetingAssistant.Speakers;
|
||||
using MeetingAssistant.Transcription;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
@@ -94,6 +95,117 @@ public sealed class MeetingSummaryTools
|
||||
return $"Added '{word.Trim()}'. Dictionary now contains {words.Count} word(s).";
|
||||
}
|
||||
|
||||
public async Task<string> AddAttendee(string attendee)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(attendee))
|
||||
{
|
||||
return "Refused: attendee must not be empty.";
|
||||
}
|
||||
|
||||
var displayName = attendee.Trim();
|
||||
var normalized = MeetingAttendeeNames.NormalizeDisplayName(displayName);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return "Refused: attendee must not be empty.";
|
||||
}
|
||||
|
||||
var note = await ReadMeetingNoteAsync();
|
||||
if (note.Frontmatter.Attendees.Any(existing => string.Equals(
|
||||
MeetingAttendeeNames.NormalizeDisplayName(existing),
|
||||
normalized,
|
||||
StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return $"Attendee {normalized} already exists.";
|
||||
}
|
||||
|
||||
note.Frontmatter.Attendees.Add(displayName);
|
||||
await WriteMeetingNoteAsync(note);
|
||||
return $"Added attendee {displayName}.";
|
||||
}
|
||||
|
||||
public async Task<string> RemoveAttendee(string attendee)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(attendee))
|
||||
{
|
||||
return "Refused: attendee must not be empty.";
|
||||
}
|
||||
|
||||
var normalized = MeetingAttendeeNames.NormalizeDisplayName(attendee);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return "Refused: attendee must not be empty.";
|
||||
}
|
||||
|
||||
var note = await ReadMeetingNoteAsync();
|
||||
var removed = note.Frontmatter.Attendees.RemoveAll(existing => string.Equals(
|
||||
MeetingAttendeeNames.NormalizeDisplayName(existing),
|
||||
normalized,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
if (removed == 0)
|
||||
{
|
||||
return $"Attendee {normalized} was not present.";
|
||||
}
|
||||
|
||||
await WriteMeetingNoteAsync(note);
|
||||
return $"Removed attendee {normalized}.";
|
||||
}
|
||||
|
||||
public async Task<string> OverrideSpeaker(string speaker, string replacement, bool merge = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(speaker) || string.IsNullOrWhiteSpace(replacement))
|
||||
{
|
||||
return "Refused: speaker and replacement must not be empty.";
|
||||
}
|
||||
|
||||
if (!File.Exists(artifacts.AssistantContextPath))
|
||||
{
|
||||
return $"Refused: assistant context file does not exist at {artifacts.AssistantContextPath}.";
|
||||
}
|
||||
|
||||
var speakerOverride = new SpeakerOverride(speaker.Trim(), replacement.Trim(), merge);
|
||||
if (string.Equals(speakerOverride.From, speakerOverride.To, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Refused: speaker and replacement must be different.";
|
||||
}
|
||||
|
||||
if (!merge && await SpeakerOverrideArtifacts.TranscriptContainsSpeakerAsync(
|
||||
artifacts.TranscriptPath,
|
||||
speakerOverride.To,
|
||||
CancellationToken.None))
|
||||
{
|
||||
return $"Refused: transcript already contains speaker {speakerOverride.To}. Call override_speaker with merge=true only when you are certain both speakers are the same identity.";
|
||||
}
|
||||
|
||||
await SpeakerOverrideArtifacts.ApplyToTranscriptAsync(artifacts.TranscriptPath, speakerOverride);
|
||||
await SpeakerOverrideArtifacts.AppendToAssistantContextAsync(artifacts.AssistantContextPath, speakerOverride);
|
||||
return merge
|
||||
? $"Merged speaker {speakerOverride.From} into {speakerOverride.To}."
|
||||
: $"Overrode speaker {speakerOverride.From} with {speakerOverride.To}.";
|
||||
}
|
||||
|
||||
public async Task<string> DeleteIdentity(string identity)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(identity))
|
||||
{
|
||||
return "Refused: identity must not be empty.";
|
||||
}
|
||||
|
||||
if (!File.Exists(artifacts.AssistantContextPath))
|
||||
{
|
||||
return $"Refused: assistant context file does not exist at {artifacts.AssistantContextPath}.";
|
||||
}
|
||||
|
||||
var deletion = await SpeakerOverrideArtifacts.ApplyDeletionToTranscriptAsync(
|
||||
artifacts.TranscriptPath,
|
||||
identity,
|
||||
CancellationToken.None);
|
||||
await SpeakerOverrideArtifacts.AppendIdentityDeletionToAssistantContextAsync(
|
||||
artifacts.AssistantContextPath,
|
||||
deletion,
|
||||
CancellationToken.None);
|
||||
return $"Deleted identity {deletion.Identity} and relabeled transcript speaker mentions to {deletion.Replacement}.";
|
||||
}
|
||||
|
||||
public async Task<string> WriteSummary(string markdown, string? title = null)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(artifacts.SummaryPath)!);
|
||||
@@ -109,9 +221,12 @@ public sealed class MeetingSummaryTools
|
||||
await File.WriteAllTextAsync(
|
||||
artifacts.SummaryPath,
|
||||
MeetingArtifactFrontmatterRenderer.Render(frontmatter, markdown));
|
||||
SummaryWasWritten = true;
|
||||
return artifacts.SummaryPath;
|
||||
}
|
||||
|
||||
public bool SummaryWasWritten { get; private set; }
|
||||
|
||||
public async Task<string> WriteContext(
|
||||
string content,
|
||||
int? @from = null,
|
||||
@@ -124,10 +239,12 @@ public sealed class MeetingSummaryTools
|
||||
return "Refused: supply either both from and to for replacement, insert for insertion, or no line arguments for overwrite.";
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!);
|
||||
var existing = File.Exists(artifacts.AssistantContextPath)
|
||||
? await File.ReadAllTextAsync(artifacts.AssistantContextPath)
|
||||
: "";
|
||||
if (!File.Exists(artifacts.AssistantContextPath))
|
||||
{
|
||||
return $"Refused: assistant context file does not exist at {artifacts.AssistantContextPath}.";
|
||||
}
|
||||
|
||||
var existing = await File.ReadAllTextAsync(artifacts.AssistantContextPath);
|
||||
var (frontmatter, body) = MeetingArtifactFrontmatterRenderer.Split(existing);
|
||||
var updatedBody = ApplyLineEdit(body, StripAccidentalFrontmatter(content), editMode);
|
||||
var updated = string.IsNullOrWhiteSpace(frontmatter)
|
||||
@@ -267,6 +384,64 @@ public sealed class MeetingSummaryTools
|
||||
return new MeetingNote(artifacts.MeetingNotePath, frontmatter, await ReadUserNotes());
|
||||
}
|
||||
|
||||
private async Task WriteMeetingNoteAsync(MeetingNote note)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!);
|
||||
await File.WriteAllTextAsync(
|
||||
artifacts.MeetingNotePath,
|
||||
RenderMeetingNote(note.Frontmatter, note.UserNotes));
|
||||
}
|
||||
|
||||
private static string RenderMeetingNote(MeetingNoteFrontmatter frontmatter, string userNotes)
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
"---",
|
||||
$"title: {EscapeScalar(frontmatter.Title)}",
|
||||
$"start_time: {EscapeNullableDateTime(frontmatter.StartTime)}",
|
||||
$"end_time: {EscapeNullableDateTime(frontmatter.EndTime)}",
|
||||
"attendees:"
|
||||
};
|
||||
|
||||
lines.AddRange(frontmatter.Attendees.Select(attendee => $"- {EscapeListItem(attendee)}"));
|
||||
lines.Add("projects:");
|
||||
lines.AddRange(frontmatter.Projects.Select(project => $"- {EscapeListItem(project)}"));
|
||||
lines.Add($"transcript: {EscapeQuoted(frontmatter.Transcript)}");
|
||||
lines.Add($"assistant_context: {EscapeQuoted(frontmatter.AssistantContext)}");
|
||||
lines.Add($"summary: {EscapeQuoted(frontmatter.Summary)}");
|
||||
lines.Add("---");
|
||||
lines.Add("");
|
||||
lines.Add(userNotes);
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
private static string EscapeScalar(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "\"\"" : EscapeListItem(value);
|
||||
}
|
||||
|
||||
private static string EscapeQuoted(string value)
|
||||
{
|
||||
return $"\"{value.Replace("\"", "\\\"", StringComparison.Ordinal)}\"";
|
||||
}
|
||||
|
||||
private static string EscapeNullableDateTime(DateTimeOffset? value)
|
||||
{
|
||||
return value is null ? "\"\"" : EscapeQuoted(value.Value.ToString("O"));
|
||||
}
|
||||
|
||||
private static string EscapeListItem(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
return value.Contains(':', StringComparison.Ordinal) || value.StartsWith("[", StringComparison.Ordinal)
|
||||
? EscapeQuoted(value)
|
||||
: value;
|
||||
}
|
||||
|
||||
private async Task<List<BoundMeetingProject>> GetSearchProjectsAsync(string[]? projects)
|
||||
{
|
||||
var boundProjects = await GetBoundProjectsAsync();
|
||||
|
||||
Reference in New Issue
Block a user