@@ -0,0 +1,104 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Api.Settings;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Api.Services.Rag;
|
||||
|
||||
public interface IOpenAiRagClient
|
||||
{
|
||||
Task<float[]> CreateEmbeddingAsync(string input, CancellationToken ct);
|
||||
Task<string> CreateChatCompletionAsync(string systemPrompt, string userPrompt, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class OpenAiRagClient : IOpenAiRagClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly OpenAiSettings _settings;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public OpenAiRagClient(HttpClient httpClient, IOptions<OpenAiSettings> 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<float[]> 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<string> 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>(T payload) => new(
|
||||
JsonSerializer.Serialize(payload, JsonOptions),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user