105 lines
3.6 KiB
C#
105 lines
3.6 KiB
C#
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"
|
|
);
|
|
}
|