Public Access
Add transcript line workflow rules
PR and Push Build/Test / build-and-test (push) Failing after 13m27s
PR and Push Build/Test / build-and-test (push) Failing after 13m27s
This commit is contained in:
@@ -14,11 +14,30 @@ public interface ITranscriptStore
|
||||
return CreateSessionAsync(cancellationToken);
|
||||
}
|
||||
|
||||
Task AppendAsync(TranscriptSession session, TranscriptionSegment segment, CancellationToken cancellationToken);
|
||||
Task AppendAsync(TranscriptSession session, TranscriptionSegment segment, CancellationToken cancellationToken)
|
||||
{
|
||||
return AppendLineAsync(
|
||||
session,
|
||||
TranscriptLineFormatter.Format(segment).Line,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken);
|
||||
|
||||
Task ReplaceAsync(
|
||||
TranscriptSession session,
|
||||
IReadOnlyList<TranscriptionSegment> replacementSegments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return ReplaceLinesAsync(
|
||||
session,
|
||||
replacementSegments.Select(segment => TranscriptLineFormatter.Format(segment).Line).ToList(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
Task ReplaceLinesAsync(
|
||||
TranscriptSession session,
|
||||
IReadOnlyList<string> replacementLines,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task UpdateMetadataAsync(
|
||||
@@ -29,3 +48,26 @@ public interface ITranscriptStore
|
||||
}
|
||||
|
||||
public sealed record TranscriptSession(string TranscriptPath);
|
||||
|
||||
public static class TranscriptLineFormatter
|
||||
{
|
||||
public static FormattedTranscriptLine Format(TranscriptionSegment segment)
|
||||
{
|
||||
var speaker = NormalizeSpeaker(segment.Speaker);
|
||||
return new FormattedTranscriptLine(
|
||||
$"[{segment.Start:hh\\:mm\\:ss}] {speaker}: {segment.Text}",
|
||||
speaker);
|
||||
}
|
||||
|
||||
public static string FormatSegmentLine(TranscriptionSegment segment)
|
||||
{
|
||||
return Format(segment).Line;
|
||||
}
|
||||
|
||||
public static string NormalizeSpeaker(string? speaker)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(speaker) ? "Unknown" : speaker;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record FormattedTranscriptLine(string Line, string Speaker);
|
||||
|
||||
@@ -433,7 +433,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
"System",
|
||||
$"Transcription profile changed to {targetProfile.Name}.");
|
||||
run.AddLiveSegment(marker);
|
||||
await transcriptStore.AppendAsync(run.Session, marker, cancellationToken);
|
||||
await AppendTranscriptSegmentAsync(run, marker, cancellationToken);
|
||||
|
||||
run.ReplacePipeline(pipeline, pipelineOptions, targetProfile.Options, targetProfile.Name);
|
||||
run.AddLiveTranscriptTask(Task.Run(
|
||||
@@ -598,10 +598,48 @@ public sealed class MeetingRecordingCoordinator
|
||||
|
||||
var relabeledSegment = run.Relabel(segment);
|
||||
run.AddLiveSegment(relabeledSegment);
|
||||
await transcriptStore.AppendAsync(run.Session, relabeledSegment, cancellationToken);
|
||||
await AppendTranscriptSegmentAsync(run, relabeledSegment, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AppendTranscriptSegmentAsync(
|
||||
RecordingRun run,
|
||||
TranscriptionSegment segment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var line = await TransformTranscriptLineAsync(run, segment, cancellationToken);
|
||||
await transcriptStore.AppendLineAsync(run.Session, line, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ReplaceTranscriptSegmentsAsync(
|
||||
RecordingRun run,
|
||||
IReadOnlyList<TranscriptionSegment> segments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var lines = new List<string>(segments.Count);
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
lines.Add(await TransformTranscriptLineAsync(run, segment, cancellationToken));
|
||||
}
|
||||
|
||||
await transcriptStore.ReplaceLinesAsync(run.Session, lines, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<string> TransformTranscriptLineAsync(
|
||||
RecordingRun run,
|
||||
TranscriptionSegment segment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var formatted = TranscriptLineFormatter.Format(segment);
|
||||
return await meetingWorkflowEngine.TransformTranscriptLineAsync(
|
||||
MeetingWorkflowEvent.TranscriptLine(
|
||||
run.Artifacts,
|
||||
formatted.Line,
|
||||
formatted.Speaker),
|
||||
run.Options,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task RunInactivitySafeguardAsync(RecordingRun run)
|
||||
{
|
||||
var safeguardOptions = run.Options.Recording.InactivitySafeguard;
|
||||
@@ -778,8 +816,8 @@ public sealed class MeetingRecordingCoordinator
|
||||
cancellationToken);
|
||||
if (run.AddSpeakerMappings(result.SpeakerMappings))
|
||||
{
|
||||
await transcriptStore.ReplaceAsync(
|
||||
run.Session,
|
||||
await ReplaceTranscriptSegmentsAsync(
|
||||
run,
|
||||
run.GetRelabeledLiveSegmentsSnapshot(),
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -995,7 +1033,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
liveSegments.Count,
|
||||
liveSamples.Count,
|
||||
liveMappings.Count);
|
||||
await transcriptStore.ReplaceAsync(run.Session, liveSegments, cancellationToken);
|
||||
await ReplaceTranscriptSegmentsAsync(run, liveSegments, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1040,7 +1078,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
finishedSpeakerResult.Segments.Count,
|
||||
samples.Count,
|
||||
knownSpeakerMappings.Count);
|
||||
await transcriptStore.ReplaceAsync(run.Session, finishedSpeakerResult.Segments, cancellationToken);
|
||||
await ReplaceTranscriptSegmentsAsync(run, finishedSpeakerResult.Segments, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<SpeakerIdentificationResult> IdentifyFinishedSpeakersForTranscriptAsync(
|
||||
@@ -1177,7 +1215,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
run.MeetingNotePath,
|
||||
run.Options,
|
||||
cancellationToken);
|
||||
await transcriptStore.ReplaceAsync(run.Session, result.Segments, cancellationToken);
|
||||
await ReplaceTranscriptSegmentsAsync(run, result.Segments, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
|
||||
@@ -35,14 +35,14 @@ public sealed class VaultTranscriptStore : ITranscriptStore
|
||||
return new TranscriptSession(path);
|
||||
}
|
||||
|
||||
public Task AppendAsync(TranscriptSession session, TranscriptionSegment segment, CancellationToken cancellationToken)
|
||||
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
||||
{
|
||||
return AppendToBodyAsync(session.TranscriptPath, FormatSegment(segment), cancellationToken);
|
||||
return AppendToBodyAsync(session.TranscriptPath, line + Environment.NewLine, cancellationToken);
|
||||
}
|
||||
|
||||
public Task ReplaceAsync(
|
||||
public Task ReplaceLinesAsync(
|
||||
TranscriptSession session,
|
||||
IReadOnlyList<TranscriptionSegment> replacementSegments,
|
||||
IReadOnlyList<string> replacementLines,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = File.Exists(session.TranscriptPath)
|
||||
@@ -52,7 +52,7 @@ public sealed class VaultTranscriptStore : ITranscriptStore
|
||||
var body = "# Meeting Transcript"
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ string.Concat(replacementSegments.Select(FormatSegment));
|
||||
+ string.Concat(replacementLines.Select(line => line + Environment.NewLine));
|
||||
var content = string.IsNullOrWhiteSpace(frontmatter)
|
||||
? body
|
||||
: "---" + Environment.NewLine + frontmatter + Environment.NewLine + "---" + Environment.NewLine + Environment.NewLine + body;
|
||||
@@ -80,12 +80,6 @@ public sealed class VaultTranscriptStore : ITranscriptStore
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static string FormatSegment(TranscriptionSegment segment)
|
||||
{
|
||||
var speaker = string.IsNullOrWhiteSpace(segment.Speaker) ? "Unknown" : segment.Speaker;
|
||||
return $"[{segment.Start:hh\\:mm\\:ss}] {speaker}: {segment.Text}{Environment.NewLine}";
|
||||
}
|
||||
|
||||
private static async Task AppendToBodyAsync(
|
||||
string path,
|
||||
string text,
|
||||
|
||||
@@ -12,6 +12,11 @@ public interface IMeetingWorkflowEngine
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<string> TransformTranscriptLineAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class NoopMeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
@@ -25,6 +30,14 @@ public sealed class NoopMeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> TransformTranscriptLineAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(workflowEvent.TranscriptLineText ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
@@ -39,7 +52,9 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
"event.type",
|
||||
"state.from",
|
||||
"state.to",
|
||||
"speaker.name"
|
||||
"speaker.name",
|
||||
"transcript.line",
|
||||
"transcript.speaker"
|
||||
];
|
||||
|
||||
private readonly IMeetingWorkflowRulesProvider rulesProvider;
|
||||
@@ -64,11 +79,35 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_ = await RunCoreAsync(workflowEvent, options, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string> TransformTranscriptLineAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (workflowEvent.Type != MeetingWorkflowEventType.TranscriptLine)
|
||||
{
|
||||
throw new ArgumentException("Workflow event must be a transcript line event.", nameof(workflowEvent));
|
||||
}
|
||||
|
||||
return await RunCoreAsync(workflowEvent, options, cancellationToken)
|
||||
?? workflowEvent.TranscriptLineText
|
||||
?? "";
|
||||
}
|
||||
|
||||
private async Task<string?> RunCoreAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rules = await rulesProvider.GetRulesAsync(options, cancellationToken);
|
||||
var context = new MeetingWorkflowExecutionContext(workflowEvent);
|
||||
if (rules.Count == 0)
|
||||
{
|
||||
return;
|
||||
return context.TranscriptLine;
|
||||
}
|
||||
|
||||
var meeting = await meetingNoteStore.ReadAsync(
|
||||
@@ -79,7 +118,7 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (!MatchesTrigger(rule, workflowEvent) ||
|
||||
!EvaluateConditions(rule.If, meeting, workflowEvent))
|
||||
!EvaluateConditions(rule.If, meeting, context))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -88,16 +127,16 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
"Applying meeting workflow rule {RuleName} for event {EventType}",
|
||||
rule.Name,
|
||||
workflowEvent.Type);
|
||||
var model = CreateTemplateModel(meeting, workflowEvent);
|
||||
var model = CreateTemplateModel(meeting, context);
|
||||
foreach (var step in rule.Steps)
|
||||
{
|
||||
noteChanged |= await ApplyStepAsync(
|
||||
step,
|
||||
meeting,
|
||||
workflowEvent,
|
||||
context,
|
||||
model,
|
||||
cancellationToken);
|
||||
model = CreateTemplateModel(meeting, workflowEvent);
|
||||
model = CreateTemplateModel(meeting, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +148,8 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
saved,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
return context.TranscriptLine;
|
||||
}
|
||||
|
||||
private static bool MatchesTrigger(
|
||||
@@ -151,41 +192,51 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (trigger.TranscriptLine is not null)
|
||||
{
|
||||
return workflowEvent.Type == MeetingWorkflowEventType.TranscriptLine &&
|
||||
(string.IsNullOrWhiteSpace(trigger.TranscriptLine.Speaker) ||
|
||||
string.Equals(
|
||||
trigger.TranscriptLine.Speaker.Trim(),
|
||||
workflowEvent.SpeakerName,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool EvaluateConditions(
|
||||
IReadOnlyList<MeetingWorkflowCondition> conditions,
|
||||
MeetingNote meeting,
|
||||
MeetingWorkflowEvent workflowEvent)
|
||||
MeetingWorkflowExecutionContext context)
|
||||
{
|
||||
return conditions.Count == 0 ||
|
||||
conditions.All(condition => EvaluateCondition(condition, meeting, workflowEvent));
|
||||
conditions.All(condition => EvaluateCondition(condition, meeting, context));
|
||||
}
|
||||
|
||||
private bool EvaluateCondition(
|
||||
MeetingWorkflowCondition condition,
|
||||
MeetingNote meeting,
|
||||
MeetingWorkflowEvent workflowEvent)
|
||||
MeetingWorkflowExecutionContext context)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(condition.Condition))
|
||||
{
|
||||
return EvaluateExpression(condition.Condition, meeting, workflowEvent);
|
||||
return EvaluateExpression(condition.Condition, meeting, context);
|
||||
}
|
||||
|
||||
if (condition.And is { Count: > 0 })
|
||||
{
|
||||
return condition.And.All(child => EvaluateCondition(child, meeting, workflowEvent));
|
||||
return condition.And.All(child => EvaluateCondition(child, meeting, context));
|
||||
}
|
||||
|
||||
if (condition.Or is { Count: > 0 })
|
||||
{
|
||||
return condition.Or.Any(child => EvaluateCondition(child, meeting, workflowEvent));
|
||||
return condition.Or.Any(child => EvaluateCondition(child, meeting, context));
|
||||
}
|
||||
|
||||
if (condition.Not is not null)
|
||||
{
|
||||
return !EvaluateCondition(condition.Not, meeting, workflowEvent);
|
||||
return !EvaluateCondition(condition.Not, meeting, context);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -194,10 +245,10 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
private bool EvaluateExpression(
|
||||
string expressionText,
|
||||
MeetingNote meeting,
|
||||
MeetingWorkflowEvent workflowEvent)
|
||||
MeetingWorkflowExecutionContext context)
|
||||
{
|
||||
var expression = new Expression(PrepareExpression(expressionText));
|
||||
foreach (var (name, value) in BuildParameters(meeting, workflowEvent))
|
||||
foreach (var (name, value) in BuildParameters(meeting, context))
|
||||
{
|
||||
expression.Parameters[name] = value;
|
||||
}
|
||||
@@ -219,7 +270,7 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
private async Task<bool> ApplyStepAsync(
|
||||
MeetingWorkflowStep step,
|
||||
MeetingNote meeting,
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingWorkflowExecutionContext context,
|
||||
MeetingWorkflowTemplateModel model,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -232,14 +283,17 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
return RemoveValue(meeting.Frontmatter.Attendees, value);
|
||||
case "add_project":
|
||||
return AddUnique(meeting.Frontmatter.Projects, value, static project => project.Trim());
|
||||
case "set_property":
|
||||
return SetProperty(meeting, step.Property ?? step.Name, value);
|
||||
case "add_context":
|
||||
await meetingArtifactStore.AppendAssistantContextAsync(
|
||||
workflowEvent.Artifacts,
|
||||
context.Event.Artifacts,
|
||||
value,
|
||||
cancellationToken);
|
||||
return false;
|
||||
case "set_property" when IsTranscriptLineProperty(step.Property ?? step.Name):
|
||||
SetTranscriptLine(context, value);
|
||||
return false;
|
||||
case "set_property":
|
||||
return SetProperty(meeting, step.Property ?? step.Name, value);
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown meeting workflow step '{step.Uses}'.");
|
||||
}
|
||||
@@ -272,6 +326,28 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
throw new InvalidOperationException($"Unsupported meeting workflow property '{property}'.");
|
||||
}
|
||||
|
||||
private static void SetTranscriptLine(
|
||||
MeetingWorkflowExecutionContext context,
|
||||
string value)
|
||||
{
|
||||
if (context.Event.Type != MeetingWorkflowEventType.TranscriptLine)
|
||||
{
|
||||
throw new InvalidOperationException("The transcript.line property can only be set during transcript_line events.");
|
||||
}
|
||||
|
||||
if (string.Equals(context.TranscriptLine, value, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.TranscriptLine = value;
|
||||
}
|
||||
|
||||
private static bool IsTranscriptLineProperty(string? property)
|
||||
{
|
||||
return string.Equals(property?.Trim(), "transcript.line", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool AddUnique(
|
||||
List<string> values,
|
||||
string value,
|
||||
@@ -338,8 +414,9 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
|
||||
private static IReadOnlyDictionary<string, object?> BuildParameters(
|
||||
MeetingNote meeting,
|
||||
MeetingWorkflowEvent workflowEvent)
|
||||
MeetingWorkflowExecutionContext context)
|
||||
{
|
||||
var workflowEvent = context.Event;
|
||||
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["meeting.attendees.count"] = meeting.Frontmatter.Attendees.Count,
|
||||
@@ -350,14 +427,17 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
["event.type"] = workflowEvent.Type.ToString(),
|
||||
["state.from"] = workflowEvent.FromState is { } from ? MeetingWorkflowStateNames.ToRuleName(from) : "",
|
||||
["state.to"] = workflowEvent.ToState is { } to ? MeetingWorkflowStateNames.ToRuleName(to) : "",
|
||||
["speaker.name"] = workflowEvent.SpeakerName
|
||||
["speaker.name"] = workflowEvent.SpeakerName,
|
||||
["transcript.line"] = context.TranscriptLine ?? "",
|
||||
["transcript.speaker"] = workflowEvent.SpeakerName ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
private static MeetingWorkflowTemplateModel CreateTemplateModel(
|
||||
MeetingNote meeting,
|
||||
MeetingWorkflowEvent workflowEvent)
|
||||
MeetingWorkflowExecutionContext context)
|
||||
{
|
||||
var workflowEvent = context.Event;
|
||||
return new MeetingWorkflowTemplateModel(
|
||||
new MeetingWorkflowMeetingModel(
|
||||
meeting.Frontmatter.Title,
|
||||
@@ -370,6 +450,22 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
workflowEvent.ToState is { } to ? MeetingWorkflowStateNames.ToRuleName(to) : null),
|
||||
string.IsNullOrWhiteSpace(workflowEvent.SpeakerName)
|
||||
? null
|
||||
: new MeetingWorkflowSpeakerModel(workflowEvent.SpeakerName));
|
||||
: new MeetingWorkflowSpeakerModel(workflowEvent.SpeakerName),
|
||||
new MeetingWorkflowTranscriptModel(
|
||||
context.TranscriptLine ?? "",
|
||||
workflowEvent.SpeakerName ?? ""));
|
||||
}
|
||||
|
||||
private sealed class MeetingWorkflowExecutionContext
|
||||
{
|
||||
public MeetingWorkflowExecutionContext(MeetingWorkflowEvent workflowEvent)
|
||||
{
|
||||
Event = workflowEvent;
|
||||
TranscriptLine = workflowEvent.TranscriptLineText;
|
||||
}
|
||||
|
||||
public MeetingWorkflowEvent Event { get; }
|
||||
|
||||
public string? TranscriptLine { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ public enum MeetingWorkflowEventType
|
||||
{
|
||||
Created,
|
||||
StateTransition,
|
||||
SpeakerIdentified
|
||||
SpeakerIdentified,
|
||||
TranscriptLine
|
||||
}
|
||||
|
||||
public sealed record MeetingWorkflowEvent(
|
||||
@@ -14,7 +15,8 @@ public sealed record MeetingWorkflowEvent(
|
||||
MeetingSessionArtifacts Artifacts,
|
||||
AssistantContextState? FromState = null,
|
||||
AssistantContextState? ToState = null,
|
||||
string? SpeakerName = null)
|
||||
string? SpeakerName = null,
|
||||
string? TranscriptLineText = null)
|
||||
{
|
||||
public static MeetingWorkflowEvent Created(MeetingSessionArtifacts artifacts)
|
||||
{
|
||||
@@ -35,4 +37,16 @@ public sealed record MeetingWorkflowEvent(
|
||||
{
|
||||
return new MeetingWorkflowEvent(MeetingWorkflowEventType.SpeakerIdentified, artifacts, SpeakerName: speakerName);
|
||||
}
|
||||
|
||||
public static MeetingWorkflowEvent TranscriptLine(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
string line,
|
||||
string speakerName)
|
||||
{
|
||||
return new MeetingWorkflowEvent(
|
||||
MeetingWorkflowEventType.TranscriptLine,
|
||||
artifacts,
|
||||
SpeakerName: speakerName,
|
||||
TranscriptLineText: line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ public sealed class MeetingWorkflowTrigger
|
||||
|
||||
[YamlMember(Alias = "speaker_identified")]
|
||||
public MeetingWorkflowSpeakerIdentifiedTrigger? SpeakerIdentified { get; set; }
|
||||
|
||||
[YamlMember(Alias = "transcript_line")]
|
||||
public MeetingWorkflowTranscriptLineTrigger? TranscriptLine { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MeetingWorkflowStateTransitionTrigger
|
||||
@@ -51,6 +54,12 @@ public sealed class MeetingWorkflowSpeakerIdentifiedTrigger
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MeetingWorkflowTranscriptLineTrigger
|
||||
{
|
||||
[YamlMember(Alias = "speaker")]
|
||||
public string? Speaker { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MeetingWorkflowCondition
|
||||
{
|
||||
[YamlMember(Alias = "condition")]
|
||||
@@ -84,7 +93,8 @@ public sealed class MeetingWorkflowStep
|
||||
public sealed record MeetingWorkflowTemplateModel(
|
||||
MeetingWorkflowMeetingModel Meeting,
|
||||
MeetingWorkflowEventModel Event,
|
||||
MeetingWorkflowSpeakerModel? Speaker);
|
||||
MeetingWorkflowSpeakerModel? Speaker,
|
||||
MeetingWorkflowTranscriptModel? Transcript);
|
||||
|
||||
public sealed record MeetingWorkflowMeetingModel(
|
||||
string Title,
|
||||
@@ -99,6 +109,8 @@ public sealed record MeetingWorkflowEventModel(
|
||||
|
||||
public sealed record MeetingWorkflowSpeakerModel(string Name);
|
||||
|
||||
public sealed record MeetingWorkflowTranscriptModel(string Line, string Speaker);
|
||||
|
||||
internal static class MeetingWorkflowStateNames
|
||||
{
|
||||
public static string ToRuleName(AssistantContextState state)
|
||||
|
||||
@@ -4,11 +4,11 @@ namespace MeetingAssistant.Workflow;
|
||||
|
||||
internal static class MeetingWorkflowRulesValidator
|
||||
{
|
||||
private static readonly string[] SupportedTriggerKeys = ["created", "state_transition", "speaker_identified"];
|
||||
private static readonly string[] SupportedTriggerKeys = ["created", "state_transition", "speaker_identified", "transcript_line"];
|
||||
private static readonly string[] SupportedConditionKeys = ["condition", "and", "or", "not"];
|
||||
private static readonly string[] SupportedStepUses =
|
||||
["add_attendee", "remove_attendee", "add_project", "set_property", "add_context"];
|
||||
private static readonly string[] SupportedSetPropertyNames = ["title", "meeting.title"];
|
||||
private static readonly string[] SupportedSetPropertyNames = ["title", "meeting.title", "transcript.line"];
|
||||
|
||||
public static async Task<IReadOnlyList<string>> ValidateAsync(MeetingWorkflowRulesFile rulesFile)
|
||||
{
|
||||
@@ -43,6 +43,7 @@ internal static class MeetingWorkflowRulesValidator
|
||||
{
|
||||
await ValidateStepAsync(
|
||||
rule.Steps[stepIndex],
|
||||
rule,
|
||||
ruleName,
|
||||
stepIndex,
|
||||
errors,
|
||||
@@ -63,7 +64,8 @@ internal static class MeetingWorkflowRulesValidator
|
||||
var configuredCount = CountConfigured(
|
||||
trigger.Created is not null,
|
||||
trigger.StateTransition is not null,
|
||||
trigger.SpeakerIdentified is not null);
|
||||
trigger.SpeakerIdentified is not null,
|
||||
trigger.TranscriptLine is not null);
|
||||
if (configuredCount == 0)
|
||||
{
|
||||
errors.Add($"{ruleName} on[{triggerIndex + 1}] must contain one supported trigger: {JoinAllowed(SupportedTriggerKeys)}.");
|
||||
@@ -120,6 +122,7 @@ internal static class MeetingWorkflowRulesValidator
|
||||
|
||||
private static async Task ValidateStepAsync(
|
||||
MeetingWorkflowStep step,
|
||||
MeetingWorkflowRule rule,
|
||||
string ruleName,
|
||||
int stepIndex,
|
||||
List<string> errors,
|
||||
@@ -151,6 +154,11 @@ internal static class MeetingWorkflowRulesValidator
|
||||
{
|
||||
errors.Add($"{stepPath} uses unsupported property '{property}'. Supported properties: {JoinAllowed(SupportedSetPropertyNames)}.");
|
||||
}
|
||||
else if (string.Equals(property.Trim(), "transcript.line", StringComparison.OrdinalIgnoreCase) &&
|
||||
!HasTranscriptLineTrigger(rule))
|
||||
{
|
||||
errors.Add($"{stepPath} can set 'transcript.line' only on rules with a transcript_line trigger.");
|
||||
}
|
||||
}
|
||||
|
||||
if (step.Value is { } value &&
|
||||
@@ -174,6 +182,11 @@ internal static class MeetingWorkflowRulesValidator
|
||||
: $"rule '{rule.Name}'";
|
||||
}
|
||||
|
||||
private static bool HasTranscriptLineTrigger(MeetingWorkflowRule rule)
|
||||
{
|
||||
return rule.On.Any(static trigger => trigger.TranscriptLine is not null);
|
||||
}
|
||||
|
||||
private static int CountConfigured(params bool[] values)
|
||||
{
|
||||
return values.Count(static value => value);
|
||||
@@ -196,7 +209,10 @@ internal static class MeetingWorkflowRulesValidator
|
||||
MeetingWorkflowEventType.StateTransition.ToString(),
|
||||
MeetingWorkflowStateNames.ToRuleName(AssistantContextState.CollectingMetadata),
|
||||
MeetingWorkflowStateNames.ToRuleName(AssistantContextState.Transcribing)),
|
||||
new MeetingWorkflowSpeakerModel("Ada Lovelace"));
|
||||
new MeetingWorkflowSpeakerModel("Ada Lovelace"),
|
||||
new MeetingWorkflowTranscriptModel(
|
||||
"[00:00:01] Ada Lovelace: Validation line.",
|
||||
"Ada Lovelace"));
|
||||
}
|
||||
|
||||
private static string FormatRazorValidationMessage(Exception exception)
|
||||
|
||||
Reference in New Issue
Block a user