* Extended functionalities
* Added different UIs
This commit is contained in:
12
MealPlan.Api/Services/AI/IOpenAiChatService.cs
Normal file
12
MealPlan.Api/Services/AI/IOpenAiChatService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MealPlan.Api.Services.AI;
|
||||
|
||||
public interface IOpenAiChatService
|
||||
{
|
||||
bool IsConfigured { get; }
|
||||
|
||||
Task<string> CompleteJsonAsync(
|
||||
string systemPrompt,
|
||||
string userPrompt,
|
||||
OpenAiUseCase useCase = OpenAiUseCase.Default,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
80
MealPlan.Api/Services/AI/OpenAiChatService.cs
Normal file
80
MealPlan.Api/Services/AI/OpenAiChatService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MealPlan.Api.Services.AI;
|
||||
|
||||
public class OpenAiChatService : IOpenAiChatService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly OpenAiOptions _options;
|
||||
private readonly ILogger<OpenAiChatService> _logger;
|
||||
|
||||
public OpenAiChatService(HttpClient http, IOptions<OpenAiOptions> options, ILogger<OpenAiChatService> logger)
|
||||
{
|
||||
_http = http;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsConfigured => _options.IsConfigured;
|
||||
|
||||
public async Task<string> CompleteJsonAsync(
|
||||
string systemPrompt,
|
||||
string userPrompt,
|
||||
OpenAiUseCase useCase = OpenAiUseCase.Default,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!IsConfigured)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"OpenAI is not configured. Set OpenAI__ApiKey in appsettings.Local.json.");
|
||||
}
|
||||
|
||||
var model = _options.ResolveModel(useCase);
|
||||
var payload = new
|
||||
{
|
||||
model,
|
||||
max_tokens = _options.MaxTokens,
|
||||
response_format = new { type = "json_object" },
|
||||
messages = new[]
|
||||
{
|
||||
new { role = "system", content = systemPrompt },
|
||||
new { role = "user", content = userPrompt },
|
||||
},
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ApiKey);
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await _http.SendAsync(request, ct);
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("OpenAI API error {Status}: {Body}", response.StatusCode, body);
|
||||
throw new InvalidOperationException("OpenAI request failed. Check API key and quota.");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var content = doc.RootElement
|
||||
.GetProperty("choices")[0]
|
||||
.GetProperty("message")
|
||||
.GetProperty("content")
|
||||
.GetString() ?? "{}";
|
||||
|
||||
return StripMarkdownFence(content);
|
||||
}
|
||||
|
||||
private static string StripMarkdownFence(string content)
|
||||
{
|
||||
var trimmed = content.Trim();
|
||||
if (!trimmed.StartsWith("```", StringComparison.Ordinal)) return trimmed;
|
||||
var firstNewline = trimmed.IndexOf('\n');
|
||||
if (firstNewline < 0) return trimmed;
|
||||
var endFence = trimmed.LastIndexOf("```", StringComparison.Ordinal);
|
||||
if (endFence <= firstNewline) return trimmed;
|
||||
return trimmed[(firstNewline + 1)..endFence].Trim();
|
||||
}
|
||||
}
|
||||
44
MealPlan.Api/Services/AI/OpenAiOptions.cs
Normal file
44
MealPlan.Api/Services/AI/OpenAiOptions.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
namespace MealPlan.Api.Services.AI;
|
||||
|
||||
public enum OpenAiUseCase
|
||||
{
|
||||
Default,
|
||||
Macros,
|
||||
Plan,
|
||||
Recipe,
|
||||
}
|
||||
|
||||
public class OpenAiOptions
|
||||
{
|
||||
public const string SectionName = "OpenAI";
|
||||
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Fallback when a task-specific model is not set.</summary>
|
||||
public string Model { get; set; } = "gpt-4.1-mini";
|
||||
|
||||
/// <summary>Macro target refinement (diet generator step 2).</summary>
|
||||
public string? MacrosModel { get; set; }
|
||||
|
||||
/// <summary>Weekly meal plan recipe selection (diet generator step 3).</summary>
|
||||
public string? PlanModel { get; set; }
|
||||
|
||||
/// <summary>AI recipe draft generation and partial regen.</summary>
|
||||
public string? RecipeModel { get; set; }
|
||||
|
||||
public int MaxTokens { get; set; } = 4096;
|
||||
|
||||
public bool IsConfigured => !string.IsNullOrWhiteSpace(ApiKey);
|
||||
|
||||
public string ResolveModel(OpenAiUseCase useCase) =>
|
||||
useCase switch
|
||||
{
|
||||
OpenAiUseCase.Macros => FirstNonEmpty(MacrosModel, Model),
|
||||
OpenAiUseCase.Plan => FirstNonEmpty(PlanModel, Model),
|
||||
OpenAiUseCase.Recipe => FirstNonEmpty(RecipeModel, Model),
|
||||
_ => Model,
|
||||
};
|
||||
|
||||
private static string FirstNonEmpty(string? preferred, string fallback) =>
|
||||
string.IsNullOrWhiteSpace(preferred) ? fallback : preferred;
|
||||
}
|
||||
Reference in New Issue
Block a user