mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
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:
Vendored
+22
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user