Add system message to LLM requests and improve indicator type resolution

- Introduced a system message in LlmController to clarify that tools are optional for LLM responses, enhancing user guidance.
- Refactored indicator type resolution in IndicatorTools to support fuzzy matching and provide suggestions for invalid types, improving user experience and error handling.
- Updated methods to utilize the new resolution logic, ensuring consistent handling of indicator types across the application.
This commit is contained in:
2026-01-04 02:00:51 +07:00
parent 8ce7650bbf
commit df27bbdfa1
2 changed files with 162 additions and 26 deletions

View File

@@ -57,6 +57,23 @@ public class LlmController : BaseController
var availableTools = await _mcpService.GetAvailableToolsAsync(); var availableTools = await _mcpService.GetAvailableToolsAsync();
request.Tools = availableTools.ToList(); 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 // Send chat request to LLM
var response = await _llmService.ChatAsync(user, request); var response = await _llmService.ChatAsync(user, request);

View File

@@ -74,21 +74,25 @@ public class IndicatorTools
{ {
try try
{ {
if (!Enum.TryParse<IndicatorType>(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}"); throw new ArgumentException($"Information not available for indicator: {indicatorType}");
} }
var resolvedType = type.Value;
return new return new
{ {
Type = type.ToString(), Type = resolvedType.ToString(),
SignalType = GetSignalType(type).ToString(), SignalType = GetSignalType(resolvedType).ToString(),
Category = GetCategory(type), Category = GetCategory(resolvedType),
Name = info.Name, Name = info.Name,
Description = info.Description, Description = info.Description,
LongDescription = info.LongDescription, LongDescription = info.LongDescription,
@@ -114,21 +118,24 @@ public class IndicatorTools
{ {
try try
{ {
if (!Enum.TryParse<IndicatorType>(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}"); throw new ArgumentException($"Information not available for indicator: {indicatorType}");
} }
var resolvedType = type.Value;
var explanation = new var explanation = new
{ {
Type = type.ToString(), Type = resolvedType.ToString(),
SignalType = GetSignalType(type).ToString(), SignalType = GetSignalType(resolvedType).ToString(),
Category = GetCategory(type), Category = GetCategory(resolvedType),
Name = info.Name, Name = info.Name,
HowItWorks = info.LongDescription, HowItWorks = info.LongDescription,
WhenToUse = info.UseCases, WhenToUse = info.UseCases,
@@ -143,7 +150,7 @@ public class IndicatorTools
}), }),
RecommendedParameters = info.RecommendedParameters, RecommendedParameters = info.RecommendedParameters,
TradingStyleRecommendations = info.TradingStyleRecommendations, TradingStyleRecommendations = info.TradingStyleRecommendations,
Examples = includeExamples ? GetIndicatorExamples(type) : null Examples = includeExamples ? GetIndicatorExamples(resolvedType) : null
}; };
return explanation; return explanation;
@@ -229,16 +236,20 @@ public class IndicatorTools
{ {
try try
{ {
if (!Enum.TryParse<IndicatorType>(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}"); throw new ArgumentException($"Information not available for indicator: {indicatorType}");
} }
var resolvedType = type.Value;
var refinements = new List<ParameterRefinement>(); var refinements = new List<ParameterRefinement>();
foreach (var param in info.Parameters) foreach (var param in info.Parameters)
@@ -249,9 +260,9 @@ public class IndicatorTools
Description = param.Description, Description = param.Description,
CurrentRecommended = param.RecommendedValue, CurrentRecommended = param.RecommendedValue,
RefinedValues = GetRefinedParameterValues( RefinedValues = GetRefinedParameterValues(
type, param.Name, tradingStyle, marketVolatility), resolvedType, param.Name, tradingStyle, marketVolatility),
Reasoning = GetParameterRefinementReasoning( Reasoning = GetParameterRefinementReasoning(
type, param.Name, tradingStyle, marketVolatility) resolvedType, param.Name, tradingStyle, marketVolatility)
}; };
refinements.Add(refinement); refinements.Add(refinement);
@@ -259,12 +270,12 @@ public class IndicatorTools
return new return new
{ {
IndicatorType = type.ToString(), IndicatorType = resolvedType.ToString(),
SignalType = GetSignalType(type).ToString(), SignalType = GetSignalType(resolvedType).ToString(),
TradingStyle = tradingStyle ?? "General", TradingStyle = tradingStyle ?? "General",
MarketVolatility = marketVolatility ?? "Normal", MarketVolatility = marketVolatility ?? "Normal",
ParameterRefinements = refinements, ParameterRefinements = refinements,
GeneralAdvice = GetGeneralParameterAdvice(type, tradingStyle, marketVolatility) GeneralAdvice = GetGeneralParameterAdvice(resolvedType, tradingStyle, marketVolatility)
}; };
} }
catch (Exception ex) catch (Exception ex)
@@ -281,16 +292,39 @@ public class IndicatorTools
{ {
try try
{ {
var types = indicatorTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) var typeStrings = indicatorTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(t => t.Trim()) .Select(t => t.Trim())
.Where(t => Enum.TryParse<IndicatorType>(t, true, out _))
.Select(t => Enum.Parse<IndicatorType>(t, true))
.Distinct()
.ToList(); .ToList();
var types = new List<IndicatorType>();
var invalidTypes = new List<string>();
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) 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 => var comparisons = types.Select(type =>
@@ -325,6 +359,91 @@ public class IndicatorTools
#region Helper Methods #region Helper Methods
/// <summary>
/// Resolves an indicator type from user input, supporting fuzzy matching
/// </summary>
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<IndicatorType>(input, true, out var exactMatch))
{
return exactMatch;
}
// Try fuzzy matching by name
foreach (var type in Enum.GetValues<IndicatorType>())
{
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;
}
/// <summary>
/// Gets suggestions for invalid indicator types
/// </summary>
private string GetIndicatorSuggestions(string input)
{
if (string.IsNullOrWhiteSpace(input))
return "Please provide an indicator type.";
var normalizedInput = input.Trim().ToLowerInvariant();
var suggestions = new List<string>();
// Find similar indicators
foreach (var type in Enum.GetValues<IndicatorType>())
{
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) private SignalType GetSignalType(IndicatorType type)
{ {
return type switch return type switch