using System; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FinlyticAnalyzer.Util; using FinlyticCore.Models.Analyzer; using FinlyticCore.Services; using FinlyticCore.Util; using Microsoft.Extensions.Configuration; namespace FinlyticAnalyzer.Services; public class N8nEvaluationService : IN8nEvaluationService { private readonly HttpClient _httpClient; private readonly ISettingsService _settingsService; private readonly IConfiguration _configuration; private readonly IFinlyticLogger _finlyticLogger; public N8nEvaluationService( HttpClient httpClient, ISettingsService settingsService, IConfiguration configuration, IFinlyticLogger finlyticLogger) { _httpClient = httpClient; _settingsService = settingsService; _configuration = configuration; _finlyticLogger = finlyticLogger; _httpClient.Timeout = TimeSpan.FromSeconds(45); } /// /// Evaluates an asset asynchronously using N8n / Gemini workflows. /// public async Task EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default) { string webhookUrl = await _settingsService.GetSettingAsync(SettingKeys.N8nWebhookUrl, cancellationToken); if (string.IsNullOrWhiteSpace(webhookUrl)) { webhookUrl = _configuration["N8N:WebhookUrl"] ?? _configuration["N8N__WebhookUrl"] ?? string.Empty; } if (string.IsNullOrWhiteSpace(webhookUrl)) { await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured in dynamic settings or environment.", request.TargetAsset.Symbol); return null; } try { await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Sending n8n AI Evaluation request {RequestId} for Asset {Symbol} (ISIN: {Isin}) to {Url}...", request.RequestId, request.TargetAsset.Symbol, request.TargetAsset.Isin, webhookUrl); using var content = JsonContent.Create( request, FinlyticJsonSerializerContext.Default.N8nAnalysisRequestDto); using var response = await _httpClient.PostAsync(webhookUrl, content, cancellationToken); if (response.IsSuccessStatusCode) { var contentStr = await response.Content.ReadAsStringAsync(cancellationToken); if (string.IsNullOrWhiteSpace(contentStr) || contentStr.Trim() == "{}" || contentStr.Trim() == "[]") { await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned an EMPTY response for Request {RequestId}. Flagging as AI Rejection (Too Risky).", request.RequestId); return CreateRejectionFallback(request, "Die KI (n8n/Gemini) stuft den Trade als zu riskant ein und empfiehlt keine Positionierung."); } string jsonToDeserialize = contentStr.Trim(); if (jsonToDeserialize.StartsWith('[') && jsonToDeserialize.EndsWith(']')) { using var doc = JsonDocument.Parse(jsonToDeserialize); if (doc.RootElement.ValueKind == JsonValueKind.Array && doc.RootElement.GetArrayLength() > 0) { jsonToDeserialize = doc.RootElement[0].GetRawText(); } } var responseDto = JsonSerializer.Deserialize( jsonToDeserialize, FinlyticJsonSerializerContext.Default.N8nAnalysisResponseDto); if (responseDto != null && !string.IsNullOrWhiteSpace(responseDto.AiDecision)) { await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Received n8n AI Response for Request {RequestId}: Decision={Decision}, Score={Score:F2}, Direction={Direction}, Timeframe={Timeframe}", request.RequestId, responseDto.AiDecision, responseDto.EvalScore, responseDto.SuggestedDirection, responseDto.SuggestedTimeframe); return responseDto; } } else { await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}", response.StatusCode, request.RequestId); } } catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) { await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Timeout waiting for n8n AI Evaluation Webhook (Request {RequestId}). Consider increasing timeout.", request.RequestId); } catch (Exception ex) { await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Error calling n8n AI Evaluation Webhook for Request {RequestId}", request.RequestId); } return null; } private static N8nAnalysisResponseDto CreateRejectionFallback(N8nAnalysisRequestDto request, string reasoning) { return new N8nAnalysisResponseDto { RequestId = request.RequestId, AiDecision = "Rejected", EvalScore = 0.0, SuggestedDirection = "NONE", SuggestedRisk = request.UserPreferences?.RiskTolerance ?? "Moderate", SuggestedTimeframe = request.UserPreferences?.TimeframeFormatted ?? "1D", AiReasoning = reasoning }; } }