| | | 1 | | using Microsoft.Agents.AI; |
| | | 2 | | using Microsoft.Extensions.AI; |
| | | 3 | | using Microsoft.Extensions.DependencyInjection; |
| | | 4 | | |
| | | 5 | | using System.Diagnostics.CodeAnalysis; |
| | | 6 | | using System.Reflection; |
| | | 7 | | |
| | | 8 | | namespace NexusLabs.Needlr.AgentFramework; |
| | | 9 | | |
| | | 10 | | internal 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 | | |
| | | 20 | | private readonly IReadOnlyDictionary<string, Type> _agentTypeMap; |
| | | 21 | | |
| | 65 | 22 | | public AgentFactory( |
| | 65 | 23 | | IServiceProvider serviceProvider, |
| | 65 | 24 | | List<Action<AgentFrameworkConfigureOptions>> configureCallbacks, |
| | 65 | 25 | | IReadOnlyList<Type> functionTypes, |
| | 65 | 26 | | IReadOnlyDictionary<string, IReadOnlyList<Type>>? functionGroupMap = null, |
| | 65 | 27 | | IReadOnlyDictionary<string, Type>? agentTypeMap = null, |
| | 65 | 28 | | IAIFunctionProvider? generatedProvider = null) |
| | | 29 | | { |
| | 65 | 30 | | _serviceProvider = serviceProvider; |
| | 65 | 31 | | _configureCallbacks = configureCallbacks; |
| | 65 | 32 | | _functionTypes = functionTypes; |
| | 65 | 33 | | _functionGroupMap = functionGroupMap ?? new Dictionary<string, IReadOnlyList<Type>>(); |
| | 65 | 34 | | _agentTypeMap = agentTypeMap ?? new Dictionary<string, Type>(); |
| | 65 | 35 | | _generatedProvider = generatedProvider; |
| | | 36 | | |
| | 118 | 37 | | _lazyConfiguredOptions = new(() => BuildConfiguredOptions()); |
| | 118 | 38 | | _lazyFunctionsCache = new(() => BuildFunctionsCache()); |
| | 65 | 39 | | } |
| | | 40 | | |
| | | 41 | | public AIAgent CreateAgent<TAgent>() where TAgent : class |
| | 17 | 42 | | => CreateAgentFromType(typeof(TAgent)); |
| | | 43 | | |
| | | 44 | | public AIAgent CreateAgent(string agentClassName) |
| | | 45 | | { |
| | 52 | 46 | | ArgumentNullException.ThrowIfNull(agentClassName); |
| | | 47 | | |
| | 52 | 48 | | if (!_agentTypeMap.TryGetValue(agentClassName, out var type)) |
| | 2 | 49 | | throw new InvalidOperationException( |
| | 2 | 50 | | $"No agent named '{agentClassName}' is registered. " + |
| | 2 | 51 | | $"Ensure the class is decorated with [NeedlrAiAgent] and registered via " + |
| | 2 | 52 | | $"AddAgent<T>(), AddAgentsFromGenerated(), or the source generator [ModuleInitializer]."); |
| | | 53 | | |
| | 50 | 54 | | return CreateAgentFromType(type); |
| | | 55 | | } |
| | | 56 | | |
| | | 57 | | private AIAgent CreateAgentFromType(Type agentType) |
| | | 58 | | { |
| | 67 | 59 | | var attr = agentType.GetCustomAttribute<NeedlrAiAgentAttribute>() |
| | 67 | 60 | | ?? throw new InvalidOperationException( |
| | 67 | 61 | | $"'{agentType.Name}' is not decorated with [NeedlrAiAgent]. " + |
| | 67 | 62 | | $"Apply [NeedlrAiAgent] to the class or use CreateAgent(configure) to set options manually."); |
| | | 63 | | |
| | 65 | 64 | | return CreateAgent(opts => |
| | 65 | 65 | | { |
| | 65 | 66 | | opts.Name = agentType.Name; |
| | 65 | 67 | | opts.Description = attr.Description; |
| | 65 | 68 | | opts.Instructions = attr.Instructions; |
| | 65 | 69 | | opts.FunctionTypes = attr.FunctionTypes; |
| | 65 | 70 | | opts.FunctionGroups = attr.FunctionGroups; |
| | 130 | 71 | | }); |
| | | 72 | | } |
| | | 73 | | |
| | | 74 | | public AIAgent CreateAgent(Action<AgentFactoryOptions>? configure = null) |
| | | 75 | | { |
| | 104 | 76 | | var agentOptions = new AgentFactoryOptions(); |
| | 104 | 77 | | configure?.Invoke(agentOptions); |
| | | 78 | | |
| | 104 | 79 | | var configuredOpts = _lazyConfiguredOptions.Value; |
| | 104 | 80 | | var allFunctions = _lazyFunctionsCache.Value; |
| | | 81 | | |
| | 104 | 82 | | var applicableTypes = GetApplicableTypes(agentOptions); |
| | 104 | 83 | | var tools = new List<AITool>(); |
| | | 84 | | |
| | 314 | 85 | | foreach (var type in applicableTypes) |
| | | 86 | | { |
| | 53 | 87 | | if (allFunctions.TryGetValue(type, out var fns)) |
| | | 88 | | { |
| | 50 | 89 | | tools.AddRange(fns); |
| | | 90 | | } |
| | | 91 | | } |
| | | 92 | | |
| | 104 | 93 | | var chatClient = configuredOpts.ChatClientFactory?.Invoke(_serviceProvider) |
| | 104 | 94 | | ?? _serviceProvider.GetRequiredService<IChatClient>(); |
| | | 95 | | |
| | 104 | 96 | | var instructions = agentOptions.Instructions ?? configuredOpts.DefaultInstructions; |
| | | 97 | | |
| | 104 | 98 | | return chatClient.AsAIAgent( |
| | 104 | 99 | | name: agentOptions.Name, |
| | 104 | 100 | | description: agentOptions.Description, |
| | 104 | 101 | | instructions: instructions, |
| | 104 | 102 | | tools: tools, |
| | 104 | 103 | | services: _serviceProvider); |
| | | 104 | | } |
| | | 105 | | |
| | | 106 | | private IEnumerable<Type> GetApplicableTypes(AgentFactoryOptions agentOptions) |
| | | 107 | | { |
| | 104 | 108 | | bool hasExplicitScope = agentOptions.FunctionTypes != null || agentOptions.FunctionGroups != null; |
| | | 109 | | |
| | 104 | 110 | | if (!hasExplicitScope) |
| | 66 | 111 | | return _functionTypes; |
| | | 112 | | |
| | 38 | 113 | | var types = new List<Type>(); |
| | | 114 | | |
| | 38 | 115 | | if (agentOptions.FunctionTypes != null) |
| | 24 | 116 | | types.AddRange(agentOptions.FunctionTypes); |
| | | 117 | | |
| | 38 | 118 | | if (agentOptions.FunctionGroups != null) |
| | | 119 | | { |
| | 58 | 120 | | foreach (var group in agentOptions.FunctionGroups) |
| | | 121 | | { |
| | 15 | 122 | | if (_functionGroupMap.TryGetValue(group, out var groupTypes)) |
| | 11 | 123 | | types.AddRange(groupTypes); |
| | | 124 | | } |
| | | 125 | | } |
| | | 126 | | |
| | 38 | 127 | | return types.Distinct(); |
| | | 128 | | } |
| | | 129 | | |
| | | 130 | | private AgentFrameworkConfigureOptions BuildConfiguredOptions() |
| | | 131 | | { |
| | 53 | 132 | | var opts = new AgentFrameworkConfigureOptions |
| | 53 | 133 | | { |
| | 53 | 134 | | ServiceProvider = _serviceProvider, |
| | 53 | 135 | | }; |
| | | 136 | | |
| | 212 | 137 | | foreach (var callback in _configureCallbacks) |
| | | 138 | | { |
| | 53 | 139 | | callback(opts); |
| | | 140 | | } |
| | | 141 | | |
| | 53 | 142 | | return opts; |
| | | 143 | | } |
| | | 144 | | |
| | | 145 | | private IReadOnlyDictionary<Type, IReadOnlyList<AIFunction>> BuildFunctionsCache() |
| | | 146 | | { |
| | 53 | 147 | | var dict = new Dictionary<Type, IReadOnlyList<AIFunction>>(); |
| | | 148 | | |
| | 468 | 149 | | foreach (var type in _functionTypes) |
| | | 150 | | { |
| | 181 | 151 | | var functions = BuildFunctionsForType(type); |
| | 181 | 152 | | if (functions.Count > 0) |
| | | 153 | | { |
| | 171 | 154 | | dict[type] = functions; |
| | | 155 | | } |
| | | 156 | | } |
| | | 157 | | |
| | 53 | 158 | | return dict; |
| | | 159 | | } |
| | | 160 | | |
| | | 161 | | [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Reflection branch is unreachable when GeneratedAIFun |
| | | 162 | | [UnconditionalSuppressMessage("TrimAnalysis", "IL2026", Justification = "Reflection fallback path is unreachable in |
| | | 163 | | [UnconditionalSuppressMessage("TrimAnalysis", "IL2067", Justification = "ActivatorUtilities.CreateInstance is only r |
| | | 164 | | private IReadOnlyList<AIFunction> BuildFunctionsForType(Type type) |
| | | 165 | | { |
| | 181 | 166 | | if (_generatedProvider?.TryGetFunctions(type, _serviceProvider, out var generated) == true) |
| | 1 | 167 | | return generated!; |
| | | 168 | | |
| | 180 | 169 | | var isStatic = type.IsAbstract && type.IsSealed; |
| | 180 | 170 | | object? instance = isStatic |
| | 180 | 171 | | ? null |
| | 180 | 172 | | : ActivatorUtilities.CreateInstance(_serviceProvider, type); |
| | | 173 | | |
| | 180 | 174 | | return BuildFunctionsForTypeViaReflection(type, isStatic, instance); |
| | | 175 | | } |
| | | 176 | | |
| | | 177 | | [RequiresDynamicCode("Reflection-based AIFunction building requires dynamic code generation.")] |
| | | 178 | | [RequiresUnreferencedCode("Reflection-based AIFunction building requires unreferenced code access.")] |
| | | 179 | | private static IReadOnlyList<AIFunction> BuildFunctionsForTypeViaReflection(Type type, bool isStatic, object? instan |
| | | 180 | | { |
| | 180 | 181 | | var bindingFlags = isStatic |
| | 180 | 182 | | ? BindingFlags.Public | BindingFlags.Static |
| | 180 | 183 | | : BindingFlags.Public | BindingFlags.Instance; |
| | | 184 | | |
| | 180 | 185 | | var functions = type.GetMethods(bindingFlags) |
| | 826 | 186 | | .Where(m => m.IsDefined(typeof(AgentFunctionAttribute), inherit: true)) |
| | 170 | 187 | | .Select(m => AIFunctionFactory.Create(m, target: instance)) |
| | 180 | 188 | | .ToList(); |
| | | 189 | | |
| | 180 | 190 | | return functions.AsReadOnly(); |
| | | 191 | | } |
| | | 192 | | } |