Skip to content

Commit

Permalink
Add support for multiple native function arguments
Browse files Browse the repository at this point in the history
- Native skills can now have any number of string parameters. The parameters are populated from context variables of the same name.  If no context variable exists for that name, it'll be populated with a default value if one was supplied via either an attribute or a default parameter value, or if there is none, the function will fail to be invoked.
- SKFunctionContextParameterAttribute may now be specified on a parameter directly.
- SKFunctionInputAttribute is now applied directly to the parameter rather than to the method. It may be applied to at most one string parameter, in which case it'll override that parameter to be named "Input".
- DefaultValue was removed from SKFunctionInput as it wasn't actually being respected.
- SKFunctionNameAttribute was removed, with the Name moving to being an optional property of the SKFunctionAttribute.
- If there's only one string parameter, it first looks to get its value from a context parameter named the same as the parameter, but if that fails, it'll fall back to using "Input".
- InvokeAsync will now catch exceptions and store the exception into the context.  This means native skills should handle all failures by throwing exceptions rather than by directly interacting with the context.
  • Loading branch information
stephentoub committed May 24, 2023
1 parent 0906f22 commit 7dba232
Show file tree
Hide file tree
Showing 50 changed files with 843 additions and 950 deletions.
18 changes: 9 additions & 9 deletions dotnet/src/Extensions/Planning.ActionPlanner/ActionPlanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ public async Task<Plan> CreatePlanAsync(string goal)
/// <param name="context">Function execution context</param>
/// <returns>List of functions, formatted accordingly to the prompt</returns>
[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();
Expand All @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
25 changes: 16 additions & 9 deletions dotnet/src/IntegrationTests/Fakes/EmailSkillFake.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]")]
public Task<SKContext> SendEmailAsync(string input, SKContext context)
public Task<SKContext> SendEmailAsync(
[SKFunctionInput("The body of the email message to send.")] string input,
[SKFunctionContextParameter("The email address to send email to.", DefaultValue = "[email protected]")] 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 = "[email protected]";
}

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<SKContext> GetEmailAddressAsync(string input, SKContext context)
public Task<SKContext> GetEmailAddressAsync(
[SKFunctionInput("The name of the person to email.")] string input,
SKContext context)
{
if (string.IsNullOrEmpty(input))
{
Expand All @@ -38,8 +44,9 @@ public Task<SKContext> GetEmailAddressAsync(string input, SKContext context)
}

[SKFunction("Write a short poem for an e-mail")]
[SKFunctionInput(Description = "The topic of the poem.")]
public Task<SKContext> WritePoemAsync(string input, SKContext context)
public Task<SKContext> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,13 @@ public static IEnumerable<object[]> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,31 @@
namespace Microsoft.SemanticKernel.SkillDefinition;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class SKFunctionAttribute : Attribute
{
/// <summary>
/// Function description, to be used by the planner to auto-discover functions.
/// </summary>
public string Description { get; }

/// <summary>
/// Tag a C# function as a native function available to SK.
/// Initializes the attribute with the specified description.
/// </summary>
/// <param name="description">Function description, to be used by the planner to auto-discover functions.</param>
/// <param name="description">Description of the function to be used by a planner to auto-discover functions.</param>
public SKFunctionAttribute(string description)
{
this.Description = description;
}

/// <summary>
/// Gets the description of the function to be used by a planner to auto-discover functions.
/// </summary>
public string Description { get; }

/// <summary>Gets or sets an optional name used for the function in the skill collection.</summary>
/// <remarks>If not specified, the name of the attributed method will be used.</remarks>
public string? Name { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,36 @@
namespace Microsoft.SemanticKernel.SkillDefinition;

/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
/// <remarks>
/// The attribute may be applied to parameters to a method attributed with <see cref="SKFunctionAttribute"/>
/// 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.
/// </remarks>
[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;

/// <summary>
/// Gets the context parameter description.
/// </summary>
public string? Description { get; }

/// <summary>
/// Parameter name. Alphanumeric chars + "_" only.
/// Gets or sets the name of the parameter.
/// </summary>
public string Name
/// <remarks>
/// 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.
/// </remarks>
public string? Name
{
get => this._name;
set
Expand All @@ -32,14 +46,16 @@ public string Name
}

/// <summary>
/// Parameter description.
/// </summary>
public string Description { get; set; } = string.Empty;

/// <summary>
/// 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.
/// </summary>
public string DefaultValue { get; set; } = string.Empty;
/// <remarks>
/// 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.
/// </remarks>
public string? DefaultValue { get; set; }

/// <summary>
/// Creates a parameter view, using information from an instance of this class.
Expand All @@ -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,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,44 @@
namespace Microsoft.SemanticKernel.SkillDefinition;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <example>
/// <code>
/// // No main parameter here, only context
/// public async Task WriteAsync(SKContext context
/// // No main parameter here, only context
/// public async Task WriteAsync(SKContext context)
/// </code>
/// </example>
/// <example>
/// <code>
/// // "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)
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
/// <example>
/// <code>
/// // 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)
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class SKFunctionInputAttribute : Attribute
{
/// <summary>
/// Parameter description.
/// Initializes the attribute with the specified input parameter description.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <param name="description">The description.</param>
public SKFunctionInputAttribute(string description) => this.Description = description;

/// <summary>
/// Default value when the value is not provided.
/// Gets the parameter description.
/// </summary>
public string DefaultValue { get; set; } = string.Empty;
public string Description { get; }

/// <summary>
/// Creates a parameter view, using information from an instance of this class.
Expand All @@ -50,7 +54,7 @@ public ParameterView ToParameterView()
{
Name = "input",
Description = this.Description,
DefaultValue = this.DefaultValue
DefaultValue = string.Empty,
};
}
}

This file was deleted.

13 changes: 2 additions & 11 deletions dotnet/src/SemanticKernel.UnitTests/CoreSkills/FileIOSkillTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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));
Expand All @@ -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
Expand Down
Loading

0 comments on commit 7dba232

Please sign in to comment.