feat(analyzer): dynamic settings, IFinlyticLogger, live log streaming, and EF migration

This commit is contained in:
2026-08-15 21:30:16 +02:00
parent 62e030e2cf
commit 0d370d09e7
13 changed files with 687 additions and 215 deletions
@@ -1,26 +1,33 @@
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 ILogger<N8nEvaluationService> _logger;
private readonly IFinlyticLogger<N8nEvaluationService> _finlyticLogger;
private readonly string _webhookUrl;
public N8nEvaluationService(HttpClient httpClient, IConfiguration configuration, ILogger<N8nEvaluationService> logger)
public N8nEvaluationService(HttpClient httpClient, IConfiguration configuration, IFinlyticLogger<N8nEvaluationService> finlyticLogger)
{
_httpClient = httpClient;
_logger = logger;
_finlyticLogger = finlyticLogger;
_webhookUrl = configuration["N8N:WebhookUrl"] ?? configuration["N8N__WebhookUrl"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(_webhookUrl))
{
_logger.LogWarning("[{Channel}] N8N:WebhookUrl configuration is missing or empty.", "AnalyzerChannel");
_ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] N8N:WebhookUrl configuration is missing or empty.");
}
// Timeout auf 45 Sekunden erhöht für komplexere LLM/Gemini Chains in n8n
_httpClient.Timeout = TimeSpan.FromSeconds(45);
}
@@ -31,16 +38,15 @@ public class N8nEvaluationService : IN8nEvaluationService
{
if (string.IsNullOrWhiteSpace(_webhookUrl))
{
_logger.LogError("[{Channel}] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured.", "AnalyzerChannel", request.TargetAsset.Symbol);
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured.", request.TargetAsset.Symbol);
return null;
}
try
{
_logger.LogInformation("[{Channel}] Sending n8n AI Evaluation request {RequestId} for Asset {Symbol} (ISIN: {Isin}) to {Url}...",
"AnalyzerChannel", request.RequestId, request.TargetAsset.Symbol, request.TargetAsset.Isin, _webhookUrl);
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);
// Typsichere AOT-Serialisierung verwenden
using var content = JsonContent.Create(
request,
FinlyticJsonSerializerContext.Default.N8nAnalysisRequestDto);
@@ -53,11 +59,10 @@ public class N8nEvaluationService : IN8nEvaluationService
if (string.IsNullOrWhiteSpace(contentStr) || contentStr.Trim() == "{}" || contentStr.Trim() == "[]")
{
_logger.LogWarning("[{Channel}] n8n Webhook returned an EMPTY response for Request {RequestId}. Flagging as AI Rejection (Too Risky).", "AnalyzerChannel", request.RequestId);
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.");
}
// N8n schickt Ergebnisse manchmal als JSON-Array [{...}] zurück
string jsonToDeserialize = contentStr.Trim();
if (jsonToDeserialize.StartsWith('[') && jsonToDeserialize.EndsWith(']'))
{
@@ -74,28 +79,28 @@ public class N8nEvaluationService : IN8nEvaluationService
if (responseDto != null && !string.IsNullOrWhiteSpace(responseDto.AiDecision))
{
_logger.LogInformation("[{Channel}] Received n8n AI Response for Request {RequestId}: Decision={Decision}, Score={Score:F2}, Direction={Direction}, Timeframe={Timeframe}",
"AnalyzerChannel", request.RequestId, responseDto.AiDecision, responseDto.EvalScore, responseDto.SuggestedDirection, responseDto.SuggestedTimeframe);
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
{
_logger.LogWarning("[{Channel}] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}",
"AnalyzerChannel", response.StatusCode, request.RequestId);
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}",
response.StatusCode, request.RequestId);
}
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
_logger.LogError(ex, "[{Channel}] Timeout waiting for n8n AI Evaluation Webhook (Request {RequestId}). Consider increasing timeout.", "AnalyzerChannel", request.RequestId);
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Timeout waiting for n8n AI Evaluation Webhook (Request {RequestId}). Consider increasing timeout.", request.RequestId);
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Error calling n8n AI Evaluation Webhook for Request {RequestId}", "AnalyzerChannel", request.RequestId);
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Error calling n8n AI Evaluation Webhook for Request {RequestId}", request.RequestId);
}
return null; // Signals RPC/Service failure to caller
return null;
}
private static N8nAnalysisResponseDto CreateRejectionFallback(N8nAnalysisRequestDto request, string reasoning)