diff --git a/Directory.Build.props b/Directory.Build.props index a5553de1..06f2ec7d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -27,7 +27,7 @@ true - 3.3.3 - 3.3.2 + 3.4.0-preview.2684 + 3.4.0-preview.857 \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 12a59b8c..bfe6051f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,77 +1,101 @@ - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Elsa.Integrations.sln b/Elsa.Integrations.sln index c17ea98b..5f176d11 100644 --- a/Elsa.Integrations.sln +++ b/Elsa.Integrations.sln @@ -57,6 +57,26 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.AzureServ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.TestServer.Web", "test\component\Elsa.TestServer.Web\Elsa.TestServer.Web.csproj", "{91D7599F-B4BC-4C2E-A346-56DDCD6F9FDC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Activities", "src\Elsa.Integrations.Agents.Activities\Elsa.Integrations.Agents.Activities.csproj", "{328F9C5A-60BD-4996-9422-32637BB1A55B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Api", "src\Elsa.Integrations.Agents.Api\Elsa.Integrations.Agents.Api.csproj", "{2FB8910D-9310-4DA3-9151-80AA401134A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Core", "src\Elsa.Integrations.Agents.Core\Elsa.Integrations.Agents.Core.csproj", "{293490F1-E0A1-4067-BDEB-E6CE09649749}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Models", "src\Elsa.Integrations.Agents.Models\Elsa.Integrations.Agents.Models.csproj", "{4E6009CF-5525-4AD3-B898-205EC493FB15}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Persistence", "src\Elsa.Integrations.Agents.Persistence\Elsa.Integrations.Agents.Persistence.csproj", "{863E1AD1-7C87-48B0-8274-A7FE0FC31C60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Persistence.EntityFrameworkCore", "src\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.csproj", "{E441ED7C-A9AF-4CC6-AA4D-02ECD38075CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql", "src\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql.csproj", "{0366AA72-4E2C-4416-A3C9-BB1964EAAB89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql", "src\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql.csproj", "{59AE52F2-8F9F-44A2-92E8-ED4EEB895F31}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite", "src\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite.csproj", "{4455BAC7-FD3C-4429-A2A2-44A83A331056}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer", "src\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer\Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer.csproj", "{4955F8F7-B4D2-4695-9C29-A2DC9DDCD8E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +115,46 @@ Global {91D7599F-B4BC-4C2E-A346-56DDCD6F9FDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {91D7599F-B4BC-4C2E-A346-56DDCD6F9FDC}.Release|Any CPU.ActiveCfg = Release|Any CPU {91D7599F-B4BC-4C2E-A346-56DDCD6F9FDC}.Release|Any CPU.Build.0 = Release|Any CPU + {328F9C5A-60BD-4996-9422-32637BB1A55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {328F9C5A-60BD-4996-9422-32637BB1A55B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {328F9C5A-60BD-4996-9422-32637BB1A55B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {328F9C5A-60BD-4996-9422-32637BB1A55B}.Release|Any CPU.Build.0 = Release|Any CPU + {2FB8910D-9310-4DA3-9151-80AA401134A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FB8910D-9310-4DA3-9151-80AA401134A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FB8910D-9310-4DA3-9151-80AA401134A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FB8910D-9310-4DA3-9151-80AA401134A8}.Release|Any CPU.Build.0 = Release|Any CPU + {293490F1-E0A1-4067-BDEB-E6CE09649749}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {293490F1-E0A1-4067-BDEB-E6CE09649749}.Debug|Any CPU.Build.0 = Debug|Any CPU + {293490F1-E0A1-4067-BDEB-E6CE09649749}.Release|Any CPU.ActiveCfg = Release|Any CPU + {293490F1-E0A1-4067-BDEB-E6CE09649749}.Release|Any CPU.Build.0 = Release|Any CPU + {4E6009CF-5525-4AD3-B898-205EC493FB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E6009CF-5525-4AD3-B898-205EC493FB15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E6009CF-5525-4AD3-B898-205EC493FB15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E6009CF-5525-4AD3-B898-205EC493FB15}.Release|Any CPU.Build.0 = Release|Any CPU + {863E1AD1-7C87-48B0-8274-A7FE0FC31C60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {863E1AD1-7C87-48B0-8274-A7FE0FC31C60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {863E1AD1-7C87-48B0-8274-A7FE0FC31C60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {863E1AD1-7C87-48B0-8274-A7FE0FC31C60}.Release|Any CPU.Build.0 = Release|Any CPU + {E441ED7C-A9AF-4CC6-AA4D-02ECD38075CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E441ED7C-A9AF-4CC6-AA4D-02ECD38075CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E441ED7C-A9AF-4CC6-AA4D-02ECD38075CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E441ED7C-A9AF-4CC6-AA4D-02ECD38075CC}.Release|Any CPU.Build.0 = Release|Any CPU + {0366AA72-4E2C-4416-A3C9-BB1964EAAB89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0366AA72-4E2C-4416-A3C9-BB1964EAAB89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0366AA72-4E2C-4416-A3C9-BB1964EAAB89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0366AA72-4E2C-4416-A3C9-BB1964EAAB89}.Release|Any CPU.Build.0 = Release|Any CPU + {59AE52F2-8F9F-44A2-92E8-ED4EEB895F31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59AE52F2-8F9F-44A2-92E8-ED4EEB895F31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59AE52F2-8F9F-44A2-92E8-ED4EEB895F31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59AE52F2-8F9F-44A2-92E8-ED4EEB895F31}.Release|Any CPU.Build.0 = Release|Any CPU + {4455BAC7-FD3C-4429-A2A2-44A83A331056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4455BAC7-FD3C-4429-A2A2-44A83A331056}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4455BAC7-FD3C-4429-A2A2-44A83A331056}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4455BAC7-FD3C-4429-A2A2-44A83A331056}.Release|Any CPU.Build.0 = Release|Any CPU + {4955F8F7-B4D2-4695-9C29-A2DC9DDCD8E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4955F8F7-B4D2-4695-9C29-A2DC9DDCD8E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4955F8F7-B4D2-4695-9C29-A2DC9DDCD8E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4955F8F7-B4D2-4695-9C29-A2DC9DDCD8E2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -111,5 +171,15 @@ Global {A1ADCCC2-01DD-41BA-8A81-FAAE96EC729D} = {AF041BAE-B45A-428B-B7F5-921CCB895558} {2175629F-FA99-4840-9F12-3FE7417706B9} = {60461F37-79E1-4EBB-8742-9F6578C6A745} {91D7599F-B4BC-4C2E-A346-56DDCD6F9FDC} = {60461F37-79E1-4EBB-8742-9F6578C6A745} + {328F9C5A-60BD-4996-9422-32637BB1A55B} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {2FB8910D-9310-4DA3-9151-80AA401134A8} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {293490F1-E0A1-4067-BDEB-E6CE09649749} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {4E6009CF-5525-4AD3-B898-205EC493FB15} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {863E1AD1-7C87-48B0-8274-A7FE0FC31C60} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {E441ED7C-A9AF-4CC6-AA4D-02ECD38075CC} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {0366AA72-4E2C-4416-A3C9-BB1964EAAB89} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {59AE52F2-8F9F-44A2-92E8-ED4EEB895F31} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {4455BAC7-FD3C-4429-A2A2-44A83A331056} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} + {4955F8F7-B4D2-4695-9C29-A2DC9DDCD8E2} = {527248D6-B851-4C8D-8667-E2FB0A91DABF} EndGlobalSection EndGlobal diff --git a/NuGet.Config b/NuGet.Config index e5fb919d..e3f020ab 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -11,6 +11,7 @@ + diff --git a/src/Elsa.Integrations.Agents.Activities/Activities/AgentActivity.cs b/src/Elsa.Integrations.Agents.Activities/Activities/AgentActivity.cs new file mode 100644 index 00000000..35acc9bd --- /dev/null +++ b/src/Elsa.Integrations.Agents.Activities/Activities/AgentActivity.cs @@ -0,0 +1,63 @@ +using System.ComponentModel; +using System.Dynamic; +using System.Text.Encodings.Web; +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.Integrations.Agents.Activities.ActivityProviders; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using Elsa.Workflows.Serialization.Converters; + +namespace Elsa.Integrations.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 AgentActivity : CodeActivity +{ + private static JsonSerializerOptions? _serializerOptions; + + private static JsonSerializerOptions SerializerOptions => + _serializerOptions ??= new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), + PropertyNameCaseInsensitive = true + }.WithConverters(new ExpandoObjectConverterFactory()); + + [JsonIgnore] internal string AgentName { get; set; } = null!; + + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + 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; + functionInput[inputDescriptor.Name] = inputValue; + } + + var agentInvoker = context.GetRequiredService(); + var result = await agentInvoker.InvokeAgentAsync(AgentName, functionInput, context.CancellationToken); + var json = result.FunctionResult.GetValue(); + 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)) + outputType = typeof(ExpandoObject); + + var converterOptions = new ObjectConverterOptions(SerializerOptions); + var outputValue = json.ConvertTo(outputType, converterOptions); + var outputDescriptor = activityDescriptor.Outputs.Single(); + var output = (Output)outputDescriptor.ValueGetter(this); + context.Set(output, outputValue); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Activities/ActivityProviders/AgentActivityProvider.cs b/src/Elsa.Integrations.Agents.Activities/ActivityProviders/AgentActivityProvider.cs new file mode 100644 index 00000000..ed7cf17c --- /dev/null +++ b/src/Elsa.Integrations.Agents.Activities/ActivityProviders/AgentActivityProvider.cs @@ -0,0 +1,94 @@ +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.Integrations.Agents.Activities.ActivityProviders; + +/// +/// Provides activities for each function of registered agents. +/// +[UsedImplicitly] +public class AgentActivityProvider( + IKernelConfigProvider kernelConfigProvider, + IActivityDescriber activityDescriber, + IActivityFactory activityFactory, + 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.Job; + activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity); + + activityDescriptor.Constructor = context => + { + var activity = (AgentActivity)activityFactory.Create(typeof(AgentActivity), context); + 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/Elsa.Integrations.Agents.Activities/Elsa.Integrations.Agents.Activities.csproj b/src/Elsa.Integrations.Agents.Activities/Elsa.Integrations.Agents.Activities.csproj new file mode 100644 index 00000000..6a0aaa7d --- /dev/null +++ b/src/Elsa.Integrations.Agents.Activities/Elsa.Integrations.Agents.Activities.csproj @@ -0,0 +1,17 @@ + + + + Provides Agent activities + elsa module agents semantic kernel llm ai + + + + + + + + + + + + diff --git a/src/Elsa.Integrations.Agents.Activities/Elsa.Integrations.Agents.Activities.csproj.DotSettings b/src/Elsa.Integrations.Agents.Activities/Elsa.Integrations.Agents.Activities.csproj.DotSettings new file mode 100644 index 00000000..7230eb35 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Activities/Elsa.Integrations.Agents.Activities.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Activities/Extensions/AgentActivitiesFeatureModuleExtensions.cs b/src/Elsa.Integrations.Agents.Activities/Extensions/AgentActivitiesFeatureModuleExtensions.cs new file mode 100644 index 00000000..1040b311 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Activities/Extensions/AgentActivitiesFeatureModuleExtensions.cs @@ -0,0 +1,22 @@ +using Elsa.Extensions; +using Elsa.Features.Services; +using Elsa.Integrations.Agents.Activities.Features; +using JetBrains.Annotations; + +// ReSharper disable once CheckNamespace +namespace Elsa.Agents; + +/// +/// An extension class that installs the Agents feature. +/// +[PublicAPI] +public static class AgentActivitiesFeatureModuleExtensions +{ + /// + /// Installs the Agents feature. + /// + public static IModule UseAgentActivities(this IModule module, Action? configure = null) + { + return module.Use(configure); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Activities/Features/AgentActivitiesFeature.cs b/src/Elsa.Integrations.Agents.Activities/Features/AgentActivitiesFeature.cs new file mode 100644 index 00000000..680429db --- /dev/null +++ b/src/Elsa.Integrations.Agents.Activities/Features/AgentActivitiesFeature.cs @@ -0,0 +1,29 @@ +using Elsa.Agents.Features; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Integrations.Agents.Activities.ActivityProviders; +using Elsa.Integrations.Agents.Activities.Handlers; +using Elsa.Workflows.Management.Features; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Integrations.Agents.Activities.Features; + +/// +/// A feature that installs Semantic Kernel functionality. +/// +[DependsOn(typeof(WorkflowManagementFeature))] +[DependsOn(typeof(AgentsFeature))] +[UsedImplicitly] +public class AgentActivitiesFeature(IModule module) : FeatureBase(module) +{ + /// + public override void Apply() + { + Services + .AddActivityProvider() + .AddNotificationHandler() + ; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Activities/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Activities/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Activities/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Activities/Handlers/RefreshActivityRegistry.cs b/src/Elsa.Integrations.Agents.Activities/Handlers/RefreshActivityRegistry.cs new file mode 100644 index 00000000..137705d3 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Activities/Handlers/RefreshActivityRegistry.cs @@ -0,0 +1,21 @@ +using Elsa.Integrations.Agents.Activities.ActivityProviders; +using Elsa.Integrations.Agents.Persistence.Notifications; +using Elsa.Mediator.Contracts; +using Elsa.Workflows; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Activities.Handlers; + +[UsedImplicitly] +public class RefreshActivityRegistry(IActivityRegistry activityRegistry, AgentActivityProvider agentActivityProvider) : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler +{ + public Task HandleAsync(AgentDefinitionCreated notification, CancellationToken cancellationToken) => RefreshRegistryAsync(cancellationToken); + 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); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Elsa.Integrations.Agents.Api.csproj b/src/Elsa.Integrations.Agents.Api/Elsa.Integrations.Agents.Api.csproj new file mode 100644 index 00000000..96133af1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Elsa.Integrations.Agents.Api.csproj @@ -0,0 +1,17 @@ + + + + Provides Agents API endpoints + elsa module agents semantic kernel api llm ai + + + + + + + + + + + + diff --git a/src/Elsa.Integrations.Agents.Api/Elsa.Integrations.Agents.Api.csproj.DotSettings b/src/Elsa.Integrations.Agents.Api/Elsa.Integrations.Agents.Api.csproj.DotSettings new file mode 100644 index 00000000..6fc31774 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Elsa.Integrations.Agents.Api.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs new file mode 100644 index 00000000..723ddb05 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs @@ -0,0 +1,33 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.BulkDelete; + +/// +/// Deletes an agent. +/// +[UsedImplicitly] +public class Endpoint(IAgentManager agentManager) : ElsaEndpoint +{ + /// + public override void Configure() + { + Post("/ai/bulk-actions/agents/delete"); + ConfigurePermissions("ai/agents:delete"); + } + + /// + public override async Task ExecuteAsync(BulkDeleteRequest req, CancellationToken ct) + { + var ids = req.Ids; + var filter = new AgentDefinitionFilter + { + Ids = ids + }; + var count = await agentManager.DeleteManyAsync(filter, ct); + return new BulkDeleteResponse(count); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Create/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Create/Endpoint.cs new file mode 100644 index 00000000..742ffbfe --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Create/Endpoint.cs @@ -0,0 +1,69 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Extensions; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using Elsa.Workflows; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.Create; + +/// +/// Lists all registered agents. +/// +[UsedImplicitly] +public class Endpoint(IAgentManager agentManager, IIdentityGenerator identityGenerator) : ElsaEndpoint +{ + /// + public override void Configure() + { + Post("/ai/agents"); + ConfigurePermissions("ai/agents:write"); + } + + /// + public override async Task ExecuteAsync(AgentInputModel req, CancellationToken ct) + { + var isNameUnique = await IsNameUniqueAsync(req.Name, ct); + + if (!isNameUnique) + { + AddError("An Agent already exists with the specified name"); + await SendErrorsAsync(cancellation: ct); + return null!; + } + + var newEntity = new AgentDefinition + { + Id = identityGenerator.GenerateId(), + Name = req.Name.Trim(), + Description = req.Description.Trim(), + AgentConfig = new AgentConfig + { + Description = req.Description.Trim(), + Name = req.Name.Trim(), + Agents = req.Agents, + ExecutionSettings = req.ExecutionSettings, + InputVariables = req.InputVariables, + OutputVariable = req.OutputVariable, + Services = req.Services, + Plugins = req.Plugins, + FunctionName = req.FunctionName, + PromptTemplate = req.PromptTemplate + } + }; + + await agentManager.AddAsync(newEntity, ct); + return newEntity.ToModel(); + } + + private async Task IsNameUniqueAsync(string name, CancellationToken ct) + { + var filter = new AgentDefinitionFilter + { + Name = name + }; + return await agentManager.FindAsync(filter, ct) == null; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Delete/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Delete/Endpoint.cs new file mode 100644 index 00000000..94d0b635 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Delete/Endpoint.cs @@ -0,0 +1,33 @@ +using Elsa.Abstractions; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.Delete; + +/// +/// Deletes an Agent. +/// +[UsedImplicitly] +public class Endpoint(IAgentManager agentManager) : ElsaEndpoint +{ + /// + public override void Configure() + { + Delete("/ai/agents/{id}"); + ConfigurePermissions("ai/agents:delete"); + } + + /// + public override async Task HandleAsync(Request req, CancellationToken ct) + { + var entity = await agentManager.GetAsync(req.Id, ct); + + if(entity == null) + { + await SendNotFoundAsync(ct); + return; + } + + await agentManager.DeleteAsync(entity, ct); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Delete/Request.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Delete/Request.cs new file mode 100644 index 00000000..ede9f143 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Delete/Request.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.Delete; + +public class Request +{ + [Required] public string Id { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs new file mode 100644 index 00000000..02d10165 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs @@ -0,0 +1,27 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.GenerateUniqueName; + +/// +/// Generates a unique name for an agent. +/// +[UsedImplicitly] +public class Endpoint(IAgentManager agentManager) : ElsaEndpointWithoutRequest +{ + /// + public override void Configure() + { + Post("/ai/actions/agents/generate-unique-name"); + ConfigurePermissions("ai/agents:write"); + } + + /// + public override async Task ExecuteAsync(CancellationToken ct) + { + var newName = await agentManager.GenerateUniqueNameAsync(ct); + return new(newName); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Get/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Get/Endpoint.cs new file mode 100644 index 00000000..f37b988e --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Get/Endpoint.cs @@ -0,0 +1,35 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Extensions; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.Get; + +/// +/// Gets an agent. +/// +[UsedImplicitly] +public class Endpoint(IAgentManager agentManager) : ElsaEndpoint +{ + /// + public override void Configure() + { + Get("/ai/agents/{id}"); + ConfigurePermissions("ai/agents:read"); + } + + /// + public override async Task ExecuteAsync(Request req, CancellationToken ct) + { + var entity = await agentManager.GetAsync(req.Id, ct); + + if(entity == null) + { + await SendNotFoundAsync(ct); + return null!; + } + + return entity.ToModel(); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Get/Request.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Get/Request.cs new file mode 100644 index 00000000..7de29c92 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Get/Request.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.Get; + +public class Request +{ + [Required] public string Id { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs new file mode 100644 index 00000000..14c6df1a --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using Elsa.Abstractions; +using Elsa.Agents; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.Invoke; + +/// +/// Invokes an agent. +/// +[UsedImplicitly] +public class Execute(AgentInvoker agentInvoker) : ElsaEndpoint +{ + /// + public override void Configure() + { + Post("/ai/agents/{agent}/invoke"); + ConfigurePermissions("ai/agents:invoke"); + } + + /// + public override async Task ExecuteAsync(Request req, CancellationToken ct) + { + var result = await agentInvoker.InvokeAgentAsync(req.Agent, req.Inputs, ct).AsJsonElementAsync(); + return result; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Invoke/Request.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Invoke/Request.cs new file mode 100644 index 00000000..e4b8816c --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Invoke/Request.cs @@ -0,0 +1,7 @@ +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.Invoke; + +public class Request +{ + public string Agent { get; set; } + public IDictionary Inputs { get; set; } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs new file mode 100644 index 00000000..5a47e288 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs @@ -0,0 +1,27 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.IsUniqueName; + +/// +/// Checks if a name is unique. +/// +[UsedImplicitly] +public class Endpoint(IAgentManager agentManager) : ElsaEndpoint +{ + /// + public override void Configure() + { + Post("/ai/queries/agents/is-unique-name"); + ConfigurePermissions("ai/agents:read"); + } + + /// + public override async Task ExecuteAsync(IsUniqueNameRequest req, CancellationToken ct) + { + var isUnique = await agentManager.IsNameUniqueAsync(req.Name, req.Id, ct); + return new(isUnique); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/List/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/List/Endpoint.cs new file mode 100644 index 00000000..9d29cf60 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/List/Endpoint.cs @@ -0,0 +1,30 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Extensions; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Models; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.List; + +/// +/// Lists all agents. +/// +[UsedImplicitly] +public class Endpoint(IAgentManager agentManager) : ElsaEndpointWithoutRequest> +{ + /// + public override void Configure() + { + Get("/ai/agents"); + ConfigurePermissions("ai/agents:read"); + } + + /// + public override async Task> ExecuteAsync(CancellationToken ct) + { + var entities = await agentManager.ListAsync(ct); + var models = entities.Select(x => x.ToModel()).ToList(); + return new ListResponse(models); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Update/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Update/Endpoint.cs new file mode 100644 index 00000000..5a0a315b --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Agents/Update/Endpoint.cs @@ -0,0 +1,67 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Extensions; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Endpoints.Agents.Update; + +/// +/// Updates an agent. +/// +[UsedImplicitly] +public class Endpoint(IAgentManager agentManager) : ElsaEndpoint +{ + /// + public override void Configure() + { + Post("/ai/agents/{id}"); + ConfigurePermissions("ai/agents:write"); + } + + /// + public override async Task ExecuteAsync(AgentInputModel req, CancellationToken ct) + { + var id = Route("id")!; + var entity = await agentManager.GetAsync(id, ct); + + if (entity == null) + { + await SendNotFoundAsync(ct); + return null!; + } + + var isNameDuplicate = await IsNameDuplicateAsync(req.Name, id, ct); + + if (isNameDuplicate) + { + AddError("Another agent already exists with the specified name"); + await SendErrorsAsync(cancellation: ct); + return entity.ToModel(); + } + + entity.Name = req.Name.Trim(); + entity.Description = req.Description.Trim(); + entity.AgentConfig = new AgentConfig + { + 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, + Agents = req.Agents + }; + + await agentManager.UpdateAsync(entity, ct); + return entity.ToModel(); + } + + private async Task IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken) + { + return !await agentManager.IsNameUniqueAsync(name, id, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs new file mode 100644 index 00000000..f2ee8606 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs @@ -0,0 +1,33 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs new file mode 100644 index 00000000..5ce03549 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs @@ -0,0 +1,50 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using Elsa.Workflows; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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 SendErrorsAsync(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/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs new file mode 100644 index 00000000..1393cdbf --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs @@ -0,0 +1,33 @@ +using Elsa.Abstractions; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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 SendNotFoundAsync(ct); + return; + } + + await store.DeleteAsync(entity, ct); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs new file mode 100644 index 00000000..96a3692f --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs new file mode 100644 index 00000000..e0f6d527 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs @@ -0,0 +1,34 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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 SendNotFoundAsync(ct); + return null!; + } + + return entity.ToModel(); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Get/Request.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Get/Request.cs new file mode 100644 index 00000000..f73de159 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Get/Request.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs new file mode 100644 index 00000000..57df8674 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs @@ -0,0 +1,29 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Models; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs new file mode 100644 index 00000000..344b82b8 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs @@ -0,0 +1,61 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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 SendNotFoundAsync(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 SendErrorsAsync(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/Elsa.Integrations.Agents.Api/Endpoints/Plugins/List/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Plugins/List/Endpoint.cs new file mode 100644 index 00000000..12fbb1aa --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Plugins/List/Endpoint.cs @@ -0,0 +1,28 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Models; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs new file mode 100644 index 00000000..6f80a597 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs @@ -0,0 +1,33 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/Services/Create/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Create/Endpoint.cs new file mode 100644 index 00000000..45335f14 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Create/Endpoint.cs @@ -0,0 +1,51 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using Elsa.Workflows; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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 SendErrorsAsync(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/Elsa.Integrations.Agents.Api/Endpoints/Services/Delete/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Delete/Endpoint.cs new file mode 100644 index 00000000..a94505ea --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Delete/Endpoint.cs @@ -0,0 +1,33 @@ +using Elsa.Abstractions; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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 SendNotFoundAsync(ct); + return; + } + + await store.DeleteAsync(entity, ct); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Delete/Request.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Delete/Request.cs new file mode 100644 index 00000000..d3453e2a --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Delete/Request.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/Services/Get/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Get/Endpoint.cs new file mode 100644 index 00000000..0625e2f0 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Get/Endpoint.cs @@ -0,0 +1,34 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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 SendNotFoundAsync(ct); + return null!; + } + + return entity.ToModel(); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Get/Request.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Get/Request.cs new file mode 100644 index 00000000..e5a3577a --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Get/Request.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/Services/List/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/List/Endpoint.cs new file mode 100644 index 00000000..b9509035 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/List/Endpoint.cs @@ -0,0 +1,29 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Models; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Api/Endpoints/Services/Update/Endpoint.cs b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Update/Endpoint.cs new file mode 100644 index 00000000..4d4fa20e --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Endpoints/Services/Update/Endpoint.cs @@ -0,0 +1,61 @@ +using Elsa.Abstractions; +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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 SendNotFoundAsync(ct); + return null!; + } + + var isNameDuplicate = await IsNameDuplicateAsync(req.Name, req.Id, ct); + + if (isNameDuplicate) + { + AddError("Another service already exists with the specified name"); + await SendErrorsAsync(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/Elsa.Integrations.Agents.Api/Extensions/AgentDefinitionExtensions.cs b/src/Elsa.Integrations.Agents.Api/Extensions/AgentDefinitionExtensions.cs new file mode 100644 index 00000000..745e9bcc --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Extensions/AgentDefinitionExtensions.cs @@ -0,0 +1,26 @@ +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Entities; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +public static class AgentDefinitionExtensions +{ + public static AgentModel ToModel(this AgentDefinition agentDefinition) + { + return new AgentModel + { + Id = agentDefinition.Id, + Name = agentDefinition.Name, + Description = agentDefinition.Description, + Agents = agentDefinition.AgentConfig.Agents, + ExecutionSettings = agentDefinition.AgentConfig.ExecutionSettings, + InputVariables = agentDefinition.AgentConfig.InputVariables, + OutputVariable = agentDefinition.AgentConfig.OutputVariable, + Services = agentDefinition.AgentConfig.Services, + Plugins = agentDefinition.AgentConfig.Plugins, + FunctionName = agentDefinition.AgentConfig.FunctionName, + PromptTemplate = agentDefinition.AgentConfig.PromptTemplate + }; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Extensions/ModuleExtensions.cs b/src/Elsa.Integrations.Agents.Api/Extensions/ModuleExtensions.cs new file mode 100644 index 00000000..9ed4aed0 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Extensions/ModuleExtensions.cs @@ -0,0 +1,19 @@ +using Elsa.Features.Services; +using Elsa.Integrations.Agents.Api.Features; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Extends with methods to install Semantic Kernel API endpoints. +/// +public static class ModuleExtensions +{ + /// + /// Installs the Semantic Kernel API feature. + /// + public static IModule UseAgentsApi(this IModule module, Action? configure = null) + { + return module.Use(configure); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/Features/AgentsApiFeature.cs b/src/Elsa.Integrations.Agents.Api/Features/AgentsApiFeature.cs new file mode 100644 index 00000000..88a48166 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/Features/AgentsApiFeature.cs @@ -0,0 +1,22 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Integrations.Agents.Persistence.Features; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Api.Features; + +/// +/// A feature that installs API endpoints to interact with skilled agents. +/// +[DependsOn(typeof(AgentPersistenceFeature))] +[UsedImplicitly] +public class AgentsApiFeature(IModule module) : FeatureBase(module) +{ + /// + public override void Configure() + { + Module.AddFastEndpointsAssembly(); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Api/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Api/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Api/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Abstractions/PluginProvider.cs b/src/Elsa.Integrations.Agents.Core/Abstractions/PluginProvider.cs new file mode 100644 index 00000000..c64a1f34 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Abstractions/PluginProvider.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public abstract class PluginProvider : IPluginProvider +{ + public virtual IEnumerable GetPlugins() => []; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Contracts/IAgentServiceProvider.cs b/src/Elsa.Integrations.Agents.Core/Contracts/IAgentServiceProvider.cs new file mode 100644 index 00000000..1eb96057 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Contracts/IAgentServiceProvider.cs @@ -0,0 +1,7 @@ +namespace Elsa.Agents; + +public interface IAgentServiceProvider +{ + string Name { get; } + void ConfigureKernel(KernelBuilderContext context); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Contracts/IKernelConfigProvider.cs b/src/Elsa.Integrations.Agents.Core/Contracts/IKernelConfigProvider.cs new file mode 100644 index 00000000..a64f7411 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Contracts/IKernelConfigProvider.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public interface IKernelConfigProvider +{ + Task GetKernelConfigAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Contracts/IPluginDiscoverer.cs b/src/Elsa.Integrations.Agents.Core/Contracts/IPluginDiscoverer.cs new file mode 100644 index 00000000..5a9fa460 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Contracts/IPluginDiscoverer.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public interface IPluginDiscoverer +{ + IEnumerable GetPluginDescriptors(); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Contracts/IPluginProvider.cs b/src/Elsa.Integrations.Agents.Core/Contracts/IPluginProvider.cs new file mode 100644 index 00000000..0db6dfd7 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Contracts/IPluginProvider.cs @@ -0,0 +1,9 @@ +namespace Elsa.Agents; + +/// +/// Implementations of this interface are responsible for providing plugins. +/// +public interface IPluginProvider +{ + IEnumerable GetPlugins(); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Contracts/IServiceDiscoverer.cs b/src/Elsa.Integrations.Agents.Core/Contracts/IServiceDiscoverer.cs new file mode 100644 index 00000000..681e6fd4 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Contracts/IServiceDiscoverer.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public interface IServiceDiscoverer +{ + IEnumerable Discover(); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Elsa.Integrations.Agents.Core.csproj b/src/Elsa.Integrations.Agents.Core/Elsa.Integrations.Agents.Core.csproj new file mode 100644 index 00000000..bd669771 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Elsa.Integrations.Agents.Core.csproj @@ -0,0 +1,21 @@ + + + + Provides an agentic framework using Semantic Kernel + elsa module agents semantic kernel llm ai + Elsa.Agents + + + + + + + + + + + + + + + diff --git a/src/Elsa.Integrations.Agents.Core/Elsa.Integrations.Agents.Core.csproj.DotSettings b/src/Elsa.Integrations.Agents.Core/Elsa.Integrations.Agents.Core.csproj.DotSettings new file mode 100644 index 00000000..0eb55f59 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Elsa.Integrations.Agents.Core.csproj.DotSettings @@ -0,0 +1,9 @@ + + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Extensions/AgentConfigExtensions.cs b/src/Elsa.Integrations.Agents.Core/Extensions/AgentConfigExtensions.cs new file mode 100644 index 00000000..2a4cdc6f --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Extensions/AgentConfigExtensions.cs @@ -0,0 +1,49 @@ +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, + }; + } + + 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/Elsa.Integrations.Agents.Core/Extensions/FunctionResultExtensions.cs b/src/Elsa.Integrations.Agents.Core/Extensions/FunctionResultExtensions.cs new file mode 100644 index 00000000..ba02314f --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Extensions/FunctionResultExtensions.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using Microsoft.SemanticKernel; + +namespace Elsa.Agents; + +public static class FunctionResultExtensions +{ + public static async Task AsJsonElementAsync(this Task resultTask) + { + var result = await resultTask; + return result.FunctionResult.AsJsonElement(); + } + + public static async Task AsJsonElementAsync(this Task resultTask) + { + var result = await resultTask; + return result.AsJsonElement(); + } + + public static JsonElement AsJsonElement(this FunctionResult result) + { + var response = result.GetValue()!; + return JsonSerializer.Deserialize(response); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Extensions/ModuleExtensions.cs b/src/Elsa.Integrations.Agents.Core/Extensions/ModuleExtensions.cs new file mode 100644 index 00000000..bf521d0f --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Extensions/ModuleExtensions.cs @@ -0,0 +1,19 @@ +using Elsa.Agents.Features; +using Elsa.Features.Services; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Extends with methods to install Semantic Kernel API endpoints. +/// +public static class ModuleExtensions +{ + /// + /// Installs the Semantic Kernel API feature. + /// + public static IModule UseAgents(this IModule module, Action? configure = null) + { + return module.Use(configure); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Extensions/ServiceCollectionExtensions.cs b/src/Elsa.Integrations.Agents.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..b33fed1c --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Agents; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddPluginProvider(this IServiceCollection services) where T: class, IPluginProvider + { + return services.AddScoped(); + } + + public static IServiceCollection AddAgentServiceProvider(this IServiceCollection services) where T: class, IAgentServiceProvider + { + return services.AddScoped(); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Features/AgentsFeature.cs b/src/Elsa.Integrations.Agents.Core/Features/AgentsFeature.cs new file mode 100644 index 00000000..518d1ebd --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Features/AgentsFeature.cs @@ -0,0 +1,40 @@ +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() + .AddAgentServiceProvider() + .AddAgentServiceProvider() + ; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Core/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Models/InvokeAgentResult.cs b/src/Elsa.Integrations.Agents.Core/Models/InvokeAgentResult.cs new file mode 100644 index 00000000..ee2732b9 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Models/InvokeAgentResult.cs @@ -0,0 +1,14 @@ +using System.Text.Json; +using Microsoft.SemanticKernel; + +namespace Elsa.Agents; + +public record InvokeAgentResult(AgentConfig Function, FunctionResult FunctionResult) +{ + public object? ParseResult() + { + var targetType = Type.GetType(Function.OutputVariable.Type) ?? typeof(JsonElement); + var json = FunctionResult.GetValue(); + return JsonSerializer.Deserialize(json, targetType); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Models/KernelBuilderContext.cs b/src/Elsa.Integrations.Agents.Core/Models/KernelBuilderContext.cs new file mode 100644 index 00000000..8fe3dc15 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Models/KernelBuilderContext.cs @@ -0,0 +1,20 @@ +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/Elsa.Integrations.Agents.Core/Models/PluginDescriptor.cs b/src/Elsa.Integrations.Agents.Core/Models/PluginDescriptor.cs new file mode 100644 index 00000000..31b0c8c7 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Models/PluginDescriptor.cs @@ -0,0 +1,33 @@ +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/Elsa.Integrations.Agents.Core/Options/AgentsOptions.cs b/src/Elsa.Integrations.Agents.Core/Options/AgentsOptions.cs new file mode 100644 index 00000000..a92862a6 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Options/AgentsOptions.cs @@ -0,0 +1,8 @@ +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/Elsa.Integrations.Agents.Core/Plugins/ImageGeneratorPlugin.cs b/src/Elsa.Integrations.Agents.Core/Plugins/ImageGeneratorPlugin.cs new file mode 100644 index 00000000..a4d62975 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Plugins/ImageGeneratorPlugin.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using JetBrains.Annotations; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.TextToImage; + +#pragma warning disable SKEXP0001 + +namespace Elsa.Agents.Plugins; + +[Description("Generates an image from text")] +[UsedImplicitly] +public class ImageGeneratorPlugin +{ + [KernelFunction("generate_image_from_text")] + [Description("Generates an image from text")] + [return: Description("The URL to the generated image")] + public async Task GenerateImage( + Kernel kernel, + [Description("The text to generate an image from")] + string description, + [Description("The width of the image to generate. When not specified, a default size will be used.")] + int width = 1024, + [Description("The height of the image to generate. When not specified, a default size will be used.")] + int height = 1024) + { + var dallE = kernel.GetRequiredService(); + var imageUrl = await dallE.GenerateImageAsync(description.Trim(), width, height); + return imageUrl; + } +} + +[UsedImplicitly] +public class ImageGeneratorPluginProvider : PluginProvider +{ + public override IEnumerable GetPlugins() + { + yield return PluginDescriptor.From(); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs b/src/Elsa.Integrations.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs new file mode 100644 index 00000000..6af538fe --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs @@ -0,0 +1,14 @@ +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/Elsa.Integrations.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs b/src/Elsa.Integrations.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs new file mode 100644 index 00000000..b20aba85 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs @@ -0,0 +1,16 @@ +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/Elsa.Integrations.Agents.Core/Services/AgentInvoker.cs b/src/Elsa.Integrations.Agents.Core/Services/AgentInvoker.cs new file mode 100644 index 00000000..a40458c5 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Services/AgentInvoker.cs @@ -0,0 +1,55 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +#pragma warning disable SKEXP0010 +#pragma warning disable SKEXP0001 + +namespace Elsa.Agents; + +public class AgentInvoker(KernelFactory kernelFactory, IKernelConfigProvider kernelConfigProvider) +{ + public async Task InvokeAgentAsync(string agentName, IDictionary input, CancellationToken cancellationToken = default) + { + var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); + var kernel = kernelFactory.CreateKernel(kernelConfig, agentName); + var agentConfig = kernelConfig.Agents[agentName]; + var executionSettings = agentConfig.ExecutionSettings; + var promptExecutionSettings = new OpenAIPromptExecutionSettings + { + Temperature = executionSettings.Temperature, + TopP = executionSettings.TopP, + MaxTokens = executionSettings.MaxTokens, + PresencePenalty = executionSettings.PresencePenalty, + FrequencyPenalty = executionSettings.FrequencyPenalty, + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, + ResponseFormat = executionSettings.ResponseFormat, + ChatSystemPrompt = agentConfig.PromptTemplate, + }; + + var promptExecutionSettingsDictionary = new Dictionary + { + [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 kernelFunction = kernel.CreateFunctionFromPrompt(promptTemplateConfig); + var kernelArguments = new KernelArguments(input); + var result = await kernelFunction.InvokeAsync(kernel, kernelArguments, cancellationToken: cancellationToken); + return new(agentConfig, result); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Services/ConfigurationKernelConfigProvider.cs b/src/Elsa.Integrations.Agents.Core/Services/ConfigurationKernelConfigProvider.cs new file mode 100644 index 00000000..e4320df2 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Services/ConfigurationKernelConfigProvider.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Options; + +namespace Elsa.Agents; + +[UsedImplicitly] +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; + return Task.FromResult(kernelConfig); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Core/Services/KernelFactory.cs b/src/Elsa.Integrations.Agents.Core/Services/KernelFactory.cs new file mode 100644 index 00000000..bdc738f4 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Services/KernelFactory.cs @@ -0,0 +1,113 @@ +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) +{ + 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/Elsa.Integrations.Agents.Core/Services/PluginDiscoverer.cs b/src/Elsa.Integrations.Agents.Core/Services/PluginDiscoverer.cs new file mode 100644 index 00000000..fa645195 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Services/PluginDiscoverer.cs @@ -0,0 +1,9 @@ +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/Elsa.Integrations.Agents.Core/Services/ServiceDiscoverer.cs b/src/Elsa.Integrations.Agents.Core/Services/ServiceDiscoverer.cs new file mode 100644 index 00000000..46f18c6e --- /dev/null +++ b/src/Elsa.Integrations.Agents.Core/Services/ServiceDiscoverer.cs @@ -0,0 +1,9 @@ +namespace Elsa.Agents; + +public class ServiceDiscoverer(IEnumerable providers) : IServiceDiscoverer +{ + public IEnumerable Discover() + { + return providers; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Agents/AgentInputModel.cs b/src/Elsa.Integrations.Agents.Models/Agents/AgentInputModel.cs new file mode 100644 index 00000000..b5c003ed --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Agents/AgentInputModel.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Agents; + +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 Agents { get; set; } = []; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Agents/AgentModel.cs b/src/Elsa.Integrations.Agents.Models/Agents/AgentModel.cs new file mode 100644 index 00000000..6adfd629 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Agents/AgentModel.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Agents; + +public class AgentModel : AgentInputModel +{ + [Required] public string Id { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/ApiKeys/ApiKeyInputModel.cs b/src/Elsa.Integrations.Agents.Models/ApiKeys/ApiKeyInputModel.cs new file mode 100644 index 00000000..6a2ad23d --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/ApiKeys/ApiKeyInputModel.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Agents; + +public class ApiKeyInputModel +{ + [Required] public string Name { get; set; } = null!; + [Required] public string Value { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/ApiKeys/ApiKeyModel.cs b/src/Elsa.Integrations.Agents.Models/ApiKeys/ApiKeyModel.cs new file mode 100644 index 00000000..678522eb --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/ApiKeys/ApiKeyModel.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Agents; + +public class ApiKeyModel : ApiKeyInputModel +{ + [Required] public string Id { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/BulkActions/BulkDeleteRequest.cs b/src/Elsa.Integrations.Agents.Models/BulkActions/BulkDeleteRequest.cs new file mode 100644 index 00000000..45a99951 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/BulkActions/BulkDeleteRequest.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Agents; + +public class BulkDeleteRequest +{ + [Required] public ICollection Ids { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/BulkActions/BulkDeleteResponse.cs b/src/Elsa.Integrations.Agents.Models/BulkActions/BulkDeleteResponse.cs new file mode 100644 index 00000000..5ad391ab --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/BulkActions/BulkDeleteResponse.cs @@ -0,0 +1,3 @@ +namespace Elsa.Agents; + +public record BulkDeleteResponse(long DeletedCount); \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Configs/AgentConfig.cs b/src/Elsa.Integrations.Agents.Models/Configs/AgentConfig.cs new file mode 100644 index 00000000..6257ae12 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Configs/AgentConfig.cs @@ -0,0 +1,17 @@ +namespace Elsa.Agents; + +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 Agents { get; set; } = []; + + +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Configs/ApiKeyConfig.cs b/src/Elsa.Integrations.Agents.Models/Configs/ApiKeyConfig.cs new file mode 100644 index 00000000..88b4e733 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Configs/ApiKeyConfig.cs @@ -0,0 +1,10 @@ +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/Elsa.Integrations.Agents.Models/Configs/ExecutionSettingsConfig.cs b/src/Elsa.Integrations.Agents.Models/Configs/ExecutionSettingsConfig.cs new file mode 100644 index 00000000..bdb512b7 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Configs/ExecutionSettingsConfig.cs @@ -0,0 +1,13 @@ +#pragma warning disable SKEXP0010 + +namespace Elsa.Agents; + +public class ExecutionSettingsConfig +{ + public int? MaxTokens { get; set; } + public double Temperature { get; set; } + public double TopP { get; set; } + public double PresencePenalty { get; set; } + public double FrequencyPenalty { get; set; } + public string? ResponseFormat { get; set; } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Configs/FunctionConfig.cs b/src/Elsa.Integrations.Agents.Models/Configs/FunctionConfig.cs new file mode 100644 index 00000000..a4b3d05b --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Configs/FunctionConfig.cs @@ -0,0 +1,7 @@ +namespace Elsa.Agents; + +public class FunctionConfig +{ + + +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Configs/InputVariableConfig.cs b/src/Elsa.Integrations.Agents.Models/Configs/InputVariableConfig.cs new file mode 100644 index 00000000..4e0faaaa --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Configs/InputVariableConfig.cs @@ -0,0 +1,8 @@ +namespace Elsa.Agents; + +public class InputVariableConfig +{ + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public string Type { get; set; } = ""; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Configs/KernelConfig.cs b/src/Elsa.Integrations.Agents.Models/Configs/KernelConfig.cs new file mode 100644 index 00000000..75c28527 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Configs/KernelConfig.cs @@ -0,0 +1,8 @@ +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/Elsa.Integrations.Agents.Models/Configs/OutputVariableConfig.cs b/src/Elsa.Integrations.Agents.Models/Configs/OutputVariableConfig.cs new file mode 100644 index 00000000..e9f4fe1c --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Configs/OutputVariableConfig.cs @@ -0,0 +1,7 @@ +namespace Elsa.Agents; + +public class OutputVariableConfig +{ + public string Description { get; set; } = ""; + public string Type { get; set; } = ""; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Configs/ServiceConfig.cs b/src/Elsa.Integrations.Agents.Models/Configs/ServiceConfig.cs new file mode 100644 index 00000000..4ad83b92 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Configs/ServiceConfig.cs @@ -0,0 +1,8 @@ +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/Elsa.Integrations.Agents.Models/Elsa.Integrations.Agents.Models.csproj b/src/Elsa.Integrations.Agents.Models/Elsa.Integrations.Agents.Models.csproj new file mode 100644 index 00000000..08a821df --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Elsa.Integrations.Agents.Models.csproj @@ -0,0 +1,9 @@ + + + + Provides models that can be shared between the API and API clients + elsa module agents semantic kernel llm ai api + Elsa.Agents + + + diff --git a/src/Elsa.Integrations.Agents.Models/Elsa.Integrations.Agents.Models.csproj.DotSettings b/src/Elsa.Integrations.Agents.Models/Elsa.Integrations.Agents.Models.csproj.DotSettings new file mode 100644 index 00000000..a9c50b1b --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Elsa.Integrations.Agents.Models.csproj.DotSettings @@ -0,0 +1,8 @@ + + True + True + True + True + True + True + True \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Models/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/Plugins/PluginDescriptor.cs b/src/Elsa.Integrations.Agents.Models/Plugins/PluginDescriptor.cs new file mode 100644 index 00000000..c8325fdf --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Plugins/PluginDescriptor.cs @@ -0,0 +1,11 @@ +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/Elsa.Integrations.Agents.Models/Services/ServiceInputModel.cs b/src/Elsa.Integrations.Agents.Models/Services/ServiceInputModel.cs new file mode 100644 index 00000000..2ffa016a --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Services/ServiceInputModel.cs @@ -0,0 +1,10 @@ +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/Elsa.Integrations.Agents.Models/Services/ServiceModel.cs b/src/Elsa.Integrations.Agents.Models/Services/ServiceModel.cs new file mode 100644 index 00000000..845c835f --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/Services/ServiceModel.cs @@ -0,0 +1,8 @@ +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/Elsa.Integrations.Agents.Models/UniqueName/GenerateUniqueNameResponse.cs b/src/Elsa.Integrations.Agents.Models/UniqueName/GenerateUniqueNameResponse.cs new file mode 100644 index 00000000..14140daf --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/UniqueName/GenerateUniqueNameResponse.cs @@ -0,0 +1,3 @@ +namespace Elsa.Agents; + +public record GenerateUniqueNameResponse(string Name); \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/UniqueName/IsUniqueNameRequest.cs b/src/Elsa.Integrations.Agents.Models/UniqueName/IsUniqueNameRequest.cs new file mode 100644 index 00000000..e7626911 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/UniqueName/IsUniqueNameRequest.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace Elsa.Agents; + +[UsedImplicitly] +public class IsUniqueNameRequest +{ + [Required] public string Name { get; set; } = null!; + public string? Id { get; set; } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Models/UniqueName/IsUniqueNameResponse.cs b/src/Elsa.Integrations.Agents.Models/UniqueName/IsUniqueNameResponse.cs new file mode 100644 index 00000000..2a36a364 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Models/UniqueName/IsUniqueNameResponse.cs @@ -0,0 +1,5 @@ +using JetBrains.Annotations; + +namespace Elsa.Agents; + +[UsedImplicitly] public record IsUniqueNameResponse(bool IsUnique); \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/AgentsMySqlProvidersExtensions.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/AgentsMySqlProvidersExtensions.cs new file mode 100644 index 00000000..a247a3b6 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/AgentsMySqlProvidersExtensions.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +// ReSharper disable once CheckNamespace +namespace Elsa.EntityFrameworkCore.Extensions; + +/// +/// Provides extensions to configure EF Core to use MySQL. +/// +public static class AgentsMySqlProvidersExtensions +{ + private static Assembly Assembly => typeof(AgentsMySqlProvidersExtensions).Assembly; + + /// + /// Configures the feature to use MySQL. + /// + public static EFCoreAgentPersistenceFeature UseMySql(this EFCoreAgentPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = null) + { + feature.UseMySql(Assembly, connectionString, options); + return feature; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/DbContextFactories.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/DbContextFactories.cs new file mode 100644 index 00000000..76ba7473 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/DbContextFactories.cs @@ -0,0 +1,15 @@ +using Elsa.EntityFrameworkCore.Abstractions; +using Elsa.EntityFrameworkCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql; + +[UsedImplicitly] +public class MySqlAgentsDbContextFactory : DesignTimeDbContextFactoryBase +{ + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaMySql(GetType().Assembly, connectionString, serverVersion: MySqlServerVersion.LatestSupportedServerVersion); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql.csproj b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql.csproj new file mode 100644 index 00000000..1b98e8e2 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql.csproj @@ -0,0 +1,19 @@ + + + + net8.0;net9.0 + Provides an EF Core migrations for MySQL for the Agents Persistence module. + elsa module agents semantic kernel llm ai persistence efcore entity framework core mysql + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/20250302144856_V3_4.Designer.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/20250302144856_V3_4.Designer.cs new file mode 100644 index 00000000..1e75e194 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/20250302144856_V3_4.Designer.cs @@ -0,0 +1,119 @@ +// +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20250302144856_V3_4")] + partial class V3_4 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Integrations.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"); + }); + + modelBuilder.Entity("Elsa.Integrations.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.Integrations.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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/20250302144856_V3_4.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/20250302144856_V3_4.cs new file mode 100644 index 00000000..828958d4 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/20250302144856_V3_4.cs @@ -0,0 +1,144 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql.Migrations +{ + /// + public partial class V3_4 : Migration + { + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; + + /// + public V3_4(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Elsa"); + + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AgentDefinitions", + 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"), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + AgentConfig = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AgentDefinitions", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + 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"), + Value = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .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"), + Type = table.Column(type: "longtext", 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") + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_AgentDefinition_Name", + schema: _schema.Schema, + table: "AgentDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_AgentDefinition_TenantId", + schema: _schema.Schema, + table: "AgentDefinitions", + column: "TenantId"); + + 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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AgentDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + } +} diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs new file mode 100644 index 00000000..ec2ad3d8 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs @@ -0,0 +1,116 @@ +// +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + partial class AgentsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Integrations.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"); + }); + + modelBuilder.Entity("Elsa.Integrations.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.Integrations.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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/efcore-3.4.sh b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/efcore-3.4.sh new file mode 100644 index 00000000..e8aa6cee --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.MySql/efcore-3.4.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_4 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/DbContextFactories.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/DbContextFactories.cs new file mode 100644 index 00000000..9070a895 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/DbContextFactories.cs @@ -0,0 +1,15 @@ +using Elsa.EntityFrameworkCore.Abstractions; +using Elsa.EntityFrameworkCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql; + +[UsedImplicitly] +public class PostgreSqlAgentsDbContextFactory : DesignTimeDbContextFactoryBase +{ + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaPostgreSql(GetType().Assembly, connectionString); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql.csproj b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql.csproj new file mode 100644 index 00000000..1e24f848 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql.csproj @@ -0,0 +1,18 @@ + + + + Provides an EF Core migrations for PostgreSQL for the Agents Persistence module. + elsa module agents semantic kernel llm ai persistence efcore entity framework core postgresql + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/20250302144916_V3_4.Designer.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/20250302144916_V3_4.Designer.cs new file mode 100644 index 00000000..bedf2012 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/20250302144916_V3_4.Designer.cs @@ -0,0 +1,119 @@ +// +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +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.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20250302144916_V3_4")] + partial class V3_4 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Integrations.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"); + }); + + modelBuilder.Entity("Elsa.Integrations.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.Integrations.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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/20250302144916_V3_4.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/20250302144916_V3_4.cs new file mode 100644 index 00000000..b50a08e3 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/20250302144916_V3_4.cs @@ -0,0 +1,124 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql.Migrations +{ + /// + public partial class V3_4 : Migration + { + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; + + /// + public V3_4(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Elsa"); + + migrationBuilder.CreateTable( + name: "AgentDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + AgentConfig = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AgentDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true) + }, + 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), + Type = table.Column(type: "text", nullable: false), + Settings = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AgentDefinition_Name", + schema: _schema.Schema, + table: "AgentDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_AgentDefinition_TenantId", + schema: _schema.Schema, + table: "AgentDefinitions", + column: "TenantId"); + + 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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AgentDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + } +} diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs new file mode 100644 index 00000000..957b4e2f --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs @@ -0,0 +1,116 @@ +// +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + partial class AgentsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Integrations.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"); + }); + + modelBuilder.Entity("Elsa.Integrations.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.Integrations.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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/PostgreSqlProvidersExtensions.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/PostgreSqlProvidersExtensions.cs new file mode 100644 index 00000000..430f3dcb --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/PostgreSqlProvidersExtensions.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +// ReSharper disable once CheckNamespace +namespace Elsa.EntityFrameworkCore.Extensions; + +/// +/// Provides extensions to configure EF Core to use SQL Server. +/// +public static class AgentsPostgreSqlProvidersExtensions +{ + private static Assembly Assembly => typeof(AgentsPostgreSqlProvidersExtensions).Assembly; + + /// + /// Configures the feature to use SQL Server. + /// + public static EFCoreAgentPersistenceFeature UsePostgreSql(this EFCoreAgentPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = null) + { + feature.UsePostgreSql(Assembly, connectionString, options); + return feature; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/efcore-3.4.sh b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/efcore-3.4.sh new file mode 100644 index 00000000..e8aa6cee --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.PostgreSql/efcore-3.4.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_4 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/DbContextFactories.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/DbContextFactories.cs new file mode 100644 index 00000000..34ae323e --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/DbContextFactories.cs @@ -0,0 +1,15 @@ +using Elsa.EntityFrameworkCore.Abstractions; +using Elsa.EntityFrameworkCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer; + +[UsedImplicitly] +public class SqlServerAgentsDbContextFactory : DesignTimeDbContextFactoryBase +{ + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaSqlServer(GetType().Assembly, connectionString); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer.csproj b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer.csproj new file mode 100644 index 00000000..79591086 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer.csproj @@ -0,0 +1,18 @@ + + + + Provides an EF Core migrations for SQL Server for the Agents Persistence module. + elsa module agents semantic kernel llm ai persistence efcore entity framework core sqlserver + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/20250302144957_V3_4.Designer.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/20250302144957_V3_4.Designer.cs new file mode 100644 index 00000000..9e6b1e39 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/20250302144957_V3_4.Designer.cs @@ -0,0 +1,119 @@ +// +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20250302144957_V3_4")] + partial class V3_4 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Integrations.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"); + }); + + modelBuilder.Entity("Elsa.Integrations.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.Integrations.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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/20250302144957_V3_4.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/20250302144957_V3_4.cs new file mode 100644 index 00000000..6e2e77f2 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/20250302144957_V3_4.cs @@ -0,0 +1,124 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer.Migrations +{ + /// + public partial class V3_4 : Migration + { + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; + + /// + public V3_4(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Elsa"); + + migrationBuilder.CreateTable( + name: "AgentDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + AgentConfig = table.Column(type: "nvarchar(max)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AgentDefinitions", x => x.Id); + }); + + 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), + Value = table.Column(type: "nvarchar(max)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true) + }, + 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), + Type = table.Column(type: "nvarchar(max)", nullable: false), + Settings = table.Column(type: "nvarchar(max)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AgentDefinition_Name", + schema: _schema.Schema, + table: "AgentDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_AgentDefinition_TenantId", + schema: _schema.Schema, + table: "AgentDefinitions", + column: "TenantId"); + + 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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AgentDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + } +} diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs new file mode 100644 index 00000000..0a8783c3 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs @@ -0,0 +1,116 @@ +// +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + partial class AgentsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Integrations.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"); + }); + + modelBuilder.Entity("Elsa.Integrations.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.Integrations.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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/SqlServerProvidersExtensions.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/SqlServerProvidersExtensions.cs new file mode 100644 index 00000000..4ded8b54 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/SqlServerProvidersExtensions.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +// ReSharper disable once CheckNamespace +namespace Elsa.EntityFrameworkCore.Extensions; + +/// +/// Provides extensions to configure EF Core to use SQL Server. +/// +public static class AgentsSqlServerProvidersExtensions +{ + private static Assembly Assembly => typeof(AgentsSqlServerProvidersExtensions).Assembly; + + /// + /// Configures the feature to use SQL Server. + /// + public static EFCoreAgentPersistenceFeature UseSqlServer(this EFCoreAgentPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = null) + { + feature.UseSqlServer(Assembly, connectionString, options); + return feature; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/efcore-3.4.sh b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/efcore-3.4.sh new file mode 100644 index 00000000..e8aa6cee --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.SqlServer/efcore-3.4.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_4 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/DbContextFactories.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/DbContextFactories.cs new file mode 100644 index 00000000..797f83e8 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/DbContextFactories.cs @@ -0,0 +1,15 @@ +using Elsa.EntityFrameworkCore.Abstractions; +using Elsa.EntityFrameworkCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite; + +[UsedImplicitly] +public class SqliteAgentsDbContextFactory : DesignTimeDbContextFactoryBase +{ + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaSqlite(GetType().Assembly, connectionString); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite.csproj b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite.csproj new file mode 100644 index 00000000..a3f647ce --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite.csproj @@ -0,0 +1,19 @@ + + + + Provides an EF Core migrations for SQLite for the Agents Persistence module. + elsa module agents semantic kernel llm ai persistence efcore entity framework core sqlite + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250302144940_V3_4.Designer.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250302144940_V3_4.Designer.cs new file mode 100644 index 00000000..942084bf --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250302144940_V3_4.Designer.cs @@ -0,0 +1,115 @@ +// +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20250302144940_V3_4")] + partial class V3_4 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.13"); + + modelBuilder.Entity("Elsa.Integrations.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"); + }); + + modelBuilder.Entity("Elsa.Integrations.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.Integrations.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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250302144940_V3_4.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250302144940_V3_4.cs new file mode 100644 index 00000000..911cfe86 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250302144940_V3_4.cs @@ -0,0 +1,124 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite.Migrations +{ + /// + public partial class V3_4 : Migration + { + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; + + /// + public V3_4(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Elsa"); + + migrationBuilder.CreateTable( + name: "AgentDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + AgentConfig = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AgentDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true) + }, + 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), + Type = table.Column(type: "TEXT", nullable: false), + Settings = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AgentDefinition_Name", + schema: _schema.Schema, + table: "AgentDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_AgentDefinition_TenantId", + schema: _schema.Schema, + table: "AgentDefinitions", + column: "TenantId"); + + 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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AgentDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + } +} diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs new file mode 100644 index 00000000..0a42dc84 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs @@ -0,0 +1,112 @@ +// +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + partial class AgentsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.13"); + + modelBuilder.Entity("Elsa.Integrations.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"); + }); + + modelBuilder.Entity("Elsa.Integrations.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.Integrations.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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs new file mode 100644 index 00000000..b3cd72c0 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +// ReSharper disable once CheckNamespace +namespace Elsa.EntityFrameworkCore.Extensions; + +/// +/// Provides extensions to configure EF Core to use Sqlite. +/// +public static class AgentsSqliteProvidersExtensions +{ + private static Assembly Assembly => typeof(AgentsSqliteProvidersExtensions).Assembly; + + /// + /// Configures the feature to use Sqlite. + /// + public static EFCoreAgentPersistenceFeature UseSqlite(this EFCoreAgentPersistenceFeature feature, string? connectionString = null, ElsaDbContextOptions? options = null) + { + feature.UseSqlite(Assembly, connectionString, options); + return feature; + } + + public static EFCoreAgentPersistenceFeature UseSqlite(this EFCoreAgentPersistenceFeature feature, Func connectionStringFunc, ElsaDbContextOptions? options = null) + { + feature.UseSqlite(Assembly, connectionStringFunc, options); + return feature; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/efcore-3.4.sh b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/efcore-3.4.sh new file mode 100644 index 00000000..e8aa6cee --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.Sqlite/efcore-3.4.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_4 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Configurations.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Configurations.cs new file mode 100644 index 00000000..d9d79dc6 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Configurations.cs @@ -0,0 +1,32 @@ +using Elsa.EntityFrameworkCore.Extensions; +using Elsa.Integrations.Agents.Persistence.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +/// +/// EF Core configuration for various entity types. +/// +public class Configurations : IEntityTypeConfiguration, IEntityTypeConfiguration, 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(); + builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(AgentDefinition)}_{nameof(AgentDefinition.Name)}"); + builder.HasIndex(x => x.TenantId).HasDatabaseName($"IX_{nameof(AgentDefinition)}_{nameof(AgentDefinition.TenantId)}"); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/DbContext.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/DbContext.cs new file mode 100644 index 00000000..d750df78 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/DbContext.cs @@ -0,0 +1,43 @@ +using Elsa.EntityFrameworkCore; +using Elsa.Integrations.Agents.Persistence.Entities; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +/// +/// DB context for the Agents module. +/// +[UsedImplicitly] +public class AgentsDbContext : ElsaDbContextBase +{ + /// + public AgentsDbContext(DbContextOptions options, IServiceProvider serviceProvider) : base(options, serviceProvider) + { + } + + /// + /// 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!; + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var configuration = new Configurations(); + modelBuilder.ApplyConfiguration(configuration); + modelBuilder.ApplyConfiguration(configuration); + modelBuilder.ApplyConfiguration(configuration); + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreAgentStore.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreAgentStore.cs new file mode 100644 index 00000000..a459a361 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreAgentStore.cs @@ -0,0 +1,54 @@ +using Elsa.EntityFrameworkCore; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +/// +/// An EF Core implementation of . +/// +[UsedImplicitly] +public class EFCoreAgentStore(EntityStore store) : IAgentStore +{ + public Task AddAsync(AgentDefinition entity, CancellationToken cancellationToken = default) + { + return store.AddAsync(entity, cancellationToken); + } + + public Task UpdateAsync(AgentDefinition entity, CancellationToken cancellationToken = default) + { + return store.UpdateAsync(entity, cancellationToken); + } + + public Task GetAsync(string id, CancellationToken cancellationToken = default) + { + var filter = new AgentDefinitionFilter + { + Id = id + }; + + return FindAsync(filter, cancellationToken); + } + + public Task FindAsync(AgentDefinitionFilter filter, CancellationToken cancellationToken = default) + { + return store.FindAsync(filter.Apply, cancellationToken); + } + + public Task> ListAsync(CancellationToken cancellationToken = default) + { + return store.ListAsync(cancellationToken); + } + + public Task DeleteAsync(AgentDefinition entity, CancellationToken cancellationToken = default) + { + return store.DeleteAsync(entity, cancellationToken); + } + + public Task DeleteManyAsync(AgentDefinitionFilter filter, CancellationToken cancellationToken = default) + { + return store.DeleteWhereAsync(filter.Apply, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreApiKeyStore.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreApiKeyStore.cs new file mode 100644 index 00000000..ec4740bd --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreApiKeyStore.cs @@ -0,0 +1,54 @@ +using Elsa.EntityFrameworkCore; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +/// +/// 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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreServiceStore.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreServiceStore.cs new file mode 100644 index 00000000..4a8f0895 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/EFCoreServiceStore.cs @@ -0,0 +1,54 @@ +using Elsa.EntityFrameworkCore; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +/// +/// 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/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.csproj b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.csproj new file mode 100644 index 00000000..2e7b7840 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore.csproj @@ -0,0 +1,16 @@ + + + + Provides an Entity Framework Core provider for the persistence layer of the Agents module + elsa module agents semantic kernel llm ai persistence efcore entity framework core + + + + + + + + + + + diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Extensions.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Extensions.cs new file mode 100644 index 00000000..c76d1dbb --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Extensions.cs @@ -0,0 +1,22 @@ +using Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; +using Elsa.Integrations.Agents.Persistence.Features; +using JetBrains.Annotations; + +// ReSharper disable once CheckNamespace +namespace Elsa.Agents; + +/// +/// Provides extensions to the feature. +/// +[PublicAPI] +public static class Extensions +{ + /// + /// Configures the to use EF Core persistence providers. + /// + public static AgentPersistenceFeature UseEntityFrameworkCore(this AgentPersistenceFeature feature, Action? configure = null) + { + feature.Module.Configure(configure); + return feature; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Feature.cs b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Feature.cs new file mode 100644 index 00000000..c8c760fa --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/Feature.cs @@ -0,0 +1,37 @@ +using Elsa.EntityFrameworkCore; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Integrations.Agents.Persistence.EntityFrameworkCore; + +/// +/// Configures the default workflow runtime to use EF Core persistence providers. +/// +[DependsOn(typeof(AgentPersistenceFeature))] +public class EFCoreAgentPersistenceFeature(IModule module) : PersistenceFeatureBase(module) +{ + /// + public override void Configure() + { + Module.Configure(feature => + { + feature + .UseApiKeyStore(sp => sp.GetRequiredService()) + .UseServiceStore(sp => sp.GetRequiredService()) + .UseAgentStore(sp => sp.GetRequiredService()); + ; + }); + } + + /// + public override void Apply() + { + base.Apply(); + AddEntityStore(); + AddEntityStore(); + AddEntityStore(); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/FodyWeavers.xml new file mode 100644 index 00000000..06ee7216 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence.EntityFrameworkCore/FodyWeavers.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Contracts/IAgentManager.cs b/src/Elsa.Integrations.Agents.Persistence/Contracts/IAgentManager.cs new file mode 100644 index 00000000..6d85291a --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Contracts/IAgentManager.cs @@ -0,0 +1,52 @@ +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; + +namespace Elsa.Integrations.Agents.Persistence.Contracts; + +public interface IAgentManager +{ + /// + /// Adds a new entity to the store. + /// + Task AddAsync(AgentDefinition entity, CancellationToken cancellationToken = default); + + /// + /// Updates the entity to the store. + /// + Task UpdateAsync(AgentDefinition 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(AgentDefinitionFilter filter, CancellationToken cancellationToken = default); + + /// + /// Gets all entities from the store. + /// + Task> ListAsync(CancellationToken cancellationToken = default); + + /// + /// Deletes the entity from the store. + /// + Task DeleteAsync(AgentDefinition entity, CancellationToken cancellationToken = default); + + /// + /// Deletes all entities from the store matching the specified filter. + /// + Task DeleteManyAsync(AgentDefinitionFilter filter, CancellationToken cancellationToken = default); + + /// + /// Generates a unique name. + /// + Task GenerateUniqueNameAsync(CancellationToken cancellationToken = default); + + /// + /// Checks if a name is unique. + /// + Task IsNameUniqueAsync(string name, string? notId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Contracts/IAgentStore.cs b/src/Elsa.Integrations.Agents.Persistence/Contracts/IAgentStore.cs new file mode 100644 index 00000000..4a329614 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Contracts/IAgentStore.cs @@ -0,0 +1,42 @@ +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; + +namespace Elsa.Integrations.Agents.Persistence.Contracts; + +public interface IAgentStore +{ + /// + /// Adds a new entity to the store. + /// + Task AddAsync(AgentDefinition entity, CancellationToken cancellationToken = default); + + /// + /// Updates the entity to the store. + /// + Task UpdateAsync(AgentDefinition 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(AgentDefinitionFilter filter, CancellationToken cancellationToken = default); + + /// + /// Gets all entities from the store. + /// + Task> ListAsync(CancellationToken cancellationToken = default); + + /// + /// Deletes the entity from the store. + /// + Task DeleteAsync(AgentDefinition entity, CancellationToken cancellationToken = default); + + /// + /// Deletes all entities from the store matching the specified filter. + /// + Task DeleteManyAsync(AgentDefinitionFilter filter, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Contracts/IApiKeyStore.cs b/src/Elsa.Integrations.Agents.Persistence/Contracts/IApiKeyStore.cs new file mode 100644 index 00000000..baa29d65 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Contracts/IApiKeyStore.cs @@ -0,0 +1,42 @@ +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Persistence/Contracts/IServiceStore.cs b/src/Elsa.Integrations.Agents.Persistence/Contracts/IServiceStore.cs new file mode 100644 index 00000000..1dc99c33 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Contracts/IServiceStore.cs @@ -0,0 +1,42 @@ +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Persistence/Elsa.Integrations.Agents.Persistence.csproj b/src/Elsa.Integrations.Agents.Persistence/Elsa.Integrations.Agents.Persistence.csproj new file mode 100644 index 00000000..9c3a6f56 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Elsa.Integrations.Agents.Persistence.csproj @@ -0,0 +1,20 @@ + + + + Provides Agents persistence services + elsa module agents semantic kernel persistence llm ai + + + + + + + + + + + + + + + diff --git a/src/Elsa.Integrations.Agents.Persistence/Elsa.Integrations.Agents.Persistence.csproj.DotSettings b/src/Elsa.Integrations.Agents.Persistence/Elsa.Integrations.Agents.Persistence.csproj.DotSettings new file mode 100644 index 00000000..d3ee2fe4 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Elsa.Integrations.Agents.Persistence.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Entities/AgentDefinition.cs b/src/Elsa.Integrations.Agents.Persistence/Entities/AgentDefinition.cs new file mode 100644 index 00000000..02e8f919 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Entities/AgentDefinition.cs @@ -0,0 +1,12 @@ +using Elsa.Agents; +using Elsa.Common.Entities; + +namespace Elsa.Integrations.Agents.Persistence.Entities; + +public class AgentDefinition : Entity +{ + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public AgentConfig AgentConfig { get; set; } = null!; + public AgentConfig ToAgentConfig() => AgentConfig; +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Entities/ApiKeyDefinition.cs b/src/Elsa.Integrations.Agents.Persistence/Entities/ApiKeyDefinition.cs new file mode 100644 index 00000000..a0088b0c --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Entities/ApiKeyDefinition.cs @@ -0,0 +1,31 @@ +using Elsa.Agents; +using Elsa.Common.Entities; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Persistence/Entities/ServiceDefinition.cs b/src/Elsa.Integrations.Agents.Persistence/Entities/ServiceDefinition.cs new file mode 100644 index 00000000..4520b5f9 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Entities/ServiceDefinition.cs @@ -0,0 +1,32 @@ +using Elsa.Agents; +using Elsa.Common.Entities; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Persistence/Extensions/ModuleExtensions.cs b/src/Elsa.Integrations.Agents.Persistence/Extensions/ModuleExtensions.cs new file mode 100644 index 00000000..ce05b394 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Extensions/ModuleExtensions.cs @@ -0,0 +1,19 @@ +using Elsa.Features.Services; +using Elsa.Integrations.Agents.Persistence.Features; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Extends with methods to install Semantic Kernel API endpoints. +/// +public static class ModuleExtensions +{ + /// + /// Installs the persistence feature for the Agents module. + /// + public static IModule UseAgentPersistence(this IModule module, Action? configure = null) + { + return module.Use(configure); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Features/AgentPersistenceFeature.cs b/src/Elsa.Integrations.Agents.Persistence/Features/AgentPersistenceFeature.cs new file mode 100644 index 00000000..ee63bd20 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Features/AgentPersistenceFeature.cs @@ -0,0 +1,59 @@ +using Elsa.Agents.Features; +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Integrations.Agents.Persistence.Features; + +[DependsOn(typeof(AgentsFeature))] +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; + return this; + } + + public override void Configure() + { + Module.UseAgents(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(); + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Filters/AgentDefinitionFilter.cs b/src/Elsa.Integrations.Agents.Persistence/Filters/AgentDefinitionFilter.cs new file mode 100644 index 00000000..92f763dd --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Filters/AgentDefinitionFilter.cs @@ -0,0 +1,20 @@ +using Elsa.Integrations.Agents.Persistence.Entities; + +namespace Elsa.Integrations.Agents.Persistence.Filters; + +public class AgentDefinitionFilter +{ + 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.Count != 0) 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/Elsa.Integrations.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs b/src/Elsa.Integrations.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs new file mode 100644 index 00000000..c92b683e --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs @@ -0,0 +1,20 @@ +using Elsa.Integrations.Agents.Persistence.Entities; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Persistence/Filters/ServiceDefinitionFilter.cs b/src/Elsa.Integrations.Agents.Persistence/Filters/ServiceDefinitionFilter.cs new file mode 100644 index 00000000..5fb9ef30 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Filters/ServiceDefinitionFilter.cs @@ -0,0 +1,20 @@ +using Elsa.Integrations.Agents.Persistence.Entities; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Persistence/FodyWeavers.xml b/src/Elsa.Integrations.Agents.Persistence/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionCreated.cs b/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionCreated.cs new file mode 100644 index 00000000..9e3027c1 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionCreated.cs @@ -0,0 +1,6 @@ +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Mediator.Contracts; + +namespace Elsa.Integrations.Agents.Persistence.Notifications; + +public record AgentDefinitionCreated(AgentDefinition AgentDefinition) : INotification; \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionDeleted.cs b/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionDeleted.cs new file mode 100644 index 00000000..49a6a64d --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionDeleted.cs @@ -0,0 +1,6 @@ +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Mediator.Contracts; + +namespace Elsa.Integrations.Agents.Persistence.Notifications; + +public record AgentDefinitionDeleted(AgentDefinition AgentDefinition) : INotification; \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionUpdated.cs b/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionUpdated.cs new file mode 100644 index 00000000..4afef48d --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionUpdated.cs @@ -0,0 +1,6 @@ +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Mediator.Contracts; + +namespace Elsa.Integrations.Agents.Persistence.Notifications; + +public record AgentDefinitionUpdated(AgentDefinition AgentDefinition) : INotification; \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionsDeletedInBulk.cs b/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionsDeletedInBulk.cs new file mode 100644 index 00000000..db6ec748 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Notifications/AgentDefinitionsDeletedInBulk.cs @@ -0,0 +1,5 @@ +using Elsa.Mediator.Contracts; + +namespace Elsa.Integrations.Agents.Persistence.Notifications; + +public record AgentDefinitionsDeletedInBulk : INotification; \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Services/AgentManager.cs b/src/Elsa.Integrations.Agents.Persistence/Services/AgentManager.cs new file mode 100644 index 00000000..396d719a --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Services/AgentManager.cs @@ -0,0 +1,77 @@ +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using Elsa.Integrations.Agents.Persistence.Notifications; +using Elsa.Mediator.Contracts; + +namespace Elsa.Integrations.Agents.Persistence; + +public class AgentManager(IAgentStore store, INotificationSender notificationSender) : IAgentManager +{ + public async Task AddAsync(AgentDefinition entity, CancellationToken cancellationToken = default) + { + await store.AddAsync(entity, cancellationToken); + await notificationSender.SendAsync(new AgentDefinitionCreated(entity), cancellationToken); + } + + public async Task UpdateAsync(AgentDefinition entity, CancellationToken cancellationToken = default) + { + await store.UpdateAsync(entity, cancellationToken); + await notificationSender.SendAsync(new AgentDefinitionUpdated(entity), cancellationToken); + } + + public Task GetAsync(string id, CancellationToken cancellationToken = default) + { + return store.GetAsync(id, cancellationToken); + } + + public Task FindAsync(AgentDefinitionFilter filter, CancellationToken cancellationToken = default) + { + return store.FindAsync(filter, cancellationToken); + } + + public Task> ListAsync(CancellationToken cancellationToken = default) + { + return store.ListAsync(cancellationToken); + } + + public async Task DeleteAsync(AgentDefinition entity, CancellationToken cancellationToken = default) + { + await store.DeleteAsync(entity, cancellationToken); + await notificationSender.SendAsync(new AgentDefinitionDeleted(entity), cancellationToken); + } + + public async Task DeleteManyAsync(AgentDefinitionFilter filter, CancellationToken cancellationToken = default) + { + var count = await store.DeleteManyAsync(filter, cancellationToken); + await notificationSender.SendAsync(new AgentDefinitionsDeletedInBulk(), cancellationToken); + return count; + } + + public async Task GenerateUniqueNameAsync(CancellationToken cancellationToken) + { + const int maxAttempts = 100; + var attempt = 0; + + while (attempt < maxAttempts) + { + var name = $"Agent {++attempt}"; + var isUnique = await IsNameUniqueAsync(name, cancellationToken: cancellationToken); + + if (isUnique) + return name; + } + + throw new Exception($"Failed to generate a unique workflow name after {maxAttempts} attempts."); + } + + public async Task IsNameUniqueAsync(string name, string? notId = null, CancellationToken cancellationToken = default) + { + var filter = new AgentDefinitionFilter + { + Name = name, + NotId = notId + }; + return await FindAsync(filter, cancellationToken) == null; + } +} \ No newline at end of file diff --git a/src/Elsa.Integrations.Agents.Persistence/Services/MemoryAgentStore.cs b/src/Elsa.Integrations.Agents.Persistence/Services/MemoryAgentStore.cs new file mode 100644 index 00000000..94fd6c34 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Services/MemoryAgentStore.cs @@ -0,0 +1,54 @@ +using Elsa.Common.Services; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.Agents.Persistence; + +[UsedImplicitly] +public class MemoryAgentStore(MemoryStore memoryStore) : IAgentStore +{ + public Task AddAsync(AgentDefinition entity, CancellationToken cancellationToken = default) + { + memoryStore.Add(entity, x => x.Id); + return Task.CompletedTask; + } + + public Task UpdateAsync(AgentDefinition 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(AgentDefinitionFilter 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(AgentDefinition entity, CancellationToken cancellationToken = default) + { + memoryStore.Delete(entity.Id); + return Task.CompletedTask; + } + + public Task DeleteManyAsync(AgentDefinitionFilter 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/Elsa.Integrations.Agents.Persistence/Services/MemoryApiKeyStore.cs b/src/Elsa.Integrations.Agents.Persistence/Services/MemoryApiKeyStore.cs new file mode 100644 index 00000000..27eea149 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Services/MemoryApiKeyStore.cs @@ -0,0 +1,54 @@ +using Elsa.Common.Services; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Persistence/Services/MemoryServiceStore.cs b/src/Elsa.Integrations.Agents.Persistence/Services/MemoryServiceStore.cs new file mode 100644 index 00000000..2693c5f9 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Services/MemoryServiceStore.cs @@ -0,0 +1,54 @@ +using Elsa.Common.Services; +using Elsa.Integrations.Agents.Persistence.Contracts; +using Elsa.Integrations.Agents.Persistence.Entities; +using Elsa.Integrations.Agents.Persistence.Filters; +using JetBrains.Annotations; + +namespace Elsa.Integrations.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/Elsa.Integrations.Agents.Persistence/Services/StoreKernelConfigProvider.cs b/src/Elsa.Integrations.Agents.Persistence/Services/StoreKernelConfigProvider.cs new file mode 100644 index 00000000..e7f45261 --- /dev/null +++ b/src/Elsa.Integrations.Agents.Persistence/Services/StoreKernelConfigProvider.cs @@ -0,0 +1,19 @@ +using Elsa.Agents; +using Elsa.Integrations.Agents.Persistence.Contracts; + +namespace Elsa.Integrations.Agents.Persistence; + +public class StoreKernelConfigProvider(IApiKeyStore apiKeyStore, IServiceStore serviceStore, 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; + } +} \ No newline at end of file diff --git a/test/component/Elsa.Integrations.AzureServiceBus.ComponentTests/Extensions/AzureServiceBusServiceCollectionExtensions.cs b/test/component/Elsa.Integrations.AzureServiceBus.ComponentTests/Extensions/AzureServiceBusServiceCollectionExtensions.cs index c83f8c4d..cec24197 100644 --- a/test/component/Elsa.Integrations.AzureServiceBus.ComponentTests/Extensions/AzureServiceBusServiceCollectionExtensions.cs +++ b/test/component/Elsa.Integrations.AzureServiceBus.ComponentTests/Extensions/AzureServiceBusServiceCollectionExtensions.cs @@ -77,6 +77,6 @@ private static ProcessMessageEventArgs CreateMessageArgs(ServiceBusMessage trans properties: props ); - return new ProcessMessageEventArgs(message, null, new CancellationToken()); + return new ProcessMessageEventArgs(message, null, CancellationToken.None); } } \ No newline at end of file diff --git a/test/component/Elsa.Integrations.AzureServiceBus.ComponentTests/Fixtures/WorkflowServer.cs b/test/component/Elsa.Integrations.AzureServiceBus.ComponentTests/Fixtures/WorkflowServer.cs index 883b7669..5ae1f131 100644 --- a/test/component/Elsa.Integrations.AzureServiceBus.ComponentTests/Fixtures/WorkflowServer.cs +++ b/test/component/Elsa.Integrations.AzureServiceBus.ComponentTests/Fixtures/WorkflowServer.cs @@ -11,6 +11,7 @@ using Elsa.MassTransit.Extensions; using Elsa.Testing.Shared.Handlers; using Elsa.Testing.Shared.Services; +using Elsa.TestServer.Web; using FluentStorage; using JetBrains.Annotations; using Microsoft.AspNetCore.Hosting; diff --git a/test/component/Elsa.TestServer.Web/Program.cs b/test/component/Elsa.TestServer.Web/Program.cs index cb64420e..5ebf09c9 100644 --- a/test/component/Elsa.TestServer.Web/Program.cs +++ b/test/component/Elsa.TestServer.Web/Program.cs @@ -17,8 +17,8 @@ .AddElsa(elsa => { elsa - .AddActivitiesFrom() - .AddWorkflowsFrom() + .AddActivitiesFrom() + .AddWorkflowsFrom() .UseIdentity(identity => { identity.TokenOptions = options => identityTokenSection.Bind(options); @@ -31,7 +31,7 @@ .UseWorkflowRuntime() .UseWorkflowsApi(api => { - api.AddFastEndpointsAssembly(); + api.AddFastEndpointsAssembly(); }) .UseJavaScript(options => { @@ -44,7 +44,7 @@ http.UseCache(); }); - ConfigureForTest?.Invoke(elsa); + Elsa.TestServer.Web.Program.ConfigureForTest?.Invoke(elsa); }); services.AddHealthChecks(); @@ -92,14 +92,17 @@ // Run. await app.RunAsync(); -/// -/// The main entry point for the application made public for end to end testing. -/// -[UsedImplicitly] -public partial class Program +namespace Elsa.TestServer.Web { /// - /// Set by the test runner to configure the module for testing. + /// The main entry point for the application made public for end to end testing. /// - public static Action? ConfigureForTest { get; set; } + [UsedImplicitly] + public partial class Program + { + /// + /// Set by the test runner to configure the module for testing. + /// + public static Action? ConfigureForTest { get; set; } + } } \ No newline at end of file diff --git a/test/unit/Elsa.Integrations.Slack.Tests/Activities/Channels/CreateChannelTests.cs b/test/unit/Elsa.Integrations.Slack.Tests/Activities/Channels/CreateChannelTests.cs index 248eaa7a..5a683bfc 100644 --- a/test/unit/Elsa.Integrations.Slack.Tests/Activities/Channels/CreateChannelTests.cs +++ b/test/unit/Elsa.Integrations.Slack.Tests/Activities/Channels/CreateChannelTests.cs @@ -1,6 +1,6 @@ using Elsa.Integrations.Slack.Activities.Channels; -namespace Elsa.Integrations.Tests.Slack.Activities.Channels; +namespace Elsa.Integrations.Slack.Tests.Activities.Channels; /// /// Contains tests for the activity.