< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Generators.AIFunctionProviderCodeGenerator
Assembly: NexusLabs.Needlr.AgentFramework.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Generators/CodeGen/AIFunctionProviderCodeGenerator.cs
Line coverage
78%
Covered lines: 184
Uncovered lines: 50
Coverable lines: 234
Total lines: 414
Line coverage: 78.6%
Branch coverage
66%
Covered branches: 89
Total branches: 134
Branch coverage: 66.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
GenerateAIFunctionProviderSource(...)93.75%1616100%
AppendAIFunctionNestedClass(...)100%2626100%
BuildJsonSchema(...)100%1010100%
BuildJsonSchemaTypeEntry(...)66.66%271870%
AppendParameterExtraction(...)40.9%2504452.63%
AppendIntegerExtraction(...)50%241463.15%
AppendNumberExtraction(...)0%2040%
GetArrayElementType(...)50%2266.66%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Generators/CodeGen/AIFunctionProviderCodeGenerator.cs

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System.Collections.Generic;
 5using System.Collections.Immutable;
 6using System.Linq;
 7using System.Text;
 8
 9namespace NexusLabs.Needlr.AgentFramework.Generators;
 10
 11internal static class AIFunctionProviderCodeGenerator
 12{
 13    public static string GenerateAIFunctionProviderSource(
 14        List<AgentFunctionTypeInfo> types,
 15        string safeAssemblyName)
 16    {
 10117        var sb = new StringBuilder();
 10118        sb.AppendLine("// <auto-generated/>");
 10119        sb.AppendLine("#nullable enable");
 10120        sb.AppendLine();
 10121        sb.AppendLine("using global::Microsoft.Extensions.DependencyInjection;");
 10122        sb.AppendLine();
 10123        sb.AppendLine($"namespace {safeAssemblyName}.Generated;");
 10124        sb.AppendLine();
 10125        sb.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.AgentFramework.Generat
 10126        sb.AppendLine("internal sealed class GeneratedAIFunctionProvider : global::NexusLabs.Needlr.AgentFramework.IAIFu
 10127        sb.AppendLine("{");
 10128        sb.AppendLine("    public bool TryGetFunctions(");
 10129        sb.AppendLine("        global::System.Type functionType,");
 10130        sb.AppendLine("        global::System.IServiceProvider serviceProvider,");
 10131        sb.AppendLine("        [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)]");
 10132        sb.AppendLine("        out global::System.Collections.Generic.IReadOnlyList<global::Microsoft.Extensions.AI.AIFu
 10133        sb.AppendLine("    {");
 34
 10135        if (types.Count == 0)
 36        {
 8137            sb.AppendLine("        functions = null;");
 8138            sb.AppendLine("        return false;");
 39        }
 40        else
 41        {
 2042            var first = true;
 8043            foreach (var type in types)
 44            {
 2045                var keyword = first ? "if" : "else if";
 2046                first = false;
 2047                sb.AppendLine($"        {keyword} (functionType == typeof({type.TypeName}))");
 2048                sb.AppendLine("        {");
 2049                if (!type.IsStatic)
 1850                    sb.AppendLine($"            var typed = serviceProvider.GetRequiredService<{type.TypeName}>();");
 2051                sb.AppendLine("            functions = new global::System.Collections.Generic.List<global::Microsoft.Ext
 2052                sb.AppendLine("            {");
 8053                foreach (var m in type.Methods)
 54                {
 2055                    var nestedName = $"{AgentDiscoveryHelper.GetShortName(type.TypeName)}_{m.MethodName}";
 2056                    if (type.IsStatic)
 257                        sb.AppendLine($"                new {nestedName}(),");
 58                    else
 1859                        sb.AppendLine($"                new {nestedName}(typed),");
 60                }
 2061                sb.AppendLine("            }.AsReadOnly();");
 2062                sb.AppendLine("            return true;");
 2063                sb.AppendLine("        }");
 64            }
 2065            sb.AppendLine("        functions = null;");
 2066            sb.AppendLine("        return false;");
 67        }
 68
 10169        sb.AppendLine("    }");
 10170        sb.AppendLine();
 71
 24272        foreach (var type in types)
 73        {
 2074            var shortTypeName = AgentDiscoveryHelper.GetShortName(type.TypeName);
 8075            foreach (var m in type.Methods)
 76            {
 2077                var nestedName = $"{shortTypeName}_{m.MethodName}";
 2078                AppendAIFunctionNestedClass(sb, type, m, nestedName);
 79            }
 80        }
 81
 10182        sb.AppendLine("}");
 10183        return sb.ToString();
 84    }
 85
 86    private static void AppendAIFunctionNestedClass(
 87        StringBuilder sb,
 88        AgentFunctionTypeInfo type,
 89        AgentFunctionMethodInfo method,
 90        string nestedClassName)
 91    {
 2092        sb.AppendLine($"    private sealed class {nestedClassName} : global::Microsoft.Extensions.AI.AIFunction");
 2093        sb.AppendLine("    {");
 94
 2095        if (!type.IsStatic)
 1896            sb.AppendLine($"        private readonly {type.TypeName} _instance;");
 97
 2098        var schemaJson = BuildJsonSchema(method.Parameters);
 2099        sb.AppendLine("        private static readonly global::System.Text.Json.JsonElement _schema =");
 20100        sb.AppendLine($"            global::System.Text.Json.JsonDocument.Parse(\"\"\"{schemaJson}\"\"\").RootElement.Cl
 101
 20102        if (!string.IsNullOrEmpty(method.ReturnJsonSchemaType))
 103        {
 16104            var returnSchemaJson = !string.IsNullOrEmpty(method.ReturnObjectSchemaJson)
 16105                ? method.ReturnObjectSchemaJson
 16106                : $"{{\"type\":\"{method.ReturnJsonSchemaType}\"}}";
 16107            sb.AppendLine("        private static readonly global::System.Text.Json.JsonElement _returnSchema =");
 16108            sb.AppendLine($"            global::System.Text.Json.JsonDocument.Parse(\"\"\"{returnSchemaJson}\"\"\").Root
 109        }
 110
 20111        sb.AppendLine();
 112
 20113        if (!type.IsStatic)
 18114            sb.AppendLine($"        public {nestedClassName}({type.TypeName} instance) {{ _instance = instance; }}");
 115        else
 2116            sb.AppendLine($"        public {nestedClassName}() {{ }}");
 117
 20118        sb.AppendLine();
 119
 20120        var escapedName = method.MethodName.Replace("\"", "\\\"");
 20121        var escapedDesc = method.Description.Replace("\\", "\\\\").Replace("\"", "\\\"");
 20122        sb.AppendLine($"        public override string Name => \"{escapedName}\";");
 20123        sb.AppendLine($"        public override string Description => \"{escapedDesc}\";");
 20124        sb.AppendLine("        public override global::System.Text.Json.JsonElement JsonSchema => _schema;");
 125
 20126        if (!string.IsNullOrEmpty(method.ReturnJsonSchemaType))
 127        {
 16128            sb.AppendLine("        public override global::System.Text.Json.JsonElement? ReturnJsonSchema => _returnSche
 129        }
 130
 20131        sb.AppendLine();
 132
 20133        if (method.IsAsync)
 3134            sb.AppendLine("        protected override async global::System.Threading.Tasks.ValueTask<object?> InvokeCore
 135        else
 17136            sb.AppendLine("        protected override global::System.Threading.Tasks.ValueTask<object?> InvokeCoreAsync(
 137
 20138        sb.AppendLine("            global::Microsoft.Extensions.AI.AIFunctionArguments arguments,");
 20139        sb.AppendLine("            global::System.Threading.CancellationToken ct)");
 20140        sb.AppendLine("        {");
 141
 74142        foreach (var param in method.Parameters)
 143        {
 17144            if (param.IsCancellationToken)
 145                continue;
 16146            AppendParameterExtraction(sb, param);
 147        }
 148
 37149        var paramList = string.Join(", ", method.Parameters.Select(p => p.IsCancellationToken ? "ct" : p.Name));
 20150        var instanceExpr = type.IsStatic ? type.TypeName : "_instance";
 151
 20152        if (method.IsAsync)
 153        {
 3154            if (method.IsVoidLike)
 155            {
 1156                sb.AppendLine($"            await {instanceExpr}.{method.MethodName}({paramList}).ConfigureAwait(false);
 1157                sb.AppendLine("            return null;");
 158            }
 159            else
 160            {
 2161                sb.AppendLine($"            var result = await {instanceExpr}.{method.MethodName}({paramList}).Configure
 2162                sb.AppendLine("            return result;");
 163            }
 164        }
 165        else
 166        {
 17167            if (method.IsVoidLike)
 168            {
 3169                sb.AppendLine($"            {instanceExpr}.{method.MethodName}({paramList});");
 3170                sb.AppendLine("            return global::System.Threading.Tasks.ValueTask.FromResult<object?>(null);");
 171            }
 172            else
 173            {
 14174                sb.AppendLine($"            var result = {instanceExpr}.{method.MethodName}({paramList});");
 14175                sb.AppendLine("            return global::System.Threading.Tasks.ValueTask.FromResult<object?>(result);"
 176            }
 177        }
 178
 20179        sb.AppendLine("        }");
 20180        sb.AppendLine("    }");
 20181        sb.AppendLine();
 20182    }
 183
 184    private static string BuildJsonSchema(ImmutableArray<AgentFunctionParameterInfo> parameters)
 185    {
 37186        var nonCtParams = parameters.Where(p => !p.IsCancellationToken).ToList();
 20187        if (nonCtParams.Count == 0)
 6188            return "{\"type\":\"object\",\"properties\":{}}";
 189
 14190        var props = new StringBuilder();
 14191        var required = new List<string>();
 192
 14193        props.Append("{");
 14194        var firstProp = true;
 60195        foreach (var param in nonCtParams)
 196        {
 18197            if (!firstProp) props.Append(",");
 16198            firstProp = false;
 16199            var escapedParamName = param.Name.Replace("\"", "\\\"");
 16200            props.Append($"\"{escapedParamName}\":");
 16201            props.Append(BuildJsonSchemaTypeEntry(param));
 16202            if (param.IsRequired)
 15203                required.Add(param.Name);
 204        }
 14205        props.Append("}");
 206
 14207        var sb = new StringBuilder();
 14208        sb.Append("{\"type\":\"object\",\"properties\":");
 14209        sb.Append(props);
 14210        if (required.Count > 0)
 211        {
 13212            sb.Append(",\"required\":[");
 28213            sb.Append(string.Join(",", required.Select(r => "\"" + r.Replace("\"", "\\\"") + "\"")));
 13214            sb.Append("]");
 215        }
 14216        sb.Append("}");
 14217        return sb.ToString();
 218    }
 219
 220    private static string BuildJsonSchemaTypeEntry(AgentFunctionParameterInfo param)
 221    {
 16222        if (string.IsNullOrEmpty(param.JsonSchemaType))
 0223            return "{}";
 224
 16225        if (param.JsonSchemaType == "array")
 226        {
 4227            var desc = string.IsNullOrEmpty(param.Description)
 4228                ? ""
 4229                : $",\"description\":\"{param.Description!.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"";
 230
 4231            if (param.ItemJsonSchemaType == "object" && !string.IsNullOrEmpty(param.ItemObjectSchemaJson))
 232            {
 233                // Complex object array items — emit the full object schema
 3234                return $"{{\"type\":\"array\",\"items\":{param.ItemObjectSchemaJson}{desc}}}";
 235            }
 236
 1237            if (!string.IsNullOrEmpty(param.ItemJsonSchemaType))
 1238                return $"{{\"type\":\"array\",\"items\":{{\"type\":\"{param.ItemJsonSchemaType}\"}}{desc}}}";
 239
 0240            return $"{{\"type\":\"array\"{desc}}}";
 241        }
 242
 12243        if (param.JsonSchemaType == "object")
 244        {
 245            // Direct complex object parameter (not in an array)
 0246            var desc = string.IsNullOrEmpty(param.Description)
 0247                ? ""
 0248                : $",\"description\":\"{param.Description!.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"";
 0249            return $"{{\"type\":\"object\"{desc}}}";
 250        }
 251
 12252        if (!string.IsNullOrEmpty(param.Description))
 253        {
 1254            var escapedDesc = param.Description!.Replace("\\", "\\\\").Replace("\"", "\\\"");
 1255            return $"{{\"type\":\"{param.JsonSchemaType}\",\"description\":\"{escapedDesc}\"}}";
 256        }
 257
 11258        return $"{{\"type\":\"{param.JsonSchemaType}\"}}";
 259    }
 260
 261    private static void AppendParameterExtraction(StringBuilder sb, AgentFunctionParameterInfo param)
 262    {
 16263        var rawVar = $"_raw_{param.Name}";
 16264        var jVar = $"_j_{param.Name}";
 16265        sb.AppendLine($"            arguments.TryGetValue(\"{param.Name}\", out var {rawVar});");
 266
 16267        switch (param.JsonSchemaType)
 268        {
 269            case "string":
 7270                sb.AppendLine($"            var {param.Name} = {rawVar} is global::System.Text.Json.JsonElement {jVar} ?
 7271                break;
 272            case "boolean":
 273            {
 0274                var bVar = $"_b_{param.Name}";
 0275                sb.AppendLine($"            var {param.Name} = {rawVar} is global::System.Text.Json.JsonElement {jVar} ?
 0276                break;
 277            }
 278            case "integer":
 5279                AppendIntegerExtraction(sb, param, rawVar, jVar);
 5280                break;
 281            case "number":
 0282                AppendNumberExtraction(sb, param, rawVar, jVar);
 0283                break;
 284            case "array":
 285            {
 286                // AOT-safe array extraction: manually enumerate JsonElement items
 287                // instead of using JsonSerializer.Deserialize<T[]> (which triggers
 288                // IL2026/IL3050 in NativeAOT builds).
 4289                var elementType = GetArrayElementType(param.TypeFullName);
 4290                sb.AppendLine($"            {param.TypeFullName} {param.Name};");
 4291                sb.AppendLine($"            if ({rawVar} is global::System.Text.Json.JsonElement {jVar} && {jVar}.ValueK
 4292                sb.AppendLine("            {");
 4293                sb.AppendLine($"                var _list_{param.Name} = new global::System.Collections.Generic.List<{el
 4294                sb.AppendLine($"                foreach (var _elem in {jVar}.EnumerateArray())");
 4295                sb.AppendLine("                {");
 296
 4297                if (param.ItemJsonSchemaType == "object" && param.ItemObjectProperties is { Count: > 0 })
 298                {
 299                    // Complex object: manual property extraction (AOT-safe)
 3300                    sb.AppendLine($"                    var _obj = new {elementType}();");
 18301                    foreach (var prop in param.ItemObjectProperties)
 302                    {
 6303                        sb.AppendLine($"                    if (_elem.TryGetProperty(\"{prop.JsonName}\", out var _p_{pr
 6304                        switch (prop.SchemaType)
 305                        {
 306                            case "string":
 6307                                sb.AppendLine($"                        _obj.{prop.CSharpName} = _p_{prop.JsonName}.GetS
 6308                                break;
 309                            case "integer":
 0310                                sb.AppendLine($"                        _obj.{prop.CSharpName} = _p_{prop.JsonName}.GetI
 0311                                break;
 312                            case "number":
 0313                                sb.AppendLine($"                        _obj.{prop.CSharpName} = _p_{prop.JsonName}.GetD
 0314                                break;
 315                            case "boolean":
 0316                                sb.AppendLine($"                        _obj.{prop.CSharpName} = _p_{prop.JsonName}.GetB
 0317                                break;
 318                            default:
 0319                                sb.AppendLine($"                        _obj.{prop.CSharpName} = _p_{prop.JsonName}.GetS
 320                                break;
 321                        }
 322                    }
 3323                    sb.AppendLine($"                    _list_{param.Name}.Add(_obj);");
 324                }
 1325                else if (param.ItemJsonSchemaType == "string")
 1326                    sb.AppendLine($"                    _list_{param.Name}.Add(_elem.GetString() ?? \"\");");
 0327                else if (param.ItemJsonSchemaType == "integer")
 0328                    sb.AppendLine($"                    _list_{param.Name}.Add(_elem.GetInt32());");
 0329                else if (param.ItemJsonSchemaType == "number")
 0330                    sb.AppendLine($"                    _list_{param.Name}.Add(_elem.GetDouble());");
 0331                else if (param.ItemJsonSchemaType == "boolean")
 0332                    sb.AppendLine($"                    _list_{param.Name}.Add(_elem.GetBoolean());");
 333                else
 0334                    sb.AppendLine($"                    _list_{param.Name}.Add(default!);");
 335
 4336                sb.AppendLine("                }");
 4337                sb.AppendLine($"                {param.Name} = _list_{param.Name}.ToArray();");
 4338                sb.AppendLine("            }");
 4339                sb.AppendLine($"            else {{ {param.Name} = {rawVar} as {param.TypeFullName} ?? global::System.Ar
 4340                break;
 341            }
 342            case "object":
 343            {
 0344                var cVar = $"_c_{param.Name}";
 0345                var nullSuppress = param.IsNullable ? "" : "!";
 0346                sb.AppendLine($"            var {param.Name} = {rawVar} is {param.TypeFullName} {cVar} ? {cVar} : defaul
 0347                break;
 348            }
 349            default:
 350            {
 0351                var cVar = $"_c_{param.Name}";
 0352                var nullSuppress = param.IsNullable ? "" : "!";
 0353                sb.AppendLine($"            var {param.Name} = {rawVar} is {param.TypeFullName} {cVar} ? {cVar} : defaul
 354                break;
 355            }
 356        }
 0357    }
 358
 359    private static void AppendIntegerExtraction(StringBuilder sb, AgentFunctionParameterInfo param, string rawVar, strin
 360    {
 5361        var typeFqn = param.TypeFullName;
 362        string getMethod, castType, convertMethod;
 5363        var iVar = $"_i_{param.Name}";
 364
 5365        if (typeFqn.Contains("System.Int64"))
 0366        { getMethod = "GetInt64()"; castType = "long"; convertMethod = "ToInt64"; }
 5367        else if (typeFqn.Contains("System.Int16"))
 0368        { getMethod = "GetInt16()"; castType = "short"; convertMethod = "ToInt16"; }
 5369        else if (typeFqn.Contains("System.SByte"))
 0370        { getMethod = "GetSByte()"; castType = "sbyte"; convertMethod = "ToSByte"; }
 5371        else if (typeFqn.Contains("System.Byte"))
 0372        { getMethod = "GetByte()"; castType = "byte"; convertMethod = "ToByte"; }
 5373        else if (typeFqn.Contains("System.UInt64"))
 0374        { getMethod = "GetUInt64()"; castType = "ulong"; convertMethod = "ToUInt64"; }
 5375        else if (typeFqn.Contains("System.UInt32"))
 0376        { getMethod = "GetUInt32()"; castType = "uint"; convertMethod = "ToUInt32"; }
 5377        else if (typeFqn.Contains("System.UInt16"))
 0378        { getMethod = "GetUInt16()"; castType = "ushort"; convertMethod = "ToUInt16"; }
 379        else
 15380        { getMethod = "GetInt32()"; castType = "int"; convertMethod = "ToInt32"; }
 381
 5382        sb.AppendLine($"            var {param.Name} = {rawVar} is global::System.Text.Json.JsonElement {jVar} ? {jVar}.
 5383    }
 384
 385    private static void AppendNumberExtraction(StringBuilder sb, AgentFunctionParameterInfo param, string rawVar, string
 386    {
 0387        var typeFqn = param.TypeFullName;
 388        string getMethod, castType, convertMethod;
 0389        var nVar = $"_n_{param.Name}";
 390
 0391        if (typeFqn.Contains("System.Single"))
 0392        { getMethod = "GetSingle()"; castType = "float"; convertMethod = "ToSingle"; }
 0393        else if (typeFqn.Contains("System.Decimal"))
 0394        { getMethod = "GetDecimal()"; castType = "decimal"; convertMethod = "ToDecimal"; }
 395        else
 0396        { getMethod = "GetDouble()"; castType = "double"; convertMethod = "ToDouble"; }
 397
 0398        sb.AppendLine($"            var {param.Name} = {rawVar} is global::System.Text.Json.JsonElement {jVar} ? {jVar}.
 0399    }
 400
 401    /// <summary>
 402    /// Extracts the element type from an array type's fully-qualified name.
 403    /// E.g., <c>"global::MyNamespace.FeedbackEntry[]"</c> → <c>"global::MyNamespace.FeedbackEntry"</c>.
 404    /// </summary>
 405    private static string GetArrayElementType(string arrayTypeFullName)
 406    {
 407        // Handle T[] syntax
 4408        if (arrayTypeFullName.EndsWith("[]"))
 4409            return arrayTypeFullName.Substring(0, arrayTypeFullName.Length - 2);
 410
 411        // Fallback: return as-is (shouldn't happen for valid array types)
 0412        return arrayTypeFullName;
 413    }
 414}