From 351ed2eb504f83eb5a1d91dc4985e5747f9e90d7 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Wed, 3 Jun 2026 17:33:12 +0200 Subject: [PATCH] Fix workflow rule validation logging --- .../MeetingWorkflowTestRules.cs | 1 - .../WorkflowRulesEditorTests.cs | 174 ++++++++++++------ .../Workflow/MeetingWorkflowEngine.cs | 24 +-- .../MeetingWorkflowRazorEngineFactory.cs | 27 +++ .../Workflow/MeetingWorkflowRuleSchema.cs | 57 ++++++ .../Workflow/MeetingWorkflowRulesValidator.cs | 30 ++- .../MeetingWorkflowTemplateRenderer.cs | 4 +- .../WorkflowRulesEditorChatPipeline.cs | 3 +- .../Workflow/WorkflowRulesEditorTools.cs | 25 ++- 9 files changed, 243 insertions(+), 102 deletions(-) create mode 100644 MeetingAssistant/Workflow/MeetingWorkflowRazorEngineFactory.cs create mode 100644 MeetingAssistant/Workflow/MeetingWorkflowRuleSchema.cs diff --git a/MeetingAssistant.Tests/MeetingWorkflowTestRules.cs b/MeetingAssistant.Tests/MeetingWorkflowTestRules.cs index e6f284f..419ecbf 100644 --- a/MeetingAssistant.Tests/MeetingWorkflowTestRules.cs +++ b/MeetingAssistant.Tests/MeetingWorkflowTestRules.cs @@ -10,7 +10,6 @@ internal static class MeetingWorkflowTestRules - transcript_line: {} if: - condition: contains(transcript.line, '*****') - - condition: transcript.speaker = 'Guest-1' steps: - uses: set_property property: transcript.line diff --git a/MeetingAssistant.Tests/WorkflowRulesEditorTests.cs b/MeetingAssistant.Tests/WorkflowRulesEditorTests.cs index a806377..0ac8722 100644 --- a/MeetingAssistant.Tests/WorkflowRulesEditorTests.cs +++ b/MeetingAssistant.Tests/WorkflowRulesEditorTests.cs @@ -538,34 +538,20 @@ public sealed class WorkflowRulesEditorTests [Fact] public async Task RulesEditorToolsRefuseInvalidYamlWithoutOverwritingFile() { - var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var rulesPath = Path.Combine(root, "rules.yaml"); - await File.WriteAllTextAsync(rulesPath, "rules: []"); - var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions - { - Automation = new AutomationOptions { RulesPath = rulesPath } - }); + var fixture = await CreateRulesEditorFixtureAsync(); - var result = await tools.WriteRules("rules:\n- name: [", replace_file: true); + var result = await fixture.Tools.WriteRules("rules:\n- name: [", replace_file: true); Assert.StartsWith("Refused: workflow rules YAML is invalid.", result); - Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath)); + Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath)); } [Fact] public async Task RulesEditorToolsRefuseWorkflowRulesThatWouldFailAtRuntime() { - var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var rulesPath = Path.Combine(root, "rules.yaml"); - await File.WriteAllTextAsync(rulesPath, "rules: []"); - var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions - { - Automation = new AutomationOptions { RulesPath = rulesPath } - }); + var fixture = await CreateRulesEditorFixtureAsync(); - var result = await tools.WriteRules(""" + var result = await fixture.Tools.WriteRules(""" rules: - name: broken-step on: @@ -579,22 +565,37 @@ public sealed class WorkflowRulesEditorTests Assert.Contains("broken-step", result); Assert.Contains("send_email", result); Assert.Contains("add_attendee", result); - Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath)); + Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath)); + } + + [Fact] + public async Task RulesEditorToolsLogWorkflowRuleValidationFailures() + { + var logger = new CapturingLogger(); + var fixture = await CreateRulesEditorFixtureAsync(logger); + + var result = await fixture.Tools.WriteRules(""" + rules: + - name: broken-step + on: + - created: {} + steps: + - uses: send_email + value: Manuel + """, replace_file: true); + + Assert.StartsWith("Refused: workflow rules are invalid.", result); + Assert.Contains(logger.Messages, message => + message.Contains(nameof(WorkflowRulesEditorTools.WriteRules), StringComparison.Ordinal) && + message.Contains("send_email", StringComparison.Ordinal)); } [Fact] public async Task RulesEditorToolsExplainUnsupportedWorkflowProperties() { - var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var rulesPath = Path.Combine(root, "rules.yaml"); - await File.WriteAllTextAsync(rulesPath, "rules: []"); - var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions - { - Automation = new AutomationOptions { RulesPath = rulesPath } - }); + var fixture = await CreateRulesEditorFixtureAsync(); - var result = await tools.WriteRules(""" + var result = await fixture.Tools.WriteRules(""" rules: - name: broken-property on: @@ -609,22 +610,15 @@ public sealed class WorkflowRulesEditorTests Assert.Contains("broken-property", result); Assert.Contains("meeting.status", result); Assert.Contains("title", result); - Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath)); + Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath)); } [Fact] public async Task RulesEditorToolsRefuseTranscriptLinePropertyOutsideTranscriptLineTrigger() { - var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var rulesPath = Path.Combine(root, "rules.yaml"); - await File.WriteAllTextAsync(rulesPath, "rules: []"); - var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions - { - Automation = new AutomationOptions { RulesPath = rulesPath } - }); + var fixture = await CreateRulesEditorFixtureAsync(); - var result = await tools.WriteRules(""" + var result = await fixture.Tools.WriteRules(""" rules: - name: broken-transcript-property on: @@ -639,22 +633,37 @@ public sealed class WorkflowRulesEditorTests Assert.Contains("broken-transcript-property", result); Assert.Contains("transcript.line", result); Assert.Contains("transcript_line", result); - Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath)); + Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath)); + } + + [Fact] + public async Task RulesEditorToolsAcceptAndParseMaskedProfanityRedactionRule() + { + var fixture = await CreateRulesEditorFixtureAsync(); + + var result = await fixture.Tools.WriteRules(MeetingWorkflowTestRules.MaskedProfanityRedactionYaml, replace_file: true); + + Assert.Equal(fixture.RulesPath, result); + var provider = new FileMeetingWorkflowRulesProvider( + NullLogger.Instance); + var rule = Assert.Single(await provider.GetRulesAsync(fixture.Options, CancellationToken.None)); + Assert.Equal("redact-masked-profanity", rule.Name); + Assert.NotNull(Assert.Single(rule.On).TranscriptLine); + Assert.Equal("contains(transcript.line, '*****')", Assert.Single(rule.If).Condition); + var step = Assert.Single(rule.Steps); + Assert.Equal("set_property", step.Uses); + Assert.Equal("transcript.line", step.Property); + Assert.Equal( + """@Model.Transcript.Line.Replace("*****", "[redacted]")""", + step.Value); } [Fact] public async Task RulesEditorToolsRefuseMalformedRazorStepValues() { - var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var rulesPath = Path.Combine(root, "rules.yaml"); - await File.WriteAllTextAsync(rulesPath, "rules: []"); - var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions - { - Automation = new AutomationOptions { RulesPath = rulesPath } - }); + var fixture = await CreateRulesEditorFixtureAsync(); - var result = await tools.WriteRules(""" + var result = await fixture.Tools.WriteRules(""" rules: - name: broken-razor on: @@ -668,21 +677,15 @@ public sealed class WorkflowRulesEditorTests Assert.Contains("broken-razor", result); Assert.Contains("Razor", result); Assert.Contains("ThisIsNotAModelVar", result); - Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath)); + Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath)); } [Fact] public async Task RulesEditorToolsAllowValidRazorAndEmailStepValues() { - var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var rulesPath = Path.Combine(root, "rules.yaml"); - var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions - { - Automation = new AutomationOptions { RulesPath = rulesPath } - }); + var fixture = await CreateRulesEditorFixtureAsync(initialContent: ""); - var result = await tools.WriteRules(""" + var result = await fixture.Tools.WriteRules(""" rules: - name: valid-razor on: @@ -692,9 +695,9 @@ public sealed class WorkflowRulesEditorTests value: "Speaker @Model.Speaker.Name can contact Support@example.com" """, replace_file: true); - Assert.Equal(rulesPath, result); - Assert.Contains("@Model.Speaker.Name", await File.ReadAllTextAsync(rulesPath)); - Assert.Contains("Support@example.com", await File.ReadAllTextAsync(rulesPath)); + Assert.Equal(fixture.RulesPath, result); + Assert.Contains("@Model.Speaker.Name", await File.ReadAllTextAsync(fixture.RulesPath)); + Assert.Contains("Support@example.com", await File.ReadAllTextAsync(fixture.RulesPath)); } [Fact] @@ -1159,6 +1162,55 @@ public sealed class WorkflowRulesEditorTests } } + private sealed class CapturingLogger : ILogger + { + public List Messages { get; } = []; + + public IDisposable? BeginScope(TState state) + where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + Messages.Add(formatter(state, exception)); + } + } + + private static async Task CreateRulesEditorFixtureAsync( + ILogger? logger = null, + string initialContent = "rules: []") + { + var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + var rulesPath = Path.Combine(root, "rules.yaml"); + await File.WriteAllTextAsync(rulesPath, initialContent); + var options = new MeetingAssistantOptions + { + Automation = new AutomationOptions { RulesPath = rulesPath } + }; + return new RulesEditorFixture( + rulesPath, + options, + new WorkflowRulesEditorTools(options, logger: logger)); + } + + private sealed record RulesEditorFixture( + string RulesPath, + MeetingAssistantOptions Options, + WorkflowRulesEditorTools Tools); + private sealed class WorkflowRulesEditorIdentityFixture : IAsyncDisposable { private readonly string tempDirectory; diff --git a/MeetingAssistant/Workflow/MeetingWorkflowEngine.cs b/MeetingAssistant/Workflow/MeetingWorkflowEngine.cs index 56da3b5..bc3574f 100644 --- a/MeetingAssistant/Workflow/MeetingWorkflowEngine.cs +++ b/MeetingAssistant/Workflow/MeetingWorkflowEngine.cs @@ -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 ?? "" }; } diff --git a/MeetingAssistant/Workflow/MeetingWorkflowRazorEngineFactory.cs b/MeetingAssistant/Workflow/MeetingWorkflowRazorEngineFactory.cs new file mode 100644 index 0000000..67e377d --- /dev/null +++ b/MeetingAssistant/Workflow/MeetingWorkflowRazorEngineFactory.cs @@ -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(); + } +} diff --git a/MeetingAssistant/Workflow/MeetingWorkflowRuleSchema.cs b/MeetingAssistant/Workflow/MeetingWorkflowRuleSchema.cs new file mode 100644 index 0000000..2fdc0ec --- /dev/null +++ b/MeetingAssistant/Workflow/MeetingWorkflowRuleSchema.cs @@ -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); + } +} diff --git a/MeetingAssistant/Workflow/MeetingWorkflowRulesValidator.cs b/MeetingAssistant/Workflow/MeetingWorkflowRulesValidator.cs index 71640ff..d02474a 100644 --- a/MeetingAssistant/Workflow/MeetingWorkflowRulesValidator.cs +++ b/MeetingAssistant/Workflow/MeetingWorkflowRulesValidator.cs @@ -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> ValidateAsync(MeetingWorkflowRulesFile rulesFile) { var errors = new List(); @@ -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."); diff --git a/MeetingAssistant/Workflow/MeetingWorkflowTemplateRenderer.cs b/MeetingAssistant/Workflow/MeetingWorkflowTemplateRenderer.cs index 98c4c1b..249d5b5 100644 --- a/MeetingAssistant/Workflow/MeetingWorkflowTemplateRenderer.cs +++ b/MeetingAssistant/Workflow/MeetingWorkflowTemplateRenderer.cs @@ -9,9 +9,7 @@ internal sealed class MeetingWorkflowTemplateRenderer @"(?[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+)@(?(?:[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 RenderAsync( string template, diff --git a/MeetingAssistant/Workflow/WorkflowRulesEditorChatPipeline.cs b/MeetingAssistant/Workflow/WorkflowRulesEditorChatPipeline.cs index 0a2a123..6a34c8a 100644 --- a/MeetingAssistant/Workflow/WorkflowRulesEditorChatPipeline.cs +++ b/MeetingAssistant/Workflow/WorkflowRulesEditorChatPipeline.cs @@ -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())) diff --git a/MeetingAssistant/Workflow/WorkflowRulesEditorTools.cs b/MeetingAssistant/Workflow/WorkflowRulesEditorTools.cs index 21fda71..bf86504 100644 --- a/MeetingAssistant/Workflow/WorkflowRulesEditorTools.cs +++ b/MeetingAssistant/Workflow/WorkflowRulesEditorTools.cs @@ -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 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 ReadConfigDocs(int? from = null, int? to = null, int? tail = null) { if (!File.Exists(configDocsPath))