diff --git a/Directory.Build.props b/Directory.Build.props index 720ea1eb..e56b8c0a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -37,8 +37,8 @@ - 3.6.0-rc1 - 3.6.0-rc1 + 3.6.0-preview.4162 + 3.6.0-preview.1345 \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 8efbcf46..5f351d6e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -140,6 +140,9 @@ + + + diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs similarity index 95% rename from src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs rename to src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs index 49b66dbe..92797d09 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/ConfiguredAgentActivity.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Activities/AgentActivity.cs @@ -15,10 +15,10 @@ 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 +public class AgentActivity : CodeActivity { private static JsonSerializerOptions? _serializerOptions; diff --git a/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs b/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs deleted file mode 100644 index 8d988402..00000000 --- a/src/modules/agents/Elsa.Agents.Activities/Activities/CodeFirstAgentActivity.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.ComponentModel; -using System.Dynamic; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; -using Elsa.Expressions.Helpers; -using Elsa.Agents.Activities.ActivityProviders; -using Elsa.Workflows; -using Elsa.Workflows.Models; -using static Elsa.Agents.Activities.Extensions.ResponseHelpers; - -namespace Elsa.Agents.Activities; - -/// -/// An activity that executes a function of a skilled agent. This is an internal activity that is used by . -/// -[Browsable(false)] -public class CodeFirstAgentActivity : CodeActivity -{ - private static JsonSerializerOptions? _serializerOptions; - - [JsonIgnore] internal string AgentName { get; set; } = null!; - [JsonIgnore] internal string MethodName { get; set; } = null!; - - /// - protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) - { - var cancellationToken = context.CancellationToken; - var activityDescriptor = context.ActivityDescriptor; - var inputDescriptors = activityDescriptor.Inputs; - var functionInput = new Dictionary(); - - foreach (var inputDescriptor in inputDescriptors) - { - var input = (Input?)inputDescriptor.ValueGetter(this); - var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; - - if (inputValue is ExpandoObject expandoObject) - inputValue = expandoObject.ConvertTo(); - - functionInput[inputDescriptor.Name] = inputValue; - } - - // Resolve the agent via the unified abstraction. - var agentResolver = context.GetRequiredService(); - var agent = await agentResolver.ResolveAsync(AgentName, cancellationToken); - var agentType = agent.GetType(); - var agentPropertyLookup = agentType.GetProperties().ToDictionary(x => x.Name, x => x); - - // Copy activity input descriptor values into the agent public properties: - foreach (var inputDescriptor in inputDescriptors) - { - var input = (Input?)inputDescriptor.ValueGetter(this); - var inputValue = input != null ? context.Get(input.MemoryBlockReference()) : null; - agentPropertyLookup[inputDescriptor.Name].SetValue(agent, inputValue); - } - - // Invoke the specified method on the agent using reflection - var method = agentType.GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public); - if (method == null) - throw new InvalidOperationException($"Method '{MethodName}' not found on agent type '{agentType.Name}'."); - - var agentExecutionContext = new AgentExecutionContext { CancellationToken = context.CancellationToken }; - var task = method.Invoke(agent, new object[] { agentExecutionContext }) as Task; - if (task == null) - throw new InvalidOperationException($"Method '{MethodName}' did not return a Task."); - - var agentExecutionResponse = await task; - var responseText = StripCodeFences(agentExecutionResponse.Text); - var isJsonResponse = IsJsonResponse(responseText); - var outputType = context.ActivityDescriptor.Outputs.Single().Type; - - // If the target type is object and the response is in JSON format, we want it to be deserialized into an ExpandoObject for dynamic field access. - if (outputType == typeof(object) && isJsonResponse) - outputType = typeof(ExpandoObject); - - var outputValue = isJsonResponse ? responseText.ConvertTo(outputType) : responseText; - var outputDescriptor = activityDescriptor.Outputs.Single(); - var output = (Output?)outputDescriptor.ValueGetter(this); - context.Set(output, outputValue, "Output"); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs similarity index 94% rename from src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs rename to src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs index 70ea85a7..4d4d8972 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/ConfiguredAgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs @@ -12,7 +12,7 @@ namespace Elsa.Agents.Activities.ActivityProviders; /// Provides activities for each registered agent. /// [UsedImplicitly] -public class ConfigurationAgentActivityProvider( +public class AgentActivityProvider( IKernelConfigProvider kernelConfigProvider, IActivityDescriber activityDescriber, IWellKnownTypeRegistry wellKnownTypeRegistry @@ -37,7 +37,7 @@ public async ValueTask> GetDescriptorsAsync(Canc private async Task CreateAgentActivityDescriptor(AgentConfig agentConfig, CancellationToken cancellationToken) { - var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(ConfiguredAgentActivity), cancellationToken); + var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(AgentActivity), cancellationToken); var functionName = string.IsNullOrWhiteSpace(agentConfig.FunctionName) ? agentConfig.Name : agentConfig.FunctionName; var activityTypeName = $"Elsa.Agents.{activityDescriptor.Name.Pascalize()}.{functionName.Pascalize()}"; activityDescriptor.Name = functionName.Pascalize(); @@ -48,11 +48,11 @@ private async Task CreateAgentActivityDescriptor(AgentConfig activityDescriptor.Category = "Agents"; activityDescriptor.Kind = ActivityKind.Task; activityDescriptor.RunAsynchronously = true; - activityDescriptor.ClrType = typeof(ConfiguredAgentActivity); + activityDescriptor.ClrType = typeof(AgentActivity); activityDescriptor.Constructor = context => { - var activity = context.CreateActivity(); + var activity = context.CreateActivity(); activity.Type = activityTypeName; activity.AgentName = agentConfig.Name; activity.RunAsynchronously = true; diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs deleted file mode 100644 index 8a5aeb79..00000000 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/CodeFirstAgentActivityProvider.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Reflection; -using Elsa.Expressions.Contracts; -using Elsa.Expressions.Extensions; -using Elsa.Extensions; -using Elsa.Workflows; -using Elsa.Workflows.Models; -using Humanizer; -using JetBrains.Annotations; -using Microsoft.Extensions.Options; - -namespace Elsa.Agents.Activities.ActivityProviders; - -/// -/// Provides activities for each code-first agent registered via . -/// Inputs are derived from public properties on the agent type using simple -/// reflection rules. Execution is delegated to -/// via the common abstraction. -/// -[UsedImplicitly] -public class CodeFirstAgentActivityProvider( - IOptions agentOptions, - IActivityDescriber activityDescriber, - IWellKnownTypeRegistry wellKnownTypeRegistry) : IActivityProvider -{ - public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) - { - var descriptors = new List(); - - foreach (var kvp in agentOptions.Value.AgentTypes) - { - var key = kvp.Key; - var type = kvp.Value; - var agentDescriptors = await CreateDescriptorsForAgentAsync(key, type, cancellationToken); - descriptors.AddRange(agentDescriptors); - } - - return descriptors; - } - - private async Task> CreateDescriptorsForAgentAsync(string key, Type agentType, CancellationToken cancellationToken) - { - var descriptors = new List(); - - // Discover all public methods that match the signature: async Task MethodName(AgentExecutionContext) - var methods = agentType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) - .Where(IsAgentActionMethod) - .ToList(); - - foreach (var method in methods) - { - var descriptor = await CreateDescriptorForAgentMethodAsync(key, agentType, method, cancellationToken); - descriptors.Add(descriptor); - } - - return descriptors; - } - - private async Task CreateDescriptorForAgentMethodAsync(string agentKey, Type agentType, MethodInfo method, CancellationToken cancellationToken) - { - var descriptor = await activityDescriber.DescribeActivityAsync(typeof(CodeFirstAgentActivity), cancellationToken); - var methodName = method.Name; - var activityTypeName = $"Elsa.Agents.CodeFirst.{agentKey.Pascalize()}.{methodName}"; - - // Strip "Async" suffix for display purposes - var displayMethodName = methodName.EndsWith("Async", StringComparison.Ordinal) - ? methodName[..^5] - : methodName; - - // Check for DisplayAttribute - var displayAttribute = method.GetCustomAttribute(); - var displayName = displayAttribute?.Name ?? displayMethodName.Humanize().Transform(To.TitleCase); - - descriptor.Name = methodName; - descriptor.TypeName = activityTypeName; - descriptor.DisplayName = displayName; - descriptor.Description = method.GetCustomAttribute()?.Description; - descriptor.Category = "Agents"; - descriptor.Kind = ActivityKind.Task; - descriptor.RunAsynchronously = true; - descriptor.IsBrowsable = true; - descriptor.ClrType = typeof(CodeFirstAgentActivity); - - descriptor.Constructor = context => - { - var activity = context.CreateActivity(); - activity.Type = activityTypeName; - activity.AgentName = agentKey; - activity.MethodName = methodName; - activity.RunAsynchronously = true; - return activity; - }; - - // Build inputs from public instance properties. - descriptor.Inputs.Clear(); - foreach (var prop in agentType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) - { - if (!IsInputProperty(prop)) - continue; - - var inputName = prop.Name; - var inputType = prop.PropertyType.FullName ?? "object"; - var nakedInputType = wellKnownTypeRegistry.GetTypeOrDefault(inputType); - var description = prop.GetCustomAttribute()?.Description; - - var inputDescriptor = new InputDescriptor - { - Name = inputName, - DisplayName = inputName.Humanize(), - Description = description, - Type = nakedInputType, - ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(inputName), - ValueSetter = (activity, value) => activity.SyntheticProperties[inputName] = value!, - IsSynthetic = true, - IsWrapped = true, - UIHint = ActivityDescriber.GetUIHint(nakedInputType) - }; - - descriptor.Inputs.Add(inputDescriptor); - } - - // For now, expose a single synthetic Output of type object, mirroring - // the existing AgentActivity behavior. - descriptor.Outputs.Clear(); - var outputName = "Output"; - var outputDescriptor = new OutputDescriptor - { - Name = outputName, - Description = "The agent's output.", - Type = typeof(object), - IsSynthetic = true, - ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(outputName), - ValueSetter = (activity, value) => activity.SyntheticProperties[outputName] = value!, - }; - descriptor.Outputs.Add(outputDescriptor); - - return descriptor; - } - - private static bool IsAgentActionMethod(MethodInfo method) - { - // Must return Task - if (method.ReturnType != typeof(Task)) - return false; - - // Must have exactly one parameter of type AgentExecutionContext - var parameters = method.GetParameters(); - if (parameters.Length != 1) - return false; - - if (parameters[0].ParameterType != typeof(AgentExecutionContext)) - return false; - - return true; - } - - private static bool IsInputProperty(PropertyInfo prop) - { - // Simple heuristic for now: - // - Must be readable and writable - // - Exclude indexers - if (!prop.CanRead || !prop.CanWrite) - return false; - - if (prop.GetIndexParameters().Length > 0) - return false; - - // In the future, you can add a dedicated [AgentInput] attribute and - // check for it here. For now, treat all simple public properties as - // potential inputs. - return true; - } -} - diff --git a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs index 046e72c4..7202e1b6 100644 --- a/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs +++ b/src/modules/agents/Elsa.Agents.Activities/Features/AgentActivitiesFeature.cs @@ -22,8 +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 8db3830f..6cc0bcf2 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, ConfigurationAgentActivityProvider configurationAgentActivityProvider) : +public class RefreshActivityRegistry(IActivityRegistry activityRegistry, AgentActivityProvider agentActivityProvider) : INotificationHandler, INotificationHandler, INotificationHandler, @@ -17,5 +17,5 @@ public class RefreshActivityRegistry(IActivityRegistry activityRegistry, Configu 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(configurationAgentActivityProvider, cancellationToken); + private Task RefreshRegistryAsync(CancellationToken cancellationToken) => activityRegistry.RefreshDescriptorsAsync(agentActivityProvider, cancellationToken); } \ 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 deleted file mode 100644 index 36b11f7f..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Elsa.Agents; - -/// -/// Minimal abstraction to represent a code-first agent that can be automatically discovered as an activity. -/// Implementing classes should define public async Task methods accepting as a parameter. -/// Each such method will be automatically discovered and exposed as an activity. -/// -public interface IAgent; \ 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 bd27d0fc..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Contracts/IAgentResolver.cs +++ /dev/null @@ -1,15 +0,0 @@ -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/Features/AgentsCoreFeature.cs b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs index 6a70e6ba..840f106d 100644 --- a/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs +++ b/src/modules/agents/Elsa.Agents.Core/Features/AgentsCoreFeature.cs @@ -12,7 +12,7 @@ namespace Elsa.Agents.Features; [UsedImplicitly] public class AgentsCoreFeature(IModule module) : FeatureBase(module) { - private Func _kernelConfigProviderFactory = sp => sp.GetRequiredService(); + private Func _kernelConfigProviderFactory = sp => sp.GetRequiredService(); public AgentsCoreFeature UseKernelConfigProvider(Func factory) { @@ -30,8 +30,7 @@ public override void Apply() .AddScoped() .AddScoped() .AddScoped(_kernelConfigProviderFactory) - .AddScoped() - .AddScoped() + .AddScoped() .AddSkillsProvider() .AddSkillsProvider() ; diff --git a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs b/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs deleted file mode 100644 index 861604e9..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Models/AgentExecutionContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Agents; - -public class AgentExecutionContext -{ - public string Message { get; set; } = null!; - public CancellationToken CancellationToken { get; set; } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs b/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs index 809dd36b..e5e1b3a5 100644 --- a/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs +++ b/src/modules/agents/Elsa.Agents.Core/Options/AgentsOptions.cs @@ -4,20 +4,4 @@ public class AgentsOptions { 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 AgentsOptions 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/Services/AgentResolver.cs b/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs deleted file mode 100644 index 14ab5390..00000000 --- a/src/modules/agents/Elsa.Agents.Core/Services/AgentResolver.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Elsa.Agents; - -public class AgentResolver(IServiceProvider serviceProvider, IOptions options) : IAgentResolver -{ - public Task ResolveAsync(string agentName, CancellationToken cancellationToken = default) - { - var agentType = options.Value.AgentTypes[agentName] ?? throw new InvalidOperationException($"No agent with name '{agentName}' was found."); - var agent = (IAgent)ActivatorUtilities.CreateInstance(serviceProvider, agentType)!; - return Task.FromResult(agent); - } -} \ No newline at end of file diff --git a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs b/src/modules/agents/Elsa.Agents.Core/Services/KernelConfigProvider.cs similarity index 84% rename from src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs rename to src/modules/agents/Elsa.Agents.Core/Services/KernelConfigProvider.cs index 469d8256..295d9899 100644 --- a/src/modules/agents/Elsa.Agents.Core/Services/ConfigurationKernelConfigProvider.cs +++ b/src/modules/agents/Elsa.Agents.Core/Services/KernelConfigProvider.cs @@ -7,7 +7,7 @@ namespace Elsa.Agents; /// Provides kernel configuration from configuration. /// [UsedImplicitly] -public class ConfigurationKernelConfigProvider(IOptions options) : IKernelConfigProvider +public class KernelConfigProvider(IOptions options) : IKernelConfigProvider { public Task GetKernelConfigAsync(CancellationToken cancellationToken = default) { diff --git a/src/modules/agents/Elsa.Agents/AgentsFeature.cs b/src/modules/agents/Elsa.Agents/AgentsFeature.cs index 239850d6..0e94c49d 100644 --- a/src/modules/agents/Elsa.Agents/AgentsFeature.cs +++ b/src/modules/agents/Elsa.Agents/AgentsFeature.cs @@ -11,12 +11,6 @@ namespace Elsa.Agents; [DependsOn(typeof(AgentActivitiesFeature))] public class AgentsFeature(IModule module) : FeatureBase(module) { - public AgentsFeature AddAgent(string? key = null) where TAgent : class, IAgent - { - Module.Services.Configure(options => options.AddAgentType(key)); - return this; - } - public AgentsFeature AddServiceDescriptor(ServiceDescriptor descriptor) { Module.Services.Configure(options => options.ServiceDescriptors.Add(descriptor)); diff --git a/src/workbench/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/workbench/Elsa.Server.Web/Elsa.Server.Web.csproj index aae3d865..fa34185c 100644 --- a/src/workbench/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/workbench/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -30,6 +30,9 @@ + + +