using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Api.Services.Contracts.Rag; using Api.Settings; using Microsoft.Extensions.Options; namespace Api.Services.Rag; public sealed class OpenAiRagClient : IAiRagClient { private readonly HttpClient _httpClient; private readonly OpenAiSettings _settings; private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; public OpenAiRagClient(HttpClient httpClient, IOptions options) { _httpClient = httpClient; _settings = options.Value; if (!string.IsNullOrWhiteSpace(_settings.ApiKey)) { _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _settings.ApiKey); } _httpClient.Timeout = TimeSpan.FromSeconds(Math.Max(15, _settings.TimeoutSeconds)); _httpClient.BaseAddress = new Uri("https://api.openai.com/v1/"); } public async Task CreateEmbeddingAsync(string input, CancellationToken ct) { EnsureConfigured(); var payload = new { model = _settings.EmbeddingModel, input }; using var response = await _httpClient.PostAsync("embeddings", ToJson(payload), ct); var json = await response.Content.ReadAsStringAsync(ct); if (!response.IsSuccessStatusCode) { throw new InvalidOperationException($"OpenAI embeddings request failed: {(int)response.StatusCode} {json}"); } using var document = JsonDocument.Parse(json); var embedding = document.RootElement.GetProperty("data")[0].GetProperty("embedding"); var result = new float[embedding.GetArrayLength()]; var i = 0; foreach (var value in embedding.EnumerateArray()) { result[i++] = value.GetSingle(); } return result; } public async Task CreateChatCompletionAsync(string systemPrompt, string userPrompt, CancellationToken ct) { EnsureConfigured(); var payload = new { model = _settings.ChatModel, temperature = 0.2, response_format = new { type = "json_object" }, messages = new[] { new { role = "system", content = systemPrompt }, new { role = "user", content = userPrompt } } }; using var response = await _httpClient.PostAsync("chat/completions", ToJson(payload), ct); var json = await response.Content.ReadAsStringAsync(ct); if (!response.IsSuccessStatusCode) { throw new InvalidOperationException($"OpenAI chat request failed: {(int)response.StatusCode} {json}"); } using var document = JsonDocument.Parse(json); return document.RootElement .GetProperty("choices")[0] .GetProperty("message") .GetProperty("content") .GetString() ?? "{}"; } private void EnsureConfigured() { if (string.IsNullOrWhiteSpace(_settings.ApiKey)) { throw new InvalidOperationException("OpenAI API key is not configured. Set OpenAI__ApiKey."); } } private static StringContent ToJson(T payload) => new( JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json" ); }