Public Access
feat: attach calendar metadata to active meetings
PR and Push Build/Test / build-and-test (push) Successful in 15m2s
PR and Push Build/Test / build-and-test (push) Successful in 15m2s
This commit is contained in:
@@ -98,7 +98,9 @@ public sealed class CalendarRecordingPromptScheduler : BackgroundService
|
||||
meeting.Subject,
|
||||
meeting.Start);
|
||||
await promptService.ShowPromptAsync(
|
||||
new MeetingStartPromptRequest(meeting),
|
||||
new MeetingStartPromptRequest(
|
||||
meeting,
|
||||
recordingController.CurrentStatus.IsRecording && meeting.Metadata is not null),
|
||||
(response, token) => HandlePromptResponseAsync(meeting, response, token),
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -139,6 +141,18 @@ public sealed class CalendarRecordingPromptScheduler : BackgroundService
|
||||
MeetingStartPromptResponse response,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (response == MeetingStartPromptResponse.AttachMetadataToCurrentMeeting)
|
||||
{
|
||||
if (meeting.Metadata is not null)
|
||||
{
|
||||
await recordingController.AttachMetadataToCurrentMeetingAsync(
|
||||
meeting.Metadata,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response != MeetingStartPromptResponse.Record)
|
||||
{
|
||||
return;
|
||||
@@ -244,12 +258,15 @@ public interface IMeetingStartPromptService
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record MeetingStartPromptRequest(CalendarMeeting Meeting);
|
||||
public sealed record MeetingStartPromptRequest(
|
||||
CalendarMeeting Meeting,
|
||||
bool CanAttachToCurrentMeeting = false);
|
||||
|
||||
public enum MeetingStartPromptResponse
|
||||
{
|
||||
Record,
|
||||
Skip
|
||||
Skip,
|
||||
AttachMetadataToCurrentMeeting
|
||||
}
|
||||
|
||||
public interface IMeetingPromptRecordingController
|
||||
@@ -262,6 +279,10 @@ public interface IMeetingPromptRecordingController
|
||||
MeetingMetadata? metadata,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<RecordingStatus> AttachMetadataToCurrentMeetingAsync(
|
||||
MeetingMetadata metadata,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<RecordingStatus> StopAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -288,6 +309,13 @@ public sealed class MeetingPromptRecordingController : IMeetingPromptRecordingCo
|
||||
return coordinator.StartFromPromptAsync(metadata, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<RecordingStatus> AttachMetadataToCurrentMeetingAsync(
|
||||
MeetingMetadata metadata,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return coordinator.AttachMetadataToCurrentMeetingAsync(metadata, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<RecordingStatus> StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return coordinator.StopAsync(cancellationToken);
|
||||
|
||||
@@ -118,21 +118,10 @@ public sealed class WindowsMeetingStartPromptService : IMeetingStartPromptServic
|
||||
string promptId,
|
||||
MeetingStartPromptRequest request)
|
||||
{
|
||||
var yesButton = new ToastButton()
|
||||
.SetContent("Yes")
|
||||
.AddArgument("source", NotificationSource)
|
||||
.AddArgument("promptId", promptId)
|
||||
.AddArgument("response", "record")
|
||||
.SetBackgroundActivation();
|
||||
var yesButton = BuildResponseButton("Yes", promptId, "record");
|
||||
var noButton = BuildResponseButton("No", promptId, "skip");
|
||||
|
||||
var noButton = new ToastButton()
|
||||
.SetContent("No")
|
||||
.AddArgument("source", NotificationSource)
|
||||
.AddArgument("promptId", promptId)
|
||||
.AddArgument("response", "skip")
|
||||
.SetBackgroundActivation();
|
||||
|
||||
return new ToastContentBuilder()
|
||||
var notification = new ToastContentBuilder()
|
||||
.AddArgument("source", NotificationSource)
|
||||
.AddArgument("promptId", promptId)
|
||||
.SetToastScenario(ToastScenario.Reminder)
|
||||
@@ -141,6 +130,30 @@ public sealed class WindowsMeetingStartPromptService : IMeetingStartPromptServic
|
||||
.AddText($"{request.Meeting.Subject} starts at {request.Meeting.Start.LocalDateTime:t}.")
|
||||
.AddButton(yesButton)
|
||||
.AddButton(noButton);
|
||||
|
||||
if (request.CanAttachToCurrentMeeting)
|
||||
{
|
||||
notification.AddButton(
|
||||
BuildResponseButton(
|
||||
"Add metadata to current meeting",
|
||||
promptId,
|
||||
"attach-metadata"));
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
private static ToastButton BuildResponseButton(
|
||||
string content,
|
||||
string promptId,
|
||||
string response)
|
||||
{
|
||||
return new ToastButton()
|
||||
.SetContent(content)
|
||||
.AddArgument("source", NotificationSource)
|
||||
.AddArgument("promptId", promptId)
|
||||
.AddArgument("response", response)
|
||||
.SetBackgroundActivation();
|
||||
}
|
||||
|
||||
private void OnNotificationInvoked(ToastNotificationActivatedEventArgsCompat args)
|
||||
@@ -166,10 +179,14 @@ public sealed class WindowsMeetingStartPromptService : IMeetingStartPromptServic
|
||||
return;
|
||||
}
|
||||
|
||||
var response = TryGetArgument(arguments, "response", out var responseValue) &&
|
||||
string.Equals(responseValue, "record", StringComparison.OrdinalIgnoreCase)
|
||||
? MeetingStartPromptResponse.Record
|
||||
: MeetingStartPromptResponse.Skip;
|
||||
var response = TryGetArgument(arguments, "response", out var responseValue)
|
||||
? responseValue.ToLowerInvariant() switch
|
||||
{
|
||||
"record" => MeetingStartPromptResponse.Record,
|
||||
"attach-metadata" => MeetingStartPromptResponse.AttachMetadataToCurrentMeeting,
|
||||
_ => MeetingStartPromptResponse.Skip
|
||||
}
|
||||
: MeetingStartPromptResponse.Skip;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
|
||||
@@ -155,6 +155,44 @@ public sealed class MeetingRecordingCoordinator
|
||||
return await StartAsync(null, metadata, suppressMetadataLookup: true, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<RecordingStatus> AttachMetadataToCurrentMeetingAsync(
|
||||
MeetingMetadata metadata,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var run = currentRun;
|
||||
if (run is null || run.IsCaptureStopping)
|
||||
{
|
||||
return CurrentStatus;
|
||||
}
|
||||
|
||||
var meetingNote = await ApplyAndPersistMeetingMetadataAsync(
|
||||
run.Artifacts,
|
||||
await meetingNoteStore.ReadAsync(run.MeetingNotePath, cancellationToken),
|
||||
metadata,
|
||||
run.Options,
|
||||
cancellationToken);
|
||||
currentMeetingNote = meetingNote;
|
||||
await transcriptStore.UpdateMetadataAsync(
|
||||
run.Session,
|
||||
run.Artifacts,
|
||||
meetingNote,
|
||||
cancellationToken);
|
||||
run.MarkPromptMetadataAttached();
|
||||
|
||||
logger.LogInformation(
|
||||
"Attached prompted calendar metadata to active meeting {MeetingNotePath}",
|
||||
run.MeetingNotePath);
|
||||
return CurrentStatus;
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RecordingStatus> StartAsync(
|
||||
string? launchProfileName,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -213,22 +251,12 @@ public sealed class MeetingRecordingCoordinator
|
||||
currentMeetingNote = await meetingNoteStore.ReadAsync(currentMeetingNote.Path, cancellationToken);
|
||||
if (suppliedMetadata is not null)
|
||||
{
|
||||
await ApplyMeetingMetadataAsync(
|
||||
currentMeetingNote = await ApplyAndPersistMeetingMetadataAsync(
|
||||
currentArtifacts,
|
||||
currentMeetingNote,
|
||||
suppliedMetadata,
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
currentMeetingNote = await meetingNoteStore.SaveAsync(
|
||||
currentMeetingNote,
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
await meetingArtifactStore.UpdateAssistantContextMetadataAsync(
|
||||
currentArtifacts,
|
||||
currentMeetingNote,
|
||||
suppliedMetadata.Agenda,
|
||||
suppliedMetadata.ScheduledEnd,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await meetingArtifactStore.UpdateAssistantContextMeetingAsync(
|
||||
@@ -1021,7 +1049,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
return;
|
||||
}
|
||||
|
||||
if (run.IsAborted)
|
||||
if (run.IsAborted || run.HasAttachedPromptMetadata)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1029,25 +1057,21 @@ public sealed class MeetingRecordingCoordinator
|
||||
await gate.WaitAsync(CancellationToken.None);
|
||||
try
|
||||
{
|
||||
if (run.IsAborted)
|
||||
if (run.IsAborted || run.HasAttachedPromptMetadata)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var meetingNote = await meetingNoteStore.ReadAsync(run.MeetingNotePath, CancellationToken.None);
|
||||
await ApplyMeetingMetadataAsync(run.Artifacts, meetingNote, metadata, run.Options, CancellationToken.None);
|
||||
meetingNote = await meetingNoteStore.SaveAsync(meetingNote, run.Options, CancellationToken.None);
|
||||
var meetingNote = await ApplyAndPersistMeetingMetadataAsync(
|
||||
run.Artifacts,
|
||||
await meetingNoteStore.ReadAsync(run.MeetingNotePath, CancellationToken.None),
|
||||
metadata,
|
||||
run.Options,
|
||||
CancellationToken.None);
|
||||
if (currentMeetingNote?.Path == meetingNote.Path)
|
||||
{
|
||||
currentMeetingNote = meetingNote;
|
||||
}
|
||||
|
||||
await meetingArtifactStore.UpdateAssistantContextMetadataAsync(
|
||||
run.Artifacts,
|
||||
meetingNote,
|
||||
metadata.Agenda,
|
||||
metadata.ScheduledEnd,
|
||||
CancellationToken.None);
|
||||
logger.LogInformation(
|
||||
"Applied Outlook meeting metadata to {MeetingNotePath}",
|
||||
run.MeetingNotePath);
|
||||
@@ -1119,6 +1143,32 @@ public sealed class MeetingRecordingCoordinator
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<MeetingNote> ApplyAndPersistMeetingMetadataAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
MeetingNote meetingNote,
|
||||
MeetingMetadata metadata,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await ApplyMeetingMetadataAsync(
|
||||
artifacts,
|
||||
meetingNote,
|
||||
metadata,
|
||||
options,
|
||||
cancellationToken);
|
||||
var savedMeetingNote = await meetingNoteStore.SaveAsync(
|
||||
meetingNote,
|
||||
options,
|
||||
cancellationToken);
|
||||
await meetingArtifactStore.UpdateAssistantContextMetadataAsync(
|
||||
artifacts,
|
||||
savedMeetingNote,
|
||||
metadata.Agenda,
|
||||
metadata.ScheduledEnd,
|
||||
cancellationToken);
|
||||
return savedMeetingNote;
|
||||
}
|
||||
|
||||
private async Task<List<string>> TransformAttendeesAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
IReadOnlyList<string> attendees,
|
||||
@@ -1908,6 +1958,8 @@ public sealed class MeetingRecordingCoordinator
|
||||
|
||||
public bool HasSwitchedProfile { get; private set; }
|
||||
|
||||
public bool HasAttachedPromptMetadata { get; private set; }
|
||||
|
||||
public AssistantContextState ContextState { get; private set; } = AssistantContextState.CollectingMetadata;
|
||||
|
||||
public DateTimeOffset? InferredEndTime
|
||||
@@ -1926,6 +1978,11 @@ public sealed class MeetingRecordingCoordinator
|
||||
CaptureCancellationSource.Cancel();
|
||||
}
|
||||
|
||||
public void MarkPromptMetadataAttached()
|
||||
{
|
||||
HasAttachedPromptMetadata = true;
|
||||
}
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
IsAborted = true;
|
||||
|
||||
Reference in New Issue
Block a user