using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using UniVerse.Application.Interfaces; using UniVerse.Application.Prompts; namespace UniVerse.Infrastructure.ExternalServices; public class LlmClient : ILlmClient { private readonly HttpClient _http; private readonly IConfiguration _config; private readonly IReviewPromptService _reviewPrompts; private readonly ILogger _logger; public LlmClient( HttpClient http, IConfiguration config, IReviewPromptService reviewPrompts, ILogger logger) { _http = http; _config = config; _reviewPrompts = reviewPrompts; _logger = logger; } public async Task AnalyzeReviewAsync(string reviewText, string lectureContext) { var promptSetting = await _reviewPrompts.GetAsync(); var prompt = ReviewPromptTemplate.Render(promptSetting.Prompt, reviewText, lectureContext); var request = new { model = _config["Llm:Model"] ?? "gpt-4o-mini", messages = new[] { new { role = "user", content = prompt } }, temperature = 0.3, response_format = new { type = "json_object" } }; var apiKey = _config["Llm:ApiKey"] ?? ""; _http.DefaultRequestHeaders.Clear(); if (!string.IsNullOrEmpty(apiKey)) _http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); var response = await _http.PostAsJsonAsync("chat/completions", request); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadFromJsonAsync(); var content = json.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString()!; var analysisJson = NormalizeJsonContent(content); var analysis = JsonSerializer.Deserialize(analysisJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; return new LlmReviewAnalysis( Math.Clamp(analysis.QualityScore, 0, 1), analysis.Sentiment ?? "", analysis.Tags ?? [], analysis.IsInformative, content); } private static string NormalizeJsonContent(string content) { var trimmed = content.Trim(); if (!trimmed.StartsWith("```", StringComparison.Ordinal)) return trimmed; var firstNewLine = trimmed.IndexOf('\n'); if (firstNewLine < 0) return trimmed; var lastFence = trimmed.LastIndexOf("```", StringComparison.Ordinal); return lastFence > firstNewLine ? trimmed[(firstNewLine + 1)..lastFence].Trim() : trimmed[(firstNewLine + 1)..].Trim(); } private record LlmRawResponse( [property: JsonPropertyName("quality_score")] double QualityScore, string? Sentiment, string[]? Tags, [property: JsonPropertyName("is_informative")] bool IsInformative); }