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

This commit is contained in:
2026-08-15 21:30:12 +02:00
parent 1522c3480f
commit 62e030e2cf
10 changed files with 519 additions and 397 deletions
@@ -1,9 +1,13 @@
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using FinlyticCore.Dtos.News;
using FinlyticCore.Dtos.Sentiment;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using FinlyticCore.Services;
using FinlyticSentiment.Util;
using Microsoft.Extensions.DependencyInjection;
namespace FinlyticSentiment.Services;
@@ -15,8 +19,6 @@ public interface IFinBertAnalyzerService
/// <summary>
/// Analyzes a news article using language-targeted FinBERT webhooks and returns structured metrics.
/// </summary>
/// <param name="article">The news article DTO to evaluate.</param>
/// <returns>A task returning the FinBERT sentiment analysis result.</returns>
Task<FinBertResultDto?> AnalyzeArticleAsync(NewsArticleDto article);
}
@@ -27,22 +29,16 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
{
private readonly HttpClient _httpClient;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<FinBertAnalyzerService> _logger;
private readonly IFinlyticLogger<FinBertAnalyzerService> _finlyticLogger;
/// <summary>
/// Initializes a new instance of the <see cref="FinBertAnalyzerService"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client instance.</param>
/// <param name="scopeFactory">The service scope factory for DB access.</param>
/// <param name="logger">The logging channel.</param>
public FinBertAnalyzerService(
HttpClient httpClient,
IServiceScopeFactory scopeFactory,
ILogger<FinBertAnalyzerService> logger)
IFinlyticLogger<FinBertAnalyzerService> finlyticLogger)
{
_httpClient = httpClient;
_scopeFactory = scopeFactory;
_logger = logger;
_finlyticLogger = finlyticLogger;
}
/// <summary>
@@ -64,7 +60,7 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
minConfidence = settings.MinConfidenceScore;
}
_logger.LogInformation("[{Channel}] Analyzing article (ID: {Id}, Lang: {Lang}) via webhook: {Url} (MinConf: {Conf})", "SentimentChannel", article.Id, article.Language ?? "de", targetUrl, minConfidence);
await _finlyticLogger.LogInfoAsync(SettingKeys.SentimentChannel, "[FinBertAnalyzerService] Analyzing article (ID: {Id}, Lang: {Lang}) via webhook: {Url} (MinConf: {Conf})", article.Id, article.Language ?? "de", targetUrl, minConfidence);
var requestBody = new
{
@@ -88,13 +84,11 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
using var doc = JsonDocument.Parse(content);
var root = doc.RootElement;
// Handle array response if n8n returns an array of items (e.g. [{ "json": { ... } }])
if (root.ValueKind == JsonValueKind.Array && root.GetArrayLength() > 0)
{
root = root[0];
}
// Unwrap n8n wrapper objects: "json", "output", "data", "result", "body"
if (root.ValueKind == JsonValueKind.Object)
{
if (root.TryGetProperty("json", out var jsonChild) && jsonChild.ValueKind == JsonValueKind.Object)
@@ -134,7 +128,6 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
neu = GetDoubleProp(probsElem, "neutral") ?? neu;
}
// Normalize German vs English labels
string label = rawLabel.Trim().ToUpperInvariant() switch
{
"POSITIV" or "POSITIVE" => "POSITIVE",
@@ -142,7 +135,6 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
_ => "NEUTRAL"
};
// If compoundScore is 0 but probabilities or label indicate sentiment, compute compoundScore
if (Math.Abs(compoundScore) < 0.001)
{
if (pos > 0 || neg > 0)
@@ -173,15 +165,14 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
SummarySnippet = snippet
};
}
}
}
_logger.LogWarning("[{Channel}] n8n Webhook returned non-success status: {StatusCode}. Falling back to rule analyzer.", "SentimentChannel", response.StatusCode);
await _finlyticLogger.LogWarningAsync(SettingKeys.SentimentChannel, "[FinBertAnalyzerService] n8n Webhook returned non-success status: {StatusCode}.", response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Failed to call n8n sentiment webhook. Executing fallback sentiment analyzer.", "SentimentChannel");
await _finlyticLogger.LogErrorAsync(SettingKeys.SentimentChannel, ex, "[FinBertAnalyzerService] Failed to call n8n sentiment webhook.");
}
return null;