Public Access
Fix workflow rule validation logging
PR and Push Build/Test / build-and-test (push) Failing after 9m19s
PR and Push Build/Test / build-and-test (push) Failing after 9m19s
This commit is contained in:
@@ -53,8 +53,8 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
"state.from",
|
||||
"state.to",
|
||||
"speaker.name",
|
||||
"transcript.line",
|
||||
"transcript.speaker"
|
||||
MeetingWorkflowRuleSchema.PropertyTranscriptLine,
|
||||
MeetingWorkflowRuleSchema.ParameterTranscriptSpeaker
|
||||
];
|
||||
|
||||
private readonly IMeetingWorkflowRulesProvider rulesProvider;
|
||||
@@ -277,22 +277,22 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
var value = await RenderValueAsync(step.Value ?? "", model);
|
||||
switch (step.Uses.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "add_attendee":
|
||||
case MeetingWorkflowRuleSchema.StepAddAttendee:
|
||||
return AddUnique(meeting.Frontmatter.Attendees, value, MeetingAttendeeNames.NormalizeDisplayName);
|
||||
case "remove_attendee":
|
||||
case MeetingWorkflowRuleSchema.StepRemoveAttendee:
|
||||
return RemoveValue(meeting.Frontmatter.Attendees, value);
|
||||
case "add_project":
|
||||
case MeetingWorkflowRuleSchema.StepAddProject:
|
||||
return AddUnique(meeting.Frontmatter.Projects, value, static project => project.Trim());
|
||||
case "add_context":
|
||||
case MeetingWorkflowRuleSchema.StepAddContext:
|
||||
await meetingArtifactStore.AppendAssistantContextAsync(
|
||||
context.Event.Artifacts,
|
||||
value,
|
||||
cancellationToken);
|
||||
return false;
|
||||
case "set_property" when IsTranscriptLineProperty(step.Property ?? step.Name):
|
||||
case MeetingWorkflowRuleSchema.StepSetProperty when IsTranscriptLineProperty(step.Property ?? step.Name):
|
||||
SetTranscriptLine(context, value);
|
||||
return false;
|
||||
case "set_property":
|
||||
case MeetingWorkflowRuleSchema.StepSetProperty:
|
||||
return SetProperty(meeting, step.Property ?? step.Name, value);
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown meeting workflow step '{step.Uses}'.");
|
||||
@@ -312,7 +312,7 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
string value)
|
||||
{
|
||||
var normalized = property?.Trim().ToLowerInvariant();
|
||||
if (normalized is "title" or "meeting.title")
|
||||
if (normalized is MeetingWorkflowRuleSchema.PropertyTitle or MeetingWorkflowRuleSchema.PropertyMeetingTitle)
|
||||
{
|
||||
if (string.Equals(meeting.Frontmatter.Title, value, StringComparison.Ordinal))
|
||||
{
|
||||
@@ -345,7 +345,7 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
|
||||
private static bool IsTranscriptLineProperty(string? property)
|
||||
{
|
||||
return string.Equals(property?.Trim(), "transcript.line", StringComparison.OrdinalIgnoreCase);
|
||||
return MeetingWorkflowRuleSchema.IsProperty(property, MeetingWorkflowRuleSchema.PropertyTranscriptLine);
|
||||
}
|
||||
|
||||
private static bool AddUnique(
|
||||
@@ -428,8 +428,8 @@ public sealed class MeetingWorkflowEngine : IMeetingWorkflowEngine
|
||||
["state.from"] = workflowEvent.FromState is { } from ? MeetingWorkflowStateNames.ToRuleName(from) : "",
|
||||
["state.to"] = workflowEvent.ToState is { } to ? MeetingWorkflowStateNames.ToRuleName(to) : "",
|
||||
["speaker.name"] = workflowEvent.SpeakerName,
|
||||
["transcript.line"] = context.TranscriptLine ?? "",
|
||||
["transcript.speaker"] = workflowEvent.SpeakerName ?? ""
|
||||
[MeetingWorkflowRuleSchema.PropertyTranscriptLine] = context.TranscriptLine ?? "",
|
||||
[MeetingWorkflowRuleSchema.ParameterTranscriptSpeaker] = workflowEvent.SpeakerName ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using RazorLight;
|
||||
|
||||
namespace MeetingAssistant.Workflow;
|
||||
|
||||
internal static class MeetingWorkflowRazorEngineFactory
|
||||
{
|
||||
public static RazorLightEngine Create()
|
||||
{
|
||||
return new RazorLightEngineBuilder()
|
||||
.UseNoProject()
|
||||
.SetOperatingAssembly(typeof(RazorLightEngine).Assembly)
|
||||
.AddMetadataReferences(CreateMetadataReferences())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static MetadataReference[] CreateMetadataReferences()
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location))
|
||||
.Select(assembly => assembly.Location)
|
||||
.Where(File.Exists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(path => MetadataReference.CreateFromFile(path))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace MeetingAssistant.Workflow;
|
||||
|
||||
internal static class MeetingWorkflowRuleSchema
|
||||
{
|
||||
public const string StepAddAttendee = "add_attendee";
|
||||
public const string StepRemoveAttendee = "remove_attendee";
|
||||
public const string StepAddProject = "add_project";
|
||||
public const string StepSetProperty = "set_property";
|
||||
public const string StepAddContext = "add_context";
|
||||
|
||||
public const string PropertyTitle = "title";
|
||||
public const string PropertyMeetingTitle = "meeting.title";
|
||||
public const string PropertyTranscriptLine = "transcript.line";
|
||||
public const string ParameterTranscriptSpeaker = "transcript.speaker";
|
||||
|
||||
public static readonly string[] SupportedTriggerKeys =
|
||||
[
|
||||
"created",
|
||||
"state_transition",
|
||||
"speaker_identified",
|
||||
"transcript_line"
|
||||
];
|
||||
|
||||
public static readonly string[] SupportedConditionKeys =
|
||||
[
|
||||
"condition",
|
||||
"and",
|
||||
"or",
|
||||
"not"
|
||||
];
|
||||
|
||||
public static readonly string[] SupportedStepUses =
|
||||
[
|
||||
StepAddAttendee,
|
||||
StepRemoveAttendee,
|
||||
StepAddProject,
|
||||
StepSetProperty,
|
||||
StepAddContext
|
||||
];
|
||||
|
||||
public static readonly string[] SupportedSetPropertyNames =
|
||||
[
|
||||
PropertyTitle,
|
||||
PropertyMeetingTitle,
|
||||
PropertyTranscriptLine
|
||||
];
|
||||
|
||||
public static bool IsStep(string? value, string step)
|
||||
{
|
||||
return string.Equals(value?.Trim(), step, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsProperty(string? value, string property)
|
||||
{
|
||||
return string.Equals(value?.Trim(), property, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,6 @@ namespace MeetingAssistant.Workflow;
|
||||
|
||||
internal static class MeetingWorkflowRulesValidator
|
||||
{
|
||||
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", "transcript.line"];
|
||||
|
||||
public static async Task<IReadOnlyList<string>> ValidateAsync(MeetingWorkflowRulesFile rulesFile)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
@@ -21,7 +15,7 @@ internal static class MeetingWorkflowRulesValidator
|
||||
var ruleName = GetRuleName(rule, ruleIndex);
|
||||
if (rule.On.Count == 0)
|
||||
{
|
||||
errors.Add($"{ruleName} must define at least one trigger in 'on'. Supported triggers: {JoinAllowed(SupportedTriggerKeys)}.");
|
||||
errors.Add($"{ruleName} must define at least one trigger in 'on'. Supported triggers: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedTriggerKeys)}.");
|
||||
}
|
||||
|
||||
for (var triggerIndex = 0; triggerIndex < rule.On.Count; triggerIndex++)
|
||||
@@ -36,7 +30,7 @@ internal static class MeetingWorkflowRulesValidator
|
||||
|
||||
if (rule.Steps.Count == 0)
|
||||
{
|
||||
errors.Add($"{ruleName} must define at least one step. Supported steps: {JoinAllowed(SupportedStepUses)}.");
|
||||
errors.Add($"{ruleName} must define at least one step. Supported steps: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedStepUses)}.");
|
||||
}
|
||||
|
||||
for (var stepIndex = 0; stepIndex < rule.Steps.Count; stepIndex++)
|
||||
@@ -68,7 +62,7 @@ internal static class MeetingWorkflowRulesValidator
|
||||
trigger.TranscriptLine is not null);
|
||||
if (configuredCount == 0)
|
||||
{
|
||||
errors.Add($"{ruleName} on[{triggerIndex + 1}] must contain one supported trigger: {JoinAllowed(SupportedTriggerKeys)}.");
|
||||
errors.Add($"{ruleName} on[{triggerIndex + 1}] must contain one supported trigger: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedTriggerKeys)}.");
|
||||
}
|
||||
else if (configuredCount > 1)
|
||||
{
|
||||
@@ -89,7 +83,7 @@ internal static class MeetingWorkflowRulesValidator
|
||||
condition.Not is not null);
|
||||
if (configuredCount == 0)
|
||||
{
|
||||
errors.Add($"{ruleName} {path} must contain one supported condition key: {JoinAllowed(SupportedConditionKeys)}.");
|
||||
errors.Add($"{ruleName} {path} must contain one supported condition key: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedConditionKeys)}.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,28 +127,28 @@ internal static class MeetingWorkflowRulesValidator
|
||||
var uses = step.Uses.Trim();
|
||||
if (string.IsNullOrWhiteSpace(uses))
|
||||
{
|
||||
errors.Add($"{stepPath} must define 'uses'. Supported steps: {JoinAllowed(SupportedStepUses)}.");
|
||||
errors.Add($"{stepPath} must define 'uses'. Supported steps: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedStepUses)}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SupportedStepUses.Contains(uses, StringComparer.OrdinalIgnoreCase))
|
||||
if (!MeetingWorkflowRuleSchema.SupportedStepUses.Contains(uses, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
errors.Add($"{stepPath} uses unsupported step '{uses}'. Supported steps: {JoinAllowed(SupportedStepUses)}.");
|
||||
errors.Add($"{stepPath} uses unsupported step '{uses}'. Supported steps: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedStepUses)}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(uses, "set_property", StringComparison.OrdinalIgnoreCase))
|
||||
if (MeetingWorkflowRuleSchema.IsStep(uses, MeetingWorkflowRuleSchema.StepSetProperty))
|
||||
{
|
||||
var property = step.Property ?? step.Name;
|
||||
if (string.IsNullOrWhiteSpace(property))
|
||||
{
|
||||
errors.Add($"{stepPath} must define 'property' or 'name' for set_property. Supported properties: {JoinAllowed(SupportedSetPropertyNames)}.");
|
||||
errors.Add($"{stepPath} must define 'property' or 'name' for set_property. Supported properties: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedSetPropertyNames)}.");
|
||||
}
|
||||
else if (!SupportedSetPropertyNames.Contains(property.Trim(), StringComparer.OrdinalIgnoreCase))
|
||||
else if (!MeetingWorkflowRuleSchema.SupportedSetPropertyNames.Contains(property.Trim(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
errors.Add($"{stepPath} uses unsupported property '{property}'. Supported properties: {JoinAllowed(SupportedSetPropertyNames)}.");
|
||||
errors.Add($"{stepPath} uses unsupported property '{property}'. Supported properties: {JoinAllowed(MeetingWorkflowRuleSchema.SupportedSetPropertyNames)}.");
|
||||
}
|
||||
else if (string.Equals(property.Trim(), "transcript.line", StringComparison.OrdinalIgnoreCase) &&
|
||||
else if (MeetingWorkflowRuleSchema.IsProperty(property, MeetingWorkflowRuleSchema.PropertyTranscriptLine) &&
|
||||
!HasTranscriptLineTrigger(rule))
|
||||
{
|
||||
errors.Add($"{stepPath} can set 'transcript.line' only on rules with a transcript_line trigger.");
|
||||
|
||||
@@ -9,9 +9,7 @@ internal sealed class MeetingWorkflowTemplateRenderer
|
||||
@"(?<![A-Za-z0-9.!#$%&'*+/=?^_`{|}~-])(?<local>[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+)@(?<domain>(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63})(?![A-Za-z0-9-])",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
|
||||
private readonly RazorLightEngine razorEngine = new RazorLightEngineBuilder()
|
||||
.UseNoProject()
|
||||
.Build();
|
||||
private readonly RazorLightEngine razorEngine = MeetingWorkflowRazorEngineFactory.Create();
|
||||
|
||||
public async Task<string> RenderAsync(
|
||||
string template,
|
||||
|
||||
@@ -76,7 +76,8 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
|
||||
launchProfiles,
|
||||
identityMergeService,
|
||||
asrDiagnosticService,
|
||||
configuration);
|
||||
configuration,
|
||||
logger: logger);
|
||||
var messages = conversation
|
||||
.Select(ToChatMessage)
|
||||
.Append(new ChatMessage(ChatRole.User, userMessage.Trim()))
|
||||
|
||||
@@ -9,6 +9,7 @@ using MeetingAssistant.Speakers;
|
||||
using MeetingAssistant.Transcription;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
@@ -47,6 +48,7 @@ public sealed class WorkflowRulesEditorTools
|
||||
private readonly ISpeakerIdentityMergeService? identityMergeService;
|
||||
private readonly AsrDiagnosticService? asrDiagnosticService;
|
||||
private readonly IConfiguration? configuration;
|
||||
private readonly ILogger? logger;
|
||||
|
||||
public WorkflowRulesEditorTools(
|
||||
MeetingAssistantOptions options,
|
||||
@@ -62,7 +64,8 @@ public sealed class WorkflowRulesEditorTools
|
||||
string? configDocsPath = null,
|
||||
string? logDirectory = null,
|
||||
string? specRootPath = null,
|
||||
string? projectAgentsTemplatePath = null)
|
||||
string? projectAgentsTemplatePath = null,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
this.options = options;
|
||||
rulesPath = WorkflowRulesPathResolver.Resolve(options.Automation.RulesPath);
|
||||
@@ -85,6 +88,7 @@ public sealed class WorkflowRulesEditorTools
|
||||
this.identityMergeService = identityMergeService;
|
||||
this.asrDiagnosticService = asrDiagnosticService;
|
||||
this.configuration = configuration;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<string> ReadRules(int? from = null, int? to = null, int? tail = null)
|
||||
@@ -106,12 +110,12 @@ public sealed class WorkflowRulesEditorTools
|
||||
{
|
||||
if (rulesPath is null)
|
||||
{
|
||||
return "Refused: workflow rules file is not configured.";
|
||||
return LogRefusal(nameof(WriteRules), "Refused: workflow rules file is not configured.");
|
||||
}
|
||||
|
||||
if (yaml is null)
|
||||
{
|
||||
return "Refused: yaml must not be null.";
|
||||
return LogRefusal(nameof(WriteRules), "Refused: yaml must not be null.");
|
||||
}
|
||||
|
||||
var existing = File.Exists(rulesPath)
|
||||
@@ -123,7 +127,7 @@ public sealed class WorkflowRulesEditorTools
|
||||
var validation = await ValidateYamlAsync(updated);
|
||||
if (validation is not null)
|
||||
{
|
||||
return validation;
|
||||
return LogRefusal(nameof(WriteRules), validation);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(rulesPath)!);
|
||||
@@ -163,13 +167,13 @@ public sealed class WorkflowRulesEditorTools
|
||||
{
|
||||
if (json is null)
|
||||
{
|
||||
return "Refused: json must not be null.";
|
||||
return LogRefusal(nameof(WriteConfig), "Refused: json must not be null.");
|
||||
}
|
||||
|
||||
var validation = ValidateJson(json);
|
||||
if (validation is not null)
|
||||
{
|
||||
return validation;
|
||||
return LogRefusal(nameof(WriteConfig), validation);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
|
||||
@@ -177,6 +181,15 @@ public sealed class WorkflowRulesEditorTools
|
||||
return configPath;
|
||||
}
|
||||
|
||||
private string LogRefusal(string toolName, string refusal)
|
||||
{
|
||||
logger?.LogWarning(
|
||||
"Workflow rules editor tool {ToolName} refused request: {Refusal}",
|
||||
toolName,
|
||||
refusal);
|
||||
return refusal;
|
||||
}
|
||||
|
||||
public async Task<string> ReadConfigDocs(int? from = null, int? to = null, int? tail = null)
|
||||
{
|
||||
if (!File.Exists(configDocsPath))
|
||||
|
||||
Reference in New Issue
Block a user