From ef713b6aef1c611d6330875dc6afaa11cc55d14b Mon Sep 17 00:00:00 2001 From: singhk97 <115390646+singhk97@users.noreply.github.com> Date: Wed, 12 Jul 2023 06:46:27 +0700 Subject: [PATCH] Turn State Infrastructure (#232) Added support for the turn state. resolves #209 Additions - Turn State - Turn State Manager - Conversation history - Integration of Turn State with AI, Application, OpenAIPlanner classes Missing that will come in subsequent PRs: - `Application` unit tests - `AI` class unit tests A few major differences between this and the JS SDK: - Dropped `Default` from `DefaultTurnState` to get `TurnState`. Not sure why `Default` was there in the first place, I presume it's to signal to the user that this component is configurable. We do that implictly in C# with interfaces (ex. `ITurnStateManager`) and unsealed class (ex. `TurnState`). - `Application` class now has two generic parameters, `TState` and `TTurnStateManager`. This was to solve the problem that there is virtually no covariance in C# generic types (i.e `ITurnStateManager var = TurnStateManager` throws compiler errors, even though `ChildTurnState` extends `TurnState`). --- .../AITests/ActionCollectionTests.cs | 26 +- .../AzureContentSafetyModeratorTests.cs | 30 +- .../AITests/OpenAIModeratorTests.cs | 21 +- .../AITests/OpenAIPlannerTests.cs | 52 +- .../AITests/PromptManagerTests.cs | 160 ++++-- .../ActivityHandlerTests.cs | 124 ++--- .../ConversationUpdateActivityTests.cs | 85 +-- .../InvokeActivityNotImplementedTests.cs | 105 ++-- .../InvokeActivityTests.cs | 106 ++-- .../ConversationHistoryTests.cs | 525 ++++++++++++++++++ .../AzureContentSafetyModeratorTests.cs | 1 + .../AzureOpenAIPlannerTests.cs | 34 +- .../IntegrationTests/OpenAIModeratorTests.cs | 1 + .../IntegrationTests/OpenAIPlannerTests.cs | 34 +- .../StateTests/TurnStateManagerTests.cs | 182 ++++++ .../TestUtils/TestActivityHandler.cs | 135 ++--- .../TestUtils/TestApplication.cs | 12 + .../TestUtils/TestDelegatingTurnContext.cs | 11 +- .../TestUtils/TestTurnState.cs | 8 + .../TestUtils/TestTurnStateManager.cs | 17 + .../UtilitiesTest.cs | 8 +- .../Microsoft.Bot.Builder.M365/AI/AI.cs | 116 ++-- .../AI/AIOptions.cs | 14 +- .../Microsoft.Bot.Builder.M365/AI/AITypes.cs | 7 +- .../AI/Action/ActionCollection.cs | 5 +- .../AI/Action/ActionEntry.cs | 5 +- .../AI/Action/DefaultActions.cs | 6 +- .../AI/Action/DoCommandActionData.cs | 3 +- .../AI/Action/IActionCollection.cs | 7 +- .../AzureContentSafetyClient.cs | 1 - .../Moderator/AzureContentSafetyModerator.cs | 33 +- .../AI/Moderator/DefaultModerator.cs | 5 +- .../AI/Moderator/IModerator.cs | 5 +- .../AI/Moderator/OpenAIModerator.cs | 15 +- .../AI/OpenAI/OpenAIClient.cs | 2 +- .../AI/Planner/AzureOpenAIPlanner.cs | 3 +- .../AI/Planner/AzureOpenAIPlannerOptions.cs | 2 +- .../AI/Planner/ConversationHistory.cs | 344 ++++++++++++ .../AI/Planner/IPlanner.cs | 5 +- .../AI/Planner/OpenAIPlanner.cs | 99 +++- .../AI/Planner/OpenAIPlannerOptions.cs | 6 +- .../AI/Planner/Plan.cs | 14 +- .../AI/Prompt/IPromptManager.cs | 5 +- .../AI/Prompt/PromptManager.cs | 47 +- .../AI/Prompt/PromptTemplate.cs | 4 +- .../AI/Prompt/SKFunctionWrapper.cs | 3 +- .../AI/Prompt/TemplateFunctionEntry.cs | 8 +- .../Microsoft.Bot.Builder.M365/Application.cs | 45 +- .../ApplicationOptions.cs | 20 +- .../Exceptions/TurnStateManagerException.cs | 9 + .../State/IReadOnlyEntry.cs | 30 + .../State/ITurnState.cs | 15 + .../State/ITurnStateManager.cs | 26 + .../State/StateBase.cs | 72 +++ .../State/TempState.cs | 51 ++ .../State/TurnState.cs | 110 ++++ .../State/TurnStateEntry.cs | 83 +++ .../State/TurnStateManager.cs | 146 +++++ .../Microsoft.Bot.Builder.M365/TurnState.cs | 10 - .../TurnStateEntry.cs | 10 - .../TurnStateManager.cs | 10 - .../Microsoft.Bot.Builder.M365/TypingTimer.cs | 9 +- .../Utilities/Verify.cs | 2 +- 63 files changed, 2471 insertions(+), 618 deletions(-) create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ConversationHistoryTests.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/StateTests/TurnStateManagerTests.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestApplication.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestTurnState.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestTurnStateManager.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/ConversationHistory.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Exceptions/TurnStateManagerException.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/IReadOnlyEntry.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/ITurnState.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/ITurnStateManager.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/StateBase.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TempState.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnState.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnStateEntry.cs create mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnStateManager.cs delete mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnState.cs delete mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnStateEntry.cs delete mode 100644 dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnStateManager.cs diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/ActionCollectionTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/ActionCollectionTests.cs index ccb4f8687..f5a1f6a5c 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/ActionCollectionTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/ActionCollectionTests.cs @@ -1,11 +1,7 @@ using Microsoft.Bot.Builder.M365.AI.Action; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Microsoft.Bot.Builder.M365.Tests.TestUtils; -namespace Microsoft.Bot.Builder.M365.Tests.AI +namespace Microsoft.Bot.Builder.M365.Tests.AITests { public class ActionCollectionTests { @@ -13,14 +9,14 @@ public class ActionCollectionTests public void Test_Simple() { // Arrange - IActionCollection actionCollection = new ActionCollection(); + IActionCollection actionCollection = new ActionCollection(); string name = "action"; - ActionHandler handler = (turnContext, turnState, data, action) => Task.FromResult(true); + ActionHandler handler = (turnContext, turnState, data, action) => Task.FromResult(true); bool allowOverrides = true; // Act actionCollection.SetAction(name, handler, allowOverrides); - ActionEntry entry = actionCollection.GetAction(name); + ActionEntry entry = actionCollection.GetAction(name); // Assert Assert.True(actionCollection.HasAction(name)); @@ -34,9 +30,9 @@ public void Test_Simple() public void Test_Set_NonOverridable_Action_Throws_Exception() { // Arrange - IActionCollection actionCollection = new ActionCollection(); + IActionCollection actionCollection = new ActionCollection(); string name = "action"; - ActionHandler handler = (turnContext, turnState, data, action) => Task.FromResult(true); + ActionHandler handler = (turnContext, turnState, data, action) => Task.FromResult(true); bool allowOverrides = false; actionCollection.SetAction(name, handler, allowOverrides); @@ -52,7 +48,7 @@ public void Test_Set_NonOverridable_Action_Throws_Exception() public void Test_Get_NonExistent_Action() { // Arrange - IActionCollection actionCollection = new ActionCollection(); + IActionCollection actionCollection = new ActionCollection(); var nonExistentAction = "non existent action"; // Act @@ -67,7 +63,7 @@ public void Test_Get_NonExistent_Action() public void Test_HasAction_False() { // Arrange - IActionCollection actionCollection = new ActionCollection(); + IActionCollection actionCollection = new ActionCollection(); var nonExistentAction = "non existent action"; // Act @@ -81,8 +77,8 @@ public void Test_HasAction_False() public void Test_HasAction_True() { // Arrange - IActionCollection actionCollection = new ActionCollection(); - ActionHandler handler = (turnContext, turnState, data, action) => Task.FromResult(true); + IActionCollection actionCollection = new ActionCollection(); + ActionHandler handler = (turnContext, turnState, data, action) => Task.FromResult(true); var name = "actionName"; // Act diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/AzureContentSafetyModeratorTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/AzureContentSafetyModeratorTests.cs index 4a1736783..058936dd9 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/AzureContentSafetyModeratorTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/AzureContentSafetyModeratorTests.cs @@ -9,6 +9,7 @@ using Moq; using System.Reflection; using Microsoft.Bot.Builder.M365.Exceptions; +using Microsoft.Bot.Builder.M365.Tests.TestUtils; namespace Microsoft.Bot.Builder.M365.Tests.AITests { @@ -22,13 +23,12 @@ public async void Test_ReviewPrompt_ThrowsException() var endpoint = "randomEndpoint"; var botAdapterMock = new Mock(); - // TODO: when TurnState is implemented, get the user input var activity = new Activity() { Text = "input", }; var turnContext = new TurnContext(botAdapterMock.Object, activity); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", new PromptTemplateConfiguration @@ -47,7 +47,7 @@ public async void Test_ReviewPrompt_ThrowsException() clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny())).ThrowsAsync(exception); var options = new AzureContentSafetyModeratorOptions(apiKey, endpoint, ModerationType.Both); - var moderator = new AzureContentSafetyModerator(options); + var moderator = new AzureContentSafetyModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act @@ -68,13 +68,13 @@ public async void Test_ReviewPrompt_Flagged(ModerationType moderate) var endpoint = "randomEndpoint"; var botAdapterMock = new Mock(); - // TODO: when TurnState is implemented, get the user input + // TODO: when TestTurnState is implemented, get the user input var activity = new Activity() { Text = "input", }; var turnContext = new TurnContext(botAdapterMock.Object, activity); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", new PromptTemplateConfiguration @@ -100,7 +100,7 @@ public async void Test_ReviewPrompt_Flagged(ModerationType moderate) clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny())).ReturnsAsync(response); var options = new AzureContentSafetyModeratorOptions(apiKey, endpoint, moderate); - var moderator = new AzureContentSafetyModerator(options); + var moderator = new AzureContentSafetyModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act @@ -133,13 +133,13 @@ public async void Test_ReviewPrompt_NotFlagged(ModerationType moderate) var endpoint = "randomEndpoint"; var botAdapterMock = new Mock(); - // TODO: when TurnState is implemented, get the user input + // TODO: when TestTurnState is implemented, get the user input var activity = new Activity() { Text = "input", }; var turnContext = new TurnContext(botAdapterMock.Object, activity); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", new PromptTemplateConfiguration @@ -165,7 +165,7 @@ public async void Test_ReviewPrompt_NotFlagged(ModerationType moderate) clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny())).ReturnsAsync(response); var options = new AzureContentSafetyModeratorOptions(apiKey, endpoint, moderate); - var moderator = new AzureContentSafetyModerator(options); + var moderator = new AzureContentSafetyModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act @@ -183,7 +183,7 @@ public async void Test_ReviewPlan_ThrowsException() var endpoint = "randomEndpoint"; var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var plan = new Plan(new List() { new PredictedDoCommand("action"), @@ -195,7 +195,7 @@ public async void Test_ReviewPlan_ThrowsException() clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny())).ThrowsAsync(exception); var options = new AzureContentSafetyModeratorOptions(apiKey, endpoint, ModerationType.Both); - var moderator = new AzureContentSafetyModerator(options); + var moderator = new AzureContentSafetyModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act @@ -216,7 +216,7 @@ public async void Test_ReviewPlan_Flagged(ModerationType moderate) var endpoint = "randomEndpoint"; var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var plan = new Plan(new List() { new PredictedDoCommand("action"), @@ -235,7 +235,7 @@ public async void Test_ReviewPlan_Flagged(ModerationType moderate) clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny())).ReturnsAsync(response); var options = new AzureContentSafetyModeratorOptions(apiKey, endpoint, moderate); - var moderator = new AzureContentSafetyModerator(options); + var moderator = new AzureContentSafetyModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act @@ -268,7 +268,7 @@ public async void Test_ReviewPlan_NotFlagged(ModerationType moderate) var endpoint = "randomEndpoint"; var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var plan = new Plan(new List() { new PredictedDoCommand("action"), @@ -287,7 +287,7 @@ public async void Test_ReviewPlan_NotFlagged(ModerationType moderate) clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny())).ReturnsAsync(response); var options = new AzureContentSafetyModeratorOptions(apiKey, endpoint, moderate); - var moderator = new AzureContentSafetyModerator(options); + var moderator = new AzureContentSafetyModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/OpenAIModeratorTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/OpenAIModeratorTests.cs index bd5cc8120..d5e541f7a 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/OpenAIModeratorTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/OpenAIModeratorTests.cs @@ -9,6 +9,8 @@ using Microsoft.Bot.Schema; using Moq; using System.Reflection; +using Microsoft.Bot.Builder.M365.State; +using Microsoft.Bot.Builder.M365.Tests.TestUtils; namespace Microsoft.Bot.Builder.M365.Tests.AITests { @@ -21,13 +23,12 @@ public async void Test_ReviewPrompt_ThrowsException() var apiKey = "randomApiKey"; var botAdapterMock = new Mock(); - // TODO: when TurnState is implemented, get the user input var activity = new Activity() { Text = "input", }; var turnContext = new TurnContext(botAdapterMock.Object, activity); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", new PromptTemplateConfiguration @@ -46,7 +47,7 @@ public async void Test_ReviewPrompt_ThrowsException() clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny(), It.IsAny())).ThrowsAsync(exception); var options = new OpenAIModeratorOptions(apiKey, ModerationType.Both); - var moderator = new OpenAIModerator(options); + var moderator = new OpenAIModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act @@ -66,13 +67,13 @@ public async void Test_ReviewPrompt_Flagged(ModerationType moderate) var apiKey = "randomApiKey"; var botAdapterMock = new Mock(); - // TODO: when TurnState is implemented, get the user input + // TODO: when TestTurnState is implemented, get the user input var activity = new Activity() { Text = "input", }; var turnContext = new TurnContext(botAdapterMock.Object, activity); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", new PromptTemplateConfiguration @@ -122,7 +123,7 @@ public async void Test_ReviewPrompt_Flagged(ModerationType moderate) clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny(), It.IsAny())).ReturnsAsync(response); var options = new OpenAIModeratorOptions(apiKey, moderate); - var moderator = new OpenAIModerator(options); + var moderator = new OpenAIModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act @@ -151,7 +152,7 @@ public async void Test_ReviewPlan_ThrowsException() var apiKey = "randomApiKey"; var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var plan = new Plan(new List() { new PredictedDoCommand("action"), @@ -163,7 +164,7 @@ public async void Test_ReviewPlan_ThrowsException() clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny(), It.IsAny())).ThrowsAsync(exception); var options = new OpenAIModeratorOptions(apiKey, ModerationType.Both); - var moderator = new OpenAIModerator(options); + var moderator = new OpenAIModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act @@ -183,7 +184,7 @@ public async void Test_ReviewPlan_Flagged(ModerationType moderate) var apiKey = "randomApiKey"; var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var plan = new Plan(new List() { new PredictedDoCommand("action"), @@ -226,7 +227,7 @@ public async void Test_ReviewPlan_Flagged(ModerationType moderate) clientMock.Setup(client => client.ExecuteTextModeration(It.IsAny(), It.IsAny())).ReturnsAsync(response); var options = new OpenAIModeratorOptions(apiKey, moderate); - var moderator = new OpenAIModerator(options); + var moderator = new OpenAIModerator(options); moderator.GetType().GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(moderator, clientMock.Object); // Act diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/OpenAIPlannerTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/OpenAIPlannerTests.cs index 81898c174..e96a9c36b 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/OpenAIPlannerTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/OpenAIPlannerTests.cs @@ -8,6 +8,8 @@ using Microsoft.Bot.Builder.M365.AI.Action; using Microsoft.Extensions.Logging; using Microsoft.Bot.Builder.M365.AI.Moderator; +using Microsoft.Bot.Builder.M365.State; +using Microsoft.Bot.Builder.M365.Tests.TestUtils; namespace Microsoft.Bot.Builder.M365.Tests.AITests { @@ -22,8 +24,8 @@ public async void Test_GeneratePlan_PromptCompletionRateLimited_ShouldRedirectTo var options = new OpenAIPlannerOptions(apiKey,model); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); - var moderatorMock = new Mock>(); + var turnStateMock = new Mock(); + var moderatorMock = new Mock>(); var promptTemplate = new PromptTemplate( "prompt", @@ -39,9 +41,9 @@ public async void Test_GeneratePlan_PromptCompletionRateLimited_ShouldRedirectTo ); static string rateLimitedFunc() => throw new PlannerException("", new AIException(AIException.ErrorCodes.Throttling)); - var planner = new CustomCompletePromptOpenAIPlanner(options, rateLimitedFunc); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); - + var planner = new CustomCompletePromptOpenAIPlanner(options, rateLimitedFunc); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + // Act var result = await planner.GeneratePlanAsync(turnContextMock.Object, turnStateMock.Object, promptTemplate, aiOptions); @@ -64,8 +66,8 @@ public async void Test_GeneratePlan_PromptCompletionFailed_ThrowsException() var options = new OpenAIPlannerOptions(apiKey, model); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); - var moderatorMock = new Mock>(); + var turnStateMock = new Mock(); + var moderatorMock = new Mock>(); var promptTemplate = new PromptTemplate( "prompt", @@ -81,8 +83,8 @@ public async void Test_GeneratePlan_PromptCompletionFailed_ThrowsException() ); static string throwsExceptionFunc() => throw new PlannerException("Exception Message"); - var planner = new CustomCompletePromptOpenAIPlanner(options, throwsExceptionFunc); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var planner = new CustomCompletePromptOpenAIPlanner(options, throwsExceptionFunc); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); // Act var exception = await Assert.ThrowsAsync(async () => await planner.GeneratePlanAsync(turnContextMock.Object, turnStateMock.Object, promptTemplate, aiOptions)); @@ -101,8 +103,8 @@ public async void Test_GeneratePlan_PromptCompletionEmptyStringResponse_ReturnsE var options = new OpenAIPlannerOptions(apiKey, model); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); - var moderatorMock = new Mock>(); + var turnStateMock = new Mock(); + var moderatorMock = new Mock>(); var promptTemplate = new PromptTemplate( "prompt", @@ -118,8 +120,8 @@ public async void Test_GeneratePlan_PromptCompletionEmptyStringResponse_ReturnsE ); static string emptyStringFunc() => String.Empty; - var planner = new CustomCompletePromptOpenAIPlanner(options, emptyStringFunc); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var planner = new CustomCompletePromptOpenAIPlanner(options, emptyStringFunc); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); // Act var result = await planner.GeneratePlanAsync(turnContextMock.Object, turnStateMock.Object, promptTemplate, aiOptions); @@ -140,8 +142,8 @@ public async void Test_GeneratePlan_PromptCompletion_OneSayPerTurn() options.OneSayPerTurn = true; var turnContextMock = new Mock(); - var turnStateMock = new Mock(); - var moderatorMock = new Mock>(); + var turnStateMock = new Mock(); + var moderatorMock = new Mock>(); var promptTemplate = new PromptTemplate( "prompt", @@ -158,8 +160,8 @@ public async void Test_GeneratePlan_PromptCompletion_OneSayPerTurn() string multipleSayCommands = @"{ 'type':'plan','commands':[{'type':'SAY','response':'responseValueA'}, {'type':'SAY','response':'responseValueB'}]}"; string multipleSayCommandsFunc() => multipleSayCommands; - var planner = new CustomCompletePromptOpenAIPlanner(options, multipleSayCommandsFunc); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var planner = new CustomCompletePromptOpenAIPlanner(options, multipleSayCommandsFunc); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); // Act @@ -183,8 +185,8 @@ public async void Test_GeneratePlan_Simple() var options = new OpenAIPlannerOptions(apiKey, model); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); - var moderatorMock = new Mock>(); + var turnStateMock = new Mock(); + var moderatorMock = new Mock>(); var promptTemplate = new PromptTemplate( "prompt", @@ -201,9 +203,9 @@ public async void Test_GeneratePlan_Simple() string simplePlan = @"{ 'type':'plan','commands':[{'type':'SAY','response':'responseValueA'}, {'type':'DO', 'action': 'actionName'}]}"; string multipleSayCommandsFunc() => simplePlan; - var planner = new CustomCompletePromptOpenAIPlanner(options, multipleSayCommandsFunc); + var planner = new CustomCompletePromptOpenAIPlanner(options, multipleSayCommandsFunc); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); // Act var result = await planner.GeneratePlanAsync(turnContextMock.Object, turnStateMock.Object, promptTemplate, aiOptions); @@ -219,13 +221,11 @@ public async void Test_GeneratePlan_Simple() Assert.Equal("actionName", doCommand.Action); } - private class CustomCompletePromptOpenAIPlanner : OpenAIPlanner - where TState : TurnState - where TOptions : OpenAIPlannerOptions - { + private class CustomCompletePromptOpenAIPlanner : OpenAIPlanner + where TState : TestTurnState { private Func customFunction; - public CustomCompletePromptOpenAIPlanner(TOptions options, Func customFunction, ILogger? logger = null) : base(options, logger) + public CustomCompletePromptOpenAIPlanner(OpenAIPlannerOptions options, Func customFunction, ILogger? logger = null) : base(options, logger) { this.customFunction = customFunction; } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/PromptManagerTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/PromptManagerTests.cs index 4024cca43..5af339a20 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/PromptManagerTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/AITests/PromptManagerTests.cs @@ -4,21 +4,22 @@ using Microsoft.Bot.Schema; using Microsoft.SemanticKernel.TemplateEngine; using Moq; +using Microsoft.Bot.Builder.M365.State; +using Microsoft.Bot.Builder.M365.Tests.TestUtils; namespace Microsoft.Bot.Builder.M365.Tests.AI { - // TODO: Complete tests once turn state infrastructure is implemented public class PromptManagerTests { [Fact] public void AddFunction_Simple() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var name = "promptFunctionName"; - PromptFunction promptFunction = (ITurnContext turnContext, TurnState turnState) => Task.FromResult(name); + PromptFunction promptFunction = (ITurnContext turnContext, TestTurnState turnState) => Task.FromResult(name); // Act promptManager.AddFunction(name, promptFunction); @@ -31,13 +32,13 @@ public void AddFunction_Simple() public void AddFunction_AlreadyExists_AllowOverride() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var name = "promptFunctionName"; var nameOverride = "promptFunctionNameOverride"; - PromptFunction promptFunction = (ITurnContext turnContext, TurnState turnState) => Task.FromResult(name); - PromptFunction promptFunctionOverride = (ITurnContext turnContext, TurnState turnState) => Task.FromResult(nameOverride); + PromptFunction promptFunction = (ITurnContext turnContext, TestTurnState turnState) => Task.FromResult(name); + PromptFunction promptFunctionOverride = (ITurnContext turnContext, TestTurnState turnState) => Task.FromResult(nameOverride); // Act promptManager.AddFunction(name, promptFunction, false); @@ -51,13 +52,13 @@ public void AddFunction_AlreadyExists_AllowOverride() public void AddFunction_AlreadyExists_NotAllowOverride() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var name = "promptFunctionName"; var nameOverride = "promptFunctionNameOverride"; - Task promptFunction(ITurnContext turnContext, TurnState turnState) => Task.FromResult(name); - Task promptFunctionOverride(ITurnContext turnContext, TurnState turnState) => Task.FromResult(nameOverride); + Task promptFunction(ITurnContext turnContext, TestTurnState turnState) => Task.FromResult(name); + Task promptFunctionOverride(ITurnContext turnContext, TestTurnState turnState) => Task.FromResult(nameOverride); // Act promptManager.AddFunction(name, promptFunction, false); @@ -71,7 +72,7 @@ public void AddFunction_AlreadyExists_NotAllowOverride() public void AddPromptTemplate_Simple() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var name = "promptTemplateName"; var promptTemplate = new PromptTemplate( "template string", @@ -97,7 +98,7 @@ public void AddPromptTemplate_Simple() public void AddPromptTemplate_AlreadyExists() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var name = "promptTemplateName"; var promptTemplate = new PromptTemplate( "template string", @@ -124,7 +125,7 @@ public void AddPromptTemplate_AlreadyExists() public void LoadPromptTemplate_FromCollection() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var name = "promptTemplateName"; var promptTemplate = new PromptTemplate( "template string", @@ -159,7 +160,7 @@ public void LoadPromptTemplate_FromFilesystem() } var directoryPath = Path.GetFullPath(Path.Combine(currentAssemblyDirectory, $"../../../AITests/prompts")); - var promptManager = new PromptManager(directoryPath); + var promptManager = new PromptManager(directoryPath); var name = "promptTemplateFolder"; var expectedPromptTemplate = new PromptTemplate( "This is a prompt template string.", @@ -211,7 +212,7 @@ public void LoadPromptTemplate_FromFilesystem_NoPromptFolderConfigured() } var directoryPath = Path.GetFullPath(Path.Combine(currentAssemblyDirectory, $"../../../AITests/prompts")); - var promptManager = new PromptManager(directoryPath); + var promptManager = new PromptManager(directoryPath); var name = "invalidPromptTemplateFolder"; // Act @@ -225,11 +226,11 @@ public void LoadPromptTemplate_FromFilesystem_NoPromptFolderConfigured() public async void RenderPrompt_PlainText() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var botAdapterStub = Mock.Of(); var turnContextMock = new Mock(botAdapterStub, new Activity { Text = "user message" }); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var configuration = new PromptTemplateConfiguration { Completion = @@ -259,11 +260,11 @@ public async void RenderPrompt_PlainText() public async void RenderPrompt_ResolveFunction_FunctionExists() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var botAdapterStub = Mock.Of(); var turnContextMock = new Mock(botAdapterStub, new Activity { Text = "user message" }); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var configuration = new PromptTemplateConfiguration { Completion = @@ -276,7 +277,7 @@ public async void RenderPrompt_ResolveFunction_FunctionExists() /// Configure function var promptFunctionName = "promptFunctionName"; var output = "output"; - PromptFunction promptFunction = (TurnContext, TurnState) => Task.FromResult(output); + PromptFunction promptFunction = (TurnContext, TestTurnState) => Task.FromResult(output); /// Configure prompt var promptString = "The output of the function is {{ " + promptFunctionName + " }}"; @@ -298,11 +299,11 @@ public async void RenderPrompt_ResolveFunction_FunctionExists() public async Task RenderPrompt_ResolveFunction_FunctionNotExists() { // Arrange - var promptManager = new PromptManager(); + var promptManager = new PromptManager(); var botAdapterStub = Mock.Of(); var turnContextMock = new Mock(botAdapterStub, new Activity { Text = "user message" }); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var configuration = new PromptTemplateConfiguration { Completion = @@ -333,33 +334,120 @@ public async Task RenderPrompt_ResolveFunction_FunctionNotExists() } [Fact] - public async void RenderPrompt_ResolveVariable_Exist() + public async void RenderPrompt_ResolveVariable() { + // Arrange + var promptManager = new PromptManager(); + var botAdapterStub = Mock.Of(); + var turnContextMock = new Mock(botAdapterStub, new Activity { Text = "user message" }); - } + var turnStateMock = new Mock(); + var configuration = new PromptTemplateConfiguration + { + Completion = + { + MaxTokens = 2000, + Temperature = 0.2, + TopP = 0.5, + } + }; + /// Configure variable + var variableKey = "variableName"; + var variableValue = "value"; - [Fact] - public async void RenderPrompt_ResolveVariable_NotExist() - { + /// Configure prompt + var promptString = "The output of the function is {{ $" + variableKey + " }}"; + var expectedRenderedPrompt = $"The output of the function is {variableValue}"; + var promptTemplate = new PromptTemplate( + promptString, + configuration + ); + // Act + promptManager.Variables[variableKey] = variableValue; + var renderedPrompt = await promptManager.RenderPrompt(turnContextMock.Object, turnStateMock.Object, promptTemplate); + + // Assert + Assert.Equal(renderedPrompt.Text, expectedRenderedPrompt); } [Fact] - public async void RenderPrompt_ResolveVariable_NestedObject() + public async void RenderPrompt_ResolveVariable_DefaultTurnState() { + // Arrange + var promptManager = new PromptManager(); + var botAdapterStub = Mock.Of(); + var turnContextMock = new Mock(botAdapterStub, new Activity { Text = "user message" }); - } + var defaultTurnState = new TestTurnState(); + var inputValue = "input"; + var outputValue = "output"; + var historyValue = "history"; + var tempState = new TempState() + { + Input = inputValue, + Output = outputValue, + History = historyValue + }; + defaultTurnState.TempStateEntry = new TurnStateEntry(tempState); - [Fact] - public async void RenderPrompt_ResolveVariable_NestedObject_GreaterThanLevel2Depth_ShouldFail() - { - // {{ $state.conversation.level2.level3 }} is not allowed + var configuration = new PromptTemplateConfiguration + { + Completion = + { + MaxTokens = 2000, + Temperature = 0.2, + TopP = 0.5, + } + }; + + /// Configure prompt + var promptString = "{{ $input }}, {{ $output }}, {{ $history }}"; + var expectedRenderedPrompt = $"{inputValue}, {outputValue}, {historyValue}"; + var promptTemplate = new PromptTemplate( + promptString, + configuration + ); + + // Act + var renderedPrompt = await promptManager.RenderPrompt(turnContextMock.Object, defaultTurnState, promptTemplate); + + // Assert + Assert.Equal(renderedPrompt.Text, expectedRenderedPrompt); } [Fact] - public async void RenderPrompt_WithFunctionAndVariableReference() + public async void RenderPrompt_ResolveVariable_NotExist_ShouldResolveToEmptyString() { + // Arrange + var promptManager = new PromptManager(); + var botAdapterStub = Mock.Of(); + var turnContextMock = new Mock(botAdapterStub, new Activity { Text = "user message" }); + + var turnStateMock = new Mock(); + var configuration = new PromptTemplateConfiguration + { + Completion = + { + MaxTokens = 2000, + Temperature = 0.2, + TopP = 0.5, + } + }; + + /// Configure prompt + var promptString = "{{ $variable }}"; + var promptTemplate = new PromptTemplate( + promptString, + configuration + ); + + // Act + var renderedPrompt = await promptManager.RenderPrompt(turnContextMock.Object, turnStateMock.Object, promptTemplate); + + // Assert + Assert.Equal("", renderedPrompt.Text); } } } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/ActivityHandlerTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/ActivityHandlerTests.cs index 64cf1b18b..971dd7f78 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/ActivityHandlerTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/ActivityHandlerTests.cs @@ -19,10 +19,10 @@ public async Task Test_MessageActivity() // Arrange var activity = MessageFactory.Text("hello"); var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -37,10 +37,10 @@ public async Task Test_MessageUpdateActivity() // Arrange var activity = new Activity { Type = ActivityTypes.MessageUpdate }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -60,10 +60,10 @@ public async Task Test_MessageUpdateActivity_MessageEdit() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -84,10 +84,10 @@ public async Task Test_MessageUpdateActivity_MessageUndelete() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -107,10 +107,10 @@ public async Task Test_MessageUpdateActivity_MessageUndelete_NoMsteams() ChannelData = new TeamsChannelData { EventType = "undeleteMessage" }, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -129,10 +129,10 @@ public async Task Test_MessageUpdateActivity_NoChannelData() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -147,10 +147,10 @@ public async Task Test_MessageDeleteActivity() // Arrange var activity = new Activity { Type = ActivityTypes.MessageDelete }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -170,10 +170,10 @@ public async Task Test_MessageDeleteActivity_MessageSoftDelete() ChannelId = Channels.Msteams }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -193,10 +193,10 @@ public async Task Test_MessageDeleteActivity_MessageSoftDelete_NoMsteams() ChannelData = new TeamsChannelData { EventType = "softMessage" } }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -215,10 +215,10 @@ public async Task Test_MessageDeleteActivity_NoChannelData() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -233,10 +233,10 @@ public async Task Test_EndOfConversationActivity() // Arrange var activity = new Activity { Type = ActivityTypes.EndOfConversation, Value = "some value" }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -251,10 +251,10 @@ public async Task Test_TypingActivity() // Arrange var activity = new Activity { Type = ActivityTypes.Typing }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -269,10 +269,10 @@ public async Task Test_InstallationUpdateActivity() // Arrange var activity = new Activity { Type = ActivityTypes.InstallationUpdate }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -291,10 +291,10 @@ public async Task Test_InstallationUpdateActivity_AddAsync() Action = "add" }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -314,10 +314,10 @@ public async Task Test_InstallationUpdateActivity_AddUpgradeAsync() Action = "add-upgrade" }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -337,10 +337,10 @@ public async Task Test_InstallationUpdateActivity_RemoveAsync() Action = "remove" }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -360,10 +360,10 @@ public async Task Test_InstallationUpdateActivity_RemoveUpgradeAsync() Action = "remove-upgrade" }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -394,10 +394,10 @@ public async Task Test_MessageReactionActivity_ReactionAdded_And_ReactionRemoved }, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -419,10 +419,10 @@ public async Task Test_EventActivity() }; var turnContext = new TurnContext(new SimpleAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -442,10 +442,10 @@ public async Task Test_EventActivity_TokenResponseEventAsync() Name = SignInConstants.TokenResponseEventName, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -465,10 +465,10 @@ public async Task Test_EventActvitiy_EventAsync() Name = "some.random.event", }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -497,10 +497,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -531,10 +531,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -566,10 +566,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -591,10 +591,10 @@ public async Task Test_EventActivity_EventNullNameAsync() Type = ActivityTypes.Event, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -615,10 +615,10 @@ public async Task Test_CommandActivityType() Value = new CommandValue { CommandId = "Test", Data = new { test = true } } }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -639,10 +639,10 @@ public async Task Test_CommandResultActivityType() }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -660,10 +660,10 @@ public async Task Test_UnrecognizedActivityType() Type = "shall.not.pass", }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -688,10 +688,10 @@ public async Task TestDelegatingTurnContext() turnContextMock.Setup(tc => tc.SendActivitiesAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(new[] { new ResourceResponse() })); turnContextMock.Setup(tc => tc.DeleteActivityAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(new ResourceResponse())); turnContextMock.Setup(tc => tc.UpdateActivityAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(new ResourceResponse())); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestDelegatingTurnContext(new ApplicationOptions()); + var bot = new TestDelegatingTurnContext(new TestApplicationOptions()); await bot.RunActivityHandlerAsync(turnContextMock.Object, turnState, default); // Assert @@ -716,7 +716,7 @@ public async Task Test_OnTurnAsync_Disable_LongRunningMessages() var turnContext = new TurnContext(new NotImplementedAdapter(), activity); // Act - var bot = new TestActivityHandler(new ApplicationOptions + var bot = new TestActivityHandler(new TestApplicationOptions { LongRunningMessages = false, StartTypingTimer = false, @@ -737,7 +737,7 @@ public void Test_OnTurnAsync_Enable_LongRunningMessages_Without_Adapter_ShouldTh var turnContext = new TurnContext(new NotImplementedAdapter(), activity); // Act - var exception = Assert.Throws(() => new TestActivityHandler(new ApplicationOptions + var exception = Assert.Throws(() => new TestActivityHandler(new TestApplicationOptions { LongRunningMessages = true, StartTypingTimer = false, @@ -759,7 +759,7 @@ public async Task Test_OnTurnAsync_Enable_LongRunningMessages_Message_Activity() adapterMock.Setup(adapter => adapter.ContinueConversationAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); // Act - var bot = new TestActivityHandler(new ApplicationOptions + var bot = new TestActivityHandler(new TestApplicationOptions { Adapter = adapterMock.Object, BotAppId = "test-bot-app-id", @@ -783,7 +783,7 @@ public async Task Test_OnTurnAsync_Enable_LongRunningMessages_NonMessage_Activit adapterMock.Setup(adapter => adapter.ContinueConversationAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); // Act - var bot = new TestActivityHandler(new ApplicationOptions + var bot = new TestActivityHandler(new TestApplicationOptions { Adapter = adapterMock.Object, BotAppId = "test-bot-app-id", diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/ConversationUpdateActivityTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/ConversationUpdateActivityTests.cs index 36fcf5513..cd6341a48 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/ConversationUpdateActivityTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/ConversationUpdateActivityTests.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.Tests.ActivityHandlerTests { @@ -30,10 +31,10 @@ public async Task Test_ConversationUpdateActivity_One_MemberAdded() Recipient = new ChannelAccount { Id = "b" }, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -57,10 +58,10 @@ public async Task Test_ConversationUpdateActivity_Two_MembersAdded() Recipient = new ChannelAccount { Id = "b" }, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -84,10 +85,10 @@ public async Task Test_ConversationUpdateActivity_One_MemberRemoved() Recipient = new ChannelAccount { Id = "c" }, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -111,10 +112,10 @@ public async Task Test_ConversationUpdateActivity_Two_MembersRemoved() Recipient = new ChannelAccount { Id = "c" }, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -138,10 +139,10 @@ public async Task Test_ConversationUpdateActivity_Bot_MemberAdded() Recipient = new ChannelAccount { Id = "b" }, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -164,10 +165,10 @@ public async Task Test_ConversationUpdateActivity_Bot_MemberRemoved() Recipient = new ChannelAccount { Id = "c" }, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -203,10 +204,10 @@ public async Task Test_ConversationUpdateActivity_BotTeamsMemberAdded() var turnContext = new TurnContext(new SimpleAdapter(), activity); turnContext.TurnState.Add(connectorClient); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -249,10 +250,10 @@ public async Task Test_ConversationUpdateActivity_TeamsMemberAdded() var turnContext = new TurnContext(new SimpleAdapter(), activity); turnContext.TurnState.Add(connectorClient); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -288,10 +289,10 @@ public async Task Test_ConversationUpdateActivity_TeamsMemberAddedNoTeam() var turnContext = new TurnContext(new SimpleAdapter(), activity); turnContext.TurnState.Add(connectorClient); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -365,10 +366,10 @@ public async Task Test_ConversationUpdateActivity_TeamsMemberAddedFullDetailsInE var turnContext = new TurnContext(new SimpleAdapter(), activity); turnContext.TurnState.Add(connectorClient); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -395,10 +396,10 @@ public async Task Test_ConversationUpdateActivity_TeamsMemberRemoved() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -420,10 +421,10 @@ public async Task Test_ConversationUpdateActivity_TeamsChannelCreated() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -444,10 +445,10 @@ public async Task Test_ConversationUpdateActivity_TeamsChannelDeleted() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -468,10 +469,10 @@ public async Task Test_ConversationUpdateActivity_TeamsChannelRenamed() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -492,10 +493,10 @@ public async Task Test_ConversationUpdateActivity_TeamsChannelRestored() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -516,10 +517,10 @@ public async Task Test_ConversationUpdateActivity_TeamsTeamArchived() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -540,10 +541,10 @@ public async Task Test_ConversationUpdateActivity_TeamsTeamDeleted() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -564,10 +565,10 @@ public async Task Test_ConversationUpdateActivity_TeamsTeamHardDeleted() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -588,10 +589,10 @@ public async Task Test_ConversationUpdateActivity_TeamsTeamRenamed() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -612,10 +613,10 @@ public async Task Test_ConversationUpdateActivity_TeamsTeamRestored() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -636,10 +637,10 @@ public async Task Test_ConversationUpdateActivity_TeamsTeamUnarchived() ChannelId = Channels.Msteams, }; var turnContext = new TurnContext(new NotImplementedAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/InvokeActivityNotImplementedTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/InvokeActivityNotImplementedTests.cs index 506c2096a..bf1999134 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/InvokeActivityNotImplementedTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/InvokeActivityNotImplementedTests.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json.Linq; using Xunit; using Microsoft.Bot.Builder.M365.Tests.TestUtils; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.Tests.ActivityHandlerTests { @@ -31,10 +32,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions()); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -71,10 +72,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -111,10 +112,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -142,10 +143,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -173,10 +174,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -204,10 +205,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -235,10 +236,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -266,10 +267,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -300,10 +301,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -334,10 +335,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -365,10 +366,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -395,10 +396,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -425,10 +426,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -456,10 +457,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -487,10 +488,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -527,10 +528,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandlerFileConsent(new ApplicationOptions { }); + var bot = new TestActivityHandlerFileConsent(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -567,10 +568,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandlerFileConsent(new ApplicationOptions { }); + var bot = new TestActivityHandlerFileConsent(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -601,10 +602,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandlerMessagePreview(new ApplicationOptions { }); + var bot = new TestActivityHandlerMessagePreview(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -636,10 +637,10 @@ void CaptureSend(Activity[] arg) var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandlerMessagePreview(new ApplicationOptions { }); + var bot = new TestActivityHandlerMessagePreview(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -676,10 +677,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -710,10 +711,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions { }); + var bot = new TestActivityHandler(new TestApplicationOptions { }); await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -723,42 +724,42 @@ void CaptureSend(Activity[] arg) Assert.Equal(400, ((InvokeResponse)activitiesToSend[0].Value).Status); } - private class TestActivityHandler : Application + private class TestActivityHandler : TestApplication { - public TestActivityHandler(ApplicationOptions options) : base(options) + public TestActivityHandler(TestApplicationOptions options) : base(options) { } } - private class TestActivityHandlerFileConsent : Application + private class TestActivityHandlerFileConsent : TestApplication { - public TestActivityHandlerFileConsent(ApplicationOptions options) : base(options) + public TestActivityHandlerFileConsent(TestApplicationOptions options) : base(options) { } - protected override Task OnFileConsentAcceptAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnFileConsentAcceptAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { return Task.CompletedTask; } - protected override Task OnFileConsentDeclineAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnFileConsentDeclineAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { return Task.CompletedTask; } } - private class TestActivityHandlerMessagePreview : Application + private class TestActivityHandlerMessagePreview : TestApplication { - public TestActivityHandlerMessagePreview(ApplicationOptions options) : base(options) + public TestActivityHandlerMessagePreview(TestApplicationOptions options) : base(options) { } - protected override Task OnMessagingExtensionBotMessagePreviewEditAsync(MessagingExtensionAction action, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionBotMessagePreviewEditAsync(MessagingExtensionAction action, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { return Task.FromResult(new MessagingExtensionActionResponse()); } - protected override Task OnMessagingExtensionBotMessagePreviewSendAsync(MessagingExtensionAction action, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionBotMessagePreviewSendAsync(MessagingExtensionAction action, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { return Task.FromResult(new MessagingExtensionActionResponse()); } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/InvokeActivityTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/InvokeActivityTests.cs index b9271a28a..79c90cef5 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/InvokeActivityTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ActivityHandlerTests/InvokeActivityTests.cs @@ -2,12 +2,8 @@ using Microsoft.Bot.Schema.Teams; using Microsoft.Bot.Schema; using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.Bot.Connector; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.Tests.ActivityHandlerTests { @@ -25,10 +21,10 @@ public async Task Test_InvokeActivity() var adapter = new TestInvokeAdapter(); var turnContext = new TurnContext(adapter, activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -48,10 +44,10 @@ public async Task Test_InvokeActivity_SignInTokenExchangeAsync() Name = SignInConstants.TokenExchangeOperationName, }; var turnContext = new TurnContext(new TestInvokeAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -74,10 +70,10 @@ public async Task Test_InvokeActivity_InvokeShouldNotMatchAsync() }; var adapter = new TestInvokeAdapter(); var turnContext = new TurnContext(adapter, activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -114,10 +110,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -158,10 +154,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -194,10 +190,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -229,10 +225,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -264,10 +260,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -299,10 +295,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -334,10 +330,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -369,10 +365,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -408,10 +404,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -447,10 +443,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -483,10 +479,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -518,10 +514,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -553,10 +549,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -588,10 +584,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -623,10 +619,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -658,10 +654,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -693,10 +689,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -727,10 +723,10 @@ void CaptureSend(Activity[] arg) } var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -759,10 +755,10 @@ public async Task Test_InvokeActivity_OnAdaptiveCardActionExecuteAsync() }; var turnContext = new TurnContext(new TestInvokeAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -780,10 +776,10 @@ public async Task Test_InvokeActivity_OnSearchInvokeAsync_NoKindOnTeamsDefaults( var activity = GetSearchActivity(value); activity.ChannelId = Channels.Msteams; var turnContext = new TurnContext(new TestInvokeAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -800,10 +796,10 @@ public async Task Test_InvokeActivity_OnSearchInvokeAsync() var value = JObject.FromObject(new SearchInvokeValue { Kind = SearchInvokeTypes.Search, QueryText = "bot" }); var activity = GetSearchActivity(value); var turnContext = new TurnContext(new TestInvokeAdapter(), activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert @@ -856,10 +852,10 @@ private async Task AssertErrorThroughInvokeAdapter(Activity activity, string err // Arrange var adapter = new TestInvokeAdapter(); var turnContext = new TurnContext(adapter, activity); - var turnState = new TurnState(); + var turnState = new TestTurnState(); // Act - var bot = new TestActivityHandler(new ApplicationOptions()); + var bot = new TestActivityHandler(new TestApplicationOptions()); var isHandlerImplemented = await bot.RunActivityHandlerAsync(turnContext, turnState, default); // Assert diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ConversationHistoryTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ConversationHistoryTests.cs new file mode 100644 index 000000000..259d996b9 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/ConversationHistoryTests.cs @@ -0,0 +1,525 @@ +using Microsoft.Bot.Builder.M365.AI.Planner; +using Microsoft.Bot.Builder.M365.State; + +namespace Microsoft.Bot.Builder.M365.Tests +{ + public class ConversationHistoryTests + { + [Fact] + public void AddLine_AddsLineToHistory() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line = "This is a line of text"; + int maxLines = 10; + + // Act + ConversationHistory.AddLine(turnState, line, maxLines); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Contains(line, history); + Assert.Single(history); + } + + [Fact] + public void AddLine_PrunesHistoryIfTooLong() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + int maxLines = 10; + var lines = new List(); + for (int i = 0; i < maxLines + 1; i++) + { + lines.Add($"Line {i}"); + } + + // Act + foreach (var line in lines) + { + ConversationHistory.AddLine(turnState, line, maxLines); + } + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Equal(maxLines, history.Count); + Assert.DoesNotContain("Line 0", history); + Assert.Contains("Line 10", history); + } + + [Fact] + public void Clear_RemovesAllLinesFromHistory() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line = "This is a line of text"; + ConversationHistory.AddLine(turnState, line); + + // Act + ConversationHistory.Clear(turnState); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Empty(history); + } + + [Fact] + public void Clear_ThrowsArgumentNullException_WhenTurnStateIsNull() + { + // Arrange + TurnState turnState = null; + + // Act and Assert + Assert.Throws(() => ConversationHistory.Clear(turnState)); + } + + [Fact] + public void Clear_ThrowsInvalidOperationException_WhenTurnStateHasNoConversationState() + { + // Arrange + var turnState = new TurnState(); + + // Act and Assert + Assert.Throws(() => ConversationHistory.Clear(turnState)); + } + + [Fact] + public void HasMoreLines_ReturnsTrueIfHistoryHasOneLine() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line = "This is a line of text"; + ConversationHistory.AddLine(turnState, line); + + // Act + var result = ConversationHistory.HasMoreLines(turnState); + + // Assert + Assert.True(result); + } + + [Fact] + public void HasMoreLines_ReturnsTrueIfHistoryHasMultipleLines() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var lines = new[] { "Line 1", "Line 2", "Line 3" }; + foreach (var line in lines) + { + ConversationHistory.AddLine(turnState, line); + } + + // Act + var result = ConversationHistory.HasMoreLines(turnState); + + // Assert + Assert.True(result); + } + + [Fact] + public void HasMoreLines_ReturnsFalseIfHistoryIsEmpty() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + + // Act + var result = ConversationHistory.HasMoreLines(turnState); + + // Assert + Assert.False(result); + } + + [Fact] + public void HasMoreLines_ThrowsArgumentNullExceptionIfTurnStateIsNull() + { + // Arrange + TurnState turnState = null; + + // Act and Assert + Assert.Throws(() => ConversationHistory.HasMoreLines(turnState)); + } + + [Fact] + public void GetLastLine_ReturnsEmptyStringIfHistoryIsEmpty() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + + // Act + var lastLine = ConversationHistory.GetLastLine(turnState); + + // Assert + Assert.Equal(string.Empty, lastLine); + } + + [Fact] + public void GetLastLine_ReturnsLastLineIfHistoryIsNotEmpty() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "This is the first line"; + var line2 = "This is the second line"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + + // Act + var lastLine = ConversationHistory.GetLastLine(turnState); + + // Assert + Assert.Equal(line2, lastLine); + } + + [Fact] + public void GetLastLine_ThrowsArgumentNullExceptionIfTurnStateIsNull() + { + // Arrange + TurnState turnState = null; + + // Act and Assert + Assert.Throws(() => ConversationHistory.GetLastLine(turnState)); + } + + [Fact] + public void GetLastSay_ReturnsEmptyStringIfHistoryIsEmpty() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + + // Act + var result = ConversationHistory.GetLastSay(turnState); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void GetLastSay_ReturnsLastSayTextIfHistoryHasSayResponse() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "User: Hello"; + var line2 = "Assistant: Hi, how can I help you? SAY Welcome to the assistant."; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + + // Act + var result = ConversationHistory.GetLastSay(turnState); + + // Assert + Assert.Equal("Welcome to the assistant.", result); + } + + [Fact] + public void GetLastSay_ReturnsLastSayTextWithoutDoStatementsIfHistoryHasSayAndDoResponse() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "User: What is the weather like?"; + var line2 = "Assistant: It is sunny and warm. SAY The weather is nice today. THEN DO ShowWeatherCard"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + + // Act + var result = ConversationHistory.GetLastSay(turnState); + + // Assert + Assert.Equal("The weather is nice today.", result); + } + + [Fact] + public void GetLastSay_ReturnsEmptyStringIfHistoryHasNoSayResponse() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "User: How are you?"; + var line2 = "Assistant: DO GreetUser"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + + // Act + var result = ConversationHistory.GetLastSay(turnState); + + // Assert + Assert.Equal(string.Empty, result); + } + + + private TurnState _GetDefaultTurnStateWithConversationState() + { + TurnState state = new() + { + ConversationStateEntry = new TurnStateEntry(new StateBase(), "") + }; + + return state; + } + + [Fact] + public void RemoveLastLine_RemovesLastLineFromHistory() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "This is the first line"; + var line2 = "This is the second line"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + + // Act + var removedLine = ConversationHistory.RemoveLastLine(turnState); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Equal(line2, removedLine); + Assert.Contains(line1, history); + Assert.DoesNotContain(line2, history); + Assert.Equal(1, history.Count); + } + + [Fact] + public void RemoveLastLine_ReturnsNullIfHistoryIsEmpty() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + + // Act + var removedLine = ConversationHistory.RemoveLastLine(turnState); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Null(removedLine); + Assert.Empty(history); + } + + [Fact] + public void RemoveLastLine_ThrowsArgumentNullExceptionIfTurnStateIsNull() + { + // Arrange + TurnState? turnState = null; + + // Act and Assert + Assert.Throws(() => ConversationHistory.RemoveLastLine(turnState)); + } + + [Fact] + public void ReplaceLastLine_ReplacesLastLineOfHistory() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "This is the first line of history"; + var line2 = "This is the second line of history"; + var line3 = "This is the new line of history"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + + // Act + ConversationHistory.ReplaceLastLine(turnState, line3); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Contains(line1, history); + Assert.Contains(line3, history); + Assert.DoesNotContain(line2, history); + Assert.Equal(2, history.Count); + } + + [Fact] + public void ReplaceLastLine_AddsLineIfHistoryIsEmpty() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line = "This is the only line of history"; + + // Act + ConversationHistory.ReplaceLastLine(turnState, line); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Contains(line, history); + Assert.Equal(1, history.Count); + } + + [Fact] + public void ReplaceLastSay_ReplacesLastSayWithNewResponse() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "User: Hello"; + var line2 = "User: I'm fine"; + var line3 = "Assistant: Hi SAY How are you?"; + var newResponse = "That's good to hear"; + var expectedLine = "Assistant: Hi SAY That's good to hear"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + ConversationHistory.AddLine(turnState, line3); + + // Act + ConversationHistory.ReplaceLastSay(turnState, newResponse); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Contains(line1, history); + Assert.Contains(line2, history); + Assert.Contains(expectedLine, history); + Assert.DoesNotContain(line3, history); + Assert.Equal(3, history.Count); + } + + [Fact] + public void ReplaceLastSay_AppendsThenSayIfLastLineHasDo() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "User: What time is it?"; + var line2 = "Assistant: It's 10:00 AM DO Show clock"; + var newResponse = "Do you have an appointment?"; + var expectedLine = "Assistant: It's 10:00 AM DO Show clock THEN SAY Do you have an appointment?"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + + // Act + ConversationHistory.ReplaceLastSay(turnState, newResponse); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Contains(line1, history); + Assert.Contains(expectedLine, history); + Assert.DoesNotContain(line2, history); + Assert.Equal(2, history.Count); + } + + [Fact] + public void ReplaceLastSay_ReplacesEntireLineIfNoSayOrDo() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "User: Tell me a joke"; + var line2 = "Assistant: Why did the chicken cross the road?"; + var newResponse = "To get to the other side"; + var expectedLine = "Assistant: To get to the other side"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + + // Act + ConversationHistory.ReplaceLastSay(turnState, newResponse); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Contains(line1, history); + Assert.Contains(expectedLine, history); + Assert.DoesNotContain(line2, history); + Assert.Equal(2, history.Count); + } + + [Fact] + public void ReplaceLastSay_AddsLineWithPrefixIfHistoryIsEmpty() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var newResponse = "Welcome to the chatbot"; + var expectedLine = "Assistant: Welcome to the chatbot"; + + // Act + ConversationHistory.ReplaceLastSay(turnState, newResponse); + + // Assert + var history = ConversationHistory.GetHistory(turnState); + Assert.Contains(expectedLine, history); + Assert.Equal(1, history.Count); + } + + [Fact] + public void ToString_ReturnsHistoryAsText() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "Hello, how are you?"; + var line2 = "I'm fine, thank you."; + var line3 = "That's good to hear."; + var lineSeparator = "\n"; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + ConversationHistory.AddLine(turnState, line3); + + // Act + var text = ConversationHistory.ToString(turnState, lineSeparator: lineSeparator); + + // Assert + var expectedText = $"{line1}{lineSeparator}{line2}{lineSeparator}{line3}"; + Assert.Equal(expectedText, text); + } + + [Fact] + public void ToString_ReturnsEmptyStringIfHistoryTooLong() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line = "This is a very long line of text that exceeds the maximum number of tokens allowed."; + var maxTokens = 10; + ConversationHistory.AddLine(turnState, line); + + // Act + var text = ConversationHistory.ToString(turnState, maxTokens: maxTokens); + + // Assert + Assert.Equal("", text); + } + + [Fact] + public void ToArray_ReturnsHistoryAsArray() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line1 = "Hello, how are you?"; + var line2 = "I'm fine, thank you."; + var line3 = "That's good to hear."; + ConversationHistory.AddLine(turnState, line1); + ConversationHistory.AddLine(turnState, line2); + ConversationHistory.AddLine(turnState, line3); + + // Act + var array = ConversationHistory.ToArray(turnState); + + // Assert + var expectedArray = new[] { line1, line2, line3 }; + Assert.Equal(expectedArray, array); + } + + [Fact] + public void ToArray_ReturnsEmptyArrayIfHistoryTooLong() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var line = "This is a very long line of text that exceeds the maximum number of tokens allowed."; + var maxTokens = 10; + ConversationHistory.AddLine(turnState, line); + + // Act + var array = ConversationHistory.ToArray(turnState, maxTokens: maxTokens); + + // Assert + Assert.Empty(array); + } + + [Fact] + public void ToArray_SkipLastLineIfItExceedsMaxToken() + { + // Arrange + var turnState = _GetDefaultTurnStateWithConversationState(); + var shortLine = "fits in max tokens"; + var longLine = "This is a very long line of text that exceeds the maximum number of tokens allowed."; + var maxTokens = 10; + ConversationHistory.AddLine(turnState, longLine); + ConversationHistory.AddLine(turnState, shortLine); + + // Act + var array = ConversationHistory.ToArray(turnState, maxTokens: maxTokens); + + // Assert + Assert.Single(array); + Assert.Equal(shortLine, array[0]); + } + + private class ConversationState : StateBase { } + + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/AzureContentSafetyModeratorTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/AzureContentSafetyModeratorTests.cs index 412a0fb44..9c41b5d4c 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/AzureContentSafetyModeratorTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/AzureContentSafetyModeratorTests.cs @@ -8,6 +8,7 @@ using Xunit.Abstractions; using Microsoft.Bot.Builder.M365.AI.Action; using Microsoft.Bot.Schema; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.Tests.Integration { diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/AzureOpenAIPlannerTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/AzureOpenAIPlannerTests.cs index ce8239fe0..891e501ba 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/AzureOpenAIPlannerTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/AzureOpenAIPlannerTests.cs @@ -7,6 +7,8 @@ using Moq; using System.Reflection; using Xunit.Abstractions; +using Microsoft.Bot.Builder.M365.State; +using Microsoft.Bot.Builder.M365.Tests.TestUtils; namespace Microsoft.Bot.Builder.M365.Tests.Integration { @@ -43,12 +45,12 @@ public async Task AzureOpenAIPlanner_CompletePromptAsync_TextCompletion(string p // Arrange var config = _configuration.GetSection("AzureOpenAI").Get(); var options = new AzureOpenAIPlannerOptions(config.ApiKey, config.ModelId, config.Endpoint); - var planner = new AzureOpenAIPlanner(options, _output); - var moderatorMock = new Mock>(); + var planner = new AzureOpenAIPlanner(options, _output); + var moderatorMock = new Mock>(); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( prompt, @@ -84,12 +86,12 @@ public async Task AzureOpenAIPlanner_CompletePromptAsync_ChatCompletion(string p Assert.NotNull(config.ChatModelId); var options = new AzureOpenAIPlannerOptions(config.ApiKey, config.ChatModelId, config.Endpoint); - var planner = new AzureOpenAIPlanner(options, _output); - var moderatorMock = new Mock>(); + var planner = new AzureOpenAIPlanner(options, _output); + var moderatorMock = new Mock>(); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( prompt, @@ -122,12 +124,12 @@ public async Task AzureOpenAIPlanner_CompletePromptAsync_Unauthorized() var invalidApiKey = "invalidApiKey"; var options = new AzureOpenAIPlannerOptions(invalidApiKey, config.ChatModelId, config.Endpoint); - var planner = new AzureOpenAIPlanner(options, _output); - var moderatorMock = new Mock>(); + var planner = new AzureOpenAIPlanner(options, _output); + var moderatorMock = new Mock>(); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", @@ -157,12 +159,12 @@ public async Task AzureOpenAIPlanner_CompletePromptAsync_InvalidModel_InvalidReq Assert.NotNull(config.ApiKey); var options = new AzureOpenAIPlannerOptions(config.ApiKey, "invalidModel", config.Endpoint); - var planner = new AzureOpenAIPlanner(options, _output); - var moderatorMock = new Mock>(); + var planner = new AzureOpenAIPlanner(options, _output); + var moderatorMock = new Mock>(); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/OpenAIModeratorTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/OpenAIModeratorTests.cs index 61d0b33f9..d191cb9f7 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/OpenAIModeratorTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/OpenAIModeratorTests.cs @@ -9,6 +9,7 @@ using System.Reflection; using Xunit.Abstractions; using Microsoft.Bot.Schema; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.Tests.IntegrationTests { diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/OpenAIPlannerTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/OpenAIPlannerTests.cs index acfd629c8..41e45edcc 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/OpenAIPlannerTests.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/IntegrationTests/OpenAIPlannerTests.cs @@ -7,6 +7,8 @@ using Moq; using System.Reflection; using Xunit.Abstractions; +using Microsoft.Bot.Builder.M365.State; +using Microsoft.Bot.Builder.M365.Tests.TestUtils; namespace Microsoft.Bot.Builder.M365.Tests.Integration { @@ -46,12 +48,12 @@ public async Task OpenAIPlanner_CompletePromptAsync_TextCompletion(string prompt Assert.NotNull(config.ChatModelId); var options = new OpenAIPlannerOptions(config.ApiKey, config.ModelId); - var planner = new OpenAIPlanner(options, _output); - var moderatorMock = new Mock>(); + var planner = new OpenAIPlanner(options, _output); + var moderatorMock = new Mock>(); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( prompt, @@ -87,12 +89,12 @@ public async Task OpenAIPlanner_CompletePromptAsync_ChatCompletion(string prompt Assert.NotNull(config.ChatModelId); var options = new OpenAIPlannerOptions(config.ApiKey, config.ChatModelId); - var planner = new OpenAIPlanner(options, _output); - var moderatorMock = new Mock>(); + var planner = new OpenAIPlanner(options, _output); + var moderatorMock = new Mock>(); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( prompt, @@ -125,12 +127,12 @@ public async Task OpenAIPlanner_CompletePromptAsync_Unauthorized() var invalidApiKey = "invalidApiKey"; var options = new OpenAIPlannerOptions(invalidApiKey, config.ChatModelId); - var planner = new OpenAIPlanner(options, _output); - var moderatorMock = new Mock>(); + var planner = new OpenAIPlanner(options, _output); + var moderatorMock = new Mock>(); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", @@ -160,12 +162,12 @@ public async Task OpenAIPlanner_CompletePromptAsync_InvalidModel_InvalidRequest( Assert.NotNull(config.ApiKey); var options = new OpenAIPlannerOptions(config.ApiKey, "invalidModel"); - var planner = new OpenAIPlanner(options, _output); - var moderatorMock = new Mock>(); + var planner = new OpenAIPlanner(options, _output); + var moderatorMock = new Mock>(); - var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); + var aiOptions = new AIOptions(planner, new PromptManager(), moderatorMock.Object); var turnContextMock = new Mock(); - var turnStateMock = new Mock(); + var turnStateMock = new Mock(); var promptTemplate = new PromptTemplate( "prompt", diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/StateTests/TurnStateManagerTests.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/StateTests/TurnStateManagerTests.cs new file mode 100644 index 000000000..f0d6e713e --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/StateTests/TurnStateManagerTests.cs @@ -0,0 +1,182 @@ + +using Microsoft.Bot.Builder.M365.State; +using Microsoft.Bot.Builder.M365.Tests.TestUtils; +using Microsoft.Bot.Schema; +using Moq; + +namespace Microsoft.Bot.Builder.M365.Tests.StateTests +{ + /// + /// TODO: Add tests that enforce required properties. Ex. If a required param/object is null, should throw exception. + /// + public class TurnStateManagerTests + { + [Fact] + public async void Test_LoadState_NoStorageProvided_ShouldInitializeEmptyConstructor() + { + // Arrange + var turnStateManager = new TurnStateManager(); + var turnContext = _createConfiguredTurnContext(); + IStorage storage = null; + + // Act + ApplicationTurnState state = await turnStateManager.LoadStateAsync(storage, turnContext); + + // Assert + Assert.NotNull(state); + Assert.NotNull(state.Conversation); + Assert.NotNull(state.User); + Assert.NotNull(state.Temp); + + Assert.Equal(state.ConversationStateEntry!.Value, new ConversationState()); + } + + [Fact] + public async void Test_LoadState_MockStorageProvided_ShouldPopulateTurnState() + { + // Arrange + var turnStateManager = new TurnStateManager(); + var turnContext = _createConfiguredTurnContext(); + Activity activity = turnContext.Activity; + string channelId = activity.ChannelId; + string botId = activity.Recipient.Id; + string conversationId = activity.Conversation.Id; + string userId = activity.From.Id; + + string conversationKey = $"{channelId}/${botId}/conversations/${conversationId}"; + string userKey = $"{channelId}/${botId}/users/${userId}"; + + var conversationState = new ConversationState(); + var userState = new UserState(); + + Mock storage = new(); + storage.Setup(storage => storage.ReadAsync(new string[] { conversationKey, userKey }, It.IsAny())).Returns(() => + { + IDictionary items = new Dictionary(); + items[conversationKey] = conversationState; + items[userKey] = userState; + return Task.FromResult(items); + }); + + // Act + ApplicationTurnState state = await turnStateManager.LoadStateAsync(storage.Object, turnContext); + + // Assert + storage.Verify(storage => storage.ReadAsync(new string[] { conversationKey, userKey }, It.IsAny())); + Assert.NotNull(state); + Assert.Equal(state.Conversation, conversationState); + Assert.Equal(state.User, userState); + Assert.NotNull(state.Temp); + } + + [Fact] + public async void Test_LoadState_MemoryStorageProvided_ShouldPopulateTurnState() + { + // Arrange + var turnStateManager = new TurnStateManager(); + var turnContext = _createConfiguredTurnContext(); + Activity activity = turnContext.Activity; + string channelId = activity.ChannelId; + string botId = activity.Recipient.Id; + string conversationId = activity.Conversation.Id; + string userId = activity.From.Id; + + string conversationKey = $"{channelId}/${botId}/conversations/${conversationId}"; + string userKey = $"{channelId}/${botId}/users/${userId}"; + + var conversationState = new ConversationState(); + var userState = new UserState(); + + IStorage storage = new MemoryStorage(); + IDictionary items = new Dictionary + { + [conversationKey] = conversationState, + [userKey] = userState + }; + await storage.WriteAsync(items); + + // Act + ApplicationTurnState state = await turnStateManager.LoadStateAsync(storage, turnContext); + + // Assert + Assert.NotNull(state); + Assert.Equal(state.ConversationStateEntry?.Value, conversationState); + Assert.Equal(state.UserStateEntry?.Value, userState); + Assert.NotNull(state.TempStateEntry); + } + + [Fact] + public async void Test_SaveState_SavesChanges() + { + // Arrange + var turnStateManager = new TurnStateManager(); + var turnContext = _createConfiguredTurnContext(); + + var storageKey = "storageKey"; + var stateValue = new ConversationState(); + ApplicationTurnState state = new() + { + ConversationStateEntry = new TurnStateEntry(stateValue, storageKey) + }; + + var storage = new MemoryStorage(); + var stateValueKey = "stateValueKey"; + + // Act + /// Mutate the conversation state to so that the changes are saved. + stateValue[stateValueKey] = "arbitaryString"; + /// Save the state + await turnStateManager.SaveStateAsync(storage, turnContext, state); + /// Load from storage + IDictionary storedItems = await storage.ReadAsync(new string[] { storageKey }, default); + + // Assert + Assert.NotNull(storedItems); + Assert.Equal(state.ConversationStateEntry.Value, storedItems[storageKey]); + } + + [Fact] + public async void Test_SaveState_DeletesChanges() + { + // Arrange + var turnStateManager = new TurnStateManager(); + var turnContext = _createConfiguredTurnContext(); + + var storageKey = "storageKey"; + var stateValue = new ConversationState(); + ApplicationTurnState state = new() + { + ConversationStateEntry = new TurnStateEntry(stateValue, storageKey) + }; + + var storage = new MemoryStorage(); + + // Act + /// Delete conversation state + state.ConversationStateEntry.Delete(); + /// Save the state + await turnStateManager.SaveStateAsync(storage, turnContext, state); + /// Load from storage + IDictionary storedItems = await storage.ReadAsync(new string[] { storageKey }, default); + + // Assert + Assert.NotNull(storedItems); + Assert.Empty(storedItems.Keys); + } + + private TurnContext _createConfiguredTurnContext() + { + return new TurnContext(new NotImplementedAdapter(), new Activity( + channelId: "channelId", + recipient: new(){ Id = "recipientId" }, + conversation: new() { Id = "conversationId" }, + from: new() { Id = "fromId" } + )); + } + + private class ConversationState : StateBase { } + private class UserState : StateBase { } + + private class ApplicationTurnState : TurnState { } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestActivityHandler.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestActivityHandler.cs index da148da8e..59374a2f5 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestActivityHandler.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestActivityHandler.cs @@ -2,126 +2,127 @@ using Microsoft.Bot.Schema.Teams; using Newtonsoft.Json.Linq; using System.Reflection; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.Tests.TestUtils { - internal class TestActivityHandler : Application + internal class TestActivityHandler : TestApplication { - public TestActivityHandler(ApplicationOptions options) : base(options) + public TestActivityHandler(TestApplicationOptions options) : base(options) { } public List Record { get; } = new List(); - protected override Task OnMessageActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessageActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessageActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnMessageUpdateActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessageUpdateActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessageUpdateActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnMessageDeleteActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessageDeleteActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessageDeleteActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnConversationUpdateActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnConversationUpdateActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnConversationUpdateActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMembersAddedAsync(membersAdded, turnContext, turnState, cancellationToken); } - protected override Task OnMembersRemovedAsync(IList membersRemoved, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMembersRemovedAsync(IList membersRemoved, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMembersRemovedAsync(membersRemoved, turnContext, turnState, cancellationToken); } - protected override Task OnMessageReactionActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessageReactionActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessageReactionActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnReactionsAddedAsync(IList messageReactions, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnReactionsAddedAsync(IList messageReactions, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnReactionsAddedAsync(messageReactions, turnContext, turnState, cancellationToken); } - protected override Task OnReactionsRemovedAsync(IList messageReactions, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnReactionsRemovedAsync(IList messageReactions, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnReactionsRemovedAsync(messageReactions, turnContext, turnState, cancellationToken); } - protected override Task OnEventActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnEventActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnEventActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnTokenResponseEventAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTokenResponseEventAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTokenResponseEventAsync(turnContext, turnState, cancellationToken); } - protected override Task OnEventAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnEventAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnEventAsync(turnContext, turnState, cancellationToken); } - protected override Task OnEndOfConversationActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnEndOfConversationActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnEndOfConversationActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnTypingActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTypingActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTypingActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnInstallationUpdateActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnInstallationUpdateActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnInstallationUpdateActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnCommandActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnCommandActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnCommandActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnCommandResultActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnCommandResultActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnCommandResultActivityAsync(turnContext, turnState, cancellationToken); } - protected override Task OnUnrecognizedActivityTypeAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnUnrecognizedActivityTypeAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnUnrecognizedActivityTypeAsync(turnContext, turnState, cancellationToken); } - protected override Task OnInvokeActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnInvokeActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); if (turnContext.Activity.Name == "some.random.invoke") @@ -132,279 +133,279 @@ protected override Task OnInvokeActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnSignInInvokeAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnSignInInvokeAsync(turnContext, turnState, cancellationToken); } - protected override Task OnInstallationUpdateAddAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnInstallationUpdateAddAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnInstallationUpdateAddAsync(turnContext, turnState, cancellationToken); } - protected override Task OnInstallationUpdateRemoveAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnInstallationUpdateRemoveAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnInstallationUpdateRemoveAsync(turnContext, turnState, cancellationToken); } - protected override Task OnAdaptiveCardActionExecuteAsync(AdaptiveCardInvokeValue invokeValue, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnAdaptiveCardActionExecuteAsync(AdaptiveCardInvokeValue invokeValue, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new AdaptiveCardInvokeResponse()); } - protected override Task OnSearchInvokeAsync(SearchInvokeValue invokeValue, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnSearchInvokeAsync(SearchInvokeValue invokeValue, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new SearchInvokeResponse()); } - protected override Task OnChannelCreatedAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnChannelCreatedAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnChannelCreatedAsync(channelInfo, teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnChannelDeletedAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnChannelDeletedAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnChannelDeletedAsync(channelInfo, teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnChannelRenamedAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnChannelRenamedAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnChannelRenamedAsync(channelInfo, teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnChannelRestoredAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnChannelRestoredAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnChannelRestoredAsync(channelInfo, teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnTeamArchivedAsync(TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTeamArchivedAsync(TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTeamArchivedAsync(teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnTeamDeletedAsync(TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTeamDeletedAsync(TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTeamDeletedAsync(teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnTeamHardDeletedAsync(TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTeamHardDeletedAsync(TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTeamHardDeletedAsync(teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnTeamRenamedAsync(TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTeamRenamedAsync(TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTeamRenamedAsync(teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnTeamRestoredAsync(TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTeamRestoredAsync(TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTeamRestoredAsync(teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnTeamUnarchivedAsync(TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTeamUnarchivedAsync(TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTeamUnarchivedAsync(teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnMembersAddedAsync(IList membersAdded, TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMembersAddedAsync(IList membersAdded, TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMembersAddedAsync(membersAdded, teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnMembersRemovedAsync(IList membersRemoved, TeamInfo teamInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMembersRemovedAsync(IList membersRemoved, TeamInfo teamInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMembersRemovedAsync(membersRemoved, teamInfo, turnContext, turnState, cancellationToken); } - protected override Task OnReadReceiptAsync(ReadReceiptInfo readReceiptInfo, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnReadReceiptAsync(ReadReceiptInfo readReceiptInfo, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); turnContext.SendActivityAsync(readReceiptInfo.LastReadMessageId); return base.OnReadReceiptAsync(readReceiptInfo, turnContext, turnState, cancellationToken); } - protected override Task OnFileConsentAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnFileConsentAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnFileConsentAsync(fileConsentCardResponse, turnContext, turnState, cancellationToken); } - protected override Task OnFileConsentAcceptAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnFileConsentAcceptAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.CompletedTask; } - protected override Task OnFileConsentDeclineAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnFileConsentDeclineAsync(FileConsentCardResponse fileConsentCardResponse, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.CompletedTask; } - protected override Task OnO365ConnectorCardActionAsync(O365ConnectorCardActionQuery query, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnO365ConnectorCardActionAsync(O365ConnectorCardActionQuery query, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.CompletedTask; } - protected override Task OnMessagingExtensionBotMessagePreviewEditAsync(MessagingExtensionAction action, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionBotMessagePreviewEditAsync(MessagingExtensionAction action, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionActionResponse()); } - protected override Task OnMessagingExtensionBotMessagePreviewSendAsync(MessagingExtensionAction action, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionBotMessagePreviewSendAsync(MessagingExtensionAction action, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionActionResponse()); } - protected override Task OnMessagingExtensionCardButtonClickedAsync(JObject obj, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionCardButtonClickedAsync(JObject obj, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessagingExtensionCardButtonClickedAsync(obj, turnContext, turnState, cancellationToken); } - protected override Task OnMessagingExtensionFetchTaskAsync(MessagingExtensionAction action, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionFetchTaskAsync(MessagingExtensionAction action, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionActionResponse()); } - protected override Task OnMessagingExtensionConfigurationQuerySettingUrlAsync(MessagingExtensionQuery query, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionConfigurationQuerySettingUrlAsync(MessagingExtensionQuery query, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionResponse()); } - protected override Task OnMessagingExtensionConfigurationSettingAsync(JObject obj, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionConfigurationSettingAsync(JObject obj, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.CompletedTask; } - protected override Task OnMessagingExtensionQueryAsync(MessagingExtensionQuery query, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionQueryAsync(MessagingExtensionQuery query, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionResponse()); } - protected override Task OnMessagingExtensionSelectItemAsync(JObject query, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionSelectItemAsync(JObject query, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionResponse()); } - protected override Task OnMessagingExtensionSubmitActionAsync(MessagingExtensionAction action, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionSubmitActionAsync(MessagingExtensionAction action, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionActionResponse()); } - protected override Task OnMessagingExtensionSubmitActionDispatchAsync(MessagingExtensionAction action, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessagingExtensionSubmitActionDispatchAsync(MessagingExtensionAction action, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessagingExtensionSubmitActionDispatchAsync(action, turnContext, turnState, cancellationToken); } - protected override Task OnAppBasedLinkQueryAsync(AppBasedLinkQuery query, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnAppBasedLinkQueryAsync(AppBasedLinkQuery query, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionResponse()); } - protected override Task OnAnonymousAppBasedLinkQueryAsync(AppBasedLinkQuery query, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnAnonymousAppBasedLinkQueryAsync(AppBasedLinkQuery query, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new MessagingExtensionResponse()); } - protected override Task OnCardActionInvokeAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnCardActionInvokeAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnCardActionInvokeAsync(turnContext, turnState, cancellationToken); } - protected override Task OnTaskModuleFetchAsync(TaskModuleRequest taskModuleRequest, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTaskModuleFetchAsync(TaskModuleRequest taskModuleRequest, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new TaskModuleResponse()); } - protected override Task OnTaskModuleSubmitAsync(TaskModuleRequest taskModuleRequest, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTaskModuleSubmitAsync(TaskModuleRequest taskModuleRequest, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new TaskModuleResponse()); } - protected override Task OnSignInVerifyStateAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnSignInVerifyStateAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.CompletedTask; } - protected override Task OnSignInTokenExchangeAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnSignInTokenExchangeAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.CompletedTask; } - protected override Task OnTabFetchAsync(TabRequest taskModuleRequest, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTabFetchAsync(TabRequest taskModuleRequest, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new TabResponse()); } - protected override Task OnTabSubmitAsync(TabSubmit taskModuleRequest, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnTabSubmitAsync(TabSubmit taskModuleRequest, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return Task.FromResult(new TabResponse()); } - protected override Task OnMeetingStartAsync(MeetingStartEventDetails meeting, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMeetingStartAsync(MeetingStartEventDetails meeting, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); turnContext.SendActivityAsync(meeting.StartTime.ToString()); return base.OnMeetingStartAsync(meeting, turnContext, turnState, cancellationToken); } - protected override Task OnMeetingEndAsync(MeetingEndEventDetails meeting, ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMeetingEndAsync(MeetingEndEventDetails meeting, ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); turnContext.SendActivityAsync(meeting.EndTime.ToString()); return base.OnMeetingEndAsync(meeting, turnContext, turnState, cancellationToken); } - protected override Task OnMessageEditAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessageEditAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessageEditAsync(turnContext, turnState, cancellationToken); } - protected override Task OnMessageUndeleteAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessageUndeleteAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessageUndeleteAsync(turnContext, turnState, cancellationToken); } - protected override Task OnMessageSoftDeleteAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override Task OnMessageSoftDeleteAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnMessageSoftDeleteAsync(turnContext, turnState, cancellationToken); diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestApplication.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestApplication.cs new file mode 100644 index 000000000..aaf061d22 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestApplication.cs @@ -0,0 +1,12 @@ + +namespace Microsoft.Bot.Builder.M365.Tests.TestUtils +{ + public class TestApplication : Application + { + public TestApplication(TestApplicationOptions options) : base(options) + { + } + } + + public class TestApplicationOptions : ApplicationOptions { } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestDelegatingTurnContext.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestDelegatingTurnContext.cs index 7ca58a3cc..2f4574198 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestDelegatingTurnContext.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestDelegatingTurnContext.cs @@ -1,19 +1,14 @@ using Microsoft.Bot.Schema; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.Bot.Builder.M365.Tests.TestUtils { - internal class TestDelegatingTurnContext : Application + internal class TestDelegatingTurnContext : TestApplication { - public TestDelegatingTurnContext(ApplicationOptions options) : base(options) + public TestDelegatingTurnContext(TestApplicationOptions options) : base(options) { } - protected override async Task OnMessageActivityAsync(ITurnContext turnContext, TurnState turnState, CancellationToken cancellationToken) + protected override async Task OnMessageActivityAsync(ITurnContext turnContext, TestTurnState turnState, CancellationToken cancellationToken) { // touch every var activity = turnContext.Activity; diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestTurnState.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestTurnState.cs new file mode 100644 index 000000000..e89788aa8 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestTurnState.cs @@ -0,0 +1,8 @@ +using Microsoft.Bot.Builder.M365.State; + +namespace Microsoft.Bot.Builder.M365.Tests.TestUtils +{ + public class TestTurnState : TurnState + { + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestTurnStateManager.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestTurnStateManager.cs new file mode 100644 index 000000000..858e2421b --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/TestUtils/TestTurnStateManager.cs @@ -0,0 +1,17 @@ +using Microsoft.Bot.Builder.M365.State; + +namespace Microsoft.Bot.Builder.M365.Tests.TestUtils +{ + public class TestTurnStateManager : ITurnStateManager + { + public Task LoadStateAsync(IStorage? storage, ITurnContext turnContext) + { + return Task.FromResult(new TestTurnState()); + } + + public Task SaveStateAsync(IStorage? storage, ITurnContext turnContext, TestTurnState turnState) + { + return Task.FromResult(true); + } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/UtilitiesTest.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/UtilitiesTest.cs index ac62fbb04..c7628753d 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/UtilitiesTest.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365.Tests/UtilitiesTest.cs @@ -5,26 +5,26 @@ namespace Microsoft.Bot.Builder.M365.Tests public class UtilitiesTest { [Fact] - public void Test_Verify_NotNull_IsNull_ShouldThrow() + public void Test_Verify_ParamNotNull_IsNull_ShouldThrow() { // Arrange object? argument = null; // Act - ArgumentNullException ex = Assert.Throws(() => Verify.NotNull(argument, nameof(argument))); + ArgumentNullException ex = Assert.Throws(() => Verify.ParamNotNull(argument, nameof(argument))); // Assert Assert.Equal("Value cannot be null. (Parameter 'argument')", ex.Message); } [Fact] - public void Test_Verify_NotNull_IsNotNull_ShouldNotThrow() + public void Test_Verify_ParamNotNull_IsNotNull_ShouldNotThrow() { // Arrange object? argument = new(); // Act - Verify.NotNull(argument, nameof(argument)); + Verify.ParamNotNull(argument, nameof(argument)); // Assert // Nothing to assert, test passes diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AI.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AI.cs index 8d63b5179..b11494d53 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AI.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AI.cs @@ -6,6 +6,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Bot.Builder.M365.AI.Moderator; using Microsoft.Bot.Builder.M365.Utilities; +using Microsoft.Bot.Builder.M365.State; +using Microsoft.Extensions.Options; namespace Microsoft.Bot.Builder.M365.AI { @@ -17,14 +19,14 @@ namespace Microsoft.Bot.Builder.M365.AI /// generating prompts. It can be used free standing or routed to by the Application object. /// /// Optional. Type of the turn state. - public class AI where TState : TurnState + public class AI where TState : ITurnState { private readonly IActionCollection _actions; private readonly AIOptions _options; public AI(AIOptions options, ILogger? logger = null) { - Verify.NotNull(options, nameof(options)); + Verify.ParamNotNull(options, nameof(options)); _options = options; _actions = new ActionCollection(); @@ -85,8 +87,8 @@ public AI(AIOptions options, ILogger? logger = null) /// public AI RegisterAction(string name, ActionHandler handler, bool allowOverrides = false) { - Verify.NotNull(name, nameof(name)); - Verify.NotNull(handler, nameof(handler)); + Verify.ParamNotNull(name, nameof(name)); + Verify.ParamNotNull(handler, nameof(handler)); if (!_actions.HasAction(name) || allowOverrides) { @@ -113,7 +115,7 @@ public AI RegisterAction(string name, ActionHandler handler, boo /// The current instance object. public AI RegisterAction(ActionEntry action) { - Verify.NotNull(action, nameof(action)); + Verify.ParamNotNull(action, nameof(action)); return RegisterAction(action.Name, action.Handler, action.AllowOverrides); } @@ -126,7 +128,7 @@ public AI RegisterAction(ActionEntry action) /// The current instance object. public AI ImportActions(object instance) { - Verify.NotNull(instance, nameof(instance)); + Verify.ParamNotNull(instance, nameof(instance)); MethodInfo[] methods = instance.GetType() .GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod); @@ -167,8 +169,8 @@ public AI ImportActions(object instance) /// This exception is thrown when an unknown (not DO or SAY) command is predicted. public async Task ChainAsync(ITurnContext turnContext, TState turnState, string? prompt = null, AIOptions? options = null, CancellationToken cancellationToken = default) { - Verify.NotNull(turnContext, nameof(turnContext)); - Verify.NotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(turnContext, nameof(turnContext)); + Verify.ParamNotNull(turnState, nameof(turnState)); AIOptions aIOptions = _ConfigureOptions(options); @@ -184,9 +186,7 @@ public async Task ChainAsync(ITurnContext turnContext, TState turnState, s } } - // TODO: Populate {{$temp.input}} - - // TODO: Populate {{$temp.history}} + _SetTempStateValues(turnState, turnContext, options); // Render the prompt PromptTemplate renderedPrompt = await aIOptions.PromptManager.RenderPrompt(turnContext, turnState, prompt); @@ -213,34 +213,62 @@ public async Task ChainAsync(ITurnContext turnContext, TState turnState, s /// This exception is thrown when an unknown (not DO or SAY) command is predicted. public async Task ChainAsync(ITurnContext turnContext, TState turnState, PromptTemplate prompt, AIOptions? options = null, CancellationToken cancellationToken = default) { - Verify.NotNull(turnContext, nameof(turnContext)); - Verify.NotNull(turnState, nameof(turnState)); - Verify.NotNull(prompt, nameof(prompt)); - - AIOptions aIOptions = _ConfigureOptions(options); + Verify.ParamNotNull(turnContext, nameof(turnContext)); + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(prompt, nameof(prompt)); - // TODO: Populate {{$temp.input}} + AIOptions opts = _ConfigureOptions(options); - // TODO: Populate {{$temp.history}} + _SetTempStateValues(turnState, turnContext, options); // Render the prompt - PromptTemplate renderedPrompt = await aIOptions.PromptManager.RenderPrompt(turnContext, turnState, prompt); + PromptTemplate renderedPrompt = await opts.PromptManager.RenderPrompt(turnContext, turnState, prompt); // Review prompt - Plan? plan = await aIOptions.Moderator.ReviewPrompt(turnContext, turnState, renderedPrompt); + Plan? plan = await opts.Moderator.ReviewPrompt(turnContext, turnState, renderedPrompt); if (plan == null) { // Generate plan - plan = await aIOptions.Planner.GeneratePlanAsync(turnContext, turnState, renderedPrompt, aIOptions, cancellationToken); - plan = await aIOptions.Moderator.ReviewPlan(turnContext, turnState, plan); + plan = await opts.Planner.GeneratePlanAsync(turnContext, turnState, renderedPrompt, opts, cancellationToken); + plan = await opts.Moderator.ReviewPlan(turnContext, turnState, plan); } // Process generated plan bool continueChain = await _actions.GetAction(DefaultActionTypes.PlanReadyActionName)!.Handler(turnContext, turnState, plan); if (continueChain) { - // TODO: Update conversation history + // Update conversation history + if (turnState != null && opts?.History != null && opts.History.TrackHistory) + { + string userPrefix = opts.History!.UserPrefix.Trim(); + string userInput = turnState.Temp!.Input.Trim(); + int doubleMaxTurns = opts.History.MaxTurns * 2; + + ConversationHistory.AddLine(turnState, $"{userPrefix} {userInput}", doubleMaxTurns); + string assisstantPrefix = Options.History!.AssistantPrefix.Trim(); + + switch (opts?.History.AssistantHistoryType) + { + case AssistantHistoryType.Text: + // Extract only the things the assistant has said + string text = string.Join("\n", plan.Commands + .OfType() + .Select(c => c.Response)); + + ConversationHistory.AddLine(turnState, $"{assisstantPrefix}, {text}"); + + break; + + case AssistantHistoryType.PlanObject: + default: + // Embed the plan object to re-enforce the model + // TODO: Add support for XML as well + ConversationHistory.AddLine(turnState, $"{assisstantPrefix} {plan.ToJsonString()}"); + break; + } + + } } for (int i = 0; i < plan.Commands.Count && continueChain; i++) @@ -251,10 +279,16 @@ public async Task ChainAsync(ITurnContext turnContext, TState turnState, P { if (_actions.HasAction(doCommand.Action)) { + DoCommandActionData data = new() + { + PredictedDoCommand = doCommand, + Handler = _actions.GetAction(doCommand.Action).Handler + }; + // Call action handler continueChain = await _actions .GetAction(DefaultActionTypes.DoCommandActionName)! - .Handler(turnContext, turnState, doCommand, doCommand.Action); + .Handler(turnContext, turnState, data, doCommand.Action); } else { @@ -290,9 +324,9 @@ public async Task ChainAsync(ITurnContext turnContext, TState turnState, P /// or threads to receive notice of cancellation. public async Task CompletePromptAsync(ITurnContext turnContext, TState turnState, PromptTemplate promptTemplate, AIOptions? options, CancellationToken cancellationToken) { - Verify.NotNull(turnContext, nameof(turnContext)); - Verify.NotNull(turnState, nameof(turnState)); - Verify.NotNull(promptTemplate, nameof(promptTemplate)); + Verify.ParamNotNull(turnContext, nameof(turnContext)); + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(promptTemplate, nameof(promptTemplate)); // Configure options AIOptions aiOptions = _ConfigureOptions(options); @@ -315,9 +349,9 @@ public async Task CompletePromptAsync(ITurnContext turnContext, TState t /// or threads to receive notice of cancellation. public async Task CompletePromptAsync(ITurnContext turnContext, TState turnState, string name, AIOptions? options, CancellationToken cancellationToken) { - Verify.NotNull(turnContext, nameof(turnContext)); - Verify.NotNull(turnState, nameof(turnState)); - Verify.NotNull(name, nameof(name)); + Verify.ParamNotNull(turnContext, nameof(turnContext)); + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(name, nameof(name)); // Configure options AIOptions aiOptions = _ConfigureOptions(options); @@ -350,7 +384,7 @@ public async Task CompletePromptAsync(ITurnContext turnContext, TState t /// A prompt function. public PromptFunction CreateSemanticFunction(string name, PromptTemplate? template, AIOptions? options) { - Verify.NotNull(name, nameof(name)); + Verify.ParamNotNull(name, nameof(name)); if (template != null) { @@ -363,7 +397,7 @@ public PromptFunction CreateSemanticFunction(string name, PromptTemplate private AIOptions _ConfigureOptions(AIOptions? options) { AIOptions configuredOptions; - + if (options != null) { configuredOptions = options; @@ -377,5 +411,23 @@ private AIOptions _ConfigureOptions(AIOptions? options) return configuredOptions; } + + private void _SetTempStateValues(TState turnState, ITurnContext turnContext, AIOptions? options) + { + TempState? tempState = turnState.Temp; + + if (tempState != null) + { + if (tempState.Input == null || tempState.Input == string.Empty) + { + tempState.Input = turnContext.Activity.Text; + } + + if (tempState.History == null && options?.History != null && options.History.TrackHistory) + { + tempState.History = ConversationHistory.ToString(turnState, options.History.MaxTokens, options.History.LineSeparator); + } + } + } } } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AIOptions.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AIOptions.cs index ad3f4aab1..642dd3a5f 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AIOptions.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AIOptions.cs @@ -1,11 +1,12 @@ using Microsoft.Bot.Builder.M365.AI.Moderator; using Microsoft.Bot.Builder.M365.AI.Planner; using Microsoft.Bot.Builder.M365.AI.Prompt; +using Microsoft.Bot.Builder.M365.State; using Microsoft.Bot.Builder.M365.Utilities; namespace Microsoft.Bot.Builder.M365.AI { - public sealed class AIOptions where TState : TurnState + public sealed class AIOptions where TState : ITurnState { /// /// The planner to use for generating plans. @@ -21,9 +22,9 @@ public sealed class AIOptions where TState : TurnState /// Optional. The moderator to use for moderating input passed to the model and the output /// returned by the model. /// - public IModerator Moderator { get; set; } + public IModerator? Moderator { get; set; } - // TODO: Support PromptTemplate and PromptSelector handler as options + // TODO: potentially support PromptTemplate and PromptSelector handler as options /// /// Optional. The prompt to use for the current turn. /// @@ -50,11 +51,10 @@ public sealed class AIOptions where TState : TurnState /// The moderator to use for moderating input passed to the model and the output /// Optional. The prompt to use for the current turn. /// Optional. The history options to use for the AI system. - public AIOptions(IPlanner planner, PromptManager promptManager, IModerator moderator, string? prompt = null, AIHistoryOptions? history = null) + public AIOptions(IPlanner planner, IPromptManager promptManager, IModerator? moderator = null, string? prompt = null, AIHistoryOptions? history = null) { - Verify.NotNull(planner, nameof(planner)); - Verify.NotNull(promptManager, nameof(promptManager)); - Verify.NotNull(moderator, nameof(moderator)); + Verify.ParamNotNull(planner, nameof(planner)); + Verify.ParamNotNull(promptManager, nameof(promptManager)); Planner = planner; PromptManager = promptManager; diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AITypes.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AITypes.cs index 3c1f5d439..d4e8282a8 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AITypes.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AITypes.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; - + namespace Microsoft.Bot.Builder.M365.AI { public class AITypes { - public const string Plan = "Plan"; + public const string Plan = "plan"; public const string DoCommand = "DO"; public const string SayCommand = "SAY"; } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/ActionCollection.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/ActionCollection.cs index 36b314372..4c6de6c0e 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/ActionCollection.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/ActionCollection.cs @@ -1,7 +1,8 @@ - +using Microsoft.Bot.Builder.M365.State; + namespace Microsoft.Bot.Builder.M365.AI.Action { - public class ActionCollection : IActionCollection where TState : TurnState + public class ActionCollection : IActionCollection where TState : ITurnState { private readonly Dictionary> _actions; diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/ActionEntry.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/ActionEntry.cs index 2211e5ea3..fcf3299f8 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/ActionEntry.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/ActionEntry.cs @@ -1,8 +1,9 @@ -using System.Reflection; +using Microsoft.Bot.Builder.M365.State; +using System.Reflection; namespace Microsoft.Bot.Builder.M365.AI.Action { - public class ActionEntry where TState : TurnState + public class ActionEntry where TState : ITurnState { /// /// The name of the action. diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/DefaultActions.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/DefaultActions.cs index 33a02cff9..ec549daed 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/DefaultActions.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/DefaultActions.cs @@ -1,15 +1,17 @@ using AdaptiveCards; using Microsoft.Bot.Builder.M365.AI.Planner; using Microsoft.Bot.Builder.M365.Exceptions; +using Microsoft.Bot.Builder.M365.State; using Microsoft.Bot.Connector; using Microsoft.Bot.Schema; using Microsoft.Extensions.Logging; namespace Microsoft.Bot.Builder.M365.AI.Action -{ +{ + // Create work item // TODO: Resolve this issue before private preview. Need more research and thinking. How are developers going to use this? // 1. Unused parameters, 2. Making the data parameter type more explicit and not just "object". - public class DefaultActions where TState : TurnState + public class DefaultActions where TState : ITurnState { private readonly ILogger? _logger; diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/DoCommandActionData.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/DoCommandActionData.cs index 5fc67147c..636a1332a 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/DoCommandActionData.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/DoCommandActionData.cs @@ -1,8 +1,9 @@ using Microsoft.Bot.Builder.M365.AI.Planner; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Action { - internal class DoCommandActionData where TState : TurnState + internal class DoCommandActionData where TState : ITurnState { public PredictedDoCommand? PredictedDoCommand { get; set; } public ActionHandler? Handler { get; set; } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/IActionCollection.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/IActionCollection.cs index 623f622bc..d1578ebe0 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/IActionCollection.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Action/IActionCollection.cs @@ -1,10 +1,11 @@ - +using Microsoft.Bot.Builder.M365.State; + namespace Microsoft.Bot.Builder.M365.AI.Action { // TODO: Support different delegate types - public delegate Task ActionHandler(ITurnContext turnContext, TState turnState, object? entities = null, string? action = null) where TState : TurnState; + public delegate Task ActionHandler(ITurnContext turnContext, TState turnState, object? entities = null, string? action = null) where TState : ITurnState; - public interface IActionCollection where TState : TurnState + public interface IActionCollection where TState : ITurnState { /// /// Set an action in the collection. diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AzureContentSafety/AzureContentSafetyClient.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AzureContentSafety/AzureContentSafetyClient.cs index 52cbf8332..ef541a5f7 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AzureContentSafety/AzureContentSafetyClient.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/AzureContentSafety/AzureContentSafetyClient.cs @@ -53,7 +53,6 @@ public virtual async Task ExecuteTextMod string responseJson = await httpResponse.Content.ReadAsStringAsync(); AzureContentSafetyTextAnalysisResponse result = JsonSerializer.Deserialize(responseJson) ?? throw new SerializationException($"Failed to deserialize moderation result response json: {content}"); - Verify.NotNull(result.HateResult); return result; } catch (AzureContentSafetyClientException) diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/AzureContentSafetyModerator.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/AzureContentSafetyModerator.cs index c342c0108..b17178d1c 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/AzureContentSafetyModerator.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/AzureContentSafetyModerator.cs @@ -2,6 +2,7 @@ using Microsoft.Bot.Builder.M365.AI.AzureContentSafety; using Microsoft.Bot.Builder.M365.AI.Planner; using Microsoft.Bot.Builder.M365.AI.Prompt; +using Microsoft.Bot.Builder.M365.State; using Microsoft.Extensions.Logging; namespace Microsoft.Bot.Builder.M365.AI.Moderator @@ -10,7 +11,7 @@ namespace Microsoft.Bot.Builder.M365.AI.Moderator /// An moderator that uses Azure Content Safety API. /// /// - public class AzureContentSafetyModerator: IModerator where TState : TurnState + public class AzureContentSafetyModerator : IModerator where TState : ITurnState { private readonly AzureContentSafetyModeratorOptions _options; private readonly AzureContentSafetyClient _client; @@ -40,13 +41,11 @@ public AzureContentSafetyModerator(AzureContentSafetyModeratorOptions options, I { case ModerationType.Input: case ModerationType.Both: - { - // get input from turnstate - // TODO: when TurnState is implemented, get the user input - string input = turnContext.Activity.Text; + { + string input = turnState.Temp?.Input ?? turnContext.Activity.Text; - return await _HandleTextModeration(input, true); - } + return await _HandleTextModeration(input, true); + } default: break; } @@ -61,22 +60,22 @@ public async Task ReviewPlan(ITurnContext turnContext, TState turnState, P { case ModerationType.Output: case ModerationType.Both: + { + foreach (IPredictedCommand command in plan.Commands) { - foreach (IPredictedCommand command in plan.Commands) + if (command is PredictedSayCommand sayCommand) { - if (command is PredictedSayCommand sayCommand) - { - string output = sayCommand.Response; + string output = sayCommand.Response; - // If plan is flagged it will be replaced - Plan? newPlan = await _HandleTextModeration(output, false); + // If plan is flagged it will be replaced + Plan? newPlan = await _HandleTextModeration(output, false); - return newPlan ?? plan; - } + return newPlan ?? plan; } - - break; } + + break; + } default: break; } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/DefaultModerator.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/DefaultModerator.cs index 53f090670..056dcb164 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/DefaultModerator.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/DefaultModerator.cs @@ -1,9 +1,10 @@ using Microsoft.Bot.Builder.M365.AI.Planner; using Microsoft.Bot.Builder.M365.AI.Prompt; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Moderator { - public class DefaultModerator : IModerator where TState : TurnState + public class DefaultModerator : IModerator where TState : ITurnState { public Task ReviewPlan(ITurnContext turnContext, TState turnState, Plan plan) { @@ -16,4 +17,4 @@ public Task ReviewPlan(ITurnContext turnContext, TState turnState, Plan pl return Task.FromResult(null); } } -} \ No newline at end of file +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/IModerator.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/IModerator.cs index c52bdb949..cc373a66b 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/IModerator.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/IModerator.cs @@ -1,6 +1,7 @@ using Microsoft.Bot.Builder.M365.AI.Planner; using Microsoft.Bot.Builder.M365.AI.Prompt; using Microsoft.Bot.Builder.M365.AI.Action; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Moderator { @@ -8,7 +9,7 @@ namespace Microsoft.Bot.Builder.M365.AI.Moderator /// A moderator is responsible for reviewing and approving AI prompts and plans. /// /// Type of the applications turn state. - public interface IModerator where TState : TurnState + public interface IModerator where TState : ITurnState { /// /// Reviews an incoming utterance and generated prompt before it's sent to the planner. @@ -48,4 +49,4 @@ public interface IModerator where TState : TurnState /// The plan to execute. Either the current plan passed in for review or a new plan. Task ReviewPlan(ITurnContext turnContext, TState turnState, Plan plan); } -} \ No newline at end of file +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/OpenAIModerator.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/OpenAIModerator.cs index 61d1694f0..e2132be8e 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/OpenAIModerator.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Moderator/OpenAIModerator.cs @@ -4,6 +4,7 @@ using Microsoft.Bot.Builder.M365.Exceptions; using Microsoft.Bot.Builder.M365.OpenAI; using Microsoft.Extensions.Logging; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Moderator { @@ -11,7 +12,7 @@ namespace Microsoft.Bot.Builder.M365.AI.Moderator /// An moderator that uses OpenAI's moderation API. /// /// - public class OpenAIModerator : IModerator where TState : TurnState + public class OpenAIModerator : IModerator where TState : ITurnState { private readonly OpenAIModeratorOptions _options; private readonly OpenAIClient _client; @@ -41,13 +42,11 @@ public OpenAIModerator(OpenAIModeratorOptions options, ILogger? logger = null, H { case ModerationType.Input: case ModerationType.Both: - { - // get input from turnstate - // TODO: when TurnState is implemented, get the user input - string input = turnContext.Activity.Text; + { + string input = turnState.Temp?.Input ?? turnContext.Activity.Text; - return await _HandleTextModeration(input, true); - } + return await _HandleTextModeration(input, true); + } default: break; } @@ -97,7 +96,7 @@ public async Task ReviewPlan(ITurnContext turnContext, TState turnState, P if (result.Flagged) { string actionName = isModelInput ? DefaultActionTypes.FlaggedInputActionName : DefaultActionTypes.FlaggedOutputActionName; - + // Flagged return new Plan() { diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/OpenAI/OpenAIClient.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/OpenAI/OpenAIClient.cs index e3b6d6d71..d1ee75056 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/OpenAI/OpenAIClient.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/OpenAI/OpenAIClient.cs @@ -106,7 +106,7 @@ private async Task _ExecutePostRequest(string url, HttpCont HttpStatusCode statusCode = response.StatusCode; string failureReason = response.ReasonPhrase; - throw new OpenAIClientException($"HTTP response failure status code: ${statusCode} ({failureReason})", statusCode); + throw new OpenAIClientException($"HTTP response failure status code: {(int)statusCode} ({failureReason})", statusCode); } catch (Exception e) diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/AzureOpenAIPlanner.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/AzureOpenAIPlanner.cs index 5db9adf7d..5dc99c18f 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/AzureOpenAIPlanner.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/AzureOpenAIPlanner.cs @@ -3,6 +3,7 @@ using Microsoft.SemanticKernel.AI.TextCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextCompletion; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Planner { @@ -11,7 +12,7 @@ namespace Microsoft.Bot.Builder.M365.AI.Planner /// /// Type of the applications turn state public class AzureOpenAIPlanner : OpenAIPlanner - where TState : TurnState + where TState : ITurnState { public AzureOpenAIPlanner(AzureOpenAIPlannerOptions options, ILogger logger) : base(options, logger) { diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/AzureOpenAIPlannerOptions.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/AzureOpenAIPlannerOptions.cs index aca683b2f..b41c46ea3 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/AzureOpenAIPlannerOptions.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/AzureOpenAIPlannerOptions.cs @@ -22,7 +22,7 @@ public class AzureOpenAIPlannerOptions : OpenAIPlannerOptions /// The default model to use. public AzureOpenAIPlannerOptions(string apiKey, string defaultModel, string endpoint) : base(apiKey, defaultModel) { - Verify.NotNull(endpoint, nameof(endpoint)); + Verify.ParamNotNull(endpoint, nameof(endpoint)); Endpoint = endpoint; } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/ConversationHistory.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/ConversationHistory.cs new file mode 100644 index 000000000..8c409f415 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/ConversationHistory.cs @@ -0,0 +1,344 @@ +using Microsoft.Bot.Builder.M365.State; +using Microsoft.Bot.Builder.M365.Utilities; +using Microsoft.SemanticKernel.Connectors.AI.OpenAI.Tokenizers; +using System.Text; + +namespace Microsoft.Bot.Builder.M365.AI.Planner +{ + public class ConversationHistory + { + public static readonly string StatePropertyName = "__history__"; + + /// + /// Adds a new line of text to conversation history + /// + /// Applications turn state. + /// Line of text to add to history. + /// Optional. Maximum number of lines to store. Defaults to 10. + public static void AddLine(ITurnState turnState, string line, int maxLines = 10) + { + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(line, nameof(line)); + + _VerifyConversationState(turnState); + + // Create history array if it doesn't exist + List history = GetHistory(turnState); + + // Add line to history + history.Add(line); + + // Prune history if too long + if (history.Count > maxLines) + { + history.RemoveRange(0, history.Count - maxLines); + } + + // Save history back to conversation state + _SetHistory(turnState, history); + } + + public static void AppendToLastLine(ITurnState turnState, string text) + { + string line = GetLastLine(turnState); + ReplaceLastLine(turnState, line + text); + } + + /// + /// Clears all conversation history for the current conversation. + /// + /// Applications turn state. + public static void Clear(ITurnState turnState) + { + Verify.ParamNotNull(turnState, nameof(turnState)); + _VerifyConversationState(turnState); + + _SetHistory(turnState, new List()); + } + + /// + /// Checks to see if one or more lines of history has persisted. + /// + /// Applications turn state. + /// True if there are 1 or more lines of history. + public static bool HasMoreLines(ITurnState turnState) + { + Verify.ParamNotNull(turnState, nameof(turnState)); + _VerifyConversationState(turnState); + + List history = GetHistory(turnState); + return history.Count > 0; + } + + /// + /// Returns the last line of history. + /// + /// Applications turn state. + /// The last line of history or an empty string. + public static string GetLastLine(ITurnState turnState) + { + Verify.ParamNotNull(turnState, nameof(turnState)); + _VerifyConversationState(turnState); + + List history = GetHistory(turnState); + return history.Count > 0 ? history[history.Count - 1] : string.Empty; + } + + /// + /// Searches the history to find the last SAY response from the assistant. + /// + /// Applications turn state. + /// Last thing said by the assistant. Defaults to an empty string. + public static string GetLastSay(ITurnState turnState) + { + // Find start of text + string lastLine = GetLastLine(turnState); + int textPos = lastLine.LastIndexOf(" SAY "); + if (textPos >= 0) + { + textPos += 5; + } + else + { + // Find end of prefix + textPos = lastLine.IndexOf(": "); + if (textPos >= 0) + { + textPos += 2; + } + else + { + // Just use whole text + textPos = 0; + } + } + + // Trim off any DO statements + string text = lastLine.Substring(textPos); + int doPos = text.IndexOf(" THEN DO "); + if (doPos >= 0) + { + text = text.Substring(0, doPos); + } + else + { + doPos = text.IndexOf(" DO "); + if (doPos >= 0) + { + text = text.Substring(0, doPos); + } + } + + return text.IndexOf("DO ") < 0 ? text.Trim() : string.Empty; + } + + /// + /// Removes the last line from the conversation history. + /// + /// Applications turn state. + /// The removed line or null. + public static string? RemoveLastLine(ITurnState turnState) + { + Verify.ParamNotNull(turnState, nameof(turnState)); + + // Get history array + List history = GetHistory(turnState); + + if (history.Count < 1) + { + return null; + } + + // Remove last line + string? line = history[history.Count - 1]; + history.RemoveAt(history.Count - 1); + + // Save history back to conversation state + _SetHistory(turnState, history); + + // Return removed line + return line; + } + + /// + /// Replaces the last line of history with a new line. + /// + /// Applications turn state. + /// New line of history. + public static void ReplaceLastLine(ITurnState turnState, string line) + { + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(line, nameof(line)); + + // Get history array + List history = GetHistory(turnState); + + // Replace the last line or add a new one + if (history.Count > 0) + { + history[history.Count - 1] = line; + } + else + { + history.Add(line); + } + + // Save history back to conversation state + _SetHistory(turnState, history); + } + + + /// + /// Replaces the last line's SAY with a new response. + /// + /// Applications turn state. + /// New response from the assistant. + /// Prefix for when a new line needs to be inserted. Defaults to 'Assistant:'. + public static void ReplaceLastSay(ITurnState turnState, string newResponse, string assistantPrefix = "Assistant:") + { + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(newResponse, nameof(newResponse)); + + // Get history array if it exists + List history = GetHistory(turnState); + + // Update the last line or add a new one + if (history.Count > 0) + { + string line = history[history.Count - 1]; + int lastSayPos = line.LastIndexOf(" SAY "); + if (lastSayPos >= 0 && line.IndexOf(" DO ", lastSayPos) < 0) + { + // We found the last SAY and it wasn't followed by a DO + history[history.Count - 1] = $"{line.Substring(0, lastSayPos)} SAY {newResponse}"; + } + else if (line.IndexOf(" DO ") >= 0) + { + // Append a THEN SAY after the DO's + history[history.Count - 1] = $"{line} THEN SAY {newResponse}"; + } + else + { + // Just replace the entire line + history[history.Count - 1] = $"{assistantPrefix.Trim()} {newResponse}"; + } + } + else + { + history.Add($"{assistantPrefix.Trim()} {newResponse}"); + } + + // Save history back to conversation state + _SetHistory(turnState, history); + } + + /// + /// Returns the current conversation history as a string of text. + /// + /// + /// The length of the returned text is gated by and only whole lines of + /// history entries will be returned. That means that if the length of the most recent history + /// entry is greater then , no text will be returned. + /// + /// Application's turn state. + /// Optional. Maximum length of the text returned. Defaults to 1000 tokens. + /// Optional. Separator used between lines. Defaults to '\n'. + /// The most recent lines of conversation history as a text string. + public static string ToString(ITurnState turnState, int maxTokens = 1000, string lineSeparator = "\n") + { + Verify.ParamNotNull(turnState, nameof(turnState)); + + // Get history array if it exists + List history = GetHistory(turnState); + + // Populate up to max chars + StringBuilder text = new(); + int textTokens = 0; + int lineSeparatorTokens = GPT3Tokenizer.Encode(lineSeparator).Count; + for (int i = history.Count - 1; i >= 0; i--) + { + // Ensure that adding line won't go over the max character length + string line = history[i]; + int lineTokens = GPT3Tokenizer.Encode(line).Count; + int newTextTokens = textTokens + lineTokens + lineSeparatorTokens; + if (newTextTokens > maxTokens) + { + break; + } + + // Prepend line to output + text.Insert(0, $"{line}{lineSeparator}"); + textTokens = newTextTokens; + } + + return text.ToString().Trim(); + } + + /// + /// Returns the current conversation history as an array of lines. + /// + /// The Application's turn state. + /// Optional. Maximum length of the text to include. Defaults to 1000 tokens. + /// The most recent lines of conversation history as an array. + public static string[] ToArray(ITurnState turnState, int maxTokens = 1000) + { + Verify.ParamNotNull(turnState, nameof(turnState)); + + // Get history array if it exists + List history = GetHistory(turnState); + + // Populate up to max chars + int textTokens = 0; + List lines = new(); + for (int i = history.Count - 1; i >= 0; i--) + { + // Ensure that adding line won't go over the max character length + string line = history[i]; + int lineTokens = GPT3Tokenizer.Encode(line).Count; + int newTextTokens = textTokens + lineTokens; + if (newTextTokens > maxTokens) + { + break; + } + + // Prepend line to output + textTokens = newTextTokens; + lines.Insert(0, line); + } + + return lines.ToArray(); + } + + /// + /// Gets the conversation history from the turn state object. + /// + /// The application turn state + /// The coversation history + public static List GetHistory(ITurnState turnState) + { + if (turnState.Conversation != null && turnState.Conversation.TryGetValue(StatePropertyName, out object history)) + { + if (history is List historyList) + { + return historyList; + } + }; + + return new List(); + } + + + private static void _VerifyConversationState(ITurnState turnState) + { + if (turnState.Conversation == null) + { + throw new ArgumentException("The passed turn state object has a null `ConversationState` property"); + } + } + + private static void _SetHistory(ITurnState turnState, List newHistory) + { + turnState.Conversation?.Set(StatePropertyName, newHistory); + } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/IPlanner.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/IPlanner.cs index 063da3717..7c791fecd 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/IPlanner.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/IPlanner.cs @@ -1,11 +1,12 @@ using Microsoft.Bot.Builder.M365.AI.Prompt; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Planner { /// /// A planner is responsible for generating a plan that the AI system will execute. /// - public interface IPlanner where TState : TurnState + public interface IPlanner where TState : ITurnState { /// /// Completes a prompt without returning a plan. @@ -29,4 +30,4 @@ public interface IPlanner where TState : TurnState /// The plan that was generated. Task GeneratePlanAsync(ITurnContext turnContext, TState turnState, PromptTemplate promptTemplate, AIOptions options, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/OpenAIPlanner.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/OpenAIPlanner.cs index ce35eb6b7..1617e149a 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/OpenAIPlanner.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/OpenAIPlanner.cs @@ -11,6 +11,7 @@ using Microsoft.SemanticKernel.SemanticFunctions; using AIException = Microsoft.SemanticKernel.AI.AIException; using PromptTemplate = Microsoft.Bot.Builder.M365.AI.Prompt.PromptTemplate; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Planner { @@ -23,7 +24,7 @@ namespace Microsoft.Bot.Builder.M365.AI.Planner /// use the chatCompletion API, otherwise the textCompletion API will be used. /// public class OpenAIPlanner : IPlanner - where TState : TurnState + where TState : ITurnState where TOptions : OpenAIPlannerOptions { private TOptions _options { get; } @@ -54,7 +55,7 @@ public OpenAIPlanner(TOptions options, ILogger? logger = null) private protected virtual ITextCompletion _CreateTextCompletionService(TOptions options) { - Verify.NotNull(options, nameof(options)); + Verify.ParamNotNull(options, nameof(options)); return new OpenAITextCompletion( options.DefaultModel, @@ -66,7 +67,7 @@ private protected virtual ITextCompletion _CreateTextCompletionService(TOptions private protected virtual IChatCompletion _CreateChatCompletionService(TOptions options) { - Verify.NotNull(options, nameof(options)); + Verify.ParamNotNull(options, nameof(options)); return new OpenAIChatCompletion( options.DefaultModel, @@ -78,9 +79,9 @@ private protected virtual IChatCompletion _CreateChatCompletionService(TOptions /// public virtual async Task CompletePromptAsync(ITurnContext turnContext, TState turnState, PromptTemplate promptTemplate, AIOptions options, CancellationToken cancellationToken = default) { - Verify.NotNull(turnContext, nameof(turnContext)); - Verify.NotNull(turnState, nameof(turnState)); - Verify.NotNull(options, nameof(options)); + Verify.ParamNotNull(turnContext, nameof(turnContext)); + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(options, nameof(options)); string model = _GetModel(promptTemplate); string result; @@ -97,8 +98,10 @@ public virtual async Task CompletePromptAsync(ITurnContext turnContext, if (isChatCompletion) { + string? userMessage = turnState?.Temp?.Input; + // Request base chat completion - IChatResult response = await _CreateChatCompletion(turnState, options, promptTemplate, cancellationToken); + IChatResult response = await _CreateChatCompletion(turnState, options, promptTemplate, userMessage, cancellationToken); ChatMessageBase message = await response.GetChatMessageAsync(cancellationToken).ConfigureAwait(false); CompletionsUsage usage = ((ITextResult)response).ModelResult.GetOpenAIChatResult().Usage; @@ -133,9 +136,9 @@ public virtual async Task CompletePromptAsync(ITurnContext turnContext, /// public async Task GeneratePlanAsync(ITurnContext turnContext, TState turnState, PromptTemplate promptTemplate, AIOptions options, CancellationToken cancellationToken = default) { - Verify.NotNull(turnContext, nameof(turnContext)); - Verify.NotNull(turnState, nameof(turnState)); - Verify.NotNull(options, nameof(options)); + Verify.ParamNotNull(turnContext, nameof(turnContext)); + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(options, nameof(options)); string result; try @@ -164,14 +167,25 @@ public async Task GeneratePlanAsync(ITurnContext turnContext, TState turnS result = result.Substring(5); } - // TODO: Remove response prefix - once Conversation History & TurnState is ported + string? assistantPrefix = options.History?.AssistantPrefix; + + if (assistantPrefix != null) + { + // The model sometimes predicts additional text for the human side of things so skip that. + int position = result.ToLower().IndexOf(assistantPrefix.ToLower()); + if (position >= 0) + { + result = result.Substring(position + assistantPrefix.Length); + } + + } // Parse response into commands Plan? plan; try { plan = ResponseParser.ParseResponse(result.Trim()); - Verify.NotNull(plan, nameof(plan)); + Verify.ParamNotNull(plan, nameof(plan)); } catch (Exception ex) { throw new PlannerException($"Failed to generate plan from model response: {ex.Message}", ex); @@ -205,7 +219,7 @@ public async Task GeneratePlanAsync(ITurnContext turnContext, TState turnS private async Task _CreateTextCompletion(PromptTemplate promptTemplate, CancellationToken cancellationToken) { - Verify.NotNull(promptTemplate, nameof(promptTemplate)); + Verify.ParamNotNull(promptTemplate, nameof(promptTemplate)); var skPromptTemplate = promptTemplate.Configuration.GetPromptTemplateConfig(); @@ -215,11 +229,11 @@ private async Task _CreateTextCompletion(PromptTemplate promptTempl return completions[0]; } - private async Task _CreateChatCompletion(TState turnState, AIOptions options, PromptTemplate promptTemplate, CancellationToken cancellationToken) + private async Task _CreateChatCompletion(TState turnState, AIOptions aiOptions, PromptTemplate promptTemplate, string? userMessage, CancellationToken cancellationToken) { - Verify.NotNull(turnState, nameof(turnState)); - Verify.NotNull(options, nameof(options)); - Verify.NotNull(promptTemplate, nameof(promptTemplate)); + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(aiOptions, nameof(aiOptions)); + Verify.ParamNotNull(promptTemplate, nameof(promptTemplate)); PromptTemplateConfig templateConfig = promptTemplate.Configuration.GetPromptTemplateConfig(); ChatRequestSettings chatRequestSettings = new() @@ -234,12 +248,47 @@ private async Task _CreateChatCompletion(TState turnState, AIOption var chatCompletion = _kernel.GetService(); - // TODO: When turn state is implemented inject history var chatHistory = chatCompletion.CreateNewChat(); - // TODO: When turn state is implemented inject history - // Users message - chatHistory.AddUserMessage(promptTemplate.Text); + + if (_options.UseSystemMessage) + { + chatHistory.AddSystemMessage(promptTemplate.Text); + } else + { + chatHistory.AddUserMessage(promptTemplate.Text); + } + + // Populate Conversation History + if (aiOptions.History != null && aiOptions.History.TrackHistory) + { + string userPrefix = aiOptions.History.UserPrefix; + string assistantPrefix = aiOptions.History.AssistantPrefix; + string[] history = ConversationHistory.ToArray(turnState, aiOptions.History.MaxTokens); + + for (int i = 0; i < history.Length; i++) + { + string line = history[i]; + if (line.StartsWith(userPrefix, StringComparison.OrdinalIgnoreCase)) + { + line = line.Substring(userPrefix.Length).Trim(); + + chatHistory.AddUserMessage(line); + } + else if (line.StartsWith(assistantPrefix, StringComparison.OrdinalIgnoreCase)) + { + line = line.Substring(assistantPrefix.Length).Trim(); + + chatHistory.AddAssistantMessage(line); + } + } + } + + // Add user message + if (userMessage != null) + { + chatHistory.AddUserMessage(userMessage); + } var completions = await chatCompletion.GetChatCompletionsAsync(chatHistory, chatRequestSettings, cancellationToken); @@ -258,4 +307,12 @@ private string _GetModel(PromptTemplate promptTemplate) } } + + /// + public class OpenAIPlanner : OpenAIPlanner where TState : ITurnState + { + public OpenAIPlanner(OpenAIPlannerOptions options, ILogger? logger = null) : base(options, logger) + { + } + } } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/OpenAIPlannerOptions.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/OpenAIPlannerOptions.cs index 5de756d38..e8d2cf926 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/OpenAIPlannerOptions.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/OpenAIPlannerOptions.cs @@ -44,7 +44,7 @@ public class OpenAIPlannerOptions /// The planner currently uses the 'user' role by default as this tends to generate more reliable instruction following. /// Defaults to false. /// - public bool? UseSystemMessage { get; set; } + public bool UseSystemMessage { get; set; } = false; /// /// A flag indicating if the planner should log requests with the provided logger. @@ -63,8 +63,8 @@ public class OpenAIPlannerOptions /// The default model to use. public OpenAIPlannerOptions(string apiKey, string defaultModel) { - Verify.NotNull(apiKey, nameof(apiKey)); - Verify.NotNull(defaultModel, nameof(defaultModel)); + Verify.ParamNotNull(apiKey, nameof(apiKey)); + Verify.ParamNotNull(defaultModel, nameof(defaultModel)); ApiKey = apiKey; DefaultModel = defaultModel; diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/Plan.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/Plan.cs index 163573d78..f88bf0c1f 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/Plan.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Planner/Plan.cs @@ -1,10 +1,8 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.Bot.Builder.M365.AI.Planner { + // TODO: Move from Newtonsoft.Json to System.Text.Json public class Plan { /// @@ -29,5 +27,15 @@ public Plan(List commands) { Commands = commands; } + + /// + /// Returns a Json string representation of the plan. + /// + public string ToJsonString() + { + // TODO: Optimize + return JsonConvert.SerializeObject(this); + } + } } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/IPromptManager.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/IPromptManager.cs index fe337f2cf..9be0f34a5 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/IPromptManager.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/IPromptManager.cs @@ -1,7 +1,8 @@ - +using Microsoft.Bot.Builder.M365.State; + namespace Microsoft.Bot.Builder.M365.AI.Prompt { - public interface IPromptManager where TState : TurnState + public interface IPromptManager where TState : ITurnState { /// /// Adds a custom function to the prompt manager. diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/PromptManager.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/PromptManager.cs index 996e39a22..04cc609e7 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/PromptManager.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/PromptManager.cs @@ -3,16 +3,18 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.TemplateEngine; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Prompt { - public class PromptManager : IPromptManager where TState : TurnState + public class PromptManager : IPromptManager where TState : ITurnState { private string? _promptsFolder; private readonly Dictionary _templates; private readonly Dictionary> _functions; + private readonly Dictionary _promptVariables; - public PromptManager(string? promptsFolder = null) + public PromptManager(string? promptsFolder = null, Dictionary? promptVariables = null) { if (promptsFolder != null) { @@ -20,15 +22,25 @@ public PromptManager(string? promptsFolder = null) _promptsFolder = promptsFolder; } + _promptVariables = promptVariables ?? new Dictionary(); + _templates = new Dictionary(); _functions = new Dictionary>(); } + /// + /// Register prompt variables. + /// + /// + /// You will be able to reference these variables in the prompt template string by using this format: `{{ $key }}`. + /// + public IDictionary Variables => _promptVariables; + /// public IPromptManager AddFunction(string name, PromptFunction promptFunction, bool allowOverrides = false) { - Verify.NotNull(name, nameof(name)); - Verify.NotNull(promptFunction, nameof(promptFunction)); + Verify.ParamNotNull(name, nameof(name)); + Verify.ParamNotNull(promptFunction, nameof(promptFunction)); if (!_functions.ContainsKey(name) || allowOverrides) { @@ -53,8 +65,8 @@ public IPromptManager AddFunction(string name, PromptFunction pr /// public IPromptManager AddPromptTemplate(string name, PromptTemplate promptTemplate) { - Verify.NotNull(name, nameof(name)); - Verify.NotNull(promptTemplate, nameof(promptTemplate)); + Verify.ParamNotNull(name, nameof(name)); + Verify.ParamNotNull(promptTemplate, nameof(promptTemplate)); if (_templates.ContainsKey(name)) { @@ -69,9 +81,9 @@ public IPromptManager AddPromptTemplate(string name, PromptTemplate prom /// public Task InvokeFunction(ITurnContext turnContext, TState turnState, string name) { - Verify.NotNull(turnContext, nameof(turnContext)); - Verify.NotNull(turnState, nameof(turnState)); - Verify.NotNull(name, nameof(name)); + Verify.ParamNotNull(turnContext, nameof(turnContext)); + Verify.ParamNotNull(turnState, nameof(turnState)); + Verify.ParamNotNull(name, nameof(name)); if (_functions.TryGetValue(name, out TemplateFunctionEntry value)) { @@ -86,7 +98,7 @@ public Task InvokeFunction(ITurnContext turnContext, TState turnState, s /// public PromptTemplate LoadPromptTemplate(string name) { - Verify.NotNull(name, nameof(name)); + Verify.ParamNotNull(name, nameof(name)); if (_templates.TryGetValue(name, out PromptTemplate template)) { @@ -157,7 +169,6 @@ internal void RegisterFunctionsIntoKernel(IKernel kernel, ITurnContext turnConte } } - /// TODO: Update this once turn state infrastructure is implemented /// /// Loads value from turn context and turn state into context variables. /// @@ -166,8 +177,18 @@ internal void RegisterFunctionsIntoKernel(IKernel kernel, ITurnContext turnConte /// Variables that could be injected into the prompt template internal void LoadStateIntoContext(SKContext context, ITurnContext turnContext, TState turnState) { - context["input"] = turnContext.Activity.Text; - // TODO: Load turn state 'temp' values into the context + foreach (KeyValuePair pair in _promptVariables) + { + context.Variables.Set(pair.Key, pair.Value); + } + + // Temp state values override the user configured variables + if (turnState as object is TurnState defaultTurnState) + { + context[TempState.OutputKey] = defaultTurnState.Temp?.Output ?? string.Empty; + context[TempState.InputKey] = defaultTurnState.Temp?.Input ?? turnContext.Activity.Text; + context[TempState.HistoryKey] = defaultTurnState.Temp?.History ?? string.Empty; + } } private PromptTemplate _LoadPromptTemplateFromFile(string name) diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/PromptTemplate.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/PromptTemplate.cs index c025172ca..f924c4703 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/PromptTemplate.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/PromptTemplate.cs @@ -10,8 +10,8 @@ public class PromptTemplate public PromptTemplate(string text, PromptTemplateConfiguration configuration) { - Verify.NotNull(text, nameof(text)); - Verify.NotNull(configuration, nameof(configuration)); + Verify.ParamNotNull(text, nameof(text)); + Verify.ParamNotNull(configuration, nameof(configuration)); Text = text; Configuration = configuration; diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/SKFunctionWrapper.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/SKFunctionWrapper.cs index 8a54341d1..eb3d574ff 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/SKFunctionWrapper.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/SKFunctionWrapper.cs @@ -4,10 +4,11 @@ using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Security; using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365.AI.Prompt { - internal class SKFunctionWrapper : ISKFunction where TState : TurnState + internal class SKFunctionWrapper : ISKFunction where TState : ITurnState { // TODO: This is a hack around to get the default skill name from SK's internal implementation. We need to fix this. public const string DefaultSkill = "_GLOBAL_FUNCTIONS_"; diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/TemplateFunctionEntry.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/TemplateFunctionEntry.cs index a5bfb299e..8aca41960 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/TemplateFunctionEntry.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/AI/Prompt/TemplateFunctionEntry.cs @@ -1,8 +1,10 @@ -namespace Microsoft.Bot.Builder.M365.AI.Prompt +using Microsoft.Bot.Builder.M365.State; + +namespace Microsoft.Bot.Builder.M365.AI.Prompt { - public delegate Task PromptFunction(ITurnContext turnContext, TState turnState) where TState : TurnState; + public delegate Task PromptFunction(ITurnContext turnContext, TState turnState) where TState : ITurnState; - internal class TemplateFunctionEntry where TState : TurnState + internal class TemplateFunctionEntry where TState : ITurnState { internal PromptFunction Handler; diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Application.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Application.cs index 8f2fbcc66..28c9c95b4 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Application.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Application.cs @@ -7,8 +7,8 @@ using Microsoft.Bot.Schema.Teams; using Microsoft.Bot.Connector; using Microsoft.Bot.Builder.Teams; -using Microsoft.Extensions.Logging; using Microsoft.Bot.Builder.M365.Utilities; +using Microsoft.Bot.Builder.M365.State; namespace Microsoft.Bot.Builder.M365 { @@ -24,7 +24,9 @@ namespace Microsoft.Bot.Builder.M365 /// bots that leverage Large Language Models (LLM) and other AI capabilities. /// /// Type of the turn state. This allows for strongly typed access to the turn state. - public class Application : IBot where TState : TurnState + public class Application : IBot + where TState : ITurnState + where TTurnStateManager : ITurnStateManager, new() { private readonly AI? _ai; private readonly int _typingTimerDelay = 1000; @@ -33,21 +35,17 @@ public class Application : IBot where TState : TurnState /// Creates a new Application instance. /// /// Optional. Options used to configure the application. - public Application(ApplicationOptions options, ILogger? logger = null) + public Application(ApplicationOptions options) { - Verify.NotNull(options); + Verify.ParamNotNull(options); Options = options; - if (Options.TurnStateManager == null) - { - // TODO: set to default turn state manager - Options.TurnStateManager = null; - } + Options.TurnStateManager ??= new TTurnStateManager(); if (Options.AI != null) { - _ai = new AI(Options.AI, logger); + _ai = new AI(Options.AI, Options.Logger); } // Validate long running messages configuration @@ -81,7 +79,7 @@ public AI AI /// /// The application's configured options. /// - public ApplicationOptions Options { get; } + public ApplicationOptions Options { get; } /// /// Handler that will execute before the turn's activity handler logic is processed. @@ -167,14 +165,21 @@ private async Task _OnTurnAsync(ITurnContext turnContext, CancellationToken canc turnContext.Activity.Text = turnContext.Activity.RemoveRecipientMention(); } - // TODO : Fix turn state loading, this is just a placeholder - TState turnState = (TState)new TurnState(); + ITurnStateManager? turnStateManager = Options.TurnStateManager; + IStorage? storage = Options.Storage; + + TState turnState = await turnStateManager!.LoadStateAsync(storage, turnContext); // Call before activity handler if (!await OnBeforeTurnAsync(turnContext, turnState, cancellationToken)) { + // Save turn state + // - This lets the bot keep track of why it ended the previous turn. It also + // allows the dialog system to be used before the AI system is called. + await turnStateManager!.SaveStateAsync(storage, turnContext, turnState); + return; - } + }; // Call activity type specific handler bool eventHandlerCalled = await RunActivityHandlerAsync(turnContext, turnState, cancellationToken); @@ -182,13 +187,13 @@ private async Task _OnTurnAsync(ITurnContext turnContext, CancellationToken canc if (!eventHandlerCalled && _ai != null && ActivityTypes.Message.Equals(turnContext.Activity.Type, StringComparison.OrdinalIgnoreCase) && turnContext.Activity.Text != null) { // Begin a new chain of AI calls - _ = await _ai.ChainAsync(turnContext, turnState); + await _ai.ChainAsync(turnContext, turnState); } // Call after turn activity handler if (await OnAfterTurnAsync(turnContext, turnState, cancellationToken)) { - // TODO : Save turn state to persistent storage + await turnStateManager!.SaveStateAsync(storage, turnContext, turnState); }; } finally @@ -912,11 +917,11 @@ protected virtual async Task OnMessageReactionActivityAsync(ITurnContext - /// Options for the class + /// Options for the class /// /// Type of the turn state - public class ApplicationOptions where TState : TurnState + public class ApplicationOptions + where TState : ITurnState + where TTurnStateManager : ITurnStateManager { /// /// Optional. Bot adapter being used. @@ -38,7 +42,15 @@ public class ApplicationOptions where TState : TurnState /// Optional. Turn state manager to use. If omitted, an instance of DefaultTurnStateManager will /// be created. /// - public TurnStateManager? TurnStateManager { get; set; } + public TTurnStateManager? TurnStateManager { get; set; } + + /// + /// Optional. Logger that will be used in this application. + /// + /// + /// and prompt completion data will is logged at the level. + /// + public ILogger? Logger { get; set; } /// /// Optional. If true, the bot will automatically remove mentions of the bot's name from incoming @@ -64,4 +76,6 @@ public class ApplicationOptions where TState : TurnState /// public bool? LongRunningMessages { get; set; } = false; } + + public class ApplicationOptions { } } diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Exceptions/TurnStateManagerException.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Exceptions/TurnStateManagerException.cs new file mode 100644 index 000000000..2fe5e6ad1 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Exceptions/TurnStateManagerException.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Bot.Builder.M365.Exceptions +{ + internal class TurnStateManagerException : Exception + { + public TurnStateManagerException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/IReadOnlyEntry.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/IReadOnlyEntry.cs new file mode 100644 index 000000000..fc7e1fac4 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/IReadOnlyEntry.cs @@ -0,0 +1,30 @@ + +namespace Microsoft.Bot.Builder.M365.State +{ + /// + /// Represents a turn state entry. + /// + /// The value type of the turn state entry + public interface IReadOnlyEntry where TValue : class + { + /// + /// Whether the entry has changed since it was loaded. + /// + bool HasChanged { get; } + + /// + /// Whether the entry has been deleted. + /// + public bool IsDeleted { get; } + + /// + /// The value of the entry. + /// + public TValue Value { get; } + + /// + /// The key used to store the entry in storage. + /// + public string? StorageKey { get; } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/ITurnState.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/ITurnState.cs new file mode 100644 index 000000000..d0e5184eb --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/ITurnState.cs @@ -0,0 +1,15 @@ + +namespace Microsoft.Bot.Builder.M365.State +{ + public interface ITurnState + where TConversationState : class + where TUserState : class + where TTempState : TempState + { + public TConversationState? Conversation { get; } + + public TUserState? User { get; } + + public TTempState? Temp{ get; } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/ITurnStateManager.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/ITurnStateManager.cs new file mode 100644 index 000000000..7f20ee04d --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/ITurnStateManager.cs @@ -0,0 +1,26 @@ +namespace Microsoft.Bot.Builder.M365.State +{ + /// + /// Interface implemented by classes responsible for loading and saving an applications turn state. + /// + /// Type of the state object being persisted. + public interface ITurnStateManager where TState : ITurnState + { + /// + /// Loads all of the state scopes for the current turn. + /// + /// Storage provider to load state scopes from. + /// Context for the current turn of conversation with the user. + /// The loaded state scopes. + Task LoadStateAsync(IStorage? storage, ITurnContext turnContext); + + /// + /// Saves all of the state scopes for the current turn. + /// + /// Storage provider to save state scopes to. + /// Context for the current turn of conversation with the user. + /// State scopes to save. + Task SaveStateAsync(IStorage? storage, ITurnContext turnContext, TState turnState); + } + +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/StateBase.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/StateBase.cs new file mode 100644 index 000000000..32608541d --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/StateBase.cs @@ -0,0 +1,72 @@ +using Microsoft.Bot.Builder.M365.Utilities; + +namespace Microsoft.Bot.Builder.M365.State +{ + /// + /// The base state class. + /// + public class StateBase : Dictionary + { + /// + /// Tries to get the value from the dictionary. + /// + /// Type of the value + /// key to look for + /// value associated with key + /// True if a value of given type is associated with key. + /// + public bool TryGetValue(string key, out T value) + { + Verify.ParamNotNull(key, nameof(key)); + + if (base.TryGetValue(key, out object entry)) + { + if (entry is T castedEntry) + { + value = castedEntry; + return true; + }; + + throw new InvalidCastException($"Failed to cast generic object to type '{typeof(T)}'"); + } + + value = default; + + return false; + } + + /// + /// Gets the value from the dictionary. + /// + /// Type of the value + /// key to look for + /// The value associated with the key + public T? Get(string key) + { + Verify.ParamNotNull(key, nameof(key)); + + if (TryGetValue(key, out T value)) + { + return value; + } + else + { + return default; + }; + } + + /// + /// Sets value in the dictionary. + /// + /// Type of value + /// key to look for + /// value associated with key + public void Set(string key, T value) + { + Verify.ParamNotNull(key, nameof(key)); + Verify.ParamNotNull(value, nameof(value)); + + this[key] = value; + } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TempState.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TempState.cs new file mode 100644 index 000000000..d30fa1b84 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TempState.cs @@ -0,0 +1,51 @@ +using Microsoft.Bot.Builder.M365.Utilities; + +namespace Microsoft.Bot.Builder.M365.State +{ + /// + /// Temporary state. + /// + /// + /// Inherit a new class from this base abstract class to strongly type the applications temp state. + /// + public class TempState : StateBase + { + public const string InputKey = "input"; + public const string OutputKey = "output"; + public const string HistoryKey = "history"; + + public TempState() : base() + { + this[InputKey] = string.Empty; + this[OutputKey] = string.Empty; + this[HistoryKey] = string.Empty; + } + + /// + /// Input pass to an AI prompt + /// + public string Input + { + get => Get(InputKey)!; + set => Set(InputKey, value); + } + + /// + /// Formatted conversation history for embedding in an AI prompt + /// + public string Output + { + get => Get(OutputKey)!; + set => Set(OutputKey, value); + } + + /// + /// Output returned from an AI prompt or function + /// + public string History + { + get => Get(HistoryKey)!; + set => Set(HistoryKey, value); + } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnState.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnState.cs new file mode 100644 index 000000000..6fc4e665f --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnState.cs @@ -0,0 +1,110 @@ +using Microsoft.Bot.Builder.M365.Utilities; + +namespace Microsoft.Bot.Builder.M365.State +{ + /// + /// Defines the default state scopes persisted by the `DefaultTurnStateManager`. + /// + /// Optional. Type of the conversation state object being persisted. + /// Optional. Type of the user state object being persisted. + /// Optional. Type of the temp state object being persisted. + public class TurnState : StateBase, ITurnState + where TConversationState : StateBase + where TUserState : StateBase + where TTempState : TempState + { + public const string ConversationStateKey = "conversationState"; + public const string UserStateKey = "userState"; + public const string TempStateKey = "tempState"; + + /// + /// Stores all the conversation-related state entry. + /// + public TurnStateEntry? ConversationStateEntry + { + get + { + return Get>(ConversationStateKey); + } + set + { + Verify.ParamNotNull(value, nameof(value)); + + Set(ConversationStateKey, value!); + } + } + + /// + /// Stores all the user related state entry. + /// + public TurnStateEntry? UserStateEntry + { + get + { + return Get>(UserStateKey); + } + set + { + Verify.ParamNotNull(value, nameof(value)); + + Set(UserStateKey, value!); + } + } + + /// + /// Stores all the temporary state entry for the current turn. + /// + public TurnStateEntry? TempStateEntry + { + get + { + return Get>(TempStateKey); + } + set + { + Verify.ParamNotNull(value, nameof(value)); + + Set(TempStateKey, value!); + } + } + + /// + /// Stores all the conversation-related state. + /// + public TConversationState? Conversation + { + get + { + return ConversationStateEntry?.Value; + } + } + + /// + /// Stores all the user related state. + /// + public TUserState? User + { + get + { + return UserStateEntry?.Value; + } + } + + /// + /// Stores the current turn's state. + /// + public TTempState? Temp + { + get + { + return TempStateEntry?.Value; + } + } + } + + /// + /// Defines the default state scopes persisted by the . + /// + public class TurnState : TurnState { } + +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnStateEntry.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnStateEntry.cs new file mode 100644 index 000000000..ebeea4919 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnStateEntry.cs @@ -0,0 +1,83 @@ +using Microsoft.Bot.Builder.M365.Utilities; +using System.Text.Json; + +namespace Microsoft.Bot.Builder.M365.State +{ + public class TurnStateEntry : IReadOnlyEntry where TValue : class + { + private TValue _value; + private string _hash; + + /// + /// Constructs the turn state entry. + /// + /// The entry value + /// The storage key used to store object entry + public TurnStateEntry(TValue value, string? storageKey = null) + { + Verify.ParamNotNull(value, nameof(value)); + + _value = value; + StorageKey = storageKey; + _hash = ComputeHash(value); + } + + /// + public bool HasChanged + { + get { return ComputeHash(_value) != _hash; } + } + + /// + public bool IsDeleted { get; private set; } = false; + + /// + public TValue Value + { + get + { + if (IsDeleted) + { + IsDeleted = false; + } + + return _value; + } + } + + /// + public string? StorageKey { get; } + + /// + /// Deletes the entry. + /// + public void Delete() + { + IsDeleted = true; + } + + /// + /// Replaces the value in the entry. + /// + /// The entry value. + public void Replace(TValue value) + { + Verify.ParamNotNull(value, nameof(value)); + + _value = value; + } + + // TODO: Optimize if possible + /// + /// Computes the hash from the object + /// + /// The object to compute has from + /// Returns a Json object representation + internal static string ComputeHash(object obj) + { + Verify.ParamNotNull(obj); + + return JsonSerializer.Serialize(obj, new JsonSerializerOptions() { MaxDepth = 64 }); + } + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnStateManager.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnStateManager.cs new file mode 100644 index 000000000..f93920bd5 --- /dev/null +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/State/TurnStateManager.cs @@ -0,0 +1,146 @@ +using Microsoft.Bot.Builder.M365.Exceptions; +using Microsoft.Bot.Builder.M365.Utilities; +using Microsoft.Bot.Schema; + +namespace Microsoft.Bot.Builder.M365.State +{ + /// + /// Defines the default state scopes persisted by the `DefaultTurnStateManager`. + /// + /// Optional. Type of the conversation state object being persisted. + /// Optional. Type of the user state object being persisted. + /// Optional. Type of the temp state object being persisted. + public class TurnStateManager : ITurnStateManager + where TState : TurnState, new() + where TConversationState : StateBase, new() + where TUserState : StateBase, new() + where TTempState : TempState, new() + { + + public TurnStateManager() { } + + /// + public async Task LoadStateAsync(IStorage? storage, ITurnContext turnContext) + { + try + { + // Compute state keys + Activity activity = turnContext.Activity; + string channelId = activity.ChannelId; + string botId = activity.Recipient.Id; + string conversationId = activity.Conversation.Id; + string userId = activity.From.Id; + + Verify.ParamNotNull(activity, "TurnContext.Activity"); + Verify.ParamNotNull(channelId, "TurnContext.Activity.ChannelId"); + Verify.ParamNotNull(botId, "TurnContext.Activity.Recipient.Id"); + Verify.ParamNotNull(conversationId, "TurnContext.Activity.Conversation.Id"); + Verify.ParamNotNull(userId, "TurnContext.Activity.From.Id"); + + string conversationKey = $"{channelId}/${botId}/conversations/${conversationId}"; + string userKey = $"{channelId}/${botId}/users/${userId}"; + + // read items from storage provider (if configured) + IDictionary items; + if (storage != null) + { + items = await storage.ReadAsync(new string[] { conversationKey, userKey }); + } + else + { + items = new Dictionary(); + } + + TState state = new(); + TUserState? userState = null; + TConversationState? conversationState = null; + + if (items.TryGetValue(userKey, out object userStateValue)) + { + userState = userStateValue as TUserState; + } + + if (items.TryGetValue(conversationKey, out object conversationStateValue)) + { + conversationState = conversationStateValue as TConversationState; + } + + userState ??= new TUserState(); + conversationState ??= new TConversationState(); + + state.UserStateEntry = new TurnStateEntry(userState, userKey); + state.ConversationStateEntry = new TurnStateEntry(conversationState, conversationKey); + state.TempStateEntry = new TurnStateEntry(new()); + + return state; + + } + catch (Exception ex) + { + throw new TurnStateManagerException($"Something went wrong when loading state: {ex.Message}", ex); + } + } + + /// + public async Task SaveStateAsync(IStorage? storage, ITurnContext turnContext, TState turnState) + { + try + { + Verify.ParamNotNull(turnContext, nameof(turnContext)); + Verify.ParamNotNull(turnState, nameof(turnState)); + + Dictionary changes = new(); + List deletions = new(); + + foreach (string key in turnState.Keys) + { + if (turnState.TryGetValue(key, out IReadOnlyEntry entry)) + { + if (entry.StorageKey != null) + { + if (entry.IsDeleted) + { + deletions.Add(key); + } + + if (entry.HasChanged) + { + changes[entry.StorageKey] = entry.Value; + } + } + } + + } + + // Do we have a storage provider? + if (storage != null) + { + // Apply changes + List tasks = new(); + if (changes.Keys.Count > 0) + { + tasks.Add(storage.WriteAsync(changes)); + } + + if (deletions.Count > 0) + { + tasks.Add(storage.DeleteAsync(deletions.ToArray())); + } + + if (tasks.Count > 0) + { + await Task.WhenAll(tasks.ToArray()); + } + } + } + catch (Exception ex) + { + throw new TurnStateManagerException($"Something went wrong when saving state: {ex.Message}", ex); + } + } + } + + public class TurnStateManager : TurnStateManager + { + } +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnState.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnState.cs deleted file mode 100644 index 60fbb3d3b..000000000 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnState.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.Bot.Builder.M365 -{ - public class TurnState : Dictionary - { - } -} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnStateEntry.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnStateEntry.cs deleted file mode 100644 index 3d1cdbe08..000000000 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnStateEntry.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.Bot.Builder.M365 -{ - public class TurnStateEntry - { - } -} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnStateManager.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnStateManager.cs deleted file mode 100644 index a02fe8728..000000000 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TurnStateManager.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.Bot.Builder.M365 -{ - public class TurnStateManager where TState : TurnState - { - } -} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TypingTimer.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TypingTimer.cs index 36fce86fa..3b2014066 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TypingTimer.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/TypingTimer.cs @@ -39,7 +39,7 @@ public TypingTimer(int interval = 1000) /// True if the timer was started, otherwise False. public bool Start(ITurnContext turnContext) { - Verify.NotNull(turnContext); + Verify.ParamNotNull(turnContext); if (turnContext.Activity.Type != ActivityTypes.Message || IsRunning()) return false; @@ -97,7 +97,10 @@ private async void SendTypingActivity(object state) try { await turnContext.SendActivityAsync(new Activity { Type = ActivityTypes.Typing }); - _timer!.Change(_interval, Timeout.Infinite); + if (IsRunning()) + { + _timer?.Change(_interval, Timeout.Infinite); + } } catch (ObjectDisposedException) { @@ -125,4 +128,4 @@ private Task StopTimerWhenSendMessageActivityHandler(ITurnCo return next(); } } -} \ No newline at end of file +} diff --git a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Utilities/Verify.cs b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Utilities/Verify.cs index 63ed8545c..75693bcbf 100644 --- a/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Utilities/Verify.cs +++ b/dotnet/packages/Microsoft.Bot.Builder.M365/Microsoft.Bot.Builder.M365/Utilities/Verify.cs @@ -3,7 +3,7 @@ namespace Microsoft.Bot.Builder.M365.Utilities { public class Verify { - public static void NotNull(object? argument, string? parameterName = default) + public static void ParamNotNull(object? argument, string? parameterName = default) { if (argument == null) {