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; + } }