From 8ed83103ff8817f0e381c4cb0e560cef5cf6860b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 10 Dec 2025 18:54:57 +0100 Subject: [PATCH 01/23] Update package versions in `Directory.Build.props` and `Directory.Packages.props` --- Directory.Build.props | 4 +-- Directory.Packages.props | 54 ++++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d4eaf59d..06a2783b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -38,8 +38,8 @@ - 3.6.0-preview.3976 - 3.6.0-preview.1286 + 3.6.0-preview.4036 + 3.6.0-preview.1293 diff --git a/Directory.Packages.props b/Directory.Packages.props index 07c627dd..f087e6ca 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -84,38 +84,38 @@ - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - + + + + + - - + + From c9253c943dfc4f4063610a074bdaf09357cf156c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:08:42 +0000 Subject: [PATCH 02/23] Initial plan From 5bbd847e093a398fc40c2197b42f59251d0f2aa9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:20:02 +0000 Subject: [PATCH 03/23] Add Microsoft Agent Framework integration with code-first support Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --- Directory.Packages.props | 2 + .../Activities/AgentWorkflowActivity.cs | 80 +++++++ .../AgentActivityProvider.cs | 183 ++++++++++----- .../Contracts/IAgentDefinition.cs | 22 ++ .../Contracts/IAgentDefinitionProvider.cs | 12 + .../Contracts/IAgentWorkflowDefinition.cs | 22 ++ .../IAgentWorkflowDefinitionProvider.cs | 12 + .../Elsa.Agents.Core/Elsa.Agents.Core.csproj | 2 + .../Extensions/ServiceCollectionExtensions.cs | 32 +++ .../Features/AgentsFeature.cs | 4 + .../Models/AgentWorkflowResult.cs | 31 +++ .../Services/AgentDefinitionProvider.cs | 17 ++ .../Services/AgentFrameworkFactory.cs | 114 +++++++++ .../Elsa.Agents.Core/Services/AgentInvoker.cs | 60 ++++- .../AgentWorkflowDefinitionProvider.cs | 17 ++ .../Services/AgentWorkflowExecutor.cs | 221 ++++++++++++++++++ .../ConfigurationKernelConfigProvider.cs | 34 ++- .../Configs/AgentWorkflowConfig.cs | 73 ++++++ .../Configs/KernelConfig.cs | 1 + .../Configs/SelectionStrategyConfig.cs | 48 ++++ .../Configs/TerminationConfig.cs | 48 ++++ 21 files changed, 977 insertions(+), 58 deletions(-) create mode 100644 src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinitionProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinition.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinitionProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/AgentDefinitionProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowDefinitionProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs create mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/AgentWorkflowConfig.cs create mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/SelectionStrategyConfig.cs create mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/TerminationConfig.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b67d493..8c305ae7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -125,6 +125,8 @@ + + diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs new file mode 100644 index 00000000..f5f4c8a7 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs @@ -0,0 +1,80 @@ +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.Agents.Activities.ActivityProviders; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using Elsa.Workflows.Serialization.Converters; + +namespace Elsa.Agents.Activities; + +/// +/// An activity that executes a multi-agent workflow. This is an internal activity used by . +/// +[Browsable(false)] +public class AgentWorkflowActivity : 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 WorkflowName { get; set; } = null!; + + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var activityDescriptor = context.ActivityDescriptor; + var inputDescriptors = activityDescriptor.Inputs; + var workflowInput = new Dictionary(); + + foreach (var inputDescriptor in inputDescriptors) + { + var input = (Input?)inputDescriptor.ValueGetter(this); + var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; + + if (inputValue is ExpandoObject expandoObject) + { + inputValue = expandoObject.ConvertTo(); + } + + workflowInput[inputDescriptor.Name] = inputValue; + } + + var kernelConfigProvider = context.GetRequiredService(); + var workflowExecutor = context.GetRequiredService(); + + var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(context.CancellationToken); + + if (!kernelConfig.AgentWorkflows.TryGetValue(WorkflowName, out var workflowConfig)) + throw new InvalidOperationException($"Agent workflow '{WorkflowName}' not found"); + + var result = await workflowExecutor.ExecuteWorkflowAsync(WorkflowName, workflowConfig, workflowInput, context.CancellationToken); + var json = result.Output?.Trim(); + + if (string.IsNullOrWhiteSpace(json)) + throw new InvalidOperationException("The workflow output is empty or null."); + + 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); + } +} diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs index 764d0be6..67c8ff1a 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs @@ -23,71 +23,148 @@ IWellKnownTypeRegistry wellKnownTypeRegistry 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) + // Add descriptors for individual agents + foreach (var kvp in kernelConfig.Agents) { var agentConfig = kvp.Value; - var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); - var activityTypeName = $"Elsa.Agents.{agentConfig.Name.Pascalize()}"; - activityDescriptor.Name = agentConfig.Name.Pascalize(); - activityDescriptor.TypeName = activityTypeName; - activityDescriptor.Description = agentConfig.Description; - activityDescriptor.DisplayName = agentConfig.Name.Humanize().Transform(To.TitleCase); - activityDescriptor.IsBrowsable = true; - activityDescriptor.Category = "Agents"; - activityDescriptor.Kind = ActivityKind.Task; - activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity); - - activityDescriptor.Constructor = context => + var descriptor = await CreateAgentActivityDescriptor(agentConfig, cancellationToken); + activityDescriptors.Add(descriptor); + } + + // Add descriptors for agent workflows + foreach (var kvp in kernelConfig.AgentWorkflows) + { + var workflowConfig = kvp.Value; + var descriptor = await CreateAgentWorkflowActivityDescriptor(workflowConfig, cancellationToken); + activityDescriptors.Add(descriptor); + } + + return activityDescriptors; + } + + private async Task CreateAgentActivityDescriptor(AgentConfig agentConfig, CancellationToken cancellationToken) + { + var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); + var activityTypeName = $"Elsa.Agents.{agentConfig.Name.Pascalize()}"; + activityDescriptor.Name = agentConfig.Name.Pascalize(); + activityDescriptor.TypeName = activityTypeName; + activityDescriptor.Description = agentConfig.Description; + activityDescriptor.DisplayName = agentConfig.Name.Humanize().Transform(To.TitleCase); + activityDescriptor.IsBrowsable = true; + activityDescriptor.Category = "Agents"; + activityDescriptor.Kind = ActivityKind.Task; + activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity); + + activityDescriptor.Constructor = context => + { + var activity = context.CreateActivity(); + activity.Type = activityTypeName; + activity.AgentName = agentConfig.Name; + return activity; + }; + + 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 { - var activity = context.CreateActivity(); - activity.Type = activityTypeName; - activity.AgentName = agentConfig.Name; - return activity; + 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); + } - activityDescriptors.Add(activityDescriptor); - activityDescriptor.Inputs.Clear(); + 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); - 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 + return activityDescriptor; + } + + private async Task CreateAgentWorkflowActivityDescriptor(AgentWorkflowConfig workflowConfig, CancellationToken cancellationToken) + { + var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentWorkflowActivity), cancellationToken); + var activityTypeName = $"Elsa.Agents.Workflows.{workflowConfig.Name.Pascalize()}"; + activityDescriptor.Name = workflowConfig.Name.Pascalize(); + activityDescriptor.TypeName = activityTypeName; + activityDescriptor.Description = workflowConfig.Description; + activityDescriptor.DisplayName = workflowConfig.Name.Humanize().Transform(To.TitleCase); + activityDescriptor.IsBrowsable = true; + activityDescriptor.Category = "Agent Workflows"; + activityDescriptor.Kind = ActivityKind.Task; + activityDescriptor.CustomProperties["RootType"] = nameof(AgentWorkflowActivity); + + activityDescriptor.Constructor = context => + { + var activity = context.CreateActivity(); + activity.Type = activityTypeName; + activity.WorkflowName = workflowConfig.Name; + return activity; + }; + + activityDescriptor.Inputs.Clear(); + + foreach (var inputVariable in workflowConfig.InputVariables) + { + var inputName = inputVariable.Name; + var inputType = inputVariable.Type == null! ? "object" : inputVariable.Type; + var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType); + var inputDescriptor = new InputDescriptor { - Name = outputName, - Description = agentConfig.OutputVariable.Description, - Type = nakedOutputType, + 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, - ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), - ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, + IsWrapped = true, + UIHint = ActivityDescriber.GetUIHint(nakedInputType) }; - activityDescriptor.Outputs.Add(outputDescriptor); + activityDescriptor.Inputs.Add(inputDescriptor); } - return activityDescriptors; + activityDescriptor.Outputs.Clear(); + var outputVariable = workflowConfig.OutputVariable; + var outputType = outputVariable.Type == null! ? "object" : outputVariable.Type; + var nakedOutputType = wellKnownTypeRegistry.GetTypeOrDefault(outputType); + var outputName = "Output"; + var outputDescriptor = new OutputDescriptor + { + Name = outputName, + Description = workflowConfig.OutputVariable.Description, + Type = nakedOutputType, + IsSynthetic = true, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), + ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, + }; + activityDescriptor.Outputs.Add(outputDescriptor); + + return activityDescriptor; } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs new file mode 100644 index 00000000..53112fa9 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs @@ -0,0 +1,22 @@ +namespace Elsa.Agents; + +/// +/// Represents a code-first agent definition that can be registered programmatically. +/// +public interface IAgentDefinition +{ + /// + /// Gets the unique name of the agent. + /// + string Name { get; } + + /// + /// Gets the description of the agent. + /// + string Description { get; } + + /// + /// Gets the agent configuration. + /// + AgentConfig GetAgentConfig(); +} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinitionProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinitionProvider.cs new file mode 100644 index 00000000..c53ab9fb --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinitionProvider.cs @@ -0,0 +1,12 @@ +namespace Elsa.Agents; + +/// +/// Provides access to registered agent definitions. +/// +public interface IAgentDefinitionProvider +{ + /// + /// Gets all registered agent definitions. + /// + IEnumerable GetDefinitions(); +} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinition.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinition.cs new file mode 100644 index 00000000..a3f2d239 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinition.cs @@ -0,0 +1,22 @@ +namespace Elsa.Agents; + +/// +/// Represents a code-first multi-agent workflow (agent team/sequence/graph) that can be registered programmatically. +/// +public interface IAgentWorkflowDefinition +{ + /// + /// Gets the unique name of the agent workflow. + /// + string Name { get; } + + /// + /// Gets the description of the agent workflow. + /// + string Description { get; } + + /// + /// Gets the agent workflow configuration. + /// + AgentWorkflowConfig GetWorkflowConfig(); +} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinitionProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinitionProvider.cs new file mode 100644 index 00000000..8ecf1e22 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinitionProvider.cs @@ -0,0 +1,12 @@ +namespace Elsa.Agents; + +/// +/// Provides access to registered agent workflow definitions. +/// +public interface IAgentWorkflowDefinitionProvider +{ + /// + /// Gets all registered agent workflow definitions. + /// + IEnumerable GetDefinitions(); +} diff --git a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj index df449170..9b65d3c5 100644 --- a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj +++ b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj @@ -11,6 +11,8 @@ + + diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs index b33fed1c..e93e28ec 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs @@ -13,4 +13,36 @@ public static IServiceCollection AddAgentServiceProvider(this IServiceCollect { return services.AddScoped(); } + + /// + /// Registers a code-first agent definition. + /// + public static IServiceCollection AddAgentDefinition(this IServiceCollection services) where T : class, IAgentDefinition + { + return services.AddSingleton(); + } + + /// + /// Registers a code-first agent definition instance. + /// + public static IServiceCollection AddAgentDefinition(this IServiceCollection services, IAgentDefinition agentDefinition) + { + return services.AddSingleton(agentDefinition); + } + + /// + /// Registers a code-first agent workflow definition. + /// + public static IServiceCollection AddAgentWorkflowDefinition(this IServiceCollection services) where T : class, IAgentWorkflowDefinition + { + return services.AddSingleton(); + } + + /// + /// Registers a code-first agent workflow definition instance. + /// + public static IServiceCollection AddAgentWorkflowDefinition(this IServiceCollection services, IAgentWorkflowDefinition workflowDefinition) + { + return services.AddSingleton(workflowDefinition); + } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs index 77c16f83..3d06395d 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs @@ -28,8 +28,12 @@ public override void Apply() Services .AddScoped() .AddScoped() + .AddScoped() + .AddScoped() .AddScoped() .AddScoped() + .AddScoped() + .AddScoped() .AddScoped(_kernelConfigProviderFactory) .AddScoped() .AddPluginProvider() diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs new file mode 100644 index 00000000..870b311b --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs @@ -0,0 +1,31 @@ +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Elsa.Agents; + +/// +/// Result of executing an agent workflow. +/// +public class AgentWorkflowResult +{ + public AgentWorkflowResult(AgentWorkflowConfig workflowConfig, string output, ChatHistory chatHistory) + { + WorkflowConfig = workflowConfig; + Output = output; + ChatHistory = chatHistory; + } + + /// + /// The workflow configuration that was executed. + /// + public AgentWorkflowConfig WorkflowConfig { get; } + + /// + /// The final output from the workflow. + /// + public string Output { get; } + + /// + /// The complete chat history from the workflow execution. + /// + public ChatHistory ChatHistory { get; } +} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentDefinitionProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentDefinitionProvider.cs new file mode 100644 index 00000000..dde21178 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentDefinitionProvider.cs @@ -0,0 +1,17 @@ +namespace Elsa.Agents; + +/// +/// Default implementation of that collects all registered agent definitions. +/// +public class AgentDefinitionProvider : IAgentDefinitionProvider +{ + private readonly IEnumerable _definitions; + + public AgentDefinitionProvider(IEnumerable definitions) + { + _definitions = definitions; + } + + /// + public IEnumerable GetDefinitions() => _definitions; +} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs new file mode 100644 index 00000000..e67e8bc7 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs @@ -0,0 +1,114 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; + +#pragma warning disable SKEXP0001 +#pragma warning disable SKEXP0010 +#pragma warning disable SKEXP0110 + +namespace Elsa.Agents; + +/// +/// Factory for creating Agent Framework agents from Elsa agent configurations. +/// +public class AgentFrameworkFactory +{ + private readonly IPluginDiscoverer _pluginDiscoverer; + private readonly IServiceDiscoverer _serviceDiscoverer; + private readonly ILoggerFactory _loggerFactory; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public AgentFrameworkFactory( + IPluginDiscoverer pluginDiscoverer, + IServiceDiscoverer serviceDiscoverer, + ILoggerFactory loggerFactory, + IServiceProvider serviceProvider, + ILogger logger) + { + _pluginDiscoverer = pluginDiscoverer; + _serviceDiscoverer = serviceDiscoverer; + _loggerFactory = loggerFactory; + _serviceProvider = serviceProvider; + _logger = logger; + } + + /// + /// Creates a ChatCompletionAgent from an Elsa agent configuration. + /// + public ChatCompletionAgent CreateAgent(KernelConfig kernelConfig, AgentConfig agentConfig) + { + var kernel = CreateKernel(kernelConfig, agentConfig); + + return new ChatCompletionAgent + { + Name = agentConfig.Name, + Description = agentConfig.Description, + Instructions = agentConfig.PromptTemplate, + Kernel = kernel + }; + } + + /// + /// Creates a Kernel configured for the specified agent. + /// + private 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); + } + + 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); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs index 907caae4..d0c614c8 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs @@ -5,12 +5,70 @@ #pragma warning disable SKEXP0010 #pragma warning disable SKEXP0001 +#pragma warning disable SKEXP0110 namespace Elsa.Agents; -public class AgentInvoker(IKernelFactory kernelFactory, IKernelConfigProvider kernelConfigProvider) +public class AgentInvoker( + IKernelFactory kernelFactory, + IKernelConfigProvider kernelConfigProvider, + AgentFrameworkFactory agentFrameworkFactory) { + /// + /// Invokes an agent using the Microsoft Agent Framework (new approach). + /// public async Task InvokeAgentAsync(string agentName, IDictionary input, CancellationToken cancellationToken = default) + { + return await InvokeAgentAsync(agentName, input, useAgentFramework: true, cancellationToken); + } + + /// + /// Invokes an agent with option to use legacy Semantic Kernel or new Agent Framework. + /// + public async Task InvokeAgentAsync(string agentName, IDictionary input, bool useAgentFramework, CancellationToken cancellationToken = default) + { + if (useAgentFramework) + return await InvokeAgentWithFrameworkAsync(agentName, input, cancellationToken); + else + return await InvokeAgentLegacyAsync(agentName, input, cancellationToken); + } + + private async Task InvokeAgentWithFrameworkAsync(string agentName, IDictionary input, CancellationToken cancellationToken) + { + var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); + var agentConfig = kernelConfig.Agents[agentName]; + + // Create agent using Agent Framework + var agent = agentFrameworkFactory.CreateAgent(kernelConfig, agentConfig); + + // Create chat history + ChatHistory chatHistory = []; + + // Format and add user input + var promptTemplateConfig = new PromptTemplateConfig + { + Template = agentConfig.PromptTemplate, + TemplateFormat = "handlebars", + Name = agentConfig.FunctionName + }; + + var templateFactory = new HandlebarsPromptTemplateFactory(); + var promptTemplate = templateFactory.Create(promptTemplateConfig); + var kernelArguments = new KernelArguments(input); + string renderedPrompt = await promptTemplate.RenderAsync(agent.Kernel, kernelArguments); + + chatHistory.AddUserMessage(renderedPrompt); + + // Get response from agent + var response = await agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken).LastOrDefaultAsync(cancellationToken); + + if (response == null) + throw new InvalidOperationException("Agent did not produce a response"); + + return new InvokeAgentResult(agentConfig, response); + } + + private async Task InvokeAgentLegacyAsync(string agentName, IDictionary input, CancellationToken cancellationToken) { var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); var kernel = kernelFactory.CreateKernel(kernelConfig, agentName); diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowDefinitionProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowDefinitionProvider.cs new file mode 100644 index 00000000..8de63c59 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowDefinitionProvider.cs @@ -0,0 +1,17 @@ +namespace Elsa.Agents; + +/// +/// Default implementation of that collects all registered agent workflow definitions. +/// +public class AgentWorkflowDefinitionProvider : IAgentWorkflowDefinitionProvider +{ + private readonly IEnumerable _definitions; + + public AgentWorkflowDefinitionProvider(IEnumerable definitions) + { + _definitions = definitions; + } + + /// + public IEnumerable GetDefinitions() => _definitions; +} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs new file mode 100644 index 00000000..25c03815 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs @@ -0,0 +1,221 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.ChatCompletion; + +#pragma warning disable SKEXP0001 +#pragma warning disable SKEXP0010 +#pragma warning disable SKEXP0110 + +namespace Elsa.Agents; + +/// +/// Executes multi-agent workflows using the Agent Framework. +/// +public class AgentWorkflowExecutor +{ + private readonly AgentFrameworkFactory _agentFactory; + private readonly IKernelConfigProvider _kernelConfigProvider; + + public AgentWorkflowExecutor( + AgentFrameworkFactory agentFactory, + IKernelConfigProvider kernelConfigProvider) + { + _agentFactory = agentFactory; + _kernelConfigProvider = kernelConfigProvider; + } + + /// + /// Executes an agent workflow and returns the result. + /// + public async Task ExecuteWorkflowAsync( + string workflowName, + AgentWorkflowConfig workflowConfig, + IDictionary input, + CancellationToken cancellationToken = default) + { + var kernelConfig = await _kernelConfigProvider.GetKernelConfigAsync(cancellationToken); + + // Create agents for the workflow + var agents = new List(); + foreach (var agentName in workflowConfig.Agents) + { + if (!kernelConfig.Agents.TryGetValue(agentName, out var agentConfig)) + continue; + + var agent = _agentFactory.CreateAgent(kernelConfig, agentConfig); + agents.Add(agent); + } + + if (agents.Count == 0) + throw new InvalidOperationException($"No agents found for workflow '{workflowName}'"); + + // Create chat history with input + ChatHistory chatHistory = []; + + // Format input as user message + var inputMessage = FormatInput(input, workflowConfig); + chatHistory.AddUserMessage(inputMessage); + + // Execute workflow based on type + var result = workflowConfig.WorkflowType switch + { + AgentWorkflowType.Sequential => await ExecuteSequentialWorkflowAsync(agents, chatHistory, workflowConfig, cancellationToken), + AgentWorkflowType.Graph => await ExecuteGraphWorkflowAsync(agents, chatHistory, workflowConfig, cancellationToken), + _ => throw new NotSupportedException($"Workflow type {workflowConfig.WorkflowType} is not supported") + }; + + return new AgentWorkflowResult(workflowConfig, result, chatHistory); + } + + private async Task ExecuteSequentialWorkflowAsync( + List agents, + ChatHistory chatHistory, + AgentWorkflowConfig config, + CancellationToken cancellationToken) + { + var agentChat = new AgentGroupChat(agents.ToArray()); + + // Configure termination + ConfigureTermination(agentChat, config); + + // Execute chat until termination + await foreach (var message in agentChat.InvokeAsync(cancellationToken)) + { + chatHistory.Add(message); + } + + // Return the last assistant message + var lastMessage = chatHistory.LastOrDefault(m => m.Role == AuthorRole.Assistant); + return lastMessage?.Content ?? string.Empty; + } + + private async Task ExecuteGraphWorkflowAsync( + List agents, + ChatHistory chatHistory, + AgentWorkflowConfig config, + CancellationToken cancellationToken) + { + // Create agent chat with selection strategy + var agentChat = new AgentGroupChat(agents.ToArray()); + + // Configure selection strategy if specified + if (config.SelectionStrategy != null) + { + ConfigureSelectionStrategy(agentChat, config.SelectionStrategy, agents); + } + + // Configure termination + ConfigureTermination(agentChat, config); + + // Execute chat + await foreach (var message in agentChat.InvokeAsync(cancellationToken)) + { + chatHistory.Add(message); + } + + var lastMessage = chatHistory.LastOrDefault(m => m.Role == AuthorRole.Assistant); + return lastMessage?.Content ?? string.Empty; + } + + private void ConfigureTermination(AgentGroupChat agentChat, AgentWorkflowConfig config) + { + // Configure termination based on the configuration + switch (config.Termination.Type) + { + case TerminationType.MaxMessages: + agentChat.ExecutionSettings = new AgentGroupChatSettings + { + TerminationStrategy = new MaxChatHistoryTerminationStrategy(config.Termination.MaxMessages) + }; + break; + case TerminationType.Keyword: + if (!string.IsNullOrEmpty(config.Termination.TerminationKeyword)) + { + agentChat.ExecutionSettings = new AgentGroupChatSettings + { + TerminationStrategy = new KeywordTerminationStrategy(config.Termination.TerminationKeyword) + }; + } + break; + // AgentDecision termination would require custom implementation + } + } + + private void ConfigureSelectionStrategy( + AgentGroupChat agentChat, + SelectionStrategyConfig strategyConfig, + List agents) + { + // Configure agent selection based on strategy type + // Note: The actual Agent Framework API may vary; this is a conceptual implementation + switch (strategyConfig.Type) + { + case SelectionStrategyType.Sequential: + agentChat.ExecutionSettings = new AgentGroupChatSettings + { + SelectionStrategy = new SequentialSelectionStrategy() + }; + break; + case SelectionStrategyType.RoundRobin: + // Round-robin is similar to sequential in most implementations + agentChat.ExecutionSettings = new AgentGroupChatSettings + { + SelectionStrategy = new SequentialSelectionStrategy() + }; + break; + // LLMBased and AgentBased would require custom strategies + } + } + + private string FormatInput(IDictionary input, AgentWorkflowConfig config) + { + if (input.Count == 0) + return "Please begin the conversation."; + + var parts = input.Select(kvp => $"{kvp.Key}: {kvp.Value}"); + return string.Join("\n", parts); + } +} + +/// +/// Simple termination strategy based on maximum chat history length. +/// +internal class MaxChatHistoryTerminationStrategy : TerminationStrategy +{ + private readonly int _maxMessages; + private int _messageCount; + + public MaxChatHistoryTerminationStrategy(int maxMessages) + { + _maxMessages = maxMessages; + } + + protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) + { + _messageCount = history.Count; + return Task.FromResult(_messageCount >= _maxMessages); + } +} + +/// +/// Termination strategy based on keyword detection. +/// +internal class KeywordTerminationStrategy : TerminationStrategy +{ + private readonly string _keyword; + + public KeywordTerminationStrategy(string keyword) + { + _keyword = keyword; + } + + protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) + { + var lastMessage = history.LastOrDefault(); + if (lastMessage?.Content == null) + return Task.FromResult(false); + + return Task.FromResult(lastMessage.Content.Contains(_keyword, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs index e4320df2..b590e3e1 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs @@ -3,15 +3,41 @@ namespace Elsa.Agents; +/// +/// Provides kernel configuration by merging configuration-based agents with code-first definitions. +/// [UsedImplicitly] -public class ConfigurationKernelConfigProvider(IOptions options) : IKernelConfigProvider +public class ConfigurationKernelConfigProvider( + IOptions options, + IAgentDefinitionProvider agentDefinitionProvider, + IAgentWorkflowDefinitionProvider workflowDefinitionProvider) : 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; + + // Add configuration-based items + 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; + + // Add code-first agents + foreach (var agentDef in agentDefinitionProvider.GetDefinitions()) + { + var agentConfig = agentDef.GetAgentConfig(); + kernelConfig.Agents[agentDef.Name] = agentConfig; + } + + // Add code-first agent workflows + foreach (var workflowDef in workflowDefinitionProvider.GetDefinitions()) + { + var workflowConfig = workflowDef.GetWorkflowConfig(); + kernelConfig.AgentWorkflows[workflowDef.Name] = workflowConfig; + } + return Task.FromResult(kernelConfig); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/AgentWorkflowConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/AgentWorkflowConfig.cs new file mode 100644 index 00000000..76c6269e --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Models/Configs/AgentWorkflowConfig.cs @@ -0,0 +1,73 @@ +namespace Elsa.Agents; + +/// +/// Configuration for a multi-agent workflow (team/sequence/graph). +/// +public class AgentWorkflowConfig +{ + /// + /// The name of the agent workflow. + /// + public string Name { get; set; } = ""; + + /// + /// The description of the agent workflow. + /// + public string Description { get; set; } = ""; + + /// + /// The type of workflow orchestration (Sequential, Parallel, Graph). + /// + public AgentWorkflowType WorkflowType { get; set; } = AgentWorkflowType.Sequential; + + /// + /// The agents participating in this workflow. + /// + public ICollection Agents { get; set; } = []; + + /// + /// Services required by the workflow. + /// + public ICollection Services { get; set; } = []; + + /// + /// Input variables for the workflow. + /// + public ICollection InputVariables { get; set; } = []; + + /// + /// Output variable for the workflow. + /// + public OutputVariableConfig OutputVariable { get; set; } = new(); + + /// + /// Execution settings for the workflow. + /// + public ExecutionSettingsConfig ExecutionSettings { get; set; } = new(); + + /// + /// The termination strategy for the workflow (e.g., after N messages, on specific condition). + /// + public TerminationConfig Termination { get; set; } = new(); + + /// + /// Optional selection strategy configuration for determining which agent acts next. + /// + public SelectionStrategyConfig? SelectionStrategy { get; set; } +} + +/// +/// Types of agent workflow orchestration. +/// +public enum AgentWorkflowType +{ + /// + /// Agents execute sequentially in order. + /// + Sequential, + + /// + /// Custom graph-based orchestration where agent selection is determined by a strategy. + /// + Graph +} diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs index 75c28527..35dbe4b4 100644 --- a/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs +++ b/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs @@ -5,4 +5,5 @@ public class KernelConfig public IDictionary ApiKeys { get; set; } = new Dictionary(); public IDictionary Services { get; } = new Dictionary(); public IDictionary Agents { get; } = new Dictionary(); + public IDictionary AgentWorkflows { get; } = new Dictionary(); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/SelectionStrategyConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/SelectionStrategyConfig.cs new file mode 100644 index 00000000..72dcd418 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Models/Configs/SelectionStrategyConfig.cs @@ -0,0 +1,48 @@ +namespace Elsa.Agents; + +/// +/// Configuration for agent selection strategy in multi-agent workflows. +/// +public class SelectionStrategyConfig +{ + /// + /// The type of selection strategy. + /// + public SelectionStrategyType Type { get; set; } = SelectionStrategyType.Sequential; + + /// + /// Custom selection prompt (when Type is LLMBased). + /// + public string? SelectionPrompt { get; set; } + + /// + /// Agent responsible for making selection decisions (when Type is AgentBased). + /// + public string? SelectorAgentName { get; set; } +} + +/// +/// Types of agent selection strategies. +/// +public enum SelectionStrategyType +{ + /// + /// Agents are selected in sequential order. + /// + Sequential, + + /// + /// Round-robin selection among agents. + /// + RoundRobin, + + /// + /// Use an LLM to decide which agent should act next. + /// + LLMBased, + + /// + /// Use a dedicated agent to make selection decisions. + /// + AgentBased +} diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/TerminationConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/TerminationConfig.cs new file mode 100644 index 00000000..ccede073 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Models/Configs/TerminationConfig.cs @@ -0,0 +1,48 @@ +namespace Elsa.Agents; + +/// +/// Configuration for agent workflow termination conditions. +/// +public class TerminationConfig +{ + /// + /// The type of termination strategy. + /// + public TerminationType Type { get; set; } = TerminationType.MaxMessages; + + /// + /// Maximum number of messages/turns before termination (when Type is MaxMessages). + /// + public int MaxMessages { get; set; } = 10; + + /// + /// Keyword or pattern that triggers termination (when Type is Keyword). + /// + public string? TerminationKeyword { get; set; } + + /// + /// Name of the agent that can trigger termination (when Type is AgentDecision). + /// + public string? TerminationAgentName { get; set; } +} + +/// +/// Types of termination strategies for agent workflows. +/// +public enum TerminationType +{ + /// + /// Terminate after a maximum number of messages/turns. + /// + MaxMessages, + + /// + /// Terminate when a specific keyword or pattern is detected. + /// + Keyword, + + /// + /// Allow a specific agent to decide when to terminate. + /// + AgentDecision +} From 7e699df1618e0a3ccffc4a463e0769583a316e11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:23:02 +0000 Subject: [PATCH 04/23] Add comprehensive documentation and examples for Agent Framework features Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --- .../agents/Examples/CodeFirstAgentExample.cs | 53 +++ src/modules/agents/README.md | 444 ++++++++++++++++++ 2 files changed, 497 insertions(+) create mode 100644 src/modules/agents/Examples/CodeFirstAgentExample.cs create mode 100644 src/modules/agents/README.md diff --git a/src/modules/agents/Examples/CodeFirstAgentExample.cs b/src/modules/agents/Examples/CodeFirstAgentExample.cs new file mode 100644 index 00000000..ad58bdda --- /dev/null +++ b/src/modules/agents/Examples/CodeFirstAgentExample.cs @@ -0,0 +1,53 @@ +using Elsa.Agents; + +namespace Elsa.Agents.Examples; + +/// +/// Example: Simple code-first agent definition +/// +public class GreeterAgent : IAgentDefinition +{ + public string Name => "GreeterAgent"; + public string Description => "Greets users warmly"; + + public AgentConfig GetAgentConfig() + { + return new AgentConfig + { + Name = Name, + Description = Description, + Services = ["OpenAIChat"], + FunctionName = "Greet", + PromptTemplate = "Greet the user by name: {{userName}}", + InputVariables = + [ + new InputVariableConfig { Name = "userName", Type = "String", Description = "User name" } + ], + OutputVariable = new OutputVariableConfig { Type = "String", Description = "Greeting" } + }; + } +} + +/// +/// Example: Multi-agent workflow definition +/// +public class ContentWorkflow : IAgentWorkflowDefinition +{ + public string Name => "ContentPipeline"; + public string Description => "Multi-agent content creation"; + + public AgentWorkflowConfig GetWorkflowConfig() + { + return new AgentWorkflowConfig + { + Name = Name, + Description = Description, + WorkflowType = AgentWorkflowType.Sequential, + Agents = ["Researcher", "Writer", "Editor"], + Services = ["OpenAIChat"], + InputVariables = [new InputVariableConfig { Name = "topic", Type = "String" }], + OutputVariable = new OutputVariableConfig { Type = "String" }, + Termination = new TerminationConfig { Type = TerminationType.MaxMessages, MaxMessages = 15 } + }; + } +} diff --git a/src/modules/agents/README.md b/src/modules/agents/README.md new file mode 100644 index 00000000..9f5337c4 --- /dev/null +++ b/src/modules/agents/README.md @@ -0,0 +1,444 @@ +# Elsa Agents Module + +The Elsa Agents module provides an agentic framework built on top of Microsoft's Agent Framework (Semantic Kernel Agents), enabling you to define and execute AI agents and multi-agent workflows as Elsa workflow activities. + +## Features + +- **JSON/Configuration-Based Agents**: Define agents via configuration (appsettings.json, database) +- **Code-First Agents**: Register agents programmatically using fluent APIs +- **Multi-Agent Workflows**: Orchestrate multiple agents working together (sequential, graph-based) +- **Workflow Integration**: All agents and agent workflows are exposed as Elsa workflow activities +- **Tool Calling**: Agents can invoke tools/plugins and other agents +- **Memory and State**: Support for persistent conversations and state management + +## Architecture + +The module consists of several packages: + +- **Elsa.Agents.Core**: Core services, abstractions, and Agent Framework integration +- **Elsa.Agents.Models**: Configuration models and data contracts +- **Elsa.Agents.Activities**: Workflow activity implementations and providers +- **Elsa.Agents.Api**: REST API endpoints for agent management +- **Elsa.Agents.Persistence**: Database persistence abstractions +- **Elsa.Studio.Agents**: Blazor UI for managing agents + +## Getting Started + +### Installation + +```bash +dotnet add package Elsa.Agents.Core +dotnet add package Elsa.Agents.Activities +``` + +### Configuration + +Add agents to your Elsa configuration: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + // Register service providers (OpenAI, Azure OpenAI, etc.) + agents.UseOpenAI(); + + // Enable agent activities + elsa.UseAgentActivities(); + }); +}); +``` + +## Defining Agents + +### 1. JSON/Configuration-Based Agents + +Define agents in `appsettings.json`: + +```json +{ + "Agents": { + "ApiKeys": [ + { + "Name": "OpenAI", + "Value": "sk-..." + } + ], + "Services": [ + { + "Name": "OpenAIChat", + "Type": "OpenAIChatCompletion", + "ApiKeyName": "OpenAI", + "Model": "gpt-4" + } + ], + "Agents": [ + { + "Name": "CustomerSupport", + "Description": "Helpful customer support agent", + "Services": ["OpenAIChat"], + "FunctionName": "HandleCustomerQuery", + "PromptTemplate": "You are a helpful customer support agent. Help the user with their question: {{question}}", + "InputVariables": [ + { + "Name": "question", + "Type": "String", + "Description": "The customer's question" + } + ], + "OutputVariable": { + "Type": "String", + "Description": "The agent's response" + }, + "Plugins": ["WebSearch", "KnowledgeBase"] + } + ] + } +} +``` + +### 2. Code-First Agents + +Register agents programmatically: + +```csharp +// Simple agent definition +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentDefinition(new SimpleAgentDefinition + { + Name = "GreeterAgent", + Description = "Greets users warmly", + AgentConfig = new AgentConfig + { + Name = "GreeterAgent", + Description = "Greets users warmly", + Services = ["OpenAIChat"], + FunctionName = "Greet", + PromptTemplate = "Greet the user by name: {{userName}}", + InputVariables = + [ + new InputVariableConfig + { + Name = "userName", + Type = "String", + Description = "The user's name" + } + ], + OutputVariable = new OutputVariableConfig + { + Type = "String", + Description = "Greeting message" + } + } + }); + }); +}); + +// Or implement IAgentDefinition +public class GreeterAgentDefinition : IAgentDefinition +{ + public string Name => "GreeterAgent"; + public string Description => "Greets users warmly"; + + public AgentConfig GetAgentConfig() + { + return new AgentConfig + { + Name = Name, + Description = Description, + Services = ["OpenAIChat"], + FunctionName = "Greet", + PromptTemplate = "Greet the user by name: {{userName}}", + InputVariables = + [ + new InputVariableConfig + { + Name = "userName", + Type = "String", + Description = "The user's name" + } + ], + OutputVariable = new OutputVariableConfig + { + Type = "String", + Description = "Greeting message" + } + }; + } +} + +// Register it +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentDefinition(); + }); +}); +``` + +### 3. Agent Workflows (Multi-Agent Teams) + +Create workflows of multiple agents: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentWorkflowDefinition(new AgentWorkflowDefinition + { + Name = "ContentPipeline", + Description = "Multi-agent content creation pipeline", + WorkflowConfig = new AgentWorkflowConfig + { + Name = "ContentPipeline", + Description = "Multi-agent content creation pipeline", + WorkflowType = AgentWorkflowType.Sequential, + Agents = ["Researcher", "Writer", "Editor", "SEOSpecialist"], + Services = ["OpenAIChat"], + InputVariables = + [ + new InputVariableConfig + { + Name = "topic", + Type = "String", + Description = "Content topic" + } + ], + OutputVariable = new OutputVariableConfig + { + Type = "String", + Description = "Final content" + }, + Termination = new TerminationConfig + { + Type = TerminationType.MaxMessages, + MaxMessages = 20 + }, + SelectionStrategy = new SelectionStrategyConfig + { + Type = SelectionStrategyType.Sequential + } + } + }); + }); +}); +``` + +#### Workflow Types + +- **Sequential**: Agents execute in order +- **Graph**: Custom orchestration with agent selection strategies + +#### Termination Strategies + +- **MaxMessages**: Stop after N messages/turns +- **Keyword**: Terminate when specific keyword is detected +- **AgentDecision**: Let a designated agent decide when to stop + +#### Selection Strategies + +- **Sequential**: Agents act in order +- **RoundRobin**: Cycle through agents +- **LLMBased**: Use LLM to decide next agent +- **AgentBased**: Dedicated agent makes selection + +## Using Agents in Workflows + +Once registered, agents automatically appear as activities in the Elsa workflow designer: + +### Categories +- **Agents**: Individual agent activities +- **Agent Workflows**: Multi-agent workflow activities + +### Activity Inputs/Outputs +Each agent activity has inputs and outputs based on its configuration: + +``` +CustomerSupport Activity: + Inputs: + - question (String): The customer's question + Outputs: + - Output (String): The agent's response +``` + +## Agent Framework Integration + +The module uses Microsoft's Agent Framework (Semantic Kernel Agents) for execution: + +### Key Components + +- **AgentFrameworkFactory**: Creates Agent Framework agents from Elsa configs +- **AgentWorkflowExecutor**: Orchestrates multi-agent workflows +- **AgentInvoker**: Executes individual agents (supports both legacy SK and Agent Framework) + +### Backward Compatibility + +The `AgentInvoker` supports both: +- **Legacy Mode**: Original Semantic Kernel implementation +- **Agent Framework Mode** (default): New Microsoft Agent Framework + +You can force legacy mode for specific scenarios: +```csharp +await agentInvoker.InvokeAgentAsync(agentName, input, useAgentFramework: false, cancellationToken); +``` + +## Tool Calling + +Agents can call: +- **Plugins**: Registered plugins (e.g., WebSearch, ImageGenerator) +- **Other Agents**: Agents can invoke other agents as tools +- **Activities**: Elsa activities exposed as agent tools + +Configure tools in agent definition: +```json +{ + "Agents": [ + { + "Name": "ResearchAgent", + "Plugins": ["WebSearch", "DocumentQuery"], + "Agents": ["FactChecker", "Summarizer"] + } + ] +} +``` + +## Service Providers + +Supported AI service providers: + +- **OpenAI**: Chat completion, embeddings, image generation +- **Azure OpenAI**: Chat completion, embeddings +- Custom providers via `IAgentServiceProvider` + +## Database Persistence + +Agents can be stored in and loaded from the database: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + // Use database provider for kernel config + agents.UseKernelConfigProvider(sp => + sp.GetRequiredService()); + }); +}); +``` + +## Examples + +See the `Elsa.Studio.Agents/Assets` directory for example agent configurations: +- `hello-world-console.json`: Simple agent example +- `customer-support.json`: Customer support agent +- `content-pipeline.json`: Multi-agent content workflow +- `document-review-process.json`: Document review workflow + +## Advanced Topics + +### Custom Service Providers + +Implement `IAgentServiceProvider`: + +```csharp +public class CustomAIProvider : IAgentServiceProvider +{ + public string Name => "CustomAI"; + + public void ConfigureKernel(KernelBuilderContext context) + { + // Configure custom AI service + context.Builder.Services.AddCustomAIChatCompletion( + context.ServiceConfig.GetSetting("ApiKey") + ); + } +} + +// Register +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentServiceProvider(); + }); +}); +``` + +### Custom Plugins + +Implement agent plugins: + +```csharp +public class WeatherPlugin +{ + [KernelFunction, Description("Gets weather for a location")] + public async Task GetWeather( + [Description("Location name")] string location) + { + // Implementation + return $"Weather in {location}: Sunny, 72°F"; + } +} + +// Register +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddPluginProvider(); + }); +}); +``` + +## API Reference + +### Core Interfaces + +- `IAgentDefinition`: Code-first agent definition +- `IAgentWorkflowDefinition`: Code-first workflow definition +- `IKernelConfigProvider`: Provides agent configuration +- `IAgentServiceProvider`: AI service provider integration + +### Extension Methods + +- `AddAgentDefinition()`: Register agent definition +- `AddAgentWorkflowDefinition()`: Register workflow definition +- `AddPluginProvider()`: Register plugin provider +- `AddAgentServiceProvider()`: Register service provider + +## Troubleshooting + +### Agent Not Appearing in Workflow Designer + +Ensure: +1. Agent is properly registered +2. AgentActivitiesFeature is enabled +3. Configuration is valid +4. Activity registry has been refreshed + +### Tool Calling Not Working + +Check: +1. Plugins are registered +2. Service provider supports function calling +3. Execution settings enable FunctionChoiceBehavior + +### Multi-Agent Workflow Issues + +Verify: +1. All referenced agents exist +2. Termination strategy is configured +3. Selection strategy is appropriate for workflow type + +## Contributing + +See the main repository CONTRIBUTING.md for contribution guidelines. + +## License + +This module is part of the Elsa Workflows project and follows the same license. From 163a17fab2f051103b9c1ea8c3ca7f6048331f52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:25:59 +0000 Subject: [PATCH 05/23] Add unit tests and fix null reference issues in ConfigurationKernelConfigProvider Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --- .../Elsa.Agents.Core/Options/AgentsOptions.cs | 6 +- .../ConfigurationKernelConfigProvider.cs | 23 +++-- .../AgentDefinitionProviderTests.cs | 63 +++++++++++++ .../AgentWorkflowDefinitionProviderTests.cs | 63 +++++++++++++ .../ConfigurationKernelConfigProviderTests.cs | 89 +++++++++++++++++++ .../Elsa.Agents.Tests.csproj | 6 ++ 6 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 test/modules/agents/Elsa.Agents.Tests/AgentDefinitionProviderTests.cs create mode 100644 test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs create mode 100644 test/modules/agents/Elsa.Agents.Tests/ConfigurationKernelConfigProviderTests.cs create mode 100644 test/modules/agents/Elsa.Agents.Tests/Elsa.Agents.Tests.csproj diff --git a/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs index a92862a6..a1275d99 100644 --- a/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs @@ -2,7 +2,7 @@ namespace Elsa.Agents; public class AgentsOptions { - public ICollection ApiKeys { get; set; } - public ICollection Services { get; set; } - public ICollection Agents { get; set; } + public ICollection ApiKeys { get; set; } = new List(); + public ICollection Services { get; set; } = new List(); + public ICollection Agents { get; set; } = new List(); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs index b590e3e1..9adf6c3c 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs @@ -16,13 +16,22 @@ public Task GetKernelConfigAsync(CancellationToken cancellationTok { var kernelConfig = new KernelConfig(); - // Add configuration-based items - 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; + // Add configuration-based items (if available) + if (options.Value.ApiKeys != null) + { + foreach (var apiKey in options.Value.ApiKeys) + kernelConfig.ApiKeys[apiKey.Name] = apiKey; + } + if (options.Value.Services != null) + { + foreach (var service in options.Value.Services) + kernelConfig.Services[service.Name] = service; + } + if (options.Value.Agents != null) + { + foreach (var agent in options.Value.Agents) + kernelConfig.Agents[agent.Name] = agent; + } // Add code-first agents foreach (var agentDef in agentDefinitionProvider.GetDefinitions()) diff --git a/test/modules/agents/Elsa.Agents.Tests/AgentDefinitionProviderTests.cs b/test/modules/agents/Elsa.Agents.Tests/AgentDefinitionProviderTests.cs new file mode 100644 index 00000000..e662cb06 --- /dev/null +++ b/test/modules/agents/Elsa.Agents.Tests/AgentDefinitionProviderTests.cs @@ -0,0 +1,63 @@ +using Elsa.Agents; +using Xunit; + +namespace Elsa.Agents.Tests; + +public class AgentDefinitionProviderTests +{ + [Fact] + public void GetDefinitions_ReturnsRegisteredDefinitions() + { + // Arrange + var definition1 = new TestAgentDefinition("Agent1"); + var definition2 = new TestAgentDefinition("Agent2"); + var definitions = new List { definition1, definition2 }; + var provider = new AgentDefinitionProvider(definitions); + + // Act + var result = provider.GetDefinitions().ToList(); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(definition1, result); + Assert.Contains(definition2, result); + } + + [Fact] + public void GetDefinitions_WithNoDefinitions_ReturnsEmpty() + { + // Arrange + var provider = new AgentDefinitionProvider(Array.Empty()); + + // Act + var result = provider.GetDefinitions().ToList(); + + // Assert + Assert.Empty(result); + } + + private class TestAgentDefinition : IAgentDefinition + { + public TestAgentDefinition(string name) + { + Name = name; + } + + public string Name { get; } + public string Description => $"Test agent {Name}"; + + public AgentConfig GetAgentConfig() + { + return new AgentConfig + { + Name = Name, + Description = Description, + Services = Array.Empty(), + FunctionName = "Test", + PromptTemplate = "Test prompt", + InputVariables = Array.Empty(), + OutputVariable = new OutputVariableConfig() + }; + } + } +} diff --git a/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs b/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs new file mode 100644 index 00000000..4b17e9f9 --- /dev/null +++ b/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs @@ -0,0 +1,63 @@ +using Elsa.Agents; +using Xunit; + +namespace Elsa.Agents.Tests; + +public class AgentWorkflowDefinitionProviderTests +{ + [Fact] + public void GetDefinitions_ReturnsRegisteredWorkflows() + { + // Arrange + var workflow1 = new TestWorkflowDefinition("Workflow1"); + var workflow2 = new TestWorkflowDefinition("Workflow2"); + var definitions = new List { workflow1, workflow2 }; + var provider = new AgentWorkflowDefinitionProvider(definitions); + + // Act + var result = provider.GetDefinitions().ToList(); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(workflow1, result); + Assert.Contains(workflow2, result); + } + + [Fact] + public void GetDefinitions_WithNoWorkflows_ReturnsEmpty() + { + // Arrange + var provider = new AgentWorkflowDefinitionProvider(Array.Empty()); + + // Act + var result = provider.GetDefinitions().ToList(); + + // Assert + Assert.Empty(result); + } + + private class TestWorkflowDefinition : IAgentWorkflowDefinition + { + public TestWorkflowDefinition(string name) + { + Name = name; + } + + public string Name { get; } + public string Description => $"Test workflow {Name}"; + + public AgentWorkflowConfig GetWorkflowConfig() + { + return new AgentWorkflowConfig + { + Name = Name, + Description = Description, + WorkflowType = AgentWorkflowType.Sequential, + Agents = Array.Empty(), + Services = Array.Empty(), + InputVariables = Array.Empty(), + OutputVariable = new OutputVariableConfig() + }; + } + } +} diff --git a/test/modules/agents/Elsa.Agents.Tests/ConfigurationKernelConfigProviderTests.cs b/test/modules/agents/Elsa.Agents.Tests/ConfigurationKernelConfigProviderTests.cs new file mode 100644 index 00000000..64f76e22 --- /dev/null +++ b/test/modules/agents/Elsa.Agents.Tests/ConfigurationKernelConfigProviderTests.cs @@ -0,0 +1,89 @@ +using Elsa.Agents; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Elsa.Agents.Tests; + +public class ConfigurationKernelConfigProviderTests +{ + [Fact] + public async Task GetKernelConfigAsync_MergesConfigurationAndCodeFirstAgents() + { + // Arrange + var options = Options.Create(new AgentsOptions + { + Agents = new List + { + new() { Name = "ConfigAgent", Description = "From config" } + } + }); + + var codeFirstAgent = new TestAgent("CodeFirstAgent"); + var agentProvider = new AgentDefinitionProvider(new[] { codeFirstAgent }); + var workflowProvider = new AgentWorkflowDefinitionProvider(Array.Empty()); + + var provider = new ConfigurationKernelConfigProvider(options, agentProvider, workflowProvider); + + // Act + var config = await provider.GetKernelConfigAsync(); + + // Assert + Assert.Equal(2, config.Agents.Count); + Assert.True(config.Agents.ContainsKey("ConfigAgent")); + Assert.True(config.Agents.ContainsKey("CodeFirstAgent")); + } + + [Fact] + public async Task GetKernelConfigAsync_IncludesAgentWorkflows() + { + // Arrange + var options = Options.Create(new AgentsOptions()); + var agentProvider = new AgentDefinitionProvider(Array.Empty()); + + var workflow = new TestWorkflow("TestWorkflow"); + var workflowProvider = new AgentWorkflowDefinitionProvider(new[] { workflow }); + + var provider = new ConfigurationKernelConfigProvider(options, agentProvider, workflowProvider); + + // Act + var config = await provider.GetKernelConfigAsync(); + + // Assert + Assert.Single(config.AgentWorkflows); + Assert.True(config.AgentWorkflows.ContainsKey("TestWorkflow")); + } + + private class TestAgent : IAgentDefinition + { + public TestAgent(string name) => Name = name; + public string Name { get; } + public string Description => $"Test {Name}"; + public AgentConfig GetAgentConfig() => new() + { + Name = Name, + Description = Description, + Services = Array.Empty(), + FunctionName = "Test", + PromptTemplate = "Test", + InputVariables = Array.Empty(), + OutputVariable = new OutputVariableConfig() + }; + } + + private class TestWorkflow : IAgentWorkflowDefinition + { + public TestWorkflow(string name) => Name = name; + public string Name { get; } + public string Description => $"Test {Name}"; + public AgentWorkflowConfig GetWorkflowConfig() => new() + { + Name = Name, + Description = Description, + WorkflowType = AgentWorkflowType.Sequential, + Agents = Array.Empty(), + Services = Array.Empty(), + InputVariables = Array.Empty(), + OutputVariable = new OutputVariableConfig() + }; + } +} diff --git a/test/modules/agents/Elsa.Agents.Tests/Elsa.Agents.Tests.csproj b/test/modules/agents/Elsa.Agents.Tests/Elsa.Agents.Tests.csproj new file mode 100644 index 00000000..79a286e8 --- /dev/null +++ b/test/modules/agents/Elsa.Agents.Tests/Elsa.Agents.Tests.csproj @@ -0,0 +1,6 @@ + + + + + + From e03d1bffece90c3b5519c97ac13ec7af6721d40a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:30:55 +0000 Subject: [PATCH 06/23] Address code review feedback - improve error handling and remove unused field Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --- .../Activities/AgentWorkflowActivity.cs | 14 +++++++++++--- .../Services/AgentWorkflowExecutor.cs | 5 ++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs index f5f4c8a7..f1b88d07 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs @@ -65,7 +65,11 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context if (string.IsNullOrWhiteSpace(json)) throw new InvalidOperationException("The workflow output is empty or null."); - var outputType = context.ActivityDescriptor.Outputs.Single().Type; + var outputs = context.ActivityDescriptor.Outputs; + if (outputs.Count != 1) + throw new InvalidOperationException($"Expected exactly one output, but found {outputs.Count}"); + + var outputType = outputs.First().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)) @@ -73,8 +77,12 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var converterOptions = new ObjectConverterOptions(SerializerOptions); var outputValue = json.ConvertTo(outputType, converterOptions); - var outputDescriptor = activityDescriptor.Outputs.Single(); - var output = (Output)outputDescriptor.ValueGetter(this); + var outputDescriptor = outputs.First(); + var output = outputDescriptor.ValueGetter(this) as Output; + + if (output == null) + throw new InvalidOperationException("Output descriptor did not return a valid Output object"); + context.Set(output, outputValue); } } diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs index 25c03815..ce74f789 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs @@ -184,7 +184,6 @@ private string FormatInput(IDictionary input, AgentWorkflowConf internal class MaxChatHistoryTerminationStrategy : TerminationStrategy { private readonly int _maxMessages; - private int _messageCount; public MaxChatHistoryTerminationStrategy(int maxMessages) { @@ -193,8 +192,8 @@ public MaxChatHistoryTerminationStrategy(int maxMessages) protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) { - _messageCount = history.Count; - return Task.FromResult(_messageCount >= _maxMessages); + // Terminate when history length reaches or exceeds the maximum + return Task.FromResult(history.Count >= _maxMessages); } } From 1d616cbfa6af79a7c0e3bb710930d08e5ea3104d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:31:44 +0000 Subject: [PATCH 07/23] Add migration guide for Agent Framework features Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --- src/modules/agents/MIGRATION_GUIDE.md | 144 ++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/modules/agents/MIGRATION_GUIDE.md diff --git a/src/modules/agents/MIGRATION_GUIDE.md b/src/modules/agents/MIGRATION_GUIDE.md new file mode 100644 index 00000000..779cd238 --- /dev/null +++ b/src/modules/agents/MIGRATION_GUIDE.md @@ -0,0 +1,144 @@ +# Migration Guide: Microsoft Agent Framework Integration + +This guide helps you understand and adopt the new Microsoft Agent Framework features in Elsa.Agents. + +## What's New + +### 1. Microsoft Agent Framework +The module now uses Microsoft's Agent Framework (Semantic Kernel Agents) for agent execution, providing: +- Better multi-agent orchestration +- More flexible agent communication patterns +- Enhanced tool calling capabilities +- Improved state management + +### 2. Code-First Agent Registration +You can now define agents programmatically instead of only via JSON/configuration: + +```csharp +// Register a code-first agent +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentDefinition(); + }); +}); + +// Define your agent +public class MyCustomAgent : IAgentDefinition +{ + public string Name => "MyAgent"; + public string Description => "My custom agent"; + + public AgentConfig GetAgentConfig() + { + return new AgentConfig { /* ... */ }; + } +} +``` + +### 3. Multi-Agent Workflows +Create workflows where multiple agents collaborate: + +```csharp +services.AddElsa(elsa => +{ + elsa.UseAgents(agents => + { + agents.Services.AddAgentWorkflowDefinition(); + }); +}); + +public class ContentPipeline : IAgentWorkflowDefinition +{ + public AgentWorkflowConfig GetWorkflowConfig() + { + return new AgentWorkflowConfig + { + WorkflowType = AgentWorkflowType.Sequential, + Agents = ["Researcher", "Writer", "Editor"], + Termination = new TerminationConfig + { + Type = TerminationType.MaxMessages, + MaxMessages = 20 + } + }; + } +} +``` + +## Backward Compatibility + +### No Breaking Changes +All existing functionality continues to work: +- JSON-defined agents ✓ +- Database-persisted agents ✓ +- Configuration-based agents ✓ +- Existing activity providers ✓ + +### Legacy Mode +The `AgentInvoker` supports both legacy and new execution modes: + +```csharp +// New Agent Framework mode (default) +await agentInvoker.InvokeAgentAsync(agentName, input, cancellationToken); + +// Legacy Semantic Kernel mode (if needed) +await agentInvoker.InvokeAgentAsync(agentName, input, useAgentFramework: false, cancellationToken); +``` + +## Migration Strategies + +### Strategy 1: No Migration Needed +If you're happy with your current setup, do nothing. Everything continues to work as before. + +### Strategy 2: Gradual Adoption +1. Keep existing JSON/DB agents +2. Add new agents using code-first approach +3. Both coexist seamlessly + +### Strategy 3: Full Migration +1. Keep JSON agents for configuration +2. Convert complex agents to code-first for better maintainability +3. Create multi-agent workflows for advanced scenarios + +## New Capabilities + +### Workflow Types +- **Sequential**: Agents execute in order +- **Graph**: Custom orchestration with agent selection + +### Termination Strategies +- **MaxMessages**: Stop after N turns +- **Keyword**: Stop when pattern detected +- **AgentDecision**: Let agent decide when done + +### Selection Strategies +- **Sequential**: Fixed order +- **RoundRobin**: Cycle through agents +- **LLMBased**: AI decides next agent +- **AgentBased**: Dedicated selector agent + +## Examples + +See the following files for complete examples: +- `README.md` - Comprehensive documentation +- `Examples/CodeFirstAgentExample.cs` - Code samples +- `Assets/*.json` - JSON configuration examples + +## Getting Help + +If you encounter issues: +1. Check the README.md for detailed documentation +2. Review the examples in `Examples/` directory +3. Ensure all agents are properly registered +4. Verify configuration is valid + +## Testing + +The module includes comprehensive tests: +```bash +dotnet test test/modules/agents/Elsa.Agents.Tests/ +``` + +All tests passing: 6/6 ✓ From 5409790f36833c65988665102748832a46b30c3a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 12 Dec 2025 13:39:42 +0100 Subject: [PATCH 08/23] Add `Elsa.Agents.Tests` project and refactor `TestWorkflowDefinition` class --- Elsa.Extensions.sln | 10 ++++++++++ .../AgentWorkflowDefinitionProviderTests.cs | 13 ++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Elsa.Extensions.sln b/Elsa.Extensions.sln index 2e5180f1..18e6ee93 100644 --- a/Elsa.Extensions.sln +++ b/Elsa.Extensions.sln @@ -277,6 +277,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "data", "data", "{6D2A4421-A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Data.Csv", "src\modules\data\Elsa.Data.Csv\Elsa.Data.Csv.csproj", "{015646EB-EC33-4ADF-8417-B9D1A6B7CF06}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "agents", "agents", "{60A25F2D-634D-438A-87EA-F204677978BE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Tests", "test\modules\agents\Elsa.Agents.Tests\Elsa.Agents.Tests.csproj", "{F997734B-468C-40ED-9CBB-F759F71AA06E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -627,6 +631,10 @@ Global {015646EB-EC33-4ADF-8417-B9D1A6B7CF06}.Debug|Any CPU.Build.0 = Debug|Any CPU {015646EB-EC33-4ADF-8417-B9D1A6B7CF06}.Release|Any CPU.ActiveCfg = Release|Any CPU {015646EB-EC33-4ADF-8417-B9D1A6B7CF06}.Release|Any CPU.Build.0 = Release|Any CPU + {F997734B-468C-40ED-9CBB-F759F71AA06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F997734B-468C-40ED-9CBB-F759F71AA06E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F997734B-468C-40ED-9CBB-F759F71AA06E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F997734B-468C-40ED-9CBB-F759F71AA06E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -751,6 +759,8 @@ Global {BEB35A25-5A80-4B56-A1EC-F005288CD11C} = {AAD61D93-7C78-42C4-9F37-2564D127A668} {6D2A4421-A388-4BEE-BB11-D0FC32A80A10} = {30CF0330-4B09-4784-B499-46BED303810B} {015646EB-EC33-4ADF-8417-B9D1A6B7CF06} = {6D2A4421-A388-4BEE-BB11-D0FC32A80A10} + {60A25F2D-634D-438A-87EA-F204677978BE} = {3DDE6F89-531C-47F8-9CD7-7A4E6984FA48} + {F997734B-468C-40ED-9CBB-F759F71AA06E} = {60A25F2D-634D-438A-87EA-F204677978BE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {11A771DA-B728-445E-8A88-AE1C84C3B3A6} diff --git a/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs b/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs index 4b17e9f9..fc1afc44 100644 --- a/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs +++ b/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs @@ -36,19 +36,14 @@ public void GetDefinitions_WithNoWorkflows_ReturnsEmpty() Assert.Empty(result); } - private class TestWorkflowDefinition : IAgentWorkflowDefinition + private class TestWorkflowDefinition(string name) : IAgentWorkflowDefinition { - public TestWorkflowDefinition(string name) - { - Name = name; - } - - public string Name { get; } + public string Name { get; } = name; public string Description => $"Test workflow {Name}"; public AgentWorkflowConfig GetWorkflowConfig() { - return new AgentWorkflowConfig + return new() { Name = Name, Description = Description, @@ -56,7 +51,7 @@ public AgentWorkflowConfig GetWorkflowConfig() Agents = Array.Empty(), Services = Array.Empty(), InputVariables = Array.Empty(), - OutputVariable = new OutputVariableConfig() + OutputVariable = new() }; } } From b0705a7a626882adac8aa39ac3b86a23c57db661 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 12 Dec 2025 16:02:29 +0100 Subject: [PATCH 09/23] Remove legacy KernelFactory and implement improved Agent Framework support. This commit removes the deprecated KernelFactory and its interfaces, shifting focus to the new Agent Framework. It introduces enhanced activities, such as the CopyWriterAndEditorActivity, updates service configuration for better agent execution, and incorporates semantic kernel dependencies for future extensibility. --- .../Activities/AgentActivity.cs | 15 ++- .../Activities/AgentWorkflowActivity.cs | 1 - .../Contracts/IkernelFactory.cs | 8 -- .../Features/AgentsFeature.cs | 1 - .../Elsa.Agents.Core/Services/AgentInvoker.cs | 109 ++++------------- .../Services/AgentWorkflowExecutor.cs | 10 +- .../Services/KernelFactory.cs | 113 ------------------ 7 files changed, 45 insertions(+), 212 deletions(-) delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IkernelFactory.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/KernelFactory.cs diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs index 6fa3213a..4a57f7a4 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs @@ -57,6 +57,8 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context if (string.IsNullOrWhiteSpace(json)) throw new InvalidOperationException("The message content is empty or null."); + + json = StripCodeFences(json); var outputType = context.ActivityDescriptor.Outputs.Single().Type; @@ -68,6 +70,17 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var outputValue = json.ConvertTo(outputType, converterOptions); var outputDescriptor = activityDescriptor.Outputs.Single(); var output = (Output)outputDescriptor.ValueGetter(this); - context.Set(output, outputValue); + context.Set(output, outputValue, "Output"); + } + + private static string StripCodeFences(string content) + { + var trimmed = content.Trim(); + + if (!trimmed.StartsWith("```", StringComparison.Ordinal)) + return trimmed; + + var lines = trimmed.Split('\n'); + return lines.Length < 2 ? trimmed : string.Join('\n', lines.Skip(1).Take(lines.Length - 2)).Trim(); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs index f1b88d07..e2cb1f50 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs @@ -53,7 +53,6 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var kernelConfigProvider = context.GetRequiredService(); var workflowExecutor = context.GetRequiredService(); - var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(context.CancellationToken); if (!kernelConfig.AgentWorkflows.TryGetValue(WorkflowName, out var workflowConfig)) diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IkernelFactory.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IkernelFactory.cs deleted file mode 100644 index 7b14d61d..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IkernelFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.SemanticKernel; -namespace Elsa.Agents; - -public interface IKernelFactory -{ - Kernel CreateKernel(KernelConfig kernelConfig, AgentConfig agentConfig); - Kernel CreateKernel(KernelConfig kernelConfig, string agentName); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs index 3d06395d..ee51091e 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs @@ -26,7 +26,6 @@ public override void Apply() Services.AddOptions(); Services - .AddScoped() .AddScoped() .AddScoped() .AddScoped() diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs index d0c614c8..2c39062d 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs @@ -9,70 +9,34 @@ namespace Elsa.Agents; -public class AgentInvoker( - IKernelFactory kernelFactory, - IKernelConfigProvider kernelConfigProvider, - AgentFrameworkFactory agentFrameworkFactory) +public class AgentInvoker(IKernelConfigProvider kernelConfigProvider, AgentFrameworkFactory agentFrameworkFactory) { /// /// Invokes an agent using the Microsoft Agent Framework (new approach). /// public async Task InvokeAgentAsync(string agentName, IDictionary input, CancellationToken cancellationToken = default) - { - return await InvokeAgentAsync(agentName, input, useAgentFramework: true, cancellationToken); - } - - /// - /// Invokes an agent with option to use legacy Semantic Kernel or new Agent Framework. - /// - public async Task InvokeAgentAsync(string agentName, IDictionary input, bool useAgentFramework, CancellationToken cancellationToken = default) - { - if (useAgentFramework) - return await InvokeAgentWithFrameworkAsync(agentName, input, cancellationToken); - else - return await InvokeAgentLegacyAsync(agentName, input, cancellationToken); - } - - private async Task InvokeAgentWithFrameworkAsync(string agentName, IDictionary input, CancellationToken cancellationToken) { var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); var agentConfig = kernelConfig.Agents[agentName]; - + // Create agent using Agent Framework var agent = agentFrameworkFactory.CreateAgent(kernelConfig, agentConfig); - + // Create chat history ChatHistory chatHistory = []; - + // Format and add user input var promptTemplateConfig = new PromptTemplateConfig { Template = agentConfig.PromptTemplate, TemplateFormat = "handlebars", - Name = agentConfig.FunctionName + Name = agentConfig.FunctionName, + AllowDangerouslySetContent = true, + }; var templateFactory = new HandlebarsPromptTemplateFactory(); var promptTemplate = templateFactory.Create(promptTemplateConfig); - var kernelArguments = new KernelArguments(input); - string renderedPrompt = await promptTemplate.RenderAsync(agent.Kernel, kernelArguments); - - chatHistory.AddUserMessage(renderedPrompt); - - // Get response from agent - var response = await agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken).LastOrDefaultAsync(cancellationToken); - - if (response == null) - throw new InvalidOperationException("Agent did not produce a response"); - - return new InvokeAgentResult(agentConfig, response); - } - - private async Task InvokeAgentLegacyAsync(string agentName, IDictionary input, CancellationToken cancellationToken) - { - 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 { @@ -91,50 +55,29 @@ private async Task InvokeAgentLegacyAsync(string agentName, I [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 templateFactory = new HandlebarsPromptTemplateFactory(); + var kernelArguments = new KernelArguments(input, promptExecutionSettingsDictionary); + var renderedPrompt = await promptTemplate.RenderAsync(agent.Kernel, kernelArguments, cancellationToken); - var promptConfig = new PromptTemplateConfig - { - Template = agentConfig.PromptTemplate, - TemplateFormat = "handlebars", - Name = agentConfig.FunctionName - }; - - var promptTemplate = templateFactory.Create(promptConfig); - - var kernelArguments = new KernelArguments(input); - string renderedPrompt = await promptTemplate.RenderAsync(kernel, kernelArguments); - - ChatHistory chatHistory = []; chatHistory.AddUserMessage(renderedPrompt); + chatHistory.AddSystemMessage( + """" + You are a function that returns *only* JSON. + + Rules: + - Return a single valid JSON object. + - Do not add explanations. + - Do not add code fences. + - Do not prefix with ```json or any other markers. + - Output must start with { and end with }. + + If there's a problem with the JSON input, include the exact JSON input in your response for troubleshooting. + """"); - IChatCompletionService chatCompletion = kernel.GetRequiredService(); - - OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() - { - FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() - }; + // Get response from agent + var response = await agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken).LastOrDefaultAsync(cancellationToken); - var response = await chatCompletion.GetChatMessageContentAsync( - chatHistory, - executionSettings: openAIPromptExecutionSettings, - kernel: kernel); + if (response == null) + throw new InvalidOperationException("Agent did not produce a response"); return new(agentConfig, response); } diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs index ce74f789..b3cb51e7 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs @@ -65,7 +65,7 @@ public async Task ExecuteWorkflowAsync( _ => throw new NotSupportedException($"Workflow type {workflowConfig.WorkflowType} is not supported") }; - return new AgentWorkflowResult(workflowConfig, result, chatHistory); + return new(workflowConfig, result, chatHistory); } private async Task ExecuteSequentialWorkflowAsync( @@ -124,7 +124,7 @@ private void ConfigureTermination(AgentGroupChat agentChat, AgentWorkflowConfig switch (config.Termination.Type) { case TerminationType.MaxMessages: - agentChat.ExecutionSettings = new AgentGroupChatSettings + agentChat.ExecutionSettings = new() { TerminationStrategy = new MaxChatHistoryTerminationStrategy(config.Termination.MaxMessages) }; @@ -132,7 +132,7 @@ private void ConfigureTermination(AgentGroupChat agentChat, AgentWorkflowConfig case TerminationType.Keyword: if (!string.IsNullOrEmpty(config.Termination.TerminationKeyword)) { - agentChat.ExecutionSettings = new AgentGroupChatSettings + agentChat.ExecutionSettings = new() { TerminationStrategy = new KeywordTerminationStrategy(config.Termination.TerminationKeyword) }; @@ -152,14 +152,14 @@ private void ConfigureSelectionStrategy( switch (strategyConfig.Type) { case SelectionStrategyType.Sequential: - agentChat.ExecutionSettings = new AgentGroupChatSettings + agentChat.ExecutionSettings = new() { SelectionStrategy = new SequentialSelectionStrategy() }; break; case SelectionStrategyType.RoundRobin: // Round-robin is similar to sequential in most implementations - agentChat.ExecutionSettings = new AgentGroupChatSettings + agentChat.ExecutionSettings = new() { SelectionStrategy = new SequentialSelectionStrategy() }; diff --git a/src/modules/agents/Elsa.Agents.Core/Services/KernelFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/KernelFactory.cs deleted file mode 100644 index 9b8dfaf8..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/KernelFactory.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; - -#pragma warning disable SKEXP0001 -#pragma warning disable SKEXP0010 - -namespace Elsa.Agents; - -public class KernelFactory(IPluginDiscoverer pluginDiscoverer, IServiceDiscoverer serviceDiscoverer, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, ILogger logger) : IKernelFactory -{ - public Kernel CreateKernel(KernelConfig kernelConfig, string agentName) - { - var agent = kernelConfig.Agents[agentName]; - return CreateKernel(kernelConfig, agent); - } - - public Kernel CreateKernel(KernelConfig kernelConfig, AgentConfig agentConfig) - { - var builder = Kernel.CreateBuilder(); - builder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Trace)); - builder.Services.AddSingleton(agentConfig); - - ApplyAgentConfig(builder, kernelConfig, agentConfig); - - return builder.Build(); - } - - private void ApplyAgentConfig(IKernelBuilder builder, KernelConfig kernelConfig, AgentConfig agentConfig) - { - var services = serviceDiscoverer.Discover().ToDictionary(x => x.Name); - - foreach (string serviceName in agentConfig.Services) - { - if (!kernelConfig.Services.TryGetValue(serviceName, out var serviceConfig)) - { - logger.LogWarning($"Service {serviceName} not found"); - continue; - } - - AddService(builder, kernelConfig, serviceConfig, services); - } - - AddPlugins(builder, agentConfig); - AddAgents(builder, kernelConfig, agentConfig); - } - - private void AddService(IKernelBuilder builder, KernelConfig kernelConfig, ServiceConfig serviceConfig, Dictionary services) - { - if (!services.TryGetValue(serviceConfig.Type, out var serviceProvider)) - { - logger.LogWarning($"Service provider {serviceConfig.Type} not found"); - return; - } - - var context = new KernelBuilderContext(builder, kernelConfig, serviceConfig); - serviceProvider.ConfigureKernel(context); - } - - private void AddPlugins(IKernelBuilder builder, AgentConfig agent) - { - var plugins = pluginDiscoverer.GetPluginDescriptors().ToDictionary(x => x.Name); - foreach (var pluginName in agent.Plugins) - { - if (!plugins.TryGetValue(pluginName, out var pluginDescriptor)) - { - logger.LogWarning($"Plugin {pluginName} not found"); - continue; - } - - var pluginType = pluginDescriptor.PluginType; - var pluginInstance = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, pluginType); - builder.Plugins.AddFromObject(pluginInstance, pluginName); - } - } - - private void AddAgents(IKernelBuilder builder, KernelConfig kernelConfig, AgentConfig agent) - { - foreach (var agentName in agent.Agents) - { - if (!kernelConfig.Agents.TryGetValue(agentName, out var subAgent)) - { - logger.LogWarning($"Agent {agentName} not found"); - continue; - } - - var promptExecutionSettings = subAgent.ToOpenAIPromptExecutionSettings(); - var promptExecutionSettingsDictionary = new Dictionary - { - [PromptExecutionSettings.DefaultServiceId] = promptExecutionSettings, - }; - var promptTemplateConfig = new PromptTemplateConfig - { - Name = subAgent.FunctionName, - Description = subAgent.Description, - Template = subAgent.PromptTemplate, - ExecutionSettings = promptExecutionSettingsDictionary, - AllowDangerouslySetContent = true, - InputVariables = subAgent.InputVariables.Select(x => new InputVariable - { - Name = x.Name, - Description = x.Description, - IsRequired = true, - AllowDangerouslySetContent = true - }).ToList() - }; - - var subAgentFunction = KernelFunctionFactory.CreateFromPrompt(promptTemplateConfig, loggerFactory: loggerFactory); - var agentPlugin = KernelPluginFactory.CreateFromFunctions(subAgent.Name, subAgent.Description, [subAgentFunction]); - builder.Plugins.Add(agentPlugin); - } - } -} \ No newline at end of file From 814cf6deb38dc0fb0ec27abb3228ce14514db3e8 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 12 Dec 2025 20:39:21 +0100 Subject: [PATCH 10/23] Remove SK-based agent workflows and introduce code-first agents This update removes Semantic Kernel-based multi-agent workflow definitions and implementations. It introduces a new extensibility model with code-first agent registration using `IElsaAgent` and related abstractions. A sample `CopyWriterAndEditorAgent` is added to demonstrate the new code-first agent infrastructure. --- Directory.Packages.props | 447 +++++++++--------- .../Activities/AgentActivity.cs | 23 +- .../Activities/AgentWorkflowActivity.cs | 70 +-- .../AgentActivityProvider.cs | 9 +- .../CodeFirstAgentActivityProvider.cs | 123 +++++ .../Contracts/IAgentDefinition.cs | 22 - .../Contracts/IAgentDefinitionProvider.cs | 12 - .../Contracts/IAgentExecutionContext.cs | 7 + .../Contracts/IAgentExecutionResponse.cs | 6 + .../Contracts/IAgentProvider.cs | 19 + .../Contracts/IAgentResolver.cs | 14 + .../Contracts/IAgentWorkflowDefinition.cs | 22 - .../IAgentWorkflowDefinitionProvider.cs | 12 - .../Elsa.Agents.Core/Contracts/IElsaAgent.cs | 14 + .../Elsa.Agents.Core/Elsa.Agents.Core.csproj | 1 + .../Extensions/AgentConfigExtensions.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 32 -- .../Features/AgentsFeature.cs | 9 +- .../Models/AgentExecutionContext.cs | 7 + .../Models/AgentExecutionResponse.cs | 6 + .../Models/AgentWorkflowResult.cs | 19 +- .../Models/PluginDescriptor.cs | 2 +- .../Options/CodeFirstAgentOptions.cs | 24 + ...tsOptions.cs => ConfiguredAgentOptions.cs} | 2 +- .../Services/AgentDefinitionProvider.cs | 17 - .../Services/AgentFrameworkFactory.cs | 51 +- .../AgentWorkflowDefinitionProvider.cs | 17 - .../Services/AgentWorkflowExecutor.cs | 220 --------- .../Services/CodeFirstAgentProvider.cs | 35 ++ .../ConfigurationKernelConfigProvider.cs | 37 +- .../Services/DefaultAgentResolver.cs | 23 + .../Services/KernelConfigAgentProvider.cs | 26 + .../Services/SemanticKernelElsaAgent.cs | 23 + 33 files changed, 625 insertions(+), 730 deletions(-) create mode 100644 src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinitionProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionContext.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionResponse.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinition.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinitionProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IElsaAgent.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionResponse.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs rename src/modules/agents/Elsa.Agents.Core/Options/{AgentsOptions.cs => ConfiguredAgentOptions.cs} (88%) delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/AgentDefinitionProvider.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowDefinitionProvider.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/DefaultAgentResolver.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/KernelConfigAgentProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/SemanticKernelElsaAgent.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5b621895..d24462e4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,225 +1,226 @@ - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs index 4a57f7a4..4b8a57f8 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs @@ -44,22 +44,23 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; if (inputValue is ExpandoObject expandoObject) - { inputValue = expandoObject.ConvertTo(); - } functionInput[inputDescriptor.Name] = inputValue; } - var agentInvoker = context.GetRequiredService(); - var result = await agentInvoker.InvokeAgentAsync(AgentName, functionInput, context.CancellationToken); - var json = result.ChatMessageContent.Content?.Trim(); - - if (string.IsNullOrWhiteSpace(json)) - throw new InvalidOperationException("The message content is empty or null."); - - json = StripCodeFences(json); + // Resolve the agent via the unified abstraction. + var agentResolver = context.GetRequiredService(); + var agent = await agentResolver.ResolveAsync(AgentName, context.CancellationToken); + // For now, pass the serialized input dictionary as a JSON prompt to the agent + // to keep compatibility with the existing JSON-output expectations. + var agentExecutionContext = new AgentExecutionContext + { + CancellationToken = context.CancellationToken + }; + var agentExecutionResponse = await agent.RunAsync(agentExecutionContext); + var json = StripCodeFences(agentExecutionResponse.Text); 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. @@ -69,7 +70,7 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var converterOptions = new ObjectConverterOptions(SerializerOptions); var outputValue = json.ConvertTo(outputType, converterOptions); var outputDescriptor = activityDescriptor.Outputs.Single(); - var output = (Output)outputDescriptor.ValueGetter(this); + var output = (Output?)outputDescriptor.ValueGetter(this); context.Set(output, outputValue, "Output"); } diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs index e2cb1f50..e02965f7 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs @@ -15,73 +15,15 @@ namespace Elsa.Agents.Activities; /// -/// An activity that executes a multi-agent workflow. This is an internal activity used by . +/// Deprecated: use instead. AgentActivity now supports +/// multi-agent workflows via IAgentResolver, so this type is kept only for +/// backward compatibility and should not be used in new code. /// [Browsable(false)] +[Obsolete("Use AgentActivity instead. AgentActivity resolves both single agents and workflows via IAgentResolver.")] public class AgentWorkflowActivity : 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 WorkflowName { get; set; } = null!; - /// - protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) - { - var activityDescriptor = context.ActivityDescriptor; - var inputDescriptors = activityDescriptor.Inputs; - var workflowInput = new Dictionary(); - - foreach (var inputDescriptor in inputDescriptors) - { - var input = (Input?)inputDescriptor.ValueGetter(this); - var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; - - if (inputValue is ExpandoObject expandoObject) - { - inputValue = expandoObject.ConvertTo(); - } - - workflowInput[inputDescriptor.Name] = inputValue; - } - - var kernelConfigProvider = context.GetRequiredService(); - var workflowExecutor = context.GetRequiredService(); - var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(context.CancellationToken); - - if (!kernelConfig.AgentWorkflows.TryGetValue(WorkflowName, out var workflowConfig)) - throw new InvalidOperationException($"Agent workflow '{WorkflowName}' not found"); - - var result = await workflowExecutor.ExecuteWorkflowAsync(WorkflowName, workflowConfig, workflowInput, context.CancellationToken); - var json = result.Output?.Trim(); - - if (string.IsNullOrWhiteSpace(json)) - throw new InvalidOperationException("The workflow output is empty or null."); - - var outputs = context.ActivityDescriptor.Outputs; - if (outputs.Count != 1) - throw new InvalidOperationException($"Expected exactly one output, but found {outputs.Count}"); - - var outputType = outputs.First().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 = outputs.First(); - var output = outputDescriptor.ValueGetter(this) as Output; - - if (output == null) - throw new InvalidOperationException("Output descriptor did not return a valid Output object"); - - context.Set(output, outputValue); - } + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => + throw new NotSupportedException("AgentWorkflowActivity is deprecated. Use AgentActivity instead."); } diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs index 67c8ff1a..e3ad7e61 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs @@ -108,7 +108,7 @@ private async Task CreateAgentActivityDescriptor(AgentConfig private async Task CreateAgentWorkflowActivityDescriptor(AgentWorkflowConfig workflowConfig, CancellationToken cancellationToken) { - var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentWorkflowActivity), cancellationToken); + var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); var activityTypeName = $"Elsa.Agents.Workflows.{workflowConfig.Name.Pascalize()}"; activityDescriptor.Name = workflowConfig.Name.Pascalize(); activityDescriptor.TypeName = activityTypeName; @@ -117,13 +117,14 @@ private async Task CreateAgentWorkflowActivityDescriptor(Age activityDescriptor.IsBrowsable = true; activityDescriptor.Category = "Agent Workflows"; activityDescriptor.Kind = ActivityKind.Task; - activityDescriptor.CustomProperties["RootType"] = nameof(AgentWorkflowActivity); + activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity); activityDescriptor.Constructor = context => { - var activity = context.CreateActivity(); + var activity = context.CreateActivity(); activity.Type = activityTypeName; - activity.WorkflowName = workflowConfig.Name; + // Workflows will be resolved by name via IAgentResolver. + activity.AgentName = workflowConfig.Name; return activity; }; diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs new file mode 100644 index 00000000..537eafd5 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs @@ -0,0 +1,123 @@ +using System.ComponentModel; +using System.Reflection; +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Extensions; +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using Humanizer; +using JetBrains.Annotations; + +namespace Elsa.Agents.Activities.ActivityProviders; + +/// +/// Provides activities for each code-first agent registered via . +/// Inputs are derived from public properties on the agent type using simple +/// reflection rules. Execution is delegated to +/// via the common abstraction. +/// +[UsedImplicitly] +public class CodeFirstAgentActivityProvider( + CodeFirstAgentOptions codeFirstAgentOptions, + IActivityDescriber activityDescriber, + IWellKnownTypeRegistry wellKnownTypeRegistry) : IActivityProvider +{ + public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var descriptors = new List(); + + foreach (var kvp in codeFirstAgentOptions.CodeFirstAgents) + { + var key = kvp.Key; + var type = kvp.Value; + var descriptor = await CreateDescriptorForAgentAsync(key, type, cancellationToken); + descriptors.Add(descriptor); + } + + return descriptors; + } + + private async Task CreateDescriptorForAgentAsync(string key, Type agentType, CancellationToken cancellationToken) + { + var descriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); + var activityTypeName = $"Elsa.Agents.CodeFirst.{key.Pascalize()}"; + + descriptor.Name = key.Pascalize(); + descriptor.TypeName = activityTypeName; + descriptor.DisplayName = key.Humanize().Transform(To.TitleCase); + descriptor.Category = "Code-First Agents"; + descriptor.Kind = ActivityKind.Task; + descriptor.CustomProperties["RootType"] = nameof(AgentActivity); + + descriptor.Constructor = context => + { + var activity = context.CreateActivity(); + activity.Type = activityTypeName; + activity.AgentName = key; + return activity; + }; + + // Build inputs from public instance properties. + descriptor.Inputs.Clear(); + foreach (var prop in agentType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (!IsInputProperty(prop)) + continue; + + var inputName = prop.Name; + var inputType = prop.PropertyType.FullName ?? "object"; + var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType); + var description = prop.GetCustomAttribute()?.Description; + + var inputDescriptor = new InputDescriptor + { + Name = inputName, + DisplayName = inputName.Humanize(), + Description = description, + Type = nakedInputType, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName), + ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!, + IsSynthetic = true, + IsWrapped = true, + UIHint = ActivityDescriber.GetUIHint(nakedInputType) + }; + + descriptor.Inputs.Add(inputDescriptor); + } + + // For now, expose a single synthetic Output of type object, mirroring + // the existing AgentActivity behavior. + descriptor.Outputs.Clear(); + var outputName = "Output"; + var outputDescriptor = new OutputDescriptor + { + Name = outputName, + Description = "The agent's JSON output.", + Type = typeof(object), + IsSynthetic = true, + ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), + ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, + }; + descriptor.Outputs.Add(outputDescriptor); + + return descriptor; + } + + private static bool IsInputProperty(PropertyInfo prop) + { + // Simple heuristic for now: + // - Must be readable and writable + // - Exclude indexers + if (!prop.CanRead || !prop.CanWrite) + return false; + + if (prop.GetIndexParameters().Length > 0) + return false; + + // In the future, you can add a dedicated [AgentInput] attribute and + // check for it here. For now, treat all simple public properties as + // potential inputs. + return true; + } +} + diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs deleted file mode 100644 index 53112fa9..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinition.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Represents a code-first agent definition that can be registered programmatically. -/// -public interface IAgentDefinition -{ - /// - /// Gets the unique name of the agent. - /// - string Name { get; } - - /// - /// Gets the description of the agent. - /// - string Description { get; } - - /// - /// Gets the agent configuration. - /// - AgentConfig GetAgentConfig(); -} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinitionProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinitionProvider.cs deleted file mode 100644 index c53ab9fb..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentDefinitionProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Provides access to registered agent definitions. -/// -public interface IAgentDefinitionProvider -{ - /// - /// Gets all registered agent definitions. - /// - IEnumerable GetDefinitions(); -} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionContext.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionContext.cs new file mode 100644 index 00000000..e2047661 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionContext.cs @@ -0,0 +1,7 @@ +namespace Elsa.Agents; + +public interface IAgentExecutionContext +{ + string Message { get; set; } + CancellationToken CancellationToken { get; set; } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionResponse.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionResponse.cs new file mode 100644 index 00000000..005864cf --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionResponse.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public interface IAgentExecutionResponse +{ + string Text { get; set; } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentProvider.cs new file mode 100644 index 00000000..f61081d2 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentProvider.cs @@ -0,0 +1,19 @@ +namespace Elsa.Agents; + +/// +/// Contract for pluggable agent providers that can contribute agents to +/// the resolver. Each provider decides which names it supports. +/// +public interface IAgentProvider +{ + /// + /// Returns true if this provider can supply an agent for the specified name. + /// + Task CanProvideAsync(string name, CancellationToken cancellationToken = default); + + /// + /// Creates an IElsaAgent for the specified name. Only called if + /// returned true. + /// + Task CreateAsync(string name, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs new file mode 100644 index 00000000..f12f4cb8 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs @@ -0,0 +1,14 @@ +namespace Elsa.Agents; + +/// +/// Resolves IElsaAgent instances by name, regardless of whether they are +/// defined via Semantic Kernel configuration, code-first MAF agents, or +/// other provider-based sources. +/// +public interface IAgentResolver +{ + /// + /// Resolve an agent by name. Throws if the agent cannot be found. + /// + Task ResolveAsync(string name, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinition.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinition.cs deleted file mode 100644 index a3f2d239..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinition.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Represents a code-first multi-agent workflow (agent team/sequence/graph) that can be registered programmatically. -/// -public interface IAgentWorkflowDefinition -{ - /// - /// Gets the unique name of the agent workflow. - /// - string Name { get; } - - /// - /// Gets the description of the agent workflow. - /// - string Description { get; } - - /// - /// Gets the agent workflow configuration. - /// - AgentWorkflowConfig GetWorkflowConfig(); -} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinitionProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinitionProvider.cs deleted file mode 100644 index 8ecf1e22..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentWorkflowDefinitionProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Provides access to registered agent workflow definitions. -/// -public interface IAgentWorkflowDefinitionProvider -{ - /// - /// Gets all registered agent workflow definitions. - /// - IEnumerable GetDefinitions(); -} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IElsaAgent.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IElsaAgent.cs new file mode 100644 index 00000000..d1c87c50 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IElsaAgent.cs @@ -0,0 +1,14 @@ +namespace Elsa.Agents; + +/// +/// Minimal abstraction over an executable agent so activities and endpoints +/// do not need to know whether the underlying implementation is SK-based, +/// ChatClientAgent-based, or something else. +/// +public interface IElsaAgent +{ + /// + /// Executes the agent with the given context and returns the primary text result. + /// + Task RunAsync(IAgentExecutionContext context); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj index 9b65d3c5..de6c63c5 100644 --- a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj +++ b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj @@ -7,6 +7,7 @@ + diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs index 5afbbab3..c28b8997 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs @@ -10,7 +10,7 @@ public static class AgentConfigExtensions { public static OpenAIPromptExecutionSettings ToOpenAIPromptExecutionSettings(this AgentConfig agentConfig) { - return new OpenAIPromptExecutionSettings + return new() { Temperature = agentConfig.ExecutionSettings.Temperature, TopP = agentConfig.ExecutionSettings.TopP, @@ -31,7 +31,7 @@ public static PromptTemplateConfig ToPromptTemplateConfig(this AgentConfig agent [PromptExecutionSettings.DefaultServiceId] = agentConfig.ToOpenAIPromptExecutionSettings(), }; - return new PromptTemplateConfig + return new() { Name = agentConfig.FunctionName, Description = agentConfig.Description, diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs index e93e28ec..b33fed1c 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs @@ -13,36 +13,4 @@ public static IServiceCollection AddAgentServiceProvider(this IServiceCollect { return services.AddScoped(); } - - /// - /// Registers a code-first agent definition. - /// - public static IServiceCollection AddAgentDefinition(this IServiceCollection services) where T : class, IAgentDefinition - { - return services.AddSingleton(); - } - - /// - /// Registers a code-first agent definition instance. - /// - public static IServiceCollection AddAgentDefinition(this IServiceCollection services, IAgentDefinition agentDefinition) - { - return services.AddSingleton(agentDefinition); - } - - /// - /// Registers a code-first agent workflow definition. - /// - public static IServiceCollection AddAgentWorkflowDefinition(this IServiceCollection services) where T : class, IAgentWorkflowDefinition - { - return services.AddSingleton(); - } - - /// - /// Registers a code-first agent workflow definition instance. - /// - public static IServiceCollection AddAgentWorkflowDefinition(this IServiceCollection services, IAgentWorkflowDefinition workflowDefinition) - { - return services.AddSingleton(workflowDefinition); - } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs index ee51091e..288fe9df 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs @@ -3,6 +3,7 @@ using Elsa.Features.Services; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Elsa.Agents.Features; @@ -23,18 +24,18 @@ public AgentsFeature UseKernelConfigProvider(Func public override void Apply() { - Services.AddOptions(); + Services.AddOptions(); Services .AddScoped() .AddScoped() - .AddScoped() .AddScoped() .AddScoped() - .AddScoped() - .AddScoped() .AddScoped(_kernelConfigProviderFactory) .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() .AddPluginProvider() .AddPluginProvider() .AddAgentServiceProvider() diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs new file mode 100644 index 00000000..d63d3ee4 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs @@ -0,0 +1,7 @@ +namespace Elsa.Agents; + +public class AgentExecutionContext : IAgentExecutionContext +{ + public string Message { get; set; } = null!; + public CancellationToken CancellationToken { get; set; } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionResponse.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionResponse.cs new file mode 100644 index 00000000..a10449c8 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionResponse.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public class AgentExecutionResponse : IAgentExecutionResponse +{ + public string Text { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs index 870b311b..5c32e3c3 100644 --- a/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs +++ b/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs @@ -5,27 +5,20 @@ namespace Elsa.Agents; /// /// Result of executing an agent workflow. /// -public class AgentWorkflowResult +public class AgentWorkflowResult(AgentWorkflowConfig workflowConfig, string output, ChatHistory chatHistory) { - public AgentWorkflowResult(AgentWorkflowConfig workflowConfig, string output, ChatHistory chatHistory) - { - WorkflowConfig = workflowConfig; - Output = output; - ChatHistory = chatHistory; - } - /// /// The workflow configuration that was executed. /// - public AgentWorkflowConfig WorkflowConfig { get; } - + public AgentWorkflowConfig WorkflowConfig { get; } = workflowConfig; + /// /// The final output from the workflow. /// - public string Output { get; } - + public string Output { get; } = output; + /// /// The complete chat history from the workflow execution. /// - public ChatHistory ChatHistory { get; } + public ChatHistory ChatHistory { get; } = chatHistory; } diff --git a/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs b/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs index 31b0c8c7..6d01db18 100644 --- a/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs +++ b/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs @@ -23,7 +23,7 @@ public static PluginDescriptor From(string? name = null) { var pluginType = typeof(TPlugin); var description = pluginType.GetCustomAttribute()?.Description ?? string.Empty; - return new PluginDescriptor + return new() { Name = name ?? pluginType.Name.Replace("Plugin", ""), Description = description, diff --git a/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs new file mode 100644 index 00000000..7c1013d8 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs @@ -0,0 +1,24 @@ +namespace Elsa.Agents; + +/// +/// Options used to configure code-first agents. Developers can register +/// agent types here and have them exposed via IAgentProvider/IAgentResolver. +/// +public class CodeFirstAgentOptions +{ + /// + /// Map from agent key to the implementing type. Keys are case-insensitive. + /// + public IDictionary CodeFirstAgents { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Registers a code-first agent type. If no key is provided, the type name + /// is used as the key. + /// + public CodeFirstAgentOptions AddAgent(string? key = null) where TAgent : class, IElsaAgent + { + key ??= typeof(TAgent).Name; + CodeFirstAgents[key] = typeof(TAgent); + return this; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs similarity index 88% rename from src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs rename to src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs index a1275d99..2f62f233 100644 --- a/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs @@ -1,6 +1,6 @@ namespace Elsa.Agents; -public class AgentsOptions +public class ConfiguredAgentOptions { public ICollection ApiKeys { get; set; } = new List(); public ICollection Services { get; set; } = new List(); diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentDefinitionProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentDefinitionProvider.cs deleted file mode 100644 index dde21178..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentDefinitionProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Default implementation of that collects all registered agent definitions. -/// -public class AgentDefinitionProvider : IAgentDefinitionProvider -{ - private readonly IEnumerable _definitions; - - public AgentDefinitionProvider(IEnumerable definitions) - { - _definitions = definitions; - } - - /// - public IEnumerable GetDefinitions() => _definitions; -} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs index e67e8bc7..9e2ea2d3 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs @@ -13,27 +13,14 @@ namespace Elsa.Agents; /// /// Factory for creating Agent Framework agents from Elsa agent configurations. /// -public class AgentFrameworkFactory +public class AgentFrameworkFactory( + IPluginDiscoverer pluginDiscoverer, + IServiceDiscoverer serviceDiscoverer, + ILoggerFactory loggerFactory, + IServiceProvider serviceProvider, + ILogger logger) { - private readonly IPluginDiscoverer _pluginDiscoverer; - private readonly IServiceDiscoverer _serviceDiscoverer; - private readonly ILoggerFactory _loggerFactory; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public AgentFrameworkFactory( - IPluginDiscoverer pluginDiscoverer, - IServiceDiscoverer serviceDiscoverer, - ILoggerFactory loggerFactory, - IServiceProvider serviceProvider, - ILogger logger) - { - _pluginDiscoverer = pluginDiscoverer; - _serviceDiscoverer = serviceDiscoverer; - _loggerFactory = loggerFactory; - _serviceProvider = serviceProvider; - _logger = logger; - } + private readonly ILoggerFactory _loggerFactory = loggerFactory; /// /// Creates a ChatCompletionAgent from an Elsa agent configuration. @@ -42,7 +29,7 @@ public ChatCompletionAgent CreateAgent(KernelConfig kernelConfig, AgentConfig ag { var kernel = CreateKernel(kernelConfig, agentConfig); - return new ChatCompletionAgent + return new() { Name = agentConfig.Name, Description = agentConfig.Description, @@ -51,6 +38,16 @@ public ChatCompletionAgent CreateAgent(KernelConfig kernelConfig, AgentConfig ag }; } + /// + /// Creates an IElsaAgent adapter for a given configuration, so callers can + /// work against a unified abstraction regardless of the underlying implementation. + /// + public IElsaAgent CreateElsaAgent(KernelConfig kernelConfig, AgentConfig agentConfig) + { + var skAgent = CreateAgent(kernelConfig, agentConfig); + return new SemanticKernelElsaAgent(skAgent); + } + /// /// Creates a Kernel configured for the specified agent. /// @@ -67,13 +64,13 @@ private Kernel CreateKernel(KernelConfig kernelConfig, AgentConfig agentConfig) private void ApplyAgentConfig(IKernelBuilder builder, KernelConfig kernelConfig, AgentConfig agentConfig) { - var services = _serviceDiscoverer.Discover().ToDictionary(x => x.Name); + 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"); + logger.LogWarning($"Service {serviceName} not found"); continue; } @@ -87,7 +84,7 @@ private void AddService(IKernelBuilder builder, KernelConfig kernelConfig, Servi { if (!services.TryGetValue(serviceConfig.Type, out var serviceProvider)) { - _logger.LogWarning($"Service provider {serviceConfig.Type} not found"); + logger.LogWarning($"Service provider {serviceConfig.Type} not found"); return; } @@ -97,17 +94,17 @@ private void AddService(IKernelBuilder builder, KernelConfig kernelConfig, Servi private void AddPlugins(IKernelBuilder builder, AgentConfig agent) { - var plugins = _pluginDiscoverer.GetPluginDescriptors().ToDictionary(x => x.Name); + 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"); + logger.LogWarning($"Plugin {pluginName} not found"); continue; } var pluginType = pluginDescriptor.PluginType; - var pluginInstance = ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, pluginType); + var pluginInstance = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, pluginType); builder.Plugins.AddFromObject(pluginInstance, pluginName); } } diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowDefinitionProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowDefinitionProvider.cs deleted file mode 100644 index 8de63c59..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowDefinitionProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Default implementation of that collects all registered agent workflow definitions. -/// -public class AgentWorkflowDefinitionProvider : IAgentWorkflowDefinitionProvider -{ - private readonly IEnumerable _definitions; - - public AgentWorkflowDefinitionProvider(IEnumerable definitions) - { - _definitions = definitions; - } - - /// - public IEnumerable GetDefinitions() => _definitions; -} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs deleted file mode 100644 index b3cb51e7..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentWorkflowExecutor.cs +++ /dev/null @@ -1,220 +0,0 @@ -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.Chat; -using Microsoft.SemanticKernel.ChatCompletion; - -#pragma warning disable SKEXP0001 -#pragma warning disable SKEXP0010 -#pragma warning disable SKEXP0110 - -namespace Elsa.Agents; - -/// -/// Executes multi-agent workflows using the Agent Framework. -/// -public class AgentWorkflowExecutor -{ - private readonly AgentFrameworkFactory _agentFactory; - private readonly IKernelConfigProvider _kernelConfigProvider; - - public AgentWorkflowExecutor( - AgentFrameworkFactory agentFactory, - IKernelConfigProvider kernelConfigProvider) - { - _agentFactory = agentFactory; - _kernelConfigProvider = kernelConfigProvider; - } - - /// - /// Executes an agent workflow and returns the result. - /// - public async Task ExecuteWorkflowAsync( - string workflowName, - AgentWorkflowConfig workflowConfig, - IDictionary input, - CancellationToken cancellationToken = default) - { - var kernelConfig = await _kernelConfigProvider.GetKernelConfigAsync(cancellationToken); - - // Create agents for the workflow - var agents = new List(); - foreach (var agentName in workflowConfig.Agents) - { - if (!kernelConfig.Agents.TryGetValue(agentName, out var agentConfig)) - continue; - - var agent = _agentFactory.CreateAgent(kernelConfig, agentConfig); - agents.Add(agent); - } - - if (agents.Count == 0) - throw new InvalidOperationException($"No agents found for workflow '{workflowName}'"); - - // Create chat history with input - ChatHistory chatHistory = []; - - // Format input as user message - var inputMessage = FormatInput(input, workflowConfig); - chatHistory.AddUserMessage(inputMessage); - - // Execute workflow based on type - var result = workflowConfig.WorkflowType switch - { - AgentWorkflowType.Sequential => await ExecuteSequentialWorkflowAsync(agents, chatHistory, workflowConfig, cancellationToken), - AgentWorkflowType.Graph => await ExecuteGraphWorkflowAsync(agents, chatHistory, workflowConfig, cancellationToken), - _ => throw new NotSupportedException($"Workflow type {workflowConfig.WorkflowType} is not supported") - }; - - return new(workflowConfig, result, chatHistory); - } - - private async Task ExecuteSequentialWorkflowAsync( - List agents, - ChatHistory chatHistory, - AgentWorkflowConfig config, - CancellationToken cancellationToken) - { - var agentChat = new AgentGroupChat(agents.ToArray()); - - // Configure termination - ConfigureTermination(agentChat, config); - - // Execute chat until termination - await foreach (var message in agentChat.InvokeAsync(cancellationToken)) - { - chatHistory.Add(message); - } - - // Return the last assistant message - var lastMessage = chatHistory.LastOrDefault(m => m.Role == AuthorRole.Assistant); - return lastMessage?.Content ?? string.Empty; - } - - private async Task ExecuteGraphWorkflowAsync( - List agents, - ChatHistory chatHistory, - AgentWorkflowConfig config, - CancellationToken cancellationToken) - { - // Create agent chat with selection strategy - var agentChat = new AgentGroupChat(agents.ToArray()); - - // Configure selection strategy if specified - if (config.SelectionStrategy != null) - { - ConfigureSelectionStrategy(agentChat, config.SelectionStrategy, agents); - } - - // Configure termination - ConfigureTermination(agentChat, config); - - // Execute chat - await foreach (var message in agentChat.InvokeAsync(cancellationToken)) - { - chatHistory.Add(message); - } - - var lastMessage = chatHistory.LastOrDefault(m => m.Role == AuthorRole.Assistant); - return lastMessage?.Content ?? string.Empty; - } - - private void ConfigureTermination(AgentGroupChat agentChat, AgentWorkflowConfig config) - { - // Configure termination based on the configuration - switch (config.Termination.Type) - { - case TerminationType.MaxMessages: - agentChat.ExecutionSettings = new() - { - TerminationStrategy = new MaxChatHistoryTerminationStrategy(config.Termination.MaxMessages) - }; - break; - case TerminationType.Keyword: - if (!string.IsNullOrEmpty(config.Termination.TerminationKeyword)) - { - agentChat.ExecutionSettings = new() - { - TerminationStrategy = new KeywordTerminationStrategy(config.Termination.TerminationKeyword) - }; - } - break; - // AgentDecision termination would require custom implementation - } - } - - private void ConfigureSelectionStrategy( - AgentGroupChat agentChat, - SelectionStrategyConfig strategyConfig, - List agents) - { - // Configure agent selection based on strategy type - // Note: The actual Agent Framework API may vary; this is a conceptual implementation - switch (strategyConfig.Type) - { - case SelectionStrategyType.Sequential: - agentChat.ExecutionSettings = new() - { - SelectionStrategy = new SequentialSelectionStrategy() - }; - break; - case SelectionStrategyType.RoundRobin: - // Round-robin is similar to sequential in most implementations - agentChat.ExecutionSettings = new() - { - SelectionStrategy = new SequentialSelectionStrategy() - }; - break; - // LLMBased and AgentBased would require custom strategies - } - } - - private string FormatInput(IDictionary input, AgentWorkflowConfig config) - { - if (input.Count == 0) - return "Please begin the conversation."; - - var parts = input.Select(kvp => $"{kvp.Key}: {kvp.Value}"); - return string.Join("\n", parts); - } -} - -/// -/// Simple termination strategy based on maximum chat history length. -/// -internal class MaxChatHistoryTerminationStrategy : TerminationStrategy -{ - private readonly int _maxMessages; - - public MaxChatHistoryTerminationStrategy(int maxMessages) - { - _maxMessages = maxMessages; - } - - protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) - { - // Terminate when history length reaches or exceeds the maximum - return Task.FromResult(history.Count >= _maxMessages); - } -} - -/// -/// Termination strategy based on keyword detection. -/// -internal class KeywordTerminationStrategy : TerminationStrategy -{ - private readonly string _keyword; - - public KeywordTerminationStrategy(string keyword) - { - _keyword = keyword; - } - - protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) - { - var lastMessage = history.LastOrDefault(); - if (lastMessage?.Content == null) - return Task.FromResult(false); - - return Task.FromResult(lastMessage.Content.Contains(_keyword, StringComparison.OrdinalIgnoreCase)); - } -} diff --git a/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentProvider.cs new file mode 100644 index 00000000..7fa6c554 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentProvider.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Elsa.Agents; + +/// +/// Agent provider that exposes code-first agents registered via AgentOptions. +/// Types must be registered in DI and implement IElsaAgent. +/// +public class CodeFirstAgentProvider(IOptions options, IServiceProvider serviceProvider) : IAgentProvider +{ + public Task CanProvideAsync(string name, CancellationToken cancellationToken = default) + { + var agents = options.Value.CodeFirstAgents; + var canProvide = agents.ContainsKey(name); + return Task.FromResult(canProvide); + } + + public Task CreateAsync(string name, CancellationToken cancellationToken = default) + { + var agents = options.Value.CodeFirstAgents; + + if (!agents.TryGetValue(name, out var agentType)) + throw new InvalidOperationException($"No code-first agent registered for key '{name}'."); + + var instance = serviceProvider.GetRequiredService(agentType); + + if (instance is not IElsaAgent elsaAgent) + throw new InvalidOperationException( + $"Type '{agentType.FullName}' registered for key '{name}' does not implement IElsaAgent."); + + return Task.FromResult(elsaAgent); + } +} + diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs index 9adf6c3c..0cf0f07f 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs @@ -7,46 +7,31 @@ namespace Elsa.Agents; /// Provides kernel configuration by merging configuration-based agents with code-first definitions. /// [UsedImplicitly] -public class ConfigurationKernelConfigProvider( - IOptions options, - IAgentDefinitionProvider agentDefinitionProvider, - IAgentWorkflowDefinitionProvider workflowDefinitionProvider) : IKernelConfigProvider +public class ConfigurationKernelConfigProvider(IOptions options) : IKernelConfigProvider { public Task GetKernelConfigAsync(CancellationToken cancellationToken = default) { var kernelConfig = new KernelConfig(); - + // Add configuration-based items (if available) - if (options.Value.ApiKeys != null) + if (options.Value.ApiKeys != null!) { - foreach (var apiKey in options.Value.ApiKeys) + foreach (var apiKey in options.Value.ApiKeys) kernelConfig.ApiKeys[apiKey.Name] = apiKey; } - if (options.Value.Services != null) + + if (options.Value.Services != null!) { - foreach (var service in options.Value.Services) + foreach (var service in options.Value.Services) kernelConfig.Services[service.Name] = service; } - if (options.Value.Agents != null) + + if (options.Value.Agents != null!) { - foreach (var agent in options.Value.Agents) + foreach (var agent in options.Value.Agents) kernelConfig.Agents[agent.Name] = agent; } - - // Add code-first agents - foreach (var agentDef in agentDefinitionProvider.GetDefinitions()) - { - var agentConfig = agentDef.GetAgentConfig(); - kernelConfig.Agents[agentDef.Name] = agentConfig; - } - - // Add code-first agent workflows - foreach (var workflowDef in workflowDefinitionProvider.GetDefinitions()) - { - var workflowConfig = workflowDef.GetWorkflowConfig(); - kernelConfig.AgentWorkflows[workflowDef.Name] = workflowConfig; - } - + return Task.FromResult(kernelConfig); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/DefaultAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Services/DefaultAgentResolver.cs new file mode 100644 index 00000000..b9fc9706 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/DefaultAgentResolver.cs @@ -0,0 +1,23 @@ +namespace Elsa.Agents; + +/// +/// Default implementation of that queries a +/// collection of instances to resolve agents by +/// name. The first provider that reports it can handle the name is used. +/// +public class DefaultAgentResolver(IEnumerable providers) : IAgentResolver +{ + public async Task ResolveAsync(string name, CancellationToken cancellationToken = default) + { + foreach (var provider in providers) + { + if (!await provider.CanProvideAsync(name, cancellationToken)) + continue; + + return await provider.CreateAsync(name, cancellationToken); + } + + throw new InvalidOperationException($"No agent provider could resolve an agent named '{name}'."); + } +} + diff --git a/src/modules/agents/Elsa.Agents.Core/Services/KernelConfigAgentProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/KernelConfigAgentProvider.cs new file mode 100644 index 00000000..e9782404 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/KernelConfigAgentProvider.cs @@ -0,0 +1,26 @@ +namespace Elsa.Agents; + +/// +/// Agent provider that exposes Semantic Kernel-configured agents via +/// and . +/// +public class KernelConfigAgentProvider( + IKernelConfigProvider kernelConfigProvider, + AgentFrameworkFactory agentFrameworkFactory) : IAgentProvider +{ + public async Task CanProvideAsync(string name, CancellationToken cancellationToken = default) + { + var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); + return kernelConfig.Agents.ContainsKey(name); + } + + public async Task CreateAsync(string name, CancellationToken cancellationToken = default) + { + var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); + + if (!kernelConfig.Agents.TryGetValue(name, out var agentConfig)) + throw new InvalidOperationException($"Agent '{name}' not found in KernelConfig."); + + return agentFrameworkFactory.CreateElsaAgent(kernelConfig, agentConfig); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/SemanticKernelElsaAgent.cs b/src/modules/agents/Elsa.Agents.Core/Services/SemanticKernelElsaAgent.cs new file mode 100644 index 00000000..356edf00 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/SemanticKernelElsaAgent.cs @@ -0,0 +1,23 @@ +using Microsoft.SemanticKernel.Agents; + +namespace Elsa.Agents; + +/// +/// IElsaAgent adapter over a Semantic Kernel ChatCompletionAgent. +/// +public class SemanticKernelElsaAgent(ChatCompletionAgent innerAgent) : IElsaAgent +{ + public async Task RunAsync(IAgentExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var result = await innerAgent.InvokeAsync(context.Message, cancellationToken: cancellationToken).LastOrDefaultAsync(cancellationToken); + if (result is null) + throw new InvalidOperationException("Agent did not produce a response."); + + var responseMessage = result.Message.Content ?? throw new InvalidOperationException("Agent did not produce a response."); + return new AgentExecutionResponse + { + Text = responseMessage + }; + } +} From d86dbd735e70fffd607479afc0994468b1419ca3 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 13 Dec 2025 13:24:29 +0100 Subject: [PATCH 11/23] Refactor agent activities and improve dependency injection Updated agent activity handling to support property mapping, improved JSON response detection, and added a code-first activity provider. Enhanced dependency injection with new agent options and packages, and modified OpenAI client API key retrieval. Refined descriptors and workflows for better configurability and clarity. --- .../Activities/AgentActivity.cs | 26 ++++++++++++++----- .../AgentActivityProvider.cs | 2 +- .../CodeFirstAgentActivityProvider.cs | 10 ++++--- .../Features/AgentActivitiesFeature.cs | 1 + .../Features/AgentsFeature.cs | 1 + 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs index 4b8a57f8..ace0ae0b 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs @@ -52,27 +52,41 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context // Resolve the agent via the unified abstraction. var agentResolver = context.GetRequiredService(); var agent = await agentResolver.ResolveAsync(AgentName, context.CancellationToken); + var agentType = agent.GetType(); + var agentPropertyLookup = agentType.GetProperties().ToDictionary(x => x.Name, x => x); - // For now, pass the serialized input dictionary as a JSON prompt to the agent - // to keep compatibility with the existing JSON-output expectations. + // Copy activity input descriptor values into the agent public properties: + foreach (var inputDescriptor in inputDescriptors) + { + var input = (Input?)inputDescriptor.ValueGetter(this); + var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; + agentPropertyLookup[inputDescriptor.Name].SetValue(agent, inputValue); + } + var agentExecutionContext = new AgentExecutionContext { CancellationToken = context.CancellationToken }; var agentExecutionResponse = await agent.RunAsync(agentExecutionContext); - var json = StripCodeFences(agentExecutionResponse.Text); + var responseText = StripCodeFences(agentExecutionResponse.Text); + var isJsonResponse = IsJsonResponse(responseText); var outputType = context.ActivityDescriptor.Outputs.Single().Type; - // If the target type is object, we want the JSON to be deserialized into an ExpandoObject for dynamic field access. - if (outputType == typeof(object)) + // If the target type is object and the response is in JSON format, we want it to be deserialized into an ExpandoObject for dynamic field access. + if (outputType == typeof(object) && isJsonResponse) outputType = typeof(ExpandoObject); var converterOptions = new ObjectConverterOptions(SerializerOptions); - var outputValue = json.ConvertTo(outputType, converterOptions); + var outputValue = isJsonResponse ? responseText.ConvertTo(outputType, converterOptions) : responseText; var outputDescriptor = activityDescriptor.Outputs.Single(); var output = (Output?)outputDescriptor.ValueGetter(this); context.Set(output, outputValue, "Output"); } + + private static bool IsJsonResponse(string text) + { + return text.StartsWith("{", StringComparison.OrdinalIgnoreCase) || text.StartsWith("[", StringComparison.OrdinalIgnoreCase); + } private static string StripCodeFences(string content) { diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs index e3ad7e61..76c600c9 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs @@ -55,7 +55,7 @@ private async Task CreateAgentActivityDescriptor(AgentConfig activityDescriptor.IsBrowsable = true; activityDescriptor.Category = "Agents"; activityDescriptor.Kind = ActivityKind.Task; - activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity); + activityDescriptor.ClrType = typeof(AgentActivity); activityDescriptor.Constructor = context => { diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs index 537eafd5..16657dd2 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs @@ -7,6 +7,7 @@ using Elsa.Workflows.Models; using Humanizer; using JetBrains.Annotations; +using Microsoft.Extensions.Options; namespace Elsa.Agents.Activities.ActivityProviders; @@ -18,7 +19,7 @@ namespace Elsa.Agents.Activities.ActivityProviders; /// [UsedImplicitly] public class CodeFirstAgentActivityProvider( - CodeFirstAgentOptions codeFirstAgentOptions, + IOptions codeFirstAgentOptions, IActivityDescriber activityDescriber, IWellKnownTypeRegistry wellKnownTypeRegistry) : IActivityProvider { @@ -26,7 +27,7 @@ public async ValueTask> GetDescriptorsAsync(Canc { var descriptors = new List(); - foreach (var kvp in codeFirstAgentOptions.CodeFirstAgents) + foreach (var kvp in codeFirstAgentOptions.Value.CodeFirstAgents) { var key = kvp.Key; var type = kvp.Value; @@ -47,7 +48,8 @@ private async Task CreateDescriptorForAgentAsync(string key, descriptor.DisplayName = key.Humanize().Transform(To.TitleCase); descriptor.Category = "Code-First Agents"; descriptor.Kind = ActivityKind.Task; - descriptor.CustomProperties["RootType"] = nameof(AgentActivity); + descriptor.IsBrowsable = true; + descriptor.ClrType = agentType; descriptor.Constructor = context => { @@ -92,7 +94,7 @@ private async Task CreateDescriptorForAgentAsync(string key, var outputDescriptor = new OutputDescriptor { Name = outputName, - Description = "The agent's JSON output.", + Description = "The agent's output.", Type = typeof(object), IsSynthetic = true, ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), diff --git a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs index 059738c6..60218128 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs @@ -23,6 +23,7 @@ public override void Apply() { Services .AddActivityProvider() + .AddActivityProvider() .AddNotificationHandler() ; } diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs index 288fe9df..6c8ff5f0 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs @@ -25,6 +25,7 @@ public AgentsFeature UseKernelConfigProvider(Func(); + Services.AddOptions(); Services .AddScoped() From 97a3299ff68ab3365a52ba2ad431cb6193e09c26 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 13 Dec 2025 14:42:02 +0100 Subject: [PATCH 12/23] Refactor agent workflow infrastructure to simplify providers. Replaced legacy agent workflow classes with streamlined implementations, consolidating agent resolution and execution. Introduced `CodeFirstAgentResolver` and `CodeFirstAgentActivity` for better modularity and flexibility. Removed obsolete types and redundant code to enhance maintainability. --- Directory.Packages.props | 450 +++++++++--------- .../Activities/AgentWorkflowActivity.cs | 29 -- ...tActivity.cs => CodeFirstAgentActivity.cs} | 20 +- .../Activities/ConfiguredAgentActivity.cs | 83 ++++ .../CodeFirstAgentActivityProvider.cs | 6 +- ....cs => ConfiguredAgentActivityProvider.cs} | 77 +-- .../Contracts/{IElsaAgent.cs => IAgent.cs} | 0 .../Contracts/IAgentProvider.cs | 19 - .../Contracts/IAgentResolver.cs | 14 - .../Contracts/ICodeFirstAgentResolver.cs | 6 + .../Elsa.Agents.Core/Elsa.Agents.Core.csproj | 32 +- .../CodeFirstAgentResolverExtensions.cs | 10 + .../Features/AgentsFeature.cs | 4 +- .../Services/CodeFirstAgentProvider.cs | 35 -- .../Services/CodeFirstAgentResolver.cs | 14 + .../Services/DefaultAgentResolver.cs | 23 - .../Services/KernelConfigAgentProvider.cs | 26 - 17 files changed, 373 insertions(+), 475 deletions(-) delete mode 100644 src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs rename src/modules/agents/Elsa.Agents.Activities/Activities/{AgentActivity.cs => CodeFirstAgentActivity.cs} (90%) create mode 100644 src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs rename src/modules/agents/Elsa.Agents.Activities/ActivityProviders/{AgentActivityProvider.cs => ConfiguredAgentActivityProvider.cs} (52%) rename src/modules/agents/Elsa.Agents.Core/Contracts/{IElsaAgent.cs => IAgent.cs} (100%) delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentProvider.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/ICodeFirstAgentResolver.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentResolver.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/DefaultAgentResolver.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/KernelConfigAgentProvider.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index d24462e4..030cc2d4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,226 +1,228 @@ - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs deleted file mode 100644 index e02965f7..00000000 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentWorkflowActivity.cs +++ /dev/null @@ -1,29 +0,0 @@ -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.Agents.Activities.ActivityProviders; -using Elsa.Workflows; -using Elsa.Workflows.Models; -using Elsa.Workflows.Serialization.Converters; - -namespace Elsa.Agents.Activities; - -/// -/// Deprecated: use instead. AgentActivity now supports -/// multi-agent workflows via IAgentResolver, so this type is kept only for -/// backward compatibility and should not be used in new code. -/// -[Browsable(false)] -[Obsolete("Use AgentActivity instead. AgentActivity resolves both single agents and workflows via IAgentResolver.")] -public class AgentWorkflowActivity : CodeActivity -{ - /// - protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => - throw new NotSupportedException("AgentWorkflowActivity is deprecated. Use AgentActivity instead."); -} diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs similarity index 90% rename from src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs rename to src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs index ace0ae0b..9e83a6cd 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs @@ -11,6 +11,8 @@ using Elsa.Workflows; using Elsa.Workflows.Models; using Elsa.Workflows.Serialization.Converters; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel.Agents; namespace Elsa.Agents.Activities; @@ -18,7 +20,7 @@ namespace Elsa.Agents.Activities; /// An activity that executes a function of a skilled agent. This is an internal activity that is used by . /// [Browsable(false)] -public class AgentActivity : CodeActivity +public class CodeFirstAgentActivity : CodeActivity { private static JsonSerializerOptions? _serializerOptions; @@ -34,6 +36,7 @@ public class AgentActivity : CodeActivity /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { + var cancellationToken = context.CancellationToken; var activityDescriptor = context.ActivityDescriptor; var inputDescriptors = activityDescriptor.Inputs; var functionInput = new Dictionary(); @@ -45,13 +48,13 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context if (inputValue is ExpandoObject expandoObject) inputValue = expandoObject.ConvertTo(); - + functionInput[inputDescriptor.Name] = inputValue; } // Resolve the agent via the unified abstraction. - var agentResolver = context.GetRequiredService(); - var agent = await agentResolver.ResolveAsync(AgentName, context.CancellationToken); + var agentResolver = context.GetRequiredService(); + var agent = await agentResolver.ResolveAsync(AgentName, cancellationToken); var agentType = agent.GetType(); var agentPropertyLookup = agentType.GetProperties().ToDictionary(x => x.Name, x => x); @@ -62,11 +65,8 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; agentPropertyLookup[inputDescriptor.Name].SetValue(agent, inputValue); } - - var agentExecutionContext = new AgentExecutionContext - { - CancellationToken = context.CancellationToken - }; + + var agentExecutionContext = new AgentExecutionContext { CancellationToken = context.CancellationToken }; var agentExecutionResponse = await agent.RunAsync(agentExecutionContext); var responseText = StripCodeFences(agentExecutionResponse.Text); var isJsonResponse = IsJsonResponse(responseText); @@ -87,7 +87,7 @@ private static bool IsJsonResponse(string text) { return text.StartsWith("{", StringComparison.OrdinalIgnoreCase) || text.StartsWith("[", StringComparison.OrdinalIgnoreCase); } - + private static string StripCodeFences(string content) { var trimmed = content.Trim(); diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs new file mode 100644 index 00000000..f543afaf --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs @@ -0,0 +1,83 @@ +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.Expressions.Helpers; +using Elsa.Extensions; +using Elsa.Agents.Activities.ActivityProviders; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using Elsa.Workflows.Serialization.Converters; + +namespace Elsa.Agents.Activities; + +/// +/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . +/// +[Browsable(false)] +public class ConfiguredAgentActivity : 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; + + if (inputValue is ExpandoObject expandoObject) + inputValue = expandoObject.ConvertTo(); + + functionInput[inputDescriptor.Name] = inputValue; + } + + var agentInvoker = context.GetRequiredService(); + var agentExecutionResponse = await agentInvoker.InvokeAgentAsync(AgentName, functionInput, context.CancellationToken); + var responseText = StripCodeFences(agentExecutionResponse.ChatMessageContent.Content!); + var isJsonResponse = IsJsonResponse(responseText); + var outputType = context.ActivityDescriptor.Outputs.Single().Type; + + // If the target type is object and the response is in JSON format, we want it to be deserialized into an ExpandoObject for dynamic field access. + if (outputType == typeof(object) && isJsonResponse) + outputType = typeof(ExpandoObject); + + var converterOptions = new ObjectConverterOptions(SerializerOptions); + var outputValue = isJsonResponse ? responseText.ConvertTo(outputType, converterOptions) : responseText; + var outputDescriptor = activityDescriptor.Outputs.Single(); + var output = (Output?)outputDescriptor.ValueGetter(this); + context.Set(output, outputValue, "Output"); + } + + private static bool IsJsonResponse(string text) + { + return text.StartsWith("{", StringComparison.OrdinalIgnoreCase) || text.StartsWith("[", StringComparison.OrdinalIgnoreCase); + } + + private static string StripCodeFences(string content) + { + var trimmed = content.Trim(); + + if (!trimmed.StartsWith("```", StringComparison.Ordinal)) + return trimmed; + + var lines = trimmed.Split('\n'); + return lines.Length < 2 ? trimmed : string.Join('\n', lines.Skip(1).Take(lines.Length - 2)).Trim(); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs index 16657dd2..73384e21 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs @@ -40,7 +40,7 @@ public async ValueTask> GetDescriptorsAsync(Canc private async Task CreateDescriptorForAgentAsync(string key, Type agentType, CancellationToken cancellationToken) { - var descriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); + var descriptor = await activityDescriber.DescribeActivityAsync(typeof(CodeFirstAgentActivity), cancellationToken); var activityTypeName = $"Elsa.Agents.CodeFirst.{key.Pascalize()}"; descriptor.Name = key.Pascalize(); @@ -49,11 +49,11 @@ private async Task CreateDescriptorForAgentAsync(string key, descriptor.Category = "Code-First Agents"; descriptor.Kind = ActivityKind.Task; descriptor.IsBrowsable = true; - descriptor.ClrType = agentType; + descriptor.ClrType = typeof(CodeFirstAgentActivity); descriptor.Constructor = context => { - var activity = context.CreateActivity(); + var activity = context.CreateActivity(); activity.Type = activityTypeName; activity.AgentName = key; return activity; diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs similarity index 52% rename from src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs rename to src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs index 76c600c9..d401bb0c 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs @@ -33,20 +33,12 @@ public async ValueTask> GetDescriptorsAsync(Canc activityDescriptors.Add(descriptor); } - // Add descriptors for agent workflows - foreach (var kvp in kernelConfig.AgentWorkflows) - { - var workflowConfig = kvp.Value; - var descriptor = await CreateAgentWorkflowActivityDescriptor(workflowConfig, cancellationToken); - activityDescriptors.Add(descriptor); - } - return activityDescriptors; } private async Task CreateAgentActivityDescriptor(AgentConfig agentConfig, CancellationToken cancellationToken) { - var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); + var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(ConfiguredAgentActivity), cancellationToken); var activityTypeName = $"Elsa.Agents.{agentConfig.Name.Pascalize()}"; activityDescriptor.Name = agentConfig.Name.Pascalize(); activityDescriptor.TypeName = activityTypeName; @@ -55,11 +47,11 @@ private async Task CreateAgentActivityDescriptor(AgentConfig activityDescriptor.IsBrowsable = true; activityDescriptor.Category = "Agents"; activityDescriptor.Kind = ActivityKind.Task; - activityDescriptor.ClrType = typeof(AgentActivity); + activityDescriptor.ClrType = typeof(ConfiguredAgentActivity); activityDescriptor.Constructor = context => { - var activity = context.CreateActivity(); + var activity = context.CreateActivity(); activity.Type = activityTypeName; activity.AgentName = agentConfig.Name; return activity; @@ -105,67 +97,4 @@ private async Task CreateAgentActivityDescriptor(AgentConfig return activityDescriptor; } - - private async Task CreateAgentWorkflowActivityDescriptor(AgentWorkflowConfig workflowConfig, CancellationToken cancellationToken) - { - var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); - var activityTypeName = $"Elsa.Agents.Workflows.{workflowConfig.Name.Pascalize()}"; - activityDescriptor.Name = workflowConfig.Name.Pascalize(); - activityDescriptor.TypeName = activityTypeName; - activityDescriptor.Description = workflowConfig.Description; - activityDescriptor.DisplayName = workflowConfig.Name.Humanize().Transform(To.TitleCase); - activityDescriptor.IsBrowsable = true; - activityDescriptor.Category = "Agent Workflows"; - activityDescriptor.Kind = ActivityKind.Task; - activityDescriptor.CustomProperties["RootType"] = nameof(AgentActivity); - - activityDescriptor.Constructor = context => - { - var activity = context.CreateActivity(); - activity.Type = activityTypeName; - // Workflows will be resolved by name via IAgentResolver. - activity.AgentName = workflowConfig.Name; - return activity; - }; - - activityDescriptor.Inputs.Clear(); - - foreach (var inputVariable in workflowConfig.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 = workflowConfig.OutputVariable; - var outputType = outputVariable.Type == null! ? "object" : outputVariable.Type; - var nakedOutputType = wellKnownTypeRegistry.GetTypeOrDefault(outputType); - var outputName = "Output"; - var outputDescriptor = new OutputDescriptor - { - Name = outputName, - Description = workflowConfig.OutputVariable.Description, - Type = nakedOutputType, - IsSynthetic = true, - ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), - ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, - }; - activityDescriptor.Outputs.Add(outputDescriptor); - - return activityDescriptor; - } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IElsaAgent.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs similarity index 100% rename from src/modules/agents/Elsa.Agents.Core/Contracts/IElsaAgent.cs rename to src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentProvider.cs deleted file mode 100644 index f61081d2..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Contract for pluggable agent providers that can contribute agents to -/// the resolver. Each provider decides which names it supports. -/// -public interface IAgentProvider -{ - /// - /// Returns true if this provider can supply an agent for the specified name. - /// - Task CanProvideAsync(string name, CancellationToken cancellationToken = default); - - /// - /// Creates an IElsaAgent for the specified name. Only called if - /// returned true. - /// - Task CreateAsync(string name, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs deleted file mode 100644 index f12f4cb8..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Resolves IElsaAgent instances by name, regardless of whether they are -/// defined via Semantic Kernel configuration, code-first MAF agents, or -/// other provider-based sources. -/// -public interface IAgentResolver -{ - /// - /// Resolve an agent by name. Throws if the agent cannot be found. - /// - Task ResolveAsync(string name, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/ICodeFirstAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ICodeFirstAgentResolver.cs new file mode 100644 index 00000000..a2900c8b --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/ICodeFirstAgentResolver.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public interface ICodeFirstAgentResolver +{ + Task ResolveAsync(string agentName, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj index de6c63c5..4fe13aaa 100644 --- a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj +++ b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj @@ -7,29 +7,31 @@ - - - - - - - - - - - + + + + + + + + + + + + + - + - + - + - + diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs new file mode 100644 index 00000000..aee8474b --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs @@ -0,0 +1,10 @@ +namespace Elsa.Agents; + +public static class CodeFirstAgentResolverExtensions +{ + public static async Task ResolveAsync(this ICodeFirstAgentResolver resolver, CancellationToken cancellationToken = default) where TAgent : IElsaAgent + { + var agentName = typeof(TAgent).Name; + return (TAgent)await resolver.ResolveAsync(agentName, cancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs index 6c8ff5f0..a6b032c0 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs @@ -34,9 +34,7 @@ public override void Apply() .AddScoped() .AddScoped(_kernelConfigProviderFactory) .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() + .AddScoped() .AddPluginProvider() .AddPluginProvider() .AddAgentServiceProvider() diff --git a/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentProvider.cs deleted file mode 100644 index 7fa6c554..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentProvider.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Elsa.Agents; - -/// -/// Agent provider that exposes code-first agents registered via AgentOptions. -/// Types must be registered in DI and implement IElsaAgent. -/// -public class CodeFirstAgentProvider(IOptions options, IServiceProvider serviceProvider) : IAgentProvider -{ - public Task CanProvideAsync(string name, CancellationToken cancellationToken = default) - { - var agents = options.Value.CodeFirstAgents; - var canProvide = agents.ContainsKey(name); - return Task.FromResult(canProvide); - } - - public Task CreateAsync(string name, CancellationToken cancellationToken = default) - { - var agents = options.Value.CodeFirstAgents; - - if (!agents.TryGetValue(name, out var agentType)) - throw new InvalidOperationException($"No code-first agent registered for key '{name}'."); - - var instance = serviceProvider.GetRequiredService(agentType); - - if (instance is not IElsaAgent elsaAgent) - throw new InvalidOperationException( - $"Type '{agentType.FullName}' registered for key '{name}' does not implement IElsaAgent."); - - return Task.FromResult(elsaAgent); - } -} - diff --git a/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentResolver.cs new file mode 100644 index 00000000..d839a010 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentResolver.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Elsa.Agents; + +public class CodeFirstAgentResolver(IServiceProvider serviceProvider, IOptions options) : ICodeFirstAgentResolver +{ + public Task ResolveAsync(string agentName, CancellationToken cancellationToken = default) + { + var agentType = options.Value.CodeFirstAgents[agentName] ?? throw new InvalidOperationException($"No agent with name '{agentName}' was found."); + var agent = (IElsaAgent)ActivatorUtilities.CreateInstance(serviceProvider, agentType)!; + return Task.FromResult(agent); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/DefaultAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Services/DefaultAgentResolver.cs deleted file mode 100644 index b9fc9706..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/DefaultAgentResolver.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Default implementation of that queries a -/// collection of instances to resolve agents by -/// name. The first provider that reports it can handle the name is used. -/// -public class DefaultAgentResolver(IEnumerable providers) : IAgentResolver -{ - public async Task ResolveAsync(string name, CancellationToken cancellationToken = default) - { - foreach (var provider in providers) - { - if (!await provider.CanProvideAsync(name, cancellationToken)) - continue; - - return await provider.CreateAsync(name, cancellationToken); - } - - throw new InvalidOperationException($"No agent provider could resolve an agent named '{name}'."); - } -} - diff --git a/src/modules/agents/Elsa.Agents.Core/Services/KernelConfigAgentProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/KernelConfigAgentProvider.cs deleted file mode 100644 index e9782404..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/KernelConfigAgentProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Agent provider that exposes Semantic Kernel-configured agents via -/// and . -/// -public class KernelConfigAgentProvider( - IKernelConfigProvider kernelConfigProvider, - AgentFrameworkFactory agentFrameworkFactory) : IAgentProvider -{ - public async Task CanProvideAsync(string name, CancellationToken cancellationToken = default) - { - var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); - return kernelConfig.Agents.ContainsKey(name); - } - - public async Task CreateAsync(string name, CancellationToken cancellationToken = default) - { - var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); - - if (!kernelConfig.Agents.TryGetValue(name, out var agentConfig)) - throw new InvalidOperationException($"Agent '{name}' not found in KernelConfig."); - - return agentFrameworkFactory.CreateElsaAgent(kernelConfig, agentConfig); - } -} \ No newline at end of file From 5089bc1e9a558cd4d82204f97d5bb909b308fbea Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 13 Dec 2025 15:28:39 +0100 Subject: [PATCH 13/23] Refactor agents framework and replace deprecated agent implementation Replaced `CopyWriterAndEditorAgent` with `NativeStoryWriterAgent` and `DecoratedStoryWriterAgent` for streamlined storytelling logic. Refactored code to introduce a unified `IAgent` abstraction, updated resolver logic, and decoupled agent persistence and activities. Cleaned up redundant code, enhanced modularity, and updated usage patterns for OpenAI API integration. --- .../Activities/CodeFirstAgentActivity.cs | 38 ++----------------- .../Activities/ConfiguredAgentActivity.cs | 17 +-------- .../CodeFirstAgentActivityProvider.cs | 2 +- .../Extensions/ResponseHelpers.cs | 20 ++++++++++ .../Features/AgentActivitiesFeature.cs | 2 +- .../Elsa.Agents.Core/Contracts/IAgent.cs | 6 ++- .../Contracts/IAgentExecutionContext.cs | 7 ---- .../Contracts/IAgentResolver.cs | 6 +++ .../Contracts/ICodeFirstAgentResolver.cs | 6 --- .../CodeFirstAgentResolverExtensions.cs | 2 +- .../Extensions/ModuleExtensions.cs | 2 +- ...{AgentsFeature.cs => AgentsCoreFeature.cs} | 8 ++-- .../Models/AgentExecutionContext.cs | 2 +- .../Options/CodeFirstAgentOptions.cs | 2 +- ...entFrameworkFactory.cs => AgentFactory.cs} | 18 +-------- .../Elsa.Agents.Core/Services/AgentInvoker.cs | 5 +-- .../Services/AgentResolver.cs | 14 +++++++ .../Services/CodeFirstAgentResolver.cs | 14 ------- .../Services/SemanticKernelElsaAgent.cs | 23 ----------- .../Features/AgentPersistenceFeature.cs | 4 +- .../agents/Elsa.Agents/AgentsFeature.cs | 18 +++++++++ .../agents/Elsa.Agents/Elsa.Agents.csproj | 21 ++++++++++ .../agents/Elsa.Agents/FodyWeavers.xml | 3 ++ .../agents/Elsa.Agents/ModuleExtensions.cs | 12 ++++++ 24 files changed, 119 insertions(+), 133 deletions(-) create mode 100644 src/modules/agents/Elsa.Agents.Activities/Extensions/ResponseHelpers.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionContext.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/ICodeFirstAgentResolver.cs rename src/modules/agents/Elsa.Agents.Core/Features/{AgentsFeature.cs => AgentsCoreFeature.cs} (82%) rename src/modules/agents/Elsa.Agents.Core/Services/{AgentFrameworkFactory.cs => AgentFactory.cs} (83%) create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentResolver.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/SemanticKernelElsaAgent.cs create mode 100644 src/modules/agents/Elsa.Agents/AgentsFeature.cs create mode 100644 src/modules/agents/Elsa.Agents/Elsa.Agents.csproj create mode 100644 src/modules/agents/Elsa.Agents/FodyWeavers.xml create mode 100644 src/modules/agents/Elsa.Agents/ModuleExtensions.cs diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs index 9e83a6cd..2be2a091 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs @@ -1,18 +1,12 @@ 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.Agents.Activities.ActivityProviders; using Elsa.Workflows; using Elsa.Workflows.Models; -using Elsa.Workflows.Serialization.Converters; -using Microsoft.Extensions.Options; -using Microsoft.SemanticKernel.Agents; +using static Elsa.Agents.Activities.Extensions.ResponseHelpers; namespace Elsa.Agents.Activities; @@ -24,13 +18,6 @@ public class CodeFirstAgentActivity : 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!; /// @@ -53,7 +40,7 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context } // Resolve the agent via the unified abstraction. - var agentResolver = context.GetRequiredService(); + var agentResolver = context.GetRequiredService(); var agent = await agentResolver.ResolveAsync(AgentName, cancellationToken); var agentType = agent.GetType(); var agentPropertyLookup = agentType.GetProperties().ToDictionary(x => x.Name, x => x); @@ -75,27 +62,10 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context // If the target type is object and the response is in JSON format, we want it to be deserialized into an ExpandoObject for dynamic field access. if (outputType == typeof(object) && isJsonResponse) outputType = typeof(ExpandoObject); - - var converterOptions = new ObjectConverterOptions(SerializerOptions); - var outputValue = isJsonResponse ? responseText.ConvertTo(outputType, converterOptions) : responseText; + + var outputValue = isJsonResponse ? responseText.ConvertTo(outputType) : responseText; var outputDescriptor = activityDescriptor.Outputs.Single(); var output = (Output?)outputDescriptor.ValueGetter(this); context.Set(output, outputValue, "Output"); } - - private static bool IsJsonResponse(string text) - { - return text.StartsWith("{", StringComparison.OrdinalIgnoreCase) || text.StartsWith("[", StringComparison.OrdinalIgnoreCase); - } - - private static string StripCodeFences(string content) - { - var trimmed = content.Trim(); - - if (!trimmed.StartsWith("```", StringComparison.Ordinal)) - return trimmed; - - var lines = trimmed.Split('\n'); - return lines.Length < 2 ? trimmed : string.Join('\n', lines.Skip(1).Take(lines.Length - 2)).Trim(); - } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs index f543afaf..8c4e208d 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs @@ -10,6 +10,7 @@ using Elsa.Workflows; using Elsa.Workflows.Models; using Elsa.Workflows.Serialization.Converters; +using static Elsa.Agents.Activities.Extensions.ResponseHelpers; namespace Elsa.Agents.Activities; @@ -64,20 +65,4 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var output = (Output?)outputDescriptor.ValueGetter(this); context.Set(output, outputValue, "Output"); } - - private static bool IsJsonResponse(string text) - { - return text.StartsWith("{", StringComparison.OrdinalIgnoreCase) || text.StartsWith("[", StringComparison.OrdinalIgnoreCase); - } - - private static string StripCodeFences(string content) - { - var trimmed = content.Trim(); - - if (!trimmed.StartsWith("```", StringComparison.Ordinal)) - return trimmed; - - var lines = trimmed.Split('\n'); - return lines.Length < 2 ? trimmed : string.Join('\n', lines.Skip(1).Take(lines.Length - 2)).Trim(); - } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs index 73384e21..4141a855 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs @@ -15,7 +15,7 @@ namespace Elsa.Agents.Activities.ActivityProviders; /// Provides activities for each code-first agent registered via . /// Inputs are derived from public properties on the agent type using simple /// reflection rules. Execution is delegated to -/// via the common abstraction. +/// via the common abstraction. /// [UsedImplicitly] public class CodeFirstAgentActivityProvider( diff --git a/src/modules/agents/Elsa.Agents.Activities/Extensions/ResponseHelpers.cs b/src/modules/agents/Elsa.Agents.Activities/Extensions/ResponseHelpers.cs new file mode 100644 index 00000000..19c96765 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Activities/Extensions/ResponseHelpers.cs @@ -0,0 +1,20 @@ +namespace Elsa.Agents.Activities.Extensions; + +public static class ResponseHelpers +{ + public static bool IsJsonResponse(string text) + { + return text.StartsWith("{", StringComparison.OrdinalIgnoreCase) || text.StartsWith("[", StringComparison.OrdinalIgnoreCase); + } + + public static string StripCodeFences(string content) + { + var trimmed = content.Trim(); + + if (!trimmed.StartsWith("```", StringComparison.Ordinal)) + return trimmed; + + var lines = trimmed.Split('\n'); + return lines.Length < 2 ? trimmed : string.Join('\n', lines.Skip(1).Take(lines.Length - 2)).Trim(); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs index 60218128..41614c13 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs @@ -14,7 +14,7 @@ namespace Elsa.Agents.Activities.Features; /// A feature that installs Semantic Kernel functionality. /// [DependsOn(typeof(WorkflowManagementFeature))] -[DependsOn(typeof(AgentsFeature))] +[DependsOn(typeof(AgentsCoreFeature))] [UsedImplicitly] public class AgentActivitiesFeature(IModule module) : FeatureBase(module) { diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs index d1c87c50..cb91371e 100644 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs @@ -1,3 +1,5 @@ +using Microsoft.Agents.AI; + namespace Elsa.Agents; /// @@ -5,10 +7,10 @@ namespace Elsa.Agents; /// do not need to know whether the underlying implementation is SK-based, /// ChatClientAgent-based, or something else. /// -public interface IElsaAgent +public interface IAgent { /// /// Executes the agent with the given context and returns the primary text result. /// - Task RunAsync(IAgentExecutionContext context); + Task RunAsync(AgentExecutionContext context); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionContext.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionContext.cs deleted file mode 100644 index e2047661..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Agents; - -public interface IAgentExecutionContext -{ - string Message { get; set; } - CancellationToken CancellationToken { get; set; } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs new file mode 100644 index 00000000..7ea4ba4b --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public interface IAgentResolver +{ + Task ResolveAsync(string agentName, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/ICodeFirstAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ICodeFirstAgentResolver.cs deleted file mode 100644 index a2900c8b..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/ICodeFirstAgentResolver.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public interface ICodeFirstAgentResolver -{ - Task ResolveAsync(string agentName, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs index aee8474b..38d7df88 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs @@ -2,7 +2,7 @@ namespace Elsa.Agents; public static class CodeFirstAgentResolverExtensions { - public static async Task ResolveAsync(this ICodeFirstAgentResolver resolver, CancellationToken cancellationToken = default) where TAgent : IElsaAgent + public static async Task ResolveAsync(this IAgentResolver resolver, CancellationToken cancellationToken = default) where TAgent : IAgent { var agentName = typeof(TAgent).Name; return (TAgent)await resolver.ResolveAsync(agentName, cancellationToken); diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/ModuleExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/ModuleExtensions.cs index bf521d0f..f7464583 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/ModuleExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/ModuleExtensions.cs @@ -12,7 +12,7 @@ public static class ModuleExtensions /// /// Installs the Semantic Kernel API feature. /// - public static IModule UseAgents(this IModule module, Action? configure = null) + public static IModule UseAgentsCore(this IModule module, Action? configure = null) { return module.Use(configure); } diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs similarity index 82% rename from src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs rename to src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs index a6b032c0..2b1d6670 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs @@ -11,11 +11,11 @@ namespace Elsa.Agents.Features; /// A feature that installs API endpoints to interact with skilled agents. /// [UsedImplicitly] -public class AgentsFeature(IModule module) : FeatureBase(module) +public class AgentsCoreFeature(IModule module) : FeatureBase(module) { private Func _kernelConfigProviderFactory = sp => sp.GetRequiredService(); - public AgentsFeature UseKernelConfigProvider(Func factory) + public AgentsCoreFeature UseKernelConfigProvider(Func factory) { _kernelConfigProviderFactory = factory; return this; @@ -29,12 +29,12 @@ public override void Apply() Services .AddScoped() - .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped(_kernelConfigProviderFactory) .AddScoped() - .AddScoped() + .AddScoped() .AddPluginProvider() .AddPluginProvider() .AddAgentServiceProvider() diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs index d63d3ee4..861604e9 100644 --- a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs +++ b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs @@ -1,6 +1,6 @@ namespace Elsa.Agents; -public class AgentExecutionContext : IAgentExecutionContext +public class AgentExecutionContext { public string Message { get; set; } = null!; public CancellationToken CancellationToken { get; set; } diff --git a/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs index 7c1013d8..c34b188e 100644 --- a/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs @@ -15,7 +15,7 @@ public class CodeFirstAgentOptions /// Registers a code-first agent type. If no key is provided, the type name /// is used as the key. /// - public CodeFirstAgentOptions AddAgent(string? key = null) where TAgent : class, IElsaAgent + public CodeFirstAgentOptions AddAgent(string? key = null) where TAgent : class, IAgent { key ??= typeof(TAgent).Name; CodeFirstAgents[key] = typeof(TAgent); diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs similarity index 83% rename from src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs rename to src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs index 9e2ea2d3..7094b313 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentFrameworkFactory.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.ChatCompletion; #pragma warning disable SKEXP0001 #pragma warning disable SKEXP0010 @@ -13,15 +12,12 @@ namespace Elsa.Agents; /// /// Factory for creating Agent Framework agents from Elsa agent configurations. /// -public class AgentFrameworkFactory( +public class AgentFactory( IPluginDiscoverer pluginDiscoverer, IServiceDiscoverer serviceDiscoverer, - ILoggerFactory loggerFactory, IServiceProvider serviceProvider, - ILogger logger) + ILogger logger) { - private readonly ILoggerFactory _loggerFactory = loggerFactory; - /// /// Creates a ChatCompletionAgent from an Elsa agent configuration. /// @@ -38,16 +34,6 @@ public ChatCompletionAgent CreateAgent(KernelConfig kernelConfig, AgentConfig ag }; } - /// - /// Creates an IElsaAgent adapter for a given configuration, so callers can - /// work against a unified abstraction regardless of the underlying implementation. - /// - public IElsaAgent CreateElsaAgent(KernelConfig kernelConfig, AgentConfig agentConfig) - { - var skAgent = CreateAgent(kernelConfig, agentConfig); - return new SemanticKernelElsaAgent(skAgent); - } - /// /// Creates a Kernel configured for the specified agent. /// diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs index 2c39062d..e41bf6ca 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs @@ -9,7 +9,7 @@ namespace Elsa.Agents; -public class AgentInvoker(IKernelConfigProvider kernelConfigProvider, AgentFrameworkFactory agentFrameworkFactory) +public class AgentInvoker(IKernelConfigProvider kernelConfigProvider, AgentFactory agentFactory) { /// /// Invokes an agent using the Microsoft Agent Framework (new approach). @@ -20,7 +20,7 @@ public async Task InvokeAgentAsync(string agentName, IDiction var agentConfig = kernelConfig.Agents[agentName]; // Create agent using Agent Framework - var agent = agentFrameworkFactory.CreateAgent(kernelConfig, agentConfig); + var agent = agentFactory.CreateAgent(kernelConfig, agentConfig); // Create chat history ChatHistory chatHistory = []; @@ -32,7 +32,6 @@ public async Task InvokeAgentAsync(string agentName, IDiction TemplateFormat = "handlebars", Name = agentConfig.FunctionName, AllowDangerouslySetContent = true, - }; var templateFactory = new HandlebarsPromptTemplateFactory(); diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs new file mode 100644 index 00000000..bbc6cd83 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Elsa.Agents; + +public class AgentResolver(IServiceProvider serviceProvider, IOptions options) : IAgentResolver +{ + public Task ResolveAsync(string agentName, CancellationToken cancellationToken = default) + { + var agentType = options.Value.CodeFirstAgents[agentName] ?? throw new InvalidOperationException($"No agent with name '{agentName}' was found."); + var agent = (IAgent)ActivatorUtilities.CreateInstance(serviceProvider, agentType)!; + return Task.FromResult(agent); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentResolver.cs deleted file mode 100644 index d839a010..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/CodeFirstAgentResolver.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Elsa.Agents; - -public class CodeFirstAgentResolver(IServiceProvider serviceProvider, IOptions options) : ICodeFirstAgentResolver -{ - public Task ResolveAsync(string agentName, CancellationToken cancellationToken = default) - { - var agentType = options.Value.CodeFirstAgents[agentName] ?? throw new InvalidOperationException($"No agent with name '{agentName}' was found."); - var agent = (IElsaAgent)ActivatorUtilities.CreateInstance(serviceProvider, agentType)!; - return Task.FromResult(agent); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/SemanticKernelElsaAgent.cs b/src/modules/agents/Elsa.Agents.Core/Services/SemanticKernelElsaAgent.cs deleted file mode 100644 index 356edf00..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/SemanticKernelElsaAgent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.SemanticKernel.Agents; - -namespace Elsa.Agents; - -/// -/// IElsaAgent adapter over a Semantic Kernel ChatCompletionAgent. -/// -public class SemanticKernelElsaAgent(ChatCompletionAgent innerAgent) : IElsaAgent -{ - public async Task RunAsync(IAgentExecutionContext context) - { - var cancellationToken = context.CancellationToken; - var result = await innerAgent.InvokeAsync(context.Message, cancellationToken: cancellationToken).LastOrDefaultAsync(cancellationToken); - if (result is null) - throw new InvalidOperationException("Agent did not produce a response."); - - var responseMessage = result.Message.Content ?? throw new InvalidOperationException("Agent did not produce a response."); - return new AgentExecutionResponse - { - Text = responseMessage - }; - } -} diff --git a/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs b/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs index f02f4d55..53e783f2 100644 --- a/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs +++ b/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs @@ -9,7 +9,7 @@ namespace Elsa.Agents.Persistence.Features; -[DependsOn(typeof(AgentsFeature))] +[DependsOn(typeof(AgentsCoreFeature))] public class AgentPersistenceFeature(IModule module) : FeatureBase(module) { private Func _apiKeyStoreFactory = sp => sp.GetRequiredService(); @@ -36,7 +36,7 @@ public AgentPersistenceFeature UseAgentStore(Func public override void Configure() { - Module.UseAgents(agents => agents.UseKernelConfigProvider(sp => sp.GetRequiredService())); + Module.UseAgentsCore(agents => agents.UseKernelConfigProvider(sp => sp.GetRequiredService())); } public override void Apply() diff --git a/src/modules/agents/Elsa.Agents/AgentsFeature.cs b/src/modules/agents/Elsa.Agents/AgentsFeature.cs new file mode 100644 index 00000000..2f812244 --- /dev/null +++ b/src/modules/agents/Elsa.Agents/AgentsFeature.cs @@ -0,0 +1,18 @@ +using Elsa.Agents.Activities.Features; +using Elsa.Agents.Features; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Agents; + +[DependsOn(typeof(AgentsCoreFeature))] +[DependsOn(typeof(AgentActivitiesFeature))] +public class AgentsFeature(IModule module) : FeatureBase(module) +{ + public void AddAgent(string? key = null) where TAgent : class, IAgent + { + Module.Services.Configure(options => options.AddAgent(key)); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents/Elsa.Agents.csproj b/src/modules/agents/Elsa.Agents/Elsa.Agents.csproj new file mode 100644 index 00000000..80cc94d9 --- /dev/null +++ b/src/modules/agents/Elsa.Agents/Elsa.Agents.csproj @@ -0,0 +1,21 @@ + + + + Provides an agentic framework using Semantic Kernel and Microsoft Agent Framework + elsa extension module agents semantic kernel llm ai maf + Elsa.Agents + + + + + + + + + + + + + + + diff --git a/src/modules/agents/Elsa.Agents/FodyWeavers.xml b/src/modules/agents/Elsa.Agents/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/modules/agents/Elsa.Agents/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents/ModuleExtensions.cs b/src/modules/agents/Elsa.Agents/ModuleExtensions.cs new file mode 100644 index 00000000..c2ccc5a8 --- /dev/null +++ b/src/modules/agents/Elsa.Agents/ModuleExtensions.cs @@ -0,0 +1,12 @@ +using Elsa.Features.Services; + +namespace Elsa.Agents; + +public static class ModuleExtensions +{ + public static IModule UseAgents(this IModule module, Action? configure = null) + { + module.Configure(configure); + return module; + } +} \ No newline at end of file From 0269faea4f58df98b0161b3248539b40ac5f9105 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 13 Dec 2025 15:46:42 +0100 Subject: [PATCH 14/23] Replace "plugins" with "skills" throughout agents framework Updated terminology across the agents framework, replacing "plugins" with "skills" to enhance semantic clarity. Includes renaming types, methods, endpoints, and properties, and refactoring related logic and configuration. --- .../Endpoints/Agents/Create/Endpoint.cs | 2 +- .../Endpoints/Agents/Update/Endpoint.cs | 2 +- .../Endpoints/Plugins/List/Endpoint.cs | 28 ---------------- .../Endpoints/Skills/List/Endpoint.cs | 27 +++++++++++++++ .../Extensions/AgentDefinitionExtensions.cs | 2 +- .../Abstractions/PluginProvider.cs | 6 ---- .../Abstractions/SkillsProvider.cs | 6 ++++ .../Contracts/IPluginDiscoverer.cs | 6 ---- .../Contracts/ISkillDiscoverer.cs | 6 ++++ ...{IPluginProvider.cs => ISkillsProvider.cs} | 6 ++-- .../Extensions/ServiceCollectionExtensions.cs | 4 +-- .../Features/AgentsCoreFeature.cs | 8 ++--- .../Models/PluginDescriptor.cs | 33 ------------------- .../Models/SkillDescriptor.cs | 33 +++++++++++++++++++ .../Elsa.Agents.Core/Services/AgentFactory.cs | 20 +++++------ .../Services/PluginDiscoverer.cs | 9 ----- .../Services/SkillDiscoverer.cs | 9 +++++ .../DocumentQuerySkill.cs} | 10 +++--- .../ImageGeneratorSkill.cs} | 10 +++--- .../Agents/AgentInputModel.cs | 2 +- .../Elsa.Agents.Models/Configs/AgentConfig.cs | 2 +- .../Plugins/PluginDescriptor.cs | 11 ------- .../Plugins/SkillDescriptorModel.cs | 11 +++++++ .../Elsa.Studio.Agents/Client/IPluginsApi.cs | 13 -------- .../Elsa.Studio.Agents/Client/ISkillsApi.cs | 13 ++++++++ .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../UI/Components/CreateAgentDialog.razor | 4 +-- .../UI/Components/CreateAgentDialog.razor.cs | 14 ++++---- .../Elsa.Studio.Agents/UI/Pages/Agent.razor | 4 +-- .../UI/Pages/Agent.razor.cs | 18 +++++----- 30 files changed, 160 insertions(+), 161 deletions(-) delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Plugins/List/Endpoint.cs create mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Skills/List/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Abstractions/PluginProvider.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Abstractions/SkillsProvider.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IPluginDiscoverer.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/ISkillDiscoverer.cs rename src/modules/agents/Elsa.Agents.Core/Contracts/{IPluginProvider.cs => ISkillsProvider.cs} (56%) delete mode 100644 src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Models/SkillDescriptor.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/PluginDiscoverer.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Services/SkillDiscoverer.cs rename src/modules/agents/Elsa.Agents.Core/{Plugins/DocumentQueryPlugin.cs => Skills/DocumentQuerySkill.cs} (88%) rename src/modules/agents/Elsa.Agents.Core/{Plugins/ImageGeneratorPlugin.cs => Skills/ImageGeneratorSkill.cs} (80%) delete mode 100644 src/modules/agents/Elsa.Agents.Models/Plugins/PluginDescriptor.cs create mode 100644 src/modules/agents/Elsa.Agents.Models/Plugins/SkillDescriptorModel.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/Client/IPluginsApi.cs create mode 100644 src/modules/agents/Elsa.Studio.Agents/Client/ISkillsApi.cs diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs index 393e813d..c6bc64c1 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs @@ -48,7 +48,7 @@ public override async Task ExecuteAsync(AgentInputModel req, Cancell InputVariables = req.InputVariables, OutputVariable = req.OutputVariable, Services = req.Services, - Plugins = req.Plugins, + Skills = req.Skills, FunctionName = req.FunctionName, PromptTemplate = req.PromptTemplate } diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs index 4d2f9ea1..66c3b5c0 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs @@ -52,7 +52,7 @@ public override async Task ExecuteAsync(AgentInputModel req, Cancell InputVariables = req.InputVariables, OutputVariable = req.OutputVariable, ExecutionSettings = req.ExecutionSettings, - Plugins = req.Plugins, + Skills = req.Skills, Agents = req.Agents }; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Plugins/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Plugins/List/Endpoint.cs deleted file mode 100644 index e2483a14..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Plugins/List/Endpoint.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Models; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Plugins.List; - -/// -/// Lists all registered plugins. -/// -[UsedImplicitly] -public class Endpoint(IPluginDiscoverer pluginDiscoverer) : ElsaEndpointWithoutRequest> -{ - /// - public override void Configure() - { - Get("/ai/plugins"); - ConfigurePermissions("ai/plugins:read"); - } - - /// - public override Task> ExecuteAsync(CancellationToken ct) - { - var descriptors = pluginDiscoverer.GetPluginDescriptors(); - var models = descriptors.Select(x => x.ToModel()).ToList(); - return Task.FromResult(new ListResponse(models)); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Skills/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Skills/List/Endpoint.cs new file mode 100644 index 00000000..d1946986 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Skills/List/Endpoint.cs @@ -0,0 +1,27 @@ +using Elsa.Abstractions; +using Elsa.Models; +using JetBrains.Annotations; + +namespace Elsa.Agents.Api.Endpoints.Skills.List; + +/// +/// Lists all registered skills. +/// +[UsedImplicitly] +public class Endpoint(ISkillDiscoverer skillDiscoverer) : ElsaEndpointWithoutRequest> +{ + /// + public override void Configure() + { + Get("/ai/skills"); + ConfigurePermissions("ai/skills:read"); + } + + /// + public override Task> ExecuteAsync(CancellationToken ct) + { + var descriptors = skillDiscoverer.DiscoverSkills(); + var models = descriptors.Select(x => x.ToModel()).ToList(); + return Task.FromResult(new ListResponse(models)); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs b/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs index f631ce23..d71b24fe 100644 --- a/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs @@ -18,7 +18,7 @@ public static AgentModel ToModel(this AgentDefinition agentDefinition) InputVariables = agentDefinition.AgentConfig.InputVariables, OutputVariable = agentDefinition.AgentConfig.OutputVariable, Services = agentDefinition.AgentConfig.Services, - Plugins = agentDefinition.AgentConfig.Plugins, + Skills = agentDefinition.AgentConfig.Skills, FunctionName = agentDefinition.AgentConfig.FunctionName, PromptTemplate = agentDefinition.AgentConfig.PromptTemplate }; diff --git a/src/modules/agents/Elsa.Agents.Core/Abstractions/PluginProvider.cs b/src/modules/agents/Elsa.Agents.Core/Abstractions/PluginProvider.cs deleted file mode 100644 index c64a1f34..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Abstractions/PluginProvider.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public abstract class PluginProvider : IPluginProvider -{ - public virtual IEnumerable GetPlugins() => []; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Abstractions/SkillsProvider.cs b/src/modules/agents/Elsa.Agents.Core/Abstractions/SkillsProvider.cs new file mode 100644 index 00000000..eb367140 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Abstractions/SkillsProvider.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public abstract class SkillsProvider : ISkillsProvider +{ + public virtual IEnumerable GetSkills() => []; +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginDiscoverer.cs deleted file mode 100644 index 5a9fa460..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginDiscoverer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public interface IPluginDiscoverer -{ - IEnumerable GetPluginDescriptors(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillDiscoverer.cs new file mode 100644 index 00000000..311469ae --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillDiscoverer.cs @@ -0,0 +1,6 @@ +namespace Elsa.Agents; + +public interface ISkillDiscoverer +{ + IEnumerable DiscoverSkills(); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillsProvider.cs similarity index 56% rename from src/modules/agents/Elsa.Agents.Core/Contracts/IPluginProvider.cs rename to src/modules/agents/Elsa.Agents.Core/Contracts/ISkillsProvider.cs index 0db6dfd7..163cc7e1 100644 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IPluginProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/ISkillsProvider.cs @@ -1,9 +1,9 @@ namespace Elsa.Agents; /// -/// Implementations of this interface are responsible for providing plugins. +/// Implementations of this interface are responsible for providing skills. /// -public interface IPluginProvider +public interface ISkillsProvider { - IEnumerable GetPlugins(); + IEnumerable GetSkills(); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs index b33fed1c..ee9e5116 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs @@ -4,9 +4,9 @@ namespace Elsa.Agents; public static class ServiceCollectionExtensions { - public static IServiceCollection AddPluginProvider(this IServiceCollection services) where T: class, IPluginProvider + public static IServiceCollection AddSkillsProvider(this IServiceCollection services) where T: class, ISkillsProvider { - return services.AddScoped(); + return services.AddScoped(); } public static IServiceCollection AddAgentServiceProvider(this IServiceCollection services) where T: class, IAgentServiceProvider diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs index 2b1d6670..8769e8b8 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs @@ -1,4 +1,4 @@ -using Elsa.Agents.Plugins; +using Elsa.Agents.Skills; using Elsa.Features.Abstractions; using Elsa.Features.Services; using JetBrains.Annotations; @@ -30,13 +30,13 @@ public override void Apply() Services .AddScoped() .AddScoped() - .AddScoped() + .AddScoped() .AddScoped() .AddScoped(_kernelConfigProviderFactory) .AddScoped() .AddScoped() - .AddPluginProvider() - .AddPluginProvider() + .AddSkillsProvider() + .AddSkillsProvider() .AddAgentServiceProvider() .AddAgentServiceProvider() .AddAgentServiceProvider() diff --git a/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs b/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs deleted file mode 100644 index 6d01db18..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Models/PluginDescriptor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.ComponentModel; -using System.Reflection; - -namespace Elsa.Agents; - -/// -/// A descriptor for a plugin. -/// -public class PluginDescriptor -{ - public string Name { get; set; } - public string Description { get; set; } - public Type PluginType { get; set; } - - public PluginDescriptorModel ToModel() => new() - { - Name = Name, - Description = Description, - PluginType = PluginType.AssemblyQualifiedName! - }; - - public static PluginDescriptor From(string? name = null) - { - var pluginType = typeof(TPlugin); - var description = pluginType.GetCustomAttribute()?.Description ?? string.Empty; - return new() - { - Name = name ?? pluginType.Name.Replace("Plugin", ""), - Description = description, - PluginType = pluginType - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/SkillDescriptor.cs b/src/modules/agents/Elsa.Agents.Core/Models/SkillDescriptor.cs new file mode 100644 index 00000000..97c54808 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Models/SkillDescriptor.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using System.Reflection; + +namespace Elsa.Agents; + +/// +/// A descriptor for a skill. +/// +public class SkillDescriptor +{ + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public Type ClrType { get; set; } = null!; + + public SkillDescriptorModel ToModel() => new() + { + Name = Name, + Description = Description, + ClrTypeName = ClrType.AssemblyQualifiedName! + }; + + public static SkillDescriptor From(string? name = null) + { + var clrType = typeof(TSkill); + var description = clrType.GetCustomAttribute()?.Description ?? string.Empty; + return new() + { + Name = name ?? clrType.Name.Replace("Skill", ""), + Description = description, + ClrType = clrType + }; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs index 7094b313..b742677f 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs @@ -13,7 +13,7 @@ namespace Elsa.Agents; /// Factory for creating Agent Framework agents from Elsa agent configurations. /// public class AgentFactory( - IPluginDiscoverer pluginDiscoverer, + ISkillDiscoverer skillDiscoverer, IServiceDiscoverer serviceDiscoverer, IServiceProvider serviceProvider, ILogger logger) @@ -63,7 +63,7 @@ private void ApplyAgentConfig(IKernelBuilder builder, KernelConfig kernelConfig, AddService(builder, kernelConfig, serviceConfig, services); } - AddPlugins(builder, agentConfig); + AddSkills(builder, agentConfig); } private void AddService(IKernelBuilder builder, KernelConfig kernelConfig, ServiceConfig serviceConfig, Dictionary services) @@ -78,20 +78,20 @@ private void AddService(IKernelBuilder builder, KernelConfig kernelConfig, Servi serviceProvider.ConfigureKernel(context); } - private void AddPlugins(IKernelBuilder builder, AgentConfig agent) + private void AddSkills(IKernelBuilder builder, AgentConfig agent) { - var plugins = pluginDiscoverer.GetPluginDescriptors().ToDictionary(x => x.Name); - foreach (var pluginName in agent.Plugins) + var skills = skillDiscoverer.DiscoverSkills().ToDictionary(x => x.Name); + foreach (var skillName in agent.Skills) { - if (!plugins.TryGetValue(pluginName, out var pluginDescriptor)) + if (!skills.TryGetValue(skillName, out var skillDescriptor)) { - logger.LogWarning($"Plugin {pluginName} not found"); + logger.LogWarning($"Skill {skillName} not found"); continue; } - var pluginType = pluginDescriptor.PluginType; - var pluginInstance = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, pluginType); - builder.Plugins.AddFromObject(pluginInstance, pluginName); + var clrType = skillDescriptor.ClrType; + var skillInstance = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, clrType); + builder.Plugins.AddFromObject(skillInstance, skillName); } } } diff --git a/src/modules/agents/Elsa.Agents.Core/Services/PluginDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Services/PluginDiscoverer.cs deleted file mode 100644 index fa645195..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/PluginDiscoverer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Elsa.Agents; - -public class PluginDiscoverer(IEnumerable providers) : IPluginDiscoverer -{ - public IEnumerable GetPluginDescriptors() - { - return providers.SelectMany(x => x.GetPlugins()); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/SkillDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Services/SkillDiscoverer.cs new file mode 100644 index 00000000..88ff8b73 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Services/SkillDiscoverer.cs @@ -0,0 +1,9 @@ +namespace Elsa.Agents; + +public class SkillDiscoverer(IEnumerable providers) : ISkillDiscoverer +{ + public IEnumerable DiscoverSkills() + { + return providers.SelectMany(x => x.GetSkills()); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Plugins/DocumentQueryPlugin.cs b/src/modules/agents/Elsa.Agents.Core/Skills/DocumentQuerySkill.cs similarity index 88% rename from src/modules/agents/Elsa.Agents.Core/Plugins/DocumentQueryPlugin.cs rename to src/modules/agents/Elsa.Agents.Core/Skills/DocumentQuerySkill.cs index 811ecf38..fb2c8348 100644 --- a/src/modules/agents/Elsa.Agents.Core/Plugins/DocumentQueryPlugin.cs +++ b/src/modules/agents/Elsa.Agents.Core/Skills/DocumentQuerySkill.cs @@ -6,9 +6,9 @@ using Microsoft.SemanticKernel.Connectors.InMemory; using Microsoft.SemanticKernel.Data; -namespace Elsa.Agents.Plugins; +namespace Elsa.Agents.Skills; -public class DocumentQueryPlugin +public class DocumentQuerySkill { [Experimental("SKEXP0001")] [KernelFunction("query_document")] @@ -39,11 +39,11 @@ public async Task QueryDocumentAsync( } } -public class DocumentQueryPluginProvider : PluginProvider +public class DocumentQuerySkillsProvider : SkillsProvider { - public override IEnumerable GetPlugins() + public override IEnumerable GetSkills() { - yield return PluginDescriptor.From(); + yield return SkillDescriptor.From(); } } diff --git a/src/modules/agents/Elsa.Agents.Core/Plugins/ImageGeneratorPlugin.cs b/src/modules/agents/Elsa.Agents.Core/Skills/ImageGeneratorSkill.cs similarity index 80% rename from src/modules/agents/Elsa.Agents.Core/Plugins/ImageGeneratorPlugin.cs rename to src/modules/agents/Elsa.Agents.Core/Skills/ImageGeneratorSkill.cs index a4d62975..8689ca2f 100644 --- a/src/modules/agents/Elsa.Agents.Core/Plugins/ImageGeneratorPlugin.cs +++ b/src/modules/agents/Elsa.Agents.Core/Skills/ImageGeneratorSkill.cs @@ -5,11 +5,11 @@ #pragma warning disable SKEXP0001 -namespace Elsa.Agents.Plugins; +namespace Elsa.Agents.Skills; [Description("Generates an image from text")] [UsedImplicitly] -public class ImageGeneratorPlugin +public class ImageGeneratorSkill { [KernelFunction("generate_image_from_text")] [Description("Generates an image from text")] @@ -30,10 +30,10 @@ public async Task GenerateImage( } [UsedImplicitly] -public class ImageGeneratorPluginProvider : PluginProvider +public class ImageGeneratorSkillsProvider : SkillsProvider { - public override IEnumerable GetPlugins() + public override IEnumerable GetSkills() { - yield return PluginDescriptor.From(); + yield return SkillDescriptor.From(); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs b/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs index b5c003ed..566a8416 100644 --- a/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs +++ b/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs @@ -12,6 +12,6 @@ public class AgentInputModel public ICollection InputVariables { get; set; } = []; [Required] public OutputVariableConfig OutputVariable { get; set; } = new(); public ExecutionSettingsConfig ExecutionSettings { get; set; } = new(); - public ICollection Plugins { get; set; } = []; + public ICollection Skills { get; set; } = []; public ICollection Agents { get; set; } = []; } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs index 6257ae12..62af3ef1 100644 --- a/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs +++ b/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs @@ -10,7 +10,7 @@ public class AgentConfig public ICollection InputVariables { get; set; } = []; public OutputVariableConfig OutputVariable { get; set; } = new(); public ExecutionSettingsConfig ExecutionSettings { get; set; } = new(); - public ICollection Plugins { get; set; } = []; + public ICollection Skills { get; set; } = []; public ICollection Agents { get; set; } = []; diff --git a/src/modules/agents/Elsa.Agents.Models/Plugins/PluginDescriptor.cs b/src/modules/agents/Elsa.Agents.Models/Plugins/PluginDescriptor.cs deleted file mode 100644 index c8325fdf..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Plugins/PluginDescriptor.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Elsa.Agents; - -/// -/// A descriptor for a plugin. -/// -public class PluginDescriptorModel -{ - public string Name { get; set; } - public string Description { get; set; } - public string PluginType { get; set; } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Plugins/SkillDescriptorModel.cs b/src/modules/agents/Elsa.Agents.Models/Plugins/SkillDescriptorModel.cs new file mode 100644 index 00000000..1a4f7e43 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Models/Plugins/SkillDescriptorModel.cs @@ -0,0 +1,11 @@ +namespace Elsa.Agents; + +/// +/// A descriptor of a skill. +/// +public class SkillDescriptorModel +{ + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public string ClrTypeName { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/IPluginsApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/IPluginsApi.cs deleted file mode 100644 index 039abf2f..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/Client/IPluginsApi.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Elsa.Agents; -using Elsa.Api.Client.Shared.Models; -using Refit; - -namespace Elsa.Studio.Agents.Client; - -/// Represents a client API for interacting with AI plugins. -public interface IPluginsApi -{ - /// Lists all services. - [Get("/ai/plugins")] - Task> ListAsync(CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/ISkillsApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/ISkillsApi.cs new file mode 100644 index 00000000..63141689 --- /dev/null +++ b/src/modules/agents/Elsa.Studio.Agents/Client/ISkillsApi.cs @@ -0,0 +1,13 @@ +using Elsa.Agents; +using Elsa.Api.Client.Shared.Models; +using Refit; + +namespace Elsa.Studio.Agents.Client; + +/// Represents a client API for interacting with AI skills. +public interface ISkillsApi +{ + /// Lists all services. + [Get("/ai/skills")] + Task> ListAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs b/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs index 5d5ffe6d..200f50c0 100644 --- a/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs @@ -24,7 +24,7 @@ public static IServiceCollection AddAgentsModule(this IServiceCollection service .AddRemoteApi(backendApiConfig) .AddRemoteApi(backendApiConfig) .AddRemoteApi(backendApiConfig) - .AddRemoteApi(backendApiConfig) + .AddRemoteApi(backendApiConfig) .AddActivityDisplaySettingsProvider() // TODO: Move this to a separate module. diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor index 93718092..917ac947 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor @@ -20,8 +20,8 @@ - - @foreach (var plugin in AvailablePlugins) + + @foreach (var plugin in AvailableSkills) { } diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs index 52540106..943b8e43 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs @@ -27,8 +27,8 @@ public partial class CreateAgentDialog [Inject] private IActivityDisplaySettingsRegistry ActivityDisplaySettingsRegistry { get; set; } = null!; private ICollection AvailableServices { get; set; } = []; private IReadOnlyCollection SelectedServices { get; set; } = []; - private ICollection AvailablePlugins { get; set; } = []; - private IReadOnlyCollection SelectedPlugins { get; set; } = []; + private ICollection AvailableSkills { get; set; } = []; + private IReadOnlyCollection SelectedSkills { get; set; } = []; /// protected override async Task OnInitializedAsync() @@ -43,14 +43,14 @@ protected override async Task OnInitializedAsync() _editContext = new(_agentInputModel); var agentsApi = await ApiClientProvider.GetApiAsync(); var servicesApi = await ApiClientProvider.GetApiAsync(); - var pluginsApi = await ApiClientProvider.GetApiAsync(); + var skillsApi = await ApiClientProvider.GetApiAsync(); _validator = new(agentsApi); var servicesResponseList = await servicesApi.ListAsync(); - var pluginsResponseList = await pluginsApi.ListAsync(); + var skillsResponseList = await skillsApi.ListAsync(); AvailableServices = servicesResponseList.Items; - AvailablePlugins = pluginsResponseList.Items; + AvailableSkills = skillsResponseList.Items; SelectedServices = _agentInputModel.Services.ToList().AsReadOnly(); - SelectedPlugins = _agentInputModel.Plugins.ToList().AsReadOnly(); + SelectedSkills = _agentInputModel.Skills.ToList().AsReadOnly(); } private Task OnCancelClicked() @@ -70,7 +70,7 @@ private async Task OnSubmitClicked() private Task OnValidSubmit() { _agentInputModel.Services = SelectedServices.ToList(); - _agentInputModel.Plugins = SelectedPlugins.ToList(); + _agentInputModel.Skills = SelectedSkills.ToList(); MudDialog.Close(_agentInputModel); ActivityRegistry.MarkStale(); ActivityDisplaySettingsRegistry.MarkStale(); diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor index 945e0db6..21075900 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor @@ -153,8 +153,8 @@ - - @foreach (var plugin in AvailablePlugins) + + @foreach (var plugin in AvailableSkills) { } diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs index 2e053cf8..d1c2b2e1 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs @@ -25,8 +25,8 @@ private bool UseJsonResponse private ICollection AvailableServices { get; set; } = []; private IReadOnlyCollection SelectedServices { get; set; } = []; - private ICollection AvailablePlugins { get; set; } = []; - private IReadOnlyCollection SelectedPlugins { get; set; } = []; + private ICollection AvailableSkills { get; set; } = []; + private IReadOnlyCollection SelectedSkills { get; set; } = []; private MudForm _form = null!; @@ -39,13 +39,13 @@ private bool UseJsonResponse protected override async Task OnInitializedAsync() { var apiClient = await ApiClientProvider.GetApiAsync(); - _validator = new AgentInputModelValidator(apiClient); + _validator = new(apiClient); var servicesApi = await ApiClientProvider.GetApiAsync(); - var pluginsApi = await ApiClientProvider.GetApiAsync(); + var skillsApi = await ApiClientProvider.GetApiAsync(); var servicesResponseList = await servicesApi.ListAsync(); - var pluginsResponseList = await pluginsApi.ListAsync(); + var skillsResponseList = await skillsApi.ListAsync(); AvailableServices = servicesResponseList.Items; - AvailablePlugins = pluginsResponseList.Items; + AvailableSkills = skillsResponseList.Items; } /// @@ -54,7 +54,7 @@ protected override async Task OnParametersSetAsync() var apiClient = await ApiClientProvider.GetApiAsync(); _agent = await apiClient.GetAsync(AgentId); SelectedServices = _agent.Services.ToList().AsReadOnly(); - SelectedPlugins = _agent.Plugins.ToList().AsReadOnly(); + SelectedSkills = _agent.Skills.ToList().AsReadOnly(); } private async Task OnSaveClicked() @@ -65,7 +65,7 @@ private async Task OnSaveClicked() return; _agent.Services = SelectedServices.ToList(); - _agent.Plugins = SelectedPlugins.ToList(); + _agent.Skills = SelectedSkills.ToList(); var apiClient = await ApiClientProvider.GetApiAsync(); _agent = await apiClient.UpdateAsync(AgentId, _agent); Snackbar.Add("Agent successfully updated.", Severity.Success); @@ -96,7 +96,7 @@ private void OnAddInputVariableClicked() private void BackupInputVariable(object obj) { var inputVariable = (InputVariableConfig)obj; - _inputVariableBackup = new InputVariableConfig + _inputVariableBackup = new() { Name = inputVariable.Name, Type = inputVariable.Type, From b020eb8b16e6dccab87a7bbbb02709fa8dff0f39 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 13 Dec 2025 20:00:17 +0100 Subject: [PATCH 15/23] Remove legacy API key and service management functionality. Deleted obsolete API key and service management classes, endpoints, and associated UI components. These functionalities are no longer required and their removal simplifies the codebase by eliminating unused or redundant features. --- .../Endpoints/Agents/BulkDelete/Endpoint.cs | 3 +- .../Endpoints/Agents/Create/Endpoint.cs | 5 +- .../Agents/GenerateUniqueName/Endpoint.cs | 1 - .../Endpoints/Agents/Get/Endpoint.cs | 1 - .../Endpoints/Agents/Invoke/Endpoint.cs | 1 - .../Endpoints/Agents/IsUniqueName/Endpoint.cs | 1 - .../Endpoints/Agents/List/Endpoint.cs | 3 +- .../Endpoints/Agents/Update/Endpoint.cs | 5 +- .../Endpoints/ApiKeys/BulkDelete/Endpoint.cs | 33 ------- .../Endpoints/ApiKeys/Create/Endpoint.cs | 50 ---------- .../Endpoints/ApiKeys/Delete/Endpoint.cs | 33 ------- .../Endpoints/ApiKeys/Delete/Request.cs | 8 -- .../Endpoints/ApiKeys/Get/Endpoint.cs | 34 ------- .../Endpoints/ApiKeys/Get/Request.cs | 8 -- .../Endpoints/ApiKeys/List/Endpoint.cs | 29 ------ .../Endpoints/ApiKeys/Update/Endpoint.cs | 61 ------------ .../ServiceProviders/List/Endpoint.cs | 27 ------ .../Endpoints/Services/BulkDelete/Endpoint.cs | 33 ------- .../Endpoints/Services/Create/Endpoint.cs | 51 ---------- .../Endpoints/Services/Delete/Endpoint.cs | 33 ------- .../Endpoints/Services/Delete/Request.cs | 8 -- .../Endpoints/Services/Get/Endpoint.cs | 34 ------- .../Endpoints/Services/Get/Request.cs | 8 -- .../Endpoints/Services/List/Endpoint.cs | 29 ------ .../Endpoints/Services/Update/Endpoint.cs | 61 ------------ .../Extensions/AgentDefinitionExtensions.cs | 4 +- .../Contracts/IAgentServiceProvider.cs | 7 -- .../Contracts/IServiceDiscoverer.cs | 6 -- .../Contracts/ServiceDescriptor.cs | 9 ++ .../Elsa.Agents.Core/Elsa.Agents.Core.csproj | 7 +- .../Extensions/AgentConfigExtensions.cs | 50 ---------- .../CodeFirstAgentResolverExtensions.cs | 10 -- .../Extensions/ServiceCollectionExtensions.cs | 5 - .../Features/AgentsCoreFeature.cs | 4 - .../Models/AgentWorkflowResult.cs | 24 ----- .../Models/KernelBuilderContext.cs | 20 ---- .../Options/ConfiguredAgentOptions.cs | 3 +- .../OpenAIChatCompletionProvider.cs | 15 --- .../OpenAIEmbeddingGenerator.cs | 23 ----- .../OpenAITextToImageProvider.cs | 16 ---- .../Elsa.Agents.Core/Services/AgentFactory.cs | 41 +++------ .../Elsa.Agents.Core/Services/AgentInvoker.cs | 4 +- .../ConfigurationKernelConfigProvider.cs | 15 +-- .../Services/ServiceDiscoverer.cs | 9 -- .../Agents/AgentInputModel.cs | 2 - .../Elsa.Agents.Models/Configs/AgentConfig.cs | 2 - .../Configs/AgentWorkflowConfig.cs | 73 --------------- .../Configs/ApiKeyConfig.cs | 10 -- .../Configs/KernelConfig.cs | 3 - .../Configs/ServiceConfig.cs | 8 -- .../Services/ServiceInputModel.cs | 10 -- .../Services/ServiceModel.cs | 8 -- .../agents/Elsa.Agents.OpenAI/Class1.cs | 5 + .../Elsa.Agents.OpenAI.csproj | 22 +++++ .../agents/Elsa.Agents.OpenAI/FodyWeavers.xml | 3 + ...sa.Agents.Persistence.EFCore.Sqlite.csproj | 1 + .../efcore-3.6.sh | 2 + .../Configurations.cs | 15 +-- .../DbContext.cs | 16 +--- .../EFCoreApiKeyStore.cs | 54 ----------- .../EFCoreServiceStore.cs | 54 ----------- .../Elsa.Agents.Persistence.EFCore/Feature.cs | 8 +- .../Contracts/IApiKeyStore.cs | 42 --------- .../Contracts/IServiceStore.cs | 42 --------- .../Entities/AgentDefinition.cs | 1 - .../Entities/ApiKeyDefinition.cs | 31 ------- .../Entities/ServiceDefinition.cs | 32 ------- .../Features/AgentPersistenceFeature.cs | 30 +----- .../Filters/ApiKeyDefinitionFilter.cs | 20 ---- .../Filters/ServiceDefinitionFilter.cs | 20 ---- .../Services/MemoryApiKeyStore.cs | 54 ----------- .../Services/MemoryServiceStore.cs | 54 ----------- .../Services/StoreKernelConfigProvider.cs | 7 +- .../agents/Elsa.Agents/AgentsFeature.cs | 9 +- .../agents/Elsa.Studio.Agents/AgentsMenu.cs | 37 +------- .../Elsa.Studio.Agents/Client/IApiKeysApi.cs | 37 -------- .../Client/IServiceProvidersApi.cs | 12 --- .../Elsa.Studio.Agents/Client/IServicesApi.cs | 37 -------- .../Extensions/ServiceCollectionExtensions.cs | 3 - .../UI/Components/CreateAgentDialog.razor | 13 +-- .../UI/Components/CreateAgentDialog.razor.cs | 8 -- .../Elsa.Studio.Agents/UI/Pages/Agent.razor | 29 +----- .../UI/Pages/Agent.razor.cs | 12 +-- .../Elsa.Studio.Agents/UI/Pages/ApiKey.razor | 43 --------- .../UI/Pages/ApiKey.razor.cs | 61 ------------ .../Elsa.Studio.Agents/UI/Pages/ApiKeys.razor | 68 -------------- .../UI/Pages/ApiKeys.razor.cs | 92 ------------------- .../Elsa.Studio.Agents/UI/Pages/Service.razor | 57 ------------ .../UI/Pages/Service.razor.cs | 81 ---------------- .../UI/Pages/Services.razor | 73 --------------- .../UI/Pages/Services.razor.cs | 89 ------------------ .../Validators/ApiKeyInputModelValidator.cs | 29 ------ .../Validators/ServiceInputModelValidator.cs | 29 ------ 93 files changed, 92 insertions(+), 2188 deletions(-) delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Request.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/ServiceProviders/List/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Create/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Request.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Request.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Services/List/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Update/Endpoint.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentServiceProvider.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IServiceDiscoverer.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Models/KernelBuilderContext.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIEmbeddingGenerator.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Services/ServiceDiscoverer.cs delete mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/AgentWorkflowConfig.cs delete mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/ApiKeyConfig.cs delete mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/ServiceConfig.cs delete mode 100644 src/modules/agents/Elsa.Agents.Models/Services/ServiceInputModel.cs delete mode 100644 src/modules/agents/Elsa.Agents.Models/Services/ServiceModel.cs create mode 100644 src/modules/agents/Elsa.Agents.OpenAI/Class1.cs create mode 100644 src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj create mode 100644 src/modules/agents/Elsa.Agents.OpenAI/FodyWeavers.xml create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh delete mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreApiKeyStore.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreServiceStore.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence/Contracts/IApiKeyStore.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence/Contracts/IServiceStore.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence/Entities/ApiKeyDefinition.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence/Entities/ServiceDefinition.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence/Filters/ServiceDefinitionFilter.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence/Services/MemoryApiKeyStore.cs delete mode 100644 src/modules/agents/Elsa.Agents.Persistence/Services/MemoryServiceStore.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/Client/IApiKeysApi.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/Client/IServiceProvidersApi.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/Client/IServicesApi.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Validators/ApiKeyInputModelValidator.cs delete mode 100644 src/modules/agents/Elsa.Studio.Agents/UI/Validators/ServiceInputModelValidator.cs diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs index a14a4d25..4c4e52a3 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/BulkDelete/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Agents.Persistence.Contracts; using Elsa.Agents.Persistence.Filters; using JetBrains.Annotations; @@ -28,6 +27,6 @@ public override async Task ExecuteAsync(BulkDeleteRequest re Ids = ids }; var count = await agentManager.DeleteManyAsync(filter, ct); - return new BulkDeleteResponse(count); + return new(count); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs index c6bc64c1..4b6f6613 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Create/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Extensions; using Elsa.Agents.Persistence.Contracts; using Elsa.Agents.Persistence.Entities; @@ -39,7 +38,7 @@ public override async Task ExecuteAsync(AgentInputModel req, Cancell Id = identityGenerator.GenerateId(), Name = req.Name.Trim(), Description = req.Description.Trim(), - AgentConfig = new AgentConfig + AgentConfig = new() { Description = req.Description.Trim(), Name = req.Name.Trim(), @@ -47,9 +46,7 @@ public override async Task ExecuteAsync(AgentInputModel req, Cancell ExecutionSettings = req.ExecutionSettings, InputVariables = req.InputVariables, OutputVariable = req.OutputVariable, - Services = req.Services, Skills = req.Skills, - FunctionName = req.FunctionName, PromptTemplate = req.PromptTemplate } }; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs index 53e4ebf7..74c02b5b 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/GenerateUniqueName/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Agents.Persistence.Contracts; using JetBrains.Annotations; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Get/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Get/Endpoint.cs index 3b70756a..568e0a35 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Get/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Get/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Extensions; using Elsa.Agents.Persistence.Contracts; using JetBrains.Annotations; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs index 9d9f7e19..6e9c59e7 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Elsa.Abstractions; -using Elsa.Agents; using JetBrains.Annotations; namespace Elsa.Agents.Api.Endpoints.Agents.Invoke; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs index 4f0a00ac..db5c2e1f 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/IsUniqueName/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Agents.Persistence.Contracts; using JetBrains.Annotations; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/List/Endpoint.cs index 19c81b52..effe7ea0 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/List/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/List/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Extensions; using Elsa.Agents.Persistence.Contracts; using Elsa.Models; @@ -25,6 +24,6 @@ public override async Task> ExecuteAsync(CancellationTo { var entities = await agentManager.ListAsync(ct); var models = entities.Select(x => x.ToModel()).ToList(); - return new ListResponse(models); + return new(models); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs index 66c3b5c0..95d3ebaf 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Update/Endpoint.cs @@ -1,5 +1,4 @@ using Elsa.Abstractions; -using Elsa.Agents; using Elsa.Extensions; using Elsa.Agents.Persistence.Contracts; using JetBrains.Annotations; @@ -42,12 +41,10 @@ public override async Task ExecuteAsync(AgentInputModel req, Cancell entity.Name = req.Name.Trim(); entity.Description = req.Description.Trim(); - entity.AgentConfig = new AgentConfig + entity.AgentConfig = new() { Name = req.Name.Trim(), Description = req.Description.Trim(), - FunctionName = req.FunctionName.Trim(), - Services = req.Services, PromptTemplate = req.PromptTemplate.Trim(), InputVariables = req.InputVariables, OutputVariable = req.OutputVariable, diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs deleted file mode 100644 index a6704639..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/BulkDelete/Endpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.BulkDelete; - -/// -/// Deletes an API key. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/bulk-actions/api-keys/delete"); - ConfigurePermissions("ai/api-keys:delete"); - } - - /// - public override async Task ExecuteAsync(BulkDeleteRequest req, CancellationToken ct) - { - var ids = req.Ids; - var filter = new ApiKeyDefinitionFilter - { - Ids = ids - }; - var count = await store.DeleteManyAsync(filter, ct); - return new BulkDeleteResponse(count); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs deleted file mode 100644 index 2e5809a5..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Create/Endpoint.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using Elsa.Workflows; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Create; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store, IIdentityGenerator identityGenerator) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/api-keys"); - ConfigurePermissions("ai/api-keys:write"); - } - - /// - public override async Task ExecuteAsync(ApiKeyInputModel req, CancellationToken ct) - { - var existingEntityFilter = new ApiKeyDefinitionFilter - { - Name = req.Name - }; - var existingEntity = await store.FindAsync(existingEntityFilter, ct); - - if (existingEntity != null) - { - AddError("An API key already exists with the specified name"); - await Send.ErrorsAsync(cancellation: ct); - return existingEntity.ToModel(); - } - - var newEntity = new ApiKeyDefinition - { - Id = identityGenerator.GenerateId(), - Name = req.Name.Trim(), - Value = req.Value.Trim() - }; - - await store.AddAsync(newEntity, ct); - return newEntity.ToModel(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs deleted file mode 100644 index 793bc120..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents.Persistence.Contracts; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Delete; - -/// -/// Delete an API key. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Delete("/ai/api-keys/{id}"); - ConfigurePermissions("ai/api-keys:delete"); - } - - /// - public override async Task HandleAsync(Request req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return; - } - - await store.DeleteAsync(entity, ct); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs deleted file mode 100644 index c9c25040..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Request.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Delete; - -public class Request -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs deleted file mode 100644 index b1da20f1..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Get; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Get("/ai/api-keys/{id}"); - ConfigurePermissions("ai/api-keys:read"); - } - - /// - public override async Task ExecuteAsync(Request req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return null!; - } - - return entity.ToModel(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Request.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Request.cs deleted file mode 100644 index d8bf2d69..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Request.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Get; - -public class Request -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs deleted file mode 100644 index ef434a33..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/List/Endpoint.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Models; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.List; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpointWithoutRequest> -{ - /// - public override void Configure() - { - Get("/ai/api-keys"); - ConfigurePermissions("ai/api-keys:read"); - } - - /// - public override async Task> ExecuteAsync(CancellationToken ct) - { - var entities = await store.ListAsync(ct); - var models = entities.Select(x => x.ToModel()).ToList(); - return new ListResponse(models); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs deleted file mode 100644 index 1950c322..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ApiKeys/Update/Endpoint.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ApiKeys.Update; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IApiKeyStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/api-keys/{id}"); - ConfigurePermissions("ai/api-keys:write"); - } - - /// - public override async Task ExecuteAsync(ApiKeyModel req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return null!; - } - - var isNameDuplicate = await IsNameDuplicateAsync(req.Name, req.Id, ct); - - if (isNameDuplicate) - { - AddError("Another API key already exists with the specified name"); - await Send.ErrorsAsync(cancellation: ct); - return entity; - } - - entity.Name = req.Name.Trim(); - entity.Value = req.Value.Trim(); - - await store.UpdateAsync(entity, ct); - return entity; - } - - private async Task IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken) - { - var filter = new ApiKeyDefinitionFilter - { - Name = name, - NotId = id - }; - - var entity = await store.FindAsync(filter, cancellationToken); - return entity != null; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/ServiceProviders/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/ServiceProviders/List/Endpoint.cs deleted file mode 100644 index e81f4424..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/ServiceProviders/List/Endpoint.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Models; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.ServiceProviders.List; - -/// -/// Lists all registered service providers. -/// -[UsedImplicitly] -public class Endpoint(IServiceDiscoverer serviceDiscoverer) : ElsaEndpointWithoutRequest> -{ - /// - public override void Configure() - { - Get("/ai/service-providers"); - ConfigurePermissions("ai/services:read"); - } - - /// - public override Task> ExecuteAsync(CancellationToken ct) - { - var providers = serviceDiscoverer.Discover().Select(x => x.Name).ToList(); - return Task.FromResult(new ListResponse(providers)); - } -} diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs deleted file mode 100644 index 2b742d2e..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/BulkDelete/Endpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.BulkDelete; - -/// -/// Deletes an API key. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/bulk-actions/services/delete"); - ConfigurePermissions("ai/api-keys:delete"); - } - - /// - public override async Task ExecuteAsync(BulkDeleteRequest req, CancellationToken ct) - { - var ids = req.Ids; - var filter = new ServiceDefinitionFilter - { - Ids = ids - }; - var count = await store.DeleteManyAsync(filter, ct); - return new BulkDeleteResponse(count); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Create/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Create/Endpoint.cs deleted file mode 100644 index 1574922f..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Create/Endpoint.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using Elsa.Workflows; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Create; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store, IIdentityGenerator identityGenerator) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/services"); - ConfigurePermissions("ai/services:write"); - } - - /// - public override async Task ExecuteAsync(ServiceInputModel req, CancellationToken ct) - { - var existingEntityFilter = new ServiceDefinitionFilter - { - Name = req.Name - }; - var existingEntity = await store.FindAsync(existingEntityFilter, ct); - - if (existingEntity != null) - { - AddError("A Service already exists with the specified name"); - await Send.ErrorsAsync(cancellation: ct); - return existingEntity.ToModel(); - } - - var newEntity = new ServiceDefinition - { - Id = identityGenerator.GenerateId(), - Name = req.Name.Trim(), - Type = req.Type, - Settings = req.Settings - }; - - await store.AddAsync(newEntity, ct); - return newEntity.ToModel(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Endpoint.cs deleted file mode 100644 index 66f50fcd..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Endpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents.Persistence.Contracts; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Delete; - -/// -/// Delete an API key. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Delete("/ai/services/{id}"); - ConfigurePermissions("ai/services:delete"); - } - - /// - public override async Task HandleAsync(Request req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return; - } - - await store.DeleteAsync(entity, ct); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Request.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Request.cs deleted file mode 100644 index 24698c9c..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Delete/Request.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Delete; - -public class Request -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Endpoint.cs deleted file mode 100644 index f7c76a27..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Endpoint.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Get; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Get("/ai/services/{id}"); - ConfigurePermissions("ai/services:read"); - } - - /// - public override async Task ExecuteAsync(Request req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return null!; - } - - return entity.ToModel(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Request.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Request.cs deleted file mode 100644 index 5d7d0470..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Get/Request.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Get; - -public class Request -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/List/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/List/Endpoint.cs deleted file mode 100644 index bfe5107b..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/List/Endpoint.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Models; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.List; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpointWithoutRequest> -{ - /// - public override void Configure() - { - Get("/ai/services"); - ConfigurePermissions("ai/services:read"); - } - - /// - public override async Task> ExecuteAsync(CancellationToken ct) - { - var entities = await store.ListAsync(ct); - var models = entities.Select(x => x.ToModel()).ToList(); - return new ListResponse(models); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Update/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Update/Endpoint.cs deleted file mode 100644 index 6684ba75..00000000 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Services/Update/Endpoint.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Elsa.Abstractions; -using Elsa.Agents; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Api.Endpoints.Services.Update; - -/// -/// Lists all registered API keys. -/// -[UsedImplicitly] -public class Endpoint(IServiceStore store) : ElsaEndpoint -{ - /// - public override void Configure() - { - Post("/ai/services/{id}"); - ConfigurePermissions("ai/services:write"); - } - - /// - public override async Task ExecuteAsync(ServiceModel req, CancellationToken ct) - { - var entity = await store.GetAsync(req.Id, ct); - - if(entity == null) - { - await Send.NotFoundAsync(ct); - return null!; - } - - var isNameDuplicate = await IsNameDuplicateAsync(req.Name, req.Id, ct); - - if (isNameDuplicate) - { - AddError("Another service already exists with the specified name"); - await Send.ErrorsAsync(cancellation: ct); - return entity.ToModel(); - } - - entity.Name = req.Name.Trim(); - entity.Type = req.Type.Trim(); - entity.Settings = req.Settings; - - await store.UpdateAsync(entity, ct); - return entity.ToModel(); - } - - private async Task IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken) - { - var filter = new ServiceDefinitionFilter - { - Name = name, - NotId = id - }; - - var entity = await store.FindAsync(filter, cancellationToken); - return entity != null; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs b/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs index d71b24fe..7232a688 100644 --- a/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Api/Extensions/AgentDefinitionExtensions.cs @@ -8,7 +8,7 @@ public static class AgentDefinitionExtensions { public static AgentModel ToModel(this AgentDefinition agentDefinition) { - return new AgentModel + return new() { Id = agentDefinition.Id, Name = agentDefinition.Name, @@ -17,9 +17,7 @@ public static AgentModel ToModel(this AgentDefinition agentDefinition) ExecutionSettings = agentDefinition.AgentConfig.ExecutionSettings, InputVariables = agentDefinition.AgentConfig.InputVariables, OutputVariable = agentDefinition.AgentConfig.OutputVariable, - Services = agentDefinition.AgentConfig.Services, Skills = agentDefinition.AgentConfig.Skills, - FunctionName = agentDefinition.AgentConfig.FunctionName, PromptTemplate = agentDefinition.AgentConfig.PromptTemplate }; } diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentServiceProvider.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentServiceProvider.cs deleted file mode 100644 index 1eb96057..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentServiceProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Agents; - -public interface IAgentServiceProvider -{ - string Name { get; } - void ConfigureKernel(KernelBuilderContext context); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IServiceDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IServiceDiscoverer.cs deleted file mode 100644 index 681e6fd4..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IServiceDiscoverer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public interface IServiceDiscoverer -{ - IEnumerable Discover(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs new file mode 100644 index 00000000..b57541fa --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs @@ -0,0 +1,9 @@ +using Microsoft.SemanticKernel; + +namespace Elsa.Agents; + +public class ServiceDescriptor +{ + public string Name { get; set; } = null!; + public Action ConfigureKernel { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj index 4fe13aaa..e0300c3e 100644 --- a/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj +++ b/src/modules/agents/Elsa.Agents.Core/Elsa.Agents.Core.csproj @@ -17,21 +17,18 @@ - + - - - - + diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs deleted file mode 100644 index c28b8997..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/AgentConfigExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -#pragma warning disable SKEXP0001 -#pragma warning disable SKEXP0010 - -namespace Elsa.Agents; - -public static class AgentConfigExtensions -{ - public static OpenAIPromptExecutionSettings ToOpenAIPromptExecutionSettings(this AgentConfig agentConfig) - { - return new() - { - Temperature = agentConfig.ExecutionSettings.Temperature, - TopP = agentConfig.ExecutionSettings.TopP, - MaxTokens = agentConfig.ExecutionSettings.MaxTokens, - PresencePenalty = agentConfig.ExecutionSettings.PresencePenalty, - FrequencyPenalty = agentConfig.ExecutionSettings.FrequencyPenalty, - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, - ResponseFormat = agentConfig.ExecutionSettings.ResponseFormat, - ChatSystemPrompt = agentConfig.PromptTemplate, - ServiceId = "default" - }; - } - - public static PromptTemplateConfig ToPromptTemplateConfig(this AgentConfig agentConfig) - { - var promptExecutionSettingsDictionary = new Dictionary - { - [PromptExecutionSettings.DefaultServiceId] = agentConfig.ToOpenAIPromptExecutionSettings(), - }; - - return new() - { - Name = agentConfig.FunctionName, - Description = agentConfig.Description, - Template = agentConfig.PromptTemplate, - ExecutionSettings = promptExecutionSettingsDictionary, - AllowDangerouslySetContent = true, - InputVariables = agentConfig.InputVariables.Select(x => new InputVariable - { - Name = x.Name, - Description = x.Description, - IsRequired = true, - AllowDangerouslySetContent = true - }).ToList() - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs deleted file mode 100644 index 38d7df88..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/CodeFirstAgentResolverExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Elsa.Agents; - -public static class CodeFirstAgentResolverExtensions -{ - public static async Task ResolveAsync(this IAgentResolver resolver, CancellationToken cancellationToken = default) where TAgent : IAgent - { - var agentName = typeof(TAgent).Name; - return (TAgent)await resolver.ResolveAsync(agentName, cancellationToken); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs index ee9e5116..baaf3bbf 100644 --- a/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Extensions/ServiceCollectionExtensions.cs @@ -8,9 +8,4 @@ public static IServiceCollection AddSkillsProvider(this IServiceCollection se { 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/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs index 8769e8b8..b88ed2eb 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs @@ -31,15 +31,11 @@ public override void Apply() .AddScoped() .AddScoped() .AddScoped() - .AddScoped() .AddScoped(_kernelConfigProviderFactory) .AddScoped() .AddScoped() .AddSkillsProvider() .AddSkillsProvider() - .AddAgentServiceProvider() - .AddAgentServiceProvider() - .AddAgentServiceProvider() ; } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs deleted file mode 100644 index 5c32e3c3..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Models/AgentWorkflowResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Elsa.Agents; - -/// -/// Result of executing an agent workflow. -/// -public class AgentWorkflowResult(AgentWorkflowConfig workflowConfig, string output, ChatHistory chatHistory) -{ - /// - /// The workflow configuration that was executed. - /// - public AgentWorkflowConfig WorkflowConfig { get; } = workflowConfig; - - /// - /// The final output from the workflow. - /// - public string Output { get; } = output; - - /// - /// The complete chat history from the workflow execution. - /// - public ChatHistory ChatHistory { get; } = chatHistory; -} diff --git a/src/modules/agents/Elsa.Agents.Core/Models/KernelBuilderContext.cs b/src/modules/agents/Elsa.Agents.Core/Models/KernelBuilderContext.cs deleted file mode 100644 index 8fe3dc15..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Models/KernelBuilderContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JetBrains.Annotations; -using Microsoft.SemanticKernel; - -namespace Elsa.Agents; - -[UsedImplicitly] -public record KernelBuilderContext(IKernelBuilder KernelBuilder, KernelConfig KernelConfig, ServiceConfig ServiceConfig) -{ - public string GetApiKey() - { - var settings = ServiceConfig.Settings; - if (settings.TryGetValue("ApiKey", out var apiKey)) - return (string)apiKey; - - if (settings.TryGetValue("ApiKeyRef", out var apiKeyRef)) - return KernelConfig.ApiKeys[(string)apiKeyRef].Value; - - throw new KeyNotFoundException($"No api key found for service {ServiceConfig.Type}"); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs index 2f62f233..aa5e7e39 100644 --- a/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs @@ -2,7 +2,6 @@ namespace Elsa.Agents; public class ConfiguredAgentOptions { - public ICollection ApiKeys { get; set; } = new List(); - public ICollection Services { get; set; } = new List(); public ICollection Agents { get; set; } = new List(); + public ICollection ServiceDescriptors { get; set; } = new List(); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs b/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs deleted file mode 100644 index 771582f3..00000000 --- a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIChatCompletionProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.SemanticKernel; - -namespace Elsa.Agents; - -public class OpenAIChatCompletionProvider : IAgentServiceProvider -{ - public string Name => "OpenAIChatCompletion"; - - public void ConfigureKernel(KernelBuilderContext context) - { - var modelId = (string)context.ServiceConfig.Settings["ModelId"]; - var apiKey = context.GetApiKey(); - context.KernelBuilder.AddOpenAIChatCompletion(modelId, apiKey); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIEmbeddingGenerator.cs b/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIEmbeddingGenerator.cs deleted file mode 100644 index accfde30..00000000 --- a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAIEmbeddingGenerator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Elsa.Extensions; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.InMemory; -using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Elsa.Agents; - -public class OpenAIEmbeddingGenerator : IAgentServiceProvider -{ - public string Name => "OpenAIEmbeddingGenerator"; - - [Experimental("SKEXP0010")] - public void ConfigureKernel(KernelBuilderContext context) - { - var modelId = (string)context.ServiceConfig.Settings["ModelId"]; - var apiKey = context.GetApiKey(); - - context.KernelBuilder.Services.AddInMemoryVectorStore(); - context.KernelBuilder.AddOpenAIEmbeddingGenerator(modelId, apiKey); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs b/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs deleted file mode 100644 index b20aba85..00000000 --- a/src/modules/agents/Elsa.Agents.Core/ServiceProviders/OpenAITextToImageProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.SemanticKernel; -#pragma warning disable SKEXP0010 - -namespace Elsa.Agents; - -public class OpenAITextToImageProvider : IAgentServiceProvider -{ - public string Name => "OpenAITextToImage"; - - public void ConfigureKernel(KernelBuilderContext context) - { - var modelId = (string)context.ServiceConfig.Settings["ModelId"]; - var apiKey = context.GetApiKey(); - context.KernelBuilder.AddOpenAITextToImage(apiKey, modelId: modelId); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs index b742677f..26f39b6a 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; @@ -14,16 +15,16 @@ namespace Elsa.Agents; /// public class AgentFactory( ISkillDiscoverer skillDiscoverer, - IServiceDiscoverer serviceDiscoverer, IServiceProvider serviceProvider, + IOptions options, ILogger logger) { /// /// Creates a ChatCompletionAgent from an Elsa agent configuration. /// - public ChatCompletionAgent CreateAgent(KernelConfig kernelConfig, AgentConfig agentConfig) + public ChatCompletionAgent CreateAgent(AgentConfig agentConfig) { - var kernel = CreateKernel(kernelConfig, agentConfig); + var kernel = CreateKernel(agentConfig); return new() { @@ -37,45 +38,27 @@ public ChatCompletionAgent CreateAgent(KernelConfig kernelConfig, AgentConfig ag /// /// Creates a Kernel configured for the specified agent. /// - private Kernel CreateKernel(KernelConfig kernelConfig, AgentConfig agentConfig) + private Kernel CreateKernel(AgentConfig agentConfig) { var builder = Kernel.CreateBuilder(); builder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Trace)); builder.Services.AddSingleton(agentConfig); - ApplyAgentConfig(builder, kernelConfig, agentConfig); + ApplyAgentConfig(builder, agentConfig); + ApplyServiceDescriptors(builder, options.Value.ServiceDescriptors); return builder.Build(); } - private void ApplyAgentConfig(IKernelBuilder builder, KernelConfig kernelConfig, AgentConfig agentConfig) + private void ApplyServiceDescriptors(IKernelBuilder builder, ICollection serviceDescriptors) { - 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); - } - - AddSkills(builder, agentConfig); + foreach (var descriptor in serviceDescriptors) + descriptor.ConfigureKernel(builder); } - private void AddService(IKernelBuilder builder, KernelConfig kernelConfig, ServiceConfig serviceConfig, Dictionary services) + private void ApplyAgentConfig(IKernelBuilder builder, AgentConfig agentConfig) { - 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); + AddSkills(builder, agentConfig); } private void AddSkills(IKernelBuilder builder, AgentConfig agent) diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs index e41bf6ca..8081b17f 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs @@ -20,7 +20,7 @@ public async Task InvokeAgentAsync(string agentName, IDiction var agentConfig = kernelConfig.Agents[agentName]; // Create agent using Agent Framework - var agent = agentFactory.CreateAgent(kernelConfig, agentConfig); + var agent = agentFactory.CreateAgent(agentConfig); // Create chat history ChatHistory chatHistory = []; @@ -30,7 +30,7 @@ public async Task InvokeAgentAsync(string agentName, IDiction { Template = agentConfig.PromptTemplate, TemplateFormat = "handlebars", - Name = agentConfig.FunctionName, + Name = "Run", AllowDangerouslySetContent = true, }; diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs index 0cf0f07f..4c1ad75b 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs @@ -4,7 +4,7 @@ namespace Elsa.Agents; /// -/// Provides kernel configuration by merging configuration-based agents with code-first definitions. +/// Provides kernel configuration from configuration. /// [UsedImplicitly] public class ConfigurationKernelConfigProvider(IOptions options) : IKernelConfigProvider @@ -13,19 +13,6 @@ public Task GetKernelConfigAsync(CancellationToken cancellationTok { var kernelConfig = new KernelConfig(); - // Add configuration-based items (if available) - if (options.Value.ApiKeys != null!) - { - foreach (var apiKey in options.Value.ApiKeys) - kernelConfig.ApiKeys[apiKey.Name] = apiKey; - } - - if (options.Value.Services != null!) - { - foreach (var service in options.Value.Services) - kernelConfig.Services[service.Name] = service; - } - if (options.Value.Agents != null!) { foreach (var agent in options.Value.Agents) diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ServiceDiscoverer.cs b/src/modules/agents/Elsa.Agents.Core/Services/ServiceDiscoverer.cs deleted file mode 100644 index 46f18c6e..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/ServiceDiscoverer.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Elsa.Agents; - -public class ServiceDiscoverer(IEnumerable providers) : IServiceDiscoverer -{ - public IEnumerable Discover() - { - return providers; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs b/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs index 566a8416..2208dc63 100644 --- a/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs +++ b/src/modules/agents/Elsa.Agents.Models/Agents/AgentInputModel.cs @@ -6,8 +6,6 @@ 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(); diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs index 62af3ef1..9dc3c6a3 100644 --- a/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs +++ b/src/modules/agents/Elsa.Agents.Models/Configs/AgentConfig.cs @@ -4,8 +4,6 @@ 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(); diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/AgentWorkflowConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/AgentWorkflowConfig.cs deleted file mode 100644 index 76c6269e..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/AgentWorkflowConfig.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Configuration for a multi-agent workflow (team/sequence/graph). -/// -public class AgentWorkflowConfig -{ - /// - /// The name of the agent workflow. - /// - public string Name { get; set; } = ""; - - /// - /// The description of the agent workflow. - /// - public string Description { get; set; } = ""; - - /// - /// The type of workflow orchestration (Sequential, Parallel, Graph). - /// - public AgentWorkflowType WorkflowType { get; set; } = AgentWorkflowType.Sequential; - - /// - /// The agents participating in this workflow. - /// - public ICollection Agents { get; set; } = []; - - /// - /// Services required by the workflow. - /// - public ICollection Services { get; set; } = []; - - /// - /// Input variables for the workflow. - /// - public ICollection InputVariables { get; set; } = []; - - /// - /// Output variable for the workflow. - /// - public OutputVariableConfig OutputVariable { get; set; } = new(); - - /// - /// Execution settings for the workflow. - /// - public ExecutionSettingsConfig ExecutionSettings { get; set; } = new(); - - /// - /// The termination strategy for the workflow (e.g., after N messages, on specific condition). - /// - public TerminationConfig Termination { get; set; } = new(); - - /// - /// Optional selection strategy configuration for determining which agent acts next. - /// - public SelectionStrategyConfig? SelectionStrategy { get; set; } -} - -/// -/// Types of agent workflow orchestration. -/// -public enum AgentWorkflowType -{ - /// - /// Agents execute sequentially in order. - /// - Sequential, - - /// - /// Custom graph-based orchestration where agent selection is determined by a strategy. - /// - Graph -} diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/ApiKeyConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/ApiKeyConfig.cs deleted file mode 100644 index 88b4e733..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/ApiKeyConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -using JetBrains.Annotations; - -namespace Elsa.Agents; - -[UsedImplicitly] -public class ApiKeyConfig -{ - public string Name { get; set; } - public string Value { get; set; } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs index 35dbe4b4..427325c0 100644 --- a/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs +++ b/src/modules/agents/Elsa.Agents.Models/Configs/KernelConfig.cs @@ -2,8 +2,5 @@ namespace Elsa.Agents; public class KernelConfig { - public IDictionary ApiKeys { get; set; } = new Dictionary(); - public IDictionary Services { get; } = new Dictionary(); public IDictionary Agents { get; } = new Dictionary(); - public IDictionary AgentWorkflows { get; } = new Dictionary(); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/ServiceConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/ServiceConfig.cs deleted file mode 100644 index 4ad83b92..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/ServiceConfig.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Elsa.Agents; - -public class ServiceConfig -{ - public string Name { get; set; } - public string Type { get; set; } - public IDictionary Settings { get; set; } = new Dictionary(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Services/ServiceInputModel.cs b/src/modules/agents/Elsa.Agents.Models/Services/ServiceInputModel.cs deleted file mode 100644 index 2ffa016a..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Services/ServiceInputModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents; - -public class ServiceInputModel -{ - [Required] public string Name { get; set; } = null!; - [Required] public string Type { get; set; } = null!; - public IDictionary Settings { get; set; } = new Dictionary(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Services/ServiceModel.cs b/src/modules/agents/Elsa.Agents.Models/Services/ServiceModel.cs deleted file mode 100644 index 845c835f..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Services/ServiceModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Elsa.Agents; - -public class ServiceModel : ServiceInputModel -{ - [Required] public string Id { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.OpenAI/Class1.cs b/src/modules/agents/Elsa.Agents.OpenAI/Class1.cs new file mode 100644 index 00000000..46ac11cc --- /dev/null +++ b/src/modules/agents/Elsa.Agents.OpenAI/Class1.cs @@ -0,0 +1,5 @@ +namespace Elsa.Agents.OpenAI; + +public class Class1 +{ +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj b/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj new file mode 100644 index 00000000..2cbd22bf --- /dev/null +++ b/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj @@ -0,0 +1,22 @@ + + + + Provides OpenAI integration with Elsa Agents + elsa extension module agents openai + + + + + + + + + + + + + + + + + diff --git a/src/modules/agents/Elsa.Agents.OpenAI/FodyWeavers.xml b/src/modules/agents/Elsa.Agents.OpenAI/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.OpenAI/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Elsa.Agents.Persistence.EFCore.Sqlite.csproj b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Elsa.Agents.Persistence.EFCore.Sqlite.csproj index 98750688..e359f9e0 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Elsa.Agents.Persistence.EFCore.Sqlite.csproj +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Elsa.Agents.Persistence.EFCore.Sqlite.csproj @@ -1,6 +1,7 @@  + net8.0;net9.0;net10.0 Provides an EF Core migrations for SQLite for the Agents Persistence module. elsa extension module agents semantic kernel llm ai persistence efcore entity framework core sqlite diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh new file mode 100644 index 00000000..b6b4bddc --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh @@ -0,0 +1,2 @@ +#ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -p ./ -o Migrations --startup-project ./Elsa.Agents.Persistence.EFCore.Sqlite.csproj" \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/Configurations.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/Configurations.cs index 72aaae52..bc2ab74a 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/Configurations.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore/Configurations.cs @@ -8,21 +8,8 @@ namespace Elsa.Agents.Persistence.EFCore; /// /// EF Core configuration for various entity types. /// -public class Configurations : IEntityTypeConfiguration, IEntityTypeConfiguration, IEntityTypeConfiguration +public class Configurations : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) - { - builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(ApiKeyDefinition)}_{nameof(ApiKeyDefinition.Name)}"); - builder.HasIndex(x => x.TenantId).HasDatabaseName($"IX_{nameof(ApiKeyDefinition)}_{nameof(ApiKeyDefinition.TenantId)}"); - } - - public void Configure(EntityTypeBuilder builder) - { - builder.Property(x => x.Settings).HasJsonValueConversion(); - builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(ServiceDefinition)}_{nameof(ServiceDefinition.Name)}"); - builder.HasIndex(x => x.TenantId).HasDatabaseName($"IX_{nameof(ServiceDefinition)}_{nameof(ServiceDefinition.TenantId)}"); - } - public void Configure(EntityTypeBuilder builder) { builder.Property(x => x.AgentConfig).HasJsonValueConversion(); diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/DbContext.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/DbContext.cs index 4bcbaa52..bded4ef0 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/DbContext.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore/DbContext.cs @@ -16,28 +16,16 @@ public AgentsDbContext(DbContextOptions options, IServiceProvid { } - /// - /// The API Keys DB set. - /// - public DbSet ApiKeysDefinitions { get; set; } = null!; - - /// - /// The Services DB set. - /// - public DbSet ServicesDefinitions { get; set; } = null!; - /// /// The Services DB set. /// - public DbSet AgentDefinitions { get; set; } = null!; + [UsedImplicitly] public DbSet AgentDefinitions { get; set; } = null!; /// protected override void OnModelCreating(ModelBuilder modelBuilder) { var configuration = new Configurations(); - modelBuilder.ApplyConfiguration(configuration); - modelBuilder.ApplyConfiguration(configuration); - modelBuilder.ApplyConfiguration(configuration); + modelBuilder.ApplyConfiguration(configuration); base.OnModelCreating(modelBuilder); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreApiKeyStore.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreApiKeyStore.cs deleted file mode 100644 index fe3f10b2..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreApiKeyStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.Persistence.EFCore; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence.EFCore; - -/// -/// An EF Core implementation of . -/// -[UsedImplicitly] -public class EFCoreApiKeyStore(EntityStore store) : IApiKeyStore -{ - public Task AddAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - return store.AddAsync(entity, cancellationToken); - } - - public Task UpdateAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - return store.UpdateAsync(entity, cancellationToken); - } - - public Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var filter = new ApiKeyDefinitionFilter - { - Id = id - }; - - return FindAsync(filter, cancellationToken); - } - - public Task FindAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default) - { - return store.FindAsync(filter.Apply, cancellationToken); - } - - public Task> ListAsync(CancellationToken cancellationToken = default) - { - return store.ListAsync(cancellationToken); - } - - public Task DeleteAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - return store.DeleteAsync(entity, cancellationToken); - } - - public Task DeleteManyAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default) - { - return store.DeleteWhereAsync(filter.Apply, cancellationToken); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreServiceStore.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreServiceStore.cs deleted file mode 100644 index 9127e73e..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/EFCoreServiceStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.Persistence.EFCore; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence.EFCore; - -/// -/// An EF Core implementation of . -/// -[UsedImplicitly] -public class EFCoreServiceStore(EntityStore store) : IServiceStore -{ - public Task AddAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - return store.AddAsync(entity, cancellationToken); - } - - public Task UpdateAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - return store.UpdateAsync(entity, cancellationToken); - } - - public Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var filter = new ServiceDefinitionFilter - { - Id = id - }; - - return FindAsync(filter, cancellationToken); - } - - public Task FindAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default) - { - return store.FindAsync(filter.Apply, cancellationToken); - } - - public Task> ListAsync(CancellationToken cancellationToken = default) - { - return store.ListAsync(cancellationToken); - } - - public Task DeleteAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - return store.DeleteAsync(entity, cancellationToken); - } - - public Task DeleteManyAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default) - { - return store.DeleteWhereAsync(filter.Apply, cancellationToken); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore/Feature.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore/Feature.cs index 2e071632..780b65ab 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore/Feature.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore/Feature.cs @@ -18,11 +18,7 @@ public override void Configure() { Module.Configure(feature => { - feature - .UseApiKeyStore(sp => sp.GetRequiredService()) - .UseServiceStore(sp => sp.GetRequiredService()) - .UseAgentStore(sp => sp.GetRequiredService()); - ; + feature.UseAgentStore(sp => sp.GetRequiredService()); }); } @@ -30,8 +26,6 @@ public override void Configure() public override void Apply() { base.Apply(); - AddEntityStore(); - AddEntityStore(); AddEntityStore(); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Contracts/IApiKeyStore.cs b/src/modules/agents/Elsa.Agents.Persistence/Contracts/IApiKeyStore.cs deleted file mode 100644 index 43fd3862..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Contracts/IApiKeyStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; - -namespace Elsa.Agents.Persistence.Contracts; - -public interface IApiKeyStore -{ - /// - /// Adds a new entity to the store. - /// - Task AddAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Updates the entity to the store. - /// - Task UpdateAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Gets the entity from the store. - /// - Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// - /// Finds a single entity using the specified filter. - /// - Task FindAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default); - - /// - /// Gets all entities from the store. - /// - Task> ListAsync(CancellationToken cancellationToken = default); - - /// - /// Deletes the entity from the store. - /// - Task DeleteAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Deletes all entities from the store that match the specified filter. - /// - Task DeleteManyAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Contracts/IServiceStore.cs b/src/modules/agents/Elsa.Agents.Persistence/Contracts/IServiceStore.cs deleted file mode 100644 index 5e3d98d0..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Contracts/IServiceStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; - -namespace Elsa.Agents.Persistence.Contracts; - -public interface IServiceStore -{ - /// - /// Adds a new entity to the store. - /// - Task AddAsync(ServiceDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Updates the entity to the store. - /// - Task UpdateAsync(ServiceDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Gets the entity from the store. - /// - Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// - /// Finds the entity from the store. - /// - Task FindAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default); - - /// - /// Gets all entities from the store. - /// - Task> ListAsync(CancellationToken cancellationToken = default); - - /// - /// Deletes the entity from the store. - /// - Task DeleteAsync(ServiceDefinition entity, CancellationToken cancellationToken = default); - - /// - /// Deletes all entities from the store that match the specified filter. - /// - Task DeleteManyAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Entities/AgentDefinition.cs b/src/modules/agents/Elsa.Agents.Persistence/Entities/AgentDefinition.cs index 9715eafc..457e9b76 100644 --- a/src/modules/agents/Elsa.Agents.Persistence/Entities/AgentDefinition.cs +++ b/src/modules/agents/Elsa.Agents.Persistence/Entities/AgentDefinition.cs @@ -1,4 +1,3 @@ -using Elsa.Agents; using Elsa.Common.Entities; namespace Elsa.Agents.Persistence.Entities; diff --git a/src/modules/agents/Elsa.Agents.Persistence/Entities/ApiKeyDefinition.cs b/src/modules/agents/Elsa.Agents.Persistence/Entities/ApiKeyDefinition.cs deleted file mode 100644 index 96beb19b..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Entities/ApiKeyDefinition.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Elsa.Agents; -using Elsa.Common.Entities; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence.Entities; - -[UsedImplicitly] -public class ApiKeyDefinition : Entity -{ - public string Name { get; set; } = null!; - public string Value { get; set; } = null!; - - public ApiKeyConfig ToApiKeyConfig() - { - return new ApiKeyConfig - { - Name = Name, - Value = Value - }; - } - - public ApiKeyModel ToModel() - { - return new ApiKeyModel - { - Id = Id, - Name = Name, - Value = Value - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Entities/ServiceDefinition.cs b/src/modules/agents/Elsa.Agents.Persistence/Entities/ServiceDefinition.cs deleted file mode 100644 index a9a47dca..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Entities/ServiceDefinition.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Elsa.Agents; -using Elsa.Common.Entities; - -namespace Elsa.Agents.Persistence.Entities; - -public class ServiceDefinition : Entity -{ - public string Name { get; set; } - public string Type { get; set; } - public IDictionary Settings { get; set; } = new Dictionary(); - - public ServiceConfig ToServiceConfig() - { - return new ServiceConfig - { - Name = Name, - Type = Type, - Settings = Settings - }; - } - - public ServiceModel ToModel() - { - return new ServiceModel - { - Id = Id, - Name = Name, - Type = Type, - Settings = Settings - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs b/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs index 53e783f2..a70a6fd5 100644 --- a/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs +++ b/src/modules/agents/Elsa.Agents.Persistence/Features/AgentPersistenceFeature.cs @@ -12,22 +12,8 @@ namespace Elsa.Agents.Persistence.Features; [DependsOn(typeof(AgentsCoreFeature))] public class AgentPersistenceFeature(IModule module) : FeatureBase(module) { - private Func _apiKeyStoreFactory = sp => sp.GetRequiredService(); - private Func _serviceStoreFactory = sp => sp.GetRequiredService(); private Func _agentStoreFactory = sp => sp.GetRequiredService(); - public AgentPersistenceFeature UseApiKeyStore(Func factory) - { - _apiKeyStoreFactory = factory; - return this; - } - - public AgentPersistenceFeature UseServiceStore(Func factory) - { - _serviceStoreFactory = factory; - return this; - } - public AgentPersistenceFeature UseAgentStore(Func factory) { _agentStoreFactory = factory; @@ -41,19 +27,9 @@ public override void Configure() public override void Apply() { - Services - .AddScoped(_apiKeyStoreFactory) - .AddScoped(_serviceStoreFactory) - .AddScoped(_agentStoreFactory); - - Services - .AddScoped(); - - Services - .AddMemoryStore() - .AddMemoryStore() - .AddMemoryStore(); - + Services.AddScoped(_agentStoreFactory); + Services.AddScoped(); + Services.AddMemoryStore(); Services.AddScoped(); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs b/src/modules/agents/Elsa.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs deleted file mode 100644 index 42bc2ed2..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Filters/ApiKeyDefinitionFilter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Elsa.Agents.Persistence.Entities; - -namespace Elsa.Agents.Persistence.Filters; - -public class ApiKeyDefinitionFilter -{ - public string? Id { get; set; } - public ICollection? Ids { get; set; } - public string? NotId { get; set; } - public string? Name { get; set; } - - public IQueryable Apply(IQueryable queryable) - { - if (!string.IsNullOrWhiteSpace(Id)) queryable = queryable.Where(x => x.Id == Id); - if (Ids != null && Ids.Any()) queryable = queryable.Where(x => Ids.Contains(x.Id)); - if (!string.IsNullOrWhiteSpace(NotId)) queryable = queryable.Where(x => x.Id != NotId); - if (!string.IsNullOrWhiteSpace(Name)) queryable = queryable.Where(x => x.Name == Name); - return queryable; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Filters/ServiceDefinitionFilter.cs b/src/modules/agents/Elsa.Agents.Persistence/Filters/ServiceDefinitionFilter.cs deleted file mode 100644 index eb74a335..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Filters/ServiceDefinitionFilter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Elsa.Agents.Persistence.Entities; - -namespace Elsa.Agents.Persistence.Filters; - -public class ServiceDefinitionFilter -{ - public string? Id { get; set; } - public ICollection? Ids { get; set; } - public string? NotId { get; set; } - public string? Name { get; set; } - - public IQueryable Apply(IQueryable queryable) - { - if (!string.IsNullOrWhiteSpace(Id)) queryable = queryable.Where(x => x.Id == Id); - if (Ids != null && Ids.Any()) queryable = queryable.Where(x => Ids.Contains(x.Id)); - if (!string.IsNullOrWhiteSpace(NotId)) queryable = queryable.Where(x => x.Id != NotId); - if (!string.IsNullOrWhiteSpace(Name)) queryable = queryable.Where(x => x.Name == Name); - return queryable; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryApiKeyStore.cs b/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryApiKeyStore.cs deleted file mode 100644 index 05c6984c..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryApiKeyStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.Common.Services; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence; - -[UsedImplicitly] -public class MemoryApiKeyStore(MemoryStore memoryStore) : IApiKeyStore -{ - public Task AddAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Add(entity, x => x.Id); - return Task.CompletedTask; - } - - public Task UpdateAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Save(entity, x => x.Id); - return Task.CompletedTask; - } - - public Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var entity = memoryStore.Find(x => x.Id == id); - return Task.FromResult(entity); - } - - public Task FindAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default) - { - var entity = memoryStore.Query(filter.Apply).FirstOrDefault(); - return Task.FromResult(entity); - } - - public Task> ListAsync(CancellationToken cancellationToken = default) - { - var entities = memoryStore.List(); - return Task.FromResult(entities); - } - - public Task DeleteAsync(ApiKeyDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Delete(entity.Id); - return Task.CompletedTask; - } - - public Task DeleteManyAsync(ApiKeyDefinitionFilter filter, CancellationToken cancellationToken = default) - { - var agents = memoryStore.Query(filter.Apply).ToList(); - memoryStore.DeleteMany(agents, x => x.Id); - return Task.FromResult(agents.Count); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryServiceStore.cs b/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryServiceStore.cs deleted file mode 100644 index e945aacf..00000000 --- a/src/modules/agents/Elsa.Agents.Persistence/Services/MemoryServiceStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.Common.Services; -using Elsa.Agents.Persistence.Contracts; -using Elsa.Agents.Persistence.Entities; -using Elsa.Agents.Persistence.Filters; -using JetBrains.Annotations; - -namespace Elsa.Agents.Persistence; - -[UsedImplicitly] -public class MemoryServiceStore(MemoryStore memoryStore) : IServiceStore -{ - public Task AddAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Add(entity, x => x.Id); - return Task.CompletedTask; - } - - public Task UpdateAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Save(entity, x => x.Id); - return Task.CompletedTask; - } - - public Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var entity = memoryStore.Find(x => x.Id == id); - return Task.FromResult(entity); - } - - public Task FindAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default) - { - var entity = memoryStore.Query(filter.Apply).FirstOrDefault(); - return Task.FromResult(entity); - } - - public Task> ListAsync(CancellationToken cancellationToken = default) - { - var entities = memoryStore.List(); - return Task.FromResult(entities); - } - - public Task DeleteAsync(ServiceDefinition entity, CancellationToken cancellationToken = default) - { - memoryStore.Delete(entity.Id); - return Task.CompletedTask; - } - - public Task DeleteManyAsync(ServiceDefinitionFilter filter, CancellationToken cancellationToken = default) - { - var agents = memoryStore.Query(filter.Apply).ToList(); - memoryStore.DeleteMany(agents, x => x.Id); - return Task.FromResult(agents.Count); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence/Services/StoreKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Persistence/Services/StoreKernelConfigProvider.cs index 8d144518..b926dd89 100644 --- a/src/modules/agents/Elsa.Agents.Persistence/Services/StoreKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Persistence/Services/StoreKernelConfigProvider.cs @@ -1,18 +1,13 @@ -using Elsa.Agents; using Elsa.Agents.Persistence.Contracts; namespace Elsa.Agents.Persistence; -public class StoreKernelConfigProvider(IApiKeyStore apiKeyStore, IServiceStore serviceStore, IAgentStore agentStore) : IKernelConfigProvider +public class StoreKernelConfigProvider(IAgentStore agentStore) : IKernelConfigProvider { public async Task GetKernelConfigAsync(CancellationToken cancellationToken = default) { var kernelConfig = new KernelConfig(); - var apiKeys = await apiKeyStore.ListAsync(cancellationToken); - var services = await serviceStore.ListAsync(cancellationToken); var agents = await agentStore.ListAsync(cancellationToken); - foreach (var apiKey in apiKeys) kernelConfig.ApiKeys[apiKey.Name] = apiKey.ToApiKeyConfig(); - foreach (var service in services) kernelConfig.Services[service.Name] = service.ToServiceConfig(); foreach (var agent in agents) kernelConfig.Agents[agent.Name] = agent.ToAgentConfig(); return kernelConfig; } diff --git a/src/modules/agents/Elsa.Agents/AgentsFeature.cs b/src/modules/agents/Elsa.Agents/AgentsFeature.cs index 2f812244..e759b9ea 100644 --- a/src/modules/agents/Elsa.Agents/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents/AgentsFeature.cs @@ -11,8 +11,15 @@ namespace Elsa.Agents; [DependsOn(typeof(AgentActivitiesFeature))] public class AgentsFeature(IModule module) : FeatureBase(module) { - public void AddAgent(string? key = null) where TAgent : class, IAgent + public AgentsFeature AddAgent(string? key = null) where TAgent : class, IAgent { Module.Services.Configure(options => options.AddAgent(key)); + return this; + } + + public AgentsFeature AddServiceDescriptor(ServiceDescriptor descriptor) + { + Module.Services.Configure(options => options.ServiceDescriptors.Add(descriptor)); + return this; } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs b/src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs index 730cefe1..0dc19de4 100644 --- a/src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs +++ b/src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs @@ -1,12 +1,11 @@ using Elsa.Studio.Contracts; using Elsa.Studio.Localization; using Elsa.Studio.Models; -using MudBlazor; namespace Elsa.Studio.Agents; /// A menu provider for the Agents module. -public class AgentsMenu(ILocalizer localizer) : IMenuProvider, IMenuGroupProvider +public class AgentsMenu(ILocalizer localizer) : IMenuProvider { /// public ValueTask> GetMenuItemsAsync(CancellationToken cancellationToken = default) @@ -19,41 +18,9 @@ public ValueTask> GetMenuItemsAsync(CancellationToken canc Href = "ai/agents", Text = localizer["Agents"], GroupName = MenuItemGroups.General.Name - }, - new() - { - Icon = AgentIcons.AI, - Text = localizer["Agents"], - GroupName = MenuItemGroups.Settings.Name, - SubMenuItems = - [ - new MenuItem - { - Icon = Icons.Material.Outlined.Key, - Href = "ai/api-keys", - Text = localizer["API Keys"] - }, - new MenuItem - { - Icon = Icons.Material.Outlined.MiscellaneousServices, - Href = "ai/services", - Text = localizer["Services"] - } - ] } }; - return new ValueTask>(menuItems); - } - - /// - public ValueTask> GetMenuGroupsAsync(CancellationToken cancellationToken = default) - { - var groups = new List - { - //new("agents", "Intelligent Agents", 10f) - }; - - return new ValueTask>(groups); + return new(menuItems); } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/IApiKeysApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/IApiKeysApi.cs deleted file mode 100644 index 33030dea..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/Client/IApiKeysApi.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Elsa.Agents; -using Elsa.Api.Client.Resources.WorkflowDefinitions.Responses; -using Elsa.Api.Client.Shared.Models; -using Refit; - -namespace Elsa.Studio.Agents.Client; - -/// Represents a client API for interacting with agents. -public interface IApiKeysApi -{ - /// Lists all API keys. - [Get("/ai/api-keys")] - Task> ListAsync(CancellationToken cancellationToken = default); - - /// Gets an API key by ID. - [Get("/ai/api-keys/{id}")] - Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// Creates a new API key. - [Post("/ai/api-keys")] - Task CreateAsync(ApiKeyInputModel request, CancellationToken cancellationToken = default); - - /// Updates an API key. - [Post("/ai/api-keys/{id}")] - Task UpdateAsync(string id, ApiKeyInputModel request, CancellationToken cancellationToken = default); - - /// Deletes an API key. - [Delete("/ai/api-keys/{id}")] - Task DeleteAsync(string id, CancellationToken cancellationToken = default); - - /// Deletes multiple API keys. - [Post("/ai/bulk-actions/api-keys/delete")] - Task BulkDeleteAsync(BulkDeleteRequest request, CancellationToken cancellationToken = default); - - /// Checks if a name is unique. - Task GetIsNameUniqueAsync(IsUniqueNameRequest request, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/IServiceProvidersApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/IServiceProvidersApi.cs deleted file mode 100644 index c39dc0fe..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/Client/IServiceProvidersApi.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Elsa.Api.Client.Shared.Models; -using Refit; - -namespace Elsa.Studio.Agents.Client; - -/// Represents a client API for retrieving available service providers. -public interface IServiceProvidersApi -{ - /// Lists all service providers. - [Get("/ai/service-providers")] - Task> ListAsync(CancellationToken cancellationToken = default); -} diff --git a/src/modules/agents/Elsa.Studio.Agents/Client/IServicesApi.cs b/src/modules/agents/Elsa.Studio.Agents/Client/IServicesApi.cs deleted file mode 100644 index 6545c26e..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/Client/IServicesApi.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Elsa.Agents; -using Elsa.Api.Client.Resources.WorkflowDefinitions.Responses; -using Elsa.Api.Client.Shared.Models; -using Refit; - -namespace Elsa.Studio.Agents.Client; - -/// Represents a client API for interacting with AI services. -public interface IServicesApi -{ - /// Lists all services. - [Get("/ai/services")] - Task> ListAsync(CancellationToken cancellationToken = default); - - /// Gets a service by ID. - [Get("/ai/services/{id}")] - Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// Creates a new service. - [Post("/ai/services")] - Task CreateAsync(ServiceInputModel request, CancellationToken cancellationToken = default); - - /// Updates a service. - [Post("/ai/services/{id}")] - Task UpdateAsync(string id, ServiceInputModel request, CancellationToken cancellationToken = default); - - /// Deletes a service. - [Delete("/ai/services/{id}")] - Task DeleteAsync(string id, CancellationToken cancellationToken = default); - - /// Deletes multiple services. - [Post("/ai/bulk-actions/services/delete")] - Task BulkDeleteAsync(BulkDeleteRequest request, CancellationToken cancellationToken = default); - - /// Checks if a name is unique. - Task GetIsNameUniqueAsync(IsUniqueNameRequest request, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs b/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs index 200f50c0..74b1eb4c 100644 --- a/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/agents/Elsa.Studio.Agents/Extensions/ServiceCollectionExtensions.cs @@ -20,10 +20,7 @@ public static IServiceCollection AddAgentsModule(this IServiceCollection service return services .AddScoped() .AddScoped() - .AddScoped() .AddRemoteApi(backendApiConfig) - .AddRemoteApi(backendApiConfig) - .AddRemoteApi(backendApiConfig) .AddRemoteApi(backendApiConfig) .AddActivityDisplaySettingsProvider() diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor index 917ac947..756a50da 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor @@ -8,18 +8,9 @@ - - - - - @foreach (var service in AvailableServices) - { - - } - - - + + @foreach (var plugin in AvailableSkills) { diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs index 943b8e43..3e081192 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Components/CreateAgentDialog.razor.cs @@ -25,8 +25,6 @@ public partial class CreateAgentDialog [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; [Inject] private IActivityRegistry ActivityRegistry { get; set; } = null!; [Inject] private IActivityDisplaySettingsRegistry ActivityDisplaySettingsRegistry { get; set; } = null!; - private ICollection AvailableServices { get; set; } = []; - private IReadOnlyCollection SelectedServices { get; set; } = []; private ICollection AvailableSkills { get; set; } = []; private IReadOnlyCollection SelectedSkills { get; set; } = []; @@ -36,20 +34,15 @@ protected override async Task OnInitializedAsync() _agentInputModel.Name = AgentName; _agentInputModel.PromptTemplate = "You are a helpful assistant."; _agentInputModel.Description = "A helpful assistant."; - _agentInputModel.FunctionName = "Reply"; _agentInputModel.OutputVariable.Type = "object"; _agentInputModel.OutputVariable.Description = "The output of the agent."; _agentInputModel.ExecutionSettings.ResponseFormat = "json_object"; _editContext = new(_agentInputModel); var agentsApi = await ApiClientProvider.GetApiAsync(); - var servicesApi = await ApiClientProvider.GetApiAsync(); var skillsApi = await ApiClientProvider.GetApiAsync(); _validator = new(agentsApi); - var servicesResponseList = await servicesApi.ListAsync(); var skillsResponseList = await skillsApi.ListAsync(); - AvailableServices = servicesResponseList.Items; AvailableSkills = skillsResponseList.Items; - SelectedServices = _agentInputModel.Services.ToList().AsReadOnly(); SelectedSkills = _agentInputModel.Skills.ToList().AsReadOnly(); } @@ -69,7 +62,6 @@ private async Task OnSubmitClicked() private Task OnValidSubmit() { - _agentInputModel.Services = SelectedServices.ToList(); _agentInputModel.Skills = SelectedSkills.ToList(); MudDialog.Close(_agentInputModel); ActivityRegistry.MarkStale(); diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor index 21075900..f8a5caeb 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor @@ -31,14 +31,7 @@ Label="@Localizer["Description"]" Variant="Variant.Outlined" HelperText="@Localizer["A description about the role and purpose of this agent."]"/> - - - + - - - - - @foreach (var service in AvailableServices) - { - - } - - - - - + - + - @foreach (var plugin in AvailableSkills) + @foreach (var skill in AvailableSkills) { - + } diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs index d1c2b2e1..71fda433 100644 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs +++ b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Agent.razor.cs @@ -21,9 +21,6 @@ private bool UseJsonResponse get => _agent.ExecutionSettings.ResponseFormat == "json_object"; set => _agent.ExecutionSettings.ResponseFormat = value ? "json_object" : "string"; } - - private ICollection AvailableServices { get; set; } = []; - private IReadOnlyCollection SelectedServices { get; set; } = []; private ICollection AvailableSkills { get; set; } = []; private IReadOnlyCollection SelectedSkills { get; set; } = []; @@ -33,18 +30,15 @@ private bool UseJsonResponse private AgentInputModelValidator _validator = null!; private AgentModel _agent = new(); private InputVariableConfig? _inputVariableBackup; - private MudTable _inputVariableTable; + private MudTable _inputVariableTable = null!; /// protected override async Task OnInitializedAsync() { var apiClient = await ApiClientProvider.GetApiAsync(); _validator = new(apiClient); - var servicesApi = await ApiClientProvider.GetApiAsync(); var skillsApi = await ApiClientProvider.GetApiAsync(); - var servicesResponseList = await servicesApi.ListAsync(); var skillsResponseList = await skillsApi.ListAsync(); - AvailableServices = servicesResponseList.Items; AvailableSkills = skillsResponseList.Items; } @@ -53,7 +47,6 @@ protected override async Task OnParametersSetAsync() { var apiClient = await ApiClientProvider.GetApiAsync(); _agent = await apiClient.GetAsync(AgentId); - SelectedServices = _agent.Services.ToList().AsReadOnly(); SelectedSkills = _agent.Skills.ToList().AsReadOnly(); } @@ -63,8 +56,7 @@ private async Task OnSaveClicked() if (!_form.IsValid) return; - - _agent.Services = SelectedServices.ToList(); + _agent.Skills = SelectedSkills.ToList(); var apiClient = await ApiClientProvider.GetApiAsync(); _agent = await apiClient.UpdateAsync(AgentId, _agent); diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor deleted file mode 100644 index a7c5d1a9..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor +++ /dev/null @@ -1,43 +0,0 @@ -@page "/ai/api-keys/{ApiKeyId}" -@using Elsa.Agents -@using Variant = MudBlazor.Variant -@inherits StudioComponentBase -@inject ILocalizer Localizer - -@Localizer["API Key"] - - - - - - - - - - - - - - - - - - - - @Localizer["Save"] - - - \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor.cs deleted file mode 100644 index a23d2fcc..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKey.razor.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using Elsa.Studio.Agents.UI.Validators; -using Elsa.Studio.Components; -using Elsa.Studio.Contracts; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Elsa.Studio.Agents.UI.Pages; - -public partial class ApiKey : StudioComponentBase -{ - /// The ID of the API key to edit. - [Parameter] public string ApiKeyId { get; set; } = null!; - - [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - [Inject] private NavigationManager NavigationManager { get; set; } = null!; - private bool IsNew => string.Equals("new", ApiKeyId, StringComparison.OrdinalIgnoreCase); - - private MudForm _form = null!; - private ApiKeyInputModelValidator _validator = null!; - private ApiKeyModel _apiKey = new(); - - /// - protected override async Task OnParametersSetAsync() - { - var apiClient = await ApiClientProvider.GetApiAsync(); - - if (IsNew) - _apiKey = new(); - else - _apiKey = await apiClient.GetAsync(ApiKeyId); - - _validator = new ApiKeyInputModelValidator(apiClient); - } - - private async Task OnSaveClicked() - { - await _form.Validate(); - - if (!_form.IsValid) - return; - - var apiClient = await ApiClientProvider.GetApiAsync(); - - if (IsNew) - { - _apiKey = await apiClient.CreateAsync(_apiKey); - Snackbar.Add("API key successfully created.", Severity.Success); - } - else - { - _apiKey = await apiClient.UpdateAsync(ApiKeyId, _apiKey); - Snackbar.Add("API key successfully updated.", Severity.Success); - } - - StateHasChanged(); - NavigationManager.NavigateTo("ai/api-keys"); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor deleted file mode 100644 index 54d33b52..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor +++ /dev/null @@ -1,68 +0,0 @@ -@page "/ai/api-keys" -@using Elsa.Agents -@using Variant = MudBlazor.Variant -@inherits StudioComponentBase -@inject ILocalizer Localizer - -@Localizer["API Keys"] - - - - - - - - Delete - - - - - @Localizer["Create API Key"] - - - - - ID - - - @Localizer["Name"] - - - @Localizer["Value"] - - - - - @context.Id - @context.Name - @context.Value - - - @Localizer["Edit"] - @Localizer["Delete"] - - - - - @Localizer["No API keys found"] - - - @Localizer["Loading"]... - - - - - - \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor.cs deleted file mode 100644 index 2416bf00..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/ApiKeys.razor.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using Elsa.Studio.Contracts; -using Elsa.Studio.DomInterop.Contracts; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Elsa.Studio.Agents.UI.Pages; - -[UsedImplicitly] -public partial class ApiKeys -{ - private MudTable _table = null!; - private HashSet _selectedRows = new(); - - [Inject] private IDialogService DialogService { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - [Inject] NavigationManager NavigationManager { get; set; } = null!; - [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; - [Inject] private IFiles Files { get; set; } = null!; - [Inject] private IDomAccessor DomAccessor { get; set; } = null!; - - private async Task GetApiClientAsync() - { - return await ApiClientProvider.GetApiAsync(); - } - - private async Task> ServerReload(TableState state, CancellationToken cancellationToken) - { - var apiClient = await GetApiClientAsync(); - var response = await apiClient.ListAsync(cancellationToken); - - return new TableData - { - TotalItems = (int)response.Count, - Items = response.Items - }; - } - - private async Task OnCreateClicked() - { - await InvokeAsync(() => NavigationManager.NavigateTo($"ai/api-keys/new")); - } - - private async Task EditAsync(string id) - { - await InvokeAsync(() => NavigationManager.NavigateTo($"ai/api-keys/{id}")); - } - - private void Reload() - { - _table.ReloadServerData(); - } - - private async Task OnEditClicked(string id) - { - await EditAsync(id); - } - - private async Task OnRowClick(TableRowClickEventArgs e) - { - await EditAsync(e.Item.Id); - } - - private async Task OnDeleteClicked(ApiKeyModel model) - { - var result = await DialogService.ShowMessageBox("Delete API Key?", "Are you sure you want to delete this API key?", yesText: "Delete", cancelText: "Cancel"); - - if (result != true) - return; - - var id = model.Id; - var apiClient = await GetApiClientAsync(); - await apiClient.DeleteAsync(id); - Reload(); - } - - private async Task OnBulkDeleteClicked() - { - var result = await DialogService.ShowMessageBox("Delete Selected API keys?", "Are you sure you want to delete the selected API keys?", yesText: "Delete", cancelText: "Cancel"); - - if (result != true) - return; - - var ids = _selectedRows.Select(x => x.Id).ToList(); - var request = new BulkDeleteRequest { Ids = ids }; - var apiClient = await GetApiClientAsync(); - await apiClient.BulkDeleteAsync(request); - Reload(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor deleted file mode 100644 index d8462c19..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor +++ /dev/null @@ -1,57 +0,0 @@ -@page "/ai/services/{ServiceId}" -@using Elsa.Agents -@using Variant = MudBlazor.Variant -@inherits StudioComponentBase -@inject ILocalizer Localizer - -@Localizer["Service"] - - - - - - - - - - - - - - @foreach (var provider in _serviceProviders) - { - @Localizer[provider] - } - - - - - - - - - - @Localizer["Save"] - - - \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor.cs deleted file mode 100644 index 49e3a91a..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Service.razor.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Text.Json; -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using Elsa.Studio.Agents.UI.Validators; -using Elsa.Studio.Components; -using Elsa.Studio.Contracts; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Elsa.Studio.Agents.UI.Pages; - -public partial class Service : StudioComponentBase -{ - /// The ID of the service to edit. - [Parameter] public string ServiceId { get; set; } = null!; - - [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - [Inject] private NavigationManager NavigationManager { get; set; } = null!; - private bool IsNew => string.Equals("new", ServiceId, StringComparison.OrdinalIgnoreCase); - - private MudForm _form = null!; - private ServiceInputModelValidator _validator = null!; - private ServiceModel _entity = new(); - private ICollection _serviceProviders = []; - - /// - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - var providersApi = await ApiClientProvider.GetApiAsync(); - var response = await providersApi.ListAsync(); - _serviceProviders = response.Items; - } - - /// - protected override async Task OnParametersSetAsync() - { - var apiClient = await ApiClientProvider.GetApiAsync(); - - if (IsNew) - _entity = new(); - else - _entity = await apiClient.GetAsync(ServiceId); - - _validator = new ServiceInputModelValidator(apiClient); - } - - private async Task OnSaveClicked() - { - await _form.Validate(); - - if (!_form.IsValid) - return; - - var apiClient = await ApiClientProvider.GetApiAsync(); - - if (IsNew) - { - _entity = await apiClient.CreateAsync(_entity); - Snackbar.Add("Service successfully created.", Severity.Success); - } - else - { - _entity = await apiClient.UpdateAsync(ServiceId, _entity); - Snackbar.Add("Service successfully updated.", Severity.Success); - } - - StateHasChanged(); - NavigationManager.NavigateTo("ai/services"); - } - - private MudBlazor.Converter, string> GetSettingsConverter() - { - return new MudBlazor.Converter, string> - { - SetFunc = x => x == null ? "{}" : JsonSerializer.Serialize(x, new JsonSerializerOptions { WriteIndented = true }), - GetFunc = x => JsonSerializer.Deserialize>(x) ?? new Dictionary() - }; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor deleted file mode 100644 index da423c91..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor +++ /dev/null @@ -1,73 +0,0 @@ -@page "/ai/services" -@using System.Text.Json -@using Elsa.Agents -@using Variant = MudBlazor.Variant -@inherits StudioComponentBase -@inject ILocalizer Localizer - -@Localizer["Services"] - - - - - - - - @Localizer["Delete"] - - - - - @Localizer["Create Service"] - - - - - ID - - - @Localizer["Name"] - - - @Localizer["Settings"] - - - - - @context.Id - @context.Name - - - @JsonSerializer.Serialize(context.Settings, JsonSerializerOptions.Default) - - - - - @Localizer["Edit"] - @Localizer["Delete"] - - - - - @Localizer["No services found"] - - - @Localizer["Loading"]... - - - - - - \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor.cs deleted file mode 100644 index 3d40ad74..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Pages/Services.razor.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using Elsa.Studio.Contracts; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Elsa.Studio.Agents.UI.Pages; - -[UsedImplicitly] -public partial class Services -{ - private MudTable _table = null!; - private HashSet _selectedRows = new(); - - [Inject] private IDialogService DialogService { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - [Inject] NavigationManager NavigationManager { get; set; } = null!; - [Inject] private IBackendApiClientProvider ApiClientProvider { get; set; } = null!; - - private async Task GetApiClientAsync() - { - return await ApiClientProvider.GetApiAsync(); - } - - private async Task> ServerReload(TableState state, CancellationToken cancellationToken) - { - var apiClient = await GetApiClientAsync(); - var response = await apiClient.ListAsync(cancellationToken); - - return new TableData - { - TotalItems = (int)response.Count, - Items = response.Items - }; - } - - private async Task OnCreateClicked() - { - await InvokeAsync(() => NavigationManager.NavigateTo($"ai/services/new")); - } - - private async Task EditAsync(string id) - { - await InvokeAsync(() => NavigationManager.NavigateTo($"ai/services/{id}")); - } - - private void Reload() - { - _table.ReloadServerData(); - } - - private async Task OnEditClicked(string id) - { - await EditAsync(id); - } - - private async Task OnRowClick(TableRowClickEventArgs e) - { - await EditAsync(e.Item.Id); - } - - private async Task OnDeleteClicked(ServiceModel model) - { - var result = await DialogService.ShowMessageBox("Delete Service?", "Are you sure you want to delete this service?", yesText: "Delete", cancelText: "Cancel"); - - if (result != true) - return; - - var id = model.Id; - var apiClient = await GetApiClientAsync(); - await apiClient.DeleteAsync(id); - Reload(); - } - - private async Task OnBulkDeleteClicked() - { - var result = await DialogService.ShowMessageBox("Delete Selected services?", "Are you sure you want to delete the selected services?", yesText: "Delete", cancelText: "Cancel"); - - if (result != true) - return; - - var ids = _selectedRows.Select(x => x.Id).ToList(); - var request = new BulkDeleteRequest { Ids = ids }; - var apiClient = await GetApiClientAsync(); - await apiClient.BulkDeleteAsync(request); - Reload(); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ApiKeyInputModelValidator.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ApiKeyInputModelValidator.cs deleted file mode 100644 index 02bd50af..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ApiKeyInputModelValidator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using FluentValidation; - -namespace Elsa.Studio.Agents.UI.Validators; - -/// -/// A validator for instances. -/// -public class ApiKeyInputModelValidator : AbstractValidator -{ - /// - public ApiKeyInputModelValidator(IApiKeysApi apiKeysApi) - { - RuleFor(x => x.Name).NotEmpty().WithMessage("Please enter a name for the API key."); - - RuleFor(x => x.Name) - .MustAsync(async (context, name, cancellationToken) => - { - var request = new IsUniqueNameRequest - { - Name = name!, - }; - var response = await apiKeysApi.GetIsNameUniqueAsync(request, cancellationToken); - return response.IsUnique; - }) - .WithMessage("An API key with this name already exists."); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ServiceInputModelValidator.cs b/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ServiceInputModelValidator.cs deleted file mode 100644 index 8f46438d..00000000 --- a/src/modules/agents/Elsa.Studio.Agents/UI/Validators/ServiceInputModelValidator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Elsa.Agents; -using Elsa.Studio.Agents.Client; -using FluentValidation; - -namespace Elsa.Studio.Agents.UI.Validators; - -/// -/// A validator for instances. -/// -public class ServiceInputModelValidator : AbstractValidator -{ - /// - public ServiceInputModelValidator(IServicesApi api) - { - RuleFor(x => x.Name).NotEmpty().WithMessage("Please enter a name for the service."); - - RuleFor(x => x.Name) - .MustAsync(async (context, name, cancellationToken) => - { - var request = new IsUniqueNameRequest - { - Name = name!, - }; - var response = await api.GetIsNameUniqueAsync(request, cancellationToken); - return response.IsUnique; - }) - .WithMessage("A service with this name already exists."); - } -} \ No newline at end of file From 89f3e1bb6a25f3e6be195c7848e551967a69f2ad Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 13 Dec 2025 21:02:57 +0100 Subject: [PATCH 16/23] Remove ApiKeyDefinition and ServiceDefinition tables. This commit adds migrations to drop the ApiKeyDefinition and ServiceDefinition tables for SQLite, MySQL, PostgreSQL, and SQL Server. It also updates `ModelSnapshot` files and adds EF Core migration scripts for the respective databases to reflect this schema change. Additionally, the solution file was updated to include new EF Core projects. --- .../20251213194317_V3_6.Designer.cs | 61 +++++++++++ .../Migrations/20251213194317_V3_6.cs | 100 ++++++++++++++++++ .../AgentsDbContextModelSnapshot.cs | 60 +---------- .../efcore-3.6.sh | 1 + .../20251213195607_V3_6.Designer.cs | 61 +++++++++++ .../Migrations/20251213195607_V3_6.cs | 89 ++++++++++++++++ .../AgentsDbContextModelSnapshot.cs | 60 +---------- .../efcore-3.6.sh | 1 + .../20251213200214_V3_6.Designer.cs | 61 +++++++++++ .../Migrations/20251213200214_V3_6.cs | 89 ++++++++++++++++ .../AgentsDbContextModelSnapshot.cs | 60 +---------- .../efcore-3.6.sh | 1 + ...sa.Agents.Persistence.EFCore.Sqlite.csproj | 1 - .../20251213193144_V3_6.Designer.cs | 57 ++++++++++ .../Migrations/20251213193144_V3_6.cs | 89 ++++++++++++++++ .../AgentsDbContextModelSnapshot.cs | 60 +---------- .../efcore-3.6.sh | 3 +- 17 files changed, 615 insertions(+), 239 deletions(-) create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.Designer.cs create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.cs create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/efcore-3.6.sh create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.Designer.cs create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.cs create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/efcore-3.6.sh create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.Designer.cs create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.cs create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/efcore-3.6.sh create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.Designer.cs create mode 100644 src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.cs diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.Designer.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.Designer.cs new file mode 100644 index 00000000..8b14cbee --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.Designer.cs @@ -0,0 +1,61 @@ +// +using Elsa.Agents.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.MySql.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20251213194317_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AgentConfig") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_AgentDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_AgentDefinition_TenantId"); + + b.ToTable("AgentDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.cs new file mode 100644 index 00000000..454b9936 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/20251213194317_V3_6.cs @@ -0,0 +1,100 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.MySql.Migrations +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Value = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeysDefinitions", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ServicesDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Settings = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Type = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_Name", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_TenantId", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_Name", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_TenantId", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "TenantId"); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs index 3f3f3b03..a38076e9 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/Migrations/AgentsDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -52,64 +52,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentDefinitions", "Elsa"); }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ApiKeyDefinition", b => - { - b.Property("Id") - .HasColumnType("varchar(255)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("varchar(255)"); - - b.Property("TenantId") - .HasColumnType("varchar(255)"); - - b.Property("Value") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ApiKeyDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ApiKeyDefinition_TenantId"); - - b.ToTable("ApiKeysDefinitions", "Elsa"); - }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ServiceDefinition", b => - { - b.Property("Id") - .HasColumnType("varchar(255)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("varchar(255)"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("TenantId") - .HasColumnType("varchar(255)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ServiceDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ServiceDefinition_TenantId"); - - b.ToTable("ServicesDefinitions", "Elsa"); - }); #pragma warning restore 612, 618 } } diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/efcore-3.6.sh new file mode 100644 index 00000000..16d773fe --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.MySql/efcore-3.6.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.Designer.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.Designer.cs new file mode 100644 index 00000000..a646c5b9 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.Designer.cs @@ -0,0 +1,61 @@ +// +using Elsa.Agents.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.PostgreSql.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20251213195607_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AgentConfig") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_AgentDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_AgentDefinition_TenantId"); + + b.ToTable("AgentDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.cs new file mode 100644 index 00000000..4fc4b8ad --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/20251213195607_V3_6.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.PostgreSql.Migrations +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true), + Value = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeysDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServicesDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Settings = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true), + Type = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_Name", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_TenantId", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_Name", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_TenantId", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "TenantId"); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs index e905428b..4f944deb 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/Migrations/AgentsDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -52,64 +52,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentDefinitions", "Elsa"); }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ApiKeyDefinition", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("text"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ApiKeyDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ApiKeyDefinition_TenantId"); - - b.ToTable("ApiKeysDefinitions", "Elsa"); - }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ServiceDefinition", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ServiceDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ServiceDefinition_TenantId"); - - b.ToTable("ServicesDefinitions", "Elsa"); - }); #pragma warning restore 612, 618 } } diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/efcore-3.6.sh new file mode 100644 index 00000000..16d773fe --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.PostgreSql/efcore-3.6.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.Designer.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.Designer.cs new file mode 100644 index 00000000..30646b43 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.Designer.cs @@ -0,0 +1,61 @@ +// +using Elsa.Agents.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.SqlServer.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20251213200214_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AgentConfig") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_AgentDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_AgentDefinition_TenantId"); + + b.ToTable("AgentDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.cs new file mode 100644 index 00000000..cb921966 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/20251213200214_V3_6.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.SqlServer.Migrations +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true), + Value = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeysDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServicesDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Settings = table.Column(type: "nvarchar(max)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true), + Type = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_Name", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_TenantId", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_Name", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_TenantId", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "TenantId"); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs index cd735a6e..74560fad 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/Migrations/AgentsDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -52,64 +52,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentDefinitions", "Elsa"); }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ApiKeyDefinition", b => - { - b.Property("Id") - .HasColumnType("nvarchar(450)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("TenantId") - .HasColumnType("nvarchar(450)"); - - b.Property("Value") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ApiKeyDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ApiKeyDefinition_TenantId"); - - b.ToTable("ApiKeysDefinitions", "Elsa"); - }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ServiceDefinition", b => - { - b.Property("Id") - .HasColumnType("nvarchar(450)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("TenantId") - .HasColumnType("nvarchar(450)"); - - b.Property("Type") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ServiceDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ServiceDefinition_TenantId"); - - b.ToTable("ServicesDefinitions", "Elsa"); - }); #pragma warning restore 612, 618 } } diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/efcore-3.6.sh new file mode 100644 index 00000000..16d773fe --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.SqlServer/efcore-3.6.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Elsa.Agents.Persistence.EFCore.Sqlite.csproj b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Elsa.Agents.Persistence.EFCore.Sqlite.csproj index e359f9e0..98750688 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Elsa.Agents.Persistence.EFCore.Sqlite.csproj +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Elsa.Agents.Persistence.EFCore.Sqlite.csproj @@ -1,7 +1,6 @@  - net8.0;net9.0;net10.0 Provides an EF Core migrations for SQLite for the Agents Persistence module. elsa extension module agents semantic kernel llm ai persistence efcore entity framework core sqlite diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.Designer.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.Designer.cs new file mode 100644 index 00000000..4632bad6 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.Designer.cs @@ -0,0 +1,57 @@ +// +using Elsa.Agents.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.Sqlite.Migrations +{ + [DbContext(typeof(AgentsDbContext))] + [Migration("20251213193144_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AgentConfig") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_AgentDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_AgentDefinition_TenantId"); + + b.ToTable("AgentDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.cs new file mode 100644 index 00000000..7f2eb029 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/20251213193144_V3_6.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Agents.Persistence.EFCore.Sqlite.Migrations +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "ServicesDefinitions", + schema: _schema.Schema); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeysDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true), + Value = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeysDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServicesDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Settings = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ServicesDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_Name", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeyDefinition_TenantId", + schema: _schema.Schema, + table: "ApiKeysDefinitions", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_Name", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceDefinition_TenantId", + schema: _schema.Schema, + table: "ServicesDefinitions", + column: "TenantId"); + } + } +} diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs index 6c9fb727..58243d29 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/Migrations/AgentsDbContextModelSnapshot.cs @@ -16,7 +16,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.13"); + .HasAnnotation("ProductVersion", "9.0.11"); modelBuilder.Entity("Elsa.Agents.Persistence.Entities.AgentDefinition", b => { @@ -48,64 +48,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentDefinitions", "Elsa"); }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ApiKeyDefinition", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("TenantId") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ApiKeyDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ApiKeyDefinition_TenantId"); - - b.ToTable("ApiKeysDefinitions", "Elsa"); - }); - - modelBuilder.Entity("Elsa.Agents.Persistence.Entities.ServiceDefinition", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("TenantId") - .HasColumnType("TEXT"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .HasDatabaseName("IX_ServiceDefinition_Name"); - - b.HasIndex("TenantId") - .HasDatabaseName("IX_ServiceDefinition_TenantId"); - - b.ToTable("ServicesDefinitions", "Elsa"); - }); #pragma warning restore 612, 618 } } diff --git a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh index b6b4bddc..16d773fe 100644 --- a/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh +++ b/src/modules/agents/Elsa.Agents.Persistence.EFCore.Sqlite/efcore-3.6.sh @@ -1,2 +1 @@ -#ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" -ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -p ./ -o Migrations --startup-project ./Elsa.Agents.Persistence.EFCore.Sqlite.csproj" \ No newline at end of file +ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c AgentsDbContext -o Migrations" \ No newline at end of file From add31db14ed4380e4a32ce2baef69e3551c508cb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 13 Dec 2025 21:31:32 +0100 Subject: [PATCH 17/23] Add Azure OpenAI integration to Elsa Agents Introduced a new module `Elsa.Agents.AzureOpenAI` for integrating Azure OpenAI with Elsa Agents. The module supports Azure OpenAI chat completions and is added to the main solution and dependencies. Adjusted related files to accommodate this new feature. --- Directory.Packages.props | 1 + .../AgentsFeatureExtensions.cs | 23 +++++++++++++++ .../Elsa.Agents.AzureOpenAI.csproj | 22 +++++++++++++++ .../Elsa.Agents.AzureOpenAI/FodyWeavers.xml | 3 ++ .../Elsa.Agents.Core/Services/AgentInvoker.cs | 28 +++++++++++-------- .../AgentsFeatureExtensions.cs | 20 +++++++++++++ .../agents/Elsa.Agents.OpenAI/Class1.cs | 5 ---- .../Elsa.Agents.OpenAI.csproj | 4 +-- 8 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 src/modules/agents/Elsa.Agents.AzureOpenAI/AgentsFeatureExtensions.cs create mode 100644 src/modules/agents/Elsa.Agents.AzureOpenAI/Elsa.Agents.AzureOpenAI.csproj create mode 100644 src/modules/agents/Elsa.Agents.AzureOpenAI/FodyWeavers.xml create mode 100644 src/modules/agents/Elsa.Agents.OpenAI/AgentsFeatureExtensions.cs delete mode 100644 src/modules/agents/Elsa.Agents.OpenAI/Class1.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 030cc2d4..bd9a5f9a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -175,6 +175,7 @@ + diff --git a/src/modules/agents/Elsa.Agents.AzureOpenAI/AgentsFeatureExtensions.cs b/src/modules/agents/Elsa.Agents.AzureOpenAI/AgentsFeatureExtensions.cs new file mode 100644 index 00000000..fbabf207 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.AzureOpenAI/AgentsFeatureExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.SemanticKernel; + +namespace Elsa.Agents.AzureOpenAI; + +public static class AgentsFeatureExtensions +{ + public static AgentsFeature AddAzureOpenAIChatCompletion(this AgentsFeature feature, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + string? apiVersion = null, + HttpClient? httpClient = null, + string name = "Azure OpenAI Chat Completion") + { + return feature.AddServiceDescriptor(new() + { + Name = name, + ConfigureKernel = kernel => kernel.Services.AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey, serviceId, modelId, apiVersion, httpClient) + }); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.AzureOpenAI/Elsa.Agents.AzureOpenAI.csproj b/src/modules/agents/Elsa.Agents.AzureOpenAI/Elsa.Agents.AzureOpenAI.csproj new file mode 100644 index 00000000..9424b0f5 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.AzureOpenAI/Elsa.Agents.AzureOpenAI.csproj @@ -0,0 +1,22 @@ + + + + Provides Azure OpenAI integration with Elsa Agents + elsa extension module agents azure openai + + + + + + + + + + + + + + + + + diff --git a/src/modules/agents/Elsa.Agents.AzureOpenAI/FodyWeavers.xml b/src/modules/agents/Elsa.Agents.AzureOpenAI/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.AzureOpenAI/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs index 8081b17f..d06cabde 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs @@ -58,19 +58,23 @@ public async Task InvokeAgentAsync(string agentName, IDiction var renderedPrompt = await promptTemplate.RenderAsync(agent.Kernel, kernelArguments, cancellationToken); chatHistory.AddUserMessage(renderedPrompt); - chatHistory.AddSystemMessage( - """" - You are a function that returns *only* JSON. - Rules: - - Return a single valid JSON object. - - Do not add explanations. - - Do not add code fences. - - Do not prefix with ```json or any other markers. - - Output must start with { and end with }. - - If there's a problem with the JSON input, include the exact JSON input in your response for troubleshooting. - """"); + if (executionSettings.ResponseFormat == "json_object") + { + chatHistory.AddSystemMessage( + """" + You are a function that returns *only* JSON. + + Rules: + - Return a single valid JSON object. + - Do not add explanations. + - Do not add code fences. + - Do not prefix with ```json or any other markers. + - Output must start with { and end with }. + + If there's a problem with the JSON input, include the exact JSON input in your response for troubleshooting. + """"); + } // Get response from agent var response = await agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken).LastOrDefaultAsync(cancellationToken); diff --git a/src/modules/agents/Elsa.Agents.OpenAI/AgentsFeatureExtensions.cs b/src/modules/agents/Elsa.Agents.OpenAI/AgentsFeatureExtensions.cs new file mode 100644 index 00000000..a82896f0 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.OpenAI/AgentsFeatureExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.SemanticKernel; + +namespace Elsa.Agents.OpenAI; + +public static class AgentsFeatureExtensions +{ + public static AgentsFeature AddOpenAIChatCompletion(this AgentsFeature feature, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + string name = "OpenAI Chat Completion") + { + return feature.AddServiceDescriptor(new() + { + Name = name, + ConfigureKernel = kernel => kernel.Services.AddOpenAIChatCompletion(modelId, apiKey, orgId, serviceId) + }); + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.OpenAI/Class1.cs b/src/modules/agents/Elsa.Agents.OpenAI/Class1.cs deleted file mode 100644 index 46ac11cc..00000000 --- a/src/modules/agents/Elsa.Agents.OpenAI/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Elsa.Agents.OpenAI; - -public class Class1 -{ -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj b/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj index 2cbd22bf..6ab1a67d 100644 --- a/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj +++ b/src/modules/agents/Elsa.Agents.OpenAI/Elsa.Agents.OpenAI.csproj @@ -11,12 +11,12 @@ - + - + From b4d7a2456e558b93dc0a294c9d3cbbbba0cf1a5b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Dec 2025 13:22:16 +0100 Subject: [PATCH 18/23] Refactor agent activity providers and registry to improve naming consistency. Renamed `AgentActivityProvider` to `ConfigurationAgentActivityProvider` and updated references across activities, features, and handlers to reflect the new naming. --- .../Activities/CodeFirstAgentActivity.cs | 2 +- .../Activities/ConfiguredAgentActivity.cs | 2 +- .../ActivityProviders/ConfiguredAgentActivityProvider.cs | 3 +-- .../Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs | 2 +- .../Handlers/RefreshActivityRegistry.cs | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs index 2be2a091..6915417b 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs @@ -11,7 +11,7 @@ namespace Elsa.Agents.Activities; /// -/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . +/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . /// [Browsable(false)] public class CodeFirstAgentActivity : CodeActivity diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs index 8c4e208d..61a7943c 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs @@ -15,7 +15,7 @@ namespace Elsa.Agents.Activities; /// -/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . +/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . /// [Browsable(false)] public class ConfiguredAgentActivity : CodeActivity diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs index d401bb0c..b0a26161 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs @@ -1,4 +1,3 @@ -using Elsa.Agents; using Elsa.Expressions.Contracts; using Elsa.Expressions.Extensions; using Elsa.Extensions; @@ -13,7 +12,7 @@ namespace Elsa.Agents.Activities.ActivityProviders; /// Provides activities for each function of registered agents. /// [UsedImplicitly] -public class AgentActivityProvider( +public class ConfigurationAgentActivityProvider( IKernelConfigProvider kernelConfigProvider, IActivityDescriber activityDescriber, IWellKnownTypeRegistry wellKnownTypeRegistry diff --git a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs index 41614c13..046e72c4 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs @@ -22,7 +22,7 @@ public class AgentActivitiesFeature(IModule module) : FeatureBase(module) public override void Apply() { Services - .AddActivityProvider() + .AddActivityProvider() .AddActivityProvider() .AddNotificationHandler() ; diff --git a/src/modules/agents/Elsa.Agents.Activities/Handlers/RefreshActivityRegistry.cs b/src/modules/agents/Elsa.Agents.Activities/Handlers/RefreshActivityRegistry.cs index 6cc0bcf2..8db3830f 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Handlers/RefreshActivityRegistry.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Handlers/RefreshActivityRegistry.cs @@ -7,7 +7,7 @@ namespace Elsa.Agents.Activities.Handlers; [UsedImplicitly] -public class RefreshActivityRegistry(IActivityRegistry activityRegistry, AgentActivityProvider agentActivityProvider) : +public class RefreshActivityRegistry(IActivityRegistry activityRegistry, ConfigurationAgentActivityProvider configurationAgentActivityProvider) : INotificationHandler, INotificationHandler, INotificationHandler, @@ -17,5 +17,5 @@ public class RefreshActivityRegistry(IActivityRegistry activityRegistry, AgentAc public Task HandleAsync(AgentDefinitionUpdated notification, CancellationToken cancellationToken) => RefreshRegistryAsync(cancellationToken); public Task HandleAsync(AgentDefinitionDeleted notification, CancellationToken cancellationToken) => RefreshRegistryAsync(cancellationToken); public Task HandleAsync(AgentDefinitionsDeletedInBulk notification, CancellationToken cancellationToken) => RefreshRegistryAsync(cancellationToken); - private Task RefreshRegistryAsync(CancellationToken cancellationToken) => activityRegistry.RefreshDescriptorsAsync(agentActivityProvider, cancellationToken); + private Task RefreshRegistryAsync(CancellationToken cancellationToken) => activityRegistry.RefreshDescriptorsAsync(configurationAgentActivityProvider, cancellationToken); } \ No newline at end of file From fe8d3a1d15d7eec6db18c1ae1282ae1886e263e3 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Dec 2025 13:26:08 +0100 Subject: [PATCH 19/23] Improve naming consistency in documentation for agent activity providers. --- .../ActivityProviders/CodeFirstAgentActivityProvider.cs | 2 +- .../ActivityProviders/ConfiguredAgentActivityProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs index 4141a855..6b441a35 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs @@ -14,7 +14,7 @@ namespace Elsa.Agents.Activities.ActivityProviders; /// /// Provides activities for each code-first agent registered via . /// Inputs are derived from public properties on the agent type using simple -/// reflection rules. Execution is delegated to +/// reflection rules. Execution is delegated to /// via the common abstraction. /// [UsedImplicitly] diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs index b0a26161..d911f8dd 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs @@ -9,7 +9,7 @@ namespace Elsa.Agents.Activities.ActivityProviders; /// -/// Provides activities for each function of registered agents. +/// Provides activities for each registered agent. /// [UsedImplicitly] public class ConfigurationAgentActivityProvider( From b8521cf1f5c6d32c58aaa60116825d0c39a0d578 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Dec 2025 13:44:05 +0100 Subject: [PATCH 20/23] Refactor runAsynchronously logic and remove TaskActivityAttribute Replaced `TaskActivityAttribute` with `RunAsynchronously` property in `ActivityDescriptor`, simplifying activity configurations. Updated references across code to adopt this new approach, ensuring consistent behavior for asynchronous task activities. Minor changes to naming conventions and style settings were also included. --- .../ActivityProviders/CodeFirstAgentActivityProvider.cs | 4 +++- .../ActivityProviders/ConfiguredAgentActivityProvider.cs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs index 6b441a35..bd418cff 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs @@ -46,8 +46,9 @@ private async Task CreateDescriptorForAgentAsync(string key, descriptor.Name = key.Pascalize(); descriptor.TypeName = activityTypeName; descriptor.DisplayName = key.Humanize().Transform(To.TitleCase); - descriptor.Category = "Code-First Agents"; + descriptor.Category = "Agents"; descriptor.Kind = ActivityKind.Task; + descriptor.RunAsynchronously = true; descriptor.IsBrowsable = true; descriptor.ClrType = typeof(CodeFirstAgentActivity); @@ -56,6 +57,7 @@ private async Task CreateDescriptorForAgentAsync(string key, var activity = context.CreateActivity(); activity.Type = activityTypeName; activity.AgentName = key; + activity.RunAsynchronously = true; return activity; }; diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs index d911f8dd..d79b9681 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs @@ -46,6 +46,7 @@ private async Task CreateAgentActivityDescriptor(AgentConfig activityDescriptor.IsBrowsable = true; activityDescriptor.Category = "Agents"; activityDescriptor.Kind = ActivityKind.Task; + activityDescriptor.RunAsynchronously = true; activityDescriptor.ClrType = typeof(ConfiguredAgentActivity); activityDescriptor.Constructor = context => @@ -53,6 +54,7 @@ private async Task CreateAgentActivityDescriptor(AgentConfig var activity = context.CreateActivity(); activity.Type = activityTypeName; activity.AgentName = agentConfig.Name; + activity.RunAsynchronously = true; return activity; }; From 5db7bfb81fab58ab9fb8c8b4052466b3b6cf5e0a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Dec 2025 13:51:09 +0100 Subject: [PATCH 21/23] Introduce `IAgentFactory` and `IAgentInvoker` interfaces to decouple agent creation and invocation logic. Replaced concrete `AgentFactory` and `AgentInvoker` usage with their interface equivalents throughout the codebase. Updated DI registrations, refactored dependent classes, and improved modularity for better maintainability and flexibility. --- .../Activities/ConfiguredAgentActivity.cs | 2 +- .../Endpoints/Agents/Invoke/Endpoint.cs | 2 +- .../Contracts/IAgentFactory.cs | 18 ++++++++++++++++++ .../Contracts/IAgentInvoker.cs | 12 ++++++++++++ .../Features/AgentsCoreFeature.cs | 4 ++-- .../Elsa.Agents.Core/Services/AgentFactory.cs | 2 +- .../Elsa.Agents.Core/Services/AgentInvoker.cs | 2 +- 7 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs index 61a7943c..f012c406 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs @@ -49,7 +49,7 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context functionInput[inputDescriptor.Name] = inputValue; } - var agentInvoker = context.GetRequiredService(); + var agentInvoker = context.GetRequiredService(); var agentExecutionResponse = await agentInvoker.InvokeAgentAsync(AgentName, functionInput, context.CancellationToken); var responseText = StripCodeFences(agentExecutionResponse.ChatMessageContent.Content!); var isJsonResponse = IsJsonResponse(responseText); diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs index 6e9c59e7..685e56bf 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs @@ -8,7 +8,7 @@ namespace Elsa.Agents.Api.Endpoints.Agents.Invoke; /// Invokes an agent. /// [UsedImplicitly] -public class Execute(AgentInvoker agentInvoker) : ElsaEndpoint +public class Execute(IAgentInvoker agentInvoker) : ElsaEndpoint { /// public override void Configure() diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs new file mode 100644 index 00000000..1e1782fb --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.SemanticKernel.Agents; + +#pragma warning disable SKEXP0001 +#pragma warning disable SKEXP0010 +#pragma warning disable SKEXP0110 + +namespace Elsa.Agents; + +/// +/// Factory for creating Agent Framework agents from Elsa agent configurations. +/// +public interface IAgentFactory +{ + /// + /// Creates a ChatCompletionAgent from an Elsa agent configuration. + /// + ChatCompletionAgent CreateAgent(AgentConfig agentConfig); +} diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs new file mode 100644 index 00000000..12a0a734 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs @@ -0,0 +1,12 @@ +namespace Elsa.Agents; + +/// +/// Invokes an agent using the Microsoft Agent Framework. +/// +public interface IAgentInvoker +{ + /// + /// Invokes an agent using the Microsoft Agent Framework. + /// + Task InvokeAgentAsync(string agentName, IDictionary input, CancellationToken cancellationToken = default); +} diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs index b88ed2eb..b0616af1 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs @@ -28,8 +28,8 @@ public override void Apply() Services.AddOptions(); Services - .AddScoped() - .AddScoped() + .AddScoped() + .AddScoped() .AddScoped() .AddScoped(_kernelConfigProviderFactory) .AddScoped() diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs index 26f39b6a..8dad3724 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs @@ -17,7 +17,7 @@ public class AgentFactory( ISkillDiscoverer skillDiscoverer, IServiceProvider serviceProvider, IOptions options, - ILogger logger) + ILogger logger) : IAgentFactory { /// /// Creates a ChatCompletionAgent from an Elsa agent configuration. diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs index d06cabde..08830750 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs @@ -9,7 +9,7 @@ namespace Elsa.Agents; -public class AgentInvoker(IKernelConfigProvider kernelConfigProvider, AgentFactory agentFactory) +public class AgentInvoker(IKernelConfigProvider kernelConfigProvider, IAgentFactory agentFactory) : IAgentInvoker { /// /// Invokes an agent using the Microsoft Agent Framework (new approach). From 4725ce543253f5644de9642103454a18dacf07a5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Dec 2025 15:10:40 +0100 Subject: [PATCH 22/23] Refactor agents framework to unify configuration and improve invocation logic. Replaced multiple agent configuration options with a single `AgentOptions` class to simplify and standardize configuration management. Introduced `InvokeAgentRequest` model and updated `IAgentInvoker` to improve invocation logic. Removed obsolete types, enhanced modularity, and aligned implementation with cleaner abstractions. --- .../Activities/ConfiguredAgentActivity.cs | 8 +++- .../CodeFirstAgentActivityProvider.cs | 4 +- .../Endpoints/Agents/Invoke/Endpoint.cs | 8 +++- .../Elsa.Agents.Core/Contracts/IAgent.cs | 4 +- .../Contracts/IAgentExecutionResponse.cs | 6 --- .../Contracts/IAgentFactory.cs | 2 +- .../Contracts/IAgentInvoker.cs | 2 +- .../Contracts/IAgentResolver.cs | 9 ++++ .../Contracts/ServiceDescriptor.cs | 23 +++++++++ .../Features/AgentsCoreFeature.cs | 4 +- .../Models/AgentExecutionResponse.cs | 6 --- .../Models/InvokeAgentRequest.cs | 29 +++++++++++ .../Elsa.Agents.Core/Options/AgentOptions.cs | 23 +++++++++ .../Options/CodeFirstAgentOptions.cs | 24 ---------- .../Options/ConfiguredAgentOptions.cs | 7 --- .../Elsa.Agents.Core/Services/AgentFactory.cs | 10 ++-- .../Elsa.Agents.Core/Services/AgentInvoker.cs | 16 +++---- .../Services/AgentResolver.cs | 4 +- .../ConfigurationKernelConfigProvider.cs | 2 +- .../Configs/FunctionConfig.cs | 7 --- .../Configs/SelectionStrategyConfig.cs | 48 ------------------- .../Configs/TerminationConfig.cs | 48 ------------------- .../agents/Elsa.Agents/AgentsFeature.cs | 4 +- 23 files changed, 120 insertions(+), 178 deletions(-) delete mode 100644 src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionResponse.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionResponse.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Models/InvokeAgentRequest.cs create mode 100644 src/modules/agents/Elsa.Agents.Core/Options/AgentOptions.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs delete mode 100644 src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs delete mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/FunctionConfig.cs delete mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/SelectionStrategyConfig.cs delete mode 100644 src/modules/agents/Elsa.Agents.Models/Configs/TerminationConfig.cs diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs index f012c406..49b66dbe 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs @@ -50,7 +50,13 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context } var agentInvoker = context.GetRequiredService(); - var agentExecutionResponse = await agentInvoker.InvokeAgentAsync(AgentName, functionInput, context.CancellationToken); + var request = new InvokeAgentRequest + { + AgentName = AgentName, + Input = functionInput, + CancellationToken = context.CancellationToken + }; + var agentExecutionResponse = await agentInvoker.InvokeAsync(request); var responseText = StripCodeFences(agentExecutionResponse.ChatMessageContent.Content!); var isJsonResponse = IsJsonResponse(responseText); var outputType = context.ActivityDescriptor.Outputs.Single().Type; diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs index bd418cff..9a8f1704 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs @@ -19,7 +19,7 @@ namespace Elsa.Agents.Activities.ActivityProviders; /// [UsedImplicitly] public class CodeFirstAgentActivityProvider( - IOptions codeFirstAgentOptions, + IOptions agentOptions, IActivityDescriber activityDescriber, IWellKnownTypeRegistry wellKnownTypeRegistry) : IActivityProvider { @@ -27,7 +27,7 @@ public async ValueTask> GetDescriptorsAsync(Canc { var descriptors = new List(); - foreach (var kvp in codeFirstAgentOptions.Value.CodeFirstAgents) + foreach (var kvp in agentOptions.Value.AgentTypes) { var key = kvp.Key; var type = kvp.Value; diff --git a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs index 685e56bf..cbf22c66 100644 --- a/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs +++ b/src/modules/agents/Elsa.Agents.Api/Endpoints/Agents/Invoke/Endpoint.cs @@ -20,7 +20,13 @@ public override void Configure() /// public override async Task ExecuteAsync(Request req, CancellationToken ct) { - var result = await agentInvoker.InvokeAgentAsync(req.Agent, req.Inputs, ct).AsJsonElementAsync(); + var request = new InvokeAgentRequest + { + AgentName = req.Agent, + Input = req.Inputs, + CancellationToken = ct + }; + var result = await agentInvoker.InvokeAsync(request).AsJsonElementAsync(); return result; } } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs index cb91371e..b09f573c 100644 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs @@ -3,9 +3,7 @@ namespace Elsa.Agents; /// -/// Minimal abstraction over an executable agent so activities and endpoints -/// do not need to know whether the underlying implementation is SK-based, -/// ChatClientAgent-based, or something else. +/// Minimal abstraction to represent a code-first agent that can be automatically discovered as an activity. /// public interface IAgent { diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionResponse.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionResponse.cs deleted file mode 100644 index 005864cf..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentExecutionResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public interface IAgentExecutionResponse -{ - string Text { get; set; } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs index 1e1782fb..7a470108 100644 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentFactory.cs @@ -7,7 +7,7 @@ namespace Elsa.Agents; /// -/// Factory for creating Agent Framework agents from Elsa agent configurations. +/// Factory for creating agents from agent configurations. /// public interface IAgentFactory { diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs index 12a0a734..bd9b55a7 100644 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentInvoker.cs @@ -8,5 +8,5 @@ public interface IAgentInvoker /// /// Invokes an agent using the Microsoft Agent Framework. /// - Task InvokeAgentAsync(string agentName, IDictionary input, CancellationToken cancellationToken = default); + Task InvokeAsync(InvokeAgentRequest request); } diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs index 7ea4ba4b..bd27d0fc 100644 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs @@ -1,6 +1,15 @@ namespace Elsa.Agents; +/// +/// Defines the contract for resolving agents by their names. +/// public interface IAgentResolver { + /// + /// Resolves an agent instance by its name asynchronously. + /// + /// The name of the agent to resolve. + /// A token to monitor for cancellation requests. + /// The resolved agent instance. Task ResolveAsync(string agentName, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs b/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs index b57541fa..d2ae1b91 100644 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs +++ b/src/modules/agents/Elsa.Agents.Core/Contracts/ServiceDescriptor.cs @@ -2,8 +2,31 @@ namespace Elsa.Agents; +/// +/// Represents a service descriptor used to define and configure services in the context of the Semantic Kernel framework. +/// +/// +/// This class encapsulates the name of the service and an action to configure the kernel builder with the specific service. +/// It is typically used to register and customize services for use within agents or features. +/// public class ServiceDescriptor { + /// + /// Gets or sets the name of the service. + /// + /// + /// The name is used to identify the service descriptor and can be helpful for distinguishing + /// between different services when configuring or resolving dependencies within the Semantic Kernel framework. + /// public string Name { get; set; } = null!; + + /// + /// Gets or sets the action used to configure the kernel builder for a specific service. + /// + /// + /// This property defines an used to customize the Semantic Kernel's configuration + /// by adding or modifying services within the kernel. It provides a mechanism to integrate and set up specific + /// functionality in the context of agents or features. + /// public Action ConfigureKernel { get; set; } = null!; } \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs index b0616af1..2237018a 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs @@ -3,7 +3,6 @@ using Elsa.Features.Services; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; namespace Elsa.Agents.Features; @@ -24,8 +23,7 @@ public AgentsCoreFeature UseKernelConfigProvider(Func public override void Apply() { - Services.AddOptions(); - Services.AddOptions(); + Services.AddOptions(); Services .AddScoped() diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionResponse.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionResponse.cs deleted file mode 100644 index a10449c8..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Agents; - -public class AgentExecutionResponse : IAgentExecutionResponse -{ - public string Text { get; set; } = null!; -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Models/InvokeAgentRequest.cs b/src/modules/agents/Elsa.Agents.Core/Models/InvokeAgentRequest.cs new file mode 100644 index 00000000..2cff2b57 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Models/InvokeAgentRequest.cs @@ -0,0 +1,29 @@ +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Elsa.Agents; + +/// +/// Represents a request to invoke an agent. +/// +public class InvokeAgentRequest +{ + /// + /// Gets or sets the name of the agent to invoke. + /// + public required string AgentName { get; set; } + + /// + /// Gets or sets the input parameters for the agent. + /// + public IDictionary Input { get; set; } = new Dictionary(); + + /// + /// Gets or sets the chat history. If null, a new chat history will be created. + /// + public ChatHistory? ChatHistory { get; set; } + + /// + /// Gets or sets the cancellation token. + /// + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; +} diff --git a/src/modules/agents/Elsa.Agents.Core/Options/AgentOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/AgentOptions.cs new file mode 100644 index 00000000..d9796968 --- /dev/null +++ b/src/modules/agents/Elsa.Agents.Core/Options/AgentOptions.cs @@ -0,0 +1,23 @@ +namespace Elsa.Agents; + +public class AgentOptions +{ + public ICollection Agents { get; set; } = new List(); + public ICollection ServiceDescriptors { get; set; } = new List(); + + /// + /// Map from agent key to the implementing type. Keys are case-insensitive. + /// + public IDictionary AgentTypes { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Registers a code-first agent type. If no key is provided, the type name + /// is used as the key. + /// + public AgentOptions AddAgentType(string? key = null) where TAgent : class, IAgent + { + key ??= typeof(TAgent).Name; + AgentTypes[key] = typeof(TAgent); + return this; + } +} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs deleted file mode 100644 index c34b188e..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Options/CodeFirstAgentOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Options used to configure code-first agents. Developers can register -/// agent types here and have them exposed via IAgentProvider/IAgentResolver. -/// -public class CodeFirstAgentOptions -{ - /// - /// Map from agent key to the implementing type. Keys are case-insensitive. - /// - public IDictionary CodeFirstAgents { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Registers a code-first agent type. If no key is provided, the type name - /// is used as the key. - /// - public CodeFirstAgentOptions AddAgent(string? key = null) where TAgent : class, IAgent - { - key ??= typeof(TAgent).Name; - CodeFirstAgents[key] = typeof(TAgent); - return this; - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs deleted file mode 100644 index aa5e7e39..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Options/ConfiguredAgentOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Agents; - -public class ConfiguredAgentOptions -{ - public ICollection Agents { get; set; } = new List(); - public ICollection ServiceDescriptors { get; set; } = new List(); -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs index 8dad3724..f6d718af 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentFactory.cs @@ -10,18 +10,14 @@ namespace Elsa.Agents; -/// -/// Factory for creating Agent Framework agents from Elsa agent configurations. -/// +/// public class AgentFactory( ISkillDiscoverer skillDiscoverer, IServiceProvider serviceProvider, - IOptions options, + IOptions options, ILogger logger) : IAgentFactory { - /// - /// Creates a ChatCompletionAgent from an Elsa agent configuration. - /// + /// public ChatCompletionAgent CreateAgent(AgentConfig agentConfig) { var kernel = CreateKernel(agentConfig); diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs index 08830750..781475ce 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs @@ -14,16 +14,16 @@ public class AgentInvoker(IKernelConfigProvider kernelConfigProvider, IAgentFact /// /// Invokes an agent using the Microsoft Agent Framework (new approach). /// - public async Task InvokeAgentAsync(string agentName, IDictionary input, CancellationToken cancellationToken = default) + public async Task InvokeAsync(InvokeAgentRequest request) { - var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(cancellationToken); - var agentConfig = kernelConfig.Agents[agentName]; + var kernelConfig = await kernelConfigProvider.GetKernelConfigAsync(request.CancellationToken); + var agentConfig = kernelConfig.Agents[request.AgentName]; // Create agent using Agent Framework var agent = agentFactory.CreateAgent(agentConfig); - // Create chat history - ChatHistory chatHistory = []; + // Use provided chat history or create new one + ChatHistory chatHistory = request.ChatHistory ?? []; // Format and add user input var promptTemplateConfig = new PromptTemplateConfig @@ -54,8 +54,8 @@ public async Task InvokeAgentAsync(string agentName, IDiction [PromptExecutionSettings.DefaultServiceId] = promptExecutionSettings, }; - var kernelArguments = new KernelArguments(input, promptExecutionSettingsDictionary); - var renderedPrompt = await promptTemplate.RenderAsync(agent.Kernel, kernelArguments, cancellationToken); + var kernelArguments = new KernelArguments(request.Input, promptExecutionSettingsDictionary); + var renderedPrompt = await promptTemplate.RenderAsync(agent.Kernel, kernelArguments, request.CancellationToken); chatHistory.AddUserMessage(renderedPrompt); @@ -77,7 +77,7 @@ You are a function that returns *only* JSON. } // Get response from agent - var response = await agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken).LastOrDefaultAsync(cancellationToken); + var response = await agent.InvokeAsync(chatHistory, cancellationToken: request.CancellationToken).LastOrDefaultAsync(request.CancellationToken); if (response == null) throw new InvalidOperationException("Agent did not produce a response"); diff --git a/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs index bbc6cd83..db53d9a6 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs @@ -3,11 +3,11 @@ namespace Elsa.Agents; -public class AgentResolver(IServiceProvider serviceProvider, IOptions options) : IAgentResolver +public class AgentResolver(IServiceProvider serviceProvider, IOptions options) : IAgentResolver { public Task ResolveAsync(string agentName, CancellationToken cancellationToken = default) { - var agentType = options.Value.CodeFirstAgents[agentName] ?? throw new InvalidOperationException($"No agent with name '{agentName}' was found."); + var agentType = options.Value.AgentTypes[agentName] ?? throw new InvalidOperationException($"No agent with name '{agentName}' was found."); var agent = (IAgent)ActivatorUtilities.CreateInstance(serviceProvider, agentType)!; return Task.FromResult(agent); } diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs index 4c1ad75b..c2bb6947 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs @@ -7,7 +7,7 @@ namespace Elsa.Agents; /// Provides kernel configuration from configuration. /// [UsedImplicitly] -public class ConfigurationKernelConfigProvider(IOptions options) : IKernelConfigProvider +public class ConfigurationKernelConfigProvider(IOptions options) : IKernelConfigProvider { public Task GetKernelConfigAsync(CancellationToken cancellationToken = default) { diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/FunctionConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/FunctionConfig.cs deleted file mode 100644 index a4b3d05b..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/FunctionConfig.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Agents; - -public class FunctionConfig -{ - - -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/SelectionStrategyConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/SelectionStrategyConfig.cs deleted file mode 100644 index 72dcd418..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/SelectionStrategyConfig.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Configuration for agent selection strategy in multi-agent workflows. -/// -public class SelectionStrategyConfig -{ - /// - /// The type of selection strategy. - /// - public SelectionStrategyType Type { get; set; } = SelectionStrategyType.Sequential; - - /// - /// Custom selection prompt (when Type is LLMBased). - /// - public string? SelectionPrompt { get; set; } - - /// - /// Agent responsible for making selection decisions (when Type is AgentBased). - /// - public string? SelectorAgentName { get; set; } -} - -/// -/// Types of agent selection strategies. -/// -public enum SelectionStrategyType -{ - /// - /// Agents are selected in sequential order. - /// - Sequential, - - /// - /// Round-robin selection among agents. - /// - RoundRobin, - - /// - /// Use an LLM to decide which agent should act next. - /// - LLMBased, - - /// - /// Use a dedicated agent to make selection decisions. - /// - AgentBased -} diff --git a/src/modules/agents/Elsa.Agents.Models/Configs/TerminationConfig.cs b/src/modules/agents/Elsa.Agents.Models/Configs/TerminationConfig.cs deleted file mode 100644 index ccede073..00000000 --- a/src/modules/agents/Elsa.Agents.Models/Configs/TerminationConfig.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Configuration for agent workflow termination conditions. -/// -public class TerminationConfig -{ - /// - /// The type of termination strategy. - /// - public TerminationType Type { get; set; } = TerminationType.MaxMessages; - - /// - /// Maximum number of messages/turns before termination (when Type is MaxMessages). - /// - public int MaxMessages { get; set; } = 10; - - /// - /// Keyword or pattern that triggers termination (when Type is Keyword). - /// - public string? TerminationKeyword { get; set; } - - /// - /// Name of the agent that can trigger termination (when Type is AgentDecision). - /// - public string? TerminationAgentName { get; set; } -} - -/// -/// Types of termination strategies for agent workflows. -/// -public enum TerminationType -{ - /// - /// Terminate after a maximum number of messages/turns. - /// - MaxMessages, - - /// - /// Terminate when a specific keyword or pattern is detected. - /// - Keyword, - - /// - /// Allow a specific agent to decide when to terminate. - /// - AgentDecision -} diff --git a/src/modules/agents/Elsa.Agents/AgentsFeature.cs b/src/modules/agents/Elsa.Agents/AgentsFeature.cs index e759b9ea..c7ca0664 100644 --- a/src/modules/agents/Elsa.Agents/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents/AgentsFeature.cs @@ -13,13 +13,13 @@ public class AgentsFeature(IModule module) : FeatureBase(module) { public AgentsFeature AddAgent(string? key = null) where TAgent : class, IAgent { - Module.Services.Configure(options => options.AddAgent(key)); + Module.Services.Configure(options => options.AddAgentType(key)); return this; } public AgentsFeature AddServiceDescriptor(ServiceDescriptor descriptor) { - Module.Services.Configure(options => options.ServiceDescriptors.Add(descriptor)); + Module.Services.Configure(options => options.ServiceDescriptors.Add(descriptor)); return this; } } \ No newline at end of file From f4c78649023e786af23d04d20345b5e62c24d943 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Dec 2025 15:13:43 +0100 Subject: [PATCH 23/23] Remove obsolete agent test suite and update solution references. Deleted outdated test classes for agents and workflows, along with their associated configuration tests (`AgentDefinitionProviderTests`, `AgentWorkflowDefinitionProviderTests`, `ConfigurationKernelConfigProviderTests`). Removed corresponding project and solution references to streamline the codebase. --- Elsa.Extensions.sln | 7 -- .../AgentDefinitionProviderTests.cs | 63 ------------- .../AgentWorkflowDefinitionProviderTests.cs | 58 ------------ .../ConfigurationKernelConfigProviderTests.cs | 89 ------------------- .../Elsa.Agents.Tests.csproj | 6 -- 5 files changed, 223 deletions(-) delete mode 100644 test/modules/agents/Elsa.Agents.Tests/AgentDefinitionProviderTests.cs delete mode 100644 test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs delete mode 100644 test/modules/agents/Elsa.Agents.Tests/ConfigurationKernelConfigProviderTests.cs delete mode 100644 test/modules/agents/Elsa.Agents.Tests/Elsa.Agents.Tests.csproj diff --git a/Elsa.Extensions.sln b/Elsa.Extensions.sln index 18e6ee93..08021ec4 100644 --- a/Elsa.Extensions.sln +++ b/Elsa.Extensions.sln @@ -279,8 +279,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Data.Csv", "src\module EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "agents", "agents", "{60A25F2D-634D-438A-87EA-F204677978BE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Tests", "test\modules\agents\Elsa.Agents.Tests\Elsa.Agents.Tests.csproj", "{F997734B-468C-40ED-9CBB-F759F71AA06E}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -631,10 +629,6 @@ Global {015646EB-EC33-4ADF-8417-B9D1A6B7CF06}.Debug|Any CPU.Build.0 = Debug|Any CPU {015646EB-EC33-4ADF-8417-B9D1A6B7CF06}.Release|Any CPU.ActiveCfg = Release|Any CPU {015646EB-EC33-4ADF-8417-B9D1A6B7CF06}.Release|Any CPU.Build.0 = Release|Any CPU - {F997734B-468C-40ED-9CBB-F759F71AA06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F997734B-468C-40ED-9CBB-F759F71AA06E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F997734B-468C-40ED-9CBB-F759F71AA06E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F997734B-468C-40ED-9CBB-F759F71AA06E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -760,7 +754,6 @@ Global {6D2A4421-A388-4BEE-BB11-D0FC32A80A10} = {30CF0330-4B09-4784-B499-46BED303810B} {015646EB-EC33-4ADF-8417-B9D1A6B7CF06} = {6D2A4421-A388-4BEE-BB11-D0FC32A80A10} {60A25F2D-634D-438A-87EA-F204677978BE} = {3DDE6F89-531C-47F8-9CD7-7A4E6984FA48} - {F997734B-468C-40ED-9CBB-F759F71AA06E} = {60A25F2D-634D-438A-87EA-F204677978BE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {11A771DA-B728-445E-8A88-AE1C84C3B3A6} diff --git a/test/modules/agents/Elsa.Agents.Tests/AgentDefinitionProviderTests.cs b/test/modules/agents/Elsa.Agents.Tests/AgentDefinitionProviderTests.cs deleted file mode 100644 index e662cb06..00000000 --- a/test/modules/agents/Elsa.Agents.Tests/AgentDefinitionProviderTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Elsa.Agents; -using Xunit; - -namespace Elsa.Agents.Tests; - -public class AgentDefinitionProviderTests -{ - [Fact] - public void GetDefinitions_ReturnsRegisteredDefinitions() - { - // Arrange - var definition1 = new TestAgentDefinition("Agent1"); - var definition2 = new TestAgentDefinition("Agent2"); - var definitions = new List { definition1, definition2 }; - var provider = new AgentDefinitionProvider(definitions); - - // Act - var result = provider.GetDefinitions().ToList(); - - // Assert - Assert.Equal(2, result.Count); - Assert.Contains(definition1, result); - Assert.Contains(definition2, result); - } - - [Fact] - public void GetDefinitions_WithNoDefinitions_ReturnsEmpty() - { - // Arrange - var provider = new AgentDefinitionProvider(Array.Empty()); - - // Act - var result = provider.GetDefinitions().ToList(); - - // Assert - Assert.Empty(result); - } - - private class TestAgentDefinition : IAgentDefinition - { - public TestAgentDefinition(string name) - { - Name = name; - } - - public string Name { get; } - public string Description => $"Test agent {Name}"; - - public AgentConfig GetAgentConfig() - { - return new AgentConfig - { - Name = Name, - Description = Description, - Services = Array.Empty(), - FunctionName = "Test", - PromptTemplate = "Test prompt", - InputVariables = Array.Empty(), - OutputVariable = new OutputVariableConfig() - }; - } - } -} diff --git a/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs b/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs deleted file mode 100644 index fc1afc44..00000000 --- a/test/modules/agents/Elsa.Agents.Tests/AgentWorkflowDefinitionProviderTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Elsa.Agents; -using Xunit; - -namespace Elsa.Agents.Tests; - -public class AgentWorkflowDefinitionProviderTests -{ - [Fact] - public void GetDefinitions_ReturnsRegisteredWorkflows() - { - // Arrange - var workflow1 = new TestWorkflowDefinition("Workflow1"); - var workflow2 = new TestWorkflowDefinition("Workflow2"); - var definitions = new List { workflow1, workflow2 }; - var provider = new AgentWorkflowDefinitionProvider(definitions); - - // Act - var result = provider.GetDefinitions().ToList(); - - // Assert - Assert.Equal(2, result.Count); - Assert.Contains(workflow1, result); - Assert.Contains(workflow2, result); - } - - [Fact] - public void GetDefinitions_WithNoWorkflows_ReturnsEmpty() - { - // Arrange - var provider = new AgentWorkflowDefinitionProvider(Array.Empty()); - - // Act - var result = provider.GetDefinitions().ToList(); - - // Assert - Assert.Empty(result); - } - - private class TestWorkflowDefinition(string name) : IAgentWorkflowDefinition - { - public string Name { get; } = name; - public string Description => $"Test workflow {Name}"; - - public AgentWorkflowConfig GetWorkflowConfig() - { - return new() - { - Name = Name, - Description = Description, - WorkflowType = AgentWorkflowType.Sequential, - Agents = Array.Empty(), - Services = Array.Empty(), - InputVariables = Array.Empty(), - OutputVariable = new() - }; - } - } -} diff --git a/test/modules/agents/Elsa.Agents.Tests/ConfigurationKernelConfigProviderTests.cs b/test/modules/agents/Elsa.Agents.Tests/ConfigurationKernelConfigProviderTests.cs deleted file mode 100644 index 64f76e22..00000000 --- a/test/modules/agents/Elsa.Agents.Tests/ConfigurationKernelConfigProviderTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Elsa.Agents; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Elsa.Agents.Tests; - -public class ConfigurationKernelConfigProviderTests -{ - [Fact] - public async Task GetKernelConfigAsync_MergesConfigurationAndCodeFirstAgents() - { - // Arrange - var options = Options.Create(new AgentsOptions - { - Agents = new List - { - new() { Name = "ConfigAgent", Description = "From config" } - } - }); - - var codeFirstAgent = new TestAgent("CodeFirstAgent"); - var agentProvider = new AgentDefinitionProvider(new[] { codeFirstAgent }); - var workflowProvider = new AgentWorkflowDefinitionProvider(Array.Empty()); - - var provider = new ConfigurationKernelConfigProvider(options, agentProvider, workflowProvider); - - // Act - var config = await provider.GetKernelConfigAsync(); - - // Assert - Assert.Equal(2, config.Agents.Count); - Assert.True(config.Agents.ContainsKey("ConfigAgent")); - Assert.True(config.Agents.ContainsKey("CodeFirstAgent")); - } - - [Fact] - public async Task GetKernelConfigAsync_IncludesAgentWorkflows() - { - // Arrange - var options = Options.Create(new AgentsOptions()); - var agentProvider = new AgentDefinitionProvider(Array.Empty()); - - var workflow = new TestWorkflow("TestWorkflow"); - var workflowProvider = new AgentWorkflowDefinitionProvider(new[] { workflow }); - - var provider = new ConfigurationKernelConfigProvider(options, agentProvider, workflowProvider); - - // Act - var config = await provider.GetKernelConfigAsync(); - - // Assert - Assert.Single(config.AgentWorkflows); - Assert.True(config.AgentWorkflows.ContainsKey("TestWorkflow")); - } - - private class TestAgent : IAgentDefinition - { - public TestAgent(string name) => Name = name; - public string Name { get; } - public string Description => $"Test {Name}"; - public AgentConfig GetAgentConfig() => new() - { - Name = Name, - Description = Description, - Services = Array.Empty(), - FunctionName = "Test", - PromptTemplate = "Test", - InputVariables = Array.Empty(), - OutputVariable = new OutputVariableConfig() - }; - } - - private class TestWorkflow : IAgentWorkflowDefinition - { - public TestWorkflow(string name) => Name = name; - public string Name { get; } - public string Description => $"Test {Name}"; - public AgentWorkflowConfig GetWorkflowConfig() => new() - { - Name = Name, - Description = Description, - WorkflowType = AgentWorkflowType.Sequential, - Agents = Array.Empty(), - Services = Array.Empty(), - InputVariables = Array.Empty(), - OutputVariable = new OutputVariableConfig() - }; - } -} diff --git a/test/modules/agents/Elsa.Agents.Tests/Elsa.Agents.Tests.csproj b/test/modules/agents/Elsa.Agents.Tests/Elsa.Agents.Tests.csproj deleted file mode 100644 index 79a286e8..00000000 --- a/test/modules/agents/Elsa.Agents.Tests/Elsa.Agents.Tests.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - - -