Public Access
Add attendee transformation workflows
This commit is contained in:
@@ -213,7 +213,12 @@ public sealed class MeetingRecordingCoordinator
|
||||
currentMeetingNote = await meetingNoteStore.ReadAsync(currentMeetingNote.Path, cancellationToken);
|
||||
if (suppliedMetadata is not null)
|
||||
{
|
||||
await ApplyMeetingMetadataAsync(currentMeetingNote, suppliedMetadata, runOptions, cancellationToken);
|
||||
await ApplyMeetingMetadataAsync(
|
||||
currentArtifacts,
|
||||
currentMeetingNote,
|
||||
suppliedMetadata,
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
currentMeetingNote = await meetingNoteStore.SaveAsync(
|
||||
currentMeetingNote,
|
||||
runOptions,
|
||||
@@ -1030,7 +1035,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
}
|
||||
|
||||
var meetingNote = await meetingNoteStore.ReadAsync(run.MeetingNotePath, CancellationToken.None);
|
||||
await ApplyMeetingMetadataAsync(meetingNote, metadata, run.Options, CancellationToken.None);
|
||||
await ApplyMeetingMetadataAsync(run.Artifacts, meetingNote, metadata, run.Options, CancellationToken.None);
|
||||
meetingNote = await meetingNoteStore.SaveAsync(meetingNote, run.Options, CancellationToken.None);
|
||||
if (currentMeetingNote?.Path == meetingNote.Path)
|
||||
{
|
||||
@@ -1082,6 +1087,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
}
|
||||
|
||||
private async Task ApplyMeetingMetadataAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
MeetingNote meetingNote,
|
||||
MeetingMetadata metadata,
|
||||
MeetingAssistantOptions options,
|
||||
@@ -1104,10 +1110,39 @@ public sealed class MeetingRecordingCoordinator
|
||||
return;
|
||||
}
|
||||
|
||||
meetingNote.Frontmatter.Attendees = await CanonicalizeAttendeesAsync(metadata.Attendees, cancellationToken);
|
||||
var canonicalized = await CanonicalizeAttendeesAsync(metadata.Attendees, cancellationToken);
|
||||
meetingNote.Frontmatter.Attendees = await TransformAttendeesAsync(
|
||||
artifacts,
|
||||
canonicalized,
|
||||
options,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<string>> TransformAttendeesAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
IReadOnlyList<string> attendees,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var transformed = new List<string>();
|
||||
foreach (var attendee in attendees)
|
||||
{
|
||||
var value = await TransformAttendeeAsync(artifacts, attendee, options, cancellationToken);
|
||||
var storageValue = string.Equals(value, attendee, StringComparison.Ordinal)
|
||||
? attendee.Trim()
|
||||
: NormalizeAttendeeName(value);
|
||||
var normalized = NormalizeAttendeeName(storageValue);
|
||||
if (!string.IsNullOrWhiteSpace(normalized) &&
|
||||
!transformed.Contains(normalized, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
transformed.Add(storageValue);
|
||||
}
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
private async Task<List<string>> CanonicalizeAttendeesAsync(
|
||||
IReadOnlyList<string> attendees,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -1422,8 +1457,17 @@ public sealed class MeetingRecordingCoordinator
|
||||
continue;
|
||||
}
|
||||
|
||||
var transformedDisplayName = await TransformAttendeeAsync(
|
||||
artifacts,
|
||||
match.DisplayName.Trim(),
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
var displayName = string.Equals(transformedDisplayName, match.DisplayName.Trim(), StringComparison.Ordinal)
|
||||
? match.DisplayName.Trim()
|
||||
: NormalizeAttendeeName(transformedDisplayName);
|
||||
var acceptedNames = match.AcceptedNames
|
||||
.Append(match.DisplayName)
|
||||
.Append(displayName)
|
||||
.Select(NormalizeAttendeeName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -1442,7 +1486,6 @@ public sealed class MeetingRecordingCoordinator
|
||||
continue;
|
||||
}
|
||||
|
||||
var displayName = match.DisplayName.Trim();
|
||||
meetingNote.Frontmatter.Attendees.Add(displayName);
|
||||
existingNames.Add(NormalizeAttendeeName(displayName));
|
||||
changed = true;
|
||||
@@ -1502,6 +1545,18 @@ public sealed class MeetingRecordingCoordinator
|
||||
return MeetingAttendeeNames.NormalizeDisplayName(attendee);
|
||||
}
|
||||
|
||||
private async Task<string> TransformAttendeeAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
string attendee,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await meetingWorkflowEngine.TransformAttendeeAsync(
|
||||
MeetingWorkflowEvent.AttendeeAdded(artifacts, attendee),
|
||||
options,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static bool RemoveDuplicateAcceptedAliases(
|
||||
List<string> attendees,
|
||||
string displayName,
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
using MeetingAssistant.Speakers;
|
||||
using MeetingAssistant.Workflow;
|
||||
|
||||
namespace MeetingAssistant.Screenshots;
|
||||
|
||||
@@ -149,6 +150,7 @@ public sealed partial class MeetingScreenshotService :
|
||||
private readonly IMeetingArtifactStore artifactStore;
|
||||
private readonly IMeetingNoteStore meetingNoteStore;
|
||||
private readonly ISpeakerIdentityAttendeeCanonicalizer attendeeCanonicalizer;
|
||||
private readonly IMeetingWorkflowEngine meetingWorkflowEngine;
|
||||
private readonly IScreenshotOcrClient ocrClient;
|
||||
private readonly ILogger<MeetingScreenshotService> logger;
|
||||
private readonly ConcurrentDictionary<string, List<PendingOcrTask>> pendingOcrByContext = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -160,12 +162,14 @@ public sealed partial class MeetingScreenshotService :
|
||||
IMeetingNoteStore meetingNoteStore,
|
||||
ISpeakerIdentityAttendeeCanonicalizer attendeeCanonicalizer,
|
||||
IScreenshotOcrClient ocrClient,
|
||||
ILogger<MeetingScreenshotService> logger)
|
||||
ILogger<MeetingScreenshotService> logger,
|
||||
IMeetingWorkflowEngine? meetingWorkflowEngine = null)
|
||||
{
|
||||
this.screenshotCapture = screenshotCapture;
|
||||
this.artifactStore = artifactStore;
|
||||
this.meetingNoteStore = meetingNoteStore;
|
||||
this.attendeeCanonicalizer = attendeeCanonicalizer;
|
||||
this.meetingWorkflowEngine = meetingWorkflowEngine ?? NoopMeetingWorkflowEngine.Instance;
|
||||
this.ocrClient = ocrClient;
|
||||
this.logger = logger;
|
||||
}
|
||||
@@ -434,6 +438,7 @@ public sealed partial class MeetingScreenshotService :
|
||||
await AddOcrAttendeesAsync(
|
||||
artifacts,
|
||||
result.Attendees,
|
||||
options,
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -477,6 +482,7 @@ public sealed partial class MeetingScreenshotService :
|
||||
private async Task AddOcrAttendeesAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
IReadOnlyList<string> attendees,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var additions = MeetingAttendeeNames.NormalizeDistinct(attendees);
|
||||
@@ -488,8 +494,13 @@ public sealed partial class MeetingScreenshotService :
|
||||
try
|
||||
{
|
||||
var meetingNote = await meetingNoteStore.ReadAsync(artifacts.MeetingNotePath, cancellationToken);
|
||||
var transformedAdditions = await TransformAttendeesAsync(
|
||||
artifacts,
|
||||
additions,
|
||||
options,
|
||||
cancellationToken);
|
||||
var canonicalized = await attendeeCanonicalizer.CanonicalizeAsync(
|
||||
meetingNote.Frontmatter.Attendees.Concat(additions).ToList(),
|
||||
meetingNote.Frontmatter.Attendees.Concat(transformedAdditions).ToList(),
|
||||
cancellationToken);
|
||||
if (meetingNote.Frontmatter.Attendees.SequenceEqual(canonicalized, StringComparer.Ordinal))
|
||||
{
|
||||
@@ -512,6 +523,30 @@ public sealed partial class MeetingScreenshotService :
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> TransformAttendeesAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
IReadOnlyList<string> attendees,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var transformed = new List<string>();
|
||||
foreach (var attendee in attendees)
|
||||
{
|
||||
var value = await meetingWorkflowEngine.TransformAttendeeAsync(
|
||||
MeetingWorkflowEvent.AttendeeAdded(artifacts, attendee),
|
||||
options,
|
||||
cancellationToken);
|
||||
var normalized = MeetingAttendeeNames.NormalizeDisplayName(value);
|
||||
if (!string.IsNullOrWhiteSpace(normalized) &&
|
||||
!transformed.Contains(normalized, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
transformed.Add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
private async Task<bool> ReplaceOcrPlaceholderAsync(
|
||||
string assistantContextPath,
|
||||
string screenshotId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
using MeetingAssistant.Speakers;
|
||||
using MeetingAssistant.Transcription;
|
||||
using MeetingAssistant.Workflow;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace MeetingAssistant.Summary;
|
||||
@@ -13,6 +14,7 @@ public sealed class MeetingSummaryTools
|
||||
private readonly MeetingAssistantOptions options;
|
||||
private readonly IDictationWordStore? dictationWordStore;
|
||||
private readonly SummaryAgentWriteAudit? writeAudit;
|
||||
private readonly IMeetingWorkflowEngine meetingWorkflowEngine;
|
||||
private readonly BoundMeetingProjectResolver projectResolver;
|
||||
|
||||
public MeetingSummaryTools(MeetingSessionArtifacts artifacts)
|
||||
@@ -24,12 +26,14 @@ public sealed class MeetingSummaryTools
|
||||
MeetingSessionArtifacts artifacts,
|
||||
MeetingAssistantOptions options,
|
||||
IDictationWordStore? dictationWordStore = null,
|
||||
SummaryAgentWriteAudit? writeAudit = null)
|
||||
SummaryAgentWriteAudit? writeAudit = null,
|
||||
IMeetingWorkflowEngine? meetingWorkflowEngine = null)
|
||||
{
|
||||
this.artifacts = artifacts;
|
||||
this.options = options;
|
||||
this.dictationWordStore = dictationWordStore;
|
||||
this.writeAudit = writeAudit;
|
||||
this.meetingWorkflowEngine = meetingWorkflowEngine ?? NoopMeetingWorkflowEngine.Instance;
|
||||
projectResolver = new BoundMeetingProjectResolver(options);
|
||||
}
|
||||
|
||||
@@ -98,7 +102,11 @@ public sealed class MeetingSummaryTools
|
||||
return "Refused: attendee must not be empty.";
|
||||
}
|
||||
|
||||
var displayName = attendee.Trim();
|
||||
var displayName = await meetingWorkflowEngine.TransformAttendeeAsync(
|
||||
MeetingWorkflowEvent.AttendeeAdded(artifacts, attendee.Trim()),
|
||||
options,
|
||||
CancellationToken.None);
|
||||
displayName = displayName.Trim();
|
||||
var normalized = MeetingAttendeeNames.NormalizeDisplayName(displayName);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
using MeetingAssistant.Transcription;
|
||||
using MeetingAssistant.Workflow;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -17,6 +18,7 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
|
||||
private readonly IMeetingSummaryFailureWriter failureWriter;
|
||||
private readonly IMeetingSummaryInstructionBuilder instructionBuilder;
|
||||
private readonly IDictationWordStore dictationWordStore;
|
||||
private readonly IMeetingWorkflowEngine meetingWorkflowEngine;
|
||||
|
||||
public OpenAiMeetingSummaryAgentPipeline(
|
||||
IOptions<MeetingAssistantOptions> options,
|
||||
@@ -25,7 +27,8 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
|
||||
IServiceProvider services,
|
||||
IMeetingSummaryFailureWriter failureWriter,
|
||||
IMeetingSummaryInstructionBuilder instructionBuilder,
|
||||
IDictationWordStore dictationWordStore)
|
||||
IDictationWordStore dictationWordStore,
|
||||
IMeetingWorkflowEngine meetingWorkflowEngine)
|
||||
{
|
||||
this.options = options.Value;
|
||||
this.loggerFactory = loggerFactory;
|
||||
@@ -34,6 +37,7 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
|
||||
this.failureWriter = failureWriter;
|
||||
this.instructionBuilder = instructionBuilder;
|
||||
this.dictationWordStore = dictationWordStore;
|
||||
this.meetingWorkflowEngine = meetingWorkflowEngine;
|
||||
}
|
||||
|
||||
public async Task<MeetingSummaryRunResult> RunAsync(
|
||||
@@ -51,7 +55,12 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
|
||||
var agentOptions = options.Agent;
|
||||
var key = ResolveApiKey(agentOptions);
|
||||
var writeAudit = new SummaryAgentWriteAudit(artifacts);
|
||||
var meetingTools = new MeetingSummaryTools(artifacts, options, dictationWordStore, writeAudit);
|
||||
var meetingTools = new MeetingSummaryTools(
|
||||
artifacts,
|
||||
options,
|
||||
dictationWordStore,
|
||||
writeAudit,
|
||||
meetingWorkflowEngine);
|
||||
var tools = CreateTools(meetingTools);
|
||||
var instructions = await instructionBuilder.BuildAsync(artifacts, options, cancellationToken);
|
||||
using var compactionSummaryClient = new LiteLlmResponsesChatClient(
|
||||
|
||||
@@ -17,6 +17,14 @@ public interface IMeetingWorkflowEngine
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<string> TransformAttendeeAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(workflowEvent.AttendeeName ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class NoopMeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
@@ -38,6 +46,14 @@ public sealed class NoopMeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
{
|
||||
return Task.FromResult(workflowEvent.TranscriptLineText ?? "");
|
||||
}
|
||||
|
||||
public Task<string> TransformAttendeeAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(workflowEvent.AttendeeName ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
@@ -54,7 +70,8 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
"state.to",
|
||||
"speaker.name",
|
||||
MeetingWorkflowRuleSchema.PropertyTranscriptLine,
|
||||
MeetingWorkflowRuleSchema.ParameterTranscriptSpeaker
|
||||
MeetingWorkflowRuleSchema.ParameterTranscriptSpeaker,
|
||||
MeetingWorkflowRuleSchema.PropertyAttendeeName
|
||||
];
|
||||
|
||||
private readonly IMeetingWorkflowRulesProvider rulesProvider;
|
||||
@@ -98,6 +115,21 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
?? "";
|
||||
}
|
||||
|
||||
public async Task<string> TransformAttendeeAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (workflowEvent.Type != MeetingWorkflowEventType.AttendeeAdded)
|
||||
{
|
||||
throw new ArgumentException("Workflow event must be an attendee added event.", nameof(workflowEvent));
|
||||
}
|
||||
|
||||
return await RunCoreAsync(workflowEvent, options, cancellationToken)
|
||||
?? workflowEvent.AttendeeName
|
||||
?? "";
|
||||
}
|
||||
|
||||
private async Task<string?> RunCoreAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
@@ -107,7 +139,7 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
var context = new MeetingWorkflowExecutionContext(workflowEvent);
|
||||
if (rules.Count == 0)
|
||||
{
|
||||
return context.TranscriptLine;
|
||||
return context.ResultValue;
|
||||
}
|
||||
|
||||
var meeting = await meetingNoteStore.ReadAsync(
|
||||
@@ -134,10 +166,18 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
var model = CreateTemplateModel(meeting, context);
|
||||
foreach (var step in rule.Steps)
|
||||
{
|
||||
if (workflowEvent.Type == MeetingWorkflowEventType.AttendeeAdded &&
|
||||
!IsAttendeeTransformStep(step))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"attendee_added workflow rules can only transform attendee.name with set_property.");
|
||||
}
|
||||
|
||||
var stepChanged = await ApplyStepAsync(
|
||||
step,
|
||||
meeting,
|
||||
context,
|
||||
options,
|
||||
model,
|
||||
cancellationToken);
|
||||
ruleNoteChanged |= stepChanged;
|
||||
@@ -176,7 +216,7 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
return context.TranscriptLine;
|
||||
return context.ResultValue;
|
||||
}
|
||||
|
||||
private static bool MatchesTrigger(
|
||||
@@ -229,9 +269,41 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (trigger.AttendeeAdded is not null)
|
||||
{
|
||||
return workflowEvent.Type == MeetingWorkflowEventType.AttendeeAdded &&
|
||||
MatchesAttendeeAddedTrigger(trigger.AttendeeAdded, workflowEvent.AttendeeName);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool MatchesAttendeeAddedTrigger(
|
||||
MeetingWorkflowAttendeeAddedTrigger trigger,
|
||||
string? attendeeName)
|
||||
{
|
||||
var attendee = attendeeName ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(trigger.EqualsValue) &&
|
||||
!string.Equals(attendee, trigger.EqualsValue.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trigger.Contains) &&
|
||||
!attendee.Contains(trigger.Contains.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trigger.Regex) &&
|
||||
!Regex.IsMatch(attendee, trigger.Regex, RegexOptions.IgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EvaluateConditions(
|
||||
IReadOnlyList<MeetingWorkflowCondition> conditions,
|
||||
MeetingNote meeting,
|
||||
@@ -298,6 +370,7 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
MeetingWorkflowStep step,
|
||||
MeetingNote meeting,
|
||||
MeetingWorkflowExecutionContext context,
|
||||
MeetingAssistantOptions options,
|
||||
MeetingWorkflowTemplateModel model,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -305,7 +378,11 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
switch (step.Uses.Trim().ToLowerInvariant())
|
||||
{
|
||||
case MeetingWorkflowRuleSchema.StepAddAttendee:
|
||||
return AddUnique(meeting.Frontmatter.Attendees, value, MeetingAttendeeNames.NormalizeDisplayName);
|
||||
var attendee = await TransformAttendeeAsync(
|
||||
MeetingWorkflowEvent.AttendeeAdded(context.Event.Artifacts, value),
|
||||
options,
|
||||
cancellationToken);
|
||||
return AddUnique(meeting.Frontmatter.Attendees, attendee, MeetingAttendeeNames.NormalizeDisplayName);
|
||||
case MeetingWorkflowRuleSchema.StepRemoveAttendee:
|
||||
return RemoveValue(meeting.Frontmatter.Attendees, value);
|
||||
case MeetingWorkflowRuleSchema.StepAddProject:
|
||||
@@ -319,6 +396,9 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
case MeetingWorkflowRuleSchema.StepSetProperty when IsTranscriptLineProperty(step.Property ?? step.Name):
|
||||
SetTranscriptLine(context, value);
|
||||
return false;
|
||||
case MeetingWorkflowRuleSchema.StepSetProperty when IsAttendeeNameProperty(step.Property ?? step.Name):
|
||||
SetAttendeeName(context, value);
|
||||
return false;
|
||||
case MeetingWorkflowRuleSchema.StepSetProperty:
|
||||
return SetProperty(meeting, step.Property ?? step.Name, value);
|
||||
default:
|
||||
@@ -370,11 +450,39 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
context.TranscriptLine = value;
|
||||
}
|
||||
|
||||
private static void SetAttendeeName(
|
||||
MeetingWorkflowExecutionContext context,
|
||||
string value)
|
||||
{
|
||||
if (context.Event.Type != MeetingWorkflowEventType.AttendeeAdded)
|
||||
{
|
||||
throw new InvalidOperationException("The attendee.name property can only be set during attendee_added events.");
|
||||
}
|
||||
|
||||
if (string.Equals(context.AttendeeName, value, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.AttendeeName = value;
|
||||
}
|
||||
|
||||
private static bool IsTranscriptLineProperty(string? property)
|
||||
{
|
||||
return MeetingWorkflowRuleSchema.IsProperty(property, MeetingWorkflowRuleSchema.PropertyTranscriptLine);
|
||||
}
|
||||
|
||||
private static bool IsAttendeeNameProperty(string? property)
|
||||
{
|
||||
return MeetingWorkflowRuleSchema.IsProperty(property, MeetingWorkflowRuleSchema.PropertyAttendeeName);
|
||||
}
|
||||
|
||||
private static bool IsAttendeeTransformStep(MeetingWorkflowStep step)
|
||||
{
|
||||
return MeetingWorkflowRuleSchema.IsStep(step.Uses, MeetingWorkflowRuleSchema.StepSetProperty) &&
|
||||
IsAttendeeNameProperty(step.Property ?? step.Name);
|
||||
}
|
||||
|
||||
private static bool AddUnique(
|
||||
List<string> values,
|
||||
string value,
|
||||
@@ -456,7 +564,8 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
["state.to"] = workflowEvent.ToState is { } to ? MeetingWorkflowStateNames.ToRuleName(to) : "",
|
||||
["speaker.name"] = workflowEvent.SpeakerName,
|
||||
[MeetingWorkflowRuleSchema.PropertyTranscriptLine] = context.TranscriptLine ?? "",
|
||||
[MeetingWorkflowRuleSchema.ParameterTranscriptSpeaker] = workflowEvent.SpeakerName ?? ""
|
||||
[MeetingWorkflowRuleSchema.ParameterTranscriptSpeaker] = workflowEvent.SpeakerName ?? "",
|
||||
[MeetingWorkflowRuleSchema.PropertyAttendeeName] = context.AttendeeName ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
@@ -480,7 +589,10 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
: new MeetingWorkflowSpeakerModel(workflowEvent.SpeakerName),
|
||||
new MeetingWorkflowTranscriptModel(
|
||||
context.TranscriptLine ?? "",
|
||||
workflowEvent.SpeakerName ?? ""));
|
||||
workflowEvent.SpeakerName ?? ""),
|
||||
string.IsNullOrWhiteSpace(context.AttendeeName)
|
||||
? null
|
||||
: new MeetingWorkflowAttendeeModel(context.AttendeeName));
|
||||
}
|
||||
|
||||
private sealed class MeetingWorkflowExecutionContext
|
||||
@@ -489,10 +601,23 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
{
|
||||
Event = workflowEvent;
|
||||
TranscriptLine = workflowEvent.TranscriptLineText;
|
||||
AttendeeName = workflowEvent.AttendeeName;
|
||||
}
|
||||
|
||||
public MeetingWorkflowEvent Event { get; }
|
||||
|
||||
public string? TranscriptLine { get; set; }
|
||||
|
||||
public string? AttendeeName { get; set; }
|
||||
|
||||
public string? ResultValue
|
||||
{
|
||||
get
|
||||
{
|
||||
return Event.Type == MeetingWorkflowEventType.AttendeeAdded
|
||||
? AttendeeName
|
||||
: TranscriptLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ public enum MeetingWorkflowEventType
|
||||
Created,
|
||||
StateTransition,
|
||||
SpeakerIdentified,
|
||||
TranscriptLine
|
||||
TranscriptLine,
|
||||
AttendeeAdded
|
||||
}
|
||||
|
||||
public sealed record MeetingWorkflowEvent(
|
||||
@@ -16,7 +17,8 @@ public sealed record MeetingWorkflowEvent(
|
||||
AssistantContextState? FromState = null,
|
||||
AssistantContextState? ToState = null,
|
||||
string? SpeakerName = null,
|
||||
string? TranscriptLineText = null)
|
||||
string? TranscriptLineText = null,
|
||||
string? AttendeeName = null)
|
||||
{
|
||||
public static MeetingWorkflowEvent Created(MeetingSessionArtifacts artifacts)
|
||||
{
|
||||
@@ -49,4 +51,14 @@ public sealed record MeetingWorkflowEvent(
|
||||
SpeakerName: speakerName,
|
||||
TranscriptLineText: line);
|
||||
}
|
||||
|
||||
public static MeetingWorkflowEvent AttendeeAdded(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
string attendeeName)
|
||||
{
|
||||
return new MeetingWorkflowEvent(
|
||||
MeetingWorkflowEventType.AttendeeAdded,
|
||||
artifacts,
|
||||
AttendeeName: attendeeName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ public sealed class MeetingWorkflowTrigger
|
||||
|
||||
[YamlMember(Alias = "transcript_line")]
|
||||
public MeetingWorkflowTranscriptLineTrigger? TranscriptLine { get; set; }
|
||||
|
||||
[YamlMember(Alias = "attendee_added")]
|
||||
public MeetingWorkflowAttendeeAddedTrigger? AttendeeAdded { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MeetingWorkflowStateTransitionTrigger
|
||||
@@ -60,6 +63,18 @@ public sealed class MeetingWorkflowTranscriptLineTrigger
|
||||
public string? Speaker { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MeetingWorkflowAttendeeAddedTrigger
|
||||
{
|
||||
[YamlMember(Alias = "equals")]
|
||||
public string? EqualsValue { get; set; }
|
||||
|
||||
[YamlMember(Alias = "contains")]
|
||||
public string? Contains { get; set; }
|
||||
|
||||
[YamlMember(Alias = "regex")]
|
||||
public string? Regex { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MeetingWorkflowCondition
|
||||
{
|
||||
[YamlMember(Alias = "condition")]
|
||||
@@ -94,7 +109,8 @@ public sealed record MeetingWorkflowTemplateModel(
|
||||
MeetingWorkflowMeetingModel Meeting,
|
||||
MeetingWorkflowEventModel Event,
|
||||
MeetingWorkflowSpeakerModel? Speaker,
|
||||
MeetingWorkflowTranscriptModel? Transcript);
|
||||
MeetingWorkflowTranscriptModel? Transcript,
|
||||
MeetingWorkflowAttendeeModel? Attendee);
|
||||
|
||||
public sealed record MeetingWorkflowMeetingModel(
|
||||
string Title,
|
||||
@@ -111,6 +127,8 @@ public sealed record MeetingWorkflowSpeakerModel(string Name);
|
||||
|
||||
public sealed record MeetingWorkflowTranscriptModel(string Line, string Speaker);
|
||||
|
||||
public sealed record MeetingWorkflowAttendeeModel(string Name);
|
||||
|
||||
internal static class MeetingWorkflowStateNames
|
||||
{
|
||||
public static string ToRuleName(AssistantContextState state)
|
||||
|
||||
@@ -12,13 +12,15 @@ internal static class MeetingWorkflowRuleSchema
|
||||
public const string PropertyMeetingTitle = "meeting.title";
|
||||
public const string PropertyTranscriptLine = "transcript.line";
|
||||
public const string ParameterTranscriptSpeaker = "transcript.speaker";
|
||||
public const string PropertyAttendeeName = "attendee.name";
|
||||
|
||||
public static readonly string[] SupportedTriggerKeys =
|
||||
[
|
||||
"created",
|
||||
"state_transition",
|
||||
"speaker_identified",
|
||||
"transcript_line"
|
||||
"transcript_line",
|
||||
"attendee_added"
|
||||
];
|
||||
|
||||
public static readonly string[] SupportedConditionKeys =
|
||||
@@ -42,7 +44,8 @@ internal static class MeetingWorkflowRuleSchema
|
||||
[
|
||||
PropertyTitle,
|
||||
PropertyMeetingTitle,
|
||||
PropertyTranscriptLine
|
||||
PropertyTranscriptLine,
|
||||
PropertyAttendeeName
|
||||
];
|
||||
|
||||
public static bool IsStep(string? value, string step)
|
||||
|
||||
@@ -59,7 +59,8 @@ internal static class MeetingWorkflowRulesValidator
|
||||
trigger.Created is not null,
|
||||
trigger.StateTransition is not null,
|
||||
trigger.SpeakerIdentified is not null,
|
||||
trigger.TranscriptLine is not null);
|
||||
trigger.TranscriptLine is not null,
|
||||
trigger.AttendeeAdded is not null);
|
||||
if (configuredCount == 0)
|
||||
{
|
||||
errors.Add($"{ruleName} on[{triggerIndex + 1}] must contain one supported trigger: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedTriggerKeys)}.");
|
||||
@@ -137,6 +138,12 @@ internal static class MeetingWorkflowRulesValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasAttendeeAddedTrigger(rule) &&
|
||||
!IsAttendeeNameSetPropertyStep(step))
|
||||
{
|
||||
errors.Add($"{stepPath} on attendee_added rules can only transform the attendee with 'set_property' property 'attendee.name'.");
|
||||
}
|
||||
|
||||
if (MeetingWorkflowRuleSchema.IsStep(uses, MeetingWorkflowRuleSchema.StepSetProperty))
|
||||
{
|
||||
var property = step.Property ?? step.Name;
|
||||
@@ -153,6 +160,11 @@ internal static class MeetingWorkflowRulesValidator
|
||||
{
|
||||
errors.Add($"{stepPath} can set 'transcript.line' only on rules with a transcript_line trigger.");
|
||||
}
|
||||
else if (MeetingWorkflowRuleSchema.IsProperty(property, MeetingWorkflowRuleSchema.PropertyAttendeeName) &&
|
||||
!HasAttendeeAddedTrigger(rule))
|
||||
{
|
||||
errors.Add($"{stepPath} can set 'attendee.name' only on rules with an attendee_added trigger.");
|
||||
}
|
||||
}
|
||||
|
||||
if (step.Value is { } value &&
|
||||
@@ -181,6 +193,17 @@ internal static class MeetingWorkflowRulesValidator
|
||||
return rule.On.Any(static trigger => trigger.TranscriptLine is not null);
|
||||
}
|
||||
|
||||
private static bool HasAttendeeAddedTrigger(MeetingWorkflowRule rule)
|
||||
{
|
||||
return rule.On.Any(static trigger => trigger.AttendeeAdded is not null);
|
||||
}
|
||||
|
||||
private static bool IsAttendeeNameSetPropertyStep(MeetingWorkflowStep step)
|
||||
{
|
||||
return MeetingWorkflowRuleSchema.IsStep(step.Uses, MeetingWorkflowRuleSchema.StepSetProperty) &&
|
||||
MeetingWorkflowRuleSchema.IsProperty(step.Property ?? step.Name, MeetingWorkflowRuleSchema.PropertyAttendeeName);
|
||||
}
|
||||
|
||||
private static int CountConfigured(params bool[] values)
|
||||
{
|
||||
return values.Count(static value => value);
|
||||
@@ -206,7 +229,8 @@ internal static class MeetingWorkflowRulesValidator
|
||||
new MeetingWorkflowSpeakerModel("Ada Lovelace"),
|
||||
new MeetingWorkflowTranscriptModel(
|
||||
"[00:00:01] Ada Lovelace: Validation line.",
|
||||
"Ada Lovelace"));
|
||||
"Ada Lovelace"),
|
||||
new MeetingWorkflowAttendeeModel("Ada Lovelace"));
|
||||
}
|
||||
|
||||
private static string FormatRazorValidationMessage(Exception exception)
|
||||
|
||||
Reference in New Issue
Block a user