Skip to content

Commit ee68b65

Browse files
.Net: Add support for partial XML and global functions in plan parser (#2824)
- Added support for parsing partial XML by appending a closing tag if not present. - Enabled plan creation with global functions by handling cases where skill name is not provided. - Updated unit tests to cover these new scenarios. Fixes #2502 ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄
1 parent c74ba11 commit ee68b65

File tree

2 files changed

+58
-2
lines changed

2 files changed

+58
-2
lines changed

dotnet/src/Extensions/Extensions.UnitTests/Planning/SequentialPlanner/SequentialPlanParserTests.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private void CreateKernelAndFunctionCreateMocks(List<(string name, string skillN
6666
var functionsView = new FunctionsView();
6767
foreach (var (name, skillName, description, isSemantic, resultString) in functions)
6868
{
69-
var functionView = new FunctionView(name, skillName, description, new List<ParameterView>(), isSemantic, true);
69+
var functionView = new FunctionView(name, skillName, description, new List<ParameterView>() { new(name: "param", description: "description") }, isSemantic, true);
7070
var mockFunction = CreateMockFunction(functionView);
7171
functionsView.AddFunction(functionView);
7272

@@ -88,6 +88,7 @@ private void CreateKernelAndFunctionCreateMocks(List<(string name, string skillN
8888
skills.Setup(x => x.GetFunction(It.Is<string>(s => s == skillName), It.Is<string>(s => s == name)))
8989
.Returns(mockFunction.Object);
9090
ISKFunction? outFunc = mockFunction.Object;
91+
skills.Setup(x => x.TryGetFunction(It.Is<string>(s => s == name), out outFunc)).Returns(true);
9192
skills.Setup(x => x.TryGetFunction(It.Is<string>(s => s == skillName), It.Is<string>(s => s == name), out outFunc)).Returns(true);
9293
}
9394
}
@@ -196,6 +197,55 @@ public void CanCreatePlanWithTextNodes(string goalText, string planText)
196197
Assert.Equal("Echo", plan.Steps[0].Name);
197198
}
198199

200+
[Theory]
201+
[InlineData("Test the functionFlowRunner", @"<goal>Test the functionFlowRunner</goal>
202+
<plan>
203+
<function.MockSkill.Echo input=""Hello World"" />")]
204+
public void CanCreatePlanWithPartialXml(string goalText, string planText)
205+
{
206+
// Arrange
207+
var functions = new List<(string name, string skillName, string description, bool isSemantic, string result)>()
208+
{
209+
("Echo", "MockSkill", "Echo an input", true, "Mock Echo Result"),
210+
};
211+
this.CreateKernelAndFunctionCreateMocks(functions, out var kernel);
212+
213+
// Act
214+
var plan = planText.ToPlanFromXml(goalText, SequentialPlanParser.GetSkillFunction(kernel.CreateNewContext()));
215+
216+
// Assert
217+
Assert.NotNull(plan);
218+
Assert.Equal(goalText, plan.Description);
219+
Assert.Equal(1, plan.Steps.Count);
220+
Assert.Equal("MockSkill", plan.Steps[0].SkillName);
221+
Assert.Equal("Echo", plan.Steps[0].Name);
222+
}
223+
224+
[Theory]
225+
[InlineData("Test the functionFlowRunner", @"<goal>Test the functionFlowRunner</goal>
226+
<plan>
227+
<function.Echo input=""Hello World"" />
228+
</plan>")]
229+
public void CanCreatePlanWithFunctionName(string goalText, string planText)
230+
{
231+
// Arrange
232+
var functions = new List<(string name, string skillName, string description, bool isSemantic, string result)>()
233+
{
234+
("Echo", "_GLOBAL_FUNCTIONS_", "Echo an input", true, "Mock Echo Result"),
235+
};
236+
this.CreateKernelAndFunctionCreateMocks(functions, out var kernel);
237+
238+
// Act
239+
var plan = planText.ToPlanFromXml(goalText, SequentialPlanParser.GetSkillFunction(kernel.CreateNewContext()));
240+
241+
// Assert
242+
Assert.NotNull(plan);
243+
Assert.Equal(goalText, plan.Description);
244+
Assert.Equal(1, plan.Steps.Count);
245+
Assert.Equal("_GLOBAL_FUNCTIONS_", plan.Steps[0].SkillName);
246+
Assert.Equal("Echo", plan.Steps[0].Name);
247+
}
248+
199249
// Test that contains a #text node in the plan
200250
[Theory]
201251
[InlineData(@"

dotnet/src/Extensions/Planning.SequentialPlanner/SequentialPlanParser.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ internal static Plan ToPlanFromXml(this string xmlString, string goal, Func<stri
8989
// '</plan>': Matches the literal string "</plan>", indicating the closing tag of the <plan> element.
9090
Regex planRegex = new(@"<plan\b[^>]*>(.*?)</plan>", RegexOptions.Singleline);
9191
Match match = planRegex.Match(xmlString);
92+
93+
if (!match.Success)
94+
{
95+
match = planRegex.Match($"{xmlString}</plan>"); // try again with a closing tag
96+
}
97+
9298
if (match.Success)
9399
{
94100
string planXml = match.Value;
@@ -205,7 +211,7 @@ internal static Plan ToPlanFromXml(this string xmlString, string goal, Func<stri
205211
private static void GetSkillFunctionNames(string skillFunctionName, out string skillName, out string functionName)
206212
{
207213
var skillFunctionNameParts = skillFunctionName.Split('.');
208-
skillName = skillFunctionNameParts?.Length > 0 ? skillFunctionNameParts[0] : string.Empty;
214+
skillName = skillFunctionNameParts?.Length > 1 ? skillFunctionNameParts[0] : string.Empty;
209215
functionName = skillFunctionNameParts?.Length > 1 ? skillFunctionNameParts[1] : skillFunctionName;
210216
}
211217

0 commit comments

Comments
 (0)