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