| | | 1 | | using System.Collections; |
| | | 2 | | using System.ComponentModel; |
| | | 3 | | using System.Reflection; |
| | | 4 | | |
| | | 5 | | namespace UIBlazor.Agents; |
| | | 6 | | |
| | | 7 | | public class BuiltInToolDefs |
| | | 8 | | { |
| | | 9 | | [Description("Request to read the contents of one or more files.")] |
| | | 10 | | public Task<string> ReadFiles([Description("File information")] ReadFileParams[] filePath) |
| | | 11 | | { |
| | | 12 | | // тут чтение файлов |
| | 0 | 13 | | return Task.FromResult(""); |
| | | 14 | | } |
| | | 15 | | |
| | | 16 | | [Description("Applies a series of Search & Replace edits to the specified file.")] |
| | | 17 | | public Task<string> ApplyDiff( |
| | | 18 | | [Description("File path")] string filePath, |
| | | 19 | | [Description("List of pairs 'search/replace'. Executed sequentially.")] DiffEdit[] edits) |
| | | 20 | | { |
| | | 21 | | // тут применение изменений |
| | 1 | 22 | | return Task.FromResult("1234"); |
| | | 23 | | } |
| | | 24 | | |
| | | 25 | | public static NativeToolDefinition MapMethodToTool(MethodInfo? method) |
| | | 26 | | { |
| | 145 | 27 | | ArgumentNullException.ThrowIfNull(method); |
| | 145 | 28 | | var methodDesc = method.GetCustomAttribute<DescriptionAttribute>()?.Description ?? string.Empty; |
| | | 29 | | |
| | 145 | 30 | | var tool = new NativeToolDefinition |
| | 145 | 31 | | { |
| | 145 | 32 | | Function = new NativeToolFunction |
| | 145 | 33 | | { |
| | 145 | 34 | | Name = method.Name, |
| | 145 | 35 | | Description = methodDesc, |
| | 145 | 36 | | Parameters = new NativeParameters |
| | 145 | 37 | | { |
| | 145 | 38 | | Type = NativeToolType.Object, |
| | 145 | 39 | | Properties = [], |
| | 145 | 40 | | Required = [] |
| | 145 | 41 | | } |
| | 145 | 42 | | } |
| | 145 | 43 | | }; |
| | | 44 | | |
| | 145 | 45 | | var parameters = tool.Function.Parameters; |
| | | 46 | | |
| | 752 | 47 | | foreach (var param in method.GetParameters()) |
| | | 48 | | { |
| | 231 | 49 | | var paramDesc = param.GetCustomAttribute<DescriptionAttribute>()?.Description ?? string.Empty; |
| | 231 | 50 | | var paramName = ToCamelCase(param.Name!); |
| | | 51 | | |
| | | 52 | | // В Strict Mode все параметры в required, но для optional - тип с null |
| | 231 | 53 | | var prop = MapTypeToProperty(param.ParameterType, paramDesc, param.HasDefaultValue); |
| | 231 | 54 | | parameters.Properties.Add(paramName, prop); |
| | 231 | 55 | | parameters.Required.Add(paramName); |
| | | 56 | | } |
| | | 57 | | |
| | 145 | 58 | | return tool; |
| | | 59 | | } |
| | | 60 | | |
| | | 61 | | private static NativePropertyDefinition MapTypeToProperty(Type type, string? description = null, bool isOptional = f |
| | | 62 | | { |
| | 818 | 63 | | var prop = new NativePropertyDefinition { Description = description }; |
| | | 64 | | |
| | | 65 | | // Обработка Nullable<T> и nullable reference types |
| | 818 | 66 | | var underlyingType = Nullable.GetUnderlyingType(type); |
| | 818 | 67 | | if (underlyingType != null) |
| | | 68 | | { |
| | 66 | 69 | | var innerProp = MapTypeToProperty(underlyingType, description, true); |
| | 66 | 70 | | return innerProp; |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | // Обработка массивов и коллекций |
| | 752 | 74 | | if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))) |
| | | 75 | | { |
| | 131 | 76 | | prop.SetSingleType(NativeToolType.Array); |
| | 131 | 77 | | var elementType = type.IsArray |
| | 131 | 78 | | ? type.GetElementType() |
| | 131 | 79 | | : type.GetGenericArguments().FirstOrDefault(); |
| | | 80 | | |
| | 131 | 81 | | var arrayObjects = MapTypeToProperty(elementType ?? typeof(object)); |
| | 131 | 82 | | prop.Items = string.Equals(arrayObjects.Type.ToString(), NativeToolType.Object, StringComparison.Ordinal) |
| | 131 | 83 | | ? new NativeParameters |
| | 131 | 84 | | { |
| | 131 | 85 | | Type = NativeToolType.Object, |
| | 131 | 86 | | Properties = arrayObjects.Properties!, |
| | 387 | 87 | | Required = [.. arrayObjects.Properties!.Select(p => p.Key)] |
| | 131 | 88 | | } |
| | 131 | 89 | | : arrayObjects; |
| | | 90 | | |
| | 131 | 91 | | if (isOptional) |
| | | 92 | | { |
| | 0 | 93 | | prop.SetUnionTypes(NativeToolType.Array, NativeToolType.Null); |
| | | 94 | | } |
| | 131 | 95 | | return prop; |
| | | 96 | | } |
| | | 97 | | |
| | | 98 | | // Обработка объектов (классы, кроме string и примитивов) |
| | 621 | 99 | | if (type.IsClass && type != typeof(string)) |
| | | 100 | | { |
| | 130 | 101 | | prop.SetSingleType(NativeToolType.Object); |
| | 130 | 102 | | prop.Properties = []; |
| | | 103 | | |
| | 1040 | 104 | | foreach (var p in type.GetProperties()) |
| | | 105 | | { |
| | 390 | 106 | | var pDesc = p.GetCustomAttribute<DescriptionAttribute>()?.Description; |
| | 390 | 107 | | var propName = ToCamelCase(p.Name); |
| | | 108 | | |
| | | 109 | | // Проверяем, имеет ли свойство дефолтное значение или nullable |
| | 390 | 110 | | var hasDefaultValue = p.GetCustomAttribute<DefaultValueAttribute>() != null; |
| | 390 | 111 | | var isNullableProperty = IsNullableProperty(p); |
| | 390 | 112 | | var isPropOptional = hasDefaultValue || isNullableProperty; |
| | 390 | 113 | | prop.Properties.Add(propName, MapTypeToProperty(p.PropertyType, pDesc, isPropOptional)); |
| | | 114 | | } |
| | | 115 | | |
| | 130 | 116 | | if (isOptional) |
| | | 117 | | { |
| | 0 | 118 | | prop.SetUnionTypes(NativeToolType.Object, NativeToolType.Null); |
| | | 119 | | } |
| | 130 | 120 | | return prop; |
| | | 121 | | } |
| | | 122 | | |
| | | 123 | | // Обработка enum |
| | 491 | 124 | | if (type.IsEnum) |
| | | 125 | | { |
| | 1 | 126 | | var enumDescription = description; |
| | 1 | 127 | | if (!string.IsNullOrEmpty(enumDescription)) |
| | 0 | 128 | | enumDescription += " "; |
| | 1 | 129 | | enumDescription += $"Possible values: {string.Join(", ", Enum.GetNames(type))}"; |
| | | 130 | | |
| | 1 | 131 | | prop.Description = enumDescription; |
| | 1 | 132 | | prop.SetSingleType(NativeToolType.String); |
| | | 133 | | |
| | 1 | 134 | | if (isOptional) |
| | | 135 | | { |
| | 0 | 136 | | prop.SetUnionTypes(NativeToolType.String, NativeToolType.Null); |
| | | 137 | | } |
| | 1 | 138 | | return prop; |
| | | 139 | | } |
| | | 140 | | |
| | | 141 | | // Обработка примитивов |
| | 490 | 142 | | var baseType = GetBaseType(type); |
| | 490 | 143 | | prop.SetSingleType(baseType); |
| | | 144 | | |
| | 490 | 145 | | if (isOptional) |
| | | 146 | | { |
| | 70 | 147 | | prop.SetUnionTypes(baseType, NativeToolType.Null); |
| | | 148 | | } |
| | | 149 | | |
| | 490 | 150 | | return prop; |
| | | 151 | | } |
| | | 152 | | |
| | | 153 | | private static string GetBaseType(Type type) |
| | | 154 | | { |
| | | 155 | | return type switch |
| | | 156 | | { |
| | 695 | 157 | | _ when type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte) => Nativ |
| | 288 | 158 | | _ when type == typeof(bool) => NativeToolType.Boolean, |
| | 288 | 159 | | _ when type == typeof(double) || type == typeof(float) || type == typeof(decimal) => NativeToolType.Number, |
| | 278 | 160 | | _ when type == typeof(DateTime) || type == typeof(DateTimeOffset) => NativeToolType.String, |
| | 276 | 161 | | _ when type == typeof(Guid) => NativeToolType.String, |
| | 272 | 162 | | _ => NativeToolType.String, |
| | | 163 | | }; |
| | | 164 | | } |
| | | 165 | | |
| | | 166 | | private static bool IsNullableProperty(PropertyInfo property) |
| | | 167 | | { |
| | | 168 | | // Проверка Nullable<T> |
| | 390 | 169 | | if (Nullable.GetUnderlyingType(property.PropertyType) != null) |
| | 65 | 170 | | return true; |
| | | 171 | | |
| | | 172 | | // Проверка nullable reference type через аннотации |
| | 325 | 173 | | var nullabilityContext = new NullabilityInfoContext(); |
| | 325 | 174 | | var nullabilityInfo = nullabilityContext.Create(property); |
| | 325 | 175 | | return nullabilityInfo.WriteState == NullabilityState.Nullable; |
| | | 176 | | } |
| | | 177 | | |
| | | 178 | | private static string ToCamelCase(string name) |
| | | 179 | | { |
| | 623 | 180 | | if (string.IsNullOrEmpty(name)) |
| | 0 | 181 | | return name; |
| | 623 | 182 | | return char.ToLowerInvariant(name[0]) + name.Substring(1); |
| | | 183 | | } |
| | | 184 | | |
| | | 185 | | public async Task<string> InvokeToolAsync(string methodName, string argumentsJson) |
| | | 186 | | { |
| | | 187 | | // Находим метод по имени (кейс-сенситив или нет - на ваше усмотрение) |
| | 1 | 188 | | var method = typeof(BuiltInToolDefs).GetMethod(methodName); |
| | 1 | 189 | | if (method == null) |
| | 0 | 190 | | return $"Error: Method {methodName} not found."; |
| | | 191 | | |
| | | 192 | | // Настраиваем десериализатор |
| | 1 | 193 | | var options = new JsonSerializerOptions |
| | 1 | 194 | | { |
| | 1 | 195 | | PropertyNameCaseInsensitive = true, |
| | 1 | 196 | | // Converters = { new JsonStringEnumConverter() } |
| | 1 | 197 | | }; |
| | | 198 | | |
| | 1 | 199 | | var parameters = method.GetParameters(); |
| | 1 | 200 | | var args = new object?[parameters.Length]; |
| | | 201 | | |
| | | 202 | | // Парсим JSON один раз в документ, чтобы разобрать по параметрам |
| | 1 | 203 | | using var doc = JsonDocument.Parse(argumentsJson); |
| | 1 | 204 | | var root = doc.RootElement; |
| | | 205 | | |
| | 6 | 206 | | for (var i = 0; i < parameters.Length; i++) |
| | | 207 | | { |
| | 2 | 208 | | var p = parameters[i]; |
| | 2 | 209 | | var jsonName = ToCamelCase(p.Name!); |
| | | 210 | | |
| | 2 | 211 | | if (root.TryGetProperty(jsonName, out var propertyElement)) |
| | | 212 | | { |
| | | 213 | | // Десериализуем конкретный параметр в нужный тип |
| | 2 | 214 | | args[i] = JsonSerializer.Deserialize(propertyElement.GetRawText(), p.ParameterType, options); |
| | | 215 | | } |
| | 0 | 216 | | else if (p.HasDefaultValue) |
| | | 217 | | { |
| | 0 | 218 | | args[i] = p.DefaultValue; |
| | | 219 | | } |
| | | 220 | | else |
| | | 221 | | { |
| | | 222 | | // В Strict Mode это не должно случиться, но для безопасности: |
| | 0 | 223 | | args[i] = null; |
| | | 224 | | } |
| | | 225 | | } |
| | | 226 | | |
| | | 227 | | // Вызываем метод (предполагаем, что они все Task<string>) |
| | 1 | 228 | | var result = method.Invoke(this, args); |
| | | 229 | | |
| | 1 | 230 | | if (result is Task<string> task) |
| | 1 | 231 | | return await task; |
| | | 232 | | |
| | 0 | 233 | | return result?.ToString() ?? string.Empty; |
| | 1 | 234 | | } |
| | | 235 | | } |