Update LLM

This commit is contained in:
2026-01-05 17:36:19 +07:00
parent fb3a628b19
commit 13474b6abb
2 changed files with 257 additions and 7 deletions

View File

@@ -1,5 +1,7 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
@@ -83,6 +85,12 @@ public class LlmController : BaseController
};
request.Messages.Insert(0, systemMessage);
// Proactively inject backtest details fetching if user is asking for analysis
await InjectBacktestDetailsFetchingIfNeeded(request, user);
// Add helpful context extraction message if backtest ID was found
AddBacktestContextGuidance(request);
// Iterative tool calling: keep looping until we get a final answer without tool calls
// Use adaptive max iterations based on query complexity
int maxIterations = DetermineMaxIterations(request);
@@ -271,16 +279,17 @@ public class LlmController : BaseController
CRITICAL ANALYSIS WORKFLOW (APPLIES TO ALL DATA):
1. RETRIEVE COMPLETE DATA:
- When asked to analyze ANY entity, ALWAYS fetch FULL details first (never rely on summary/paginated data alone)
- Backtests: get_backtest_by_id() for positions + complete metrics
- Bundles: analyze_bundle_backtest() for aggregated statistics
- Indicators: get_indicator_info() for detailed specs
- Backtests: get_backtest_by_id(id='xxx') for positions + complete metrics
- Bundles: analyze_bundle_backtest(bundleRequestId='xxx') for aggregated statistics
- Indicators: get_indicator_info(indicatorType='RsiDivergence') for detailed specs - ALWAYS provide indicatorType parameter
- Use conversation context: "that X" or "this Y" extract ID from previous messages
2. CONTEXT EXTRACTION:
- Pay attention to backtest IDs mentioned in conversation history
- Pay attention to IDs and names mentioned in conversation history (backtest IDs, indicator names, etc.)
- When user says "analyze this backtest" or "show me details", extract the backtest ID from previous messages
- When user asks about an indicator by name (e.g., "What is RsiDivergence?"), extract the indicator type from the question
- If multiple backtests were listed, use the most recently mentioned one or the top-ranked one
- NEVER ask user for IDs that were already provided in conversation
- NEVER ask user for IDs/names that were already provided in conversation
3. BACKTEST DETAIL WORKFLOW:
When user requests backtest details/analysis:
@@ -289,7 +298,15 @@ public class LlmController : BaseController
c) If no ID but refers to "recent/latest" call get_backtests_paginated(sortOrder='desc', pageSize=1) THEN get_backtest_by_id()
d) If completely ambiguous ask ONCE for clarification, then proceed
4. ANALYZE WITH EXPERTISE:
4. INDICATOR WORKFLOW:
When user asks about indicators:
a) General questions ("What indicators exist?") call list_indicators()
b) Specific indicator ("What is RsiDivergence?") extract name from question, call get_indicator_info(indicatorType='RsiDivergence')
c) Explaining usage ("Explain MACD") extract name, call explain_indicator(indicatorType='Macd')
d) NEVER call get_indicator_info() without indicatorType parameter
e) If indicator name is unclear call list_indicators() first to show available options
5. ANALYZE WITH EXPERTISE:
After retrieving data, provide comprehensive analysis:
Backtests: Performance (PnL, growth, ROI), Risk (Sharpe, drawdown), Win rate, Position patterns, Trade duration, Strengths/weaknesses, Recommendations
@@ -297,7 +314,7 @@ public class LlmController : BaseController
Indicators: Use cases, Parameter sensitivity, Combination suggestions, Pitfalls
General: Compare to benchmarks, Statistical significance, Actionable insights
5. BE PROACTIVE:
6. BE PROACTIVE:
- Execute multiple tool iterations for complete data
- Interpret data, don't just return it
- Chain tool calls automatically (list get_by_id analyze)
@@ -338,4 +355,234 @@ public class LlmController : BaseController
request.Messages = trimmedMessages;
}
/// <summary>
/// Proactively injects backtest details fetching when user asks for backtest analysis.
/// Extracts backtest IDs from message content and automatically calls get_backtest_by_id.
/// </summary>
private async Task InjectBacktestDetailsFetchingIfNeeded(LlmChatRequest request, User user)
{
var lastUserMessage = request.Messages.LastOrDefault(m => m.Role == "user");
if (lastUserMessage == null || string.IsNullOrWhiteSpace(lastUserMessage.Content))
return;
var lastMessageLower = lastUserMessage.Content.ToLowerInvariant();
// Check if user is asking for backtest analysis/details
var isRequestingBacktestDetails =
(lastMessageLower.Contains("backtest") || lastMessageLower.Contains("bt")) &&
(lastMessageLower.Contains("analyz") || lastMessageLower.Contains("detail") ||
lastMessageLower.Contains("show") || lastMessageLower.Contains("look at") ||
lastMessageLower.Contains("explain") || lastMessageLower.Contains("performance") ||
lastMessageLower.Contains("this") || lastMessageLower.Contains("that"));
if (!isRequestingBacktestDetails)
return;
// Extract backtest ID from conversation context
var backtestId = ExtractBacktestIdFromConversation(request.Messages);
if (string.IsNullOrEmpty(backtestId))
{
_logger.LogInformation("User requested backtest analysis but no backtest ID found in conversation context");
return;
}
_logger.LogInformation("Proactively fetching backtest details for ID: {BacktestId}", backtestId);
try
{
// Execute get_backtest_by_id tool to fetch complete details
var backtestDetails = await _mcpService.ExecuteToolAsync(
user,
"get_backtest_by_id",
new Dictionary<string, object> { ["id"] = backtestId }
);
_logger.LogInformation("Successfully fetched backtest details for ID: {BacktestId}. Result type: {ResultType}",
backtestId, backtestDetails?.GetType().Name ?? "null");
// Inject the backtest details as a tool result in the conversation
var toolCallId = Guid.NewGuid().ToString();
// Add assistant message indicating tool was called
request.Messages.Add(new LlmMessage
{
Role = "assistant",
Content = "I'll analyze the complete backtest details for you.",
ToolCalls = new List<LlmToolCall>
{
new LlmToolCall
{
Id = toolCallId,
Name = "get_backtest_by_id",
Arguments = new Dictionary<string, object> { ["id"] = backtestId }
}
}
});
// Add tool result message
var serializedResult = JsonSerializer.Serialize(backtestDetails);
_logger.LogInformation("Serialized backtest details length: {Length} characters", serializedResult.Length);
request.Messages.Add(new LlmMessage
{
Role = "tool",
Content = serializedResult,
ToolCallId = toolCallId
});
_logger.LogInformation("Successfully injected backtest details into conversation for ID: {BacktestId}", backtestId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching backtest details for ID: {BacktestId}", backtestId);
// Inject an error message so LLM knows what happened
var toolCallId = Guid.NewGuid().ToString();
request.Messages.Add(new LlmMessage
{
Role = "assistant",
Content = "Let me try to fetch the backtest details.",
ToolCalls = new List<LlmToolCall>
{
new LlmToolCall
{
Id = toolCallId,
Name = "get_backtest_by_id",
Arguments = new Dictionary<string, object> { ["id"] = backtestId }
}
}
});
request.Messages.Add(new LlmMessage
{
Role = "tool",
Content = $"Error: Failed to fetch backtest details - {ex.Message}",
ToolCallId = toolCallId
});
}
}
/// <summary>
/// Adds guidance to the LLM if a backtest ID is present in the message
/// </summary>
private void AddBacktestContextGuidance(LlmChatRequest request)
{
var lastUserMessage = request.Messages.LastOrDefault(m => m.Role == "user");
if (lastUserMessage == null)
return;
var backtestId = ExtractBacktestIdFromConversation(request.Messages);
if (string.IsNullOrEmpty(backtestId))
return;
// Check if we already injected tool results
var hasToolResults = request.Messages.Any(m =>
m.Role == "tool" &&
m.Content?.Contains(backtestId) == true);
if (hasToolResults)
{
// Add a system reminder that the data is already available
request.Messages.Add(new LlmMessage
{
Role = "system",
Content = $"Note: Complete backtest details for ID '{backtestId}' have been fetched and are available in the conversation context. Use this data for your analysis."
});
}
}
/// <summary>
/// Extracts backtest ID from conversation messages by looking for patterns like:
/// - "Backtest ID: xxx"
/// - JSON objects containing backtest IDs
/// - Direct mentions of IDs in recent context
/// </summary>
private string? ExtractBacktestIdFromConversation(List<LlmMessage> messages)
{
_logger.LogDebug("Extracting backtest ID from {Count} messages", messages.Count);
// Look through messages in reverse order (most recent first)
for (int i = messages.Count - 1; i >= 0; i--)
{
var message = messages[i];
if (string.IsNullOrWhiteSpace(message.Content))
continue;
var content = message.Content;
_logger.LogDebug("Checking message {Index} (Role: {Role}): {Preview}",
i, message.Role, content.Length > 100 ? content.Substring(0, 100) + "..." : content);
// Try to extract from JSON in tool results (most reliable)
if (message.Role == "tool")
{
try
{
using var doc = JsonDocument.Parse(content);
var root = doc.RootElement;
// Check if it's a single backtest object with an Id property
if (root.TryGetProperty("id", out var idElement))
{
return idElement.GetString();
}
// Check if it's an array/list of backtests
if (root.ValueKind == JsonValueKind.Array)
{
var firstItem = root.EnumerateArray().FirstOrDefault();
if (firstItem.ValueKind != JsonValueKind.Undefined &&
firstItem.TryGetProperty("id", out var firstId))
{
return firstId.GetString();
}
}
// Check for nested items (paginated results)
if (root.TryGetProperty("items", out var itemsElement) &&
itemsElement.ValueKind == JsonValueKind.Array)
{
var firstItem = itemsElement.EnumerateArray().FirstOrDefault();
if (firstItem.ValueKind != JsonValueKind.Undefined &&
firstItem.TryGetProperty("id", out var firstId))
{
return firstId.GetString();
}
}
}
catch
{
// Not valid JSON or doesn't contain expected structure, continue searching
}
}
// Try to extract from text patterns
// Pattern: "Backtest ID: <guid>" or "ID: <guid>"
var idPattern = @"(?:backtest\s+)?id[:\s]+([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})";
var match = Regex.Match(content, idPattern,
RegexOptions.IgnoreCase);
if (match.Success)
{
var extractedId = match.Groups[1].Value;
_logger.LogInformation("Extracted backtest ID from text pattern: {BacktestId}", extractedId);
return extractedId;
}
// Pattern: standalone GUID that might be a backtest ID
var guidPattern = @"\b([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\b";
var guidMatch = Regex.Match(content, guidPattern,
RegexOptions.IgnoreCase);
if (guidMatch.Success && i >= messages.Count - 5) // Only use standalone GUIDs from recent messages
{
var extractedId = guidMatch.Groups[1].Value;
_logger.LogInformation("Extracted backtest ID from standalone GUID: {BacktestId}", extractedId);
return extractedId;
}
}
_logger.LogWarning("No backtest ID found in conversation messages");
return null;
}
}