diff --git a/dotnet/src/Extensions/Planning.ActionPlanner/ActionPlanner.cs b/dotnet/src/Extensions/Planning.ActionPlanner/ActionPlanner.cs index 1ba7cabbd2825..32e2fc095b953 100644 --- a/dotnet/src/Extensions/Planning.ActionPlanner/ActionPlanner.cs +++ b/dotnet/src/Extensions/Planning.ActionPlanner/ActionPlanner.cs @@ -145,9 +145,9 @@ public async Task CreatePlanAsync(string goal) /// Function execution context /// List of functions, formatted accordingly to the prompt [SKFunction("List all functions available in the kernel")] - [SKFunctionName("ListOfFunctions")] - [SKFunctionInput(Description = "The current goal processed by the planner", DefaultValue = "")] - public string ListOfFunctions(string goal, SKContext context) + public string ListOfFunctions( + [SKFunctionInput("The current goal processed by the planner")] string goal, + SKContext context) { Verify.NotNull(context.Skills); var functionsAvailable = context.Skills.GetFunctionsView(); @@ -163,9 +163,9 @@ public string ListOfFunctions(string goal, SKContext context) // TODO: generate string programmatically // TODO: use goal to find relevant examples [SKFunction("List a few good examples of plans to generate")] - [SKFunctionName("GoodExamples")] - [SKFunctionInput(Description = "The current goal processed by the planner", DefaultValue = "")] - public string GoodExamples(string goal, SKContext context) + public string GoodExamples( + [SKFunctionInput("The current goal processed by the planner")] string goal, + SKContext context) { return @" [EXAMPLE] @@ -198,9 +198,9 @@ No parameters. // TODO: generate string programmatically [SKFunction("List a few edge case examples of plans to handle")] - [SKFunctionName("EdgeCaseExamples")] - [SKFunctionInput(Description = "The current goal processed by the planner", DefaultValue = "")] - public string EdgeCaseExamples(string goal, SKContext context) + public string EdgeCaseExamples( + [SKFunctionInput("The current goal processed by the planner")] string goal, + SKContext context) { return @" [EXAMPLE] diff --git a/dotnet/src/IntegrationTests/Fakes/EmailSkillFake.cs b/dotnet/src/IntegrationTests/Fakes/EmailSkillFake.cs index 5491dce8ce792..d3de85e45d3a5 100644 --- a/dotnet/src/IntegrationTests/Fakes/EmailSkillFake.cs +++ b/dotnet/src/IntegrationTests/Fakes/EmailSkillFake.cs @@ -10,18 +10,24 @@ namespace SemanticKernel.IntegrationTests.Fakes; internal sealed class EmailSkillFake { [SKFunction("Given an email address and message body, send an email")] - [SKFunctionInput(Description = "The body of the email message to send.")] - [SKFunctionContextParameter(Name = "email_address", Description = "The email address to send email to.", DefaultValue = "default@email.com")] - public Task SendEmailAsync(string input, SKContext context) + public Task SendEmailAsync( + [SKFunctionInput("The body of the email message to send.")] string input, + [SKFunctionContextParameter("The email address to send email to.", DefaultValue = "default@email.com")] string? email_address, + SKContext context) { - context.Variables.Get("email_address", out string emailAddress); - context.Variables.Update($"Sent email to: {emailAddress}. Body: {input}"); + if (string.IsNullOrWhiteSpace(email_address)) + { + email_address = "default@email.com"; + } + + context.Variables.Update($"Sent email to: {email_address}. Body: {input}"); return Task.FromResult(context); } [SKFunction("Lookup an email address for a person given a name")] - [SKFunctionInput(Description = "The name of the person to email.")] - public Task GetEmailAddressAsync(string input, SKContext context) + public Task GetEmailAddressAsync( + [SKFunctionInput("The name of the person to email.")] string input, + SKContext context) { if (string.IsNullOrEmpty(input)) { @@ -38,8 +44,9 @@ public Task GetEmailAddressAsync(string input, SKContext context) } [SKFunction("Write a short poem for an e-mail")] - [SKFunctionInput(Description = "The topic of the poem.")] - public Task WritePoemAsync(string input, SKContext context) + public Task WritePoemAsync( + [SKFunctionInput("The topic of the poem.")] string input, + SKContext context) { context.Variables.Update($"Roses are red, violets are blue, {input} is hard, so is this test."); return Task.FromResult(context); diff --git a/dotnet/src/IntegrationTests/TemplateLanguage/PromptTemplateEngineTests.cs b/dotnet/src/IntegrationTests/TemplateLanguage/PromptTemplateEngineTests.cs index bcdb89a62df06..b94e0942e5606 100644 --- a/dotnet/src/IntegrationTests/TemplateLanguage/PromptTemplateEngineTests.cs +++ b/dotnet/src/IntegrationTests/TemplateLanguage/PromptTemplateEngineTests.cs @@ -163,15 +163,13 @@ public static IEnumerable GetTemplateLanguageTests() public class MySkill { - [SKFunction("This is a test")] - [SKFunctionName("check123")] + [SKFunction("This is a test", Name = "check123")] public string MyFunction(string input) { return input == "123" ? "123 ok" : input + " != 123"; } - [SKFunction("This is a test")] - [SKFunctionName("asis")] + [SKFunction("This is a test", Name = "asis")] public string MyFunction2(string input) { return input; diff --git a/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionAttribute.cs b/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionAttribute.cs index a63af1604aebc..5872f406863d6 100644 --- a/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionAttribute.cs +++ b/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionAttribute.cs @@ -5,25 +5,31 @@ namespace Microsoft.SemanticKernel.SkillDefinition; /// -/// Attribute required to register native functions into the kernel. -/// The registration is required by the prompt templating engine and by the pipeline generator (aka planner). -/// The quality of the description affects the planner ability to reason about complex tasks. -/// The description is used both with LLM prompts and embedding comparisons. +/// Specifies that a method is a native function available to Semantic Kernel. /// +/// +/// For a method to be recognized by the kernel as a native function, it must be tagged with this attribute. +/// The supplied description is used both with LLM prompts and embedding comparisons. +/// The quality of the description affects the planner ability to reason about complex tasks. +/// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class SKFunctionAttribute : Attribute { /// - /// Function description, to be used by the planner to auto-discover functions. - /// - public string Description { get; } - - /// - /// Tag a C# function as a native function available to SK. + /// Initializes the attribute with the specified description. /// - /// Function description, to be used by the planner to auto-discover functions. + /// Description of the function to be used by a planner to auto-discover functions. public SKFunctionAttribute(string description) { this.Description = description; } + + /// + /// Gets the description of the function to be used by a planner to auto-discover functions. + /// + public string Description { get; } + + /// Gets or sets an optional name used for the function in the skill collection. + /// If not specified, the name of the attributed method will be used. + public string? Name { get; set; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionContextParameterAttribute.cs b/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionContextParameterAttribute.cs index 73dfe21c1fe3d..0856fd7047023 100644 --- a/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionContextParameterAttribute.cs +++ b/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionContextParameterAttribute.cs @@ -6,22 +6,36 @@ namespace Microsoft.SemanticKernel.SkillDefinition; /// -/// Attribute to describe the parameters required by a native function. -/// -/// Note: the class has no ctor, to force the use of setters and keep the attribute use readable -/// e.g. -/// Readable: [SKFunctionContextParameter(Name = "...", Description = "...", DefaultValue = "...")] -/// Not readable: [SKFunctionContextParameter("...", "...", "...")] +/// Attribute to describe a parameters used by a native function. /// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +/// +/// The attribute may be applied to parameters to a method attributed with +/// to provide additional information for that parameter, including a description, an optional default value, +/// and an optional name that may be used to override the name of the parameter as specified in source code. +/// The attribute may also be applied to a method itself to describe a context variable that is not specified +/// in the method's signature, in which case a name is required to identify which context variable is being described. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true)] public sealed class SKFunctionContextParameterAttribute : Attribute { - private string _name = ""; + private string? _name; + + public SKFunctionContextParameterAttribute(string description) => this.Description = description; + + /// + /// Gets the context parameter description. + /// + public string? Description { get; } /// - /// Parameter name. Alphanumeric chars + "_" only. + /// Gets or sets the name of the parameter. /// - public string Name + /// + /// This may only be ASCII letters or digits, or the underscore character. If this attribute is applied to a parameter, + /// this property is optional and the parameter name is used unless this property is set to override the name. If this + /// attribute is applied to a method, this property is required in order to identify which parameter is being described. + /// + public string? Name { get => this._name; set @@ -32,14 +46,16 @@ public string Name } /// - /// Parameter description. - /// - public string Description { get; set; } = string.Empty; - - /// - /// Default value when the value is not provided. + /// Gets or sets the default value of the parameter to use if no context variable is supplied matching the parameter name. /// - public string DefaultValue { get; set; } = string.Empty; + /// + /// There are two ways to supply a default value to a parameter. A default value can be supplied for the parameter in + /// the method signature itself, or a default value can be specified using this property. If both are specified, the + /// value in the attribute is used. The attribute is most useful when the target parameter is followed by a non-optional + /// parameter (such that this parameter isn't permitted to be optional) or when the attribute is applied to a method + /// to indicate a context parameter that is not specified as a method parameter but that's still used by the method body. + /// + public string? DefaultValue { get; set; } /// /// Creates a parameter view, using information from an instance of this class. @@ -54,9 +70,9 @@ public ParameterView ToParameterView() return new ParameterView { - Name = this.Name, - Description = this.Description, - DefaultValue = this.DefaultValue + Name = this.Name ?? string.Empty, + Description = this.Description ?? string.Empty, + DefaultValue = this.DefaultValue ?? string.Empty, }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionInputAttribute.cs b/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionInputAttribute.cs index 9b242367ef656..9ba5eb6f0b6cf 100644 --- a/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionInputAttribute.cs +++ b/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionInputAttribute.cs @@ -5,40 +5,44 @@ namespace Microsoft.SemanticKernel.SkillDefinition; /// -/// Attribute to describe the main parameter required by a native function, -/// e.g. the first "string" parameter, if the function requires one. +/// Attribute to describe the main "input" parameter required by a native function. /// /// -/// The class has no constructor and requires the use of setters for readability. -/// e.g. -/// Readable: [SKFunctionInput(Description = "...", DefaultValue = "...")] -/// Not readable: [SKFunctionInput("...", "...")] +/// The attribute may be applied to any string parameter in the function signature. +/// It may be applied to at most one parameter in a function signature. +/// The attribute allows providing a default value if no main Input is available. /// /// /// -/// // No main parameter here, only context -/// public async Task WriteAsync(SKContext context +/// // No main parameter here, only context +/// public async Task WriteAsync(SKContext context) /// /// /// /// -/// // "path" is the input parameter -/// [SKFunctionInput("Source file path")] -/// public async Task{string?} ReadAsync(string path, SKContext context +/// // No main parameter; path parameter is looked up using the name "path" +/// public async Task{string?} ReadAsync(string path, SKContext context) /// /// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +/// +/// +/// // Main parameter; path parameter is looked up using the main name "input" +/// public async Task{string?} ReadAsync([SKFunctionInput("Source file path")] string path, SKContext context) +/// +/// +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] public sealed class SKFunctionInputAttribute : Attribute { /// - /// Parameter description. + /// Initializes the attribute with the specified input parameter description. /// - public string Description { get; set; } = string.Empty; + /// The description. + public SKFunctionInputAttribute(string description) => this.Description = description; /// - /// Default value when the value is not provided. + /// Gets the parameter description. /// - public string DefaultValue { get; set; } = string.Empty; + public string Description { get; } /// /// Creates a parameter view, using information from an instance of this class. @@ -50,7 +54,7 @@ public ParameterView ToParameterView() { Name = "input", Description = this.Description, - DefaultValue = this.DefaultValue + DefaultValue = string.Empty, }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionNameAttribute.cs b/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionNameAttribute.cs deleted file mode 100644 index 0badbca51c043..0000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/SkillDefinition/SKFunctionNameAttribute.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.SemanticKernel.Diagnostics; - -namespace Microsoft.SemanticKernel.SkillDefinition; - -/// -/// Optional attribute to set the name used for the function in the skill collection. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public sealed class SKFunctionNameAttribute : Attribute -{ - /// - /// Function name - /// - public string Name { get; } - - /// - /// Tag a C# function as a native function available to SK. - /// - /// Function name - public SKFunctionNameAttribute(string name) - { - Verify.ValidFunctionName(name); - this.Name = name; - } -} diff --git a/dotnet/src/SemanticKernel.UnitTests/CoreSkills/FileIOSkillTests.cs b/dotnet/src/SemanticKernel.UnitTests/CoreSkills/FileIOSkillTests.cs index 0dfe8b2d53a04..ffd464f677a69 100644 --- a/dotnet/src/SemanticKernel.UnitTests/CoreSkills/FileIOSkillTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/CoreSkills/FileIOSkillTests.cs @@ -3,19 +3,14 @@ using System; using System.IO; using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.CoreSkills; -using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Orchestration; using Xunit; namespace SemanticKernel.UnitTests.CoreSkills; public class FileIOSkillTests { - private readonly SKContext _context = new(new ContextVariables(), NullMemory.Instance, null, NullLogger.Instance); - [Fact] public void ItCanBeInstantiated() { @@ -72,11 +67,9 @@ public async Task ItCanWriteAsync() // Arrange var skill = new FileIOSkill(); var path = Path.GetTempFileName(); - this._context["path"] = path; - this._context["content"] = "hello world"; // Act - await skill.WriteAsync(this._context); + await skill.WriteAsync(path, "hello world"); // Assert Assert.Equal("hello world", await File.ReadAllTextAsync(path)); @@ -89,13 +82,11 @@ public async Task ItCannotWriteAsync() var skill = new FileIOSkill(); var path = Path.GetTempFileName(); File.SetAttributes(path, FileAttributes.ReadOnly); - this._context["path"] = path; - this._context["content"] = "hello world"; // Act Task Fn() { - return skill.WriteAsync(this._context); + return skill.WriteAsync(path, "hello world"); } // Assert diff --git a/dotnet/src/SemanticKernel.UnitTests/CoreSkills/HttpSkillTests.cs b/dotnet/src/SemanticKernel.UnitTests/CoreSkills/HttpSkillTests.cs index 21e141d6afb54..a0f822847a3c0 100644 --- a/dotnet/src/SemanticKernel.UnitTests/CoreSkills/HttpSkillTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/CoreSkills/HttpSkillTests.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.CoreSkills; -using Microsoft.SemanticKernel.Orchestration; using Moq; using Moq.Protected; using Xunit; @@ -16,7 +15,6 @@ namespace SemanticKernel.UnitTests.CoreSkills; public class HttpSkillTests : IDisposable { - private readonly SKContext _context = new(); private readonly string _content = "hello world"; private readonly string _uriString = "http://www.example.com"; @@ -53,7 +51,7 @@ public async Task ItCanGetAsync() using var skill = new HttpSkill(client); // Act - var result = await skill.GetAsync(this._uriString, this._context); + var result = await skill.GetAsync(this._uriString); // Assert Assert.Equal(this._content, result); @@ -67,10 +65,9 @@ public async Task ItCanPostAsync() var mockHandler = this.CreateMock(); using var client = new HttpClient(mockHandler.Object); using var skill = new HttpSkill(client); - this._context["body"] = this._content; // Act - var result = await skill.PostAsync(this._uriString, this._context); + var result = await skill.PostAsync(this._uriString, this._content); // Assert Assert.Equal(this._content, result); @@ -84,10 +81,9 @@ public async Task ItCanPutAsync() var mockHandler = this.CreateMock(); using var client = new HttpClient(mockHandler.Object); using var skill = new HttpSkill(client); - this._context["body"] = this._content; // Act - var result = await skill.PutAsync(this._uriString, this._context); + var result = await skill.PutAsync(this._uriString, this._content); // Assert Assert.Equal(this._content, result); @@ -103,7 +99,7 @@ public async Task ItCanDeleteAsync() using var skill = new HttpSkill(client); // Act - var result = await skill.DeleteAsync(this._uriString, this._context); + var result = await skill.DeleteAsync(this._uriString); // Assert Assert.Equal(this._content, result); diff --git a/dotnet/src/SemanticKernel.UnitTests/CoreSkills/MathSkillTests.cs b/dotnet/src/SemanticKernel.UnitTests/CoreSkills/MathSkillTests.cs index 1c3cc2f405578..94e4021e25c2e 100644 --- a/dotnet/src/SemanticKernel.UnitTests/CoreSkills/MathSkillTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/CoreSkills/MathSkillTests.cs @@ -2,13 +2,8 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.CoreSkills; -using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.SkillDefinition; -using Moq; using Xunit; namespace SemanticKernel.UnitTests.CoreSkills; @@ -44,16 +39,10 @@ public void ItCanBeImported() public async Task AddAsyncWhenValidParametersShouldSucceedAsync(string initialValue, string amount, string expectedResult) { // Arrange - var variables = new ContextVariables - { - ["Amount"] = amount - }; - - var context = new SKContext(variables, new Mock().Object, new Mock().Object, new Mock().Object); var target = new MathSkill(); // Act - string result = await target.AddAsync(initialValue, context); + string result = await target.AddAsync(initialValue, amount); // Assert Assert.Equal(expectedResult, result); @@ -71,16 +60,10 @@ public async Task AddAsyncWhenValidParametersShouldSucceedAsync(string initialVa public async Task SubtractAsyncWhenValidParametersShouldSucceedAsync(string initialValue, string amount, string expectedResult) { // Arrange - var variables = new ContextVariables - { - ["Amount"] = amount - }; - - var context = new SKContext(variables, new Mock().Object, new Mock().Object, new Mock().Object); var target = new MathSkill(); // Act - string result = await target.SubtractAsync(initialValue, context); + string result = await target.SubtractAsync(initialValue, amount); // Assert Assert.Equal(expectedResult, result); @@ -101,18 +84,12 @@ public async Task SubtractAsyncWhenValidParametersShouldSucceedAsync(string init public async Task AddAsyncWhenInvalidInitialValueShouldThrowAsync(string initialValue) { // Arrange - var variables = new ContextVariables - { - ["Amount"] = "1" - }; - - var context = new SKContext(variables, new Mock().Object, new Mock().Object, new Mock().Object); var target = new MathSkill(); // Act var exception = await Assert.ThrowsAsync(async () => { - await target.AddAsync(initialValue, context); + await target.AddAsync(initialValue, "1"); }); // Assert @@ -136,24 +113,18 @@ public async Task AddAsyncWhenInvalidInitialValueShouldThrowAsync(string initial public async Task AddAsyncWhenInvalidAmountShouldThrowAsync(string amount) { // Arrange - var variables = new ContextVariables - { - ["Amount"] = amount - }; - - var context = new SKContext(variables, new Mock().Object, new Mock().Object, new Mock().Object); var target = new MathSkill(); // Act var exception = await Assert.ThrowsAsync(async () => { - await target.AddAsync("1", context); + await target.AddAsync("1", amount); }); // Assert Assert.NotNull(exception); Assert.Equal(amount, exception.ActualValue); - Assert.Equal("context", exception.ParamName); + Assert.Equal("amount", exception.ParamName); } [Theory] @@ -171,18 +142,12 @@ public async Task AddAsyncWhenInvalidAmountShouldThrowAsync(string amount) public async Task SubtractAsyncWhenInvalidInitialValueShouldThrowAsync(string initialValue) { // Arrange - var variables = new ContextVariables - { - ["Amount"] = "1" - }; - - var context = new SKContext(variables, new Mock().Object, new Mock().Object, new Mock().Object); var target = new MathSkill(); // Act var exception = await Assert.ThrowsAsync(async () => { - await target.SubtractAsync(initialValue, context); + await target.SubtractAsync(initialValue, "1"); }); // Assert @@ -206,23 +171,17 @@ public async Task SubtractAsyncWhenInvalidInitialValueShouldThrowAsync(string in public async Task SubtractAsyncWhenInvalidAmountShouldThrowAsync(string amount) { // Arrange - var variables = new ContextVariables - { - ["Amount"] = amount - }; - - var context = new SKContext(variables, new Mock().Object, new Mock().Object, new Mock().Object); var target = new MathSkill(); // Act var exception = await Assert.ThrowsAsync(async () => { - await target.SubtractAsync("1", context); + await target.SubtractAsync("1", amount); }); // Assert Assert.NotNull(exception); Assert.Equal(amount, exception.ActualValue); - Assert.Equal("context", exception.ParamName); + Assert.Equal("amount", exception.ParamName); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/CoreSkills/TextSkillTests.cs b/dotnet/src/SemanticKernel.UnitTests/CoreSkills/TextSkillTests.cs index 138e17982b469..447b5e2baf107 100644 --- a/dotnet/src/SemanticKernel.UnitTests/CoreSkills/TextSkillTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/CoreSkills/TextSkillTests.cs @@ -1,12 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.CoreSkills; -using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Orchestration; -using Microsoft.SemanticKernel.SkillDefinition; -using Moq; using Xunit; namespace SemanticKernel.UnitTests.CoreSkills; @@ -126,17 +121,11 @@ public void ItCanLength(string textToLength, int expectedLength) public void ItCanConcat(string textToConcat, string text2ToConcat) { // Arrange - var variables = new ContextVariables - { - ["input2"] = text2ToConcat - }; - - var context = new SKContext(variables, new Mock().Object, new Mock().Object, new Mock().Object); var target = new TextSkill(); var expected = string.Concat(textToConcat, text2ToConcat); // Act - string result = target.Concat(textToConcat, context); + string result = target.Concat(textToConcat, text2ToConcat); // Assert Assert.Equal(expected, result); diff --git a/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKContextTests.cs b/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKContextTests.cs index 3f76e71e188e8..46842f685ef3a 100644 --- a/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKContextTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKContextTests.cs @@ -68,9 +68,9 @@ private sealed class Parrot { [SKFunction("say something")] // ReSharper disable once UnusedMember.Local - public string Say(string text) + public string Say(string input) { - return text; + return input; } } } diff --git a/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKFunctionTests2.cs b/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKFunctionTests2.cs index a150ecb3dbf0a..2473fdc5a66fc 100644 --- a/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKFunctionTests2.cs +++ b/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKFunctionTests2.cs @@ -32,7 +32,6 @@ public async Task ItSupportsStaticVoidVoidAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static void Test() { s_actual = s_expected; @@ -55,7 +54,6 @@ public async Task ItSupportsStaticVoidStringAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static string Test() { s_actual = s_expected; @@ -81,7 +79,6 @@ public async Task ItSupportsStaticVoidTaskStringAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static Task Test() { s_actual = s_expected; @@ -107,7 +104,6 @@ public async Task ItSupportsStaticContextVoidAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static void Test(SKContext cx) { s_actual = s_expected; @@ -133,7 +129,6 @@ public async Task ItSupportsStaticContextStringAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static string Test(SKContext cx) { s_actual = cx["someVar"]; @@ -161,7 +156,6 @@ public async Task ItSupportsInstanceContextStringNullableAsync() int invocationCount = 0; [SKFunction("Test")] - [SKFunctionName("Test")] string? Test(SKContext cx) { invocationCount++; @@ -192,7 +186,6 @@ public async Task ItSupportsInstanceContextTaskStringAsync() int invocationCount = 0; [SKFunction("Test")] - [SKFunctionName("Test")] Task Test(SKContext cx) { invocationCount++; @@ -224,7 +217,6 @@ public async Task ItSupportsInstanceContextTaskContextAsync() int invocationCount = 0; [SKFunction("Test")] - [SKFunctionName("Test")] async Task TestAsync(SKContext cx) { await Task.Delay(0); @@ -258,7 +250,6 @@ public async Task ItSupportsInstanceStringVoidAsync() int invocationCount = 0; [SKFunction("Test")] - [SKFunctionName("Test")] void Test(string input) { invocationCount++; @@ -286,7 +277,6 @@ public async Task ItSupportsInstanceStringStringAsync() int invocationCount = 0; [SKFunction("Test")] - [SKFunctionName("Test")] string Test(string input) { invocationCount++; @@ -316,7 +306,6 @@ public async Task ItSupportsInstanceStringTaskStringAsync() int invocationCount = 0; [SKFunction("Test")] - [SKFunctionName("Test")] Task Test(string input) { invocationCount++; @@ -346,7 +335,6 @@ public async Task ItSupportsInstanceStringContextVoidAsync() int invocationCount = 0; [SKFunction("Test")] - [SKFunctionName("Test")] void Test(string input, SKContext cx) { invocationCount++; @@ -378,7 +366,6 @@ public async Task ItSupportsInstanceContextStringVoidAsync() int invocationCount = 0; [SKFunction("Test")] - [SKFunctionName("Test")] void Test(SKContext cx, string input) { invocationCount++; @@ -408,7 +395,6 @@ public async Task ItSupportsStaticStringContextStringAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static string Test(string input, SKContext cx) { s_actual = s_expected; @@ -437,7 +423,6 @@ public async Task ItSupportsStaticStringContextTaskStringAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static Task Test(string input, SKContext cx) { s_actual = s_expected; @@ -466,7 +451,6 @@ public async Task ItSupportsStaticStringContextTaskContextAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static Task Test(string input, SKContext cx) { s_actual = s_expected; @@ -519,7 +503,6 @@ public async Task ItSupportsStaticStringTaskAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static Task TestAsync(string input) { s_actual = s_expected; @@ -543,7 +526,6 @@ public async Task ItSupportsStaticContextTaskAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static Task TestAsync(SKContext cx) { s_actual = s_expected; @@ -571,7 +553,6 @@ public async Task ItSupportsStaticStringContextTaskAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static Task TestAsync(string input, SKContext cx) { s_actual = s_expected; @@ -599,7 +580,6 @@ public async Task ItSupportsStaticVoidTaskAsync() { // Arrange [SKFunction("Test")] - [SKFunctionName("Test")] static Task TestAsync() { s_actual = s_expected; @@ -618,6 +598,98 @@ static Task TestAsync() Assert.Equal(s_expected, s_actual); } + [Fact] + public async Task ItSupportsUsingNamedInputValueFromContext() + { + [SKFunction("Test")] + static string Test(string input) => "Result: " + input; + + var context = this.MockContext("input value"); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test)); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + Assert.Equal(result.Variables.Input, "Result: input value"); + } + + [Fact] + public async Task ItSupportsUsingNonNamedInputValueFromContext() + { + [SKFunction("Test")] + static string Test(string other) => "Result: " + other; + + var context = this.MockContext("input value"); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test)); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + Assert.Equal(result.Variables.Input, "Result: input value"); + } + + [Fact] + public async Task ItSupportsPreferringNamedValueOverInputFromContext() + { + [SKFunction("Test")] + static string Test(string other) => "Result: " + other; + + var context = this.MockContext("input value"); + context.Variables.Set("other", "other value"); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test)); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + Assert.Equal(result.Variables.Input, "Result: other value"); + } + + [Fact] + public async Task ItSupportsOverridingNameWithAttribute() + { + [SKFunction("Test")] + static string Test([SKFunctionInput("description")] string other) => "Result: " + other; + + var context = this.MockContext("input value"); + context.Variables.Set("other", "other value"); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test)); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + Assert.Equal(result.Variables.Input, "Result: input value"); + } + + [Fact] + public async Task ItSupportNullDefaultValuesOverInput() + { + [SKFunction("Test")] + static string Test(string? input = null, string? other = null) => "Result: " + (other is null); + + var context = this.MockContext("input value"); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test)); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + Assert.Equal(result.Variables.Input, "Result: True"); + } + private static MethodInfo Method(Delegate method) { return method.Method; diff --git a/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKFunctionTests3.cs b/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKFunctionTests3.cs index fc675ac8d80e4..6dfe3ff815ac5 100644 --- a/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKFunctionTests3.cs +++ b/dotnet/src/SemanticKernel.UnitTests/SkillDefinition/SKFunctionTests3.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; @@ -54,14 +55,14 @@ public void ItThrowsForInvalidFunctions() { SKFunction.FromNativeMethod(method, instance, "skill"); } - catch (KernelException e) when (e.ErrorCode == KernelException.ErrorCodes.FunctionTypeNotSupported) + catch (KernelException e) when (e.ErrorCode is KernelException.ErrorCodes.FunctionTypeNotSupported or KernelException.ErrorCodes.InvalidFunctionDescription) { count++; } } // Assert - Assert.Equal(2, count); + Assert.Equal(9, count); } [Fact] @@ -134,14 +135,49 @@ async Task ExecuteAsync(SKContext contextIn) private sealed class InvalidSkill { [SKFunction("one")] - public void Invalid1(string x, string y) + public void Invalid1([SKFunctionInput("The x parameter")] string x, [SKFunctionInput("The y parameter")] string y) { } - [SKFunction("three")] + [SKFunction("two")] public void Invalid2(string y, int n) { } + + [SKFunction("three")] + public void Invalid3(SKContext context1, SKContext context2) + { + } + + [SKFunction("four")] + public void Invalid4(CancellationToken ct1, CancellationToken ct2) + { + } + + [SKFunction("five")] + public void Invalid5([SKFunctionInput("x")][SKFunctionContextParameter("x")] string x) + { + } + + [SKFunction("six")] + public void Invalid6([SKFunctionInput("x")] CancellationToken ct) + { + } + + [SKFunction("seven")] + public void Invalid7([SKFunctionInput("x")] SKContext context) + { + } + + [SKFunction("eight")] + public void Invalid8([SKFunctionContextParameter("x")] CancellationToken ct) + { + } + + [SKFunction("nine")] + public void Invalid9([SKFunctionContextParameter("x")] SKContext context) + { + } } private sealed class LocalExampleSkill @@ -214,73 +250,73 @@ public async Task Type07Async(SKContext context) } [SKFunction("eight")] - public void Type08(string x) + public void Type08(string input) { } [SKFunction("eight2")] - public void Type08Nullable(string? x) + public void Type08Nullable(string? input) { } [SKFunction("nine")] - public string Type09(string x) + public string Type09(string input) { return ""; } [SKFunction("nine2")] - public string? Type09Nullable(string? x = null) + public string? Type09Nullable(string? input = null) { return ""; } [SKFunction("ten")] - public async Task Type10Async(string x) + public async Task Type10Async(string input) { await Task.Delay(0); return ""; } [SKFunction("ten2")] - public async Task Type10NullableAsync(string? x) + public async Task Type10NullableAsync(string? input) { await Task.Delay(0); return ""; } [SKFunction("eleven")] - public void Type11(string x, SKContext context) + public void Type11(string input, SKContext context) { } [SKFunction("eleven2")] - public void Type11Nullable(string? x = null, SKContext? context = null) + public void Type11Nullable(string? input = null, SKContext? context = null) { } [SKFunction("twelve")] - public string Type12(string x, SKContext context) + public string Type12(string input, SKContext context) { return ""; } [SKFunction("thirteen")] - public async Task Type13Async(string x, SKContext context) + public async Task Type13Async(string input, SKContext context) { await Task.Delay(0); return ""; } [SKFunction("fourteen")] - public async Task Type14Async(string x, SKContext context) + public async Task Type14Async(string input, SKContext context) { await Task.Delay(0); return context; } [SKFunction("fifteen")] - public async Task Type15Async(string x) + public async Task Type15Async(string input) { await Task.Delay(0); } @@ -292,7 +328,7 @@ public async Task Type16Async(SKContext context) } [SKFunction("seventeen")] - public async Task Type17Async(string x, SKContext context) + public async Task Type17Async(string input, SKContext context) { await Task.Delay(0); } diff --git a/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/PromptTemplateEngineTests.cs b/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/PromptTemplateEngineTests.cs index 20272abfcdb07..8a5b1cddc9056 100644 --- a/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/PromptTemplateEngineTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/PromptTemplateEngineTests.cs @@ -122,8 +122,7 @@ public void ItRendersVariables() public async Task ItRendersCodeUsingInputAsync() { // Arrange - [SKFunction("test")] - [SKFunctionName("test")] + [SKFunction("test", Name = "test")] string MyFunctionAsync(SKContext cx) { this._logger.WriteLine("MyFunction call received, input: {0}", cx.Variables.Input); @@ -153,8 +152,7 @@ string MyFunctionAsync(SKContext cx) public async Task ItRendersCodeUsingVariablesAsync() { // Arrange - [SKFunction("test")] - [SKFunctionName("test")] + [SKFunction("test", Name = "test")] string MyFunctionAsync(SKContext cx) { this._logger.WriteLine("MyFunction call received, input: {0}", cx.Variables.Input); @@ -184,8 +182,7 @@ string MyFunctionAsync(SKContext cx) public async Task ItRendersAsyncCodeUsingVariablesAsync() { // Arrange - [SKFunction("test")] - [SKFunctionName("test")] + [SKFunction("test", Name = "test")] Task MyFunctionAsync(SKContext cx) { // Input value should be "BAR" because the variable $myVar is passed in diff --git a/dotnet/src/SemanticKernel/CoreSkills/ConversationSummarySkill.cs b/dotnet/src/SemanticKernel/CoreSkills/ConversationSummarySkill.cs index 2ee81d840a31d..0ec597d9c92d3 100644 --- a/dotnet/src/SemanticKernel/CoreSkills/ConversationSummarySkill.cs +++ b/dotnet/src/SemanticKernel/CoreSkills/ConversationSummarySkill.cs @@ -64,10 +64,8 @@ public ConversationSummarySkill(IKernel kernel) /// /// A long conversation transcript. /// The SKContext for function execution. - [SKFunction("Given a long conversation transcript, summarize the conversation.")] - [SKFunctionName("SummarizeConversation")] - [SKFunctionInput(Description = "A long conversation transcript.")] - public Task SummarizeConversationAsync(string input, SKContext context) + [SKFunction("Given a long conversation transcript, summarize the conversation.", Name = "SummarizeConversation")] + public Task SummarizeConversationAsync([SKFunctionInput("A long conversation transcript.")] string input, SKContext context) { List lines = TextChunker.SplitPlainTextLines(input, MaxTokens); List paragraphs = TextChunker.SplitPlainTextParagraphs(lines, MaxTokens); @@ -81,10 +79,8 @@ public Task SummarizeConversationAsync(string input, SKContext contex /// /// A long conversation transcript. /// The SKContext for function execution. - [SKFunction("Given a long conversation transcript, identify action items.")] - [SKFunctionName("GetConversationActionItems")] - [SKFunctionInput(Description = "A long conversation transcript.")] - public Task GetConversationActionItemsAsync(string input, SKContext context) + [SKFunction("Given a long conversation transcript, identify action items.", Name = "GetConversationActionItems")] + public Task GetConversationActionItemsAsync([SKFunctionInput("A long conversation transcript.")] string input, SKContext context) { List lines = TextChunker.SplitPlainTextLines(input, MaxTokens); List paragraphs = TextChunker.SplitPlainTextParagraphs(lines, MaxTokens); @@ -98,10 +94,8 @@ public Task GetConversationActionItemsAsync(string input, SKContext c /// /// A long conversation transcript. /// The SKContext for function execution. - [SKFunction("Given a long conversation transcript, identify topics worth remembering.")] - [SKFunctionName("GetConversationTopics")] - [SKFunctionInput(Description = "A long conversation transcript.")] - public Task GetConversationTopicsAsync(string input, SKContext context) + [SKFunction("Given a long conversation transcript, identify topics worth remembering.", Name = "GetConversationTopics")] + public Task GetConversationTopicsAsync([SKFunctionInput("A long conversation transcript.")] string input, SKContext context) { List lines = TextChunker.SplitPlainTextLines(input, MaxTokens); List paragraphs = TextChunker.SplitPlainTextParagraphs(lines, MaxTokens); diff --git a/dotnet/src/SemanticKernel/CoreSkills/FileIOSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/FileIOSkill.cs index 4450826fba2f4..01353869228e8 100644 --- a/dotnet/src/SemanticKernel/CoreSkills/FileIOSkill.cs +++ b/dotnet/src/SemanticKernel/CoreSkills/FileIOSkill.cs @@ -4,7 +4,6 @@ using System.IO; using System.Text; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; namespace Microsoft.SemanticKernel.CoreSkills; @@ -29,8 +28,7 @@ public class FileIOSkill /// Source file /// File content [SKFunction("Read a file")] - [SKFunctionInput(Description = "Source file")] - public async Task ReadAsync(string path) + public async Task ReadAsync([SKFunctionInput("Source file")] string path) { using var reader = File.OpenText(path); return await reader.ReadToEndAsync().ConfigureAwait(false); @@ -42,17 +40,15 @@ public async Task ReadAsync(string path) /// /// {{file.writeAsync}} /// - /// - /// Contains the 'path' for the Destination file and 'content' of the file to write. - /// + /// The destination file path + /// The file content to write /// An awaitable task [SKFunction("Write a file")] - [SKFunctionContextParameter(Name = "path", Description = "Destination file")] - [SKFunctionContextParameter(Name = "content", Description = "File content")] - public async Task WriteAsync(SKContext context) + public async Task WriteAsync( + [SKFunctionContextParameter("Destination file")] string path, + [SKFunctionContextParameter("File content")] string content) { - byte[] text = Encoding.UTF8.GetBytes(context["content"]); - string path = context["path"]; + byte[] text = Encoding.UTF8.GetBytes(content); if (File.Exists(path) && File.GetAttributes(path).HasFlag(FileAttributes.ReadOnly)) { // Most environments will throw this with OpenWrite, but running inside docker on Linux will not. diff --git a/dotnet/src/SemanticKernel/CoreSkills/HttpSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/HttpSkill.cs index 9e81c4d69600d..af37cce471849 100644 --- a/dotnet/src/SemanticKernel/CoreSkills/HttpSkill.cs +++ b/dotnet/src/SemanticKernel/CoreSkills/HttpSkill.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; namespace Microsoft.SemanticKernel.CoreSkills; @@ -23,7 +22,7 @@ namespace Microsoft.SemanticKernel.CoreSkills; /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Semantic Kernel operates on strings")] -public class HttpSkill : IDisposable +public sealed class HttpSkill : IDisposable { private static readonly HttpClientHandler s_httpClientHandler = new() { CheckCertificateRevocationList = true }; private readonly HttpClient _client; @@ -49,50 +48,60 @@ public HttpSkill(HttpClient client) => /// Sends an HTTP GET request to the specified URI and returns the response body as a string. /// /// URI of the request - /// The context for the operation. + /// The token to use to request cancellation. /// The response body as a string. [SKFunction("Makes a GET request to a uri")] - public Task GetAsync(string uri, SKContext context) => - this.SendRequestAsync(uri, HttpMethod.Get, cancellationToken: context.CancellationToken); + public Task GetAsync( + [SKFunctionInput("The URI of the request")] string uri, + CancellationToken cancellationToken = default) => + this.SendRequestAsync(uri, HttpMethod.Get, requestContent: null, cancellationToken); /// /// Sends an HTTP POST request to the specified URI and returns the response body as a string. /// /// URI of the request - /// Contains the body of the request + /// The body of the request + /// The token to use to request cancellation. /// The response body as a string. [SKFunction("Makes a POST request to a uri")] - [SKFunctionContextParameter(Name = "body", Description = "The body of the request")] - public Task PostAsync(string uri, SKContext context) => - this.SendRequestAsync(uri, HttpMethod.Post, new StringContent(context["body"]), context.CancellationToken); + public Task PostAsync( + [SKFunctionInput("The URI of the request")] string uri, + [SKFunctionContextParameter("The body of the request")] string body, + CancellationToken cancellationToken = default) => + this.SendRequestAsync(uri, HttpMethod.Post, new StringContent(body), cancellationToken); /// /// Sends an HTTP PUT request to the specified URI and returns the response body as a string. /// /// URI of the request - /// Contains the body of the request + /// The body of the request + /// The token to use to request cancellation. /// The response body as a string. [SKFunction("Makes a PUT request to a uri")] - [SKFunctionContextParameter(Name = "body", Description = "The body of the request")] - public Task PutAsync(string uri, SKContext context) => - this.SendRequestAsync(uri, HttpMethod.Put, new StringContent(context["body"]), context.CancellationToken); + public Task PutAsync( + [SKFunctionInput("The URI of the request")] string uri, + [SKFunctionContextParameter("The body of the request")] string body, + CancellationToken cancellationToken = default) => + this.SendRequestAsync(uri, HttpMethod.Put, new StringContent(body), cancellationToken); /// /// Sends an HTTP DELETE request to the specified URI and returns the response body as a string. /// /// URI of the request - /// The context for the operation. + /// The token to use to request cancellation. /// The response body as a string. [SKFunction("Makes a DELETE request to a uri")] - public Task DeleteAsync(string uri, SKContext context) => - this.SendRequestAsync(uri, HttpMethod.Delete, cancellationToken: context.CancellationToken); + public Task DeleteAsync( + [SKFunctionInput("The URI of the request")] string uri, + CancellationToken cancellationToken = default) => + this.SendRequestAsync(uri, HttpMethod.Delete, requestContent: null, cancellationToken); /// Sends an HTTP request and returns the response content as a string. /// The URI of the request. /// The HTTP method for the request. /// Optional request content. /// The token to use to request cancellation. - private async Task SendRequestAsync(string uri, HttpMethod method, HttpContent? requestContent = null, CancellationToken cancellationToken = default) + private async Task SendRequestAsync(string uri, HttpMethod method, HttpContent? requestContent, CancellationToken cancellationToken) { using var request = new HttpRequestMessage(method, uri) { Content = requestContent }; using var response = await this._client.SendAsync(request, cancellationToken).ConfigureAwait(false); @@ -102,21 +111,5 @@ private async Task SendRequestAsync(string uri, HttpMethod method, HttpC /// /// Disposes resources /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Dispose internal resources - /// - /// Whether the method is explicitly called by the public Dispose method - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - this._client.Dispose(); - } - } + public void Dispose() => this._client.Dispose(); } diff --git a/dotnet/src/SemanticKernel/CoreSkills/MathSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/MathSkill.cs index 7d48e2f166adf..64453e6982c34 100644 --- a/dotnet/src/SemanticKernel/CoreSkills/MathSkill.cs +++ b/dotnet/src/SemanticKernel/CoreSkills/MathSkill.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; namespace Microsoft.SemanticKernel.CoreSkills; @@ -22,29 +21,27 @@ public class MathSkill /// Returns the Addition result of initial and amount values provided. /// /// Initial value as string to add the specified amount - /// Contains the context to get the numbers from + /// The amount to add as a string. /// The resulting sum as a string. - [SKFunction("Adds value to a value")] - [SKFunctionName("Add")] - [SKFunctionInput(Description = "The value to add")] - [SKFunctionContextParameter(Name = "Amount", Description = "Amount to add")] - public Task AddAsync(string initialValueText, SKContext context) => - AddOrSubtractAsync(initialValueText, context, add: true); + [SKFunction("Adds value to a value", Name = "Add")] + public Task AddAsync( + [SKFunctionInput("The value to add")] string initialValueText, + [SKFunctionContextParameter("Amount to add")] string amount) => + AddOrSubtractAsync(initialValueText, amount, add: true); /// /// Returns the Sum of two SKContext numbers provided. /// /// Initial value as string to subtract the specified amount - /// Contains the context to get the numbers from + /// The amount to subtract as a string. /// The resulting subtraction as a string. - [SKFunction("Subtracts value to a value")] - [SKFunctionName("Subtract")] - [SKFunctionInput(Description = "The value to subtract")] - [SKFunctionContextParameter(Name = "Amount", Description = "Amount to subtract")] - public Task SubtractAsync(string initialValueText, SKContext context) => - AddOrSubtractAsync(initialValueText, context, add: false); + [SKFunction("Subtracts value to a value", Name = "Subtract")] + public Task SubtractAsync( + [SKFunctionInput("The value to subtract")] string initialValueText, + [SKFunctionContextParameter("Amount to add")] string amount) => + AddOrSubtractAsync(initialValueText, amount, add: false); - private static Task AddOrSubtractAsync(string initialValueText, SKContext context, bool add) + private static Task AddOrSubtractAsync(string initialValueText, string amount, bool add) { if (!int.TryParse(initialValueText, NumberStyles.Any, CultureInfo.InvariantCulture, out var initialValue)) { @@ -52,16 +49,15 @@ private static Task AddOrSubtractAsync(string initialValueText, SKContex nameof(initialValueText), initialValueText, "Initial value provided is not in numeric format")); } - string contextAmount = context["Amount"]; - if (!int.TryParse(contextAmount, NumberStyles.Any, CultureInfo.InvariantCulture, out var amount)) + if (!int.TryParse(amount, NumberStyles.Any, CultureInfo.InvariantCulture, out var amountValue)) { return Task.FromException(new ArgumentOutOfRangeException( - nameof(context), contextAmount, "Context amount provided is not in numeric format")); + nameof(amount), amount, "Context amount provided is not in numeric format")); } var result = add - ? initialValue + amount - : initialValue - amount; + ? initialValue + amountValue + : initialValue - amountValue; return Task.FromResult(result.ToString(CultureInfo.InvariantCulture)); } diff --git a/dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs b/dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs index bf93ccfa1441b..300423c8bffda 100644 --- a/dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs +++ b/dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text.Json; @@ -64,22 +65,21 @@ public TextMemorySkill(string collection = DefaultCollection, string relevance = /// /// Key-based lookup for a specific memory /// + /// Memories collection associated with the memory to retrieve + /// The key associated with the memory to retrieve. + /// Context containing the memory /// /// SKContext[TextMemorySkill.KeyParam] = "countryInfo1" /// {{memory.retrieve }} /// - /// Contains the 'collection' containing the memory to retrieve and the `key` associated with it. - [SKFunction("Key-based lookup for a specific memory")] - [SKFunctionName("Retrieve")] - [SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection associated with the memory to retrieve", - DefaultValue = DefaultCollection)] - [SKFunctionContextParameter(Name = KeyParam, Description = "The key associated with the memory to retrieve")] - public async Task RetrieveAsync(SKContext context) + [SKFunction("Key-based lookup for a specific memory", Name = "Retrieve")] + public async Task RetrieveAsync( + [SKFunctionContextParameter("Memories collection associated with the memory to retrieve", DefaultValue = DefaultCollection)] string? collection, + [SKFunctionContextParameter("The key associated with the memory to retrieve")] string key, + SKContext context) { - var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : DefaultCollection; + collection ??= DefaultCollection; Verify.NotNullOrWhiteSpace(collection, $"{nameof(context)}.{nameof(context.Variables)}[{CollectionParam}]"); - - var key = context.Variables.ContainsKey(KeyParam) ? context[KeyParam] : string.Empty; Verify.NotNullOrWhiteSpace(key, $"{nameof(context)}.{nameof(context.Variables)}[{KeyParam}]"); context.Log.LogTrace("Recalling memory with key '{0}' from collection '{1}'", key, collection); @@ -96,24 +96,28 @@ public async Task RetrieveAsync(SKContext context) /// SKContext["input"] = "what is the capital of France?" /// {{memory.recall $input }} => "Paris" /// - /// The input text to find related memories for - /// Contains the 'collection' to search for the topic and 'relevance' score - [SKFunction("Semantic search and return up to N memories related to the input text")] - [SKFunctionName("Recall")] - [SKFunctionInput(Description = "The input text to find related memories for")] - [SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection to search", DefaultValue = DefaultCollection)] - [SKFunctionContextParameter(Name = RelevanceParam, Description = "The relevance score, from 0.0 to 1.0, where 1.0 means perfect match", - DefaultValue = DefaultRelevance)] - [SKFunctionContextParameter(Name = LimitParam, Description = "The maximum number of relevant memories to recall", DefaultValue = DefaultLimit)] - public async Task RecallAsync(string text, SKContext context) + /// The input text to find related memories for. + /// Memories collection to search. + /// The relevance score, from 0.0 to 1.0, where 1.0 means perfect match. + /// The maximum number of relevant memories to recall. + /// Contains the memory to search. + [SKFunction("Semantic search and return up to N memories related to the input text", Name = "Recall")] + public async Task RecallAsync( + [SKFunctionInput("The input text to find related memories for")] string text, + [SKFunctionContextParameter("Memories collection to search", DefaultValue = DefaultCollection)] string? collection, + [SKFunctionContextParameter("The relevance score, from 0.0 to 1.0, where 1.0 means perfect match", DefaultValue = DefaultRelevance)] string? relevance, + [SKFunctionContextParameter("The maximum number of relevant memories to recall", DefaultValue = DefaultLimit)] string? limit, + SKContext context) { - var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : this._collection; + Debug.Assert(nameof(collection) == nameof(CollectionParam)); + + collection ??= this._collection; Verify.NotNullOrWhiteSpace(collection, $"{nameof(context)}.{nameof(context.Variables)}[{CollectionParam}]"); - var relevance = context.Variables.ContainsKey(RelevanceParam) ? context[RelevanceParam] : this._relevance; + relevance ??= this._relevance; if (string.IsNullOrWhiteSpace(relevance)) { relevance = DefaultRelevance; } - var limit = context.Variables.ContainsKey(LimitParam) ? context[LimitParam] : this._limit; + limit ??= this._limit; if (string.IsNullOrWhiteSpace(limit)) { limit = DefaultLimit; } context.Log.LogTrace("Searching memories in collection '{0}', relevance '{1}'", collection, relevance); @@ -147,19 +151,18 @@ public async Task RecallAsync(string text, SKContext context) /// {{memory.save $input }} /// /// The information to save - /// Contains the 'collection' to save the information and unique 'key' to associate it with. - [SKFunction("Save information to semantic memory")] - [SKFunctionName("Save")] - [SKFunctionInput(Description = "The information to save")] - [SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection associated with the information to save", - DefaultValue = DefaultCollection)] - [SKFunctionContextParameter(Name = KeyParam, Description = "The key associated with the information to save")] - public async Task SaveAsync(string text, SKContext context) + /// Memories collection associated with the information to save + /// The key associated with the information to save + /// Contains the memory to save. + [SKFunction("Save information to semantic memory", Name = "Save")] + public async Task SaveAsync( + [SKFunctionInput("The information to save")] string text, + [SKFunctionContextParameter("Memories collection associated with the information to save", DefaultValue = DefaultCollection)] string? collection, + [SKFunctionContextParameter("The key associated with the information to save")] string key, + SKContext context) { - var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : DefaultCollection; + collection ??= DefaultCollection; Verify.NotNullOrWhiteSpace(collection, $"{nameof(context)}.{nameof(context.Variables)}[{CollectionParam}]"); - - var key = context.Variables.ContainsKey(KeyParam) ? context[KeyParam] : string.Empty; Verify.NotNullOrWhiteSpace(key, $"{nameof(context)}.{nameof(context.Variables)}[{KeyParam}]"); context.Log.LogTrace("Saving memory to collection '{0}'", collection); @@ -174,18 +177,17 @@ public async Task SaveAsync(string text, SKContext context) /// SKContext[TextMemorySkill.KeyParam] = "countryInfo1" /// {{memory.remove }} /// - /// Contains the 'collection' containing the memory to remove. - [SKFunction("Remove specific memory")] - [SKFunctionName("Remove")] - [SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection associated with the memory to remove", - DefaultValue = DefaultCollection)] - [SKFunctionContextParameter(Name = KeyParam, Description = "The key associated with the memory to remove")] - public async Task RemoveAsync(SKContext context) + /// Memories collection associated with the information to save + /// The key associated with the information to save + /// Contains the memory from which to remove. + [SKFunction("Remove specific memory", Name = "Remove")] + public async Task RemoveAsync( + [SKFunctionContextParameter("Memories collection associated with the information to save", DefaultValue = DefaultCollection)] string? collection, + [SKFunctionContextParameter("The key associated with the information to save")] string key, + SKContext context) { - var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : DefaultCollection; + collection ??= DefaultCollection; Verify.NotNullOrWhiteSpace(collection, $"{nameof(context)}.{nameof(context.Variables)}[{CollectionParam}]"); - - var key = context.Variables.ContainsKey(KeyParam) ? context[KeyParam] : string.Empty; Verify.NotNullOrWhiteSpace(key, $"{nameof(context)}.{nameof(context.Variables)}[{KeyParam}]"); context.Log.LogTrace("Removing memory from collection '{0}'", collection); @@ -194,8 +196,6 @@ public async Task RemoveAsync(SKContext context) } private readonly string _collection; - private readonly string _relevance; - private readonly string _limit; } diff --git a/dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs index ca41af80d04ac..ab41f67559fe1 100644 --- a/dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs +++ b/dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; namespace Microsoft.SemanticKernel.CoreSkills; @@ -30,12 +29,12 @@ public class TextSkill /// SKContext["input"] = " hello world " /// {{text.trim $input}} => "hello world" /// - /// The string to trim. + /// The string to trim. /// The trimmed string. [SKFunction("Trim whitespace from the start and end of a string.")] - public string Trim(string text) + public string Trim(string input) { - return text.Trim(); + return input.Trim(); } /// @@ -45,12 +44,12 @@ public string Trim(string text) /// SKContext["input"] = " hello world " /// {{text.trimStart $input} => "hello world " /// - /// The string to trim. + /// The string to trim. /// The trimmed string. [SKFunction("Trim whitespace from the start of a string.")] - public string TrimStart(string text) + public string TrimStart(string input) { - return text.TrimStart(); + return input.TrimStart(); } /// @@ -60,12 +59,12 @@ public string TrimStart(string text) /// SKContext["input"] = " hello world " /// {{text.trimEnd $input} => " hello world" /// - /// The string to trim. + /// The string to trim. /// The trimmed string. [SKFunction("Trim whitespace from the end of a string.")] - public string TrimEnd(string text) + public string TrimEnd(string input) { - return text.TrimEnd(); + return input.TrimEnd(); } /// @@ -75,12 +74,12 @@ public string TrimEnd(string text) /// SKContext["input"] = "hello world" /// {{text.uppercase $input}} => "HELLO WORLD" /// - /// The string to convert. + /// The string to convert. /// The converted string. [SKFunction("Convert a string to uppercase.")] - public string Uppercase(string text) + public string Uppercase(string input) { - return text.ToUpper(System.Globalization.CultureInfo.CurrentCulture); + return input.ToUpper(System.Globalization.CultureInfo.CurrentCulture); } /// @@ -90,12 +89,12 @@ public string Uppercase(string text) /// SKContext["input"] = "HELLO WORLD" /// {{text.lowercase $input}} => "hello world" /// - /// The string to convert. + /// The string to convert. /// The converted string. [SKFunction("Convert a string to lowercase.")] - public string Lowercase(string text) + public string Lowercase(string input) { - return text.ToLower(System.Globalization.CultureInfo.CurrentCulture); + return input.ToLower(System.Globalization.CultureInfo.CurrentCulture); } /// @@ -105,12 +104,12 @@ public string Lowercase(string text) /// SKContext["input"] = "HELLO WORLD" /// {{text.length $input}} => "11" /// - /// The string to get length. + /// The string to get length. /// The length size of string (0) if null or empty. [SKFunction("Get the length of a string.")] - public string Length(string text) + public string Length(string input) { - return (text?.Length ?? 0).ToString(System.Globalization.CultureInfo.InvariantCulture); + return (input?.Length ?? 0).ToString(System.Globalization.CultureInfo.InvariantCulture); } /// @@ -121,14 +120,14 @@ public string Length(string text) /// SKContext["input2"] = "WORLD" /// Result: "HELLO WORLD" /// - /// The string to get length. - /// Context where the input2 value will be retrieved + /// First input to concatenate with + /// Second input to concatenate with /// Concatenation result from both inputs. [SKFunction("Concat two strings into one.")] - [SKFunctionInput(Description = "First input to concatenate with")] - [SKFunctionContextParameter(Name = "input2", Description = "Second input to concatenate with")] - public string Concat(string text, SKContext context) + public string Concat( + [SKFunctionInput("First input to concatenate with")] string input1, + [SKFunctionContextParameter("Second input to concatenate with")] string input2) { - return string.Concat(text, context["input2"]); + return string.Concat(input1, input2); } } diff --git a/dotnet/src/SemanticKernel/CoreSkills/WaitSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/WaitSkill.cs index 41bb699c1cdee..462bcb97e7b5f 100644 --- a/dotnet/src/SemanticKernel/CoreSkills/WaitSkill.cs +++ b/dotnet/src/SemanticKernel/CoreSkills/WaitSkill.cs @@ -43,10 +43,9 @@ public WaitSkill(IWaitProvider? waitProvider = null) /// /// {{wait.seconds 10}} (Wait 10 seconds) /// - [SKFunction("Wait a given amount of seconds")] - [SKFunctionName("Seconds")] - [SKFunctionInput(DefaultValue = "0", Description = "The number of seconds to wait")] - public async Task SecondsAsync(string secondsText) + [SKFunction("Wait a given amount of seconds", Name = "Seconds")] + public async Task SecondsAsync( + [SKFunctionInput("The number of seconds to wait")] string secondsText) { if (!decimal.TryParse(secondsText, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) { diff --git a/dotnet/src/SemanticKernel/SkillDefinition/SKFunction.cs b/dotnet/src/SemanticKernel/SkillDefinition/SKFunction.cs index d2888aefd7917..561dcc26dae26 100644 --- a/dotnet/src/SemanticKernel/SkillDefinition/SKFunction.cs +++ b/dotnet/src/SemanticKernel/SkillDefinition/SKFunction.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -50,20 +51,20 @@ public sealed class SKFunction : ISKFunction, IDisposable /// /// Create a native function instance, wrapping a native object method /// - /// Signature of the method to invoke - /// Object containing the method to invoke + /// Signature of the method to invoke + /// Object containing the method to invoke /// SK skill name /// Application logger /// SK function instance public static ISKFunction? FromNativeMethod( - MethodInfo methodSignature, - object? methodContainerInstance = null, - string skillName = "", + MethodInfo method, + object? target = null, + string? skillName = null, ILogger? log = null) { - if (!methodSignature.IsStatic && methodContainerInstance is null) + if (!method.IsStatic && target is null) { - throw new ArgumentNullException(nameof(methodContainerInstance), "Argument cannot be null for non-static methods"); + throw new ArgumentNullException(nameof(target), "Argument cannot be null for non-static methods"); } if (string.IsNullOrWhiteSpace(skillName)) @@ -71,7 +72,7 @@ public sealed class SKFunction : ISKFunction, IDisposable skillName = SkillCollection.GlobalSkill; } - MethodDetails methodDetails = GetMethodDetails(methodSignature, methodContainerInstance, true, log); + MethodDetails methodDetails = GetMethodDetails(method, target, skAttributeRequired: true, log); // If the given method is not a valid SK function if (!methodDetails.HasSkFunctionAttribute) @@ -82,7 +83,7 @@ public sealed class SKFunction : ISKFunction, IDisposable return new SKFunction( delegateFunction: methodDetails.Function, parameters: methodDetails.Parameters, - skillName: skillName, + skillName: skillName!, functionName: methodDetails.Name, description: methodDetails.Description, isSemantic: false, @@ -107,7 +108,7 @@ public static ISKFunction FromNativeFunction( IEnumerable? parameters = null, ILogger? log = null) { - MethodDetails methodDetails = GetMethodDetails(nativeFunction.Method, nativeFunction.Target, false, log); + MethodDetails methodDetails = GetMethodDetails(nativeFunction.Method, nativeFunction.Target, skAttributeRequired: false, log); return new SKFunction( delegateFunction: methodDetails.Function, @@ -192,21 +193,31 @@ public FunctionView Describe() } /// - public Task InvokeAsync(SKContext context, CompleteRequestSettings? settings = null) + public async Task InvokeAsync(SKContext context, CompleteRequestSettings? settings = null) { // If the function is invoked manually, the user might have left out the skill collection context.Skills ??= this._skillCollection; - return this.IsSemantic ? - InvokeSemanticAsync(context, settings) : - this._function(null, settings, context); - - async Task InvokeSemanticAsync(SKContext context, CompleteRequestSettings? settings) + if (this.IsSemantic) { var resultContext = await this._function(this._aiService?.Value, settings ?? this._aiRequestSettings, context).ConfigureAwait(false); context.Variables.Update(resultContext.Variables); - return context; } + else + { + try + { + context = await this._function(null, settings, context).ConfigureAwait(false); + } + catch (Exception e) when (!e.IsCriticalException()) + { + const string Message = "Something went wrong while executing the native function. Function: {0}. Error: {2}"; + this._log.LogError(e, Message, this._function.Method.Name, e.Message); + context.Fail(e.Message, e); + } + } + + return context; } /// @@ -335,85 +346,35 @@ private void VerifyIsSemantic() private static MethodDetails GetMethodDetails( MethodInfo methodSignature, - object? methodContainerInstance, - bool skAttributesRequired = true, + object? targetInstance, + bool skAttributeRequired, ILogger? log = null) { Verify.NotNull(methodSignature); + SKFunctionAttribute? skFunctionAttribute = methodSignature.GetCustomAttribute(inherit: true); + var result = new MethodDetails { - Name = methodSignature.Name, - Parameters = new List(), + Name = !string.IsNullOrWhiteSpace(skFunctionAttribute?.Name) ? skFunctionAttribute!.Name! : SanitizeMetadataName(methodSignature.Name!), + Description = skFunctionAttribute?.Description ?? string.Empty, + HasSkFunctionAttribute = skFunctionAttribute != null, }; - // SKFunction attribute - SKFunctionAttribute? skFunctionAttribute = methodSignature - .GetCustomAttributes(typeof(SKFunctionAttribute), true) - .Cast() - .FirstOrDefault(); - - result.HasSkFunctionAttribute = skFunctionAttribute != null; - - if (!result.HasSkFunctionAttribute || skFunctionAttribute == null) + if (!result.HasSkFunctionAttribute) { log?.LogTrace("Method '{0}' doesn't have '{1}' attribute", result.Name, nameof(SKFunctionAttribute)); - if (skAttributesRequired) { return result; } + if (skAttributeRequired) + { + return result; + } } else { result.HasSkFunctionAttribute = true; } - (result.Function, bool hasStringParam) = GetDelegateInfo(methodContainerInstance, methodSignature); - - // SKFunctionName attribute - SKFunctionNameAttribute? skFunctionNameAttribute = methodSignature - .GetCustomAttributes(typeof(SKFunctionNameAttribute), true) - .Cast() - .FirstOrDefault(); - - if (skFunctionNameAttribute != null) - { - result.Name = skFunctionNameAttribute.Name; - } - - // SKFunctionInput attribute - SKFunctionInputAttribute? skMainParam = methodSignature - .GetCustomAttributes(typeof(SKFunctionInputAttribute), true) - .Cast() - .FirstOrDefault(); - - // SKFunctionContextParameter attribute - IList skContextParams = methodSignature - .GetCustomAttributes(typeof(SKFunctionContextParameterAttribute), true) - .Cast().ToList(); - - // Handle main string param description, if available/valid - // Note: Using [SKFunctionInput] is optional - if (hasStringParam) - { - result.Parameters.Add(skMainParam != null - ? skMainParam.ToParameterView() // Use the developer description - : new ParameterView { Name = "input", Description = "Input string", DefaultValue = "" }); // Use a default description - } - else if (skMainParam != null) - { - // The developer used [SKFunctionInput] on a function that doesn't support a string input - var message = $"The method '{result.Name}' doesn't have a string parameter, do not use '{nameof(SKFunctionInputAttribute)}' attribute."; - throw new KernelException(KernelException.ErrorCodes.InvalidFunctionDescription, message); - } - - // Handle named arg passed via the SKContext object - // Note: "input" is added first to the list, before context params - // Note: Using [SKFunctionContextParameter] is optional - result.Parameters.AddRange(skContextParams.Select(x => x.ToParameterView())); - - // Check for param names conflict - // Note: the name "input" is reserved for the main parameter - Verify.ParametersUniqueness(result.Parameters); - - result.Description = skFunctionAttribute?.Description ?? ""; + (result.Function, result.Parameters, bool hasStringParam) = GetDelegateInfo(targetInstance, methodSignature); log?.LogTrace("Method '{0}' found", result.Name); @@ -421,37 +382,89 @@ private static MethodDetails GetMethodDetails( } // Inspect a method and returns the corresponding delegate and related info - private static (Func> function, bool hasStringParam) GetDelegateInfo(object? instance, MethodInfo method) + private static (Func> function, List, bool hasStringParam) GetDelegateInfo(object? instance, MethodInfo method) { - // Get marshaling funcs for parameters + var stringParameterViews = new List(); + var parameters = method.GetParameters(); - if (parameters.Length > 2) + + // For compatibility with previous uses and the promotion of context.Variables.Input, special-case a single string + // parameter to fall back to using Input rather than failing. + int stringParameterCount = 0; + foreach (ParameterInfo p in parameters) { - ThrowForInvalidSignature(); + if (p.ParameterType == typeof(string)) + { + stringParameterCount++; + } } + // Get marshaling funcs for parameters and build up the parameter views. var parameterFuncs = new Func[parameters.Length]; - bool hasStringParam = false; - bool hasContextParam = false; + bool hasMainParam = false, hasSKContextParam = false, hasCancellationTokenParam = false; for (int i = 0; i < parameters.Length; i++) { - if (!hasStringParam && parameters[i].ParameterType == typeof(string)) + var p = parameters[i]; + + if (p.GetCustomAttribute(inherit: true) is SKFunctionInputAttribute mainParamAttr) { - hasStringParam = true; + // The parameter is attributed with SKFunctionInputAttribute to be the main Input. + TrackUniqueParameterType(ref hasMainParam, $"Too many {nameof(SKFunctionInputAttribute)} attributes."); + ThrowForInvalidSignatureIf(p.ParameterType != typeof(string), $"{nameof(SKFunctionInputAttribute)} applied to a parameter with a type other than {nameof(String)}."); + ThrowIfErroneousContextParameter(p); + parameterFuncs[i] = static (SKContext ctx) => ctx.Variables.Input; + stringParameterViews.Add(mainParamAttr.ToParameterView()); + } + else if (parameters[i].ParameterType == typeof(CancellationToken)) + { + TrackUniqueParameterType(ref hasCancellationTokenParam, $"Too many {nameof(CancellationToken)} parameters."); + ThrowIfErroneousContextParameter(p); + parameterFuncs[i] = static (SKContext ctx) => ctx.CancellationToken; } - else if (!hasContextParam && parameters[i].ParameterType == typeof(SKContext)) + else if (parameters[i].ParameterType == typeof(SKContext)) { - hasContextParam = true; + TrackUniqueParameterType(ref hasSKContextParam, $"Too many {nameof(SKContext)} parameters."); + ThrowIfErroneousContextParameter(p); parameterFuncs[i] = static (SKContext ctx) => ctx; } + else if (parameters[i].ParameterType == typeof(string)) + { + // If this were SKFunctionInput, we would have already handled it above. + // Use either the parameter's name or an override from an applied SKFunctionContextParameter attribute. + SKFunctionContextParameterAttribute? contextAttr = p.GetCustomAttribute(inherit: true); + string name = contextAttr?.Name ?? SanitizeMetadataName(p.Name); + ThrowForInvalidSignatureIf(string.IsNullOrWhiteSpace(name), $"Parameter {p.Name}'s context attribute defines an invalid name."); + + // Use either the parameter's optional default value as contained in parameter metadata (e.g. `string s = "hello"`) + // or an override from an applied SKFunctionContextParameter attribute. Note that a default value specified + // on an optional parameter may be null. + string? defaultValue = contextAttr?.DefaultValue; + bool hasDefaultValue = defaultValue is not null; + if (!hasDefaultValue && p.HasDefaultValue) + { + hasDefaultValue = true; + defaultValue = p.DefaultValue as string; + } + + parameterFuncs[i] = (SKContext ctx) => + ctx.Variables.Get(name, out string value) ? value : // 1. Use the value of the variable if it exists + stringParameterCount == 1 ? ctx.Variables.Input : // 2. Use Input if this is the sole string parameter + hasDefaultValue ? defaultValue! : // 3. Use the default value if there is one, sourced either from an attribute or the parameter's default + throw new KernelException(KernelException.ErrorCodes.FunctionInvokeError, $"Missing context variable '{name}'"); // 4. Fail + + stringParameterViews.Add(new ParameterView(name, contextAttr?.Description ?? string.Empty, defaultValue ?? string.Empty)); + } else { - ThrowForInvalidSignature(); + ThrowForInvalidSignature($"Unknown parameter type {p.ParameterType}"); } } - // Get marshaling func for the return value + // Add context parameters applied to the method itself. + stringParameterViews.AddRange(method.GetCustomAttributes(inherit: true).Select(x => x.ToParameterView())); + + // Get marshaling func for the return value. Func> returnFunc; if (method.ReturnType == typeof(void)) { @@ -491,7 +504,7 @@ private static (Func - throw new KernelException( - KernelException.ErrorCodes.FunctionTypeNotSupported, - $"Function '{method.Name}' has an invalid signature not supported by the kernel."); + // Return the func and whether it has a string param + return (function, stringParameterViews, hasMainParam); static object ThrowIfNullResult(object? result) => result ?? throw new KernelException( KernelException.ErrorCodes.FunctionInvokeError, "Function returned null unexpectedly."); + + void ThrowForInvalidSignature(string reason) => + throw new KernelException( + KernelException.ErrorCodes.FunctionTypeNotSupported, + $"Function '{method.Name}' is not supported by the kernel. {reason}"); + + void ThrowForInvalidSignatureIf(bool condition, string reason) + { + if (condition) { ThrowForInvalidSignature(reason); } + } + + void TrackUniqueParameterType(ref bool hasParameterType, string failureMessage) + { + ThrowForInvalidSignatureIf(hasParameterType, failureMessage); + hasParameterType = true; + } + + void ThrowIfErroneousContextParameter(ParameterInfo p) => + ThrowForInvalidSignatureIf( + p.GetCustomAttribute() is not null, + $"{nameof(SKFunctionContextParameterAttribute)} applied incorrectly."); } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"{this.Name} ({this.Description})"; + /// + /// Remove characters from method name that are valid in metadata but invalid for SK. + /// + private static string SanitizeMetadataName(string methodName) => + s_invalidNameCharsRegex.Replace(methodName, "_"); + + /// Regex that flags any character other than ASCII digits or letters or the underscore. + private static readonly Regex s_invalidNameCharsRegex = new("[^0-9A-Za-z_]"); + #endregion } diff --git a/dotnet/src/Skills/Skills.Document/DocumentSkill.cs b/dotnet/src/Skills/Skills.Document/DocumentSkill.cs index aa5336d6ceba0..55e4c699787f2 100644 --- a/dotnet/src/Skills/Skills.Document/DocumentSkill.cs +++ b/dotnet/src/Skills/Skills.Document/DocumentSkill.cs @@ -2,6 +2,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -69,11 +70,10 @@ public DocumentSkill(IDocumentConnector documentConnector, IFileSystemConnector /// Read all text from a document, using as the file path. /// [SKFunction("Read all text from a document")] - [SKFunctionInput(Description = "Path to the file to read")] - public async Task ReadTextAsync(string filePath, SKContext context) + public async Task ReadTextAsync([SKFunctionInput("Path to the file to read")] string filePath, CancellationToken cancellationToken = default) { this._logger.LogInformation("Reading text from {0}", filePath); - using var stream = await this._fileSystemConnector.GetFileContentStreamAsync(filePath, context.CancellationToken).ConfigureAwait(false); + using var stream = await this._fileSystemConnector.GetFileContentStreamAsync(filePath, cancellationToken).ConfigureAwait(false); return this._documentConnector.ReadText(stream); } @@ -81,27 +81,27 @@ public async Task ReadTextAsync(string filePath, SKContext context) /// Append the text in to a document. If the document doesn't exist, it will be created. /// [SKFunction("Append text to a document. If the document doesn't exist, it will be created.")] - [SKFunctionInput(Description = "Text to append")] - [SKFunctionContextParameter(Name = Parameters.FilePath, Description = "Destination file path")] - public async Task AppendTextAsync(string text, SKContext context) + public async Task AppendTextAsync( + [SKFunctionInput("Text to append")] string text, + [SKFunctionContextParameter("Destination file path")] string filePath, + CancellationToken cancellationToken = default) { - if (!context.Variables.Get(Parameters.FilePath, out string filePath)) + if (string.IsNullOrWhiteSpace(filePath)) { - context.Fail($"Missing variable {Parameters.FilePath}."); - return; + throw new ArgumentException("Variable was null or whitespace", nameof(filePath)); } // If the document already exists, open it. If not, create it. - if (await this._fileSystemConnector.FileExistsAsync(filePath).ConfigureAwait(false)) + if (await this._fileSystemConnector.FileExistsAsync(filePath, cancellationToken).ConfigureAwait(false)) { this._logger.LogInformation("Writing text to file {0}", filePath); - using Stream stream = await this._fileSystemConnector.GetWriteableFileStreamAsync(filePath, context.CancellationToken).ConfigureAwait(false); + using Stream stream = await this._fileSystemConnector.GetWriteableFileStreamAsync(filePath, cancellationToken).ConfigureAwait(false); this._documentConnector.AppendText(stream, text); } else { this._logger.LogInformation("File does not exist. Creating file at {0}", filePath); - using Stream stream = await this._fileSystemConnector.CreateFileAsync(filePath).ConfigureAwait(false); + using Stream stream = await this._fileSystemConnector.CreateFileAsync(filePath, cancellationToken).ConfigureAwait(false); this._documentConnector.Initialize(stream); this._logger.LogInformation("Writing text to {0}", filePath); diff --git a/dotnet/src/Skills/Skills.MsGraph/CalendarSkill.cs b/dotnet/src/Skills/Skills.MsGraph/CalendarSkill.cs index 632d2d35707ad..f0836856c65c9 100644 --- a/dotnet/src/Skills/Skills.MsGraph/CalendarSkill.cs +++ b/dotnet/src/Skills/Skills.MsGraph/CalendarSkill.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -63,6 +65,11 @@ public static class Parameters private readonly ICalendarConnector _connector; private readonly ILogger _logger; + private static readonly JsonSerializerOptions s_options = new() + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; /// /// Initializes a new instance of the class. @@ -81,56 +88,39 @@ public CalendarSkill(ICalendarConnector connector, ILogger? logge /// Add an event to my calendar using as the subject. /// [SKFunction("Add an event to my calendar.")] - [SKFunctionInput(Description = "Event subject")] - [SKFunctionContextParameter(Name = Parameters.Start, Description = "Event start date/time as DateTimeOffset")] - [SKFunctionContextParameter(Name = Parameters.End, Description = "Event end date/time as DateTimeOffset")] - [SKFunctionContextParameter(Name = Parameters.Location, Description = "Event location (optional)")] - [SKFunctionContextParameter(Name = Parameters.Content, Description = "Event content/body (optional)")] - [SKFunctionContextParameter(Name = Parameters.Attendees, Description = "Event attendees, separated by ',' or ';'.")] - public async Task AddEventAsync(string subject, SKContext context) + public async Task AddEventAsync( + [SKFunctionInput("Event subject")] string subject, + [SKFunctionContextParameter("Event start date/time as DateTimeOffset")] string start, + [SKFunctionContextParameter("Event end date/time as DateTimeOffset")] string end, + [SKFunctionContextParameter("Event location (optional)")] string? location = null, + [SKFunctionContextParameter("Event content/body (optional)")] string? content = null, + [SKFunctionContextParameter("Event attendees, separated by ',' or ';'.")] string? attendees = null) { - ContextVariables memory = context.Variables; - if (string.IsNullOrWhiteSpace(subject)) { - context.Fail("Missing variables input to use as event subject."); - return; + throw new ArgumentException("Variable was null or whitespace", nameof(subject)); } - if (!memory.Get(Parameters.Start, out string start)) + if (string.IsNullOrWhiteSpace(start)) { - context.Fail($"Missing variable {Parameters.Start}."); - return; + throw new ArgumentException("Variable was null or whitespace", nameof(start)); } - if (!memory.Get(Parameters.End, out string end)) + if (string.IsNullOrWhiteSpace(end)) { - context.Fail($"Missing variable {Parameters.End}."); - return; + throw new ArgumentException("Variable was null or whitespace", nameof(end)); } CalendarEvent calendarEvent = new() { - Subject = memory.Input, + Subject = subject, Start = DateTimeOffset.Parse(start, CultureInfo.InvariantCulture.DateTimeFormat), - End = DateTimeOffset.Parse(end, CultureInfo.InvariantCulture.DateTimeFormat) + End = DateTimeOffset.Parse(end, CultureInfo.InvariantCulture.DateTimeFormat), + Location = location, + Content = content, + Attendees = attendees is not null ? attendees.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) : Enumerable.Empty(), }; - if (memory.Get(Parameters.Location, out string location)) - { - calendarEvent.Location = location; - } - - if (memory.Get(Parameters.Content, out string content)) - { - calendarEvent.Content = content; - } - - if (memory.Get(Parameters.Attendees, out string attendees)) - { - calendarEvent.Attendees = attendees.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); - } - this._logger.LogInformation("Adding calendar event '{0}'", calendarEvent.Subject); await this._connector.AddEventAsync(calendarEvent).ConfigureAwait(false); } @@ -139,47 +129,22 @@ public async Task AddEventAsync(string subject, SKContext context) /// Get calendar events with specified optional clauses used to query for messages. /// [SKFunction("Get calendar events.")] - [SKFunctionContextParameter(Name = Parameters.MaxResults, Description = "Optional limit of the number of events to retrieve.", DefaultValue = "10")] - [SKFunctionContextParameter(Name = Parameters.Skip, Description = "Optional number of events to skip before retrieving results.", DefaultValue = "0")] - public async Task GetCalendarEventsAsync(SKContext context) + public async Task GetCalendarEventsAsync( + [SKFunctionContextParameter("Optional limit of the number of events to retrieve.")] string maxResults = "10", + [SKFunctionContextParameter("Optional number of events to skip before retrieving results.")] string skip = "0", + CancellationToken cancellationToken = default) { - context.Variables.Get(Parameters.MaxResults, out string maxResultsString); - context.Variables.Get(Parameters.Skip, out string skipString); - this._logger.LogInformation("Getting calendar events with query options top: '{0}', skip:'{1}'.", maxResultsString, skipString); - - string selectString = "start,subject,organizer,location"; - - int? top = null; - if (!string.IsNullOrWhiteSpace(maxResultsString)) - { - if (int.TryParse(maxResultsString, out int topValue)) - { - top = topValue; - } - } + this._logger.LogInformation("Getting calendar events with query options top: '{0}', skip:'{1}'.", maxResults, skip); - int? skip = null; - if (!string.IsNullOrWhiteSpace(skipString)) - { - if (int.TryParse(skipString, out int skipValue)) - { - skip = skipValue; - } - } + const string SelectString = "start,subject,organizer,location"; IEnumerable events = await this._connector.GetEventsAsync( - top: top, - skip: skip, - select: selectString, - context.CancellationToken + top: int.TryParse(maxResults, out int topValue) ? topValue : null, + skip: int.TryParse(skip, out int skipValue) ? skipValue : null, + select: SelectString, + cancellationToken ).ConfigureAwait(false); - return JsonSerializer.Serialize( - value: events, - options: new JsonSerializerOptions - { - WriteIndented = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }); + return JsonSerializer.Serialize(value: events, options: s_options); } } diff --git a/dotnet/src/Skills/Skills.MsGraph/CloudDriveSkill.cs b/dotnet/src/Skills/Skills.MsGraph/CloudDriveSkill.cs index 815cfe4d9d147..8b5c868bae235 100644 --- a/dotnet/src/Skills/Skills.MsGraph/CloudDriveSkill.cs +++ b/dotnet/src/Skills/Skills.MsGraph/CloudDriveSkill.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -41,11 +43,10 @@ public CloudDriveSkill(ICloudDriveConnector connector, ILogger? /// Get the contents of a file stored in a cloud drive. /// [SKFunction("Get the contents of a file in a cloud drive.")] - [SKFunctionInput(Description = "Path to file")] - public async Task GetFileContentAsync(string filePath, SKContext context) + public async Task GetFileContentAsync([SKFunctionInput("Path to file")] string filePath, CancellationToken cancellationToken = default) { this._logger.LogDebug("Getting file content for '{0}'", filePath); - Stream fileContentStream = await this._connector.GetFileContentStreamAsync(filePath, context.CancellationToken).ConfigureAwait(false); + Stream fileContentStream = await this._connector.GetFileContentStreamAsync(filePath, cancellationToken).ConfigureAwait(false); using StreamReader sr = new(fileContentStream); string content = await sr.ReadToEndAsync().ConfigureAwait(false); @@ -57,39 +58,32 @@ public async Task GetFileContentAsync(string filePath, SKContext context /// Upload a small file to OneDrive (less than 4MB). /// [SKFunction("Upload a small file to OneDrive (less than 4MB).")] - public async Task UploadFileAsync(string filePath, SKContext context) + public async Task UploadFileAsync( + [SKFunctionInput("Path to file")] string filePath, + [SKFunctionContextParameter("Remote path to store the file")] string destinationPath, + CancellationToken cancellationToken = default) { - if (!context.Variables.Get(Parameters.DestinationPath, out string destinationPath)) + if (string.IsNullOrWhiteSpace(destinationPath)) { - context.Fail($"Missing variable {Parameters.DestinationPath}."); - return; + throw new ArgumentException("Variable was null or whitespace", nameof(destinationPath)); } this._logger.LogDebug("Uploading file '{0}'", filePath); // TODO Add support for large file uploads (i.e. upload sessions) - - try - { - await this._connector.UploadSmallFileAsync(filePath, destinationPath, context.CancellationToken).ConfigureAwait(false); - } - catch (IOException ex) - { - context.Fail(ex.Message, ex); - } + await this._connector.UploadSmallFileAsync(filePath, destinationPath, cancellationToken).ConfigureAwait(false); } /// /// Create a sharable link to a file stored in a cloud drive. /// [SKFunction("Create a sharable link to a file stored in a cloud drive.")] - [SKFunctionInput(Description = "Path to file")] - public async Task CreateLinkAsync(string filePath, SKContext context) + public async Task CreateLinkAsync([SKFunctionInput("Path to file")] string filePath, CancellationToken cancellationToken = default) { this._logger.LogDebug("Creating link for '{0}'", filePath); const string Type = "view"; // TODO expose this as an SK variable const string Scope = "anonymous"; // TODO expose this as an SK variable - return await this._connector.CreateShareLinkAsync(filePath, Type, Scope, context.CancellationToken).ConfigureAwait(false); + return await this._connector.CreateShareLinkAsync(filePath, Type, Scope, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Skills/Skills.MsGraph/EmailSkill.cs b/dotnet/src/Skills/Skills.MsGraph/EmailSkill.cs index 8a3594e5fab2a..9d30f3b80df9d 100644 --- a/dotnet/src/Skills/Skills.MsGraph/EmailSkill.cs +++ b/dotnet/src/Skills/Skills.MsGraph/EmailSkill.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -47,6 +48,11 @@ public static class Parameters private readonly IEmailConnector _connector; private readonly ILogger _logger; + private static readonly JsonSerializerOptions s_options = new() + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; /// /// Initializes a new instance of the class. @@ -72,75 +78,47 @@ public async Task GetMyEmailAddressAsync() /// Send an email using as the body. /// [SKFunction("Send an email to one or more recipients.")] - [SKFunctionInput(Description = "Email content/body")] - [SKFunctionContextParameter(Name = Parameters.Recipients, Description = "Recipients of the email, separated by ',' or ';'.")] - [SKFunctionContextParameter(Name = Parameters.Subject, Description = "Subject of the email")] - public async Task SendEmailAsync(string content, SKContext context) + public async Task SendEmailAsync( + [SKFunctionInput("Email content/body")] string content, + [SKFunctionContextParameter("Recipients of the email, separated by ',' or ';'.")] string recipients, + [SKFunctionContextParameter("Subject of the email")] string subject, + CancellationToken cancellationToken = default) { - if (!context.Variables.Get(Parameters.Recipients, out string recipients)) + if (string.IsNullOrWhiteSpace(recipients)) { - context.Fail($"Missing variable {Parameters.Recipients}."); - return; + throw new ArgumentException("Variable was null or whitespace", nameof(recipients)); } - if (!context.Variables.Get(Parameters.Subject, out string subject)) + if (string.IsNullOrWhiteSpace(subject)) { - context.Fail($"Missing variable {Parameters.Subject}."); - return; + throw new ArgumentException("Variable was null or whitespace", nameof(subject)); } this._logger.LogInformation("Sending email to '{0}' with subject '{1}'", recipients, subject); string[] recipientList = recipients.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); - await this._connector.SendEmailAsync(subject, content, recipientList).ConfigureAwait(false); + await this._connector.SendEmailAsync(subject, content, recipientList, cancellationToken).ConfigureAwait(false); } /// /// Get email messages with specified optional clauses used to query for messages. /// [SKFunction("Get email messages.")] - [SKFunctionContextParameter(Name = Parameters.MaxResults, Description = "Optional limit of the number of message to retrieve.", - DefaultValue = "10")] - [SKFunctionContextParameter(Name = Parameters.Skip, Description = "Optional number of message to skip before retrieving results.", - DefaultValue = "0")] - public async Task GetEmailMessagesAsync(SKContext context) + public async Task GetEmailMessagesAsync( + [SKFunctionContextParameter("Optional limit of the number of message to retrieve.")] string maxResults = "10", + [SKFunctionContextParameter("Optional number of message to skip before retrieving results.")] string skip = "0", + CancellationToken cancellationToken = default) { - context.Variables.Get(Parameters.MaxResults, out string maxResultsString); - context.Variables.Get(Parameters.Skip, out string skipString); - this._logger.LogInformation("Getting email messages with query options top: '{0}', skip:'{1}'.", maxResultsString, skipString); - - string selectString = "subject,receivedDateTime,bodyPreview"; + this._logger.LogInformation("Getting email messages with query options top: '{0}', skip:'{1}'.", maxResults, skip); - int? top = null; - if (!string.IsNullOrWhiteSpace(maxResultsString)) - { - if (int.TryParse(maxResultsString, out int topValue)) - { - top = topValue; - } - } - - int? skip = null; - if (!string.IsNullOrWhiteSpace(skipString)) - { - if (int.TryParse(skipString, out int skipValue)) - { - skip = skipValue; - } - } + const string SelectString = "subject,receivedDateTime,bodyPreview"; IEnumerable messages = await this._connector.GetMessagesAsync( - top: top, - skip: skip, - select: selectString, - context.CancellationToken) + top: int.TryParse(maxResults, out int topValue) ? topValue : null, + skip: int.TryParse(skip, out int skipValue) ? skipValue : null, + select: SelectString, + cancellationToken) .ConfigureAwait(false); - return JsonSerializer.Serialize( - value: messages, - options: new JsonSerializerOptions - { - WriteIndented = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }); + return JsonSerializer.Serialize(value: messages, options: s_options); } } diff --git a/dotnet/src/Skills/Skills.MsGraph/Models/CalendarEvent.cs b/dotnet/src/Skills/Skills.MsGraph/Models/CalendarEvent.cs index 592a614b28c58..ff1b644c621bb 100644 --- a/dotnet/src/Skills/Skills.MsGraph/Models/CalendarEvent.cs +++ b/dotnet/src/Skills/Skills.MsGraph/Models/CalendarEvent.cs @@ -19,7 +19,7 @@ public class CalendarEvent /// /// Body/content of the event. /// - public string? Content { get; set; } = null; + public string? Content { get; set; } /// /// Start time of the event. @@ -34,7 +34,7 @@ public class CalendarEvent /// /// Location of the event. /// - public string? Location { get; set; } = null; + public string? Location { get; set; } /// /// Attendees of the event. diff --git a/dotnet/src/Skills/Skills.MsGraph/OrganizationHierarchySkill.cs b/dotnet/src/Skills/Skills.MsGraph/OrganizationHierarchySkill.cs index 19aacf87d0fc2..f1e4cc3f83a2d 100644 --- a/dotnet/src/Skills/Skills.MsGraph/OrganizationHierarchySkill.cs +++ b/dotnet/src/Skills/Skills.MsGraph/OrganizationHierarchySkill.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; using Microsoft.SemanticKernel.Skills.MsGraph.Diagnostics; @@ -26,20 +26,20 @@ public OrganizationHierarchySkill(IOrganizationHierarchyConnector connector) /// Get the emails of the direct reports of the current user. /// [SKFunction("Get my direct report's email addresses.")] - public async Task> GetMyDirectReportsEmailAsync(SKContext context) - => await this._connector.GetDirectReportsEmailAsync(context.CancellationToken).ConfigureAwait(false); + public async Task> GetMyDirectReportsEmailAsync(CancellationToken cancellationToken = default) + => await this._connector.GetDirectReportsEmailAsync(cancellationToken).ConfigureAwait(false); /// /// Get the email of the manager of the current user. /// [SKFunction("Get my manager's email address.")] - public async Task GetMyManagerEmailAsync(SKContext context) - => await this._connector.GetManagerEmailAsync(context.CancellationToken).ConfigureAwait(false); + public async Task GetMyManagerEmailAsync(CancellationToken cancellationToken = default) + => await this._connector.GetManagerEmailAsync(cancellationToken).ConfigureAwait(false); /// /// Get the name of the manager of the current user. /// [SKFunction("Get my manager's name.")] - public async Task GetMyManagerNameAsync(SKContext context) - => await this._connector.GetManagerNameAsync(context.CancellationToken).ConfigureAwait(false); + public async Task GetMyManagerNameAsync(CancellationToken cancellationToken = default) + => await this._connector.GetManagerNameAsync(cancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/Skills/Skills.MsGraph/TaskListSkill.cs b/dotnet/src/Skills/Skills.MsGraph/TaskListSkill.cs index 563b6fa51eb60..236c12dedaa90 100644 --- a/dotnet/src/Skills/Skills.MsGraph/TaskListSkill.cs +++ b/dotnet/src/Skills/Skills.MsGraph/TaskListSkill.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -72,58 +73,46 @@ public static DateTimeOffset GetNextDayOfWeek(DayOfWeek dayOfWeek, TimeSpan time /// Add a task to a To-Do list with an optional reminder. /// [SKFunction("Add a task to a task list with an optional reminder.")] - [SKFunctionInput(Description = "Title of the task.")] - [SKFunctionContextParameter(Name = Parameters.Reminder, Description = "Reminder for the task in DateTimeOffset (optional)")] - public async Task AddTaskAsync(string title, SKContext context) + public async Task AddTaskAsync( + [SKFunctionInput("Title of the task.")] string title, + [SKFunctionContextParameter("Reminder for the task in DateTimeOffset (optional)")] string? reminder = null, + CancellationToken cancellationToken = default) { - TaskManagementTaskList? defaultTaskList = await this._connector.GetDefaultTaskListAsync(context.CancellationToken).ConfigureAwait(false); + TaskManagementTaskList? defaultTaskList = await this._connector.GetDefaultTaskListAsync(cancellationToken).ConfigureAwait(false); if (defaultTaskList == null) { - context.Fail("No default task list found."); - return; + throw new InvalidOperationException("No default task list found."); } TaskManagementTask task = new( id: Guid.NewGuid().ToString(), - title: title); - - if (context.Variables.Get(Parameters.Reminder, out string reminder)) - { - task.Reminder = reminder; - } + title: title, + reminder: reminder); this._logger.LogInformation("Adding task '{0}' to task list '{1}'", task.Title, defaultTaskList.Name); - await this._connector.AddTaskAsync(defaultTaskList.Id, task, context.CancellationToken).ConfigureAwait(false); + await this._connector.AddTaskAsync(defaultTaskList.Id, task, cancellationToken).ConfigureAwait(false); } /// /// Get tasks from the default task list. /// [SKFunction("Get tasks from the default task list.")] - [SKFunctionContextParameter(Name = Parameters.IncludeCompleted, Description = "Whether to include completed tasks (optional)", DefaultValue = "false")] - public async Task GetDefaultTasksAsync(SKContext context) + public async Task GetDefaultTasksAsync( + [SKFunctionContextParameter("Whether to include completed tasks (optional)")] string includeCompleted = "false", + CancellationToken cancellationToken = default) { - TaskManagementTaskList? defaultTaskList = await this._connector.GetDefaultTaskListAsync(context.CancellationToken) - .ConfigureAwait(false); - + TaskManagementTaskList? defaultTaskList = await this._connector.GetDefaultTaskListAsync(cancellationToken).ConfigureAwait(false); if (defaultTaskList == null) { - context.Fail("No default task list found."); - return string.Empty; + throw new InvalidOperationException("No default task list found."); } - bool includeCompleted = false; - if (context.Variables.Get(Parameters.IncludeCompleted, out string includeCompletedString)) + if (!bool.TryParse(includeCompleted, out bool includeCompletedValue)) { - if (!bool.TryParse(includeCompletedString, out includeCompleted)) - { - this._logger.LogWarning("Invalid value for '{0}' variable: '{1}'", Parameters.IncludeCompleted, includeCompletedString); - } + this._logger.LogWarning("Invalid value for '{0}' variable: '{1}'", nameof(includeCompleted), includeCompleted); } - IEnumerable tasks = await this._connector.GetTasksAsync(defaultTaskList.Id, includeCompleted, context.CancellationToken) - .ConfigureAwait(false); - + IEnumerable tasks = await this._connector.GetTasksAsync(defaultTaskList.Id, includeCompletedValue, cancellationToken).ConfigureAwait(false); return JsonSerializer.Serialize(tasks); } } diff --git a/dotnet/src/Skills/Skills.OpenAPI/JsonPathSkill.cs b/dotnet/src/Skills/Skills.OpenAPI/JsonPathSkill.cs index 1c21cd7463227..6ad585bb2c68c 100644 --- a/dotnet/src/Skills/Skills.OpenAPI/JsonPathSkill.cs +++ b/dotnet/src/Skills/Skills.OpenAPI/JsonPathSkill.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Linq; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; @@ -25,20 +26,13 @@ public static class Parameters /// Retrieve the value of a JSON element from a JSON string using a JsonPath query. /// [SKFunction("Retrieve the value of a JSON element from a JSON string using a JsonPath query.")] - [SKFunctionInput(Description = "JSON string")] - [SKFunctionContextParameter(Name = "JsonPath", Description = "JSON path query.")] - public string GetJsonElementValue(string json, SKContext context) + public string GetJsonElementValue( + [SKFunctionInput("JSON string")] string json, + [SKFunctionContextParameter("JSON path query.")] string jsonPath) { if (string.IsNullOrWhiteSpace(json)) { - context.Fail("Missing input JSON."); - return string.Empty; - } - - if (!context.Variables.Get(Parameters.JsonPath, out string jsonPath)) - { - context.Fail($"Missing variable {Parameters.JsonPath}."); - return string.Empty; + throw new ArgumentException("Variable was null or whitespace", nameof(json)); } JObject jsonObject = JObject.Parse(json); @@ -52,20 +46,13 @@ public string GetJsonElementValue(string json, SKContext context) /// Retrieve a collection of JSON elements from a JSON string using a JsonPath query. /// [SKFunction("Retrieve a collection of JSON elements from a JSON string using a JsonPath query.")] - [SKFunctionInput(Description = "JSON string")] - [SKFunctionContextParameter(Name = "JsonPath", Description = "JSON path query.")] - public string GetJsonElements(string json, SKContext context) + public string GetJsonElements( + [SKFunctionInput("JSON string")] string json, + [SKFunctionContextParameter("JSON path query.")] string jsonPath) { if (string.IsNullOrWhiteSpace(json)) { - context.Fail("Missing input JSON."); - return string.Empty; - } - - if (!context.Variables.Get(Parameters.JsonPath, out string jsonPath)) - { - context.Fail($"Missing variable {Parameters.JsonPath}."); - return string.Empty; + throw new ArgumentException("Variable was null or whitespace", nameof(json)); } JObject jsonObject = JObject.Parse(json); diff --git a/dotnet/src/Skills/Skills.UnitTests/Document/DocumentSkillTests.cs b/dotnet/src/Skills/Skills.UnitTests/Document/DocumentSkillTests.cs index 5827954443d3a..9251a1f61e315 100644 --- a/dotnet/src/Skills/Skills.UnitTests/Document/DocumentSkillTests.cs +++ b/dotnet/src/Skills/Skills.UnitTests/Document/DocumentSkillTests.cs @@ -11,7 +11,6 @@ using Microsoft.SemanticKernel.Skills.Document.FileSystem; using Moq; using Xunit; -using static Microsoft.SemanticKernel.Skills.Document.DocumentSkill; namespace SemanticKernel.Skills.UnitTests.Document; @@ -40,7 +39,7 @@ public async Task ReadTextAsyncSucceedsAsync() var target = new DocumentSkill(documentConnectorMock.Object, fileSystemConnectorMock.Object); // Act - string actual = await target.ReadTextAsync(anyFilePath, this._context); + string actual = await target.ReadTextAsync(anyFilePath); // Assert Assert.Equal(expectedText, actual); @@ -72,10 +71,8 @@ public async Task AppendTextAsyncFileExistsSucceedsAsync() var target = new DocumentSkill(documentConnectorMock.Object, fileSystemConnectorMock.Object); - this._context.Variables.Set(Parameters.FilePath, anyFilePath); - // Act - await target.AppendTextAsync(anyText, this._context); + await target.AppendTextAsync(anyText, anyFilePath); // Assert Assert.False(this._context.ErrorOccurred); @@ -108,10 +105,8 @@ public async Task AppendTextAsyncFileDoesNotExistSucceedsAsync() var target = new DocumentSkill(documentConnectorMock.Object, fileSystemConnectorMock.Object); - this._context.Variables.Set(Parameters.FilePath, anyFilePath); - // Act - await target.AppendTextAsync(anyText, this._context); + await target.AppendTextAsync(anyText, anyFilePath); // Assert Assert.False(this._context.ErrorOccurred); @@ -130,11 +125,11 @@ public async Task AppendTextAsyncNoFilePathFailsAsync() var target = new DocumentSkill(documentConnectorMock.Object, fileSystemConnectorMock.Object); - // Act - await target.AppendTextAsync(anyText, this._context); + // Act/Assert + await Assert.ThrowsAnyAsync(() => + target.AppendTextAsync(anyText, null!)); // Assert - Assert.True(this._context.ErrorOccurred); fileSystemConnectorMock.Verify(mock => mock.GetWriteableFileStreamAsync(It.IsAny(), It.IsAny()), Times.Never()); documentConnectorMock.Verify(mock => mock.AppendText(It.IsAny(), It.IsAny()), Times.Never()); } diff --git a/dotnet/src/Skills/Skills.UnitTests/MsGraph/CalendarSkillTests.cs b/dotnet/src/Skills/Skills.UnitTests/MsGraph/CalendarSkillTests.cs index 98d9fc054d058..91679ea38bc41 100644 --- a/dotnet/src/Skills/Skills.UnitTests/MsGraph/CalendarSkillTests.cs +++ b/dotnet/src/Skills/Skills.UnitTests/MsGraph/CalendarSkillTests.cs @@ -50,14 +50,14 @@ public async Task AddEventAsyncSucceedsAsync() CalendarSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.Location, anyLocation); - this._context.Variables.Set(Parameters.Content, anyContent); - this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); - // Act - await target.AddEventAsync(anySubject, this._context); + await target.AddEventAsync( + anySubject, + anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyLocation, + anyContent, + string.Join(";", anyAttendees)); // Assert Assert.False(this._context.ErrorOccurred); @@ -89,13 +89,14 @@ public async Task AddEventAsyncWithoutLocationSucceedsAsync() CalendarSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.Content, anyContent); - this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); - // Act - await target.AddEventAsync(anySubject, this._context); + await target.AddEventAsync( + anySubject, + anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + null, + anyContent, + string.Join(";", anyAttendees)); // Assert Assert.False(this._context.ErrorOccurred); @@ -127,13 +128,14 @@ public async Task AddEventAsyncWithoutContentSucceedsAsync() CalendarSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.Location, anyLocation); - this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); - // Act - await target.AddEventAsync(anySubject, this._context); + await target.AddEventAsync( + anySubject, + anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyLocation, + null, + string.Join(";", anyAttendees)); // Assert Assert.False(this._context.ErrorOccurred); @@ -165,13 +167,13 @@ public async Task AddEventAsyncWithoutAttendeesSucceedsAsync() CalendarSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.Location, anyLocation); - this._context.Variables.Set(Parameters.Content, anyContent); - // Act - await target.AddEventAsync(anySubject, this._context); + await target.AddEventAsync( + anySubject, + anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyLocation, + anyContent); // Assert Assert.False(this._context.ErrorOccurred); @@ -192,16 +194,15 @@ public async Task AddEventAsyncWithoutStartFailsAsync() CalendarSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.Location, anyLocation); - this._context.Variables.Set(Parameters.Content, anyContent); - this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); - - // Act - await target.AddEventAsync(anySubject, this._context); - - // Assert - Assert.True(this._context.ErrorOccurred); + // Act/Assert + await Assert.ThrowsAnyAsync(() => + target.AddEventAsync( + anySubject, + null!, + anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyLocation, + anyContent, + string.Join(";", anyAttendees))); } [Fact] @@ -218,16 +219,15 @@ public async Task AddEventAsyncWithoutEndFailsAsync() CalendarSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.Location, anyLocation); - this._context.Variables.Set(Parameters.Content, anyContent); - this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); - - // Act - await target.AddEventAsync(anySubject, this._context); - - // Assert - Assert.True(this._context.ErrorOccurred); + // Act/Assert + await Assert.ThrowsAnyAsync(() => + target.AddEventAsync( + anySubject, + anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + null!, + anyLocation, + anyContent, + string.Join(";", anyAttendees))); } [Fact] @@ -244,17 +244,15 @@ public async Task AddEventAsyncWithoutSubjectFailsAsync() CalendarSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); - this._context.Variables.Set(Parameters.Location, anyLocation); - this._context.Variables.Set(Parameters.Content, anyContent); - this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); - - // Act - await target.AddEventAsync(string.Empty, this._context); - - // Assert - Assert.True(this._context.ErrorOccurred); + // Act/Assert + await Assert.ThrowsAnyAsync(() => + target.AddEventAsync( + string.Empty, + anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat), + anyLocation, + anyContent, + string.Join(";", anyAttendees))); } protected virtual void Dispose(bool disposing) diff --git a/dotnet/src/Skills/Skills.UnitTests/MsGraph/CloudDriveSkillTests.cs b/dotnet/src/Skills/Skills.UnitTests/MsGraph/CloudDriveSkillTests.cs index 4d48d79091b66..2cb682ac2cb2a 100644 --- a/dotnet/src/Skills/Skills.UnitTests/MsGraph/CloudDriveSkillTests.cs +++ b/dotnet/src/Skills/Skills.UnitTests/MsGraph/CloudDriveSkillTests.cs @@ -12,7 +12,6 @@ using SemanticKernel.Skills.UnitTests.XunitHelpers; using Xunit; using Xunit.Abstractions; -using static Microsoft.SemanticKernel.Skills.MsGraph.CloudDriveSkill; namespace SemanticKernel.Skills.UnitTests.MsGraph; @@ -38,11 +37,10 @@ public async Task UploadSmallFileAsyncSucceedsAsync() connectorMock.Setup(c => c.UploadSmallFileAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); - this._context.Variables.Set(Parameters.DestinationPath, Guid.NewGuid().ToString()); CloudDriveSkill target = new(connectorMock.Object); // Act - await target.UploadFileAsync(anyFilePath, this._context); + await target.UploadFileAsync(anyFilePath, Guid.NewGuid().ToString()); // Assert connectorMock.VerifyAll(); @@ -62,7 +60,7 @@ public async Task CreateLinkAsyncSucceedsAsync() CloudDriveSkill target = new(connectorMock.Object); // Act - string actual = await target.CreateLinkAsync(anyFilePath, this._context); + string actual = await target.CreateLinkAsync(anyFilePath); // Assert Assert.Equal(anyLink, actual); @@ -84,7 +82,7 @@ public async Task GetFileContentAsyncSucceedsAsync() CloudDriveSkill target = new(connectorMock.Object); // Act - string actual = await target.GetFileContentAsync(anyFilePath, this._context); + string actual = await target.GetFileContentAsync(anyFilePath); // Assert Assert.Equal(expectedContent, actual); diff --git a/dotnet/src/Skills/Skills.UnitTests/MsGraph/EmailSkillTests.cs b/dotnet/src/Skills/Skills.UnitTests/MsGraph/EmailSkillTests.cs index 88ecb743c2b50..5ed6b313c3996 100644 --- a/dotnet/src/Skills/Skills.UnitTests/MsGraph/EmailSkillTests.cs +++ b/dotnet/src/Skills/Skills.UnitTests/MsGraph/EmailSkillTests.cs @@ -33,7 +33,7 @@ public async Task SendEmailAsyncSucceedsAsync() this._context.Variables.Set(Parameters.Subject, anySubject); // Act - await target.SendEmailAsync(anyContent, this._context); + await target.SendEmailAsync(anyContent, anyRecipient, anySubject); // Assert Assert.False(this._context.ErrorOccurred); @@ -50,14 +50,11 @@ public async Task SendEmailAsyncNoRecipientFailsAsync() string anyContent = Guid.NewGuid().ToString(); string anySubject = Guid.NewGuid().ToString(); - this._context.Variables.Set(Parameters.Subject, anySubject); - this._context.Variables.Update(anyContent); - - // Act - await target.SendEmailAsync(anyContent, this._context); + // Act/Assert + await Assert.ThrowsAnyAsync(() => + target.SendEmailAsync(anyContent, null!, anySubject)); // Assert - Assert.True(this._context.ErrorOccurred); connectorMock.VerifyAll(); } @@ -71,14 +68,11 @@ public async Task SendEmailAsyncNoSubjectFailsAsync() string anyContent = Guid.NewGuid().ToString(); string anyRecipient = Guid.NewGuid().ToString(); - this._context.Variables.Set(Parameters.Recipients, anyRecipient); - this._context.Variables.Update(anyContent); - - // Act - await target.SendEmailAsync(anyContent, this._context); + // Act/Assert + await Assert.ThrowsAnyAsync(() => + target.SendEmailAsync(anyContent, anyRecipient, null!)); // Assert - Assert.True(this._context.ErrorOccurred); connectorMock.VerifyAll(); } diff --git a/dotnet/src/Skills/Skills.UnitTests/MsGraph/OrganizationHierarchySkillTests.cs b/dotnet/src/Skills/Skills.UnitTests/MsGraph/OrganizationHierarchySkillTests.cs index f6b0a8d7272ab..2a251b7e6c9a4 100644 --- a/dotnet/src/Skills/Skills.UnitTests/MsGraph/OrganizationHierarchySkillTests.cs +++ b/dotnet/src/Skills/Skills.UnitTests/MsGraph/OrganizationHierarchySkillTests.cs @@ -35,7 +35,7 @@ public async Task GetMyDirectReportsEmailAsyncSucceedsAsync() OrganizationHierarchySkill target = new(connectorMock.Object); // Act - IEnumerable actual = await target.GetMyDirectReportsEmailAsync(this._context); + IEnumerable actual = await target.GetMyDirectReportsEmailAsync(); // Assert var set = new HashSet(actual); @@ -57,7 +57,7 @@ public async Task GetMyManagerEmailAsyncSucceedsAsync() OrganizationHierarchySkill target = new(connectorMock.Object); // Act - string actual = await target.GetMyManagerEmailAsync(this._context); + string actual = await target.GetMyManagerEmailAsync(); // Assert Assert.Equal(anyManagerEmail, actual); @@ -74,7 +74,7 @@ public async Task GetMyManagerNameAsyncSucceedsAsync() OrganizationHierarchySkill target = new(connectorMock.Object); // Act - string actual = await target.GetMyManagerNameAsync(this._context); + string actual = await target.GetMyManagerNameAsync(); // Assert Assert.Equal(anyManagerName, actual); diff --git a/dotnet/src/Skills/Skills.UnitTests/MsGraph/TaskListSkillTests.cs b/dotnet/src/Skills/Skills.UnitTests/MsGraph/TaskListSkillTests.cs index 6530a2465d5af..29040b502540c 100644 --- a/dotnet/src/Skills/Skills.UnitTests/MsGraph/TaskListSkillTests.cs +++ b/dotnet/src/Skills/Skills.UnitTests/MsGraph/TaskListSkillTests.cs @@ -42,11 +42,8 @@ public async Task AddTaskAsyncNoReminderSucceedsAsync() TaskListSkill target = new(connectorMock.Object); - // Verify no reminder is set - Assert.False(this._context.Variables.Get(Parameters.Reminder, out _)); - // Act - await target.AddTaskAsync(anyTitle, this._context); + await target.AddTaskAsync(anyTitle); // Assert Assert.False(this._context.ErrorOccurred); @@ -69,10 +66,9 @@ public async Task AddTaskAsyncWithReminderSucceedsAsync() string anyReminder = (DateTimeOffset.Now + TimeSpan.FromHours(1)).ToString("o"); TaskListSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.Reminder, anyReminder); // Act - await target.AddTaskAsync(anyTitle, this._context); + await target.AddTaskAsync(anyTitle, anyReminder); // Assert Assert.False(this._context.ErrorOccurred); @@ -94,13 +90,12 @@ public async Task AddTaskAsyncNoDefaultTaskListFailsAsync() string anyReminder = (DateTimeOffset.Now + TimeSpan.FromHours(1)).ToString("o"); TaskListSkill target = new(connectorMock.Object); - this._context.Variables.Set(Parameters.Reminder, anyReminder); - // Act - await target.AddTaskAsync(anyTitle, this._context); + // Act/Assert + await Assert.ThrowsAnyAsync(() => + target.AddTaskAsync(anyTitle, anyReminder)); // Assert - Assert.True(this._context.ErrorOccurred); connectorMock.VerifyAll(); } diff --git a/dotnet/src/Skills/Skills.UnitTests/OpenAPI/JsonPathSkillTests.cs b/dotnet/src/Skills/Skills.UnitTests/OpenAPI/JsonPathSkillTests.cs index 534da099c5fb9..f35f39b1992ae 100644 --- a/dotnet/src/Skills/Skills.UnitTests/OpenAPI/JsonPathSkillTests.cs +++ b/dotnet/src/Skills/Skills.UnitTests/OpenAPI/JsonPathSkillTests.cs @@ -48,10 +48,9 @@ public void GetJsonElementValueSucceeds(string jsonPath, string expected) var target = new JsonPathSkill(); ContextVariables variables = new(Json); - variables[JsonPathSkill.Parameters.JsonPath] = jsonPath; SKContext context = new(variables); - string actual = target.GetJsonElementValue(Json, context); + string actual = target.GetJsonElementValue(Json, jsonPath); Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase); } @@ -65,10 +64,9 @@ public void GetJsonPropertyValueSucceeds(string jsonPath, string expected) var target = new JsonPathSkill(); ContextVariables variables = new(Json); - variables[JsonPathSkill.Parameters.JsonPath] = jsonPath; SKContext context = new(variables); - string actual = target.GetJsonElements(Json, context); + string actual = target.GetJsonElements(Json, jsonPath); Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase); } diff --git a/dotnet/src/Skills/Skills.UnitTests/Web/WebSearchEngineSkillTests.cs b/dotnet/src/Skills/Skills.UnitTests/Web/WebSearchEngineSkillTests.cs index 7b30206428b9b..2aa7d439a0cf4 100644 --- a/dotnet/src/Skills/Skills.UnitTests/Web/WebSearchEngineSkillTests.cs +++ b/dotnet/src/Skills/Skills.UnitTests/Web/WebSearchEngineSkillTests.cs @@ -39,7 +39,7 @@ public async Task SearchAsyncSucceedsAsync() string anyQuery = Guid.NewGuid().ToString(); // Act - await target.SearchAsync(anyQuery, this._context); + await target.SearchAsync(anyQuery); // Assert Assert.False(this._context.ErrorOccurred); diff --git a/dotnet/src/Skills/Skills.Web/SearchUrlSkill.cs b/dotnet/src/Skills/Skills.Web/SearchUrlSkill.cs index 3633d05291625..fc337eb2ad740 100644 --- a/dotnet/src/Skills/Skills.Web/SearchUrlSkill.cs +++ b/dotnet/src/Skills/Skills.Web/SearchUrlSkill.cs @@ -19,7 +19,7 @@ public class SearchUrlSkill /// Get search URL for Amazon /// [SKFunction("Return URL for Amazon search query")] - public string AmazonSearchUrl(string query) + public string AmazonSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.amazon.com/s?k={encoded}"; @@ -32,8 +32,7 @@ public string AmazonSearchUrl(string query) /// Get search URL for Bing /// [SKFunction("Return URL for Bing search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string BingSearchUrl(string query) + public string BingSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.bing.com/search?q={encoded}"; @@ -43,8 +42,7 @@ public string BingSearchUrl(string query) /// Get search URL for Bing Images /// [SKFunction("Return URL for Bing Images search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string BingImagesSearchUrl(string query) + public string BingImagesSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.bing.com/images/search?q={encoded}"; @@ -54,8 +52,7 @@ public string BingImagesSearchUrl(string query) /// Get search URL for Bing Maps /// [SKFunction("Return URL for Bing Maps search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string BingMapsSearchUrl(string query) + public string BingMapsSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.bing.com/maps?q={encoded}"; @@ -65,8 +62,7 @@ public string BingMapsSearchUrl(string query) /// Get search URL for Bing Shopping /// [SKFunction("Return URL for Bing Shopping search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string BingShoppingSearchUrl(string query) + public string BingShoppingSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.bing.com/shop?q={encoded}"; @@ -76,8 +72,7 @@ public string BingShoppingSearchUrl(string query) /// Get search URL for Bing News /// [SKFunction("Return URL for Bing News search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string BingNewsSearchUrl(string query) + public string BingNewsSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.bing.com/news/search?q={encoded}"; @@ -87,8 +82,7 @@ public string BingNewsSearchUrl(string query) /// Get search URL for Bing Travel /// [SKFunction("Return URL for Bing Travel search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string BingTravelSearchUrl(string query) + public string BingTravelSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.bing.com/travel/search?q={encoded}"; @@ -101,8 +95,7 @@ public string BingTravelSearchUrl(string query) /// Get search URL for Facebook /// [SKFunction("Return URL for Facebook search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string FacebookSearchUrl(string query) + public string FacebookSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.facebook.com/search/top/?q={encoded}"; @@ -115,8 +108,7 @@ public string FacebookSearchUrl(string query) /// Get search URL for GitHub /// [SKFunction("Return URL for GitHub search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string GitHubSearchUrl(string query) + public string GitHubSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://github.com/search?q={encoded}"; @@ -129,8 +121,7 @@ public string GitHubSearchUrl(string query) /// Get search URL for LinkedIn /// [SKFunction("Return URL for LinkedIn search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string LinkedInSearchUrl(string query) + public string LinkedInSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://www.linkedin.com/search/results/index/?keywords={encoded}"; @@ -143,8 +134,7 @@ public string LinkedInSearchUrl(string query) /// Get search URL for Twitter /// [SKFunction("Return URL for Twitter search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string TwitterSearchUrl(string query) + public string TwitterSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://twitter.com/search?q={encoded}"; @@ -157,8 +147,7 @@ public string TwitterSearchUrl(string query) /// Get search URL for Wikipedia /// [SKFunction("Return URL for Wikipedia search query.")] - [SKFunctionInput(Description = "Text to search for")] - public string WikipediaSearchUrl(string query) + public string WikipediaSearchUrl([SKFunctionInput("Text to search for")] string query) { string encoded = UrlEncoder.Default.Encode(query); return $"https://wikipedia.org/w/index.php?search={encoded}"; diff --git a/dotnet/src/Skills/Skills.Web/WebFileDownloadSkill.cs b/dotnet/src/Skills/Skills.Web/WebFileDownloadSkill.cs index 643f84d5a5ba7..8a9952f880214 100644 --- a/dotnet/src/Skills/Skills.Web/WebFileDownloadSkill.cs +++ b/dotnet/src/Skills/Skills.Web/WebFileDownloadSkill.cs @@ -4,10 +4,10 @@ using System.Collections.Generic; using System.IO; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; namespace Microsoft.SemanticKernel.Skills.Web; @@ -41,35 +41,27 @@ public WebFileDownloadSkill(ILogger? logger = null) /// Downloads a file to a local file path. /// /// URI of file to download - /// Semantic Kernel context + /// Path where to save file locally + /// The token to use to request cancellation. /// Task. /// Thrown when the location where to download the file is not provided - [SKFunction("Downloads a file to local storage")] - [SKFunctionName("DownloadToFile")] - [SKFunctionInput(Description = "URL of file to download")] - [SKFunctionContextParameter(Name = FilePathParamName, Description = "Path where to save file locally")] - public async Task DownloadToFileAsync(string source, SKContext context) + [SKFunction("Downloads a file to local storage", Name = "DownloadToFile")] + public async Task DownloadToFileAsync( + [SKFunctionInput("URL of file to download")] string source, + [SKFunctionContextParameter("Path where to save file locally")] string filePath, + CancellationToken cancellationToken = default) { this._logger.LogDebug($"{nameof(this.DownloadToFileAsync)} got called"); - if (!context.Variables.Get(FilePathParamName, out string filePath)) - { - this._logger.LogError($"Missing context variable in {nameof(this.DownloadToFileAsync)}"); - string errorMessage = $"Missing variable {FilePathParamName}"; - context.Fail(errorMessage); - - return; - } - this._logger.LogDebug("Sending GET request for {0}", source); - using HttpResponseMessage response = await this._httpClient.GetAsync(new Uri(source), HttpCompletionOption.ResponseHeadersRead, context.CancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await this._httpClient.GetAsync(new Uri(source), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); this._logger.LogDebug("Response received: {0}", response.StatusCode); using Stream webStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using FileStream outputFileStream = new(Environment.ExpandEnvironmentVariables(filePath), FileMode.Create); - await webStream.CopyToAsync(outputFileStream, 81920 /*same value used by default*/, cancellationToken: context.CancellationToken).ConfigureAwait(false); + await webStream.CopyToAsync(outputFileStream, 81920 /*same value used by default*/, cancellationToken).ConfigureAwait(false); } /// diff --git a/dotnet/src/Skills/Skills.Web/WebSearchEngineSkill.cs b/dotnet/src/Skills/Skills.Web/WebSearchEngineSkill.cs index 206a0d9ab5c8e..ecc8caef34a06 100644 --- a/dotnet/src/Skills/Skills.Web/WebSearchEngineSkill.cs +++ b/dotnet/src/Skills/Skills.Web/WebSearchEngineSkill.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Globalization; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; namespace Microsoft.SemanticKernel.Skills.Web; @@ -27,25 +28,22 @@ public WebSearchEngineSkill(IWebSearchEngineConnector connector) this._connector = connector; } - [SKFunction("Perform a web search.")] - [SKFunctionName("Search")] - [SKFunctionInput(Description = "Text to search for")] - [SKFunctionContextParameter(Name = CountParam, Description = "Number of results", DefaultValue = DefaultCount)] - [SKFunctionContextParameter(Name = OffsetParam, Description = "Number of results to skip", DefaultValue = DefaultOffset)] - public async Task SearchAsync(string query, SKContext context) + [SKFunction("Perform a web search.", Name = "Search")] + public async Task SearchAsync( + [SKFunctionInput("Text to search for")] string query, + [SKFunctionContextParameter("Number of results")] string count = DefaultCount, + [SKFunctionContextParameter("Number of results to skip")] string offset = DefaultOffset, + CancellationToken cancellationToken = default) { - var count = context.Variables.ContainsKey(CountParam) ? context[CountParam] : DefaultCount; if (string.IsNullOrWhiteSpace(count)) { count = DefaultCount; } - - var offset = context.Variables.ContainsKey(OffsetParam) ? context[OffsetParam] : DefaultOffset; if (string.IsNullOrWhiteSpace(offset)) { offset = DefaultOffset; } int countInt = int.Parse(count, CultureInfo.InvariantCulture); int offsetInt = int.Parse(offset, CultureInfo.InvariantCulture); - var results = await this._connector.SearchAsync(query, countInt, offsetInt, context.CancellationToken).ConfigureAwait(false); + var results = await this._connector.SearchAsync(query, countInt, offsetInt, cancellationToken).ConfigureAwait(false); if (!results.Any()) { - context.Fail("Failed to get a response from the web search engine."); + throw new InvalidOperationException("Failed to get a response from the web search engine."); } return countInt == 1 diff --git a/samples/dotnet/FileCompression/FileCompressionSkill.cs b/samples/dotnet/FileCompression/FileCompressionSkill.cs index f458e50126c8c..c480d6f906665 100644 --- a/samples/dotnet/FileCompression/FileCompressionSkill.cs +++ b/samples/dotnet/FileCompression/FileCompressionSkill.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -59,28 +60,21 @@ public FileCompressionSkill(IFileCompressor fileCompressor, ILogger /// Path of file to compress - /// Semantic Kernel context + /// Path of compressed file to create + /// The token to use to request cancellation. /// Path of created compressed file /// [SKFunction("Compresses an input file to an output file")] - [SKFunctionInput(Description = "Path of file to compress")] - [SKFunctionContextParameter(Name = Parameters.DestinationFilePath, Description = "Path of compressed file to create")] - public async Task CompressFileAsync(string sourceFilePath, SKContext context) + public async Task CompressFileAsync( + [SKFunctionInput("Path of file to compress")] string sourceFilePath, + [SKFunctionContextParameter("Path of compressed file to create")] string destinationFilePath, + CancellationToken cancellationToken = default) { this._logger.LogTrace($"{nameof(this.CompressFileAsync)} got called"); - if (!context.Variables.Get(Parameters.DestinationFilePath, out string destinationFilePath)) - { - const string ErrorMessage = $"Missing context variable {Parameters.DestinationFilePath} in {nameof(this.CompressFileAsync)}"; - this._logger.LogError(ErrorMessage); - context.Fail(ErrorMessage); - - return null; - } - await this._fileCompressor.CompressFileAsync(Environment.ExpandEnvironmentVariables(sourceFilePath), Environment.ExpandEnvironmentVariables(destinationFilePath), - context.CancellationToken); + cancellationToken); return destinationFilePath; } @@ -89,28 +83,21 @@ await this._fileCompressor.CompressFileAsync(Environment.ExpandEnvironmentVariab /// Compresses a directory to an output file. /// /// Path of directory to compress - /// Semantic Kernel context + /// Path of compressed file to create + /// The token to use to request cancellation. /// Path of created compressed file /// [SKFunction("Compresses a directory to an output file")] - [SKFunctionInput(Description = "Path of directory to compress")] - [SKFunctionContextParameter(Name = Parameters.DestinationFilePath, Description = "Path of compressed file to create")] - public async Task CompressDirectoryAsync(string sourceDirectoryPath, SKContext context) + public async Task CompressDirectoryAsync( + [SKFunctionInput("Path of directory to compress")] string sourceDirectoryPath, + [SKFunctionContextParameter("Path of compressed file to create")] string destinationFilePath, + CancellationToken cancellationToken = default) { this._logger.LogTrace($"{nameof(this.CompressDirectoryAsync)} got called"); - if (!context.Variables.Get(Parameters.DestinationFilePath, out string destinationFilePath)) - { - const string ErrorMessage = $"Missing context variable {Parameters.DestinationFilePath} in {nameof(this.CompressDirectoryAsync)}"; - this._logger.LogError(ErrorMessage); - context.Fail(ErrorMessage); - - return null; - } - await this._fileCompressor.CompressDirectoryAsync(Environment.ExpandEnvironmentVariables(sourceDirectoryPath), Environment.ExpandEnvironmentVariables(destinationFilePath), - context.CancellationToken); + cancellationToken); return destinationFilePath; } @@ -119,25 +106,18 @@ await this._fileCompressor.CompressDirectoryAsync(Environment.ExpandEnvironmentV /// Decompresses an input file. /// /// Path of file to decompress - /// Semantic Kernel context + /// Directory into which to extract the decompressed content + /// The token to use to request cancellation. /// Path of created compressed file /// [SKFunction("Decompresses an input file")] - [SKFunctionInput(Description = "Path of directory into which decompressed content was extracted")] - [SKFunctionContextParameter(Name = Parameters.DestinationDirectoryPath, Description = "Directory into which to extract the decompressed content")] - public async Task DecompressFileAsync(string sourceFilePath, SKContext context) + public async Task DecompressFileAsync( + [SKFunctionInput("Path of directory into which decompressed content was extracted")] string sourceFilePath, + [SKFunctionContextParameter("Directory into which to extract the decompressed content")] string destinationDirectoryPath, + CancellationToken cancellationToken = default) { this._logger.LogTrace($"{nameof(this.DecompressFileAsync)} got called"); - if (!context.Variables.Get(Parameters.DestinationDirectoryPath, out string destinationDirectoryPath)) - { - const string ErrorMessage = $"Missing context variable {Parameters.DestinationDirectoryPath} in {nameof(this.DecompressFileAsync)}"; - this._logger.LogError(ErrorMessage); - context.Fail(ErrorMessage); - - return null; - } - if (!Directory.Exists(destinationDirectoryPath)) { Directory.CreateDirectory(destinationDirectoryPath); @@ -145,7 +125,7 @@ await this._fileCompressor.CompressDirectoryAsync(Environment.ExpandEnvironmentV await this._fileCompressor.DecompressFileAsync(Environment.ExpandEnvironmentVariables(sourceFilePath), Environment.ExpandEnvironmentVariables(destinationDirectoryPath), - context.CancellationToken); + cancellationToken); return destinationDirectoryPath; } diff --git a/samples/dotnet/github-skills/GitHubSkill.cs b/samples/dotnet/github-skills/GitHubSkill.cs index 34d0e21233e42..2f087efb7346e 100644 --- a/samples/dotnet/github-skills/GitHubSkill.cs +++ b/samples/dotnet/github-skills/GitHubSkill.cs @@ -98,22 +98,23 @@ public GitHubSkill(IKernel kernel, WebFileDownloadSkill downloadSkill, ILogger /// URI to download the repository content to be summarized + /// Name of the repository repositoryBranch which will be downloaded and summarized + /// The search string to match against the names of files in the repository /// Semantic kernel context /// Task - [SKFunction("Downloads a repository and summarizes the content")] - [SKFunctionName("SummarizeRepository")] - [SKFunctionInput(Description = "URL of the GitHub repository to summarize")] - [SKFunctionContextParameter(Name = RepositoryBranchParamName, - Description = "Name of the repository repositoryBranch which will be downloaded and summarized")] - [SKFunctionContextParameter(Name = SearchPatternParamName, Description = "The search string to match against the names of files in the repository")] - public async Task SummarizeRepositoryAsync(string source, SKContext context) + [SKFunction("Downloads a repository and summarizes the content", Name = "SummarizeRepository")] + public async Task SummarizeRepositoryAsync( + [SKFunctionInput("URL of the GitHub repository to summarize")] string source, + [SKFunctionContextParameter("Name of the repository repositoryBranch which will be downloaded and summarized", DefaultValue = "main")] string repositoryBranch, + [SKFunctionContextParameter("The search string to match against the names of files in the repository", DefaultValue = "*.md")] string searchPattern, + SKContext context) { - if (!context.Variables.Get(RepositoryBranchParamName, out string repositoryBranch) || string.IsNullOrEmpty(repositoryBranch)) + if (string.IsNullOrEmpty(repositoryBranch)) { repositoryBranch = "main"; } - if (!context.Variables.Get(SearchPatternParamName, out string searchPattern) || string.IsNullOrEmpty(searchPattern)) + if (string.IsNullOrEmpty(searchPattern)) { searchPattern = "*.md"; } @@ -125,13 +126,11 @@ public async Task SummarizeRepositoryAsync(string source, SKContext context) try { var repositoryUri = source.Trim(s_trimChars); - var context1 = new SKContext(logger: context.Log); - context1.Variables.Set(FilePathParamName, filePath); - await this._downloadSkill.DownloadToFileAsync($"{repositoryUri}/archive/refs/heads/{repositoryBranch}.zip", context1); + await this._downloadSkill.DownloadToFileAsync($"{repositoryUri}/archive/refs/heads/{repositoryBranch}.zip", filePath, context.CancellationToken); ZipFile.ExtractToDirectory(filePath, directoryPath); - await this.SummarizeCodeDirectoryAsync(directoryPath, searchPattern, repositoryUri, repositoryBranch, context); + await this.SummarizeCodeDirectoryAsync(directoryPath, searchPattern, repositoryUri, repositoryBranch); context.Variables.Set(MemoryCollectionNameParamName, $"{repositoryUri}-{repositoryBranch}"); } @@ -205,7 +204,7 @@ await this._kernel.Memory.SaveInformationAsync( /// /// Summarize the code found under a directory into embeddings (one per file) /// - private async Task SummarizeCodeDirectoryAsync(string directoryPath, string searchPattern, string repositoryUri, string repositoryBranch, SKContext context) + private async Task SummarizeCodeDirectoryAsync(string directoryPath, string searchPattern, string repositoryUri, string repositoryBranch) { string[] filePaths = Directory.GetFiles(directoryPath, searchPattern, SearchOption.AllDirectories); diff --git a/samples/dotnet/kernel-syntax-examples/Example09_FunctionTypes.cs b/samples/dotnet/kernel-syntax-examples/Example09_FunctionTypes.cs index 7fc8086aaf58c..61566a8ba8a45 100644 --- a/samples/dotnet/kernel-syntax-examples/Example09_FunctionTypes.cs +++ b/samples/dotnet/kernel-syntax-examples/Example09_FunctionTypes.cs @@ -108,8 +108,7 @@ public string Type02() return ""; } - [SKFunction("Native function type 3")] - [SKFunctionName("Type03")] + [SKFunction("Native function type 3", Name = "Type03")] public async Task Type03Async() { await Task.Delay(0); @@ -130,8 +129,7 @@ public string Type05(SKContext context) return ""; } - [SKFunction("Native function type 6")] - [SKFunctionName("Type06")] + [SKFunction("Native function type 6", Name = "Type06")] public async Task Type06Async(SKContext context) { var summarizer = context.Func("SummarizeSkill", "Summarize"); @@ -142,8 +140,7 @@ public async Task Type06Async(SKContext context) return ""; } - [SKFunction("Native function type 7")] - [SKFunctionName("Type07")] + [SKFunction("Native function type 7", Name = "Type07")] public async Task Type07Async(SKContext context) { await Task.Delay(0); @@ -164,8 +161,7 @@ public string Type09(string x) return ""; } - [SKFunction("Native function type 10")] - [SKFunctionName("Type10")] + [SKFunction("Native function type 10", Name = "Type10")] public async Task Type10Async(string x) { await Task.Delay(0); @@ -186,8 +182,7 @@ public string Type12(string x, SKContext context) return ""; } - [SKFunction("Native function type 13")] - [SKFunctionName("Type13")] + [SKFunction("Native function type 13", Name = "Type13")] public async Task Type13Async(string x, SKContext context) { await Task.Delay(0); @@ -195,8 +190,7 @@ public async Task Type13Async(string x, SKContext context) return ""; } - [SKFunction("Native function type 14")] - [SKFunctionName("Type14")] + [SKFunction("Native function type 14", Name = "Type14")] public async Task Type14Async(string x, SKContext context) { await Task.Delay(0); @@ -204,32 +198,28 @@ public async Task Type14Async(string x, SKContext context) return context; } - [SKFunction("Native function type 15")] - [SKFunctionName("Type15")] + [SKFunction("Native function type 15", Name = "Type15")] public async Task Type15Async(string x) { await Task.Delay(0); Console.WriteLine("Running function type 15"); } - [SKFunction("Native function type 16")] - [SKFunctionName("Type16")] + [SKFunction("Native function type 16", Name = "Type16")] public async Task Type16Async(SKContext context) { await Task.Delay(0); Console.WriteLine("Running function type 16"); } - [SKFunction("Native function type 17")] - [SKFunctionName("Type17")] + [SKFunction("Native function type 17", Name = "Type17")] public async Task Type17Async(string x, SKContext context) { await Task.Delay(0); Console.WriteLine("Running function type 17"); } - [SKFunction("Native function type 18")] - [SKFunctionName("Type18")] + [SKFunction("Native function type 18", Name = "Type18")] public async Task Type18Async() { await Task.Delay(0); diff --git a/samples/dotnet/kernel-syntax-examples/Example15_MemorySkill.cs b/samples/dotnet/kernel-syntax-examples/Example15_MemorySkill.cs index d972be6bfbb05..acc2466049271 100644 --- a/samples/dotnet/kernel-syntax-examples/Example15_MemorySkill.cs +++ b/samples/dotnet/kernel-syntax-examples/Example15_MemorySkill.cs @@ -48,8 +48,7 @@ public static async Task RunAsync() // ========= Test memory remember ========= Console.WriteLine("========= Example: Recalling a Memory ========="); - context[TextMemorySkill.KeyParam] = "info1"; - var answer = await memorySkill.RetrieveAsync(context); + var answer = await memorySkill.RetrieveAsync(MemoryCollectionName, "info5", context); Console.WriteLine("Memory associated with 'info1': {0}", answer); /* Output: @@ -59,12 +58,11 @@ public static async Task RunAsync() // ========= Test memory recall ========= Console.WriteLine("========= Example: Recalling an Idea ========="); - context[TextMemorySkill.LimitParam] = "2"; - answer = await memorySkill.RecallAsync("where did I grow up?", context); + answer = await memorySkill.RecallAsync("where did I grow up?", MemoryCollectionName, relevance: null, limit: "2", context: context); Console.WriteLine("Ask: where did I grow up?"); Console.WriteLine("Answer:\n{0}", answer); - answer = await memorySkill.RecallAsync("where do I live?", context); + answer = await memorySkill.RecallAsync("where do I live?", MemoryCollectionName, relevance: null, limit: "2", context: context); Console.WriteLine("Ask: where do I live?"); Console.WriteLine("Answer:\n{0}", answer); @@ -133,7 +131,7 @@ My name is Andrea and my family is from New York. I work as a tourist operator. */ context[TextMemorySkill.KeyParam] = "info1"; - await memorySkill.RemoveAsync(context); + await memorySkill.RemoveAsync(collection: null, "info1", context); result = await aboutMeOracle.InvokeAsync("Tell me a bit about myself", context); diff --git a/samples/dotnet/kernel-syntax-examples/Example31_CustomPlanner.cs b/samples/dotnet/kernel-syntax-examples/Example31_CustomPlanner.cs index 7d9ce0408a1da..38a2aeebf1c00 100644 --- a/samples/dotnet/kernel-syntax-examples/Example31_CustomPlanner.cs +++ b/samples/dotnet/kernel-syntax-examples/Example31_CustomPlanner.cs @@ -136,8 +136,7 @@ private static IKernel InitializeKernel() // Example Skill that can process XML Markup created by ContextQuery public class MarkupSkill { - [SKFunction("Run Markup")] - [SKFunctionName("RunMarkup")] + [SKFunction("Run Markup", Name = "RunMarkup")] public async Task RunMarkupAsync(SKContext context) { var docString = context.Variables.Input; diff --git a/samples/dotnet/kernel-syntax-examples/Skills/EmailSkill.cs b/samples/dotnet/kernel-syntax-examples/Skills/EmailSkill.cs index 7b08d47e59958..06a5bd47eab8c 100644 --- a/samples/dotnet/kernel-syntax-examples/Skills/EmailSkill.cs +++ b/samples/dotnet/kernel-syntax-examples/Skills/EmailSkill.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; @@ -10,20 +9,22 @@ namespace Skills; internal sealed class EmailSkill { [SKFunction("Given an e-mail and message body, send an email")] - [SKFunctionInput(Description = "The body of the email message to send.")] - [SKFunctionContextParameter(Name = "email_address", Description = "The email address to send email to.")] - public Task SendEmail(string input, SKContext context) + public SKContext SendEmail( + [SKFunctionInput("The body of the email message to send.")] string input, + [SKFunctionContextParameter("The email address to send email to.")] string email_address, + SKContext context) { - context.Variables.Update($"Sent email to: {context.Variables["email_address"]}. Body: {input}"); - return Task.FromResult(context); + context.Variables.Update($"Sent email to: {email_address}. Body: {input}"); + return context; } [SKFunction("Given a name, find email address")] - [SKFunctionInput(Description = "The name of the person to email.")] - public Task GetEmailAddress(string input, SKContext context) + public SKContext GetEmailAddress( + [SKFunctionInput("The name of the person whose email address needs to be found.")] string input, + SKContext context) { context.Log.LogDebug("Returning hard coded email for {0}", input); context.Variables.Update("johndoe1234@example.com"); - return Task.FromResult(context); + return context; } } diff --git a/samples/dotnet/kernel-syntax-examples/Skills/StaticTextSkill.cs b/samples/dotnet/kernel-syntax-examples/Skills/StaticTextSkill.cs index 62ea748998180..04f8fa5690338 100644 --- a/samples/dotnet/kernel-syntax-examples/Skills/StaticTextSkill.cs +++ b/samples/dotnet/kernel-syntax-examples/Skills/StaticTextSkill.cs @@ -11,17 +11,17 @@ namespace Skills; public class StaticTextSkill { [SKFunction("Change all string chars to uppercase")] - [SKFunctionInput(Description = "Text to uppercase")] - public static string Uppercase(string input) + public static string Uppercase([SKFunctionInput("Text to uppercase")] string input) { return input.ToUpperInvariant(); } [SKFunction("Append the day variable")] - [SKFunctionInput(Description = "Text to append to")] - [SKFunctionContextParameter(Name = "day", Description = "Value of the day to append")] - public static string AppendDay(string input, SKContext context) + public static string AppendDay( + [SKFunctionInput("Text to append to")] string input, + [SKFunctionContextParameter("Value of the day to append")] string day, + SKContext context) { - return input + context["day"]; + return input + day; } } diff --git a/samples/dotnet/kernel-syntax-examples/Skills/TextSkill.cs b/samples/dotnet/kernel-syntax-examples/Skills/TextSkill.cs index a8c208f6dd8e5..ab6c79db9f537 100644 --- a/samples/dotnet/kernel-syntax-examples/Skills/TextSkill.cs +++ b/samples/dotnet/kernel-syntax-examples/Skills/TextSkill.cs @@ -8,37 +8,32 @@ namespace Skills; public class TextSkill { [SKFunction("Remove spaces to the left of a string")] - [SKFunctionInput(Description = "Text to edit")] - public string LStrip(string input) + public string LStrip([SKFunctionInput("Text to edit")] string input) { return input.TrimStart(); } [SKFunction("Remove spaces to the right of a string")] - [SKFunctionInput(Description = "Text to edit")] - public string RStrip(string input) + public string RStrip([SKFunctionInput("Text to edit")] string input) { return input.TrimEnd(); } [SKFunction("Remove spaces to the left and right of a string")] - [SKFunctionInput(Description = "Text to edit")] - public string Strip(string input) + public string Strip([SKFunctionInput("Text to edit")] string input) { return input.Trim(); } [SKFunction("Change all string chars to uppercase")] - [SKFunctionInput(Description = "Text to uppercase")] - public string Uppercase(string input) + public string Uppercase([SKFunctionInput("Text to uppercase")] string input) { return input.ToUpperInvariant(); } [SKFunction("Change all string chars to lowercase")] - [SKFunctionInput(Description = "Text to lowercase")] [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "By design.")] - public string Lowercase(string input) + public string Lowercase([SKFunctionInput("Text to lowercase")] string input) { return input.ToLowerInvariant(); }