Updates and tests for plan serialization and deserialization (#620)

### Motivation and Context
The change improves the serialization and deserialization of Plan
objects.

### Description
Allows deserialization of Plans without a context. Adds unit tests for
the Plan and ContextVariables classes, which cover various scenarios and
edge cases of serializing and deserializing plans and context variables.

Details:
- Updates the custom converter for the ContextVariables class, which
serializes and deserializes the key-value pairs as an array of objects,
instead of a dictionary. This improves the efficiency and readability of
the JSON format.
- Add a new file PlanSerializationTests.cs to the
SemanticKernel.UnitTests project, which contains unit tests for the Plan
class. The tests verify that a plan can be serialized and deserialized
correctly, and that the plan state is updated correctly after each step
execution. The tests also check for invalid JSON strings.
- Add a new file ContextVariablesConverterTests.cs to the
SemanticKernel.UnitTests project, which contains unit tests for the
ContextVariablesConverter class. The tests verify that the converter can
correctly serialize and deserialize context variables to and from JSON,
and handle null, empty, and invalid inputs.
This commit is contained in:
Lee Miller
2023-04-26 15:04:26 -07:00
committed by GitHub
parent 2607c14b0d
commit 56ea209535
7 changed files with 859 additions and 9 deletions
+22
View File
@@ -137,6 +137,28 @@
"cwd": "${workspaceFolder}/dotnet/src/SemanticKernel.UnitTests/"
}
},
{
"label": "Test - Extensions (Code Coverage)",
"command": "dotnet",
"type": "process",
"args": [
"test",
"--collect",
"XPlat Code Coverage;Format=lcov",
"--filter",
"${input:filter}",
"Extensions.UnitTests.csproj"
],
"problemMatcher": "$msCompile",
"group": "test",
"presentation": {
"reveal": "always",
"panel": "shared"
},
"options": {
"cwd": "${workspaceFolder}/dotnet/src/Extensions/Extensions.UnitTests/"
}
},
{
"label": "Test - Semantic-Kernel Integration (Code Coverage)",
"command": "dotnet",
@@ -18,6 +18,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>
@@ -157,6 +157,40 @@ public sealed class PlanTests : IDisposable
Assert.Equal($"Sent email to: {email}. Body: {expectedBody}".Trim(), plan.State.ToString());
}
[Theory]
[InlineData(null, "Write a poem or joke and send it in an e-mail to Kai.", null)]
[InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")]
[InlineData("Hello World!", "Write a poem or joke and send it in an e-mail to Kai.", "[email protected]")]
public async Task CanExecuteRunPlanSimpleManualStateNoVariableAsync(string input, string goal, string email)
{
// Arrange
IKernel target = this.InitializeKernel();
var emailSkill = target.ImportSkill(new EmailSkillFake());
// Create the input mapping from parent (plan) plan state to child plan (sendEmailPlan) state.
var cv = new ContextVariables();
cv.Set("email_address", string.Empty);
var sendEmailPlan = new Plan(emailSkill["SendEmailAsync"])
{
NamedParameters = cv,
};
var plan = new Plan(goal);
plan.AddSteps(sendEmailPlan);
plan.State.Set("email_address", email); // manually prepare the state
// Act
var result = await target.StepAsync(input, plan);
// Assert
var expectedBody = string.IsNullOrEmpty(input) ? goal : input;
Assert.Single(result.Steps);
Assert.Equal(1, result.NextStepIndex);
Assert.False(result.HasNextStep);
Assert.Equal(goal, plan.Description);
Assert.Equal($"Sent email to: {email}. Body: {expectedBody}".Trim(), plan.State.ToString());
}
[Theory]
[InlineData(null, "Write a poem or joke and send it in an e-mail to Kai.", null)]
[InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")]
@@ -314,6 +348,63 @@ public sealed class PlanTests : IDisposable
Assert.True(expectedBody.Length < result.Result.Length);
}
[Theory]
[InlineData("Summarize an input, translate to french, and e-mail to Kai", "This is a story about a dog.", "French", "Kai", "[email protected]")]
public async Task CanExecuteRunSequentialOnDeserializedPlanAsync(string goal, string inputToSummarize, string inputLanguage, string inputName,
string expectedEmail)
{
// Arrange
IKernel target = this.InitializeKernel();
var summarizeSkill = TestHelpers.GetSkills(target, "SummarizeSkill");
var writerSkill = TestHelpers.GetSkills(target, "WriterSkill");
var emailSkill = target.ImportSkill(new EmailSkillFake());
var expectedBody = $"Sent email to: {expectedEmail}. Body:".Trim();
var summarizePlan = new Plan(summarizeSkill["Summarize"]);
var cv = new ContextVariables();
cv.Set("language", inputLanguage);
var outputs = new ContextVariables();
outputs.Set("TRANSLATED_SUMMARY", string.Empty);
var translatePlan = new Plan(writerSkill["Translate"])
{
NamedParameters = cv,
NamedOutputs = outputs,
};
cv = new ContextVariables();
cv.Update(inputName);
outputs = new ContextVariables();
outputs.Set("TheEmailFromState", string.Empty);
var getEmailPlan = new Plan(emailSkill["GetEmailAddressAsync"])
{
NamedParameters = cv,
NamedOutputs = outputs,
};
cv = new ContextVariables();
cv.Set("email_address", "$TheEmailFromState");
cv.Set("input", "$TRANSLATED_SUMMARY");
var sendEmailPlan = new Plan(emailSkill["SendEmailAsync"])
{
NamedParameters = cv
};
var plan = new Plan(goal);
plan.AddSteps(summarizePlan, translatePlan, getEmailPlan, sendEmailPlan);
// Act
var serializedPlan = plan.ToJson();
var deserializedPlan = Plan.FromJson(serializedPlan, target.CreateNewContext());
var result = await target.RunAsync(inputToSummarize, deserializedPlan);
// Assert
Assert.Contains(expectedBody, result.Result, StringComparison.OrdinalIgnoreCase);
Assert.True(expectedBody.Length < result.Result.Length);
}
[Theory]
[InlineData("Summarize an input, translate to french, and e-mail to Kai", "This is a story about a dog.", "French", "[email protected]")]
public async Task CanExecuteRunSequentialFunctionsAsync(string goal, string inputToSummarize, string inputLanguage, string expectedEmail)
@@ -0,0 +1,186 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.SemanticKernel.Orchestration;
using Xunit;
namespace SemanticKernel.UnitTests.Orchestration;
/// <summary>
/// Unit tests of <see cref="ContextVariablesConverter"/>.
/// </summary>
public class ContextVariablesConverterTests
{
[Fact]
public void ReadFromJsonSucceeds()
{
// Arrange
string json = /*lang=json,strict*/ @"[{""Key"":""a"", ""Value"":""b""}]";
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
// Act
var result = JsonSerializer.Deserialize<ContextVariables>(json, options);
// Assert
Assert.Equal("b", result!["a"]);
Assert.Equal(string.Empty, result!["INPUT"]);
}
[Fact]
public void ReadFromJsonWrongTypeThrows()
{
// Arrange
string json = /*lang=json,strict*/ @"[{""Key"":""a"", ""Value"":""b""}]";
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
// Act and Assert
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Dictionary<string, string>>(json, options));
}
[Fact]
public void ReadFromJsonSucceedsWithInput()
{
// Arrange
string json = /*lang=json,strict*/ @"[{""Key"":""INPUT"", ""Value"":""c""}, {""Key"":""a"", ""Value"":""b""}]";
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
// Act
var result = JsonSerializer.Deserialize<ContextVariables>(json, options);
// Assert
Assert.Equal("b", result!["a"]);
Assert.Equal("c", result!["INPUT"]);
}
// input value
// params key/value
[Theory]
[InlineData(null, new[] { "a", "b" }, new[]
{
/*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""""}", /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}"
})]
[InlineData("", new[] { "a", "b" }, new[]
{
/*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""""}", /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}"
})]
[InlineData("c", new[] { "a", "b" }, new[]
{
/*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""c""}", /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}"
})]
[InlineData("c", new[] { "a", "b", "d", "e" }, new[]
{
/*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""c""}", /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}", /*lang=json,strict*/
@"{""Key"":""d"",""Value"":""e""}"
})]
public void WriteToJsonSuceeds(string inputValue, IList<string> contextToSet, IList<string> expectedJson)
{
// Arrange
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
var context = new ContextVariables();
if (inputValue != null)
{
context.Update(inputValue);
}
for (int i = 0; i < contextToSet.Count; i += 2)
{
context.Set(contextToSet[i], contextToSet[i + 1]);
}
// Act
string result = JsonSerializer.Serialize(context, options);
// Assert
foreach (string key in expectedJson)
{
Assert.Contains(key, result, StringComparison.Ordinal);
}
}
[Fact]
public void WriteToJsonSucceedsAfterClearing()
{
// Arrange
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
var context = new ContextVariables();
context.Set("a", "b");
context.Set("INPUT", "c");
context.Set("d", "e");
context.Set("f", "ThingToBeCleared");
context.Set("f", null);
context.Set("g", string.Empty);
// Act
string result = JsonSerializer.Serialize(context, options);
// Assert
Assert.Contains( /*lang=json,strict*/ @"{""Key"":""INPUT"",""Value"":""c""}", result, StringComparison.Ordinal);
Assert.Contains( /*lang=json,strict*/ @"{""Key"":""a"",""Value"":""b""}", result, StringComparison.Ordinal);
Assert.Contains( /*lang=json,strict*/ @"{""Key"":""d"",""Value"":""e""}", result, StringComparison.Ordinal);
Assert.DoesNotContain(@"""Key"":""f""", result, StringComparison.Ordinal);
Assert.Contains( /*lang=json,strict*/ @"{""Key"":""g"",""Value"":""""}", result, StringComparison.Ordinal);
}
// Error Tests
[Fact]
public void ReadFromJsonReturnsNullWithNull()
{
// Arrange
string json = /*lang=json,strict*/ @"null";
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
// Act
var result = JsonSerializer.Deserialize<ContextVariables>(json, options);
// Assert
Assert.Null(result);
}
[Fact]
public void ReadFromJsonReturnsDefaultWithEmpty()
{
// Arrange
string json = /*lang=json,strict*/ @"[]";
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
// Act
var result = JsonSerializer.Deserialize<ContextVariables>(json, options);
// Assert
Assert.NotNull(result);
Assert.Equal(string.Empty, result!["INPUT"]);
}
[Fact]
public void ReadFromJsonThrowsWithInvalidJson()
{
// Arrange
string json = /*lang=json,strict*/ @"[{""Key"":""a"", ""Value"":""b""";
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
// Act & Assert
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ContextVariables>(json, options));
}
[Fact]
public void ReadFromJsonThrowsWithInvalidJson2()
{
// Arrange
string json = /*lang=json,strict*/ @"[{""Keys"":""a"", ""Value"":""b""}]";
var options = new JsonSerializerOptions();
options.Converters.Add(new ContextVariablesConverter());
// Act & Assert
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ContextVariables>(json, options));
}
}
@@ -0,0 +1,538 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.SkillDefinition;
using Moq;
using Xunit;
namespace SemanticKernel.UnitTests.Planning;
public sealed class PlanSerializationTests
{
[Fact]
public void CanSerializePlan()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var expectedSteps = "\"steps\":[]";
var plan = new Plan(goal);
// Act
var serializedPlan = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan);
Assert.NotEmpty(serializedPlan);
Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CanSerializePlanWithGoalAndSteps()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var expectedSteps = "\"steps\":[{";
var plan = new Plan(goal, new Mock<ISKFunction>().Object, new Mock<ISKFunction>().Object);
// Act
var serializedPlan = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan);
Assert.NotEmpty(serializedPlan);
Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CanSerializePlanWithGoalAndSubPlans()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var expectedSteps = "\"steps\":[{";
var plan = new Plan(goal, new Plan("Write a poem or joke"), new Plan("Send it in an e-mail to Kai"));
// Act
var serializedPlan = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan);
Assert.NotEmpty(serializedPlan);
Assert.Contains($"\"description\":\"{goal}\"", serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains($"\"description\":\"Write a poem or joke\"", serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains($"\"description\":\"Send it in an e-mail to Kai\"", serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CanSerializePlanWithPlanStep()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var stepOutput = "Output: The input was: ";
var expectedSteps = "\"steps\":[{";
var plan = new Plan(goal);
// Arrange Mocks
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
.Returns(() => Task.FromResult(returnContext));
plan.AddSteps(new Plan(mockFunction.Object));
// Act
var serializedPlan = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan);
Assert.NotEmpty(serializedPlan);
Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CanSerializePlanWithFunctionStep()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var stepOutput = "Output: The input was: ";
var expectedSteps = "\"steps\":[{";
var plan = new Plan(goal);
// Arrange
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
.Returns(() => Task.FromResult(returnContext));
plan.AddSteps(mockFunction.Object);
// Act
var serializedPlan = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan);
Assert.NotEmpty(serializedPlan);
Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CanSerializePlanWithFunctionSteps()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var stepOutput = "Output: The input was: ";
var expectedSteps = "\"steps\":[{";
var plan = new Plan(goal);
// Arrange
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
.Returns(() => Task.FromResult(returnContext));
plan.AddSteps(mockFunction.Object, mockFunction.Object);
// Act
var serializedPlan = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan);
Assert.NotEmpty(serializedPlan);
Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CanSerializePlanWithStepsAndFunction()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var stepOutput = "Output: The input was: ";
var expectedSteps = "\"steps\":[{";
var plan = new Plan(goal);
// Arrange
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
.Returns(() => Task.FromResult(returnContext));
plan.AddSteps(new Plan(mockFunction.Object), mockFunction.Object);
// Act
var serializedPlan = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan);
Assert.NotEmpty(serializedPlan);
Assert.Contains(goal, serializedPlan, StringComparison.OrdinalIgnoreCase);
Assert.Contains(expectedSteps, serializedPlan, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CanSerializePlanWithSteps()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var stepOutput = "Output: The input was: ";
var plan = new Plan(goal);
// Arrange
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
.Returns(() => Task.FromResult(returnContext));
plan.AddSteps(new Plan(mockFunction.Object), new Plan(mockFunction.Object));
// Act
var serializedPlan = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan);
Assert.NotEmpty(serializedPlan);
}
[Fact]
public async Task CanStepAndSerializePlanWithStepsAsync()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var planInput = "Some input";
var stepOutput = "Output: The input was: ";
var plan = new Plan(goal);
// Arrange
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
.Returns(() => Task.FromResult(returnContext));
plan.AddSteps(mockFunction.Object, mockFunction.Object);
var serializedPlan1 = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan1);
Assert.NotEmpty(serializedPlan1);
Assert.Contains("\"next_step_index\":0", serializedPlan1, StringComparison.OrdinalIgnoreCase);
var result = await kernel.Object.StepAsync(planInput, plan);
// Act
var serializedPlan2 = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan2);
Assert.NotEmpty(serializedPlan2);
Assert.NotEqual(serializedPlan1, serializedPlan2);
Assert.Contains("\"next_step_index\":1", serializedPlan2, StringComparison.OrdinalIgnoreCase);
result = await kernel.Object.StepAsync(result);
var serializedPlan3 = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan3);
Assert.NotEmpty(serializedPlan3);
Assert.NotEqual(serializedPlan1, serializedPlan3);
Assert.NotEqual(serializedPlan2, serializedPlan3);
Assert.Contains("\"next_step_index\":2", serializedPlan3, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task CanStepAndSerializePlanWithStepsAndContextAsync()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var planInput = "Some input";
var stepOutput = "Output: The input was: ";
var plan = new Plan(goal);
// Arrange
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
{
c.Variables.Get("variables", out var v);
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input + v);
})
.Returns(() => Task.FromResult(returnContext));
mockFunction.Setup(x => x.Describe()).Returns(new FunctionView()
{
Parameters = new List<ParameterView>()
{
new ParameterView() { Name = "variables" }
}
});
plan.AddSteps(mockFunction.Object, mockFunction.Object);
var cv = new ContextVariables(planInput);
cv.Set("variables", "foo");
plan = await kernel.Object.StepAsync(cv, plan);
// Act
var serializedPlan1 = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan1);
Assert.NotEmpty(serializedPlan1);
Assert.Contains("\"next_step_index\":1", serializedPlan1, StringComparison.OrdinalIgnoreCase);
// Act
cv.Set("variables", "bar");
cv.Update(string.Empty);
plan = await kernel.Object.StepAsync(cv, plan);
// Assert
Assert.NotNull(plan);
Assert.Equal($"{stepOutput}{planInput}foo{stepOutput}{planInput}foobar", plan.State.ToString());
mockFunction.Verify(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null), Times.Exactly(2));
// Act
var serializedPlan2 = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan2);
Assert.NotEmpty(serializedPlan2);
Assert.NotEqual(serializedPlan1, serializedPlan2);
Assert.Contains("\"next_step_index\":2", serializedPlan2, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task CanStepAndSerializeAndDeserializePlanWithStepsAndContextAsync()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var planInput = "Some input";
var stepOutput = "Output: The input was: ";
var plan = new Plan(goal);
// Arrange
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
{
c.Variables.Get("variables", out var v);
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input + v);
})
.Returns(() => Task.FromResult(returnContext));
mockFunction.Setup(x => x.Describe()).Returns(new FunctionView()
{
Parameters = new List<ParameterView>()
{
new ParameterView() { Name = "variables" }
}
});
skills.Setup(x => x.HasNativeFunction(It.IsAny<string>(), It.IsAny<string>())).Returns(true);
skills.Setup(x => x.GetNativeFunction(It.IsAny<string>(), It.IsAny<string>())).Returns(mockFunction.Object);
plan.AddSteps(mockFunction.Object, mockFunction.Object);
var serializedPlan = plan.ToJson();
var cv = new ContextVariables(planInput);
cv.Set("variables", "foo");
plan = await kernel.Object.StepAsync(cv, plan);
// Act
var serializedPlan1 = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan1);
Assert.NotEmpty(serializedPlan1);
Assert.NotEqual(serializedPlan, serializedPlan1);
Assert.Contains("\"next_step_index\":1", serializedPlan1, StringComparison.OrdinalIgnoreCase);
// Act
cv.Set("variables", "bar");
cv.Update(string.Empty);
var nextContext = new SKContext(
new ContextVariables(),
memory.Object,
skills.Object,
log.Object
);
plan = Plan.FromJson(serializedPlan1, nextContext);
plan = await kernel.Object.StepAsync(cv, plan);
// Assert
Assert.NotNull(plan);
Assert.Equal($"{stepOutput}{planInput}foo{stepOutput}{planInput}foobar", plan.State.ToString());
mockFunction.Verify(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null), Times.Exactly(2));
// Act
var serializedPlan2 = plan.ToJson();
// Assert
Assert.NotNull(serializedPlan2);
Assert.NotEmpty(serializedPlan2);
Assert.NotEqual(serializedPlan1, serializedPlan2);
Assert.Contains("\"next_step_index\":2", serializedPlan2, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CanDeserializePlan()
{
// Arrange
var goal = "Write a poem or joke and send it in an e-mail to Kai.";
var stepOutput = "Output: The input was: ";
var plan = new Plan(goal);
// Arrange
var kernel = new Mock<IKernel>();
var log = new Mock<ILogger>();
var memory = new Mock<ISemanticTextMemory>();
var skills = new Mock<ISkillCollection>();
var returnContext = new SKContext(
new ContextVariables(stepOutput),
memory.Object,
skills.Object,
log.Object
);
var mockFunction = new Mock<ISKFunction>();
mockFunction.Setup(x => x.InvokeAsync(It.IsAny<SKContext>(), null, null, null))
.Callback<SKContext, CompleteRequestSettings, ILogger, CancellationToken?>((c, s, l, ct) =>
returnContext.Variables.Update(returnContext.Variables.Input + c.Variables.Input))
.Returns(() => Task.FromResult(returnContext));
plan.AddSteps(new Plan("Step1", mockFunction.Object), mockFunction.Object);
// Act
var serializedPlan = plan.ToJson();
var deserializedPlan = Plan.FromJson(serializedPlan, returnContext);
// Assert
Assert.NotNull(deserializedPlan);
Assert.Equal(goal, deserializedPlan.Description);
Assert.Equal(string.Join(",", plan.NamedOutputs.Select(kv => $"{kv.Key}:{kv.Value}")),
string.Join(",", deserializedPlan.NamedOutputs.Select(kv => $"{kv.Key}:{kv.Value}")));
Assert.Equal(string.Join(",", plan.NamedParameters.Select(kv => $"{kv.Key}:{kv.Value}")),
string.Join(",", deserializedPlan.NamedParameters.Select(kv => $"{kv.Key}:{kv.Value}")));
Assert.Equal(string.Join(",", plan.State.Select(kv => $"{kv.Key}:{kv.Value}")),
string.Join(",", deserializedPlan.State.Select(kv => $"{kv.Key}:{kv.Value}")));
Assert.Equal(plan.Steps[0].Name, deserializedPlan.Steps[0].Name);
Assert.Equal(plan.Steps[1].Name, deserializedPlan.Steps[1].Name);
}
}
@@ -16,12 +16,11 @@ public class ContextVariablesConverter : JsonConverter<ContextVariables>
/// Read the JSON and convert to ContextVariables
/// </summary>
/// <param name="reader">The JSON reader.</param>
/// <param name="typeToConvert">The type to convert from.</param>
/// <param name="typeToConvert">The type to convert.</param>
/// <param name="options">The JSON serializer options.</param>
/// <returns>The deserialized <see cref="ContextVariables"/>.</returns>
public override ContextVariables Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Needs Test
var keyValuePairs = JsonSerializer.Deserialize<IEnumerable<KeyValuePair<string, string>>>(ref reader, options);
var context = new ContextVariables();
@@ -35,7 +34,16 @@ public class ContextVariablesConverter : JsonConverter<ContextVariables>
public override void Write(Utf8JsonWriter writer, ContextVariables value, JsonSerializerOptions options)
{
// Needs Test
JsonSerializer.Serialize(writer, value, options);
writer.WriteStartArray();
foreach (var kvp in value)
{
writer.WriteStartObject();
writer.WriteString("Key", kvp.Key);
writer.WriteString("Value", kvp.Value);
writer.WriteEndObject();
}
writer.WriteEndArray();
}
}
+6 -5
View File
@@ -159,12 +159,15 @@ public sealed class Plan : ISKFunction
/// <param name="json">JSON string representation of a Plan</param>
/// <param name="context">The context to use for function registrations.</param>
/// <returns>An instance of a Plan object.</returns>
public static Plan FromJson(string json, SKContext context)
/// <remarks>If Context is not supplied, plan will not be able to execute.</remarks>
public static Plan FromJson(string json, SKContext? context = null)
{
// Needs Test
var plan = JsonSerializer.Deserialize<Plan>(json, new JsonSerializerOptions() { IncludeFields = true }) ?? new Plan(string.Empty);
plan = SetRegisteredFunctions(plan, context);
if (context != null)
{
plan = SetRegisteredFunctions(plan, context);
}
return plan;
}
@@ -174,7 +177,6 @@ public sealed class Plan : ISKFunction
/// </summary>
public string ToJson()
{
// Needs Test
return JsonSerializer.Serialize(this);
}
@@ -449,7 +451,6 @@ public sealed class Plan : ISKFunction
}
else if (this.State.Get(param.Name, out value) && !string.IsNullOrEmpty(value))
{
// Needs test
stepVariables.Set(param.Name, value);
}
}