* Extended functionalities

* Added different UIs
This commit is contained in:
2026-06-24 21:43:40 +02:00
parent 282f4864c4
commit f15bb7a916
348 changed files with 59057 additions and 498 deletions

View 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);
}

View 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();
}
}

View 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;
}