diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs index 1cd1b2bc941..311c35b157d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs @@ -9,6 +9,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; +using Microsoft.Extensions.Logging; namespace Microsoft.Agents.AI.Workflows.Declarative.Interpreter; @@ -27,6 +28,7 @@ internal static class Steps public static string Restart(string actionId) => $"{actionId}_{nameof(Restart)}"; } + private readonly ILogger _logger; private readonly Executor _rootAction; private readonly WorkflowModel> _workflowModel; private readonly DeclarativeWorkflowOptions _workflowOptions; @@ -37,6 +39,7 @@ public WorkflowActionVisitor( WorkflowFormulaState state, DeclarativeWorkflowOptions options) { + this._logger = options.LoggerFactory.CreateLogger(); this._rootAction = rootAction; this._workflowModel = new WorkflowModel>((IModeledAction)rootAction); this._workflowOptions = options; @@ -645,6 +648,7 @@ private static string GetParentId(BotElement item) => private void NotSupported(DialogAction item) { Debug.WriteLine($"> UNKNOWN: {new string('\t', this._workflowModel.GetDepth(item.GetParentId()))}{FormatItem(item)} => {FormatParent(item)}"); + this._logger.LogWarning("Unsupported action skipped: {ActionType} ({ActionId}).", item.GetType().Name, item.GetId()); this.HasUnsupportedActions = true; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs index d3e828fef65..9744aa2123b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs @@ -11,6 +11,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using Moq; using Xunit.Sdk; @@ -246,6 +247,36 @@ public void UnsupportedAction(Type type) Assert.True(visitor.HasUnsupportedActions); } + [Fact] + public void UnsupportedActionLogsWarning() + { + SearchKnowledgeSources.Builder unsupportedAction = new() { Id = "action_bad" }; + AdaptiveDialog.Builder dialogBuilder = + new() + { + BeginDialog = + new OnActivity.Builder() + { + Id = "anything", + Actions = [unsupportedAction] + } + }; + AdaptiveDialog dialog = dialogBuilder.Build(); + + WorkflowFormulaState state = new(RecalcEngineFactory.Create()); + Mock mockAgentProvider = CreateMockProvider("1"); + CapturingLoggerFactory loggerFactory = new(); + DeclarativeWorkflowOptions options = new(mockAgentProvider.Object) { LoggerFactory = loggerFactory }; + WorkflowActionVisitor visitor = new(new DeclarativeWorkflowExecutor(WorkflowActionVisitor.Steps.Root("anything"), options, state, (message) => DeclarativeWorkflowBuilder.DefaultTransform(message)), state, options); + WorkflowElementWalker walker = new(visitor); + walker.Visit(dialog); + + Assert.True(visitor.HasUnsupportedActions); + (LogLevel Level, string Message) warning = Assert.Single(loggerFactory.Entries, entry => entry.Level == LogLevel.Warning); + Assert.Contains(nameof(SearchKnowledgeSources), warning.Message, StringComparison.Ordinal); + Assert.Contains("action_bad", warning.Message, StringComparison.Ordinal); + } + [Theory] [InlineData("CaseInsensitive.yaml", "end_when_match")] [InlineData("ClearAllVariables.yaml", "clear_all")] @@ -405,4 +436,29 @@ private static Mock CreateMockHttpRequestHandler() })); return mockHandler; } + + private sealed class CapturingLoggerFactory : ILoggerFactory + { + public List<(LogLevel Level, string Message)> Entries { get; } = []; + + public void AddProvider(ILoggerProvider provider) + { + } + + public ILogger CreateLogger(string categoryName) => new CapturingLogger(this.Entries); + + public void Dispose() + { + } + + private sealed class CapturingLogger(List<(LogLevel Level, string Message)> entries) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + => entries.Add((logLevel, formatter(state, exception))); + } + } }