diff --git a/src/Managing.Api/Controllers/LlmController.cs b/src/Managing.Api/Controllers/LlmController.cs index 67c5265b..d02bb899 100644 --- a/src/Managing.Api/Controllers/LlmController.cs +++ b/src/Managing.Api/Controllers/LlmController.cs @@ -57,6 +57,23 @@ public class LlmController : BaseController var availableTools = await _mcpService.GetAvailableToolsAsync(); request.Tools = availableTools.ToList(); + // Add system message to clarify that tools are optional and the LLM can respond directly + // Check if a system message already exists + var hasSystemMessage = request.Messages.Any(m => m.Role == "system"); + if (!hasSystemMessage) + { + var systemMessage = new LlmMessage + { + Role = "system", + Content = "You are a helpful AI assistant with expertise in quantitative finance, algorithmic trading, and financial mathematics. " + + "You can answer questions directly using your knowledge. " + + "Tools are available for specific operations (backtesting, agent management, market data retrieval, etc.) but are optional. " + + "Use tools only when they are needed for the specific task. " + + "For general questions, explanations, calculations, or discussions, respond directly without using tools." + }; + request.Messages.Insert(0, systemMessage); + } + // Send chat request to LLM var response = await _llmService.ChatAsync(user, request); diff --git a/src/Managing.Mcp/Tools/IndicatorTools.cs b/src/Managing.Mcp/Tools/IndicatorTools.cs index df8afee3..6986b761 100644 --- a/src/Managing.Mcp/Tools/IndicatorTools.cs +++ b/src/Managing.Mcp/Tools/IndicatorTools.cs @@ -74,21 +74,25 @@ public class IndicatorTools { try { - if (!Enum.TryParse(indicatorType, true, out var type)) + var type = ResolveIndicatorType(indicatorType); + if (type == null) { - throw new ArgumentException($"Invalid indicator type: {indicatorType}"); + var suggestions = GetIndicatorSuggestions(indicatorType); + throw new ArgumentException($"Invalid indicator type: '{indicatorType}'. {suggestions}"); } - if (!_indicatorInfoCache.TryGetValue(type, out var info)) + if (!_indicatorInfoCache.TryGetValue(type.Value, out var info)) { throw new ArgumentException($"Information not available for indicator: {indicatorType}"); } + var resolvedType = type.Value; + return new { - Type = type.ToString(), - SignalType = GetSignalType(type).ToString(), - Category = GetCategory(type), + Type = resolvedType.ToString(), + SignalType = GetSignalType(resolvedType).ToString(), + Category = GetCategory(resolvedType), Name = info.Name, Description = info.Description, LongDescription = info.LongDescription, @@ -114,21 +118,24 @@ public class IndicatorTools { try { - if (!Enum.TryParse(indicatorType, true, out var type)) + var type = ResolveIndicatorType(indicatorType); + if (type == null) { - throw new ArgumentException($"Invalid indicator type: {indicatorType}"); + var suggestions = GetIndicatorSuggestions(indicatorType); + throw new ArgumentException($"Invalid indicator type: '{indicatorType}'. {suggestions}"); } - if (!_indicatorInfoCache.TryGetValue(type, out var info)) + if (!_indicatorInfoCache.TryGetValue(type.Value, out var info)) { throw new ArgumentException($"Information not available for indicator: {indicatorType}"); } + var resolvedType = type.Value; var explanation = new { - Type = type.ToString(), - SignalType = GetSignalType(type).ToString(), - Category = GetCategory(type), + Type = resolvedType.ToString(), + SignalType = GetSignalType(resolvedType).ToString(), + Category = GetCategory(resolvedType), Name = info.Name, HowItWorks = info.LongDescription, WhenToUse = info.UseCases, @@ -143,7 +150,7 @@ public class IndicatorTools }), RecommendedParameters = info.RecommendedParameters, TradingStyleRecommendations = info.TradingStyleRecommendations, - Examples = includeExamples ? GetIndicatorExamples(type) : null + Examples = includeExamples ? GetIndicatorExamples(resolvedType) : null }; return explanation; @@ -229,16 +236,20 @@ public class IndicatorTools { try { - if (!Enum.TryParse(indicatorType, true, out var type)) + var type = ResolveIndicatorType(indicatorType); + if (type == null) { - throw new ArgumentException($"Invalid indicator type: {indicatorType}"); + var suggestions = GetIndicatorSuggestions(indicatorType); + throw new ArgumentException($"Invalid indicator type: '{indicatorType}'. {suggestions}"); } - if (!_indicatorInfoCache.TryGetValue(type, out var info)) + if (!_indicatorInfoCache.TryGetValue(type.Value, out var info)) { throw new ArgumentException($"Information not available for indicator: {indicatorType}"); } + var resolvedType = type.Value; + var refinements = new List(); foreach (var param in info.Parameters) @@ -249,9 +260,9 @@ public class IndicatorTools Description = param.Description, CurrentRecommended = param.RecommendedValue, RefinedValues = GetRefinedParameterValues( - type, param.Name, tradingStyle, marketVolatility), + resolvedType, param.Name, tradingStyle, marketVolatility), Reasoning = GetParameterRefinementReasoning( - type, param.Name, tradingStyle, marketVolatility) + resolvedType, param.Name, tradingStyle, marketVolatility) }; refinements.Add(refinement); @@ -259,12 +270,12 @@ public class IndicatorTools return new { - IndicatorType = type.ToString(), - SignalType = GetSignalType(type).ToString(), + IndicatorType = resolvedType.ToString(), + SignalType = GetSignalType(resolvedType).ToString(), TradingStyle = tradingStyle ?? "General", MarketVolatility = marketVolatility ?? "Normal", ParameterRefinements = refinements, - GeneralAdvice = GetGeneralParameterAdvice(type, tradingStyle, marketVolatility) + GeneralAdvice = GetGeneralParameterAdvice(resolvedType, tradingStyle, marketVolatility) }; } catch (Exception ex) @@ -281,16 +292,39 @@ public class IndicatorTools { try { - var types = indicatorTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + var typeStrings = indicatorTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(t => t.Trim()) - .Where(t => Enum.TryParse(t, true, out _)) - .Select(t => Enum.Parse(t, true)) - .Distinct() .ToList(); + var types = new List(); + var invalidTypes = new List(); + + foreach (var typeString in typeStrings) + { + var resolvedType = ResolveIndicatorType(typeString); + if (resolvedType.HasValue) + { + types.Add(resolvedType.Value); + } + else + { + invalidTypes.Add(typeString); + } + } + + types = types.Distinct().ToList(); + if (types.Count == 0) { - throw new ArgumentException("At least one valid indicator type is required"); + var suggestions = invalidTypes.Count > 0 + ? $"Invalid indicator types: {string.Join(", ", invalidTypes)}. {GetIndicatorSuggestions(invalidTypes.First())}" + : "At least one valid indicator type is required"; + throw new ArgumentException(suggestions); + } + + if (invalidTypes.Count > 0) + { + _logger.LogWarning("Some indicator types could not be resolved: {InvalidTypes}", string.Join(", ", invalidTypes)); } var comparisons = types.Select(type => @@ -325,6 +359,91 @@ public class IndicatorTools #region Helper Methods + /// + /// Resolves an indicator type from user input, supporting fuzzy matching + /// + private IndicatorType? ResolveIndicatorType(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return null; + + var normalizedInput = input.Trim().Replace(" ", "").Replace("-", "").Replace("_", "").ToLowerInvariant(); + + // First try exact enum match (case-insensitive) + if (Enum.TryParse(input, true, out var exactMatch)) + { + return exactMatch; + } + + // Try fuzzy matching by name + foreach (var type in Enum.GetValues()) + { + if (type == IndicatorType.Composite) continue; + + var name = GetIndicatorName(type).Replace(" ", "").Replace("-", "").Replace("_", "").ToLowerInvariant(); + var typeName = type.ToString().ToLowerInvariant(); + var category = GetCategory(type).ToLowerInvariant(); + + // Check if input matches name, type name, or category + if (name.Contains(normalizedInput) || + normalizedInput.Contains(name) || + typeName.Contains(normalizedInput) || + normalizedInput.Contains(typeName) || + category.Contains(normalizedInput) || + normalizedInput.Contains(category)) + { + // Special handling for "RSI" - prefer RsiDivergence over RsiDivergenceConfirm + if (normalizedInput == "rsi" || normalizedInput == "rsidivergence") + { + return IndicatorType.RsiDivergence; + } + return type; + } + } + + return null; + } + + /// + /// Gets suggestions for invalid indicator types + /// + private string GetIndicatorSuggestions(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return "Please provide an indicator type."; + + var normalizedInput = input.Trim().ToLowerInvariant(); + var suggestions = new List(); + + // Find similar indicators + foreach (var type in Enum.GetValues()) + { + if (type == IndicatorType.Composite) continue; + + var name = GetIndicatorName(type).ToLowerInvariant(); + var typeName = type.ToString().ToLowerInvariant(); + var category = GetCategory(type).ToLowerInvariant(); + + if (name.Contains(normalizedInput) || + typeName.Contains(normalizedInput) || + category.Contains(normalizedInput) || + normalizedInput.Contains(name) || + normalizedInput.Contains(typeName) || + normalizedInput.Contains(category)) + { + suggestions.Add($"'{type}' ({GetIndicatorName(type)})"); + } + } + + if (suggestions.Count > 0) + { + return $"Did you mean: {string.Join(", ", suggestions.Take(5))}? Use 'list_indicators' to see all available indicators."; + } + + // If no matches, suggest common indicators + return "Available RSI indicators: 'RsiDivergence', 'RsiDivergenceConfirm'. Use 'list_indicators' to see all available indicators."; + } + private SignalType GetSignalType(IndicatorType type) { return type switch