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 FinlyticCore.Services;
using FinlyticSentiment.Util;
using Microsoft.Extensions.DependencyInjection;
namespace FinlyticSentiment.Services;
///
/// Defines the sentiment analysis contract for evaluating news articles via FinBERT webhooks.
///
public interface IFinBertAnalyzerService
{
///
/// Analyzes a news article using language-targeted FinBERT webhooks and returns structured metrics.
///
Task AnalyzeArticleAsync(NewsArticleDto article);
}
///
/// Implementation of the sentiment analysis contract for evaluating news articles via FinBERT webhooks.
///
public class FinBertAnalyzerService : IFinBertAnalyzerService
{
private readonly HttpClient _httpClient;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IFinlyticLogger _finlyticLogger;
public FinBertAnalyzerService(
HttpClient httpClient,
IServiceScopeFactory scopeFactory,
IFinlyticLogger finlyticLogger)
{
_httpClient = httpClient;
_scopeFactory = scopeFactory;
_finlyticLogger = finlyticLogger;
}
///
/// Analyzes a news article using language-targeted FinBERT webhooks and returns structured metrics.
///
public async Task AnalyzeArticleAsync(NewsArticleDto article)
{
ArgumentNullException.ThrowIfNull(article);
string targetUrl;
double minConfidence;
using (var scope = _scopeFactory.CreateScope())
{
var settingsDb = scope.ServiceProvider.GetRequiredService();
var settings = await settingsDb.GetSettingsAsync();
targetUrl = string.Equals(article.Language, "en", StringComparison.OrdinalIgnoreCase)
? settings.EnglishWebhookUrl
: settings.GermanWebhookUrl;
minConfidence = settings.MinConfidenceScore;
}
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
{
article_id = article.Id.ToString(),
title = article.Title,
summary = article.Summary ?? string.Empty,
content = article.ContentRaw ?? string.Empty,
source = article.Author ?? "FinlyticNews",
source_url = article.SourceUrl,
published_at = article.PublishedAt.ToString("o")
};
try
{
using var response = await _httpClient.PostAsJsonAsync(targetUrl, requestBody);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(content))
{
using var doc = JsonDocument.Parse(content);
var root = doc.RootElement;
if (root.ValueKind == JsonValueKind.Array && root.GetArrayLength() > 0)
{
root = root[0];
}
if (root.ValueKind == JsonValueKind.Object)
{
if (root.TryGetProperty("json", out var jsonChild) && jsonChild.ValueKind == JsonValueKind.Object)
root = jsonChild;
else if (root.TryGetProperty("output", out var outChild) && outChild.ValueKind == JsonValueKind.Object)
root = outChild;
else if (root.TryGetProperty("data", out var dataChild) && dataChild.ValueKind == JsonValueKind.Object)
root = dataChild;
else if (root.TryGetProperty("body", out var bodyChild) && bodyChild.ValueKind == JsonValueKind.Object)
root = bodyChild;
}
if (root.ValueKind == JsonValueKind.Object)
{
string rawLabel = GetStringProp(root, "label")
?? GetStringProp(root, "sentiment_label")
?? GetStringProp(root, "sentiment")
?? "NEUTRAL";
double compoundScore = GetDoubleProp(root, "compound_score")
?? GetDoubleProp(root, "compoundScore")
?? GetDoubleProp(root, "score")
?? 0.0;
double confidence = GetDoubleProp(root, "confidence")
?? GetDoubleProp(root, "confidence_score")
?? 0.5;
string snippet = GetStringProp(root, "summary_snippet")
?? GetStringProp(root, "summary")
?? GetStringProp(root, "text")
?? article.Summary
?? article.Title;
double pos = 0.0, neg = 0.0, neu = 1.0;
if (root.TryGetProperty("probabilities", out var probsElem) && probsElem.ValueKind == JsonValueKind.Object)
{
pos = GetDoubleProp(probsElem, "positive") ?? pos;
neg = GetDoubleProp(probsElem, "negative") ?? neg;
neu = GetDoubleProp(probsElem, "neutral") ?? neu;
}
string label = rawLabel.Trim().ToUpperInvariant() switch
{
"POSITIV" or "POSITIVE" => "POSITIVE",
"NEGATIV" or "NEGATIVE" => "NEGATIVE",
_ => "NEUTRAL"
};
if (Math.Abs(compoundScore) < 0.001)
{
if (pos > 0 || neg > 0)
{
compoundScore = pos - neg;
}
else if (label == "POSITIVE")
{
compoundScore = 0.8;
}
else if (label == "NEGATIVE")
{
compoundScore = -0.8;
}
}
return new FinBertResultDto
{
Label = label,
CompoundScore = Math.Round(compoundScore, 4),
Confidence = Math.Round(confidence, 4),
Probabilities = new FinBertProbabilities
{
Positive = Math.Round(pos, 4),
Negative = Math.Round(neg, 4),
Neutral = Math.Round(neu, 4)
},
SummarySnippet = snippet
};
}
}
}
await _finlyticLogger.LogWarningAsync(SettingKeys.SentimentChannel, "[FinBertAnalyzerService] n8n Webhook returned non-success status: {StatusCode}.", response.StatusCode);
}
catch (Exception ex)
{
await _finlyticLogger.LogErrorAsync(SettingKeys.SentimentChannel, ex, "[FinBertAnalyzerService] Failed to call n8n sentiment webhook.");
}
return null;
}
private static string? GetStringProp(JsonElement elem, string propName)
{
return elem.TryGetProperty(propName, out var prop) && prop.ValueKind == JsonValueKind.String ? prop.GetString() : null;
}
private static double? GetDoubleProp(JsonElement elem, string propName)
{
if (elem.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;
}
return null;
}
}