< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.AgentFactory
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/AgentFactory.cs
Line coverage
98%
Covered lines: 147
Uncovered lines: 2
Coverable lines: 149
Total lines: 305
Line coverage: 98.6%
Branch coverage
90%
Covered branches: 72
Total branches: 80
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%66100%
CreateAgent()100%11100%
CreateAgent(...)100%11100%
CreateAgent(...)100%22100%
CreateAgent(...)100%22100%
ResolveTools(...)100%2260%
ResolveTools()100%11100%
ResolveToolsFromType(...)50%44100%
ResolveToolsCore(...)100%44100%
CreateAgentFromType(...)100%1010100%
CreateAgentCore(...)80%1010100%
ApplyPlugins(...)100%1212100%
GetApplicableTypes(...)100%1212100%
BuildConfiguredOptions()100%22100%
BuildFunctionsCache()100%44100%
BuildFunctionsForType(...)87.5%88100%
BuildFunctionsForTypeViaReflection(...)100%22100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/AgentFactory.cs

#LineLine coverage
 1using Microsoft.Agents.AI;
 2using Microsoft.Extensions.AI;
 3using Microsoft.Extensions.DependencyInjection;
 4
 5using System.Diagnostics.CodeAnalysis;
 6using System.Reflection;
 7
 8namespace NexusLabs.Needlr.AgentFramework;
 9
 10internal sealed class AgentFactory : IAgentFactory
 11{
 12    private readonly IServiceProvider _serviceProvider;
 13    private readonly List<Action<AgentFrameworkConfigureOptions>> _configureCallbacks;
 14    private readonly IReadOnlyList<Type> _functionTypes;
 15    private readonly IReadOnlyDictionary<string, IReadOnlyList<Type>> _functionGroupMap;
 16    private readonly Lazy<AgentFrameworkConfigureOptions> _lazyConfiguredOptions;
 17    private readonly Lazy<IReadOnlyDictionary<Type, IReadOnlyList<AIFunction>>> _lazyFunctionsCache;
 18    private readonly IAIFunctionProvider? _generatedProvider;
 19    private readonly IReadOnlyList<IAIAgentBuilderPlugin> _plugins;
 20    private readonly Func<AgentResilienceAttribute, IAIAgentBuilderPlugin>? _perAgentResilienceFactory;
 21
 22    private readonly IReadOnlyDictionary<string, Type> _agentTypeMap;
 23
 18824    public AgentFactory(
 18825        IServiceProvider serviceProvider,
 18826        List<Action<AgentFrameworkConfigureOptions>> configureCallbacks,
 18827        IReadOnlyList<Type> functionTypes,
 18828        IReadOnlyDictionary<string, IReadOnlyList<Type>>? functionGroupMap = null,
 18829        IReadOnlyDictionary<string, Type>? agentTypeMap = null,
 18830        IAIFunctionProvider? generatedProvider = null,
 18831        IReadOnlyList<IAIAgentBuilderPlugin>? plugins = null,
 18832        Func<AgentResilienceAttribute, IAIAgentBuilderPlugin>? perAgentResilienceFactory = null)
 33    {
 18834        _serviceProvider = serviceProvider;
 18835        _configureCallbacks = configureCallbacks;
 18836        _functionTypes = functionTypes;
 18837        _functionGroupMap = functionGroupMap ?? new Dictionary<string, IReadOnlyList<Type>>();
 18838        _agentTypeMap = agentTypeMap ?? new Dictionary<string, Type>();
 18839        _generatedProvider = generatedProvider;
 18840        _plugins = plugins ?? [];
 18841        _perAgentResilienceFactory = perAgentResilienceFactory;
 42
 34043        _lazyConfiguredOptions = new(() => BuildConfiguredOptions());
 34544        _lazyFunctionsCache = new(() => BuildFunctionsCache());
 18845    }
 46
 47    public AIAgent CreateAgent<TAgent>() where TAgent : class
 2748        => CreateAgentFromType(typeof(TAgent));
 49
 50    public AIAgent CreateAgent<TAgent>(Action<AgentFactoryOptions> configure) where TAgent : class
 51    {
 352        ArgumentNullException.ThrowIfNull(configure);
 253        return CreateAgentFromType(typeof(TAgent), configure);
 54    }
 55
 56    public AIAgent CreateAgent(string agentClassName)
 57    {
 22658        ArgumentNullException.ThrowIfNull(agentClassName);
 59
 22660        if (!_agentTypeMap.TryGetValue(agentClassName, out var type))
 261            throw new InvalidOperationException(
 262                $"No agent named '{agentClassName}' is registered. " +
 263                $"Ensure the class is decorated with [NeedlrAiAgent] and registered via " +
 264                $"AddAgent<T>(), AddAgentsFromGenerated(), or the source generator [ModuleInitializer].");
 65
 22466        return CreateAgentFromType(type);
 67    }
 68
 69    public AIAgent CreateAgent(string agentClassName, Action<AgentFactoryOptions> configure)
 70    {
 1571        ArgumentNullException.ThrowIfNull(agentClassName);
 1572        ArgumentNullException.ThrowIfNull(configure);
 73
 1474        if (!_agentTypeMap.TryGetValue(agentClassName, out var type))
 175            throw new InvalidOperationException(
 176                $"No agent named '{agentClassName}' is registered. " +
 177                $"Ensure the class is decorated with [NeedlrAiAgent] and registered via " +
 178                $"AddAgent<T>(), AddAgentsFromGenerated(), or the source generator [ModuleInitializer].");
 79
 1380        return CreateAgentFromType(type, configure);
 81    }
 82
 83    public IReadOnlyList<AITool> ResolveTools(Action<AgentFactoryOptions>? configure = null)
 84    {
 485        var agentOptions = new AgentFactoryOptions();
 486        configure?.Invoke(agentOptions);
 487        return ResolveToolsCore(agentOptions);
 88    }
 89
 90    public IReadOnlyList<AITool> ResolveTools<TAgent>() where TAgent : class
 191        => ResolveToolsFromType(typeof(TAgent));
 92
 93    public IReadOnlyList<AITool> ResolveTools<TAgent>(Action<AgentFactoryOptions> configure) where TAgent : class
 94    {
 095        ArgumentNullException.ThrowIfNull(configure);
 096        return ResolveToolsFromType(typeof(TAgent), configure);
 97    }
 98
 99    private IReadOnlyList<AITool> ResolveToolsFromType(
 100        Type agentType,
 101        Action<AgentFactoryOptions>? additionalConfigure = null)
 102    {
 1103        var attr = agentType.GetCustomAttribute<NeedlrAiAgentAttribute>()
 1104            ?? throw new InvalidOperationException(
 1105                $"'{agentType.Name}' is not decorated with [NeedlrAiAgent]. " +
 1106                $"Apply [NeedlrAiAgent] to the class or use ResolveTools(configure) to set options manually.");
 107
 1108        var agentOptions = new AgentFactoryOptions
 1109        {
 1110            FunctionTypes = attr.FunctionTypes,
 1111            FunctionGroups = attr.FunctionGroups,
 1112        };
 1113        additionalConfigure?.Invoke(agentOptions);
 114
 1115        return ResolveToolsCore(agentOptions);
 116    }
 117
 118    private IReadOnlyList<AITool> ResolveToolsCore(AgentFactoryOptions agentOptions)
 119    {
 341120        var allFunctions = _lazyFunctionsCache.Value;
 341121        var applicableTypes = GetApplicableTypes(agentOptions);
 341122        var tools = new List<AITool>();
 123
 1072124        foreach (var type in applicableTypes)
 125        {
 195126            if (allFunctions.TryGetValue(type, out var fns))
 127            {
 191128                tools.AddRange(fns);
 129            }
 130        }
 131
 341132        return tools;
 133    }
 134
 135    private AIAgent CreateAgentFromType(Type agentType, Action<AgentFactoryOptions>? additionalConfigure = null)
 136    {
 266137        var attr = agentType.GetCustomAttribute<NeedlrAiAgentAttribute>()
 266138            ?? throw new InvalidOperationException(
 266139                $"'{agentType.Name}' is not decorated with [NeedlrAiAgent]. " +
 266140                $"Apply [NeedlrAiAgent] to the class or use CreateAgent(configure) to set options manually.");
 141
 142        // Build a per-agent resilience plugin if [AgentResilience] is present on this type.
 264143        var resAttr = agentType.GetCustomAttribute<AgentResilienceAttribute>();
 264144        IAIAgentBuilderPlugin? perAgentPlugin = resAttr != null && _perAgentResilienceFactory != null
 264145            ? _perAgentResilienceFactory(resAttr)
 264146            : null;
 147
 264148        return CreateAgentCore(
 264149            additionalPlugins: perAgentPlugin != null ? [perAgentPlugin] : null,
 264150            configure: opts =>
 264151            {
 264152                // Populate from [NeedlrAiAgent] attribute as defaults
 264153                opts.Name = agentType.Name;
 264154                opts.Description = attr.Description;
 264155                opts.Instructions = attr.Instructions;
 264156                opts.FunctionTypes = attr.FunctionTypes;
 264157                opts.FunctionGroups = attr.FunctionGroups;
 264158
 264159                // Let the caller override any of the above
 264160                additionalConfigure?.Invoke(opts);
 279161            });
 162    }
 163
 164    public AIAgent CreateAgent(Action<AgentFactoryOptions>? configure = null)
 72165        => CreateAgentCore(additionalPlugins: null, configure: configure);
 166
 167    private AIAgent CreateAgentCore(
 168        IReadOnlyList<IAIAgentBuilderPlugin>? additionalPlugins,
 169        Action<AgentFactoryOptions>? configure)
 170    {
 336171        var agentOptions = new AgentFactoryOptions();
 336172        configure?.Invoke(agentOptions);
 173
 336174        var configuredOpts = _lazyConfiguredOptions.Value;
 336175        var tools = (IList<AITool>)ResolveToolsCore(agentOptions);
 176
 336177        var chatClient = configuredOpts.ChatClientFactory?.Invoke(_serviceProvider)
 336178            ?? _serviceProvider.GetRequiredService<IChatClient>();
 179
 180        // Apply per-agent middleware if configured
 336181        if (agentOptions.ChatClientFactory is { } perAgentFactory)
 182        {
 1183            chatClient = perAgentFactory(chatClient);
 184        }
 185
 336186        var instructions = agentOptions.Instructions ?? configuredOpts.DefaultInstructions;
 187
 336188        var rawAgent = chatClient.AsAIAgent(
 336189            name: agentOptions.Name,
 336190            description: agentOptions.Description,
 336191            instructions: instructions,
 336192            tools: tools,
 336193            services: _serviceProvider);
 194
 336195        return ApplyPlugins(rawAgent, additionalPlugins);
 196    }
 197
 198    private AIAgent ApplyPlugins(AIAgent rawAgent, IReadOnlyList<IAIAgentBuilderPlugin>? additionalPlugins)
 199    {
 200        // Merge global plugins with any per-agent additional plugins.
 201        // Per-agent plugins (e.g., [AgentResilience] overrides) are applied after global ones
 202        // so they take the outermost position in the middleware stack.
 336203        var allPlugins = additionalPlugins is { Count: > 0 }
 336204            ? [.. _plugins, .. additionalPlugins]
 336205            : _plugins;
 206
 336207        if (allPlugins.Count == 0)
 284208            return rawAgent;
 209
 52210        var builder = new AIAgentBuilder(rawAgent);
 220211        foreach (var plugin in allPlugins)
 212        {
 58213            plugin.Configure(new AIAgentBuilderPluginOptions { AgentBuilder = builder });
 214        }
 215
 52216        return builder.Build(_serviceProvider);
 217    }
 218
 219    private IEnumerable<Type> GetApplicableTypes(AgentFactoryOptions agentOptions)
 220    {
 341221        bool hasExplicitScope = agentOptions.FunctionTypes != null || agentOptions.FunctionGroups != null;
 222
 341223        if (!hasExplicitScope)
 297224            return _functionTypes;
 225
 44226        var types = new List<Type>();
 227
 44228        if (agentOptions.FunctionTypes != null)
 28229            types.AddRange(agentOptions.FunctionTypes);
 230
 44231        if (agentOptions.FunctionGroups != null)
 232        {
 66233            foreach (var group in agentOptions.FunctionGroups)
 234            {
 17235                if (_functionGroupMap.TryGetValue(group, out var groupTypes))
 13236                    types.AddRange(groupTypes);
 237            }
 238        }
 239
 44240        return types.Distinct();
 241    }
 242
 243    private AgentFrameworkConfigureOptions BuildConfiguredOptions()
 244    {
 152245        var opts = new AgentFrameworkConfigureOptions
 152246        {
 152247            ServiceProvider = _serviceProvider,
 152248        };
 249
 730250        foreach (var callback in _configureCallbacks)
 251        {
 213252            callback(opts);
 253        }
 254
 152255        return opts;
 256    }
 257
 258    private IReadOnlyDictionary<Type, IReadOnlyList<AIFunction>> BuildFunctionsCache()
 259    {
 157260        var dict = new Dictionary<Type, IReadOnlyList<AIFunction>>();
 261
 1112262        foreach (var type in _functionTypes)
 263        {
 399264            var functions = BuildFunctionsForType(type);
 399265            if (functions.Count > 0)
 266            {
 385267                dict[type] = functions;
 268            }
 269        }
 270
 157271        return dict;
 272    }
 273
 274    [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Reflection branch is unreachable when GeneratedAIFun
 275    [UnconditionalSuppressMessage("TrimAnalysis", "IL2026", Justification = "Reflection fallback path is unreachable in 
 276    [UnconditionalSuppressMessage("TrimAnalysis", "IL2067", Justification = "ActivatorUtilities.CreateInstance is only r
 277    private IReadOnlyList<AIFunction> BuildFunctionsForType(Type type)
 278    {
 399279        if (_generatedProvider?.TryGetFunctions(type, _serviceProvider, out var generated) == true)
 1280            return generated!;
 281
 398282        var isStatic = type.IsAbstract && type.IsSealed;
 398283        object? instance = isStatic
 398284            ? null
 398285            : ActivatorUtilities.CreateInstance(_serviceProvider, type);
 286
 398287        return BuildFunctionsForTypeViaReflection(type, isStatic, instance);
 288    }
 289
 290    [RequiresDynamicCode("Reflection-based AIFunction building requires dynamic code generation.")]
 291    [RequiresUnreferencedCode("Reflection-based AIFunction building requires unreferenced code access.")]
 292    private static IReadOnlyList<AIFunction> BuildFunctionsForTypeViaReflection(Type type, bool isStatic, object? instan
 293    {
 398294        var bindingFlags = isStatic
 398295            ? BindingFlags.Public | BindingFlags.Static
 398296            : BindingFlags.Public | BindingFlags.Instance;
 297
 398298        var functions = type.GetMethods(bindingFlags)
 1874299            .Where(m => m.IsDefined(typeof(AgentFunctionAttribute), inherit: true))
 418300            .Select(m => AIFunctionFactory.Create(m, target: instance))
 398301            .ToList();
 302
 398303        return functions.AsReadOnly();
 304    }
 305}

Methods/Properties

.ctor(System.IServiceProvider,System.Collections.Generic.List`1<System.Action`1<NexusLabs.Needlr.AgentFramework.AgentFrameworkConfigureOptions>>,System.Collections.Generic.IReadOnlyList`1<System.Type>,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Collections.Generic.IReadOnlyList`1<System.Type>>,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Type>,NexusLabs.Needlr.AgentFramework.IAIFunctionProvider,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.AgentFramework.IAIAgentBuilderPlugin>,System.Func`2<NexusLabs.Needlr.AgentFramework.AgentResilienceAttribute,NexusLabs.Needlr.AgentFramework.IAIAgentBuilderPlugin>)
CreateAgent()
CreateAgent(System.Action`1<NexusLabs.Needlr.AgentFramework.AgentFactoryOptions>)
CreateAgent(System.String)
CreateAgent(System.String,System.Action`1<NexusLabs.Needlr.AgentFramework.AgentFactoryOptions>)
ResolveTools(System.Action`1<NexusLabs.Needlr.AgentFramework.AgentFactoryOptions>)
ResolveTools()
ResolveToolsFromType(System.Type,System.Action`1<NexusLabs.Needlr.AgentFramework.AgentFactoryOptions>)
ResolveToolsCore(NexusLabs.Needlr.AgentFramework.AgentFactoryOptions)
CreateAgentFromType(System.Type,System.Action`1<NexusLabs.Needlr.AgentFramework.AgentFactoryOptions>)
CreateAgentCore(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.AgentFramework.IAIAgentBuilderPlugin>,System.Action`1<NexusLabs.Needlr.AgentFramework.AgentFactoryOptions>)
ApplyPlugins(Microsoft.Agents.AI.AIAgent,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.AgentFramework.IAIAgentBuilderPlugin>)
GetApplicableTypes(NexusLabs.Needlr.AgentFramework.AgentFactoryOptions)
BuildConfiguredOptions()
BuildFunctionsCache()
BuildFunctionsForType(System.Type)
BuildFunctionsForTypeViaReflection(System.Type,System.Boolean,System.Object)