diff --git a/Directory.Build.props b/Directory.Build.props index dcda08f8..1649ba72 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,7 +28,6 @@ true snupkg true - false @@ -36,4 +35,10 @@ $(NoWarn);IL2026;IL2046;IL2057;IL2067;IL2070;IL2072;IL2075;IL2087;IL2091 + + + 3.6.0-preview.4036 + 3.6.0-preview.1297 + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b67d493..bd9a5f9a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,13 +3,6 @@ true false - - 3.7.0-preview.3961 - 3.7.0-preview.1198 - 8.0.22 - 9.0.11 - 10.0.0 - @@ -47,6 +40,7 @@ + @@ -56,6 +50,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -106,28 +168,16 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + @@ -137,10 +187,10 @@ - + @@ -163,9 +213,8 @@ - - + @@ -177,47 +226,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Elsa.Extensions.sln b/Elsa.Extensions.sln index 2e5180f1..08021ec4 100644 --- a/Elsa.Extensions.sln +++ b/Elsa.Extensions.sln @@ -277,6 +277,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "data", "data", "{6D2A4421-A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Data.Csv", "src\modules\data\Elsa.Data.Csv\Elsa.Data.Csv.csproj", "{015646EB-EC33-4ADF-8417-B9D1A6B7CF06}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "agents", "agents", "{60A25F2D-634D-438A-87EA-F204677978BE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -751,6 +753,7 @@ Global {BEB35A25-5A80-4B56-A1EC-F005288CD11C} = {AAD61D93-7C78-42C4-9F37-2564D127A668} {6D2A4421-A388-4BEE-BB11-D0FC32A80A10} = {30CF0330-4B09-4784-B499-46BED303810B} {015646EB-EC33-4ADF-8417-B9D1A6B7CF06} = {6D2A4421-A388-4BEE-BB11-D0FC32A80A10} + {60A25F2D-634D-438A-87EA-F204677978BE} = {3DDE6F89-531C-47F8-9CD7-7A4E6984FA48} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {11A771DA-B728-445E-8A88-AE1C84C3B3A6} diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs new file mode 100644 index 00000000..6915417b --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs @@ -0,0 +1,71 @@ +using System.ComponentModel; +using System.Dynamic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Expressions.Helpers; +using Elsa.Agents.Activities.ActivityProviders; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using static Elsa.Agents.Activities.Extensions.ResponseHelpers; + +namespace Elsa.Agents.Activities; + +/// +/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . +/// +[Browsable(false)] +public class CodeFirstAgentActivity : CodeActivity +{ + private static JsonSerializerOptions? _serializerOptions; + + [JsonIgnore] internal string AgentName { get; set; } = null!; + + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var activityDescriptor = context.ActivityDescriptor; + var inputDescriptors = activityDescriptor.Inputs; + var functionInput = new Dictionary(); + + foreach (var inputDescriptor in inputDescriptors) + { + var input = (Input?)inputDescriptor.ValueGetter(this); + var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; + + if (inputValue is ExpandoObject expandoObject) + inputValue = expandoObject.ConvertTo(); + + functionInput[inputDescriptor.Name] = inputValue; + } + + // Resolve the agent via the unified abstraction. + var agentResolver = context.GetRequiredService(); + var agent = await agentResolver.ResolveAsync(AgentName, cancellationToken); + var agentType = agent.GetType(); + var agentPropertyLookup = agentType.GetProperties().ToDictionary(x => x.Name, x => x); + + // Copy activity input descriptor values into the agent public properties: + foreach (var inputDescriptor in inputDescriptors) + { + var input = (Input?)inputDescriptor.ValueGetter(this); + var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; + agentPropertyLookup[inputDescriptor.Name].SetValue(agent, inputValue); + } + + var agentExecutionContext = new AgentExecutionContext { CancellationToken = context.CancellationToken }; + var agentExecutionResponse = await agent.RunAsync(agentExecutionContext); + var responseText = StripCodeFences(agentExecutionResponse.Text); + var isJsonResponse = IsJsonResponse(responseText); + var outputType = context.ActivityDescriptor.Outputs.Single().Type; + + // If the target type is object and the response is in JSON format, we want it to be deserialized into an ExpandoObject for dynamic field access. + if (outputType == typeof(object) && isJsonResponse) + outputType = typeof(ExpandoObject); + + var outputValue = isJsonResponse ? responseText.ConvertTo(outputType) : responseText; + var outputDescriptor = activityDescriptor.Outputs.Single(); + var output = (Output?)outputDescriptor.ValueGetter(this); + context.Set(output, outputValue, "Output"); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs similarity index 63% rename from src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs rename to src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs index 6fa3213a..49b66dbe 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs @@ -4,21 +4,21 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Unicode; -using Elsa.Agents; using Elsa.Expressions.Helpers; using Elsa.Extensions; using Elsa.Agents.Activities.ActivityProviders; using Elsa.Workflows; using Elsa.Workflows.Models; using Elsa.Workflows.Serialization.Converters; +using static Elsa.Agents.Activities.Extensions.ResponseHelpers; namespace Elsa.Agents.Activities; /// -/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . +/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . /// [Browsable(false)] -public class AgentActivity : CodeActivity +public class ConfiguredAgentActivity : CodeActivity { private static JsonSerializerOptions? _serializerOptions; @@ -44,30 +44,31 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; if (inputValue is ExpandoObject expandoObject) - { inputValue = expandoObject.ConvertTo(); - } functionInput[inputDescriptor.Name] = inputValue; } - var agentInvoker = context.GetRequiredService(); - var result = await agentInvoker.InvokeAgentAsync(AgentName, functionInput, context.CancellationToken); - var json = result.ChatMessageContent.Content?.Trim(); - - if (string.IsNullOrWhiteSpace(json)) - throw new InvalidOperationException("The message content is empty or null."); - + var agentInvoker = context.GetRequiredService(); + var request = new InvokeAgentRequest + { + AgentName = AgentName, + Input = functionInput, + CancellationToken = context.CancellationToken + }; + var agentExecutionResponse = await agentInvoker.InvokeAsync(request); + var responseText = StripCodeFences(agentExecutionResponse.ChatMessageContent.Content!); + var isJsonResponse = IsJsonResponse(responseText); var outputType = context.ActivityDescriptor.Outputs.Single().Type; - // If the target type is object, we want the JSON to be deserialized into an ExpandoObject for dynamic field access. - if (outputType == typeof(object)) + // If the target type is object and the response is in JSON format, we want it to be deserialized into an ExpandoObject for dynamic field access. + if (outputType == typeof(object) && isJsonResponse) outputType = typeof(ExpandoObject); var converterOptions = new ObjectConverterOptions(SerializerOptions); - var outputValue = json.ConvertTo(outputType, converterOptions); + var outputValue = isJsonResponse ? responseText.ConvertTo(outputType, converterOptions) : responseText; var outputDescriptor = activityDescriptor.Outputs.Single(); - var output = (Output)outputDescriptor.ValueGetter(this); - context.Set(output, outputValue); + var output = (Output?)outputDescriptor.ValueGetter(this); + context.Set(output, outputValue, "Output"); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs deleted file mode 100644 index 764d0be6..00000000 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Elsa.Agents; -using Elsa.Expressions.Contracts; -using Elsa.Expressions.Extensions; -using Elsa.Extensions; -using Elsa.Workflows; -using Elsa.Workflows.Models; -using Humanizer; -using JetBrains.Annotations; - -namespace Elsa.Agents.Activities.ActivityProviders; - -/// -/// Provides activities for each function of registered agents. -/// -[UsedImplicitly] -public class AgentActivityProvider( - IKernelConfigProvider kernelConfigProvider, - IActivityDescriber activityDescriber, - IWellKnownTypeRegistry wellKnownTypeRegistry -) : IActivityProvider -{ - /// - public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) - { - var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); - var agents = kernelConfig.Agents; - var activityDescriptors = new List(); - - foreach (var kvp in agents) - { - var agentConfig = kvp.Value; - var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); - var activityTypeName = $"Elsa.Agents.{agentConfig.Name.Pascalize()}"; - activityDescriptor.Name = agentConfig.Name.Pascalize(); - activityDescriptor.TypeName = activityTypeName; - activityDescriptor.Description = agentConfig.Description; - activityDescriptor.DisplayName = agentConfig.Name.Humanize().Transform(To.TitleCase); - activityDescriptor.IsBrowsable = true; - activityDescriptor.Category = "Agents"; - activityDescriptor.Kind = ActivityKind.Task; - activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity); - - activityDescriptor.Constructor = context => - { - var activity = context.CreateActivity(); - activity.Type = activityTypeName; - activity.AgentName = agentConfig.Name; - return activity; - }; - - activityDescriptors.Add(activityDescriptor); - activityDescriptor.Inputs.Clear(); - - foreach (var inputVariable in agentConfig.InputVariables) - { - var inputName = inputVariable.Name; - var inputType = inputVariable.Type == null! ? "object" : inputVariable.Type; - var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType); - var inputDescriptor = new InputDescriptor - { - Name = inputVariable.Name, - DisplayName = inputVariable.Name.Humanize(), - Description = inputVariable.Description, - Type = nakedInputType, - ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName), - ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!, - IsSynthetic = true, - IsWrapped = true, - UIHint = ActivityDescriber.GetUIHint(nakedInputType) - }; - activityDescriptor.Inputs.Add(inputDescriptor); - } - - activityDescriptor.Outputs.Clear(); - var outputVariable = agentConfig.OutputVariable; - var outputType = outputVariable.Type == null! ? "object" : outputVariable.Type; - var nakedOutputType = wellKnownTypeRegistry.GetTypeOrDefault(outputType); - var outputName = "Output"; - var outputDescriptor = new OutputDescriptor - { - Name = outputName, - Description = agentConfig.OutputVariable.Description, - Type = nakedOutputType, - IsSynthetic = true, - ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), - ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, - }; - activityDescriptor.Outputs.Add(outputDescriptor); - } - - return activityDescriptors; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs new file mode 100644 index 00000000..9a8f1704 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs @@ -0,0 +1,127 @@ +using System.ComponentModel; +using System.Reflection; +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Extensions; +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using Humanizer; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; + +namespace Elsa.Agents.Activities.ActivityProviders; + +/// +/// Provides activities for each code-first agent registered via . +/// Inputs are derived from public properties on the agent type using simple +/// reflection rules. Execution is delegated to +/// via the common abstraction. +/// +[UsedImplicitly] +public class CodeFirstAgentActivityProvider( + IOptions agentOptions, + IActivityDescriber activityDescriber, + IWellKnownTypeRegistry wellKnownTypeRegistry) : IActivityProvider +{ + public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var descriptors = new List(); + + foreach (var kvp in agentOptions.Value.AgentTypes) + { + var key = kvp.Key; + var type = kvp.Value; + var descriptor = await CreateDescriptorForAgentAsync(key, type, cancellationToken); + descriptors.Add(descriptor); + } + + return descriptors; + } + + private async Task CreateDescriptorForAgentAsync(string key, Type agentType, CancellationToken cancellationToken) + { + var descriptor = await activityDescriber.DescribeActivityAsync(typeof(CodeFirstAgentActivity), cancellationToken); + var activityTypeName = $"Elsa.Agents.CodeFirst.{key.Pascalize()}"; + + descriptor.Name = key.Pascalize(); + descriptor.TypeName = activityTypeName; + descriptor.DisplayName = key.Humanize().Transform(To.TitleCase); + descriptor.Category = "Agents"; + descriptor.Kind = ActivityKind.Task; + descriptor.RunAsynchronously = true; + descriptor.IsBrowsable = true; + descriptor.ClrType = typeof(CodeFirstAgentActivity); + + descriptor.Constructor = context => + { + var activity = context.CreateActivity(); + activity.Type = activityTypeName; + activity.AgentName = key; + activity.RunAsynchronously = true; + return activity; + }; + + // Build inputs from public instance properties. + descriptor.Inputs.Clear(); + foreach (var prop in agentType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (!IsInputProperty(prop)) + continue; + + var inputName = prop.Name; + var inputType = prop.PropertyType.FullName ?? "object"; + var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType); + var description = prop.GetCustomAttribute()?.Description; + + var inputDescriptor = new InputDescriptor + { + Name = inputName, + DisplayName = inputName.Humanize(), + Description = description, + Type = nakedInputType, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName), + ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!, + IsSynthetic = true, + IsWrapped = true, + UIHint = ActivityDescriber.GetUIHint(nakedInputType) + }; + + descriptor.Inputs.Add(inputDescriptor); + } + + // For now, expose a single synthetic Output of type object, mirroring + // the existing AgentActivity behavior. + descriptor.Outputs.Clear(); + var outputName = "Output"; + var outputDescriptor = new OutputDescriptor + { + Name = outputName, + Description = "The agent's output.", + Type = typeof(object), + IsSynthetic = true, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), + ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, + }; + descriptor.Outputs.Add(outputDescriptor); + + return descriptor; + } + + private static bool IsInputProperty(PropertyInfo prop) + { + // Simple heuristic for now: + // - Must be readable and writable + // - Exclude indexers + if (!prop.CanRead || !prop.CanWrite) + return false; + + if (prop.GetIndexParameters().Length > 0) + return false; + + // In the future, you can add a dedicated [AgentInput] attribute and + // check for it here. For now, treat all simple public properties as + // potential inputs. + return true; + } +} + diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs new file mode 100644 index 00000000..d79b9681 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs @@ -0,0 +1,101 @@ +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Extensions; +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using Humanizer; +using JetBrains.Annotations; + +namespace Elsa.Agents.Activities.ActivityProviders; + +/// +/// Provides activities for each registered agent. +/// +[UsedImplicitly] +public class ConfigurationAgentActivityProvider( + IKernelConfigProvider kernelConfigProvider, + IActivityDescriber activityDescriber, + IWellKnownTypeRegistry wellKnownTypeRegistry +) : IActivityProvider +{ + /// + public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); + var activityDescriptors = new List(); + + // Add descriptors for individual agents + foreach (var kvp in kernelConfig.Agents) + { + var agentConfig = kvp.Value; + var descriptor = await CreateAgentActivityDescriptor(agentConfig, cancellationToken); + activityDescriptors.Add(descriptor); + } + + return activityDescriptors; + } + + private async Task CreateAgentActivityDescriptor(AgentConfig agentConfig, CancellationToken cancellationToken) + { + var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(ConfiguredAgentActivity), cancellationToken); + var activityTypeName = $"Elsa.Agents.{agentConfig.Name.Pascalize()}"; + activityDescriptor.Name = agentConfig.Name.Pascalize(); + activityDescriptor.TypeName = activityTypeName; + activityDescriptor.Description = agentConfig.Description; + activityDescriptor.DisplayName = agentConfig.Name.Humanize().Transform(To.TitleCase); + activityDescriptor.IsBrowsable = true; + activityDescriptor.Category = "Agents"; + activityDescriptor.Kind = ActivityKind.Task; + activityDescriptor.RunAsynchronously = true; + activityDescriptor.ClrType = typeof(ConfiguredAgentActivity); + + activityDescriptor.Constructor = context => + { + var activity = context.CreateActivity(); + activity.Type = activityTypeName; + activity.AgentName = agentConfig.Name; + activity.RunAsynchronously = true; + return activity; + }; + + activityDescriptor.Inputs.Clear(); + + foreach (var inputVariable in agentConfig.InputVariables) + { + var inputName = inputVariable.Name; + var inputType = inputVariable.Type == null! ? "object" : inputVariable.Type; + var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType); + var inputDescriptor = new InputDescriptor + { + Name = inputVariable.Name, + DisplayName = inputVariable.Name.Humanize(), + Description = inputVariable.Description, + Type = nakedInputType, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName), + ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!, + IsSynthetic = true, + IsWrapped = true, + UIHint = ActivityDescriber.GetUIHint(nakedInputType) + }; + activityDescriptor.Inputs.Add(inputDescriptor); + } + + activityDescriptor.Outputs.Clear(); + var outputVariable = agentConfig.OutputVariable; + var outputType = outputVariable.Type == null! ? "object" : outputVariable.Type; + var nakedOutputType = wellKnownTypeRegistry.GetTypeOrDefault(outputType); + var outputName = "Output"; + var outputDescriptor = new OutputDescriptor + { + Name = outputName, + Description = agentConfig.OutputVariable.Description, + Type = nakedOutputType, + IsSynthetic = true, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), + ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, + }; + activityDescriptor.Outputs.Add(outputDescriptor); + + return activityDescriptor; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/Extensions/ResponseHelpers.cs b/src/modules/agents/Elsa.Agents.Activities/Extensions/ResponseHelpers.cs new file mode 100644 index 00000000..19c96765 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Activities/Extensions/ResponseHelpers.cs @@ -0,0 +1,20 @@ +namespace Elsa.Agents.Activities.Extensions; + +public static class ResponseHelpers +{ + public static bool IsJsonResponse(string text) + { + return text.StartsWith("{", StringComparison.OrdinalIgnoreCase) || text.StartsWith("[", StringComparison.OrdinalIgnoreCase); + } + + public static string StripCodeFences(string content) + { + var trimmed = content.Trim(); + + if (!trimmed.StartsWith("```", StringComparison.Ordinal)) + return trimmed; + + var lines = trimmed.Split('\n'); + return lines.Length < 2 ? trimmed : string.Join('\n', lines.Skip(1).Take(lines.Length - 2)).Trim(); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs index 059738c6..046e72c4 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs @@ -14,7 +14,7 @@ namespace Elsa.Agents.Activities.Features; /// A feature that installs Semantic Kernel functionality. /// [DependsOn(typeof(WorkflowManagementFeature))] -[DependsOn(typeof(AgentsFeature))] +[DependsOn(typeof(AgentsCoreFeature))] [UsedImplicitly] public class AgentActivitiesFeature(IModule module) : FeatureBase(module) { @@ -22,7 +22,8 @@ public class AgentActivitiesFeature(IModule module) : FeatureBase(module) public override void Apply() { Services - .AddActivityProvider() + .AddActivityProvider() + .AddActivityProvider() .AddNotificationHandler() ; } diff --git a/src/modules/agents/Elsa.Agents.Activities/Handlers/RefreshActivityRegistry.cs b/src/modules/agents/Elsa.Agents.Activities/Handlers/RefreshActivityRegistry.cs index 6cc0bcf2..8db3830f 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Handlers/RefreshActivityRegistry.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Handlers/RefreshActivityRegistry.cs @@ -7,7 +7,7 @@ namespace Elsa.Agents.Activities.Handlers; [UsedImplicitly] -public class RefreshActivityRegistry(IActivityRegistry activityRegistry, AgentActivityProvider agentActivityProvider) : +public class RefreshActivityRegistry(IActivityRegistry activityRegistry, ConfigurationAgentActivityProvider configurationAgentActivityProvider) : INotificationHandler, INotificationHandler, INotificationHandler, @@ -17,5 +17,5 @@ public class RefreshActivityRegistry(IActivityRegistry activityRegistry, AgentAc public Task HandleAsync(AgentDefinitionUpdated notification, CancellationToken cancellationToken) => RefreshRegistryAsync(cancellationToken); public Task HandleAsync(AgentDefinitionDeleted notification, CancellationToken cancellationToken) => RefreshRegistryAsync(cancellationToken); public Task HandleAsync(AgentDefinitionsDeletedInBulk notification, CancellationToken cancellationToken) => RefreshRegistryAsync(cancellationToken); - private Task RefreshRegistryAsync(CancellationToken cancellationToken) => activityRegistry.RefreshDescriptorsAsync(agentActivityProvider, cancellationToken); + private Task RefreshRegistryAsync(CancellationToken cancellationToken) => activityRegistry.RefreshDescriptorsAsync(configurationAgentActivityProvider, cancellationToken); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs index a14a4d25..4c4e52a3 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Agents.Persistence.Contracts; using Elsa.Agents.Persistence.Filters; using JetBrains.Annotations; @@ -28,6 +27,6 @@ public override async Task ExecuteAsync(BulkDeleteRequest re Ids = ids }; var count = await agentManager.DeleteManyAsync(filter, ct); - return new BulkDeleteResponse(count); + return new(count); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs index 393e813d..4b6f6613 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Extensions; using Elsa.Agents.Persistence.Contracts; using Elsa.Agents.Persistence.Entities; @@ -39,7 +38,7 @@ public override async Task ExecuteAsync(AgentInputModel req, Cancell Id = identityGenerator.GenerateId(), Name = req.Name.Trim(), Description = req.Description.Trim(), - AgentConfig = new AgentConfig + AgentConfig = new() { Description = req.Description.Trim(), Name = req.Name.Trim(), @@ -47,9 +46,7 @@ public override async Task ExecuteAsync(AgentInputModel req, Cancell ExecutionSettings = req.ExecutionSettings, InputVariables = req.InputVariables, OutputVariable = req.OutputVariable, - Services = req.Services, - Plugins = req.Plugins, - FunctionName = req.FunctionName, + Skills = req.Skills, PromptTemplate = req.PromptTemplate } }; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs index 53e4ebf7..74c02b5b 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Agents.Persistence.Contracts; using JetBrains.Annotations; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Get/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Get/Endpoint.cs index 3b70756a..568e0a35 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Get/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Get/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Extensions; using Elsa.Agents.Persistence.Contracts; using JetBrains.Annotations; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs index 9d9f7e19..cbf22c66 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Elsa.Abstractions; -using Elsa.Agents; using JetBrains.Annotations; namespace Elsa.Agents.Api.Endpoints.Agents.Invoke; @@ -9,7 +8,7 @@ namespace Elsa.Agents.Api.Endpoints.Agents.Invoke; /// Invokes an agent. /// [UsedImplicitly] -public class Execute(AgentInvoker agentInvoker) : ElsaEndpoint +public class Execute(IAgentInvoker agentInvoker) : ElsaEndpoint { /// public override void Configure() @@ -21,7 +20,13 @@ public override void Configure() /// public override async Task ExecuteAsync(Request req, CancellationToken ct) { - var result = await agentInvoker.InvokeAgentAsync(req.Agent, req.Inputs, ct).AsJsonElementAsync(); + var request = new InvokeAgentRequest + { + AgentName = req.Agent, + Input = req.Inputs, + CancellationToken = ct + }; + var result = await agentInvoker.InvokeAsync(request).AsJsonElementAsync(); return result; } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs index 4f0a00ac..db5c2e1f 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Agents.Persistence.Contracts; using JetBrains.Annotations; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/List/Endpoint.cs index 19c81b52..effe7ea0 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/List/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/List/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Extensions; using Elsa.Agents.Persistence.Contracts; using Elsa.Models; @@ -25,6 +24,6 @@ public override async Task> ExecuteAsync(CancellationTo { var entities = await agentManager.ListAsync(ct); var models = entities.Select(x => x.ToModel()).ToList(); - return new ListResponse(models); + return new(models); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs index 4d2f9ea1..95d3ebaf 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Extensions; using Elsa.Agents.Persistence.Contracts; using JetBrains.Annotations; @@ -42,17 +41,15 @@ public override async Task ExecuteAsync(AgentInputModel req, Cancell entity.Name = req.Name.Trim(); entity.Description = req.Description.Trim(); - entity.AgentConfig = new AgentConfig + entity.AgentConfig = new() { Name = req.Name.Trim(), Description = req.Description.Trim(), - FunctionName = req.FunctionName.Trim(), - Services = req.Services, PromptTemplate = req.PromptTemplate.Trim(), InputVariables = req.InputVariables, OutputVariable = req.OutputVariable, ExecutionSettings = req.ExecutionSettings, - Plugins = req.Plugins, + Skills = req.Skills, Agents = req.Agents }; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs deleted file mode 100644 index a6704639..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.BulkDelete; - -/// -/// Deletes an API key. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/bulk-actions/api-keys/delete"); - ConfigurePermissions("ai/api-keys:delete"); - } - - /// - public override async Task ExecuteAsync(BulkDeleteRequest req, CancellationToken ct) - { - var ids = req.Ids; - var filter = new ApiKeyDefinitionFilter - { - Ids = ids - }; - var count = await store.DeleteManyAsync(filter, ct); - return new BulkDeleteResponse(count); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs deleted file mode 100644 index 2e5809a5..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using Elsa.Workflows; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Create; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store, IIdentityGenerator identityGenerator) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/api-keys"); - ConfigurePermissions("ai/api-keys:write"); - } - - /// - public override async Task ExecuteAsync(ApiKeyInputModel req, CancellationToken ct) - { - var existingEntityFilter = new ApiKeyDefinitionFilter - { - Name = req.Name - }; - var existingEntity = await store.FindAsync(existingEntityFilter, ct); - - if (existingEntity != null) - { - AddError("An API key already exists with the specified name"); - await Send.ErrorsAsync(cancellation: ct); - return existingEntity.ToModel(); - } - - var newEntity = new ApiKeyDefinition - { - Id = identityGenerator.GenerateId(), - Name = req.Name.Trim(), - Value = req.Value.Trim() - }; - - await store.AddAsync(newEntity, ct); - return newEntity.ToModel(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs deleted file mode 100644 index 793bc120..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents.Persistence.Contracts; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Delete; - -/// -/// Delete an API key. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Delete("/ai/api-keys/{id}"); - ConfigurePermissions("ai/api-keys:delete"); - } - - /// - public override async Task HandleAsync(Request req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return; - } - - await store.DeleteAsync(entity, ct); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs deleted file mode 100644 index c9c25040..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Delete; - -public class Request -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs deleted file mode 100644 index b1da20f1..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Get; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Get("/ai/api-keys/{id}"); - ConfigurePermissions("ai/api-keys:read"); - } - - /// - public override async Task ExecuteAsync(Request req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return null!; - } - - return entity.ToModel(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Request.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Request.cs deleted file mode 100644 index d8bf2d69..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Request.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Get; - -public class Request -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs deleted file mode 100644 index ef434a33..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Models; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.List; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpointWithoutRequest> -{ - /// - public override void Configure() - { - Get("/ai/api-keys"); - ConfigurePermissions("ai/api-keys:read"); - } - - /// - public override async Task> ExecuteAsync(CancellationToken ct) - { - var entities = await store.ListAsync(ct); - var models = entities.Select(x => x.ToModel()).ToList(); - return new ListResponse(models); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs deleted file mode 100644 index 1950c322..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Update; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/api-keys/{id}"); - ConfigurePermissions("ai/api-keys:write"); - } - - /// - public override async Task ExecuteAsync(ApiKeyModel req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return null!; - } - - var isNameDuplicate = await IsNameDuplicateAsync(req.Name, req.Id, ct); - - if (isNameDuplicate) - { - AddError("Another API key already exists with the specified name"); - await Send.ErrorsAsync(cancellation: ct); - return entity; - } - - entity.Name = req.Name.Trim(); - entity.Value = req.Value.Trim(); - - await store.UpdateAsync(entity, ct); - return entity; - } - - private async Task IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken) - { - var filter = new ApiKeyDefinitionFilter - { - Name = name, - NotId = id - }; - - var entity = await store.FindAsync(filter, cancellationToken); - return entity != null; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Plugins/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Plugins/List/Endpoint.cs deleted file mode 100644 index e2483a14..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Plugins/List/Endpoint.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Models; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Plugins.List; - -/// -/// Lists all registered plugins. -/// -[UsedImplicitly] -public class Endpoint(IPluginDiscoverer pluginDiscoverer) : ElsaEndpointWithoutRequest> -{ - /// - public override void Configure() - { - Get("/ai/plugins"); - ConfigurePermissions("ai/plugins:read"); - } - - /// - public override Task> ExecuteAsync(CancellationToken ct) - { - var descriptors = pluginDiscoverer.GetPluginDescriptors(); - var models = descriptors.Select(x => x.ToModel()).ToList(); - return Task.FromResult(new ListResponse(models)); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ServiceProviders/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ServiceProviders/List/Endpoint.cs deleted file mode 100644 index e81f4424..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ServiceProviders/List/Endpoint.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Models; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ServiceProviders.List; - -/// -/// Lists all registered service providers. -/// -[UsedImplicitly] -public class Endpoint(IServiceDiscoverer serviceDiscoverer) : ElsaEndpointWithoutRequest> -{ - /// - public override void Configure() - { - Get("/ai/service-providers"); - ConfigurePermissions("ai/services:read"); - } - - /// - public override Task> ExecuteAsync(CancellationToken ct) - { - var providers = serviceDiscoverer.Discover().Select(x => x.Name).ToList(); - return Task.FromResult(new ListResponse(providers)); - } -} diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs deleted file mode 100644 index 2b742d2e..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.BulkDelete; - -/// -/// Deletes an API key. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/bulk-actions/services/delete"); - ConfigurePermissions("ai/api-keys:delete"); - } - - /// - public override async Task ExecuteAsync(BulkDeleteRequest req, CancellationToken ct) - { - var ids = req.Ids; - var filter = new ServiceDefinitionFilter - { - Ids = ids - }; - var count = await store.DeleteManyAsync(filter, ct); - return new BulkDeleteResponse(count); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Create/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Create/Endpoint.cs deleted file mode 100644 index 1574922f..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Create/Endpoint.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using Elsa.Workflows; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Create; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store, IIdentityGenerator identityGenerator) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/services"); - ConfigurePermissions("ai/services:write"); - } - - /// - public override async Task ExecuteAsync(ServiceInputModel req, CancellationToken ct) - { - var existingEntityFilter = new ServiceDefinitionFilter - { - Name = req.Name - }; - var existingEntity = await store.FindAsync(existingEntityFilter, ct); - - if (existingEntity != null) - { - AddError("A Service already exists with the specified name"); - await Send.ErrorsAsync(cancellation: ct); - return existingEntity.ToModel(); - } - - var newEntity = new ServiceDefinition - { - Id = identityGenerator.GenerateId(), - Name = req.Name.Trim(), - Type = req.Type, - Settings = req.Settings - }; - - await store.AddAsync(newEntity, ct); - return newEntity.ToModel(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Endpoint.cs deleted file mode 100644 index 66f50fcd..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Endpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents.Persistence.Contracts; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Delete; - -/// -/// Delete an API key. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Delete("/ai/services/{id}"); - ConfigurePermissions("ai/services:delete"); - } - - /// - public override async Task HandleAsync(Request req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return; - } - - await store.DeleteAsync(entity, ct); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Request.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Request.cs deleted file mode 100644 index 24698c9c..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Request.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Delete; - -public class Request -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Endpoint.cs deleted file mode 100644 index f7c76a27..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Endpoint.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Get; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Get("/ai/services/{id}"); - ConfigurePermissions("ai/services:read"); - } - - /// - public override async Task ExecuteAsync(Request req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return null!; - } - - return entity.ToModel(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Request.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Request.cs deleted file mode 100644 index 5d7d0470..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Request.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Get; - -public class Request -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/List/Endpoint.cs deleted file mode 100644 index bfe5107b..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/List/Endpoint.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Models; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.List; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpointWithoutRequest> -{ - /// - public override void Configure() - { - Get("/ai/services"); - ConfigurePermissions("ai/services:read"); - } - - /// - public override async Task> ExecuteAsync(CancellationToken ct) - { - var entities = await store.ListAsync(ct); - var models = entities.Select(x => x.ToModel()).ToList(); - return new ListResponse(models); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Update/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Update/Endpoint.cs deleted file mode 100644 index 6684ba75..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Update/Endpoint.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Update; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/services/{id}"); - ConfigurePermissions("ai/services:write"); - } - - /// - public override async Task ExecuteAsync(ServiceModel req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return null!; - } - - var isNameDuplicate = await IsNameDuplicateAsync(req.Name, req.Id, ct); - - if (isNameDuplicate) - { - AddError("Another service already exists with the specified name"); - await Send.ErrorsAsync(cancellation: ct); - return entity.ToModel(); - } - - entity.Name = req.Name.Trim(); - entity.Type = req.Type.Trim(); - entity.Settings = req.Settings; - - await store.UpdateAsync(entity, ct); - return entity.ToModel(); - } - - private async Task IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken) - { - var filter = new ServiceDefinitionFilter - { - Name = name, - NotId = id - }; - - var entity = await store.FindAsync(filter, cancellationToken); - return entity != null; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Skills/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Skills/List/Endpoint.cs new file mode 100644 index 00000000..d1946986 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Skills/List/Endpoint.cs @@ -0,0 +1,27 @@ +using Elsa.Abstractions; +using Elsa.Models; +using JetBrains.Annotations; + +namespace Elsa.Agents.Api.Endpoints.Skills.List; + +/// +/// Lists all registered skills. +/// +[UsedImplicitly] +public class Endpoint(ISkillDiscoverer skillDiscoverer) : ElsaEndpointWithoutRequest> +{ + /// + public override void Configure() + { + Get("/ai/skills"); + ConfigurePermissions("ai/skills:read"); + } + + /// + public override Task> ExecuteAsync(CancellationToken ct) + { + var descriptors = skillDiscoverer.DiscoverSkills(); + var models = descriptors.Select(x => x.ToModel()).ToList(); + return Task.FromResult(new ListResponse(models)); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs b/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs index f631ce23..7232a688 100644 --- a/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs @@ -8,7 +8,7 @@ public static class AgentDefinitionExtensions { public static AgentModel ToModel(this AgentDefinition agentDefinition) { - return new AgentModel + return new() { Id = agentDefinition.Id, Name = agentDefinition.Name, @@ -17,9 +17,7 @@ public static AgentModel ToModel(this AgentDefinition agentDefinition) ExecutionSettings = agentDefinition.AgentConfig.ExecutionSettings, InputVariables = agentDefinition.AgentConfig.InputVariables, OutputVariable = agentDefinition.AgentConfig.OutputVariable, - Services = agentDefinition.AgentConfig.Services, - Plugins = agentDefinition.AgentConfig.Plugins, - FunctionName = agentDefinition.AgentConfig.FunctionName, + Skills = agentDefinition.AgentConfig.Skills, PromptTemplate = agentDefinition.AgentConfig.PromptTemplate }; } diff --git a/src/modules/agents/Elsa.Agents.AzureOpenAI/AgentsFeatureExtensions.cs b/src/modules/agents/Elsa.Agents.AzureOpenAI/AgentsFeatureExtensions.cs new file mode 100644 index 00000000..fbabf207 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.AzureOpenAI/AgentsFeatureExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.SemanticKernel; + +namespace Elsa.Agents.AzureOpenAI; + +public static class AgentsFeatureExtensions +{ + public static AgentsFeature AddAzureOpenAIChatCompletion(this AgentsFeature feature, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + string? apiVersion = null, + HttpClient? httpClient = null, + string name = "Azure OpenAI Chat Completion") + { + return feature.AddServiceDescriptor(new() + { + Name = name, + ConfigureKernel = kernel => kernel.Services.AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey, serviceId, modelId, apiVersion, httpClient) + }); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.AzureOpenAI/Elsa.Agents.AzureOpenAI.csproj b/src/modules/agents/Elsa.Agents.AzureOpenAI/Elsa.Agents.AzureOpenAI.csproj new file mode 100644 index 00000000..9424b0f5 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.AzureOpenAI/Elsa.Agents.AzureOpenAI.csproj @@ -0,0 +1,22 @@ + + + + Provides Azure OpenAI integration with Elsa Agents + elsa extension module agents azure openai + + + + + + + + + + + + + + + + + diff --git a/src/modules/agents/Elsa.Agents.AzureOpenAI/FodyWeavers.xml b/src/modules/agents/Elsa.Agents.AzureOpenAI/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.AzureOpenAI/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Abstractions/PluginProvider.cs b/src/modules/agents/Elsa.Agents.Core/Abstractions/PluginProvider.cs deleted file mode 100644 index c64a1f34..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Abstractions/PluginProvider.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public abstract class PluginProvider : IPluginProvider -{ - public virtual IEnumerable GetPlugins() => []; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Abstractions/SkillsProvider.cs b/src/modules/agents/Elsa.Agents.Core/Abstractions/SkillsProvider.cs new file mode 100644 index 00000000..eb367140 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Abstractions/SkillsProvider.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public abstract class SkillsProvider : ISkillsProvider +{ + public virtual IEnumerable GetSkills() => []; +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs new file mode 100644 index 00000000..b09f573c --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs @@ -0,0 +1,14 @@ +using Microsoft.Agents.AI; + +namespace Elsa.Agents; + +/// +/// Minimal abstraction to represent a code-first agent that can be automatically discovered as an activity. +/// +public interface IAgent +{ + /// + /// Executes the agent with the given context and returns the primary text result. + /// + Task RunAsync(AgentExecutionContext context); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs new file mode 100644 index 00000000..7a470108 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.SemanticKernel.Agents; + +#pragma warning disable SKEXP0001 +#pragma warning disable SKEXP0010 +#pragma warning disable SKEXP0110 + +namespace Elsa.Agents; + +/// +/// Factory for creating agents from agent configurations. +/// +public interface IAgentFactory +{ + /// + /// Creates a ChatCompletionAgent from an Elsa agent configuration. + /// + ChatCompletionAgent CreateAgent(AgentConfig agentConfig); +} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs new file mode 100644 index 00000000..bd9b55a7 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs @@ -0,0 +1,12 @@ +namespace Elsa.Agents; + +/// +/// Invokes an agent using the Microsoft Agent Framework. +/// +public interface IAgentInvoker +{ + /// + /// Invokes an agent using the Microsoft Agent Framework. + /// + Task InvokeAsync(InvokeAgentRequest request); +} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs new file mode 100644 index 00000000..bd27d0fc --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs @@ -0,0 +1,15 @@ +namespace Elsa.Agents; + +/// +/// Defines the contract for resolving agents by their names. +/// +public interface IAgentResolver +{ + /// + /// Resolves an agent instance by its name asynchronously. + /// + /// The name of the agent to resolve. + /// A token to monitor for cancellation requests. + /// The resolved agent instance. + Task ResolveAsync(string agentName, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentServiceProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentServiceProvider.cs deleted file mode 100644 index 1eb96057..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentServiceProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Agents; - -public interface IAgentServiceProvider -{ - string Name { get; } - void ConfigureKernel(KernelBuilderContext context); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginDiscoverer.cs deleted file mode 100644 index 5a9fa460..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginDiscoverer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public interface IPluginDiscoverer -{ - IEnumerable GetPluginDescriptors(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IServiceDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IServiceDiscoverer.cs deleted file mode 100644 index 681e6fd4..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IServiceDiscoverer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public interface IServiceDiscoverer -{ - IEnumerable Discover(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillDiscoverer.cs new file mode 100644 index 00000000..311469ae --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillDiscoverer.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public interface ISkillDiscoverer +{ + IEnumerable DiscoverSkills(); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillsProvider.cs similarity index 56% rename from src/modules/agents/Elsa.Agents.Core/Contracts/IPluginProvider.cs rename to src/modules/agents/Elsa.Agents.Core/Contracts/ISkillsProvider.cs index 0db6dfd7..163cc7e1 100644 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillsProvider.cs @@ -1,9 +1,9 @@ namespace Elsa.Agents; /// -/// Implementations of this interface are responsible for providing plugins. +/// Implementations of this interface are responsible for providing skills. /// -public interface IPluginProvider +public interface ISkillsProvider { - IEnumerable GetPlugins(); + IEnumerable GetSkills(); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IkernelFactory.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IkernelFactory.cs deleted file mode 100644 index 7b14d61d..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IkernelFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.SemanticKernel; -namespace Elsa.Agents; - -public interface IKernelFactory -{ - Kernel CreateKernel(KernelConfig kernelConfig, AgentConfig agentConfig); - Kernel CreateKernel(KernelConfig kernelConfig, string agentName); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs new file mode 100644 index 00000000..d2ae1b91 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs @@ -0,0 +1,32 @@ +using Microsoft.SemanticKernel; + +namespace Elsa.Agents; + +/// +/// Represents a service descriptor used to define and configure services in the context of the Semantic Kernel framework. +/// +/// +/// This class encapsulates the name of the service and an action to configure the kernel builder with the specific service. +/// It is typically used to register and customize services for use within agents or features. +/// +public class ServiceDescriptor +{ + /// + /// Gets or sets the name of the service. + /// + /// + /// The name is used to identify the service descriptor and can be helpful for distinguishing + /// between different services when configuring or resolving dependencies within the Semantic Kernel framework. + /// + public string Name { get; set; } = null!; + + /// + /// Gets or sets the action used to configure the kernel builder for a specific service. + /// + /// + /// This property defines an used to customize the Semantic Kernel's configuration + /// by adding or modifying services within the kernel. It provides a mechanism to integrate and set up specific + /// functionality in the context of agents or features. + /// + public Action ConfigureKernel { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj index df449170..e0300c3e 100644 --- a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj +++ b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj @@ -7,26 +7,28 @@ - - - - - - - - + + + + + + + + + + + + - - - - - + + - - + + + diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs deleted file mode 100644 index 5afbbab3..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -#pragma warning disable SKEXP0001 -#pragma warning disable SKEXP0010 - -namespace Elsa.Agents; - -public static class AgentConfigExtensions -{ - public static OpenAIPromptExecutionSettings ToOpenAIPromptExecutionSettings(this AgentConfig agentConfig) - { - return new OpenAIPromptExecutionSettings - { - Temperature = agentConfig.ExecutionSettings.Temperature, - TopP = agentConfig.ExecutionSettings.TopP, - MaxTokens = agentConfig.ExecutionSettings.MaxTokens, - PresencePenalty = agentConfig.ExecutionSettings.PresencePenalty, - FrequencyPenalty = agentConfig.ExecutionSettings.FrequencyPenalty, - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, - ResponseFormat = agentConfig.ExecutionSettings.ResponseFormat, - ChatSystemPrompt = agentConfig.PromptTemplate, - ServiceId = "default" - }; - } - - public static PromptTemplateConfig ToPromptTemplateConfig(this AgentConfig agentConfig) - { - var promptExecutionSettingsDictionary = new Dictionary - { - [PromptExecutionSettings.DefaultServiceId] = agentConfig.ToOpenAIPromptExecutionSettings(), - }; - - return new PromptTemplateConfig - { - Name = agentConfig.FunctionName, - Description = agentConfig.Description, - Template = agentConfig.PromptTemplate, - ExecutionSettings = promptExecutionSettingsDictionary, - AllowDangerouslySetContent = true, - InputVariables = agentConfig.InputVariables.Select(x => new InputVariable - { - Name = x.Name, - Description = x.Description, - IsRequired = true, - AllowDangerouslySetContent = true - }).ToList() - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/ModuleExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/ModuleExtensions.cs index bf521d0f..f7464583 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/ModuleExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/ModuleExtensions.cs @@ -12,7 +12,7 @@ public static class ModuleExtensions /// /// Installs the Semantic Kernel API feature. /// - public static IModule UseAgents(this IModule module, Action? configure = null) + public static IModule UseAgentsCore(this IModule module, Action? configure = null) { return module.Use(configure); } diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs index b33fed1c..baaf3bbf 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs @@ -4,13 +4,8 @@ namespace Elsa.Agents; public static class ServiceCollectionExtensions { - public static IServiceCollection AddPluginProvider(this IServiceCollection services) where T: class, IPluginProvider + public static IServiceCollection AddSkillsProvider(this IServiceCollection services) where T: class, ISkillsProvider { - return services.AddScoped(); - } - - public static IServiceCollection AddAgentServiceProvider(this IServiceCollection services) where T: class, IAgentServiceProvider - { - return services.AddScoped(); + return services.AddScoped(); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs new file mode 100644 index 00000000..2237018a --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs @@ -0,0 +1,39 @@ +using Elsa.Agents.Skills; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Agents.Features; + +/// +/// A feature that installs API endpoints to interact with skilled agents. +/// +[UsedImplicitly] +public class AgentsCoreFeature(IModule module) : FeatureBase(module) +{ + private Func _kernelConfigProviderFactory = sp => sp.GetRequiredService(); + + public AgentsCoreFeature UseKernelConfigProvider(Func factory) + { + _kernelConfigProviderFactory = factory; + return this; + } + + /// + public override void Apply() + { + Services.AddOptions(); + + Services + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped(_kernelConfigProviderFactory) + .AddScoped() + .AddScoped() + .AddSkillsProvider() + .AddSkillsProvider() + ; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs deleted file mode 100644 index 77c16f83..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Elsa.Agents.Plugins; -using Elsa.Features.Abstractions; -using Elsa.Features.Services; -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; - -namespace Elsa.Agents.Features; - -/// -/// A feature that installs API endpoints to interact with skilled agents. -/// -[UsedImplicitly] -public class AgentsFeature(IModule module) : FeatureBase(module) -{ - private Func _kernelConfigProviderFactory = sp => sp.GetRequiredService(); - - public AgentsFeature UseKernelConfigProvider(Func factory) - { - _kernelConfigProviderFactory = factory; - return this; - } - - /// - public override void Apply() - { - Services.AddOptions(); - - Services - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped(_kernelConfigProviderFactory) - .AddScoped() - .AddPluginProvider() - .AddPluginProvider() - .AddAgentServiceProvider() - .AddAgentServiceProvider() - .AddAgentServiceProvider() - ; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs new file mode 100644 index 00000000..861604e9 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs @@ -0,0 +1,7 @@ +namespace Elsa.Agents; + +public class AgentExecutionContext +{ + public string Message { get; set; } = null!; + public CancellationToken CancellationToken { get; set; } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/InvokeAgentRequest.cs b/src/modules/agents/Elsa.Agents.Core/Models/InvokeAgentRequest.cs new file mode 100644 index 00000000..2cff2b57 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Models/InvokeAgentRequest.cs @@ -0,0 +1,29 @@ +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Elsa.Agents; + +/// +/// Represents a request to invoke an agent. +/// +public class InvokeAgentRequest +{ + /// + /// Gets or sets the name of the agent to invoke. + /// + public required string AgentName { get; set; } + + /// + /// Gets or sets the input parameters for the agent. + /// + public IDictionary Input { get; set; } = new Dictionary(); + + /// + /// Gets or sets the chat history. If null, a new chat history will be created. + /// + public ChatHistory? ChatHistory { get; set; } + + /// + /// Gets or sets the cancellation token. + /// + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; +} diff --git a/src/modules/agents/Elsa.Agents.Core/Models/KernelBuilderContext.cs b/src/modules/agents/Elsa.Agents.Core/Models/KernelBuilderContext.cs deleted file mode 100644 index 8fe3dc15..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Models/KernelBuilderContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JetBrains.Annotations; -using Microsoft.SemanticKernel; - -namespace Elsa.Agents; - -[UsedImplicitly] -public record KernelBuilderContext(IKernelBuilder KernelBuilder, KernelConfig KernelConfig, ServiceConfig ServiceConfig) -{ - public string GetApiKey() - { - var settings = ServiceConfig.Settings; - if (settings.TryGetValue("ApiKey", out var apiKey)) - return (string)apiKey; - - if (settings.TryGetValue("ApiKeyRef", out var apiKeyRef)) - return KernelConfig.ApiKeys[(string)apiKeyRef].Value; - - throw new KeyNotFoundException($"No api key found for service {ServiceConfig.Type}"); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs b/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs deleted file mode 100644 index 31b0c8c7..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.ComponentModel; -using System.Reflection; - -namespace Elsa.Agents; - -/// -/// A descriptor for a plugin. -/// -public class PluginDescriptor -{ - public string Name { get; set; } - public string Description { get; set; } - public Type PluginType { get; set; } - - public PluginDescriptorModel ToModel() => new() - { - Name = Name, - Description = Description, - PluginType = PluginType.AssemblyQualifiedName! - }; - - public static PluginDescriptor From(string? name = null) - { - var pluginType = typeof(TPlugin); - var description = pluginType.GetCustomAttribute()?.Description ?? string.Empty; - return new PluginDescriptor - { - Name = name ?? pluginType.Name.Replace("Plugin", ""), - Description = description, - PluginType = pluginType - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/SkillDescriptor.cs b/src/modules/agents/Elsa.Agents.Core/Models/SkillDescriptor.cs new file mode 100644 index 00000000..97c54808 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Models/SkillDescriptor.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using System.Reflection; + +namespace Elsa.Agents; + +/// +/// A descriptor for a skill. +/// +public class SkillDescriptor +{ + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public Type ClrType { get; set; } = null!; + + public SkillDescriptorModel ToModel() => new() + { + Name = Name, + Description = Description, + ClrTypeName = ClrType.AssemblyQualifiedName! + }; + + public static SkillDescriptor From(string? name = null) + { + var clrType = typeof(TSkill); + var description = clrType.GetCustomAttribute()?.Description ?? string.Empty; + return new() + { + Name = name ?? clrType.Name.Replace("Skill", ""), + Description = description, + ClrType = clrType + }; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Options/AgentOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/AgentOptions.cs new file mode 100644 index 00000000..d9796968 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Options/AgentOptions.cs @@ -0,0 +1,23 @@ +namespace Elsa.Agents; + +public class AgentOptions +{ + public ICollection Agents { get; set; } = new List(); + public ICollection ServiceDescriptors { get; set; } = new List(); + + /// + /// Map from agent key to the implementing type. Keys are case-insensitive. + /// + public IDictionary AgentTypes { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Registers a code-first agent type. If no key is provided, the type name + /// is used as the key. + /// + public AgentOptions AddAgentType(string? key = null) where TAgent : class, IAgent + { + key ??= typeof(TAgent).Name; + AgentTypes[key] = typeof(TAgent); + return this; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs deleted file mode 100644 index a92862a6..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Elsa.Agents; - -public class AgentsOptions -{ - public ICollection ApiKeys { get; set; } - public ICollection Services { get; set; } - public ICollection Agents { get; set; } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs b/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs deleted file mode 100644 index 771582f3..00000000 --- a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.SemanticKernel; - -namespace Elsa.Agents; - -public class OpenAIChatCompletionProvider : IAgentServiceProvider -{ - public string Name => "OpenAIChatCompletion"; - - public void ConfigureKernel(KernelBuilderContext context) - { - var modelId = (string)context.ServiceConfig.Settings["ModelId"]; - var apiKey = context.GetApiKey(); - context.KernelBuilder.AddOpenAIChatCompletion(modelId, apiKey); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIEmbeddingGenerator.cs b/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIEmbeddingGenerator.cs deleted file mode 100644 index accfde30..00000000 --- a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIEmbeddingGenerator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Elsa.Extensions; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.InMemory; -using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Elsa.Agents; - -public class OpenAIEmbeddingGenerator : IAgentServiceProvider -{ - public string Name => "OpenAIEmbeddingGenerator"; - - [Experimental("SKEXP0010")] - public void ConfigureKernel(KernelBuilderContext context) - { - var modelId = (string)context.ServiceConfig.Settings["ModelId"]; - var apiKey = context.GetApiKey(); - - context.KernelBuilder.Services.AddInMemoryVectorStore(); - context.KernelBuilder.AddOpenAIEmbeddingGenerator(modelId, apiKey); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs b/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs deleted file mode 100644 index b20aba85..00000000 --- a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.SemanticKernel; -#pragma warning disable SKEXP0010 - -namespace Elsa.Agents; - -public class OpenAITextToImageProvider : IAgentServiceProvider -{ - public string Name => "OpenAITextToImage"; - - public void ConfigureKernel(KernelBuilderContext context) - { - var modelId = (string)context.ServiceConfig.Settings["ModelId"]; - var apiKey = context.GetApiKey(); - context.KernelBuilder.AddOpenAITextToImage(apiKey, modelId: modelId); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs new file mode 100644 index 00000000..f6d718af --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; + +#pragma warning disable SKEXP0001 +#pragma warning disable SKEXP0010 +#pragma warning disable SKEXP0110 + +namespace Elsa.Agents; + +/// +public class AgentFactory( + ISkillDiscoverer skillDiscoverer, + IServiceProvider serviceProvider, + IOptions options, + ILogger logger) : IAgentFactory +{ + /// + public ChatCompletionAgent CreateAgent(AgentConfig agentConfig) + { + var kernel = CreateKernel(agentConfig); + + return new() + { + Name = agentConfig.Name, + Description = agentConfig.Description, + Instructions = agentConfig.PromptTemplate, + Kernel = kernel + }; + } + + /// + /// Creates a Kernel configured for the specified agent. + /// + private Kernel CreateKernel(AgentConfig agentConfig) + { + var builder = Kernel.CreateBuilder(); + builder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Trace)); + builder.Services.AddSingleton(agentConfig); + + ApplyAgentConfig(builder, agentConfig); + ApplyServiceDescriptors(builder, options.Value.ServiceDescriptors); + + return builder.Build(); + } + + private void ApplyServiceDescriptors(IKernelBuilder builder, ICollection serviceDescriptors) + { + foreach (var descriptor in serviceDescriptors) + descriptor.ConfigureKernel(builder); + } + + private void ApplyAgentConfig(IKernelBuilder builder, AgentConfig agentConfig) + { + AddSkills(builder, agentConfig); + } + + private void AddSkills(IKernelBuilder builder, AgentConfig agent) + { + var skills = skillDiscoverer.DiscoverSkills().ToDictionary(x => x.Name); + foreach (var skillName in agent.Skills) + { + if (!skills.TryGetValue(skillName, out var skillDescriptor)) + { + logger.LogWarning($"Skill {skillName} not found"); + continue; + } + + var clrType = skillDescriptor.ClrType; + var skillInstance = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, clrType); + builder.Plugins.AddFromObject(skillInstance, skillName); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs index 907caae4..781475ce 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs @@ -5,16 +5,37 @@ #pragma warning disable SKEXP0010 #pragma warning disable SKEXP0001 +#pragma warning disable SKEXP0110 namespace Elsa.Agents; -public class AgentInvoker(IKernelFactory kernelFactory, IKernelConfigProvider kernelConfigProvider) +public class AgentInvoker(IKernelConfigProvider kernelConfigProvider, IAgentFactory agentFactory) : IAgentInvoker { - public async Task InvokeAgentAsync(string agentName, IDictionary input, CancellationToken cancellationToken = default) + /// + /// Invokes an agent using the Microsoft Agent Framework (new approach). + /// + public async Task InvokeAsync(InvokeAgentRequest request) { - var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); - var kernel = kernelFactory.CreateKernel(kernelConfig, agentName); - var agentConfig = kernelConfig.Agents[agentName]; + var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(request.CancellationToken); + var agentConfig = kernelConfig.Agents[request.AgentName]; + + // Create agent using Agent Framework + var agent = agentFactory.CreateAgent(agentConfig); + + // Use provided chat history or create new one + ChatHistory chatHistory = request.ChatHistory ?? []; + + // Format and add user input + var promptTemplateConfig = new PromptTemplateConfig + { + Template = agentConfig.PromptTemplate, + TemplateFormat = "handlebars", + Name = "Run", + AllowDangerouslySetContent = true, + }; + + var templateFactory = new HandlebarsPromptTemplateFactory(); + var promptTemplate = templateFactory.Create(promptTemplateConfig); var executionSettings = agentConfig.ExecutionSettings; var promptExecutionSettings = new OpenAIPromptExecutionSettings { @@ -33,50 +54,33 @@ public async Task InvokeAgentAsync(string agentName, IDiction [PromptExecutionSettings.DefaultServiceId] = promptExecutionSettings, }; - var promptTemplateConfig = new PromptTemplateConfig - { - Name = agentConfig.FunctionName, - Description = agentConfig.Description, - Template = agentConfig.PromptTemplate, - ExecutionSettings = promptExecutionSettingsDictionary, - AllowDangerouslySetContent = true, - InputVariables = agentConfig.InputVariables.Select(x => new InputVariable - { - Name = x.Name, - Description = x.Description, - IsRequired = true, - AllowDangerouslySetContent = true - }).ToList() - }; + var kernelArguments = new KernelArguments(request.Input, promptExecutionSettingsDictionary); + var renderedPrompt = await promptTemplate.RenderAsync(agent.Kernel, kernelArguments, request.CancellationToken); - var templateFactory = new HandlebarsPromptTemplateFactory(); + chatHistory.AddUserMessage(renderedPrompt); - var promptConfig = new PromptTemplateConfig + if (executionSettings.ResponseFormat == "json_object") { - Template = agentConfig.PromptTemplate, - TemplateFormat = "handlebars", - Name = agentConfig.FunctionName - }; + chatHistory.AddSystemMessage( + """" + You are a function that returns *only* JSON. - var promptTemplate = templateFactory.Create(promptConfig); + Rules: + - Return a single valid JSON object. + - Do not add explanations. + - Do not add code fences. + - Do not prefix with ```json or any other markers. + - Output must start with { and end with }. + + If there's a problem with the JSON input, include the exact JSON input in your response for troubleshooting. + """"); + } - var kernelArguments = new KernelArguments(input); - string renderedPrompt = await promptTemplate.RenderAsync(kernel, kernelArguments); - - ChatHistory chatHistory = []; - chatHistory.AddUserMessage(renderedPrompt); - - IChatCompletionService chatCompletion = kernel.GetRequiredService(); - - OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() - { - FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() - }; + // Get response from agent + var response = await agent.InvokeAsync(chatHistory, cancellationToken: request.CancellationToken).LastOrDefaultAsync(request.CancellationToken); - var response = await chatCompletion.GetChatMessageContentAsync( - chatHistory, - executionSettings: openAIPromptExecutionSettings, - kernel: kernel); + if (response == null) + throw new InvalidOperationException("Agent did not produce a response"); return new(agentConfig, response); } diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs new file mode 100644 index 00000000..db53d9a6 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Elsa.Agents; + +public class AgentResolver(IServiceProvider serviceProvider, IOptions options) : IAgentResolver +{ + public Task ResolveAsync(string agentName, CancellationToken cancellationToken = default) + { + var agentType = options.Value.AgentTypes[agentName] ?? throw new InvalidOperationException($"No agent with name '{agentName}' was found."); + var agent = (IAgent)ActivatorUtilities.CreateInstance(serviceProvider, agentType)!; + return Task.FromResult(agent); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs index e4320df2..c2bb6947 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs @@ -3,15 +3,22 @@ namespace Elsa.Agents; +/// +/// Provides kernel configuration from configuration. +/// [UsedImplicitly] -public class ConfigurationKernelConfigProvider(IOptions options) : IKernelConfigProvider +public class ConfigurationKernelConfigProvider(IOptions options) : IKernelConfigProvider { public Task GetKernelConfigAsync(CancellationToken cancellationToken = default) { var kernelConfig = new KernelConfig(); - foreach (var apiKey in options.Value.ApiKeys) kernelConfig.ApiKeys[apiKey.Name] = apiKey; - foreach (var service in options.Value.Services) kernelConfig.Services[service.Name] = service; - foreach (var agent in options.Value.Agents) kernelConfig.Agents[agent.Name] = agent; + + if (options.Value.Agents != null!) + { + foreach (var agent in options.Value.Agents) + kernelConfig.Agents[agent.Name] = agent; + } + return Task.FromResult(kernelConfig); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/KernelFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/KernelFactory.cs deleted file mode 100644 index 9b8dfaf8..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/KernelFactory.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; - -#pragma warning disable SKEXP0001 -#pragma warning disable SKEXP0010 - -namespace Elsa.Agents; - -public class KernelFactory(IPluginDiscoverer pluginDiscoverer, IServiceDiscoverer serviceDiscoverer, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, ILogger logger) : IKernelFactory -{ - public Kernel CreateKernel(KernelConfig kernelConfig, string agentName) - { - var agent = kernelConfig.Agents[agentName]; - return CreateKernel(kernelConfig, agent); - } - - public Kernel CreateKernel(KernelConfig kernelConfig, AgentConfig agentConfig) - { - var builder = Kernel.CreateBuilder(); - builder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Trace)); - builder.Services.AddSingleton(agentConfig); - - ApplyAgentConfig(builder, kernelConfig, agentConfig); - - return builder.Build(); - } - - private void ApplyAgentConfig(IKernelBuilder builder, KernelConfig kernelConfig, AgentConfig agentConfig) - { - var services = serviceDiscoverer.Discover().ToDictionary(x => x.Name); - - foreach (string serviceName in agentConfig.Services) - { - if (!kernelConfig.Services.TryGetValue(serviceName, out var serviceConfig)) - { - logger.LogWarning($"Service {serviceName} not found"); - continue; - } - - AddService(builder, kernelConfig, serviceConfig, services); - } - - AddPlugins(builder, agentConfig); - AddAgents(builder, kernelConfig, agentConfig); - } - - private void AddService(IKernelBuilder builder, KernelConfig kernelConfig, ServiceConfig serviceConfig, Dictionary services) - { - if (!services.TryGetValue(serviceConfig.Type, out var serviceProvider)) - { - logger.LogWarning($"Service provider {serviceConfig.Type} not found"); - return; - } - - var context = new KernelBuilderContext(builder, kernelConfig, serviceConfig); - serviceProvider.ConfigureKernel(context); - } - - private void AddPlugins(IKernelBuilder builder, AgentConfig agent) - { - var plugins = pluginDiscoverer.GetPluginDescriptors().ToDictionary(x => x.Name); - foreach (var pluginName in agent.Plugins) - { - if (!plugins.TryGetValue(pluginName, out var pluginDescriptor)) - { - logger.LogWarning($"Plugin {pluginName} not found"); - continue; - } - - var pluginType = pluginDescriptor.PluginType; - var pluginInstance = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, pluginType); - builder.Plugins.AddFromObject(pluginInstance, pluginName); - } - } - - private void AddAgents(IKernelBuilder builder, KernelConfig kernelConfig, AgentConfig agent) - { - foreach (var agentName in agent.Agents) - { - if (!kernelConfig.Agents.TryGetValue(agentName, out var subAgent)) - { - logger.LogWarning($"Agent {agentName} not found"); - continue; - } - - var promptExecutionSettings = subAgent.ToOpenAIPromptExecutionSettings(); - var promptExecutionSettingsDictionary = new Dictionary - { - [PromptExecutionSettings.DefaultServiceId] = promptExecutionSettings, - }; - var promptTemplateConfig = new PromptTemplateConfig - { - Name = subAgent.FunctionName, - Description = subAgent.Description, - Template = subAgent.PromptTemplate, - ExecutionSettings = promptExecutionSettingsDictionary, - AllowDangerouslySetContent = true, - InputVariables = subAgent.InputVariables.Select(x => new InputVariable - { - Name = x.Name, - Description = x.Description, - IsRequired = true, - AllowDangerouslySetContent = true - }).ToList() - }; - - var subAgentFunction = KernelFunctionFactory.CreateFromPrompt(promptTemplateConfig, loggerFactory: loggerFactory); - var agentPlugin = KernelPluginFactory.CreateFromFunctions(subAgent.Name, subAgent.Description, [subAgentFunction]); - builder.Plugins.Add(agentPlugin); - } - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/PluginDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Services/PluginDiscoverer.cs deleted file mode 100644 index fa645195..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/PluginDiscoverer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Elsa.Agents; - -public class PluginDiscoverer(IEnumerable providers) : IPluginDiscoverer -{ - public IEnumerable GetPluginDescriptors() - { - return providers.SelectMany(x => x.GetPlugins()); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ServiceDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Services/ServiceDiscoverer.cs deleted file mode 100644 index 46f18c6e..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/ServiceDiscoverer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Elsa.Agents; - -public class ServiceDiscoverer(IEnumerable providers) : IServiceDiscoverer -{ - public IEnumerable Discover() - { - return providers; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/SkillDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Services/SkillDiscoverer.cs new file mode 100644 index 00000000..88ff8b73 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/SkillDiscoverer.cs @@ -0,0 +1,9 @@ +namespace Elsa.Agents; + +public class SkillDiscoverer(IEnumerable providers) : ISkillDiscoverer +{ + public IEnumerable DiscoverSkills() + { + return providers.SelectMany(x => x.GetSkills()); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Plugins/DocumentQueryPlugin.cs b/src/modules/agents/Elsa.Agents.Core/Skills/DocumentQuerySkill.cs similarity index 88% rename from src/modules/agents/Elsa.Agents.Core/Plugins/DocumentQueryPlugin.cs rename to src/modules/agents/Elsa.Agents.Core/Skills/DocumentQuerySkill.cs index 811ecf38..fb2c8348 100644 --- a/src/modules/agents/Elsa.Agents.Core/Plugins/DocumentQueryPlugin.cs +++ b/src/modules/agents/Elsa.Agents.Core/Skills/DocumentQuerySkill.cs @@ -6,9 +6,9 @@ using Microsoft.SemanticKernel.Connectors.InMemory; using Microsoft.SemanticKernel.Data; -namespace Elsa.Agents.Plugins; +namespace Elsa.Agents.Skills; -public class DocumentQueryPlugin +public class DocumentQuerySkill { [Experimental("SKEXP0001")] [KernelFunction("query_document")] @@ -39,11 +39,11 @@ public async Task QueryDocumentAsync( } } -public class DocumentQueryPluginProvider : PluginProvider +public class DocumentQuerySkillsProvider : SkillsProvider { - public override IEnumerable GetPlugins() + public override IEnumerable GetSkills() { - yield return PluginDescriptor.From(); + yield return SkillDescriptor.From(); } } diff --git a/src/modules/agents/Elsa.Agents.Core/Plugins/ImageGeneratorPlugin.cs b/src/modules/agents/Elsa.Agents.Core/Skills/ImageGeneratorSkill.cs similarity index 80% rename from src/modules/agents/Elsa.Agents.Core/Plugins/ImageGeneratorPlugin.cs rename to src/modules/agents/Elsa.Agents.Core/Skills/ImageGeneratorSkill.cs index a4d62975..8689ca2f 100644 --- a/src/modules/agents/Elsa.Agents.Core/Plugins/ImageGeneratorPlugin.cs +++ b/src/modules/agents/Elsa.Agents.Core/Skills/ImageGeneratorSkill.cs @@ -5,11 +5,11 @@ #pragma warning disable SKEXP0001 -namespace Elsa.Agents.Plugins; +namespace Elsa.Agents.Skills; [Description("Generates an image from text")] [UsedImplicitly] -public class ImageGeneratorPlugin +public class ImageGeneratorSkill { [KernelFunction("generate_image_from_text")] [Description("Generates an image from text")] @@ -30,10 +30,10 @@ public async Task GenerateImage( } [UsedImplicitly] -public class ImageGeneratorPluginProvider : PluginProvider +public class ImageGeneratorSkillsProvider : SkillsProvider { - public override IEnumerable GetPlugins() + public override IEnumerable GetSkills() { - yield return PluginDescriptor.From(); + yield return SkillDescriptor.From(); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs b/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs index b5c003ed..2208dc63 100644 --- a/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs +++ b/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs @@ -6,12 +6,10 @@ public class AgentInputModel { [Required] public string Name { get; set; } = ""; [Required] public string Description { get; set; } = ""; - public ICollection Services { get; set; } = []; - [Required] public string FunctionName { get; set; } = ""; [Required] public string PromptTemplate { get; set; } = ""; public ICollection InputVariables { get; set; } = []; [Required] public OutputVariableConfig OutputVariable { get; set; } = new(); public ExecutionSettingsConfig ExecutionSettings { get; set; } = new(); - public ICollection Plugins { get; set; } = []; + public ICollection Skills { get; set; } = []; public ICollection Agents { get; set; } = []; } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs index 6257ae12..9dc3c6a3 100644 --- a/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs +++ b/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs @@ -4,13 +4,11 @@ public class AgentConfig { public string Name { get; set; } = ""; public string Description { get; set; } = ""; - public ICollection Services { get; set; } = []; - public string FunctionName { get; set; } = null!; public string PromptTemplate { get; set; } = null!; public ICollection InputVariables { get; set; } = []; public OutputVariableConfig OutputVariable { get; set; } = new(); public ExecutionSettingsConfig ExecutionSettings { get; set; } = new(); - public ICollection Plugins { get; set; } = []; + public ICollection Skills { get; set; } = []; public ICollection Agents { get; set; } = []; diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/ApiKeyConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/ApiKeyConfig.cs deleted file mode 100644 index 88b4e733..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/ApiKeyConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -using JetBrains.Annotations; - -namespace Elsa.Agents; - -[UsedImplicitly] -public class ApiKeyConfig -{ - public string Name { get; set; } - public string Value { get; set; } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/FunctionConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/FunctionConfig.cs deleted file mode 100644 index a4b3d05b..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/FunctionConfig.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Agents; - -public class FunctionConfig -{ - - -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs index 75c28527..427325c0 100644 --- a/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs +++ b/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs @@ -2,7 +2,5 @@ namespace Elsa.Agents; public class KernelConfig { - public IDictionary ApiKeys { get; set; } = new Dictionary(); - public IDictionary Services { get; } = new Dictionary(); public IDictionary Agents { get; } = new Dictionary(); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/ServiceConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/ServiceConfig.cs deleted file mode 100644 index 4ad83b92..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/ServiceConfig.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Elsa.Agents; - -public class ServiceConfig -{ - public string Name { get; set; } - public string Type { get; set; } - public IDictionary Settings { get; set; } = new Dictionary(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Plugins/PluginDescriptor.cs b/src/modules/agents/Elsa.Agents.Models/Plugins/PluginDescriptor.cs deleted file mode 100644 index c8325fdf..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Plugins/PluginDescriptor.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Elsa.Agents; - -/// -/// A descriptor for a plugin. -/// -public class PluginDescriptorModel -{ - public string Name { get; set; } - public string Description { get; set; } - public string PluginType { get; set; } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Plugins/SkillDescriptorModel.cs b/src/modules/agents/Elsa.Agents.Models/Plugins/SkillDescriptorModel.cs new file mode 100644 index 00000000..1a4f7e43 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Models/Plugins/SkillDescriptorModel.cs @@ -0,0 +1,11 @@ +namespace Elsa.Agents; + +/// +/// A descriptor of a skill. +/// +public class SkillDescriptorModel +{ + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public string ClrTypeName { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Services/ServiceInputModel.cs b/src/modules/agents/Elsa.Agents.Models/Services/ServiceInputModel.cs deleted file mode 100644 index 2ffa016a..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Services/ServiceInputModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents; - -public class ServiceInputModel -{ - [Required] public string Name { get; set; } = null!; - [Required] public string Type { get; set; } = null!; - public IDictionary Settings { get; set; } = new Dictionary(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Services/ServiceModel.cs b/src/modules/agents/Elsa.Agents.Models/Services/ServiceModel.cs deleted file mode 100644 index 845c835f..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Services/ServiceModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents; - -public class ServiceModel : ServiceInputModel -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.OpenAI/AgentsFeatureExtensions.cs b/src/modules/agents/Elsa.Agents.OpenAI/AgentsFeatureExtensions.cs new file mode 100644 index 00000000..a82896f0 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.OpenAI/AgentsFeatureExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.SemanticKernel; + +namespace Elsa.Agents.OpenAI; + +public static class AgentsFeatureExtensions +{ + public static AgentsFeature AddOpenAIChatCompletion(this AgentsFeature feature, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + string name = "OpenAI Chat Completion") + { + return feature.AddServiceDescriptor(new() + { + Name = name, + ConfigureKernel = kernel => kernel.Services.AddOpenAIChatCompletion(modelId, apiKey, orgId, serviceId) + }); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj b/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj new file mode 100644 index 00000000..6ab1a67d --- /dev/null +++ b/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj @@ -0,0 +1,22 @@ + + + + Provides OpenAI integration with Elsa Agents + elsa extension module agents openai + + + + + + + + + + + + + + + + + diff --git a/src/modules/agents/Elsa.Agents.OpenAI/FodyWeavers.xml b/src/modules/agents/Elsa.Agents.OpenAI/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.OpenAI/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.Designer.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.Designer.cs new file mode 100644 index 00000000..8b14cbee --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.Designer.cs @@ -0,0 +1,61 @@ +// +using Elsa.Agents.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.MySql.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20251213194317_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AgentConfig") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_AgentDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_AgentDefinition_TenantId"); + + b.ToTable("AgentDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.cs new file mode 100644 index 00000000..454b9936 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.cs @@ -0,0 +1,100 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.MySql.Migrations +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Value = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeysDefinitions", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ServicesDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Settings = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Type = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_Name", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_TenantId", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_Name", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_TenantId", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "TenantId"); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs index 3f3f3b03..a38076e9 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -52,64 +52,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentDefinitions", "Elsa"); }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ApiKeyDefinition", b => - { - b.Property("Id") - .HasColumnType("varchar(255)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("varchar(255)"); - - b.Property("TenantId") - .HasColumnType("varchar(255)"); - - b.Property("Value") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ApiKeyDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ApiKeyDefinition_TenantId"); - - b.ToTable("ApiKeysDefinitions", "Elsa"); - }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ServiceDefinition", b => - { - b.Property("Id") - .HasColumnType("varchar(255)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("varchar(255)"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("TenantId") - .HasColumnType("varchar(255)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ServiceDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ServiceDefinition_TenantId"); - - b.ToTable("ServicesDefinitions", "Elsa"); - }); #pragma warning restore 612, 618 } } diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/efcore-3.6.sh new file mode 100644 index 00000000..16d773fe --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/efcore-3.6.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.Designer.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.Designer.cs new file mode 100644 index 00000000..a646c5b9 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.Designer.cs @@ -0,0 +1,61 @@ +// +using Elsa.Agents.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.PostgreSql.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20251213195607_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AgentConfig") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_AgentDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_AgentDefinition_TenantId"); + + b.ToTable("AgentDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.cs new file mode 100644 index 00000000..4fc4b8ad --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.PostgreSql.Migrations +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true), + Value = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeysDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServicesDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Settings = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true), + Type = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_Name", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_TenantId", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_Name", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_TenantId", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "TenantId"); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs index e905428b..4f944deb 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -52,64 +52,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentDefinitions", "Elsa"); }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ApiKeyDefinition", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("text"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ApiKeyDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ApiKeyDefinition_TenantId"); - - b.ToTable("ApiKeysDefinitions", "Elsa"); - }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ServiceDefinition", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ServiceDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ServiceDefinition_TenantId"); - - b.ToTable("ServicesDefinitions", "Elsa"); - }); #pragma warning restore 612, 618 } } diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/efcore-3.6.sh new file mode 100644 index 00000000..16d773fe --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/efcore-3.6.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.Designer.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.Designer.cs new file mode 100644 index 00000000..30646b43 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.Designer.cs @@ -0,0 +1,61 @@ +// +using Elsa.Agents.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.SqlServer.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20251213200214_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AgentConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_AgentDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_AgentDefinition_TenantId"); + + b.ToTable("AgentDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.cs new file mode 100644 index 00000000..cb921966 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.SqlServer.Migrations +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true), + Value = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeysDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServicesDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Settings = table.Column(type: "nvarchar(max)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true), + Type = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_Name", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_TenantId", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_Name", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_TenantId", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "TenantId"); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs index cd735a6e..74560fad 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -52,64 +52,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentDefinitions", "Elsa"); }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ApiKeyDefinition", b => - { - b.Property("Id") - .HasColumnType("nvarchar(450)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("TenantId") - .HasColumnType("nvarchar(450)"); - - b.Property("Value") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ApiKeyDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ApiKeyDefinition_TenantId"); - - b.ToTable("ApiKeysDefinitions", "Elsa"); - }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ServiceDefinition", b => - { - b.Property("Id") - .HasColumnType("nvarchar(450)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("TenantId") - .HasColumnType("nvarchar(450)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ServiceDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ServiceDefinition_TenantId"); - - b.ToTable("ServicesDefinitions", "Elsa"); - }); #pragma warning restore 612, 618 } } diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/efcore-3.6.sh new file mode 100644 index 00000000..16d773fe --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/efcore-3.6.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.Designer.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.Designer.cs new file mode 100644 index 00000000..4632bad6 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.Designer.cs @@ -0,0 +1,57 @@ +// +using Elsa.Agents.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.Sqlite.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20251213193144_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AgentConfig") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_AgentDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_AgentDefinition_TenantId"); + + b.ToTable("AgentDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.cs new file mode 100644 index 00000000..7f2eb029 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.Sqlite.Migrations +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true), + Value = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeysDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServicesDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Settings = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_Name", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_TenantId", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_Name", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_TenantId", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "TenantId"); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs index 6c9fb727..58243d29 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs @@ -16,7 +16,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.13"); + .HasAnnotation("ProductVersion", "9.0.11"); modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => { @@ -48,64 +48,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentDefinitions", "Elsa"); }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ApiKeyDefinition", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("TenantId") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ApiKeyDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ApiKeyDefinition_TenantId"); - - b.ToTable("ApiKeysDefinitions", "Elsa"); - }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ServiceDefinition", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("TenantId") - .HasColumnType("TEXT"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ServiceDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ServiceDefinition_TenantId"); - - b.ToTable("ServicesDefinitions", "Elsa"); - }); #pragma warning restore 612, 618 } } diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh new file mode 100644 index 00000000..16d773fe --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/Configurations.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/Configurations.cs index 72aaae52..bc2ab74a 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/Configurations.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore/Configurations.cs @@ -8,21 +8,8 @@ namespace Elsa.Agents.Persistence.EFCore; /// /// EF Core configuration for various entity types. /// -public class Configurations : IEntityTypeConfiguration, IEntityTypeConfiguration, IEntityTypeConfiguration +public class Configurations : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) - { - builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(ApiKeyDefinition)}_{nameof(ApiKeyDefinition.Name)}"); - builder.HasIndex(x => x.TenantId).HasDatabaseName($"IX_{nameof(ApiKeyDefinition)}_{nameof(ApiKeyDefinition.TenantId)}"); - } - - public void Configure(EntityTypeBuilder builder) - { - builder.Property(x => x.Settings).HasJsonValueConversion(); - builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(ServiceDefinition)}_{nameof(ServiceDefinition.Name)}"); - builder.HasIndex(x => x.TenantId).HasDatabaseName($"IX_{nameof(ServiceDefinition)}_{nameof(ServiceDefinition.TenantId)}"); - } - public void Configure(EntityTypeBuilder builder) { builder.Property(x => x.AgentConfig).HasJsonValueConversion(); diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/DbContext.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/DbContext.cs index 4bcbaa52..bded4ef0 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/DbContext.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore/DbContext.cs @@ -16,28 +16,16 @@ public AgentsDbContext(DbContextOptions options, IServiceProvid { } - /// - /// The API Keys DB set. - /// - public DbSet ApiKeysDefinitions { get; set; } = null!; - - /// - /// The Services DB set. - /// - public DbSet ServicesDefinitions { get; set; } = null!; - /// /// The Services DB set. /// - public DbSet AgentDefinitions { get; set; } = null!; + [UsedImplicitly] public DbSet AgentDefinitions { get; set; } = null!; /// protected override void OnModelCreating(ModelBuilder modelBuilder) { var configuration = new Configurations(); - modelBuilder.ApplyConfiguration(configuration); - modelBuilder.ApplyConfiguration(configuration); - modelBuilder.ApplyConfiguration(configuration); + modelBuilder.ApplyConfiguration(configuration); base.OnModelCreating(modelBuilder); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreApiKeyStore.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreApiKeyStore.cs deleted file mode 100644 index fe3f10b2..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreApiKeyStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.Persistence.EFCore; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence.EFCore; - -/// -/// An EF Core implementation of . -/// -[UsedImplicitly] -public class EFCoreApiKeyStore(EntityStore store) : IApiKeyStore -{ - public Task AddAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - return store.AddAsync(entity, cancellationToken); - } - - public Task UpdateAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - return store.UpdateAsync(entity, cancellationToken); - } - - public Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var filter = new ApiKeyDefinitionFilter - { - Id = id - }; - - return FindAsync(filter, cancellationToken); - } - - public Task FindAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default) - { - return store.FindAsync(filter.Apply, cancellationToken); - } - - public Task> ListAsync(CancellationToken cancellationToken = default) - { - return store.ListAsync(cancellationToken); - } - - public Task DeleteAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - return store.DeleteAsync(entity, cancellationToken); - } - - public Task DeleteManyAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default) - { - return store.DeleteWhereAsync(filter.Apply, cancellationToken); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreServiceStore.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreServiceStore.cs deleted file mode 100644 index 9127e73e..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreServiceStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.Persistence.EFCore; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence.EFCore; - -/// -/// An EF Core implementation of . -/// -[UsedImplicitly] -public class EFCoreServiceStore(EntityStore store) : IServiceStore -{ - public Task AddAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - return store.AddAsync(entity, cancellationToken); - } - - public Task UpdateAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - return store.UpdateAsync(entity, cancellationToken); - } - - public Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var filter = new ServiceDefinitionFilter - { - Id = id - }; - - return FindAsync(filter, cancellationToken); - } - - public Task FindAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default) - { - return store.FindAsync(filter.Apply, cancellationToken); - } - - public Task> ListAsync(CancellationToken cancellationToken = default) - { - return store.ListAsync(cancellationToken); - } - - public Task DeleteAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - return store.DeleteAsync(entity, cancellationToken); - } - - public Task DeleteManyAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default) - { - return store.DeleteWhereAsync(filter.Apply, cancellationToken); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/Feature.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/Feature.cs index 2e071632..780b65ab 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/Feature.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore/Feature.cs @@ -18,11 +18,7 @@ public override void Configure() { Module.Configure(feature => { - feature - .UseApiKeyStore(sp => sp.GetRequiredService()) - .UseServiceStore(sp => sp.GetRequiredService()) - .UseAgentStore(sp => sp.GetRequiredService()); - ; + feature.UseAgentStore(sp => sp.GetRequiredService()); }); } @@ -30,8 +26,6 @@ public override void Configure() public override void Apply() { base.Apply(); - AddEntityStore(); - AddEntityStore(); AddEntityStore(); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Contracts/IApiKeyStore.cs b/src/modules/agents/Elsa.Agents.Persistence/Contracts/IApiKeyStore.cs deleted file mode 100644 index 43fd3862..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Contracts/IApiKeyStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; - -namespace Elsa.Agents.Persistence.Contracts; - -public interface IApiKeyStore -{ - /// - /// Adds a new entity to the store. - /// - Task AddAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Updates the entity to the store. - /// - Task UpdateAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Gets the entity from the store. - /// - Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// - /// Finds a single entity using the specified filter. - /// - Task FindAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default); - - /// - /// Gets all entities from the store. - /// - Task> ListAsync(CancellationToken cancellationToken = default); - - /// - /// Deletes the entity from the store. - /// - Task DeleteAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Deletes all entities from the store that match the specified filter. - /// - Task DeleteManyAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Contracts/IServiceStore.cs b/src/modules/agents/Elsa.Agents.Persistence/Contracts/IServiceStore.cs deleted file mode 100644 index 5e3d98d0..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Contracts/IServiceStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; - -namespace Elsa.Agents.Persistence.Contracts; - -public interface IServiceStore -{ - /// - /// Adds a new entity to the store. - /// - Task AddAsync(ServiceDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Updates the entity to the store. - /// - Task UpdateAsync(ServiceDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Gets the entity from the store. - /// - Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// - /// Finds the entity from the store. - /// - Task FindAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default); - - /// - /// Gets all entities from the store. - /// - Task> ListAsync(CancellationToken cancellationToken = default); - - /// - /// Deletes the entity from the store. - /// - Task DeleteAsync(ServiceDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Deletes all entities from the store that match the specified filter. - /// - Task DeleteManyAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Entities/AgentDefinition.cs b/src/modules/agents/Elsa.Agents.Persistence/Entities/AgentDefinition.cs index 9715eafc..457e9b76 100644 --- a/src/modules/agents/Elsa.Agents.Persistence/Entities/AgentDefinition.cs +++ b/src/modules/agents/Elsa.Agents.Persistence/Entities/AgentDefinition.cs @@ -1,4 +1,3 @@ -using Elsa.Agents; using Elsa.Common.Entities; namespace Elsa.Agents.Persistence.Entities; diff --git a/src/modules/agents/Elsa.Agents.Persistence/Entities/ApiKeyDefinition.cs b/src/modules/agents/Elsa.Agents.Persistence/Entities/ApiKeyDefinition.cs deleted file mode 100644 index 96beb19b..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Entities/ApiKeyDefinition.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Elsa.Agents; -using Elsa.Common.Entities; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence.Entities; - -[UsedImplicitly] -public class ApiKeyDefinition : Entity -{ - public string Name { get; set; } = null!; - public string Value { get; set; } = null!; - - public ApiKeyConfig ToApiKeyConfig() - { - return new ApiKeyConfig - { - Name = Name, - Value = Value - }; - } - - public ApiKeyModel ToModel() - { - return new ApiKeyModel - { - Id = Id, - Name = Name, - Value = Value - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Entities/ServiceDefinition.cs b/src/modules/agents/Elsa.Agents.Persistence/Entities/ServiceDefinition.cs deleted file mode 100644 index a9a47dca..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Entities/ServiceDefinition.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Elsa.Agents; -using Elsa.Common.Entities; - -namespace Elsa.Agents.Persistence.Entities; - -public class ServiceDefinition : Entity -{ - public string Name { get; set; } - public string Type { get; set; } - public IDictionary Settings { get; set; } = new Dictionary(); - - public ServiceConfig ToServiceConfig() - { - return new ServiceConfig - { - Name = Name, - Type = Type, - Settings = Settings - }; - } - - public ServiceModel ToModel() - { - return new ServiceModel - { - Id = Id, - Name = Name, - Type = Type, - Settings = Settings - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs b/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs index f02f4d55..a70a6fd5 100644 --- a/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs +++ b/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs @@ -9,25 +9,11 @@ namespace Elsa.Agents.Persistence.Features; -[DependsOn(typeof(AgentsFeature))] +[DependsOn(typeof(AgentsCoreFeature))] public class AgentPersistenceFeature(IModule module) : FeatureBase(module) { - private Func _apiKeyStoreFactory = sp => sp.GetRequiredService(); - private Func _serviceStoreFactory = sp => sp.GetRequiredService(); private Func _agentStoreFactory = sp => sp.GetRequiredService(); - public AgentPersistenceFeature UseApiKeyStore(Func factory) - { - _apiKeyStoreFactory = factory; - return this; - } - - public AgentPersistenceFeature UseServiceStore(Func factory) - { - _serviceStoreFactory = factory; - return this; - } - public AgentPersistenceFeature UseAgentStore(Func factory) { _agentStoreFactory = factory; @@ -36,24 +22,14 @@ public AgentPersistenceFeature UseAgentStore(Func public override void Configure() { - Module.UseAgents(agents => agents.UseKernelConfigProvider(sp => sp.GetRequiredService())); + Module.UseAgentsCore(agents => agents.UseKernelConfigProvider(sp => sp.GetRequiredService())); } public override void Apply() { - Services - .AddScoped(_apiKeyStoreFactory) - .AddScoped(_serviceStoreFactory) - .AddScoped(_agentStoreFactory); - - Services - .AddScoped(); - - Services - .AddMemoryStore() - .AddMemoryStore() - .AddMemoryStore(); - + Services.AddScoped(_agentStoreFactory); + Services.AddScoped(); + Services.AddMemoryStore(); Services.AddScoped(); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs b/src/modules/agents/Elsa.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs deleted file mode 100644 index 42bc2ed2..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Elsa.Agents.Persistence.Entities; - -namespace Elsa.Agents.Persistence.Filters; - -public class ApiKeyDefinitionFilter -{ - public string? Id { get; set; } - public ICollection? Ids { get; set; } - public string? NotId { get; set; } - public string? Name { get; set; } - - public IQueryable Apply(IQueryable queryable) - { - if (!string.IsNullOrWhiteSpace(Id)) queryable = queryable.Where(x => x.Id == Id); - if (Ids != null && Ids.Any()) queryable = queryable.Where(x => Ids.Contains(x.Id)); - if (!string.IsNullOrWhiteSpace(NotId)) queryable = queryable.Where(x => x.Id != NotId); - if (!string.IsNullOrWhiteSpace(Name)) queryable = queryable.Where(x => x.Name == Name); - return queryable; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Filters/ServiceDefinitionFilter.cs b/src/modules/agents/Elsa.Agents.Persistence/Filters/ServiceDefinitionFilter.cs deleted file mode 100644 index eb74a335..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Filters/ServiceDefinitionFilter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Elsa.Agents.Persistence.Entities; - -namespace Elsa.Agents.Persistence.Filters; - -public class ServiceDefinitionFilter -{ - public string? Id { get; set; } - public ICollection? Ids { get; set; } - public string? NotId { get; set; } - public string? Name { get; set; } - - public IQueryable Apply(IQueryable queryable) - { - if (!string.IsNullOrWhiteSpace(Id)) queryable = queryable.Where(x => x.Id == Id); - if (Ids != null && Ids.Any()) queryable = queryable.Where(x => Ids.Contains(x.Id)); - if (!string.IsNullOrWhiteSpace(NotId)) queryable = queryable.Where(x => x.Id != NotId); - if (!string.IsNullOrWhiteSpace(Name)) queryable = queryable.Where(x => x.Name == Name); - return queryable; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryApiKeyStore.cs b/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryApiKeyStore.cs deleted file mode 100644 index 05c6984c..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryApiKeyStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.Common.Services; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence; - -[UsedImplicitly] -public class MemoryApiKeyStore(MemoryStore memoryStore) : IApiKeyStore -{ - public Task AddAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Add(entity, x => x.Id); - return Task.CompletedTask; - } - - public Task UpdateAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Save(entity, x => x.Id); - return Task.CompletedTask; - } - - public Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var entity = memoryStore.Find(x => x.Id == id); - return Task.FromResult(entity); - } - - public Task FindAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default) - { - var entity = memoryStore.Query(filter.Apply).FirstOrDefault(); - return Task.FromResult(entity); - } - - public Task> ListAsync(CancellationToken cancellationToken = default) - { - var entities = memoryStore.List(); - return Task.FromResult(entities); - } - - public Task DeleteAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Delete(entity.Id); - return Task.CompletedTask; - } - - public Task DeleteManyAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default) - { - var agents = memoryStore.Query(filter.Apply).ToList(); - memoryStore.DeleteMany(agents, x => x.Id); - return Task.FromResult(agents.Count); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryServiceStore.cs b/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryServiceStore.cs deleted file mode 100644 index e945aacf..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryServiceStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.Common.Services; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence; - -[UsedImplicitly] -public class MemoryServiceStore(MemoryStore memoryStore) : IServiceStore -{ - public Task AddAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Add(entity, x => x.Id); - return Task.CompletedTask; - } - - public Task UpdateAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Save(entity, x => x.Id); - return Task.CompletedTask; - } - - public Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var entity = memoryStore.Find(x => x.Id == id); - return Task.FromResult(entity); - } - - public Task FindAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default) - { - var entity = memoryStore.Query(filter.Apply).FirstOrDefault(); - return Task.FromResult(entity); - } - - public Task> ListAsync(CancellationToken cancellationToken = default) - { - var entities = memoryStore.List(); - return Task.FromResult(entities); - } - - public Task DeleteAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Delete(entity.Id); - return Task.CompletedTask; - } - - public Task DeleteManyAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default) - { - var agents = memoryStore.Query(filter.Apply).ToList(); - memoryStore.DeleteMany(agents, x => x.Id); - return Task.FromResult(agents.Count); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Services/StoreKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Persistence/Services/StoreKernelConfigProvider.cs index 8d144518..b926dd89 100644 --- a/src/modules/agents/Elsa.Agents.Persistence/Services/StoreKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Persistence/Services/StoreKernelConfigProvider.cs @@ -1,18 +1,13 @@ -using Elsa.Agents; using Elsa.Agents.Persistence.Contracts; namespace Elsa.Agents.Persistence; -public class StoreKernelConfigProvider(IApiKeyStore apiKeyStore, IServiceStore serviceStore, IAgentStore agentStore) : IKernelConfigProvider +public class StoreKernelConfigProvider(IAgentStore agentStore) : IKernelConfigProvider { public async Task GetKernelConfigAsync(CancellationToken cancellationToken = default) { var kernelConfig = new KernelConfig(); - var apiKeys = await apiKeyStore.ListAsync(cancellationToken); - var services = await serviceStore.ListAsync(cancellationToken); var agents = await agentStore.ListAsync(cancellationToken); - foreach (var apiKey in apiKeys) kernelConfig.ApiKeys[apiKey.Name] = apiKey.ToApiKeyConfig(); - foreach (var service in services) kernelConfig.Services[service.Name] = service.ToServiceConfig(); foreach (var agent in agents) kernelConfig.Agents[agent.Name] = agent.ToAgentConfig(); return kernelConfig; } diff --git a/src/modules/agents/Elsa.Agents/AgentsFeature.cs b/src/modules/agents/Elsa.Agents/AgentsFeature.cs new file mode 100644 index 00000000..c7ca0664 --- /dev/null +++ b/src/modules/agents/Elsa.Agents/AgentsFeature.cs @@ -0,0 +1,25 @@ +using Elsa.Agents.Activities.Features; +using Elsa.Agents.Features; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Agents; + +[DependsOn(typeof(AgentsCoreFeature))] +[DependsOn(typeof(AgentActivitiesFeature))] +public class AgentsFeature(IModule module) : FeatureBase(module) +{ + public AgentsFeature AddAgent(string? key = null) where TAgent : class, IAgent + { + Module.Services.Configure(options => options.AddAgentType(key)); + return this; + } + + public AgentsFeature AddServiceDescriptor(ServiceDescriptor descriptor) + { + Module.Services.Configure(options => options.ServiceDescriptors.Add(descriptor)); + return this; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents/Elsa.Agents.csproj b/src/modules/agents/Elsa.Agents/Elsa.Agents.csproj new file mode 100644 index 00000000..80cc94d9 --- /dev/null +++ b/src/modules/agents/Elsa.Agents/Elsa.Agents.csproj @@ -0,0 +1,21 @@ + + + + Provides an agentic framework using Semantic Kernel and Microsoft Agent Framework + elsa extension module agents semantic kernel llm ai maf + Elsa.Agents + + + + + + + + + + + + + + + diff --git a/src/modules/agents/Elsa.Agents/FodyWeavers.xml b/src/modules/agents/Elsa.Agents/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/modules/agents/Elsa.Agents/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents/ModuleExtensions.cs b/src/modules/agents/Elsa.Agents/ModuleExtensions.cs new file mode 100644 index 00000000..c2ccc5a8 --- /dev/null +++ b/src/modules/agents/Elsa.Agents/ModuleExtensions.cs @@ -0,0 +1,12 @@ +using Elsa.Features.Services; + +namespace Elsa.Agents; + +public static class ModuleExtensions +{ + public static IModule UseAgents(this IModule module, Action? configure = null) + { + module.Configure(configure); + return module; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs b/src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs index 730cefe1..0dc19de4 100644 --- a/src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs +++ b/src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs @@ -1,12 +1,11 @@ using Elsa.Studio.Contracts; using Elsa.Studio.Localization; using Elsa.Studio.Models; -using MudBlazor; namespace Elsa.Studio.Agents; /// A menu provider for the Agents module. -public class AgentsMenu(ILocalizer localizer) : IMenuProvider, IMenuGroupProvider +public class AgentsMenu(ILocalizer localizer) : IMenuProvider { /// public ValueTask> GetMenuItemsAsync(CancellationToken cancellationToken = default) @@ -19,41 +18,9 @@ public ValueTask> GetMenuItemsAsync(CancellationToken canc Href = "ai/agents", Text = localizer["Agents"], GroupName = MenuItemGroups.General.Name - }, - new() - { - Icon = AgentIcons.AI, - Text = localizer["Agents"], - GroupName = MenuItemGroups.Settings.Name, - SubMenuItems = - [ - new MenuItem - { - Icon = Icons.Material.Outlined.Key, - Href = "ai/api-keys", - Text = localizer["API Keys"] - }, - new MenuItem - { - Icon = Icons.Material.Outlined.MiscellaneousServices, - Href = "ai/services", - Text = localizer["Services"] - } - ] } }; - return new ValueTask>(menuItems); - } - - /// - public ValueTask> GetMenuGroupsAsync(CancellationToken cancellationToken = default) - { - var groups = new List - { - //new("agents", "Intelligent Agents", 10f) - }; - - return new ValueTask>(groups); + return new(menuItems); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/IApiKeysApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/IApiKeysApi.cs deleted file mode 100644 index 33030dea..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/Client/IApiKeysApi.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Elsa.Agents; -using Elsa.Api.Client.Resources.WorkflowDefinitions.Responses; -using Elsa.Api.Client.Shared.Models; -using Refit; - -namespace Elsa.Studio.Agents.Client; - -/// Represents a client API for interacting with agents. -public interface IApiKeysApi -{ - /// Lists all API keys. - [Get("/ai/api-keys")] - Task> ListAsync(CancellationToken cancellationToken = default); - - /// Gets an API key by ID. - [Get("/ai/api-keys/{id}")] - Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// Creates a new API key. - [Post("/ai/api-keys")] - Task CreateAsync(ApiKeyInputModel request, CancellationToken cancellationToken = default); - - /// Updates an API key. - [Post("/ai/api-keys/{id}")] - Task UpdateAsync(string id, ApiKeyInputModel request, CancellationToken cancellationToken = default); - - /// Deletes an API key. - [Delete("/ai/api-keys/{id}")] - Task DeleteAsync(string id, CancellationToken cancellationToken = default); - - /// Deletes multiple API keys. - [Post("/ai/bulk-actions/api-keys/delete")] - Task BulkDeleteAsync(BulkDeleteRequest request, CancellationToken cancellationToken = default); - - /// Checks if a name is unique. - Task GetIsNameUniqueAsync(IsUniqueNameRequest request, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/IPluginsApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/IPluginsApi.cs deleted file mode 100644 index 039abf2f..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/Client/IPluginsApi.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Elsa.Agents; -using Elsa.Api.Client.Shared.Models; -using Refit; - -namespace Elsa.Studio.Agents.Client; - -/// Represents a client API for interacting with AI plugins. -public interface IPluginsApi -{ - /// Lists all services. - [Get("/ai/plugins")] - Task> ListAsync(CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/IServiceProvidersApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/IServiceProvidersApi.cs deleted file mode 100644 index c39dc0fe..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/Client/IServiceProvidersApi.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Elsa.Api.Client.Shared.Models; -using Refit; - -namespace Elsa.Studio.Agents.Client; - -/// Represents a client API for retrieving available service providers. -public interface IServiceProvidersApi -{ - /// Lists all service providers. - [Get("/ai/service-providers")] - Task> ListAsync(CancellationToken cancellationToken = default); -} diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/IServicesApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/IServicesApi.cs deleted file mode 100644 index 6545c26e..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/Client/IServicesApi.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Elsa.Agents; -using Elsa.Api.Client.Resources.WorkflowDefinitions.Responses; -using Elsa.Api.Client.Shared.Models; -using Refit; - -namespace Elsa.Studio.Agents.Client; - -/// Represents a client API for interacting with AI services. -public interface IServicesApi -{ - /// Lists all services. - [Get("/ai/services")] - Task> ListAsync(CancellationToken cancellationToken = default); - - /// Gets a service by ID. - [Get("/ai/services/{id}")] - Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// Creates a new service. - [Post("/ai/services")] - Task CreateAsync(ServiceInputModel request, CancellationToken cancellationToken = default); - - /// Updates a service. - [Post("/ai/services/{id}")] - Task UpdateAsync(string id, ServiceInputModel request, CancellationToken cancellationToken = default); - - /// Deletes a service. - [Delete("/ai/services/{id}")] - Task DeleteAsync(string id, CancellationToken cancellationToken = default); - - /// Deletes multiple services. - [Post("/ai/bulk-actions/services/delete")] - Task BulkDeleteAsync(BulkDeleteRequest request, CancellationToken cancellationToken = default); - - /// Checks if a name is unique. - Task GetIsNameUniqueAsync(IsUniqueNameRequest request, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/ISkillsApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/ISkillsApi.cs new file mode 100644 index 00000000..63141689 --- /dev/null +++ b/src/modules/agents/Elsa.Studio.Agents/Client/ISkillsApi.cs @@ -0,0 +1,13 @@ +using Elsa.Agents; +using Elsa.Api.Client.Shared.Models; +using Refit; + +namespace Elsa.Studio.Agents.Client; + +/// Represents a client API for interacting with AI skills. +public interface ISkillsApi +{ + /// Lists all services. + [Get("/ai/skills")] + Task> ListAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs b/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs index 25c06e32..74b1eb4c 100644 --- a/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs @@ -20,13 +20,12 @@ public static IServiceCollection AddAgentsModule(this IServiceCollection service return services .AddScoped() .AddScoped() - .AddScoped() .AddRemoteApi(backendApiConfig) - .AddRemoteApi(backendApiConfig) - .AddRemoteApi(backendApiConfig) - .AddRemoteApi(backendApiConfig) + .AddRemoteApi(backendApiConfig) .AddActivityDisplaySettingsProvider() - .AddScoped() + + // TODO: Move this to a separate module. + //.AddScoped() ; } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor index 93718092..756a50da 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor @@ -8,20 +8,11 @@ - - - - - @foreach (var service in AvailableServices) - { - - } - - - - - @foreach (var plugin in AvailablePlugins) + + + + @foreach (var plugin in AvailableSkills) { } diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs index 52540106..3e081192 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs @@ -25,10 +25,8 @@ public partial class CreateAgentDialog [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; [Inject] private IActivityRegistry ActivityRegistry { get; set; } = null!; [Inject] private IActivityDisplaySettingsRegistry ActivityDisplaySettingsRegistry { get; set; } = null!; - private ICollection AvailableServices { get; set; } = []; - private IReadOnlyCollection SelectedServices { get; set; } = []; - private ICollection AvailablePlugins { get; set; } = []; - private IReadOnlyCollection SelectedPlugins { get; set; } = []; + private ICollection AvailableSkills { get; set; } = []; + private IReadOnlyCollection SelectedSkills { get; set; } = []; /// protected override async Task OnInitializedAsync() @@ -36,21 +34,16 @@ protected override async Task OnInitializedAsync() _agentInputModel.Name = AgentName; _agentInputModel.PromptTemplate = "You are a helpful assistant."; _agentInputModel.Description = "A helpful assistant."; - _agentInputModel.FunctionName = "Reply"; _agentInputModel.OutputVariable.Type = "object"; _agentInputModel.OutputVariable.Description = "The output of the agent."; _agentInputModel.ExecutionSettings.ResponseFormat = "json_object"; _editContext = new(_agentInputModel); var agentsApi = await ApiClientProvider.GetApiAsync(); - var servicesApi = await ApiClientProvider.GetApiAsync(); - var pluginsApi = await ApiClientProvider.GetApiAsync(); + var skillsApi = await ApiClientProvider.GetApiAsync(); _validator = new(agentsApi); - var servicesResponseList = await servicesApi.ListAsync(); - var pluginsResponseList = await pluginsApi.ListAsync(); - AvailableServices = servicesResponseList.Items; - AvailablePlugins = pluginsResponseList.Items; - SelectedServices = _agentInputModel.Services.ToList().AsReadOnly(); - SelectedPlugins = _agentInputModel.Plugins.ToList().AsReadOnly(); + var skillsResponseList = await skillsApi.ListAsync(); + AvailableSkills = skillsResponseList.Items; + SelectedSkills = _agentInputModel.Skills.ToList().AsReadOnly(); } private Task OnCancelClicked() @@ -69,8 +62,7 @@ private async Task OnSubmitClicked() private Task OnValidSubmit() { - _agentInputModel.Services = SelectedServices.ToList(); - _agentInputModel.Plugins = SelectedPlugins.ToList(); + _agentInputModel.Skills = SelectedSkills.ToList(); MudDialog.Close(_agentInputModel); ActivityRegistry.MarkStale(); ActivityDisplaySettingsRegistry.MarkStale(); diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor index 945e0db6..f8a5caeb 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor @@ -31,14 +31,7 @@ Label="@Localizer["Description"]" Variant="Variant.Outlined" HelperText="@Localizer["A description about the role and purpose of this agent."]"/> - - - + - - - - - @foreach (var service in AvailableServices) - { - - } - - - - - + - - - @foreach (var plugin in AvailablePlugins) + + + @foreach (var skill in AvailableSkills) { - + } diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs index 2e053cf8..71fda433 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs @@ -21,31 +21,25 @@ private bool UseJsonResponse get => _agent.ExecutionSettings.ResponseFormat == "json_object"; set => _agent.ExecutionSettings.ResponseFormat = value ? "json_object" : "string"; } - - private ICollection AvailableServices { get; set; } = []; - private IReadOnlyCollection SelectedServices { get; set; } = []; - private ICollection AvailablePlugins { get; set; } = []; - private IReadOnlyCollection SelectedPlugins { get; set; } = []; + private ICollection AvailableSkills { get; set; } = []; + private IReadOnlyCollection SelectedSkills { get; set; } = []; private MudForm _form = null!; private AgentInputModelValidator _validator = null!; private AgentModel _agent = new(); private InputVariableConfig? _inputVariableBackup; - private MudTable _inputVariableTable; + private MudTable _inputVariableTable = null!; /// protected override async Task OnInitializedAsync() { var apiClient = await ApiClientProvider.GetApiAsync(); - _validator = new AgentInputModelValidator(apiClient); - var servicesApi = await ApiClientProvider.GetApiAsync(); - var pluginsApi = await ApiClientProvider.GetApiAsync(); - var servicesResponseList = await servicesApi.ListAsync(); - var pluginsResponseList = await pluginsApi.ListAsync(); - AvailableServices = servicesResponseList.Items; - AvailablePlugins = pluginsResponseList.Items; + _validator = new(apiClient); + var skillsApi = await ApiClientProvider.GetApiAsync(); + var skillsResponseList = await skillsApi.ListAsync(); + AvailableSkills = skillsResponseList.Items; } /// @@ -53,8 +47,7 @@ protected override async Task OnParametersSetAsync() { var apiClient = await ApiClientProvider.GetApiAsync(); _agent = await apiClient.GetAsync(AgentId); - SelectedServices = _agent.Services.ToList().AsReadOnly(); - SelectedPlugins = _agent.Plugins.ToList().AsReadOnly(); + SelectedSkills = _agent.Skills.ToList().AsReadOnly(); } private async Task OnSaveClicked() @@ -63,9 +56,8 @@ private async Task OnSaveClicked() if (!_form.IsValid) return; - - _agent.Services = SelectedServices.ToList(); - _agent.Plugins = SelectedPlugins.ToList(); + + _agent.Skills = SelectedSkills.ToList(); var apiClient = await ApiClientProvider.GetApiAsync(); _agent = await apiClient.UpdateAsync(AgentId, _agent); Snackbar.Add("Agent successfully updated.", Severity.Success); @@ -96,7 +88,7 @@ private void OnAddInputVariableClicked() private void BackupInputVariable(object obj) { var inputVariable = (InputVariableConfig)obj; - _inputVariableBackup = new InputVariableConfig + _inputVariableBackup = new() { Name = inputVariable.Name, Type = inputVariable.Type, diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor deleted file mode 100644 index a7c5d1a9..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor +++ /dev/null @@ -1,43 +0,0 @@ -@page "/ai/api-keys/{ApiKeyId}" -@using Elsa.Agents -@using Variant = MudBlazor.Variant -@inherits StudioComponentBase -@inject ILocalizer Localizer - -@Localizer["API Key"] - - - - - - - - - - - - - - - - - - - - @Localizer["Save"] - - - \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor.cs deleted file mode 100644 index a23d2fcc..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using Elsa.Studio.Agents.UI.Validators; -using Elsa.Studio.Components; -using Elsa.Studio.Contracts; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Elsa.Studio.Agents.UI.Pages; - -public partial class ApiKey : StudioComponentBase -{ - /// The ID of the API key to edit. - [Parameter] public string ApiKeyId { get; set; } = null!; - - [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - [Inject] private NavigationManager NavigationManager { get; set; } = null!; - private bool IsNew => string.Equals("new", ApiKeyId, StringComparison.OrdinalIgnoreCase); - - private MudForm _form = null!; - private ApiKeyInputModelValidator _validator = null!; - private ApiKeyModel _apiKey = new(); - - /// - protected override async Task OnParametersSetAsync() - { - var apiClient = await ApiClientProvider.GetApiAsync(); - - if (IsNew) - _apiKey = new(); - else - _apiKey = await apiClient.GetAsync(ApiKeyId); - - _validator = new ApiKeyInputModelValidator(apiClient); - } - - private async Task OnSaveClicked() - { - await _form.Validate(); - - if (!_form.IsValid) - return; - - var apiClient = await ApiClientProvider.GetApiAsync(); - - if (IsNew) - { - _apiKey = await apiClient.CreateAsync(_apiKey); - Snackbar.Add("API key successfully created.", Severity.Success); - } - else - { - _apiKey = await apiClient.UpdateAsync(ApiKeyId, _apiKey); - Snackbar.Add("API key successfully updated.", Severity.Success); - } - - StateHasChanged(); - NavigationManager.NavigateTo("ai/api-keys"); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor deleted file mode 100644 index 54d33b52..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor +++ /dev/null @@ -1,68 +0,0 @@ -@page "/ai/api-keys" -@using Elsa.Agents -@using Variant = MudBlazor.Variant -@inherits StudioComponentBase -@inject ILocalizer Localizer - -@Localizer["API Keys"] - - - - - - - - Delete - - - - - @Localizer["Create API Key"] - - - - - ID - - - @Localizer["Name"] - - - @Localizer["Value"] - - - - - @context.Id - @context.Name - @context.Value - - - @Localizer["Edit"] - @Localizer["Delete"] - - - - - @Localizer["No API keys found"] - - - @Localizer["Loading"]... - - - - - - \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor.cs deleted file mode 100644 index 2416bf00..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using Elsa.Studio.Contracts; -using Elsa.Studio.DomInterop.Contracts; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Elsa.Studio.Agents.UI.Pages; - -[UsedImplicitly] -public partial class ApiKeys -{ - private MudTable _table = null!; - private HashSet _selectedRows = new(); - - [Inject] private IDialogService DialogService { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - [Inject] NavigationManager NavigationManager { get; set; } = null!; - [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; - [Inject] private IFiles Files { get; set; } = null!; - [Inject] private IDomAccessor DomAccessor { get; set; } = null!; - - private async Task GetApiClientAsync() - { - return await ApiClientProvider.GetApiAsync(); - } - - private async Task> ServerReload(TableState state, CancellationToken cancellationToken) - { - var apiClient = await GetApiClientAsync(); - var response = await apiClient.ListAsync(cancellationToken); - - return new TableData - { - TotalItems = (int)response.Count, - Items = response.Items - }; - } - - private async Task OnCreateClicked() - { - await InvokeAsync(() => NavigationManager.NavigateTo($"ai/api-keys/new")); - } - - private async Task EditAsync(string id) - { - await InvokeAsync(() => NavigationManager.NavigateTo($"ai/api-keys/{id}")); - } - - private void Reload() - { - _table.ReloadServerData(); - } - - private async Task OnEditClicked(string id) - { - await EditAsync(id); - } - - private async Task OnRowClick(TableRowClickEventArgs e) - { - await EditAsync(e.Item.Id); - } - - private async Task OnDeleteClicked(ApiKeyModel model) - { - var result = await DialogService.ShowMessageBox("Delete API Key?", "Are you sure you want to delete this API key?", yesText: "Delete", cancelText: "Cancel"); - - if (result != true) - return; - - var id = model.Id; - var apiClient = await GetApiClientAsync(); - await apiClient.DeleteAsync(id); - Reload(); - } - - private async Task OnBulkDeleteClicked() - { - var result = await DialogService.ShowMessageBox("Delete Selected API keys?", "Are you sure you want to delete the selected API keys?", yesText: "Delete", cancelText: "Cancel"); - - if (result != true) - return; - - var ids = _selectedRows.Select(x => x.Id).ToList(); - var request = new BulkDeleteRequest { Ids = ids }; - var apiClient = await GetApiClientAsync(); - await apiClient.BulkDeleteAsync(request); - Reload(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor deleted file mode 100644 index d8462c19..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor +++ /dev/null @@ -1,57 +0,0 @@ -@page "/ai/services/{ServiceId}" -@using Elsa.Agents -@using Variant = MudBlazor.Variant -@inherits StudioComponentBase -@inject ILocalizer Localizer - -@Localizer["Service"] - - - - - - - - - - - - - - @foreach (var provider in _serviceProviders) - { - @Localizer[provider] - } - - - - - - - - - - @Localizer["Save"] - - - \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor.cs deleted file mode 100644 index 49e3a91a..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Text.Json; -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using Elsa.Studio.Agents.UI.Validators; -using Elsa.Studio.Components; -using Elsa.Studio.Contracts; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Elsa.Studio.Agents.UI.Pages; - -public partial class Service : StudioComponentBase -{ - /// The ID of the service to edit. - [Parameter] public string ServiceId { get; set; } = null!; - - [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - [Inject] private NavigationManager NavigationManager { get; set; } = null!; - private bool IsNew => string.Equals("new", ServiceId, StringComparison.OrdinalIgnoreCase); - - private MudForm _form = null!; - private ServiceInputModelValidator _validator = null!; - private ServiceModel _entity = new(); - private ICollection _serviceProviders = []; - - /// - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - var providersApi = await ApiClientProvider.GetApiAsync(); - var response = await providersApi.ListAsync(); - _serviceProviders = response.Items; - } - - /// - protected override async Task OnParametersSetAsync() - { - var apiClient = await ApiClientProvider.GetApiAsync(); - - if (IsNew) - _entity = new(); - else - _entity = await apiClient.GetAsync(ServiceId); - - _validator = new ServiceInputModelValidator(apiClient); - } - - private async Task OnSaveClicked() - { - await _form.Validate(); - - if (!_form.IsValid) - return; - - var apiClient = await ApiClientProvider.GetApiAsync(); - - if (IsNew) - { - _entity = await apiClient.CreateAsync(_entity); - Snackbar.Add("Service successfully created.", Severity.Success); - } - else - { - _entity = await apiClient.UpdateAsync(ServiceId, _entity); - Snackbar.Add("Service successfully updated.", Severity.Success); - } - - StateHasChanged(); - NavigationManager.NavigateTo("ai/services"); - } - - private MudBlazor.Converter, string> GetSettingsConverter() - { - return new MudBlazor.Converter, string> - { - SetFunc = x => x == null ? "{}" : JsonSerializer.Serialize(x, new JsonSerializerOptions { WriteIndented = true }), - GetFunc = x => JsonSerializer.Deserialize>(x) ?? new Dictionary() - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor deleted file mode 100644 index da423c91..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor +++ /dev/null @@ -1,73 +0,0 @@ -@page "/ai/services" -@using System.Text.Json -@using Elsa.Agents -@using Variant = MudBlazor.Variant -@inherits StudioComponentBase -@inject ILocalizer Localizer - -@Localizer["Services"] - - - - - - - - @Localizer["Delete"] - - - - - @Localizer["Create Service"] - - - - - ID - - - @Localizer["Name"] - - - @Localizer["Settings"] - - - - - @context.Id - @context.Name - - - @JsonSerializer.Serialize(context.Settings, JsonSerializerOptions.Default) - - - - - @Localizer["Edit"] - @Localizer["Delete"] - - - - - @Localizer["No services found"] - - - @Localizer["Loading"]... - - - - - - \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor.cs deleted file mode 100644 index 3d40ad74..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using Elsa.Studio.Contracts; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Elsa.Studio.Agents.UI.Pages; - -[UsedImplicitly] -public partial class Services -{ - private MudTable _table = null!; - private HashSet _selectedRows = new(); - - [Inject] private IDialogService DialogService { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - [Inject] NavigationManager NavigationManager { get; set; } = null!; - [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; - - private async Task GetApiClientAsync() - { - return await ApiClientProvider.GetApiAsync(); - } - - private async Task> ServerReload(TableState state, CancellationToken cancellationToken) - { - var apiClient = await GetApiClientAsync(); - var response = await apiClient.ListAsync(cancellationToken); - - return new TableData - { - TotalItems = (int)response.Count, - Items = response.Items - }; - } - - private async Task OnCreateClicked() - { - await InvokeAsync(() => NavigationManager.NavigateTo($"ai/services/new")); - } - - private async Task EditAsync(string id) - { - await InvokeAsync(() => NavigationManager.NavigateTo($"ai/services/{id}")); - } - - private void Reload() - { - _table.ReloadServerData(); - } - - private async Task OnEditClicked(string id) - { - await EditAsync(id); - } - - private async Task OnRowClick(TableRowClickEventArgs e) - { - await EditAsync(e.Item.Id); - } - - private async Task OnDeleteClicked(ServiceModel model) - { - var result = await DialogService.ShowMessageBox("Delete Service?", "Are you sure you want to delete this service?", yesText: "Delete", cancelText: "Cancel"); - - if (result != true) - return; - - var id = model.Id; - var apiClient = await GetApiClientAsync(); - await apiClient.DeleteAsync(id); - Reload(); - } - - private async Task OnBulkDeleteClicked() - { - var result = await DialogService.ShowMessageBox("Delete Selected services?", "Are you sure you want to delete the selected services?", yesText: "Delete", cancelText: "Cancel"); - - if (result != true) - return; - - var ids = _selectedRows.Select(x => x.Id).ToList(); - var request = new BulkDeleteRequest { Ids = ids }; - var apiClient = await GetApiClientAsync(); - await apiClient.BulkDeleteAsync(request); - Reload(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ApiKeyInputModelValidator.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ApiKeyInputModelValidator.cs deleted file mode 100644 index 02bd50af..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ApiKeyInputModelValidator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using FluentValidation; - -namespace Elsa.Studio.Agents.UI.Validators; - -/// -/// A validator for instances. -/// -public class ApiKeyInputModelValidator : AbstractValidator -{ - /// - public ApiKeyInputModelValidator(IApiKeysApi apiKeysApi) - { - RuleFor(x => x.Name).NotEmpty().WithMessage("Please enter a name for the API key."); - - RuleFor(x => x.Name) - .MustAsync(async (context, name, cancellationToken) => - { - var request = new IsUniqueNameRequest - { - Name = name!, - }; - var response = await apiKeysApi.GetIsNameUniqueAsync(request, cancellationToken); - return response.IsUnique; - }) - .WithMessage("An API key with this name already exists."); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ServiceInputModelValidator.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ServiceInputModelValidator.cs deleted file mode 100644 index 8f46438d..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ServiceInputModelValidator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using FluentValidation; - -namespace Elsa.Studio.Agents.UI.Validators; - -/// -/// A validator for instances. -/// -public class ServiceInputModelValidator : AbstractValidator -{ - /// - public ServiceInputModelValidator(IServicesApi api) - { - RuleFor(x => x.Name).NotEmpty().WithMessage("Please enter a name for the service."); - - RuleFor(x => x.Name) - .MustAsync(async (context, name, cancellationToken) => - { - var request = new IsUniqueNameRequest - { - Name = name!, - }; - var response = await api.GetIsNameUniqueAsync(request, cancellationToken); - return response.IsUnique; - }) - .WithMessage("A service with this name already exists."); - } -} \ No newline at end of file diff --git a/src/modules/agents/Examples/CodeFirstAgentExample.cs b/src/modules/agents/Examples/CodeFirstAgentExample.cs new file mode 100644 index 00000000..ad58bdda --- /dev/null +++ b/src/modules/agents/Examples/CodeFirstAgentExample.cs @@ -0,0 +1,53 @@ +using Elsa.Agents; + +namespace Elsa.Agents.Examples; + +/// +/// Example: Simple code-first agent definition +/// +public class GreeterAgent : IAgentDefinition +{ + public string Name => "GreeterAgent"; + public string Description => "Greets users warmly"; + + public AgentConfig GetAgentConfig() + { + return new AgentConfig + { + Name = Name, + Description = Description, + Services = ["OpenAIChat"], + FunctionName = "Greet", + PromptTemplate = "Greet the user by name: {{userName}}", + InputVariables = + [ + new InputVariableConfig { Name = "userName", Type = "String", Description = "User name" } + ], + OutputVariable = new OutputVariableConfig { Type = "String", Description = "Greeting" } + }; + } +} + +/// +/// Example: Multi-agent workflow definition +/// +public class ContentWorkflow : IAgentWorkflowDefinition +{ + public string Name => "ContentPipeline"; + public string Description => "Multi-agent content creation"; + + public AgentWorkflowConfig GetWorkflowConfig() + { + return new AgentWorkflowConfig + { + Name = Name, + Description = Description, + WorkflowType = AgentWorkflowType.Sequential, + Agents = ["Researcher", "Writer", "Editor"], + Services = ["OpenAIChat"], + InputVariables = [new InputVariableConfig { Name = "topic", Type = "String" }], + OutputVariable = new OutputVariableConfig { Type = "String" }, + Termination = new TerminationConfig { Type = TerminationType.MaxMessages, MaxMessages = 15 } + }; + } +} diff --git a/src/modules/agents/MIGRATION_GUIDE.md b/src/modules/agents/MIGRATION_GUIDE.md new file mode 100644 index 00000000..779cd238 --- /dev/null +++ b/src/modules/agents/MIGRATION_GUIDE.md @@ -0,0 +1,144 @@ +# Migration Guide: Microsoft Agent Framework Integration + +This guide helps you understand and adopt the new Microsoft Agent Framework features in Elsa.Agents. + +## What's New + +### 1. Microsoft Agent Framework +The module now uses Microsoft's Agent Framework (Semantic Kernel Agents) for agent execution, providing: +- Better multi-agent orchestration +- More flexible agent communication patterns +- Enhanced tool calling capabilities +- Improved state management + +### 2. Code-First Agent Registration +You can now define agents programmatically instead of only via JSON/configuration: + +```csharp +// Register a code-first agent +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentDefinition(); + }); +}); + +// Define your agent +public class MyCustomAgent : IAgentDefinition +{ + public string Name => "MyAgent"; + public string Description => "My custom agent"; + + public AgentConfig GetAgentConfig() + { + return new AgentConfig { /* ... */ }; + } +} +``` + +### 3. Multi-Agent Workflows +Create workflows where multiple agents collaborate: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentWorkflowDefinition(); + }); +}); + +public class ContentPipeline : IAgentWorkflowDefinition +{ + public AgentWorkflowConfig GetWorkflowConfig() + { + return new AgentWorkflowConfig + { + WorkflowType = AgentWorkflowType.Sequential, + Agents = ["Researcher", "Writer", "Editor"], + Termination = new TerminationConfig + { + Type = TerminationType.MaxMessages, + MaxMessages = 20 + } + }; + } +} +``` + +## Backward Compatibility + +### No Breaking Changes +All existing functionality continues to work: +- JSON-defined agents ✓ +- Database-persisted agents ✓ +- Configuration-based agents ✓ +- Existing activity providers ✓ + +### Legacy Mode +The `AgentInvoker` supports both legacy and new execution modes: + +```csharp +// New Agent Framework mode (default) +await agentInvoker.InvokeAgentAsync(agentName, input, cancellationToken); + +// Legacy Semantic Kernel mode (if needed) +await agentInvoker.InvokeAgentAsync(agentName, input, useAgentFramework: false, cancellationToken); +``` + +## Migration Strategies + +### Strategy 1: No Migration Needed +If you're happy with your current setup, do nothing. Everything continues to work as before. + +### Strategy 2: Gradual Adoption +1. Keep existing JSON/DB agents +2. Add new agents using code-first approach +3. Both coexist seamlessly + +### Strategy 3: Full Migration +1. Keep JSON agents for configuration +2. Convert complex agents to code-first for better maintainability +3. Create multi-agent workflows for advanced scenarios + +## New Capabilities + +### Workflow Types +- **Sequential**: Agents execute in order +- **Graph**: Custom orchestration with agent selection + +### Termination Strategies +- **MaxMessages**: Stop after N turns +- **Keyword**: Stop when pattern detected +- **AgentDecision**: Let agent decide when done + +### Selection Strategies +- **Sequential**: Fixed order +- **RoundRobin**: Cycle through agents +- **LLMBased**: AI decides next agent +- **AgentBased**: Dedicated selector agent + +## Examples + +See the following files for complete examples: +- `README.md` - Comprehensive documentation +- `Examples/CodeFirstAgentExample.cs` - Code samples +- `Assets/*.json` - JSON configuration examples + +## Getting Help + +If you encounter issues: +1. Check the README.md for detailed documentation +2. Review the examples in `Examples/` directory +3. Ensure all agents are properly registered +4. Verify configuration is valid + +## Testing + +The module includes comprehensive tests: +```bash +dotnet test test/modules/agents/Elsa.Agents.Tests/ +``` + +All tests passing: 6/6 ✓ diff --git a/src/modules/agents/README.md b/src/modules/agents/README.md new file mode 100644 index 00000000..9f5337c4 --- /dev/null +++ b/src/modules/agents/README.md @@ -0,0 +1,444 @@ +# Elsa Agents Module + +The Elsa Agents module provides an agentic framework built on top of Microsoft's Agent Framework (Semantic Kernel Agents), enabling you to define and execute AI agents and multi-agent workflows as Elsa workflow activities. + +## Features + +- **JSON/Configuration-Based Agents**: Define agents via configuration (appsettings.json, database) +- **Code-First Agents**: Register agents programmatically using fluent APIs +- **Multi-Agent Workflows**: Orchestrate multiple agents working together (sequential, graph-based) +- **Workflow Integration**: All agents and agent workflows are exposed as Elsa workflow activities +- **Tool Calling**: Agents can invoke tools/plugins and other agents +- **Memory and State**: Support for persistent conversations and state management + +## Architecture + +The module consists of several packages: + +- **Elsa.Agents.Core**: Core services, abstractions, and Agent Framework integration +- **Elsa.Agents.Models**: Configuration models and data contracts +- **Elsa.Agents.Activities**: Workflow activity implementations and providers +- **Elsa.Agents.Api**: REST API endpoints for agent management +- **Elsa.Agents.Persistence**: Database persistence abstractions +- **Elsa.Studio.Agents**: Blazor UI for managing agents + +## Getting Started + +### Installation + +```bash +dotnet add package Elsa.Agents.Core +dotnet add package Elsa.Agents.Activities +``` + +### Configuration + +Add agents to your Elsa configuration: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + // Register service providers (OpenAI, Azure OpenAI, etc.) + agents.UseOpenAI(); + + // Enable agent activities + elsa.UseAgentActivities(); + }); +}); +``` + +## Defining Agents + +### 1. JSON/Configuration-Based Agents + +Define agents in `appsettings.json`: + +```json +{ + "Agents": { + "ApiKeys": [ + { + "Name": "OpenAI", + "Value": "sk-..." + } + ], + "Services": [ + { + "Name": "OpenAIChat", + "Type": "OpenAIChatCompletion", + "ApiKeyName": "OpenAI", + "Model": "gpt-4" + } + ], + "Agents": [ + { + "Name": "CustomerSupport", + "Description": "Helpful customer support agent", + "Services": ["OpenAIChat"], + "FunctionName": "HandleCustomerQuery", + "PromptTemplate": "You are a helpful customer support agent. Help the user with their question: {{question}}", + "InputVariables": [ + { + "Name": "question", + "Type": "String", + "Description": "The customer's question" + } + ], + "OutputVariable": { + "Type": "String", + "Description": "The agent's response" + }, + "Plugins": ["WebSearch", "KnowledgeBase"] + } + ] + } +} +``` + +### 2. Code-First Agents + +Register agents programmatically: + +```csharp +// Simple agent definition +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentDefinition(new SimpleAgentDefinition + { + Name = "GreeterAgent", + Description = "Greets users warmly", + AgentConfig = new AgentConfig + { + Name = "GreeterAgent", + Description = "Greets users warmly", + Services = ["OpenAIChat"], + FunctionName = "Greet", + PromptTemplate = "Greet the user by name: {{userName}}", + InputVariables = + [ + new InputVariableConfig + { + Name = "userName", + Type = "String", + Description = "The user's name" + } + ], + OutputVariable = new OutputVariableConfig + { + Type = "String", + Description = "Greeting message" + } + } + }); + }); +}); + +// Or implement IAgentDefinition +public class GreeterAgentDefinition : IAgentDefinition +{ + public string Name => "GreeterAgent"; + public string Description => "Greets users warmly"; + + public AgentConfig GetAgentConfig() + { + return new AgentConfig + { + Name = Name, + Description = Description, + Services = ["OpenAIChat"], + FunctionName = "Greet", + PromptTemplate = "Greet the user by name: {{userName}}", + InputVariables = + [ + new InputVariableConfig + { + Name = "userName", + Type = "String", + Description = "The user's name" + } + ], + OutputVariable = new OutputVariableConfig + { + Type = "String", + Description = "Greeting message" + } + }; + } +} + +// Register it +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentDefinition(); + }); +}); +``` + +### 3. Agent Workflows (Multi-Agent Teams) + +Create workflows of multiple agents: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentWorkflowDefinition(new AgentWorkflowDefinition + { + Name = "ContentPipeline", + Description = "Multi-agent content creation pipeline", + WorkflowConfig = new AgentWorkflowConfig + { + Name = "ContentPipeline", + Description = "Multi-agent content creation pipeline", + WorkflowType = AgentWorkflowType.Sequential, + Agents = ["Researcher", "Writer", "Editor", "SEOSpecialist"], + Services = ["OpenAIChat"], + InputVariables = + [ + new InputVariableConfig + { + Name = "topic", + Type = "String", + Description = "Content topic" + } + ], + OutputVariable = new OutputVariableConfig + { + Type = "String", + Description = "Final content" + }, + Termination = new TerminationConfig + { + Type = TerminationType.MaxMessages, + MaxMessages = 20 + }, + SelectionStrategy = new SelectionStrategyConfig + { + Type = SelectionStrategyType.Sequential + } + } + }); + }); +}); +``` + +#### Workflow Types + +- **Sequential**: Agents execute in order +- **Graph**: Custom orchestration with agent selection strategies + +#### Termination Strategies + +- **MaxMessages**: Stop after N messages/turns +- **Keyword**: Terminate when specific keyword is detected +- **AgentDecision**: Let a designated agent decide when to stop + +#### Selection Strategies + +- **Sequential**: Agents act in order +- **RoundRobin**: Cycle through agents +- **LLMBased**: Use LLM to decide next agent +- **AgentBased**: Dedicated agent makes selection + +## Using Agents in Workflows + +Once registered, agents automatically appear as activities in the Elsa workflow designer: + +### Categories +- **Agents**: Individual agent activities +- **Agent Workflows**: Multi-agent workflow activities + +### Activity Inputs/Outputs +Each agent activity has inputs and outputs based on its configuration: + +``` +CustomerSupport Activity: + Inputs: + - question (String): The customer's question + Outputs: + - Output (String): The agent's response +``` + +## Agent Framework Integration + +The module uses Microsoft's Agent Framework (Semantic Kernel Agents) for execution: + +### Key Components + +- **AgentFrameworkFactory**: Creates Agent Framework agents from Elsa configs +- **AgentWorkflowExecutor**: Orchestrates multi-agent workflows +- **AgentInvoker**: Executes individual agents (supports both legacy SK and Agent Framework) + +### Backward Compatibility + +The `AgentInvoker` supports both: +- **Legacy Mode**: Original Semantic Kernel implementation +- **Agent Framework Mode** (default): New Microsoft Agent Framework + +You can force legacy mode for specific scenarios: +```csharp +await agentInvoker.InvokeAgentAsync(agentName, input, useAgentFramework: false, cancellationToken); +``` + +## Tool Calling + +Agents can call: +- **Plugins**: Registered plugins (e.g., WebSearch, ImageGenerator) +- **Other Agents**: Agents can invoke other agents as tools +- **Activities**: Elsa activities exposed as agent tools + +Configure tools in agent definition: +```json +{ + "Agents": [ + { + "Name": "ResearchAgent", + "Plugins": ["WebSearch", "DocumentQuery"], + "Agents": ["FactChecker", "Summarizer"] + } + ] +} +``` + +## Service Providers + +Supported AI service providers: + +- **OpenAI**: Chat completion, embeddings, image generation +- **Azure OpenAI**: Chat completion, embeddings +- Custom providers via `IAgentServiceProvider` + +## Database Persistence + +Agents can be stored in and loaded from the database: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + // Use database provider for kernel config + agents.UseKernelConfigProvider(sp => + sp.GetRequiredService()); + }); +}); +``` + +## Examples + +See the `Elsa.Studio.Agents/Assets` directory for example agent configurations: +- `hello-world-console.json`: Simple agent example +- `customer-support.json`: Customer support agent +- `content-pipeline.json`: Multi-agent content workflow +- `document-review-process.json`: Document review workflow + +## Advanced Topics + +### Custom Service Providers + +Implement `IAgentServiceProvider`: + +```csharp +public class CustomAIProvider : IAgentServiceProvider +{ + public string Name => "CustomAI"; + + public void ConfigureKernel(KernelBuilderContext context) + { + // Configure custom AI service + context.Builder.Services.AddCustomAIChatCompletion( + context.ServiceConfig.GetSetting("ApiKey") + ); + } +} + +// Register +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentServiceProvider(); + }); +}); +``` + +### Custom Plugins + +Implement agent plugins: + +```csharp +public class WeatherPlugin +{ + [KernelFunction, Description("Gets weather for a location")] + public async Task GetWeather( + [Description("Location name")] string location) + { + // Implementation + return $"Weather in {location}: Sunny, 72°F"; + } +} + +// Register +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddPluginProvider(); + }); +}); +``` + +## API Reference + +### Core Interfaces + +- `IAgentDefinition`: Code-first agent definition +- `IAgentWorkflowDefinition`: Code-first workflow definition +- `IKernelConfigProvider`: Provides agent configuration +- `IAgentServiceProvider`: AI service provider integration + +### Extension Methods + +- `AddAgentDefinition()`: Register agent definition +- `AddAgentWorkflowDefinition()`: Register workflow definition +- `AddPluginProvider()`: Register plugin provider +- `AddAgentServiceProvider()`: Register service provider + +## Troubleshooting + +### Agent Not Appearing in Workflow Designer + +Ensure: +1. Agent is properly registered +2. AgentActivitiesFeature is enabled +3. Configuration is valid +4. Activity registry has been refreshed + +### Tool Calling Not Working + +Check: +1. Plugins are registered +2. Service provider supports function calling +3. Execution settings enable FunctionChoiceBehavior + +### Multi-Agent Workflow Issues + +Verify: +1. All referenced agents exist +2. Termination strategy is configured +3. Selection strategy is appropriate for workflow type + +## Contributing + +See the main repository CONTRIBUTING.md for contribution guidelines. + +## License + +This module is part of the Elsa Workflows project and follows the same license.