feat(sentiment): add persistent sentiment entities, summaries, SentimentDbService and MQTT RPC refactoring

This commit is contained in:
2026-08-24 21:35:48 +02:00
parent 600ccf299e
commit 12e7b57b16
24 changed files with 1510 additions and 1300 deletions
@@ -52,26 +52,13 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
double minConfidence = 0.60;
using (var scope = _scopeFactory.CreateScope())
{
var settingsService = scope.ServiceProvider.GetService<ISettingsService>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
bool isEnglish = string.Equals(article.Language, "en", StringComparison.OrdinalIgnoreCase);
if (settingsService != null)
{
targetUrl = isEnglish
? await settingsService.GetSettingAsync(SettingKeys.EnglishWebhookUrl)
: await settingsService.GetSettingAsync(SettingKeys.GermanWebhookUrl);
minConfidence = await settingsService.GetSettingAsync(SettingKeys.MinimumConfidenceThreshold);
}
if (string.IsNullOrWhiteSpace(targetUrl))
{
var settingsDb = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
var settings = await settingsDb.GetSettingsAsync();
targetUrl = isEnglish
? settings.EnglishWebhookUrl
: settings.GermanWebhookUrl;
minConfidence = settings.MinConfidenceScore;
}
targetUrl = isEnglish
? await settingsService.GetSettingAsync(SettingKeys.EnglishWebhookUrl)
: await settingsService.GetSettingAsync(SettingKeys.GermanWebhookUrl);
minConfidence = await settingsService.GetSettingAsync(SettingKeys.MinimumConfidenceThreshold);
}
await _finlyticLogger.LogInfoAsync(SettingKeys.SentimentChannel, "[FinBertAnalyzerService] Analyzing article (ID: {Id}, Lang: {Lang}) via webhook: {Url} (MinConf: {Conf})", article.Id, article.Language ?? "de", targetUrl, minConfidence);
@@ -128,11 +115,11 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
double confidence = GetDoubleProp(root, "confidence")
?? GetDoubleProp(root, "confidence_score")
?? 0.5;
string snippet = GetStringProp(root, "summary_snippet")
string? impact = GetStringProp(root, "impact") ?? "MEDIUM";
string? keyHighlight = GetStringProp(root, "key_highlight")
?? GetStringProp(root, "summary_snippet")
?? GetStringProp(root, "summary")
?? GetStringProp(root, "text")
?? article.Summary
?? article.Title;
?? article.Summary;
double pos = 0.0, neg = 0.0, neu = 1.0;
if (root.TryGetProperty("probabilities", out var probsElem) && probsElem.ValueKind == JsonValueKind.Object)
@@ -165,44 +152,57 @@ public class FinBertAnalyzerService : IFinBertAnalyzerService
}
}
return new FinBertResultDto
var result = new FinBertResultDto
{
Label = label,
CompoundScore = Math.Round(compoundScore, 4),
Confidence = Math.Round(confidence, 4),
Impact = impact.ToUpperInvariant(),
KeyHighlight = keyHighlight,
Probabilities = new FinBertProbabilities
{
Positive = Math.Round(pos, 4),
Negative = Math.Round(neg, 4),
Neutral = Math.Round(neu, 4)
},
SummarySnippet = snippet
}
};
await _finlyticLogger.LogInfoAsync(SettingKeys.SentimentChannel, "[FinBertAnalyzerService] Successfully analyzed article {Id}: {Label} (Compound: {Score}, Conf: {Conf}, Impact: {Impact})", article.Id, result.Label, result.CompoundScore, result.Confidence, result.Impact);
return result;
}
}
}
await _finlyticLogger.LogWarningAsync(SettingKeys.SentimentChannel, "[FinBertAnalyzerService] n8n Webhook returned non-success status: {StatusCode}.", response.StatusCode);
else
{
await _finlyticLogger.LogWarningAsync(SettingKeys.SentimentChannel, "[FinBertAnalyzerService] Webhook call failed with status: {Status} (Article ID: {Id})", response.StatusCode, article.Id);
}
}
catch (Exception ex)
{
await _finlyticLogger.LogErrorAsync(SettingKeys.SentimentChannel, ex, "[FinBertAnalyzerService] Failed to call n8n sentiment webhook.");
await _finlyticLogger.LogErrorAsync(SettingKeys.SentimentChannel, ex, "[FinBertAnalyzerService] Exception during webhook request for article: {Id}", article.Id);
}
return null;
}
private static string? GetStringProp(JsonElement elem, string propName)
private static string? GetStringProp(JsonElement element, string propName)
{
return elem.TryGetProperty(propName, out var prop) && prop.ValueKind == JsonValueKind.String ? prop.GetString() : null;
if (element.TryGetProperty(propName, out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString();
}
return null;
}
private static double? GetDoubleProp(JsonElement elem, string propName)
private static double? GetDoubleProp(JsonElement element, string propName)
{
if (elem.TryGetProperty(propName, out var prop))
if (element.TryGetProperty(propName, out var prop))
{
if (prop.ValueKind == JsonValueKind.Number && prop.TryGetDouble(out var d)) return d;
if (prop.ValueKind == JsonValueKind.String && double.TryParse(prop.GetString(), out var parsed)) return parsed;
if (prop.ValueKind == JsonValueKind.Number && prop.TryGetDouble(out var d))
return d;
if (prop.ValueKind == JsonValueKind.String && double.TryParse(prop.GetString(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var parsed))
return parsed;
}
return null;
}