< Summary

Information
Class: UIBlazor.Processors.SchemaProcessor
Assembly: UIBlazor
File(s): /home/runner/work/InvAit/InvAit/UIBlazor/Processors/SchemaProcessor.cs
Tag: 71_26091983037
Line coverage
94%
Covered lines: 164
Uncovered lines: 10
Coverable lines: 174
Total lines: 388
Line coverage: 94.2%
Branch coverage
92%
Covered branches: 199
Total branches: 216
Branch coverage: 92.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
DeserializeSchema(...)100%4444100%
BuildSchemaDescription(...)100%2287.5%
AppendSchemaDescription(...)90%5050100%
GenerateExample(...)83.33%201883.33%
GetEnumOrDefault(...)100%44100%
GenerateObjectExample(...)100%44100%
ValidateAndConvertArguments(...)100%1010100%
ConvertValueBySchema(...)87.5%323292.85%
TryConvertTo(...)100%161689.47%
ConvertArrayValue(...)89.28%282892%
ConvertObjectValue(...)75%8887.5%

File(s)

/home/runner/work/InvAit/InvAit/UIBlazor/Processors/SchemaProcessor.cs

#LineLine coverage
 1using System.Collections;
 2using Shared.Contracts.Mcp;
 3
 4namespace UIBlazor.Processors;
 5
 6public static class SchemaProcessor
 7{
 8    private const int _maxDepth = 5;
 9
 10    /// <summary>
 11    /// Deserializes a JsonElement representing a JSON Schema into a JsonSchemaProperty object graph.
 12    /// </summary>
 13    /// <param name="schemaElement">The JsonElement containing the schema.</param>
 14    /// <param name="currentDepth">The current recursion depth (used internally).</param>
 15    /// <param name="maxDepth">The maximum allowed nesting depth.</param>
 16    /// <returns>A JsonSchemaProperty object representing the schema, or null if the input is invalid.</returns>
 17    public static JsonSchemaProperty? DeserializeSchema(JsonElement? schemaElement, int currentDepth = 0, int maxDepth =
 18    {
 9719        if (!schemaElement.HasValue || schemaElement.Value.ValueKind != JsonValueKind.Object)
 20        {
 3321            return null;
 22        }
 23
 6424        if (currentDepth > maxDepth)
 25        {
 226            return null;
 27        }
 28
 6229        var schema = schemaElement.Value;
 6230        var property = new JsonSchemaProperty();
 31
 6232        if (schema.TryGetProperty("type", out var typeElement))
 33        {
 5234            property.Type = typeElement.GetString();
 35        }
 36
 6237        if (schema.TryGetProperty("description", out var descriptionElement))
 38        {
 539            property.Description = descriptionElement.GetString();
 40        }
 41
 42        // Handle constraints
 6243        if (schema.TryGetProperty("enum", out var enumElement) && enumElement.ValueKind == JsonValueKind.Array)
 44        {
 645            property.EnumValues = [];
 4846            foreach (var val in enumElement.EnumerateArray())
 47            {
 48                // Store raw values, conversion happens later if needed
 1849                property.EnumValues.Add(val.Clone());
 50            }
 51        }
 52
 6253        if (schema.TryGetProperty("minimum", out var minElement))
 54        {
 355            property.Minimum = minElement.GetDouble();
 56        }
 6257        if (schema.TryGetProperty("maximum", out var maxElement))
 58        {
 359            property.Maximum = maxElement.GetDouble();
 60        }
 6261        if (schema.TryGetProperty("minLength", out var minLengthElement))
 62        {
 363            property.MinLength = minLengthElement.GetInt32();
 64        }
 6265        if (schema.TryGetProperty("maxLength", out var maxLengthElement))
 66        {
 367            property.MaxLength = maxLengthElement.GetInt32();
 68        }
 6269        if (schema.TryGetProperty("pattern", out var patternElement))
 70        {
 271            property.Pattern = patternElement.GetString();
 72        }
 6273        if (schema.TryGetProperty("required", out var requiredElement) && requiredElement.ValueKind == JsonValueKind.Arr
 74        {
 1375            property.Required = [.. requiredElement.EnumerateArray().Select(r => r.ToString())];
 76        }
 77
 78        // Recursively handle nested structures
 6279        if (property.Type == "object" && schema.TryGetProperty("properties", out var propsElement) && propsElement.Value
 80        {
 1081            property.Properties = [];
 5482            foreach (var prop in propsElement.EnumerateObject())
 83            {
 1784                var nestedProp = DeserializeSchema(prop.Value, currentDepth + 1, maxDepth);
 1785                if (nestedProp != null)
 86                {
 1787                    property.Properties[prop.Name] = nestedProp;
 88                }
 89            }
 90        }
 91
 6292        if (property.Type == "array" && schema.TryGetProperty("items", out var itemsElement))
 93        {
 594            property.Items = DeserializeSchema(itemsElement, currentDepth + 1, maxDepth);
 95        }
 96
 6297        return property;
 98    }
 99
 100    /// <summary>
 101    /// Build a readable schema description for LLM prompt
 102    /// </summary>
 103    public static string BuildSchemaDescription(string toolName, McpToolConfig toolConfig)
 104    {
 105        try
 106        {
 27107            var schemaElement = toolConfig.InputSchema;
 27108            var schemaProperty = DeserializeSchema(schemaElement);
 27109            if (schemaProperty == null)
 110            {
 24111                return string.Empty;
 112            }
 113
 3114            var sb = new StringBuilder();
 3115            var exampleObj = GenerateObjectExample(schemaProperty);
 116
 3117            sb.AppendLine("For example:");
 3118            sb.AppendLine($"<function name=\"{toolName}\">");
 3119            sb.AppendLine(JsonUtils.Serialize(exampleObj));
 3120            sb.AppendLine("</function>");
 121
 3122            sb.AppendLine("*Properties schema:*");
 3123            AppendSchemaDescription(sb, schemaProperty, parentPath: "");
 124
 3125            return sb.ToString();
 126        }
 0127        catch (Exception)
 128        {
 0129            return string.Empty;
 130        }
 27131    }
 132
 133    public static void AppendSchemaDescription(StringBuilder sb, JsonSchemaProperty prop, string parentPath, int depth =
 134    {
 23135        if (prop.Properties != null)
 136        {
 78137            foreach (var nestedPropKvp in prop.Properties)
 138            {
 20139                var required = prop.Required is not null
 20140                    ? prop.Required.Contains(nestedPropKvp.Key)
 20141                    : false;
 20142                var currentPath = string.IsNullOrEmpty(parentPath) ? nestedPropKvp.Key : $"{parentPath}.{nestedPropKvp.K
 20143                var nestedProp = nestedPropKvp.Value;
 144
 20145                var typeInfo = nestedProp.Type ?? "unknown";
 20146                if (nestedProp.EnumValues != null && nestedProp.EnumValues.Count > 0)
 147                {
 7148                    typeInfo += $" (enum: {string.Join(", ", nestedProp.EnumValues.Select(v => v.ToString()))})";
 149                }
 22150                if (nestedProp.Minimum.HasValue) typeInfo += $" (min: {nestedProp.Minimum.Value})";
 22151                if (nestedProp.Maximum.HasValue) typeInfo += $" (max: {nestedProp.Maximum.Value})";
 22152                if (nestedProp.MinLength.HasValue) typeInfo += $" (minLen: {nestedProp.MinLength.Value})";
 22153                if (nestedProp.MaxLength.HasValue) typeInfo += $" (maxLen: {nestedProp.MaxLength.Value})";
 22154                if (!string.IsNullOrEmpty(nestedProp.Pattern)) typeInfo += $" (pattern: {nestedProp.Pattern})";
 155
 20156                var requiredStr = required ? "REQUIRED" : "(optional)";
 20157                sb.AppendLine($"{currentPath} : [{typeInfo}] {nestedProp.Description ?? ""} {requiredStr}");
 158
 159                // Recurse for nested objects
 20160                if (nestedProp.Type?.ToLowerInvariant() == "object")
 161                {
 3162                    AppendSchemaDescription(sb, nestedProp, currentPath, depth + 1);
 163                }
 164            }
 165        }
 4166        else if (prop.Type?.ToLowerInvariant() == "array" && prop.Items != null)
 167        {
 168            // Describe the array item type
 3169            var itemTypeInfo = prop.Items.Type ?? "unknown";
 3170            if (prop.Items.EnumValues != null && prop.Items.EnumValues.Count > 0)
 171            {
 3172                itemTypeInfo += $" (enum: {string.Join(", ", prop.Items.EnumValues.Select(v => v.ToString()))})";
 173            }
 174            // ... potentially add minItems, maxItems if schema defines them ...
 3175            sb.AppendLine($"{parentPath}[] : [array of {itemTypeInfo}] {prop.Description ?? ""}");
 176
 177            // If the item itself is an object, recurse
 3178            if (prop.Items.Type?.ToLowerInvariant() == "object")
 179            {
 1180                var itemPath = $"{parentPath}[]_item"; // Represent the item placeholder
 1181                AppendSchemaDescription(sb, prop.Items, itemPath, depth + 1);
 182            }
 183        }
 23184    }
 185
 186    /// <summary>
 187    /// Генерирует объект для передачи в MCP с заполненными данными для простого примера
 188    /// </summary>
 189    public static object? GenerateExample(JsonSchemaProperty? schemaProperty, int depth = 0)
 190    {
 28191        if (schemaProperty == null || depth > _maxDepth)
 192        {
 2193            return new Dictionary<string, object?>();
 194        }
 195
 26196        return schemaProperty.Type?.ToLowerInvariant() switch
 26197        {
 8198            "string" => GetEnumOrDefault(schemaProperty, "sample_string"),
 5199            "number" => GetEnumOrDefault(schemaProperty, 0.1),
 6200            "integer" => GetEnumOrDefault(schemaProperty, 0),
 5201            "boolean" => GetEnumOrDefault(schemaProperty, true),
 0202            "array" => new List<object?> { GenerateExample(schemaProperty.Items, depth + 1) },
 2203            "object" => GenerateObjectExample(schemaProperty, depth),
 0204            _ => "unknown_type"
 26205        };
 206    }
 207
 208    private static object? GetEnumOrDefault(JsonSchemaProperty schemaProperty, object defaultValue)
 209    {
 24210        return schemaProperty.EnumValues is { Count: > 0 }
 24211            ? $"One of strings: {string.Join(',', schemaProperty.EnumValues)}"
 24212            : defaultValue;
 213    }
 214
 215    private static Dictionary<string, object?> GenerateObjectExample(JsonSchemaProperty schemaProperty, int depth = 0)
 216    {
 5217        var obj = new Dictionary<string, object?>();
 5218        if (schemaProperty.Properties != null)
 219        {
 20220            foreach (var (key, value) in schemaProperty.Properties)
 221            {
 6222                obj[key] = GenerateExample(value, depth + 1);
 223            }
 224        }
 5225        return obj;
 226    }
 227
 228    /// <summary>
 229    /// Validates and converts arguments based on the schema definition.
 230    /// </summary>
 231    /// <param name="schemaProperty">The root schema property definition.</param>
 232    /// <param name="inputArgs">The input arguments dictionary.</param>
 233    /// <param name="currentDepth">The current recursion depth (used internally).</param>
 234    /// <param name="maxDepth">The maximum allowed nesting depth.</param>
 235    /// <returns>A dictionary with validated and correctly typed arguments.</returns>
 236    public static Dictionary<string, object> ValidateAndConvertArguments(JsonSchemaProperty? schemaProperty, IReadOnlyDi
 237    {
 53238        var result = new Dictionary<string, object>();
 239
 53240        if (schemaProperty?.Properties == null || currentDepth > maxDepth)
 241        {
 242            // If no properties defined or max depth reached, pass through original args or return empty
 243            // For MCP, we likely want to pass through only known valid keys if possible, otherwise just return input on
 244            // Returning empty here if no properties are defined might be safer depending on use case.
 245            // Let's assume if properties are null, we cannot validate further and return empty.
 4246            return result;
 247        }
 248
 200249        foreach (var propDefKvp in schemaProperty.Properties)
 250        {
 51251            var argName = propDefKvp.Key;
 51252            var propDef = propDefKvp.Value;
 253
 51254            if (inputArgs.TryGetValue(argName, out var inputValue))
 255            {
 49256                result[argName] = ConvertValueBySchema(inputValue, propDef, currentDepth, maxDepth);
 257            }
 258        }
 259
 49260        return result;
 261    }
 262
 263    private static object ConvertValueBySchema(object inputValue, JsonSchemaProperty propDef, int currentDepth, int maxD
 264    {
 55265        if (currentDepth > maxDepth)
 266        {
 0267            return inputValue; // Return as is if max depth hit
 268        }
 269
 55270        var expectedType = propDef.Type?.ToLowerInvariant();
 55271        var convertedValue = expectedType switch
 55272        {
 14273            "integer" => TryConvertTo<int>(inputValue, out var intVal) ? (object?)intVal ?? inputValue : inputValue,
 5274            "number" => TryConvertTo<double>(inputValue, out var doubleVal) ? (object?)doubleVal ?? inputValue : inputVa
 15275            "boolean" => TryConvertTo<bool>(inputValue, out var boolVal) ? (object?)boolVal ?? inputValue : inputValue,
 9276            "string" => inputValue?.ToString() ?? string.Empty,
 5277            "array" => ConvertArrayValue(inputValue, propDef.Items, currentDepth, maxDepth),
 5278            "object" => ConvertObjectValue(inputValue, propDef, currentDepth, maxDepth),
 2279            _ => inputValue // Default: return as is if type is unknown or null
 55280        };
 281
 55282        return convertedValue;
 283    }
 284
 285    private static bool TryConvertTo<T>(object value, out T? result) where T : struct
 286    {
 34287        result = null;
 288        try
 289        {
 34290            if (value is T directValue)
 291            {
 4292                result = directValue;
 4293                return true;
 294            }
 30295            else if (value is string stringValue)
 296            {
 23297                if (typeof(T) == typeof(int) && int.TryParse(stringValue, out var intParsed))
 298                {
 2299                    result = (T)(object)intParsed;
 2300                    return true;
 301                }
 21302                else if (typeof(T) == typeof(double) && double.TryParse(stringValue, out var doubleParsed))
 303                {
 1304                    result = (T)(object)doubleParsed;
 1305                    return true;
 306                }
 20307                else if (typeof(T) == typeof(bool) && bool.TryParse(stringValue, out var boolParsed))
 308                {
 6309                    result = (T)(object)boolParsed;
 6310                    return true;
 311                }
 312            }
 21313        }
 0314        catch
 315        {
 316            // Parsing failed
 0317        }
 21318        return false;
 13319    }
 320
 321    private static object ConvertArrayValue(object inputValue, JsonSchemaProperty? itemSchema, int currentDepth, int max
 322    {
 5323        if (inputValue is JsonElement jsonEl && jsonEl.ValueKind == JsonValueKind.Array)
 324        {
 1325            var list = new List<object>();
 8326            foreach (var item in jsonEl.EnumerateArray())
 327            {
 3328                var convertedItem = itemSchema != null ? ConvertValueBySchema(item, itemSchema, currentDepth + 1, maxDep
 3329                list.Add(convertedItem);
 330            }
 1331            return list;
 332        }
 4333        else if (inputValue is IEnumerable enumerable && !(inputValue is string))
 334        {
 1335            var list = new List<object>();
 8336            foreach (var item in enumerable)
 337            {
 3338                var convertedItem = itemSchema != null ? ConvertValueBySchema(item!, itemSchema, currentDepth + 1, maxDe
 3339                list.Add(convertedItem);
 340            }
 1341            return list;
 342        }
 3343        else if (inputValue is string inputValueStr)
 344        {
 3345            var trimmed = inputValueStr.Trim();
 3346            if (trimmed.StartsWith('[') && trimmed.EndsWith(']'))
 347            {
 348                try
 349                {
 2350                    var deInputValueStr = JsonSerializer.Deserialize<IEnumerable<object>>(trimmed) ?? [];
 2351                    var list = new List<object>();
 16352                    foreach (var item in deInputValueStr)
 353                    {
 6354                        var convertedItem = itemSchema != null ? ConvertValueBySchema(item!, itemSchema, currentDepth + 
 6355                        list.Add(convertedItem);
 356                    }
 2357                    return list;
 358                }
 0359                catch
 360                {
 0361                    return inputValue;
 362                }
 363            }
 364        }
 365        // Fallback: return as is if conversion fails
 1366        return inputValue;
 2367    }
 368
 369    private static object ConvertObjectValue(object inputValue, JsonSchemaProperty propDef, int currentDepth, int maxDep
 370    {
 371        // Assuming inputValue is a dictionary-like object (e.g., Dictionary<string, object>, JsonElement object)
 5372        if (inputValue is Dictionary<string, object> dict)
 373        {
 4374            return ValidateAndConvertArguments(propDef, dict, currentDepth + 1, maxDepth);
 375        }
 1376        else if (inputValue is JsonElement jsonEl && jsonEl.ValueKind == JsonValueKind.Object)
 377        {
 1378            var tempDict = new Dictionary<string, object>();
 6379            foreach (var prop in jsonEl.EnumerateObject())
 380            {
 2381                tempDict[prop.Name] = prop.Value; // Pass raw JsonElement down for conversion
 382            }
 1383            return ValidateAndConvertArguments(propDef, tempDict, currentDepth + 1, maxDepth);
 384        }
 385        // Fallback: return as is if conversion fails
 0386        return inputValue;
 387    }
 388}