< Summary

Information
Class: UIBlazor.Services.Settings.ToolManager
Assembly: UIBlazor
File(s): /home/runner/work/InvAit/InvAit/UIBlazor/Services/Settings/ToolManager.cs
Tag: 14_22728831704
Line coverage
70%
Covered lines: 162
Uncovered lines: 67
Coverable lines: 229
Total lines: 415
Line coverage: 70.7%
Branch coverage
58%
Covered branches: 71
Total branches: 122
Branch coverage: 58.1%
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()0%2040%
UpdateCategorySettings(...)0%620%
ToggleTool(...)0%620%
RegisterAllTools()100%22100%
AfterInitAsync()100%22100%
ResetAsync()0%2040%
GetEnabledTools()100%1190%
GetAllTools()100%11100%
GetTool(...)100%22100%
GetMcpTools()87.5%8895.23%
GetApprovalModeByToolName(...)50%151063.63%
GetToolUseSystemInstructions(...)50%402672.83%
BuildSchemaDescription(...)79.16%242496.29%
GetSampleByType(...)50%231050%
GetArgumentByType(...)25%231257.14%
GetArgumentNamesFromSchema(...)78.57%141490%

File(s)

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

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Diagnostics;
 3using Shared.Contracts.Mcp;
 4
 5namespace UIBlazor.Services.Settings;
 6
 7public partial class ToolManager(
 8    BuiltInAgent builtInAgent,
 9    ILogger<ToolManager> logger,
 10    ILocalStorageService localStorage,
 11    IMcpSettingsProvider mcpSettingsProvider,
 12    IVsBridge vsBridge)
 1213    : BaseSettingsProvider<ToolSettings>(localStorage, logger, "ToolSettings"), IToolManager
 14{
 1215    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            {
 023                if (!Current.CategoryStates.TryGetValue(group.Key, out var state))
 24                {
 025                    state = new ToolCategorySettings
 026                    {
 027                        IsEnabled = true,
 028                        ApprovalMode = ToolApprovalMode.Allow
 029                    };
 030                    Current.CategoryStates[group.Key] = state;
 31                }
 32            }
 33
 34            Current.DisabledTools = [.. _registeredTools.Values.Where(t => !t.Enabled).Select(t => t.Name)];
 35
 036            await base.SaveAsync();
 037        }
 038        catch (Exception ex)
 39        {
 40            Debug.WriteLine($"Failed to save tool settings: {ex.Message}");
 041        }
 042    }
 43
 44    public void UpdateCategorySettings(ToolCategory category, bool isEnabled, ToolApprovalMode approvalMode)
 45    {
 046        if (!Current.CategoryStates.TryGetValue(category, out var state))
 47        {
 048            state = new ToolCategorySettings();
 049            Current.CategoryStates[category] = state;
 50        }
 051        state.IsEnabled = isEnabled;
 052        state.ApprovalMode = approvalMode;
 053        _ = SaveAsync();
 054    }
 55
 56    public void ToggleTool(string toolName, bool isEnabled)
 57    {
 058        if (_registeredTools.TryGetValue(toolName, out var tool))
 59        {
 060            tool.Enabled = isEnabled;
 061            _ = SaveAsync();
 62        }
 063    }
 64
 65    public void RegisterAllTools()
 66    {
 2667        foreach (var tool in builtInAgent.Tools)
 68        {
 769            _registeredTools[tool.Name] = tool;
 70        }
 71
 72        // Load tool settings after registration
 673        _ = InitializeAsync();
 674    }
 75
 76    protected override Task AfterInitAsync()
 77    {
 2678        foreach (var tool in _registeredTools.Values)
 79        {
 780            tool.Enabled = !Current.DisabledTools.Contains(tool.Name);
 81        }
 682        return Task.CompletedTask;
 83    }
 84
 85    public override Task ResetAsync()
 86    {
 087        foreach (var tool in _registeredTools.Values)
 88        {
 089            tool.Enabled = true;
 90        }
 91
 092        foreach (var state in Current.CategoryStates.Values)
 93        {
 094            state.IsEnabled = true;
 095            state.ApprovalMode = ToolApprovalMode.Allow;
 96        }
 97
 098        Current.DisabledTools.Clear();
 099        return SaveAsync();
 100    }
 101
 102    public IEnumerable<Tool> GetEnabledTools()
 103    {
 4104        var builtIn = _registeredTools.Values.Where(t =>
 4105        {
 3106            if (Current.CategoryStates.TryGetValue(t.Category, out var state))
 4107            {
 0108                return state.IsEnabled && t.Enabled;
 4109            }
 3110            return t.Enabled;
 4111        });
 112
 5113        var mcp = GetMcpTools().Where(t => t.Enabled);
 114
 4115        return builtIn.Concat(mcp);
 116    }
 117
 118    public IEnumerable<Tool> GetAllTools()
 119    {
 7120        return _registeredTools.Values.Concat(GetMcpTools());
 121    }
 122
 123    public Tool? GetTool(string name)
 124    {
 4125        if (_registeredTools.TryGetValue(name, out var tool))
 3126            return tool;
 127
 1128        return GetMcpTools().FirstOrDefault(t => t.Name == name);
 129    }
 130
 131    private IEnumerable<Tool> GetMcpTools()
 132    {
 10133        if (!mcpSettingsProvider.Current.Enabled)
 134        {
 0135            yield break;
 136        }
 137
 138        // перебираем все MCP сервера
 27139        foreach (var server in mcpSettingsProvider.Current.Servers.Where(s =>
 14140            mcpSettingsProvider.Current.ServerEnabledStates.TryGetValue(s.Name, out var serverEnabled)
 14141                ? serverEnabled
 14142                : s.Enabled))
 143        {
 144            // перебор всех тулзов в MCP этом сервере
 15145            foreach (var toolConfig in server.Tools)
 146            {
 4147                var toolName = $"mcp__{server.Name}__{toolConfig.Name}";
 148
 4149                var isEnabled = !mcpSettingsProvider.Current.ToolDisabledStates.Contains(toolName);
 150
 4151                yield return new Tool
 4152                {
 4153                    Name = toolName,
 4154                    DisplayName = toolConfig.Name,
 4155                    Description = toolConfig.Description ?? string.Empty,
 4156                    Category = ToolCategory.Mcp,
 4157                    Enabled = isEnabled,
 4158                    ExampleToSystemMessage = BuildSchemaDescription(toolName, toolConfig),
 4159                    ExecuteAsync = (args) =>
 4160                    {
 1161                        var arguments = GetArgumentNamesFromSchema(toolConfig.InputSchema, args);
 1162                        var mcpArgs = new Dictionary<string, object>
 1163                        {
 1164                            { "serverId", server.Name },
 1165                            { "toolName", toolConfig.Name },
 1166                            { "arguments", arguments }
 1167                        };
 4168
 1169                        return vsBridge.ExecuteToolAsync(BasicEnum.McpCallTool, mcpArgs);
 4170                    }
 4171                };
 172            }
 3173        }
 9174    }
 175
 176    public ToolApprovalMode GetApprovalModeByToolName(string name)
 177    {
 2178        if (name.StartsWith("mcp__"))
 179        {
 2180            var parts = name.Split("__"); // TODO есть риск что сервер в названии содержит __ и тогда он всегда будет Au
 2181            if (parts.Length >= 2)
 182            {
 2183                var serverName = parts[1];
 2184                if (mcpSettingsProvider.Current.ServerApprovalModes.TryGetValue(serverName, out var mode))
 185                {
 1186                    return mode;
 187                }
 188            }
 1189            return ToolApprovalMode.Allow;
 190        }
 191
 0192        var tool = GetTool(name);
 0193        return tool != null && Current.CategoryStates.TryGetValue(tool.Category, out var state)
 0194            ? state.ApprovalMode
 0195            : ToolApprovalMode.Allow;
 196    }
 197
 198    public string GetToolUseSystemInstructions(AppMode mode, bool hasSkills)
 199    {
 2200        var enabledTools = GetEnabledTools().ToList();
 201
 2202        if (!hasSkills) // если нет скиллов, то не нужно их читать
 203        {
 3204            enabledTools = [.. enabledTools.Where(t => t.Name != BasicEnum.ReadSkillContent)];
 205        }
 206
 207        // Filter tools based on mode
 2208        enabledTools = mode switch
 2209        {
 0210            AppMode.Chat => [.. enabledTools.Where(t => t.Category is ToolCategory.ReadFiles or ToolCategory.ModeSwitch)
 2211            AppMode.Agent => enabledTools,
 0212            AppMode.Plan => [.. enabledTools.Where(t => t.Category is ToolCategory.ReadFiles or ToolCategory.ModeSwitch 
 0213            _ => enabledTools
 2214        };
 215
 2216        var sb = new StringBuilder();
 2217        sb.AppendLine($"Current date: {DateTime.Now:f}");
 2218        sb.AppendLine($"Current Application Mode: {mode}");
 2219        sb.AppendLine("Available modes: Chat (for discussion, reading and explanations), Agent (for taking actions and a
 2220        sb.AppendLine("Use Mermaid diagrams for clarity in explanations. This will help you better visualize the answer 
 221
 2222        if (mode == AppMode.Plan)
 223        {
 0224            sb.AppendLine("""
 0225                          ## Planning Mode Instructions
 0226                          You are currently in **PLANNING MODE**. Your goal is to analyze the user's request, explore th
 0227
 0228                          1. **Analyze**: Use available tools to understand the current state of the project.
 0229                          2. **Propose**: Create a structured plan. The plan should be realistic and broken down into lo
 0230                          3. **Format**: Wrap your final plan in `<plan>` tags. Each step should be clear and actionable
 0231
 0232                          **Example:**
 0233                          <plan>
 0234                          1. Create a new service `StorageService`.
 0235                          2. Register it in `Program.cs`.
 0236                          3. Update `SettingsPage` to use the new service.
 0237                          </plan>
 0238
 0239                          In this mode, you should NOT make any changes to files. Your goal is to get user approval for 
 0240                          Once the plan is approved, the mode will be switched to **Agent** for execution.
 0241                          """);
 242        }
 243
 3244        if (enabledTools.Any(t => t.Name == BasicEnum.SwitchMode))
 245        {
 0246            sb.AppendLine($"You can use '{BasicEnum.SwitchMode}' tool to change current mode if you need more tools or w
 247        }
 248
 2249        if (enabledTools.Count == 0)
 250        {
 1251            return sb.ToString();
 252        }
 253
 1254        if (mode == AppMode.Agent)
 255        {
 1256            sb.AppendLine("You are a tool-calling agent. You should take actions to fulfill the user's request.");
 1257            sb.AppendLine();
 258        }
 259
 1260        if (enabledTools.Count > 0)
 261        {
 1262            sb.AppendLine("""
 1263                      ## Tool use instructions
 1264
 1265                      You have access to several tools/functions that you can use at any time to retrieve information an
 1266
 1267                      ## Execution Rules
 1268
 1269                      **Multi-call:** You SHOULD invoke multiple tools within a single message if the task requires it. 
 1270
 1271                      **Prioritize Examples:** Each tool has a specific usage example below. Always follow the tool's sp
 1272
 1273                      **Syntax:** You MUST invoke tools exclusively with the following literal syntax:
 1274
 1275                            <function name="toolName">
 1276                            Parameters
 1277                            </function>
 1278
 1279                      **Constraints:**
 1280                            - Use <function> tags ONLY for actual tool calls.
 1281                            - Stop generation immediately after the last tool call.
 1282                            - No conversational filler or explanations after tools.
 1283
 1284                      The following tools/functions are available to you:
 1285
 1286                      """);
 287
 4288            foreach (var tool in enabledTools)
 289            {
 1290                sb.AppendLine("---");
 1291                sb.AppendLine($"### Tool: {tool.Name}");
 1292                sb.AppendLine($"**Description:** {tool.Description}");
 1293                sb.AppendLine("**Calling:**");
 1294                sb.AppendLine(tool.ExampleToSystemMessage);
 1295                sb.AppendLine();
 296            }
 297
 1298            sb.AppendLine("""
 1299
 1300                          If it seems like the User's request could be solved with the tools, choose the BEST tool for t
 1301                          Do not perform actions with/for hypothetical files. Use tools to deduce which files are releva
 1302                          You can call multiple tools once.
 1303                          """);
 304        }
 305
 1306        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    {
 4314        var schemaElement = toolConfig.InputSchema;
 4315        var requiredArgs = toolConfig.RequiredArguments;
 4316        if (!schemaElement.HasValue || schemaElement.Value.ValueKind != JsonValueKind.Object)
 317        {
 3318            return string.Empty;
 319        }
 320
 1321        var sb = new StringBuilder();
 1322        var schema = schemaElement.Value;
 323
 1324        sb.AppendLine("For example:");
 1325        sb.AppendLine($"<function name =\"{toolName}\">");
 326
 1327        var propDesc = new List<string>();
 328        // Get properties
 1329        if (schema.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Object)
 330        {
 4331            foreach (var prop in props.EnumerateObject())
 332            {
 1333                var isRequired = requiredArgs.Contains(prop.Name);
 1334                var type = "string";
 1335                var description = string.Empty;
 336
 1337                if (prop.Value.ValueKind == JsonValueKind.Object)
 338                {
 1339                    if (prop.Value.TryGetProperty("type", out var typeProp))
 340                    {
 1341                        type = typeProp.GetString() ?? "string";
 342                    }
 1343                    if (prop.Value.TryGetProperty("description", out var descProp))
 344                    {
 0345                        description = descProp.GetString() ?? string.Empty;
 346                    }
 347                }
 348
 349                // TODO еще enum (допустимые значения), minimum, maximum (для чисел), minLength, maxLength, pattern (для
 1350                var requiredMark = isRequired ? " (required)" : "";
 1351                propDesc.Add($"{prop.Name} : [{type}]{requiredMark} {description}");
 352
 1353                sb.AppendLine($"{prop.Name} : {GetSampleByType(type)}");
 354            }
 355        }
 356
 1357        sb.AppendLine("</function>");
 358
 1359        if (propDesc.Count > 0)
 360        {
 1361            sb.AppendLine("*Properties schema:*");
 1362            sb.AppendLine(string.Join(Environment.NewLine, propDesc));
 363        }
 364
 1365        return sb.ToString();
 366    }
 367
 368    private static string GetSampleByType(string propType)
 369    {
 1370        return propType switch
 1371        {
 0372            "number" or "integer" => "123456",
 0373            "boolean" => "true",
 0374            "array" => "[]", // TODO нужно сделать поддержку массивов
 0375            "object" => "object", // TODO вложенный объект...
 1376            _ => "\"string\"",
 1377        };
 378    }
 379
 380    private static object GetArgumentByType(string propType, object arg)
 381    {
 382        // TODO array и object и enum
 1383        return propType switch
 1384        {
 0385            "integer" => int.TryParse(arg.ToString(), out var valueInt) ? valueInt : arg,
 0386            "number" => double.TryParse(arg.ToString(), out var valueDouble) ? valueDouble : arg,
 0387            "boolean" => bool.TryParse(arg.ToString(), out var valueBool) ? valueBool : arg,
 1388            _ => arg
 1389        };
 390    }
 391
 392    private static Dictionary<string, object> GetArgumentNamesFromSchema(JsonElement? schemaElement, IReadOnlyDictionary
 393    {
 1394        if (!schemaElement.HasValue || schemaElement.Value.ValueKind != JsonValueKind.Object)
 395        {
 0396            return [];
 397        }
 398
 1399        var result = new Dictionary<string, object>();
 1400        var schema = schemaElement.Value;
 1401        if (schema.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Object)
 402        {
 4403            foreach (var prop in props.EnumerateObject())
 404            {
 1405                if (args.TryGetValue(prop.Name, out var arg))
 406                {
 1407                    var propType = prop.Value.GetProperty("type").GetString() ?? string.Empty; // TODO этого не может бы
 1408                    result[prop.Name] = GetArgumentByType(propType, arg);
 409                }
 410            }
 411        }
 412
 1413        return result;
 414    }
 415}