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 = string.Empty; double minConfidence = 0.60; using (var scope = _scopeFactory.CreateScope()) { var settingsService = scope.ServiceProvider.GetRequiredService(); bool isEnglish = string.Equals(article.Language, "en", StringComparison.OrdinalIgnoreCase); 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); 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? impact = GetStringProp(root, "impact") ?? "MEDIUM"; string? keyHighlight = GetStringProp(root, "key_highlight") ?? GetStringProp(root, "summary_snippet") ?? GetStringProp(root, "summary") ?? article.Summary; 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; } } 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) } }; 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; } } } 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] Exception during webhook request for article: {Id}", article.Id); } return null; } private static string? GetStringProp(JsonElement element, string propName) { if (element.TryGetProperty(propName, out var prop) && prop.ValueKind == JsonValueKind.String) { return prop.GetString(); } return null; } private static double? GetDoubleProp(JsonElement element, string propName) { 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(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var parsed)) return parsed; } return null; } }