diff --git a/docs/TASK_ENVIRONMENTS_SETUP.md b/docs/TASK_ENVIRONMENTS_SETUP.md
index ae4c475c..6393023c 100644
--- a/docs/TASK_ENVIRONMENTS_SETUP.md
+++ b/docs/TASK_ENVIRONMENTS_SETUP.md
@@ -175,3 +175,6 @@ Task Environment (offset ports)
2. Install dev-manager-mcp: See `docs/INSTALL_VIBE_KANBAN_AND_DEV_MANAGER.md`
3. Configure agent command: See `.cursor/commands/start-dev-env.md`
+
+
+
diff --git a/src/Managing.Api/Controllers/LlmController.cs b/src/Managing.Api/Controllers/LlmController.cs
index 4e0c598a..4a3b5e92 100644
--- a/src/Managing.Api/Controllers/LlmController.cs
+++ b/src/Managing.Api/Controllers/LlmController.cs
@@ -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;
}
+
+ ///
+ /// Proactively injects backtest details fetching when user asks for backtest analysis.
+ /// Extracts backtest IDs from message content and automatically calls get_backtest_by_id.
+ ///
+ 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 { ["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
+ {
+ new LlmToolCall
+ {
+ Id = toolCallId,
+ Name = "get_backtest_by_id",
+ Arguments = new Dictionary { ["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
+ {
+ new LlmToolCall
+ {
+ Id = toolCallId,
+ Name = "get_backtest_by_id",
+ Arguments = new Dictionary { ["id"] = backtestId }
+ }
+ }
+ });
+
+ request.Messages.Add(new LlmMessage
+ {
+ Role = "tool",
+ Content = $"Error: Failed to fetch backtest details - {ex.Message}",
+ ToolCallId = toolCallId
+ });
+ }
+ }
+
+ ///
+ /// Adds guidance to the LLM if a backtest ID is present in the message
+ ///
+ 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."
+ });
+ }
+ }
+
+ ///
+ /// 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
+ ///
+ private string? ExtractBacktestIdFromConversation(List 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: " or "ID: "
+ 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;
+ }
}