diff --git a/Elsa.SemanticKernel/Activities/ActivityProviders/SemanticKernelSkillActivityProvider.cs b/Elsa.SemanticKernel/Activities/ActivityProviders/SemanticKernelSkillActivityProvider.cs index b84bbd49c..7ffaa1cb9 100644 --- a/Elsa.SemanticKernel/Activities/ActivityProviders/SemanticKernelSkillActivityProvider.cs +++ b/Elsa.SemanticKernel/Activities/ActivityProviders/SemanticKernelSkillActivityProvider.cs @@ -25,6 +25,7 @@ using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Reliability; +using Microsoft.SemanticKernel.SkillDefinition; using Microsoft.SKDevTeam; namespace Elsa.SemanticKernel; @@ -44,47 +45,196 @@ public class SemanticKernelActivityProvider : IActivityProvider } public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) { - //get a list of skills in the assembly - var skills = await LoadSkillsFromAssemblyAsync("skills"); - var descriptors = new List(); - foreach (var skill in skills) + // get the kernel + var kernel = KernelBuilder(); + + // get a list of skills in the assembly + var skills = LoadSkillsFromAssemblyAsync("skills", kernel); + SKContext context = new SKContext(); + var functionsAvailable = context.Skills.GetFunctionsView(); + + // create activity descriptors for each skilland function + var activities = new List(); + foreach (KeyValuePair> skill in functionsAvailable.SemanticFunctions) { - //var descriptor = await CreateActivityDescriptors(skill, cancellationToken); - // descriptors.Add(descriptor); + Console.WriteLine($"Creating Activities for Skill: {skill.Key}"); + foreach (FunctionView func in skill.Value) + { + activities.Add(CreateActivityDescriptorFromSkillAndFunction(func, cancellationToken)); + } } - return descriptors; + + return activities; + } + + /// + /// Creates an activity descriptor from a skill and function. + /// + /// The semantic kernel function + /// An optional cancellation token. + /// An activity descriptor. + private ActivityDescriptor CreateActivityDescriptorFromSkillAndFunction(FunctionView function, CancellationToken cancellationToken = default) + { + // Create a fully qualified type name for the activity + var thisNamespace = GetType().Namespace; + var fullTypeName = $"{thisNamespace}.{function.SkillName}.{function.Name}"; + Console.WriteLine($"Creating Activity: {fullTypeName}"); + + // create inputs from the function parameters - the SemanticKernelSkill activity will be the base for each activity + var inputs = new List(); + foreach (var p in function.Parameters) { inputs.Add(CreateInputDescriptorFromSKParameter(p)); } + inputs.Add(CreateInputDescriptor(typeof(string), "SkillName", function.SkillName, "The name of the skill to use (generated, do not change)")); + inputs.Add(CreateInputDescriptor(typeof(string), "FunctionName", function.Name, "The name of the function to use (generated, do not change)")); + inputs.Add(CreateInputDescriptor(typeof(int), "MaxRetries", KernelSettings.DefaultMaxRetries, "Max Retries to contact AI Service")); + + return new ActivityDescriptor + { + Kind = ActivityKind.Task, + Category = "Semantic Kernel", + Description = function.Description, + Name = function.Name, + TypeName = fullTypeName, + Namespace = $"{thisNamespace}.{function.SkillName}", + DisplayName = $"{function.SkillName}.{function.Name}", + Inputs = inputs, + Outputs = new[] {new OutputDescriptor()}, + Constructor = context => + { + // The constructor is called when an activity instance of this type is requested. + + // Create the activity instance. + var activityInstance = _activityFactory.Create(context); + + // Customize the activity type name. + activityInstance.Type = fullTypeName; + + // Configure the activity's URL and method properties. + activityInstance.SkillName = new Input(function.SkillName); + activityInstance.FunctionName = new Input(function.Name); + + return activityInstance; + } + }; + + } + /// + /// Creates an input descriptor for a single line string + /// + /// The name of the input field + /// The description of the input field + private InputDescriptor CreateInputDescriptor(Type inputType, string name, Object defaultValue, string description) + { + var inputDescriptor = new InputDescriptor + { + Description = description, + DefaultValue = defaultValue, + Type = inputType, + Name = name, + DisplayName = name, + IsSynthetic = true, // This is a synthetic property, i.e. it is not part of the activity's .NET type. + IsWrapped = true, // This property is wrapped within an Input object. + UIHint = InputUIHints.SingleLine, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(name), + ValueSetter = (activity, value) => activity.SyntheticProperties[name] = value!, + }; + return inputDescriptor; + } + + /// + /// Creates an input descriptor from an sk funciton parameter definition. + /// + /// The function parameter. + /// An input descriptor. + private InputDescriptor CreateInputDescriptorFromSKParameter(ParameterView parameter) + { + var inputDescriptor = new InputDescriptor + { + Description = string.IsNullOrEmpty(parameter.Description) ? parameter.Name : parameter.Description, + DefaultValue = string.IsNullOrEmpty(parameter.DefaultValue) ? string.Empty : parameter.DefaultValue, + Type = typeof(string), + Name = parameter.Name, + DisplayName = parameter.Name, + IsSynthetic = true, // This is a synthetic property, i.e. it is not part of the activity's .NET type. + IsWrapped = true, // This property is wrapped within an Input object. + UIHint = InputUIHints.MultiLine, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(parameter.Name), + ValueSetter = (activity, value) => activity.SyntheticProperties[parameter.Name] = value!, + + }; + return inputDescriptor; } /// /// Gets a list of the skills in the assembly /// - private async Task> LoadSkillsFromAssemblyAsync(string assemblyName) + private IEnumerable LoadSkillsFromAssemblyAsync(string assemblyName, IKernel kernel) { var skills = new List(); var assembly = Assembly.Load(assemblyName); - //IEnumerable skillTypes = GetTypesInNamespace(assembly, "skills"); - Type[] skillTypes = assembly.GetTypes().ToArray(); - foreach(Type skillType in skillTypes) + Type[] skillTypes = assembly.GetTypes().ToArray(); + foreach (Type skillType in skillTypes) + { + if (skillType.Namespace.Equals("Microsoft.SKDevTeam")) { - Console.WriteLine($"Found type: {assembly.FullName}.{skillType.Namespace}.{skillType.Name}"); - if(skillType.Namespace.Equals("Microsoft.SKDevTeam")) + skills.Add(skillType.Name); + var functions = skillType.GetFields(); + foreach (var function in functions) { - skills.Add(skillType.Name); + string field = function.FieldType.ToString(); + if (field.Equals("Microsoft.SKDevTeam.SemanticFunctionConfig")) + { + var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillType.Name, function.Name); + var skfunc = kernel.CreateSemanticFunction( + skillConfig.PromptTemplate, + skillConfig.Name, + skillConfig.SkillName, + skillConfig.Description, + skillConfig.MaxTokens, + skillConfig.Temperature, + skillConfig.TopP, + skillConfig.PPenalty, + skillConfig.FPenalty); - Console.WriteLine($"Added skill: {skillType.Name}"); + Console.WriteLine($"SK Added function: {skfunc.SkillName}.{skfunc.Name}"); + } } - } + } return skills; } - - - private IEnumerable GetTypesInNamespace(Assembly assembly, string nameSpace) + /// + /// Gets a semantic kernel instance + /// + /// Microsoft.SemanticKernel.IKernel + private IKernel KernelBuilder() { - return - assembly.GetTypes() - .Where(t => String.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)); + var kernelSettings = KernelSettings.LoadSettings(); + var kernelConfig = new KernelConfig(); + + using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(kernelSettings.LogLevel ?? LogLevel.Warning); + }); + + var kernel = new KernelBuilder() + .WithLogger(loggerFactory.CreateLogger()) + .WithAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey, true, kernelSettings.ServiceId, true) + .WithConfiguration(kernelConfig) + .Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig + { + MaxRetryCount = KernelSettings.DefaultMaxRetries, + UseExponentialBackoff = true, + // MinRetryDelay = TimeSpan.FromSeconds(2), + // MaxRetryDelay = TimeSpan.FromSeconds(8), + MaxTotalRetryTime = TimeSpan.FromSeconds(300), + // RetryableStatusCodes = new[] { HttpStatusCode.TooManyRequests, HttpStatusCode.RequestTimeout }, + // RetryableExceptions = new[] { typeof(HttpRequestException) } + })) + .Build(); + + return kernel; } + } diff --git a/Elsa.SemanticKernel/Activities/SemanticKernel.cs b/Elsa.SemanticKernel/Activities/SemanticKernel.cs index f3beac43e..f8b6dcbe7 100644 --- a/Elsa.SemanticKernel/Activities/SemanticKernel.cs +++ b/Elsa.SemanticKernel/Activities/SemanticKernel.cs @@ -33,12 +33,6 @@ namespace Elsa.SemanticKernel; [PublicAPI] public class SemanticKernelSkill : CodeActivity { - //constructor - called by the workflow engine - public SemanticKernelSkill(string? source = default, int? line = default) : base(source, line) - { - - } - [Input( Description = "System Prompt", UIHint = InputUIHints.MultiLine, @@ -54,7 +48,7 @@ public class SemanticKernelSkill : CodeActivity [Input( Description = "Max retries", UIHint = InputUIHints.SingleLine, - DefaultValue = 9)] + DefaultValue = KernelSettings.DefaultMaxRetries)] public Input MaxRetries { get; set; } [Input( @@ -98,6 +92,17 @@ public class SemanticKernelSkill : CodeActivity // get the kernel var kernel = KernelBuilder(); + // load the skill + var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName); + + var function = kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName, + skillConfig.Description, skillConfig.MaxTokens, skillConfig.Temperature, + skillConfig.TopP, skillConfig.PPenalty, skillConfig.FPenalty); + + // set the context (our prompt) + var contextVars = new ContextVariables(); + contextVars.Set("input", prompt); + /* var interestingMemories = kernel.Memory.SearchAsync("ImportedMemories", prompt, 2); var wafContext = "Consider the following contextual snippets:"; await foreach (var memory in interestingMemories) @@ -105,61 +110,60 @@ public class SemanticKernelSkill : CodeActivity wafContext += $"\n {memory.Metadata.Text}"; } */ - var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName); - - - /* var function = kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName, - skillConfig.Description, skillConfig.MaxTokens, skillConfig.Temperature, - skillConfig.TopP, skillConfig.PPenalty, skillConfig.FPenalty); */ - - var contextVars = new ContextVariables(); - contextVars.Set("input", prompt); - SKContext context = kernel.CreateNewContext(); - - var theSkills = LoadSkillsFromAssemblyAsync("skills", kernel); - var functionsAvailable = context.Skills.GetFunctionsView(); - - var list = new StringBuilder(); - foreach (KeyValuePair> skill in functionsAvailable.SemanticFunctions) - { - Console.WriteLine($"Skill: {skill.Key}"); - foreach (FunctionView func in skill.Value) - { - // Function description - if (func.Description != null) - { - list.AppendLine($"// {func.Description}"); - } - else - { - Console.WriteLine("{0}.{1} is missing a description", func.SkillName, func.Name); - list.AppendLine($"// Function {func.SkillName}.{func.Name}."); - } - - // Function name - list.AppendLine($"{func.SkillName}.{func.Name}"); - - // Function parameters - foreach (var p in func.Parameters) - { - var description = string.IsNullOrEmpty(p.Description) ? p.Name : p.Description; - var defaultValueString = string.IsNullOrEmpty(p.DefaultValue) ? string.Empty : $" (default value: {p.DefaultValue})"; - list.AppendLine($"Parameter \"{p.Name}\": {description} {defaultValueString}"); - } - } - } - - Console.WriteLine($"List of all skills ----- {list.ToString()}"); //context.Set("wafContext", wafContext); - SKContext answer = await kernel.RunAsync(contextVars, functionName).ConfigureAwait(false); + SKContext answer = await kernel.RunAsync(contextVars, function).ConfigureAwait(false); string result = answer.Result; workflowContext.SetResult(result); } } + /// + /// Load the skills into the kernel + /// + private string ListSkillsInKernel(IKernel kernel) + { + + var theSkills = LoadSkillsFromAssemblyAsync("skills", kernel); + SKContext context = kernel.CreateNewContext(); + var functionsAvailable = context.Skills.GetFunctionsView(); + + var list = new StringBuilder(); + foreach (KeyValuePair> skill in functionsAvailable.SemanticFunctions) + { + Console.WriteLine($"Skill: {skill.Key}"); + foreach (FunctionView func in skill.Value) + { + // Function description + if (func.Description != null) + { + list.AppendLine($"// {func.Description}"); + } + else + { + Console.WriteLine("{0}.{1} is missing a description", func.SkillName, func.Name); + list.AppendLine($"// Function {func.SkillName}.{func.Name}."); + } + + // Function name + list.AppendLine($"{func.SkillName}.{func.Name}"); + + // Function parameters + foreach (var p in func.Parameters) + { + var description = string.IsNullOrEmpty(p.Description) ? p.Name : p.Description; + var defaultValueString = string.IsNullOrEmpty(p.DefaultValue) ? string.Empty : $" (default value: {p.DefaultValue})"; + list.AppendLine($"Parameter \"{p.Name}\": {description} {defaultValueString}"); + } + } + } + + Console.WriteLine($"List of all skills ----- {list.ToString()}"); + return list.ToString(); + } + /// /// Gets a semantic kernel instance /// @@ -187,7 +191,7 @@ public class SemanticKernelSkill : CodeActivity .WithConfiguration(kernelConfig) .Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig { - MaxRetryCount = maxRetries, + MaxRetryCount = KernelSettings.DefaultMaxRetries, UseExponentialBackoff = true, // MinRetryDelay = TimeSpan.FromSeconds(2), // MaxRetryDelay = TimeSpan.FromSeconds(8), diff --git a/Elsa.SemanticKernel/Config/KernelSettings.cs b/Elsa.SemanticKernel/Config/KernelSettings.cs index 6db087552..7e549c9af 100644 --- a/Elsa.SemanticKernel/Config/KernelSettings.cs +++ b/Elsa.SemanticKernel/Config/KernelSettings.cs @@ -9,6 +9,7 @@ internal class KernelSettings public const string DefaultConfigFile = "config/appsettings.json"; public const string OpenAI = "OPENAI"; public const string AzureOpenAI = "AZUREOPENAI"; + public const int DefaultMaxRetries = 9; [JsonPropertyName("serviceType")] public string ServiceType { get; set; } = string.Empty; diff --git a/WorkflowsApp/Program.cs b/WorkflowsApp/Program.cs index 1e83d307e..05c965e0f 100644 --- a/WorkflowsApp/Program.cs +++ b/WorkflowsApp/Program.cs @@ -1,5 +1,6 @@ using Elsa.EntityFrameworkCore.Extensions; using Elsa.EntityFrameworkCore.Modules.Management; +using Elsa.EntityFrameworkCore.Modules.Runtime; using Elsa.Extensions; using Elsa.Workflows.Core.Models; using Elsa.Identity.Features; @@ -12,6 +13,8 @@ builder.Services.AddElsa(elsa => { // Configure management feature to use EF Core. elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite())); + + elsa.UseWorkflowRuntime(runtime =>runtime.UseEntityFrameworkCore()); // Expose API endpoints. elsa.UseWorkflowsApi(); @@ -34,6 +37,9 @@ builder.Services.AddElsa(elsa => }); +// Add dynamic Activity Provider for SK skills. +builder.Services.AddActivityProvider(); + // Add Razor pages. builder.Services.AddRazorPages();