< Summary

Information
Class: UIBlazor.Services.Settings.ToolManager
Assembly: UIBlazor
File(s): /home/runner/work/InvAit/InvAit/UIBlazor/Services/Settings/ToolManager.cs
Tag: 71_26091983037
Line coverage
97%
Covered lines: 200
Uncovered lines: 5
Coverable lines: 205
Total lines: 355
Line coverage: 97.5%
Branch coverage
94%
Covered branches: 72
Total branches: 76
Branch coverage: 94.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SaveAsync()100%4476.92%
UpdateCategorySettings(...)100%22100%
ToggleTool(...)100%22100%
RegisterAllTools()100%22100%
AfterInitAsync()100%22100%
ResetAsync()100%44100%
GetEnabledTools()50%22100%
GetBuiltInTools()100%210%
GetAllTools()100%11100%
GetTool(...)100%22100%
GetMcpTools()100%22100%
RefreshMcpTools()100%11100%
BuildMcpTools()100%88100%
GetApprovalModeByToolName(...)100%1010100%
GetModeDesc(...)100%44100%
GetToolUseSystemInstructions(...)90%303098.78%
GetArgumentNamesFromSchema(...)100%22100%

File(s)

/home/runner/work/InvAit/InvAit/UIBlazor/Services/Settings/ToolManager.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2
 3namespace UIBlazor.Services.Settings;
 4
 5public class ToolManager(
 6    BuiltInAgent builtInAgent,
 7    ILogger<ToolManager> logger,
 8    ILocalStorageService localStorage,
 9    IMcpSettingsProvider mcpSettingsProvider,
 10    IVsBridge vsBridge)
 6111    : BaseSettingsProvider<ToolSettings>(localStorage, logger, "ToolSettings"), IToolManager
 12{
 6113    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            {
 1123                if (!Current.CategoryStates.TryGetValue(group.Key, out var state))
 24                {
 725                    state = new ToolCategorySettings
 726                    {
 727                        IsEnabled = true,
 728                        ApprovalMode = ToolApprovalMode.Allow
 729                    };
 730                    Current.CategoryStates[group.Key] = state;
 31                }
 32            }
 33
 34            Current.DisabledTools = [.. _registeredTools.Values.Where(t => !t.Enabled).Select(t => t.Name)];
 35
 1036            await base.SaveAsync();
 1037        }
 038        catch (Exception ex)
 39        {
 040            logger.LogError(ex, "Failed to save tool settings");
 041        }
 1042    }
 43
 44    public void UpdateCategorySettings(ToolCategory category, bool isEnabled, ToolApprovalMode approvalMode)
 45    {
 246        if (!Current.CategoryStates.TryGetValue(category, out var state))
 47        {
 148            state = new ToolCategorySettings();
 149            Current.CategoryStates[category] = state;
 50        }
 251        state.IsEnabled = isEnabled;
 252        state.ApprovalMode = approvalMode;
 253        _ = SaveAsync();
 254    }
 55
 56    public void ToggleTool(string toolName, bool isEnabled)
 57    {
 358        if (_registeredTools.TryGetValue(toolName, out var tool))
 59        {
 260            tool.Enabled = isEnabled;
 261            _ = SaveAsync();
 62        }
 363    }
 64
 65    public void RegisterAllTools()
 66    {
 12267        foreach (var tool in builtInAgent.Tools)
 68        {
 3269            _registeredTools[tool.Name] = tool;
 70        }
 71
 72        // Load tool settings after registration
 2973        _ = InitializeAsync();
 2974    }
 75
 76    protected override Task AfterInitAsync()
 77    {
 12878        foreach (var tool in _registeredTools.Values)
 79        {
 3380            tool.Enabled = !Current.DisabledTools.Contains(tool.Name);
 81        }
 82
 3183        mcpSettingsProvider.OnSaved += RefreshMcpTools;
 3184        return Task.CompletedTask;
 85    }
 86
 87    public override Task ResetAsync()
 88    {
 1289        foreach (var tool in _registeredTools.Values)
 90        {
 391            tool.Enabled = true;
 92        }
 93
 2094        foreach (var state in Current.CategoryStates.Values)
 95        {
 796            state.IsEnabled = true;
 797            state.ApprovalMode = ToolApprovalMode.Allow;
 98        }
 99
 3100        Current.DisabledTools.Clear();
 3101        return SaveAsync();
 102    }
 103
 104    public IEnumerable<Tool> GetEnabledTools()
 105    {
 17106        var builtIn = _registeredTools.Values.Where(t =>
 17107        {
 12108            if (Current.CategoryStates.TryGetValue(t.Category, out var state))
 17109            {
 3110                return state.IsEnabled && t.Enabled;
 17111            }
 9112            return t.Enabled;
 17113        });
 114
 17115        var mcp = mcpSettingsProvider.Current.Enabled
 5116            ? GetMcpTools().Where(t => !mcpSettingsProvider.Current.ToolDisabledStates.Contains(t.Name))
 17117            : [];
 118
 17119        return builtIn.Concat(mcp);
 120    }
 121
 0122    public IEnumerable<Tool> GetBuiltInTools() => _registeredTools.Values;
 123
 124    public IEnumerable<Tool> GetAllTools()
 125    {
 10126        return _registeredTools.Values.Concat(GetMcpTools());
 127    }
 128
 129    public Tool? GetTool(string name)
 130    {
 17131        if (_registeredTools.TryGetValue(name, out var tool))
 13132            return tool;
 133
 6134        return GetMcpTools().FirstOrDefault(t => t.Name == name);
 135    }
 136
 137    public IEnumerable<Tool> GetMcpTools()
 138    {
 37139        if (_mcpToolsCache != null)
 3140            return _mcpToolsCache;
 141
 34142        _mcpToolsCache = BuildMcpTools();
 34143        return _mcpToolsCache;
 144    }
 145
 1146    public void RefreshMcpTools() => _mcpToolsCache = null;
 147
 148    public IEnumerable<Tool> BuildMcpTools()
 149    {
 43150        if (!mcpSettingsProvider.Current.Enabled)
 151        {
 2152            yield break;
 153        }
 154
 155        // перебираем все MCP сервера
 130156        foreach (var server in mcpSettingsProvider.Current.Servers.Where(s =>
 69157            mcpSettingsProvider.Current.ServerEnabledStates.TryGetValue(s.Name, out var serverEnabled)
 69158                ? serverEnabled
 69159                : s.Enabled))
 160        {
 161            // перебор всех тулзов в MCP этом сервере
 102162            foreach (var toolConfig in server.Tools)
 163            {
 27164                var toolName = $"mcp__{server.Name}__{toolConfig.Name}";
 165
 27166                var isEnabled = !mcpSettingsProvider.Current.ToolDisabledStates.Contains(toolName);
 27167                var currentToolConfig = toolConfig;
 27168                var currentServer = server;
 27169                yield return new Tool
 27170                {
 27171                    Name = toolName,
 27172                    DisplayName = currentToolConfig.Name,
 27173                    Description = currentToolConfig.Description ?? string.Empty,
 27174                    Category = ToolCategory.Mcp,
 27175                    Server = currentServer.Name,
 27176                    Enabled = isEnabled,
 27177                    ExampleToSystemMessage = SchemaProcessor.BuildSchemaDescription(toolName, currentToolConfig),
 27178                    ExecuteAsync = (args, cancellationToken) =>
 27179                    {
 3180                        var arguments = GetArgumentNamesFromSchema(currentToolConfig.InputSchema, args);
 3181                        var mcpArgs = new Dictionary<string, object>
 3182                        {
 3183                            { "serverId", currentServer.Name },
 3184                            { "toolName", currentToolConfig.Name },
 3185                            { "arguments", arguments },
 3186                            // Command/Arguments for auto-start if needed
 3187                            { "command", currentServer.Command },
 3188                            { "args", string.Join(" ", currentServer.Args) },
 3189                            { "env", currentServer.Env }
 3190                        };
 27191
 3192                        return vsBridge.ExecuteToolAsync(BasicEnum.McpCallTool, mcpArgs, cancellationToken);
 27193                    }
 27194                };
 195            }
 22196        }
 37197    }
 198
 199    public ToolApprovalMode GetApprovalModeByToolName(string name)
 200    {
 6201        if (name.StartsWith("mcp__"))
 202        {
 4203            var parts = name.Split("__", 3, StringSplitOptions.None);
 4204            if (parts.Length >= 2)
 205            {
 4206                var serverName = parts[1];
 4207                if (mcpSettingsProvider.Current.ServerApprovalModes.TryGetValue(serverName, out var mode))
 208                {
 3209                    return mode;
 210                }
 211            }
 1212            return ToolApprovalMode.Allow;
 213        }
 214
 2215        var tool = GetTool(name);
 2216        return tool != null && Current.CategoryStates.TryGetValue(tool.Category, out var state)
 2217            ? state.ApprovalMode
 2218            : ToolApprovalMode.Allow;
 219    }
 220
 221    private string GetModeDesc(AppMode mode)
 33222        => mode switch
 33223        {
 11224            AppMode.Agent => $"{mode} (for taking actions and applying changes)",
 11225            AppMode.Plan => $"{mode} (for planning before taking actions)",
 11226            _ => $"{mode} (for discussion, reading and explanations)",
 33227        };
 228
 229    public string GetToolUseSystemInstructions(AppMode mode, bool hasSkills)
 230    {
 11231        var enabledTools = GetEnabledTools().ToList();
 232
 11233        if (!hasSkills) // если нет скиллов, то не нужно их читать
 234        {
 19235            enabledTools = [.. enabledTools.Where(t => t.Name != BasicEnum.ReadSkillContent)];
 236        }
 237
 238        // Filter tools based on mode
 11239        enabledTools = mode switch
 11240        {
 8241            AppMode.Chat => [.. enabledTools.Where(t => t.Category is ToolCategory.ReadFiles or ToolCategory.ModeSwitch 
 6242            AppMode.Agent => enabledTools,
 2243            AppMode.Plan => [.. enabledTools.Where(t => t.Category is ToolCategory.ReadFiles or ToolCategory.ModeSwitch 
 0244            _ => enabledTools
 11245        };
 246
 66247        var otherModes = string.Join(", ", Enum.GetValues<AppMode>().Where(m => m != mode).Select(m => GetModeDesc(m)));
 248
 11249        var sb = new StringBuilder();
 11250        sb.AppendLine($"Your current mode: {GetModeDesc(mode)}");
 19251        if (enabledTools.FirstOrDefault(t => t.Category == ToolCategory.ModeSwitch)?.Enabled == true)
 252        {
 1253            sb.AppendLine($"Other available modes: {otherModes}.");
 254        }
 11255        sb.AppendLine("Use Mermaid diagrams for clarity in explanations. This will help you better visualize the answer 
 256
 11257        if (mode == AppMode.Plan)
 258        {
 1259            sb.AppendLine("""
 1260                          ## Planning Mode Instructions
 1261                          You are currently in **PLANNING MODE**. Your goal is to analyze the user's request, explore th
 1262
 1263                          1. **Analyze**: Use available tools to understand the current state of the project.
 1264                          2. **Propose**: Create a structured plan. The plan should be realistic and broken down into lo
 1265                          3. **Format**: Wrap your final plan in `<plan>` tags. Each step should be clear and actionable
 1266
 1267                          **Example:**
 1268                          <plan>
 1269                          1. Create a new service `StorageService`.
 1270                          2. Register it in `Program.cs`.
 1271                          3. Update `SettingsPage` to use the new service.
 1272                          </plan>
 1273
 1274                          In this mode, you should NOT make any changes to files. Your goal is to get user approval for 
 1275                          Once the plan is approved, the mode will be switched to **Agent** for execution.
 1276                          """);
 277        }
 278
 19279        if (enabledTools.Any(t => t.Name == BasicEnum.SwitchMode))
 280        {
 1281            sb.AppendLine($"You can use '{BasicEnum.SwitchMode}' tool to change current mode if you need more tools or w
 282        }
 283
 11284        if (enabledTools.Count == 0)
 285        {
 3286            return sb.ToString();
 287        }
 288
 8289        if (mode == AppMode.Agent)
 290        {
 4291            sb.AppendLine("You are a tool-calling agent. You should take actions to fulfill the user's request.");
 4292            sb.AppendLine();
 293        }
 294
 8295        if (enabledTools.Count > 0)
 296        {
 8297            sb.AppendLine("""
 8298                      ## Tool use instructions
 8299
 8300                      You have access to several tools/functions that you can use at any time to retrieve information an
 8301
 8302                      ## Execution Rules
 8303
 8304                      **Multi-call:** You SHOULD invoke multiple tools within a single message if the task requires it. 
 8305
 8306                      **Prioritize Examples:** Each tool has a specific usage example below. Always follow the tool's sp
 8307
 8308                      **Syntax:** You MUST invoke tools exclusively with the following literal syntax:
 8309
 8310                            <function name="toolName">
 8311                            Parameters
 8312                            </function>
 8313
 8314                      **Constraints:**
 8315                            - Use <function> tags ONLY for actual tool calls.
 8316                            - Stop generation immediately after the last tool call.
 8317                            - No conversational filler or explanations after tools.
 8318
 8319                      The following tools/functions are available to you:
 8320
 8321                      """);
 322
 32323            foreach (var tool in enabledTools)
 324            {
 8325                sb.AppendLine("---");
 8326                sb.AppendLine($"### Tool: {tool.Name}");
 8327                sb.AppendLine($"**Description:** {tool.Description}");
 8328                sb.AppendLine("**Calling:**");
 8329                sb.AppendLine(tool.ExampleToSystemMessage);
 8330                sb.AppendLine();
 331            }
 332
 8333            sb.AppendLine("""
 8334
 8335                          If it seems like the User's request could be solved with the tools, choose the BEST tool for t
 8336                          Do not perform actions with/for hypothetical files. Use tools to deduce which files are releva
 8337                          You can call multiple tools once.
 8338                          """);
 339        }
 340
 8341        return sb.ToString();
 342    }
 343
 344    // Uses SchemaProcessor for recursive handling
 345    private static Dictionary<string, object> GetArgumentNamesFromSchema(JsonElement? schemaElement, IReadOnlyDictionary
 346    {
 3347        var schemaProperty = SchemaProcessor.DeserializeSchema(schemaElement);
 3348        if (schemaProperty == null)
 349        {
 2350            return [];
 351        }
 352
 1353        return SchemaProcessor.ValidateAndConvertArguments(schemaProperty, args);
 354    }
 355}