210 lines
9.2 KiB
C#
210 lines
9.2 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Defines the sentiment analysis contract for evaluating news articles via FinBERT webhooks.
|
|
/// </summary>
|
|
public interface IFinBertAnalyzerService
|
|
{
|
|
/// <summary>
|
|
/// Analyzes a news article using language-targeted FinBERT webhooks and returns structured metrics.
|
|
/// </summary>
|
|
Task<FinBertResultDto?> AnalyzeArticleAsync(NewsArticleDto article);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implementation of the sentiment analysis contract for evaluating news articles via FinBERT webhooks.
|
|
/// </summary>
|
|
public class FinBertAnalyzerService : IFinBertAnalyzerService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly IFinlyticLogger<FinBertAnalyzerService> _finlyticLogger;
|
|
|
|
public FinBertAnalyzerService(
|
|
HttpClient httpClient,
|
|
IServiceScopeFactory scopeFactory,
|
|
IFinlyticLogger<FinBertAnalyzerService> finlyticLogger)
|
|
{
|
|
_httpClient = httpClient;
|
|
_scopeFactory = scopeFactory;
|
|
_finlyticLogger = finlyticLogger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyzes a news article using language-targeted FinBERT webhooks and returns structured metrics.
|
|
/// </summary>
|
|
public async Task<FinBertResultDto?> AnalyzeArticleAsync(NewsArticleDto article)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(article);
|
|
|
|
string targetUrl = string.Empty;
|
|
double minConfidence = 0.60;
|
|
using (var scope = _scopeFactory.CreateScope())
|
|
{
|
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
|
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;
|
|
}
|
|
}
|