diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/AITests/ActionPlannerTests.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/AITests/ActionPlannerTests.cs new file mode 100644 index 000000000..5e92de371 --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/AITests/ActionPlannerTests.cs @@ -0,0 +1,502 @@ +using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.AI; +using Microsoft.Teams.AI.AI.Augmentations; +using Microsoft.Teams.AI.AI.Models; +using Microsoft.Teams.AI.AI.Planners; +using Microsoft.Teams.AI.AI.Prompts; +using Microsoft.Teams.AI.AI.Tokenizers; +using Microsoft.Teams.AI.AI.Validators; +using Microsoft.Teams.AI.State; +using Microsoft.Teams.AI.Tests.TestUtils; +using Moq; + +namespace Microsoft.Teams.AI.Tests.AITests +{ + public class ActionPlannerTests + { + [Fact] + public async void Test_CompletePromptAsync_HasPrompt() + { + // Arrange + var modelMock = new Mock(); + var responseMock = new Mock(); + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(responseMock.Object); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(new Mock().Object) + ); + var context = new Mock(); + var memory = new TestMemory(); + var planner = new ActionPlanner(options, new TestLoggerFactory()); + + // Act + var result = await planner.CompletePromptAsync(context.Object, memory, promptTemplate, null); + + // Assert + Assert.Equal(responseMock.Object, result); + } + + [Fact] + public async void Test_CompletePromptAsync_DoesNotHavePrompt() + { + // Arrange + var modelMock = new Mock(); + var responseMock = new Mock(); + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(responseMock.Object); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var prompts = new PromptManager(); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(new Mock().Object) + ); + var context = new Mock(); + var memory = new TestMemory(); + var planner = new ActionPlanner(options, new TestLoggerFactory()); + + // Act + var result = await planner.CompletePromptAsync(context.Object, memory, promptTemplate, null); + + // Assert + Assert.True(prompts.HasPrompt("prompt")); + Assert.Equal(responseMock.Object, result); + } + + [Fact] + public async void Test_ContinueTaskAsync_PromptResponseStatusError() + { + // Arrange + var modelMock = new Mock(); + var response = new PromptResponse() + { + Status = PromptResponseStatus.Error, + Error = new("failed") + }; + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(response); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(promptTemplate) + ); + var turnContext = TurnStateConfig.CreateConfiguredTurnContext(); + var state = new TurnState(); + await state.LoadStateAsync(null, turnContext); + state.Temp.Input = "test"; + var planner = new ActionPlanner(options, new TestLoggerFactory()); + var ai = new AI(new(planner)); + + // Act + var exception = await Assert.ThrowsAsync(async () => await planner.ContinueTaskAsync(turnContext, state, ai)); + + // Assert + Assert.Equal("failed", exception.Message); + } + + [Fact] + public async void Test_ContinueTaskAsync_PromptResponseStatusError_ErrorNull() + { + // Arrange + var modelMock = new Mock(); + var response = new PromptResponse() + { + Status = PromptResponseStatus.Error + }; + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(response); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(promptTemplate) + ); + var turnContext = TurnStateConfig.CreateConfiguredTurnContext(); + var state = new TurnState(); + await state.LoadStateAsync(null, turnContext); + state.Temp.Input = "test"; + var planner = new ActionPlanner(options, new TestLoggerFactory()); + var ai = new AI(new(planner)); + + // Act + var exception = await Assert.ThrowsAsync(async () => await planner.ContinueTaskAsync(turnContext, state, ai)); + + // Assert + Assert.Equal("[Action Planner]: an error has occurred", exception.Message); + } + + [Fact] + public async void Test_ContinueTaskAsync_PlanNull() + { + // Arrange + var modelMock = new Mock(); + var response = new PromptResponse() + { + Status = PromptResponseStatus.Success + }; + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(response); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var augmentationMock = new Mock(); + Plan? plan = null; + augmentationMock.Setup(augmentation => augmentation.CreatePlanFromResponseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(plan); + augmentationMock.Setup(augmentation => augmentation.ValidateResponseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new Validation { Valid = true }); + promptTemplate.Augmentation = augmentationMock.Object; + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(promptTemplate) + ); + var turnContext = TurnStateConfig.CreateConfiguredTurnContext(); + var state = new TurnState(); + await state.LoadStateAsync(null, turnContext); + state.Temp.Input = "test"; + var planner = new ActionPlanner(options, new TestLoggerFactory()); + var ai = new AI(new(planner)); + + // Act + var exception = await Assert.ThrowsAsync(async () => await planner.ContinueTaskAsync(turnContext, state, ai)); + + // Assert + Assert.Equal("[Action Planner]: failed to create plan", exception.Message); + } + + [Fact] + public async void Test_ContinueTaskAsync() + { + // Arrange + var modelMock = new Mock(); + var response = new PromptResponse() + { + Status = PromptResponseStatus.Success + }; + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(response); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var augmentationMock = new Mock(); + var planMock = new Mock(); + augmentationMock.Setup(augmentation => augmentation.CreatePlanFromResponseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(planMock.Object); + augmentationMock.Setup(augmentation => augmentation.ValidateResponseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new Validation { Valid = true }); + promptTemplate.Augmentation = augmentationMock.Object; + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(promptTemplate) + ); + var turnContext = TurnStateConfig.CreateConfiguredTurnContext(); + var state = new TurnState(); + await state.LoadStateAsync(null, turnContext); + state.Temp.Input = "test"; + var planner = new ActionPlanner(options, new TestLoggerFactory()); + var ai = new AI(new(planner)); + + // Act + var result = await planner.ContinueTaskAsync(turnContext, state, ai); + + // Assert + Assert.Equal(planMock.Object, result); + } + + [Fact] + public async void Test_BeginTaskAsync_PromptResponseStatusError() + { + // Arrange + var modelMock = new Mock(); + var response = new PromptResponse() + { + Status = PromptResponseStatus.Error, + Error = new("failed") + }; + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(response); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(promptTemplate) + ); + var turnContext = TurnStateConfig.CreateConfiguredTurnContext(); + var state = new TurnState(); + await state.LoadStateAsync(null, turnContext); + state.Temp.Input = "test"; + var planner = new ActionPlanner(options, new TestLoggerFactory()); + var ai = new AI(new(planner)); + + // Act + var exception = await Assert.ThrowsAsync(async () => await planner.BeginTaskAsync(turnContext, state, ai)); + + // Assert + Assert.Equal("failed", exception.Message); + } + + [Fact] + public async void Test_BeginTaskAsync_PromptResponseStatusError_ErrorNull() + { + // Arrange + var modelMock = new Mock(); + var response = new PromptResponse() + { + Status = PromptResponseStatus.Error + }; + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(response); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(promptTemplate) + ); + var turnContext = TurnStateConfig.CreateConfiguredTurnContext(); + var state = new TurnState(); + await state.LoadStateAsync(null, turnContext); + state.Temp.Input = "test"; + var planner = new ActionPlanner(options, new TestLoggerFactory()); + var ai = new AI(new(planner)); + + // Act + var exception = await Assert.ThrowsAsync(async () => await planner.BeginTaskAsync(turnContext, state, ai)); + + // Assert + Assert.Equal("[Action Planner]: an error has occurred", exception.Message); + } + + [Fact] + public async void Test_BeginTaskAsync_PlanNull() + { + // Arrange + var modelMock = new Mock(); + var response = new PromptResponse() + { + Status = PromptResponseStatus.Success + }; + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(response); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var augmentationMock = new Mock(); + Plan? plan = null; + augmentationMock.Setup(augmentation => augmentation.CreatePlanFromResponseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(plan); + augmentationMock.Setup(augmentation => augmentation.ValidateResponseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new Validation { Valid = true }); + promptTemplate.Augmentation = augmentationMock.Object; + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(promptTemplate) + ); + var turnContext = TurnStateConfig.CreateConfiguredTurnContext(); + var state = new TurnState(); + await state.LoadStateAsync(null, turnContext); + state.Temp.Input = "test"; + var planner = new ActionPlanner(options, new TestLoggerFactory()); + var ai = new AI(new(planner)); + + // Act + var exception = await Assert.ThrowsAsync(async () => await planner.BeginTaskAsync(turnContext, state, ai)); + + // Assert + Assert.Equal("[Action Planner]: failed to create plan", exception.Message); + } + + [Fact] + public async void Test_BeginTaskAsync() + { + // Arrange + var modelMock = new Mock(); + var response = new PromptResponse() + { + Status = PromptResponseStatus.Success + }; + modelMock.Setup(model => model.CompletePromptAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(response); + var promptTemplate = new PromptTemplate( + "prompt", + new(new() { }) + ); + var augmentationMock = new Mock(); + var planMock = new Mock(); + augmentationMock.Setup(augmentation => augmentation.CreatePlanFromResponseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(planMock.Object); + augmentationMock.Setup(augmentation => augmentation.ValidateResponseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new Validation { Valid = true }); + promptTemplate.Augmentation = augmentationMock.Object; + var prompts = new PromptManager(); + prompts.AddPrompt("prompt", promptTemplate); + var options = new ActionPlannerOptions( + modelMock.Object, + prompts, + (context, state, planner) => Task.FromResult(promptTemplate) + ); + var turnContext = TurnStateConfig.CreateConfiguredTurnContext(); + var state = new TurnState(); + await state.LoadStateAsync(null, turnContext); + state.Temp.Input = "test"; + var planner = new ActionPlanner(options, new TestLoggerFactory()); + var ai = new AI(new(planner)); + + // Act + var result = await planner.BeginTaskAsync(turnContext, state, ai); + + // Assert + Assert.Equal(planMock.Object, result); + } + + private sealed class TestMemory : IMemory + { + public Dictionary Values { get; set; } = new Dictionary(); + + public void DeleteValue(string path) + { + Values.Remove(path); + } + + public object? GetValue(string path) + { + return Values.GetValueOrDefault(path); + } + + public bool HasValue(string path) + { + return Values.ContainsKey(path); + } + + public void SetValue(string path, object value) + { + Values[path] = value; + } + } + } +}