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:
@@ -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
|
||||
|
||||
@@ -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<FileMeetingWorkflowRulesProvider>.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<string> Messages { get; } = [];
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state)
|
||||
where TState : notnull
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel,
|
||||
EventId eventId,
|
||||
TState state,
|
||||
Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
Messages.Add(formatter(state, exception));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<RulesEditorFixture> 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;
|
||||
|
||||
@@ -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