| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.Diagnostics; |
| | | 3 | | using Shared.Contracts.Mcp; |
| | | 4 | | |
| | | 5 | | namespace UIBlazor.Services.Settings; |
| | | 6 | | |
| | | 7 | | public partial class ToolManager( |
| | | 8 | | BuiltInAgent builtInAgent, |
| | | 9 | | ILogger<ToolManager> logger, |
| | | 10 | | ILocalStorageService localStorage, |
| | | 11 | | IMcpSettingsProvider mcpSettingsProvider, |
| | | 12 | | IVsBridge vsBridge) |
| | 12 | 13 | | : BaseSettingsProvider<ToolSettings>(localStorage, logger, "ToolSettings"), IToolManager |
| | | 14 | | { |
| | 12 | 15 | | private readonly ConcurrentDictionary<string, Tool> _registeredTools = new(); |
| | | 16 | | |
| | | 17 | | public override async Task SaveAsync() |
| | | 18 | | { |
| | | 19 | | try |
| | | 20 | | { |
| | | 21 | | foreach (var group in _registeredTools.Values.GroupBy(t => t.Category)) |
| | | 22 | | { |
| | 0 | 23 | | if (!Current.CategoryStates.TryGetValue(group.Key, out var state)) |
| | | 24 | | { |
| | 0 | 25 | | state = new ToolCategorySettings |
| | 0 | 26 | | { |
| | 0 | 27 | | IsEnabled = true, |
| | 0 | 28 | | ApprovalMode = ToolApprovalMode.Allow |
| | 0 | 29 | | }; |
| | 0 | 30 | | Current.CategoryStates[group.Key] = state; |
| | | 31 | | } |
| | | 32 | | } |
| | | 33 | | |
| | | 34 | | Current.DisabledTools = [.. _registeredTools.Values.Where(t => !t.Enabled).Select(t => t.Name)]; |
| | | 35 | | |
| | 0 | 36 | | await base.SaveAsync(); |
| | 0 | 37 | | } |
| | 0 | 38 | | catch (Exception ex) |
| | | 39 | | { |
| | | 40 | | Debug.WriteLine($"Failed to save tool settings: {ex.Message}"); |
| | 0 | 41 | | } |
| | 0 | 42 | | } |
| | | 43 | | |
| | | 44 | | public void UpdateCategorySettings(ToolCategory category, bool isEnabled, ToolApprovalMode approvalMode) |
| | | 45 | | { |
| | 0 | 46 | | if (!Current.CategoryStates.TryGetValue(category, out var state)) |
| | | 47 | | { |
| | 0 | 48 | | state = new ToolCategorySettings(); |
| | 0 | 49 | | Current.CategoryStates[category] = state; |
| | | 50 | | } |
| | 0 | 51 | | state.IsEnabled = isEnabled; |
| | 0 | 52 | | state.ApprovalMode = approvalMode; |
| | 0 | 53 | | _ = SaveAsync(); |
| | 0 | 54 | | } |
| | | 55 | | |
| | | 56 | | public void ToggleTool(string toolName, bool isEnabled) |
| | | 57 | | { |
| | 0 | 58 | | if (_registeredTools.TryGetValue(toolName, out var tool)) |
| | | 59 | | { |
| | 0 | 60 | | tool.Enabled = isEnabled; |
| | 0 | 61 | | _ = SaveAsync(); |
| | | 62 | | } |
| | 0 | 63 | | } |
| | | 64 | | |
| | | 65 | | public void RegisterAllTools() |
| | | 66 | | { |
| | 26 | 67 | | foreach (var tool in builtInAgent.Tools) |
| | | 68 | | { |
| | 7 | 69 | | _registeredTools[tool.Name] = tool; |
| | | 70 | | } |
| | | 71 | | |
| | | 72 | | // Load tool settings after registration |
| | 6 | 73 | | _ = InitializeAsync(); |
| | 6 | 74 | | } |
| | | 75 | | |
| | | 76 | | protected override Task AfterInitAsync() |
| | | 77 | | { |
| | 26 | 78 | | foreach (var tool in _registeredTools.Values) |
| | | 79 | | { |
| | 7 | 80 | | tool.Enabled = !Current.DisabledTools.Contains(tool.Name); |
| | | 81 | | } |
| | 6 | 82 | | return Task.CompletedTask; |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | public override Task ResetAsync() |
| | | 86 | | { |
| | 0 | 87 | | foreach (var tool in _registeredTools.Values) |
| | | 88 | | { |
| | 0 | 89 | | tool.Enabled = true; |
| | | 90 | | } |
| | | 91 | | |
| | 0 | 92 | | foreach (var state in Current.CategoryStates.Values) |
| | | 93 | | { |
| | 0 | 94 | | state.IsEnabled = true; |
| | 0 | 95 | | state.ApprovalMode = ToolApprovalMode.Allow; |
| | | 96 | | } |
| | | 97 | | |
| | 0 | 98 | | Current.DisabledTools.Clear(); |
| | 0 | 99 | | return SaveAsync(); |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | public IEnumerable<Tool> GetEnabledTools() |
| | | 103 | | { |
| | 4 | 104 | | var builtIn = _registeredTools.Values.Where(t => |
| | 4 | 105 | | { |
| | 3 | 106 | | if (Current.CategoryStates.TryGetValue(t.Category, out var state)) |
| | 4 | 107 | | { |
| | 0 | 108 | | return state.IsEnabled && t.Enabled; |
| | 4 | 109 | | } |
| | 3 | 110 | | return t.Enabled; |
| | 4 | 111 | | }); |
| | | 112 | | |
| | 5 | 113 | | var mcp = GetMcpTools().Where(t => t.Enabled); |
| | | 114 | | |
| | 4 | 115 | | return builtIn.Concat(mcp); |
| | | 116 | | } |
| | | 117 | | |
| | | 118 | | public IEnumerable<Tool> GetAllTools() |
| | | 119 | | { |
| | 7 | 120 | | return _registeredTools.Values.Concat(GetMcpTools()); |
| | | 121 | | } |
| | | 122 | | |
| | | 123 | | public Tool? GetTool(string name) |
| | | 124 | | { |
| | 4 | 125 | | if (_registeredTools.TryGetValue(name, out var tool)) |
| | 3 | 126 | | return tool; |
| | | 127 | | |
| | 1 | 128 | | return GetMcpTools().FirstOrDefault(t => t.Name == name); |
| | | 129 | | } |
| | | 130 | | |
| | | 131 | | private IEnumerable<Tool> GetMcpTools() |
| | | 132 | | { |
| | 10 | 133 | | if (!mcpSettingsProvider.Current.Enabled) |
| | | 134 | | { |
| | 0 | 135 | | yield break; |
| | | 136 | | } |
| | | 137 | | |
| | | 138 | | // перебираем все MCP сервера |
| | 27 | 139 | | foreach (var server in mcpSettingsProvider.Current.Servers.Where(s => |
| | 14 | 140 | | mcpSettingsProvider.Current.ServerEnabledStates.TryGetValue(s.Name, out var serverEnabled) |
| | 14 | 141 | | ? serverEnabled |
| | 14 | 142 | | : s.Enabled)) |
| | | 143 | | { |
| | | 144 | | // перебор всех тулзов в MCP этом сервере |
| | 15 | 145 | | foreach (var toolConfig in server.Tools) |
| | | 146 | | { |
| | 4 | 147 | | var toolName = $"mcp__{server.Name}__{toolConfig.Name}"; |
| | | 148 | | |
| | 4 | 149 | | var isEnabled = !mcpSettingsProvider.Current.ToolDisabledStates.Contains(toolName); |
| | | 150 | | |
| | 4 | 151 | | yield return new Tool |
| | 4 | 152 | | { |
| | 4 | 153 | | Name = toolName, |
| | 4 | 154 | | DisplayName = toolConfig.Name, |
| | 4 | 155 | | Description = toolConfig.Description ?? string.Empty, |
| | 4 | 156 | | Category = ToolCategory.Mcp, |
| | 4 | 157 | | Enabled = isEnabled, |
| | 4 | 158 | | ExampleToSystemMessage = BuildSchemaDescription(toolName, toolConfig), |
| | 4 | 159 | | ExecuteAsync = (args) => |
| | 4 | 160 | | { |
| | 1 | 161 | | var arguments = GetArgumentNamesFromSchema(toolConfig.InputSchema, args); |
| | 1 | 162 | | var mcpArgs = new Dictionary<string, object> |
| | 1 | 163 | | { |
| | 1 | 164 | | { "serverId", server.Name }, |
| | 1 | 165 | | { "toolName", toolConfig.Name }, |
| | 1 | 166 | | { "arguments", arguments } |
| | 1 | 167 | | }; |
| | 4 | 168 | | |
| | 1 | 169 | | return vsBridge.ExecuteToolAsync(BasicEnum.McpCallTool, mcpArgs); |
| | 4 | 170 | | } |
| | 4 | 171 | | }; |
| | | 172 | | } |
| | 3 | 173 | | } |
| | 9 | 174 | | } |
| | | 175 | | |
| | | 176 | | public ToolApprovalMode GetApprovalModeByToolName(string name) |
| | | 177 | | { |
| | 2 | 178 | | if (name.StartsWith("mcp__")) |
| | | 179 | | { |
| | 2 | 180 | | var parts = name.Split("__"); // TODO есть риск что сервер в названии содержит __ и тогда он всегда будет Au |
| | 2 | 181 | | if (parts.Length >= 2) |
| | | 182 | | { |
| | 2 | 183 | | var serverName = parts[1]; |
| | 2 | 184 | | if (mcpSettingsProvider.Current.ServerApprovalModes.TryGetValue(serverName, out var mode)) |
| | | 185 | | { |
| | 1 | 186 | | return mode; |
| | | 187 | | } |
| | | 188 | | } |
| | 1 | 189 | | return ToolApprovalMode.Allow; |
| | | 190 | | } |
| | | 191 | | |
| | 0 | 192 | | var tool = GetTool(name); |
| | 0 | 193 | | return tool != null && Current.CategoryStates.TryGetValue(tool.Category, out var state) |
| | 0 | 194 | | ? state.ApprovalMode |
| | 0 | 195 | | : ToolApprovalMode.Allow; |
| | | 196 | | } |
| | | 197 | | |
| | | 198 | | public string GetToolUseSystemInstructions(AppMode mode, bool hasSkills) |
| | | 199 | | { |
| | 2 | 200 | | var enabledTools = GetEnabledTools().ToList(); |
| | | 201 | | |
| | 2 | 202 | | if (!hasSkills) // если нет скиллов, то не нужно их читать |
| | | 203 | | { |
| | 3 | 204 | | enabledTools = [.. enabledTools.Where(t => t.Name != BasicEnum.ReadSkillContent)]; |
| | | 205 | | } |
| | | 206 | | |
| | | 207 | | // Filter tools based on mode |
| | 2 | 208 | | enabledTools = mode switch |
| | 2 | 209 | | { |
| | 0 | 210 | | AppMode.Chat => [.. enabledTools.Where(t => t.Category is ToolCategory.ReadFiles or ToolCategory.ModeSwitch) |
| | 2 | 211 | | AppMode.Agent => enabledTools, |
| | 0 | 212 | | AppMode.Plan => [.. enabledTools.Where(t => t.Category is ToolCategory.ReadFiles or ToolCategory.ModeSwitch |
| | 0 | 213 | | _ => enabledTools |
| | 2 | 214 | | }; |
| | | 215 | | |
| | 2 | 216 | | var sb = new StringBuilder(); |
| | 2 | 217 | | sb.AppendLine($"Current date: {DateTime.Now:f}"); |
| | 2 | 218 | | sb.AppendLine($"Current Application Mode: {mode}"); |
| | 2 | 219 | | sb.AppendLine("Available modes: Chat (for discussion, reading and explanations), Agent (for taking actions and a |
| | 2 | 220 | | sb.AppendLine("Use Mermaid diagrams for clarity in explanations. This will help you better visualize the answer |
| | | 221 | | |
| | 2 | 222 | | if (mode == AppMode.Plan) |
| | | 223 | | { |
| | 0 | 224 | | sb.AppendLine(""" |
| | 0 | 225 | | ## Planning Mode Instructions |
| | 0 | 226 | | You are currently in **PLANNING MODE**. Your goal is to analyze the user's request, explore th |
| | 0 | 227 | | |
| | 0 | 228 | | 1. **Analyze**: Use available tools to understand the current state of the project. |
| | 0 | 229 | | 2. **Propose**: Create a structured plan. The plan should be realistic and broken down into lo |
| | 0 | 230 | | 3. **Format**: Wrap your final plan in `<plan>` tags. Each step should be clear and actionable |
| | 0 | 231 | | |
| | 0 | 232 | | **Example:** |
| | 0 | 233 | | <plan> |
| | 0 | 234 | | 1. Create a new service `StorageService`. |
| | 0 | 235 | | 2. Register it in `Program.cs`. |
| | 0 | 236 | | 3. Update `SettingsPage` to use the new service. |
| | 0 | 237 | | </plan> |
| | 0 | 238 | | |
| | 0 | 239 | | In this mode, you should NOT make any changes to files. Your goal is to get user approval for |
| | 0 | 240 | | Once the plan is approved, the mode will be switched to **Agent** for execution. |
| | 0 | 241 | | """); |
| | | 242 | | } |
| | | 243 | | |
| | 3 | 244 | | if (enabledTools.Any(t => t.Name == BasicEnum.SwitchMode)) |
| | | 245 | | { |
| | 0 | 246 | | sb.AppendLine($"You can use '{BasicEnum.SwitchMode}' tool to change current mode if you need more tools or w |
| | | 247 | | } |
| | | 248 | | |
| | 2 | 249 | | if (enabledTools.Count == 0) |
| | | 250 | | { |
| | 1 | 251 | | return sb.ToString(); |
| | | 252 | | } |
| | | 253 | | |
| | 1 | 254 | | if (mode == AppMode.Agent) |
| | | 255 | | { |
| | 1 | 256 | | sb.AppendLine("You are a tool-calling agent. You should take actions to fulfill the user's request."); |
| | 1 | 257 | | sb.AppendLine(); |
| | | 258 | | } |
| | | 259 | | |
| | 1 | 260 | | if (enabledTools.Count > 0) |
| | | 261 | | { |
| | 1 | 262 | | sb.AppendLine(""" |
| | 1 | 263 | | ## Tool use instructions |
| | 1 | 264 | | |
| | 1 | 265 | | You have access to several tools/functions that you can use at any time to retrieve information an |
| | 1 | 266 | | |
| | 1 | 267 | | ## Execution Rules |
| | 1 | 268 | | |
| | 1 | 269 | | **Multi-call:** You SHOULD invoke multiple tools within a single message if the task requires it. |
| | 1 | 270 | | |
| | 1 | 271 | | **Prioritize Examples:** Each tool has a specific usage example below. Always follow the tool's sp |
| | 1 | 272 | | |
| | 1 | 273 | | **Syntax:** You MUST invoke tools exclusively with the following literal syntax: |
| | 1 | 274 | | |
| | 1 | 275 | | <function name="toolName"> |
| | 1 | 276 | | Parameters |
| | 1 | 277 | | </function> |
| | 1 | 278 | | |
| | 1 | 279 | | **Constraints:** |
| | 1 | 280 | | - Use <function> tags ONLY for actual tool calls. |
| | 1 | 281 | | - Stop generation immediately after the last tool call. |
| | 1 | 282 | | - No conversational filler or explanations after tools. |
| | 1 | 283 | | |
| | 1 | 284 | | The following tools/functions are available to you: |
| | 1 | 285 | | |
| | 1 | 286 | | """); |
| | | 287 | | |
| | 4 | 288 | | foreach (var tool in enabledTools) |
| | | 289 | | { |
| | 1 | 290 | | sb.AppendLine("---"); |
| | 1 | 291 | | sb.AppendLine($"### Tool: {tool.Name}"); |
| | 1 | 292 | | sb.AppendLine($"**Description:** {tool.Description}"); |
| | 1 | 293 | | sb.AppendLine("**Calling:**"); |
| | 1 | 294 | | sb.AppendLine(tool.ExampleToSystemMessage); |
| | 1 | 295 | | sb.AppendLine(); |
| | | 296 | | } |
| | | 297 | | |
| | 1 | 298 | | sb.AppendLine(""" |
| | 1 | 299 | | |
| | 1 | 300 | | If it seems like the User's request could be solved with the tools, choose the BEST tool for t |
| | 1 | 301 | | Do not perform actions with/for hypothetical files. Use tools to deduce which files are releva |
| | 1 | 302 | | You can call multiple tools once. |
| | 1 | 303 | | """); |
| | | 304 | | } |
| | | 305 | | |
| | 1 | 306 | | return sb.ToString(); |
| | | 307 | | } |
| | | 308 | | |
| | | 309 | | /// <summary> |
| | | 310 | | /// Build a readable schema description for LLM prompt |
| | | 311 | | /// </summary> |
| | | 312 | | private static string BuildSchemaDescription(string toolName, McpToolConfig toolConfig) |
| | | 313 | | { |
| | 4 | 314 | | var schemaElement = toolConfig.InputSchema; |
| | 4 | 315 | | var requiredArgs = toolConfig.RequiredArguments; |
| | 4 | 316 | | if (!schemaElement.HasValue || schemaElement.Value.ValueKind != JsonValueKind.Object) |
| | | 317 | | { |
| | 3 | 318 | | return string.Empty; |
| | | 319 | | } |
| | | 320 | | |
| | 1 | 321 | | var sb = new StringBuilder(); |
| | 1 | 322 | | var schema = schemaElement.Value; |
| | | 323 | | |
| | 1 | 324 | | sb.AppendLine("For example:"); |
| | 1 | 325 | | sb.AppendLine($"<function name =\"{toolName}\">"); |
| | | 326 | | |
| | 1 | 327 | | var propDesc = new List<string>(); |
| | | 328 | | // Get properties |
| | 1 | 329 | | if (schema.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Object) |
| | | 330 | | { |
| | 4 | 331 | | foreach (var prop in props.EnumerateObject()) |
| | | 332 | | { |
| | 1 | 333 | | var isRequired = requiredArgs.Contains(prop.Name); |
| | 1 | 334 | | var type = "string"; |
| | 1 | 335 | | var description = string.Empty; |
| | | 336 | | |
| | 1 | 337 | | if (prop.Value.ValueKind == JsonValueKind.Object) |
| | | 338 | | { |
| | 1 | 339 | | if (prop.Value.TryGetProperty("type", out var typeProp)) |
| | | 340 | | { |
| | 1 | 341 | | type = typeProp.GetString() ?? "string"; |
| | | 342 | | } |
| | 1 | 343 | | if (prop.Value.TryGetProperty("description", out var descProp)) |
| | | 344 | | { |
| | 0 | 345 | | description = descProp.GetString() ?? string.Empty; |
| | | 346 | | } |
| | | 347 | | } |
| | | 348 | | |
| | | 349 | | // TODO еще enum (допустимые значения), minimum, maximum (для чисел), minLength, maxLength, pattern (для |
| | 1 | 350 | | var requiredMark = isRequired ? " (required)" : ""; |
| | 1 | 351 | | propDesc.Add($"{prop.Name} : [{type}]{requiredMark} {description}"); |
| | | 352 | | |
| | 1 | 353 | | sb.AppendLine($"{prop.Name} : {GetSampleByType(type)}"); |
| | | 354 | | } |
| | | 355 | | } |
| | | 356 | | |
| | 1 | 357 | | sb.AppendLine("</function>"); |
| | | 358 | | |
| | 1 | 359 | | if (propDesc.Count > 0) |
| | | 360 | | { |
| | 1 | 361 | | sb.AppendLine("*Properties schema:*"); |
| | 1 | 362 | | sb.AppendLine(string.Join(Environment.NewLine, propDesc)); |
| | | 363 | | } |
| | | 364 | | |
| | 1 | 365 | | return sb.ToString(); |
| | | 366 | | } |
| | | 367 | | |
| | | 368 | | private static string GetSampleByType(string propType) |
| | | 369 | | { |
| | 1 | 370 | | return propType switch |
| | 1 | 371 | | { |
| | 0 | 372 | | "number" or "integer" => "123456", |
| | 0 | 373 | | "boolean" => "true", |
| | 0 | 374 | | "array" => "[]", // TODO нужно сделать поддержку массивов |
| | 0 | 375 | | "object" => "object", // TODO вложенный объект... |
| | 1 | 376 | | _ => "\"string\"", |
| | 1 | 377 | | }; |
| | | 378 | | } |
| | | 379 | | |
| | | 380 | | private static object GetArgumentByType(string propType, object arg) |
| | | 381 | | { |
| | | 382 | | // TODO array и object и enum |
| | 1 | 383 | | return propType switch |
| | 1 | 384 | | { |
| | 0 | 385 | | "integer" => int.TryParse(arg.ToString(), out var valueInt) ? valueInt : arg, |
| | 0 | 386 | | "number" => double.TryParse(arg.ToString(), out var valueDouble) ? valueDouble : arg, |
| | 0 | 387 | | "boolean" => bool.TryParse(arg.ToString(), out var valueBool) ? valueBool : arg, |
| | 1 | 388 | | _ => arg |
| | 1 | 389 | | }; |
| | | 390 | | } |
| | | 391 | | |
| | | 392 | | private static Dictionary<string, object> GetArgumentNamesFromSchema(JsonElement? schemaElement, IReadOnlyDictionary |
| | | 393 | | { |
| | 1 | 394 | | if (!schemaElement.HasValue || schemaElement.Value.ValueKind != JsonValueKind.Object) |
| | | 395 | | { |
| | 0 | 396 | | return []; |
| | | 397 | | } |
| | | 398 | | |
| | 1 | 399 | | var result = new Dictionary<string, object>(); |
| | 1 | 400 | | var schema = schemaElement.Value; |
| | 1 | 401 | | if (schema.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Object) |
| | | 402 | | { |
| | 4 | 403 | | foreach (var prop in props.EnumerateObject()) |
| | | 404 | | { |
| | 1 | 405 | | if (args.TryGetValue(prop.Name, out var arg)) |
| | | 406 | | { |
| | 1 | 407 | | var propType = prop.Value.GetProperty("type").GetString() ?? string.Empty; // TODO этого не может бы |
| | 1 | 408 | | result[prop.Name] = GetArgumentByType(propType, arg); |
| | | 409 | | } |
| | | 410 | | } |
| | | 411 | | } |
| | | 412 | | |
| | 1 | 413 | | return result; |
| | | 414 | | } |
| | | 415 | | } |