using Microsoft.Extensions.Configuration; using Nop.Plugin.Misc.FruitBankPlugin.Models; using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Configuration; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace Nop.Plugin.Misc.FruitBankPlugin.Services { public class OpenAIApiService : IAIAPIService { private readonly ISettingService _settingService; private readonly FruitBankSettings _fruitBankSettings; private readonly HttpClient _httpClient; private static Action? _callback; private static Action? _onComplete; private static Action? _onError; private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions"; private const string OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations"; private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files"; private const string BaseUrl = "https://api.openai.com/v1"; private string? _assistantId; private string? _vectorStoreId; public OpenAIApiService(ISettingService settingService, HttpClient httpClient) { _settingService = settingService; _fruitBankSettings = _settingService.LoadSetting(); _httpClient = httpClient; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {GetApiKey()}"); } public string GetApiKey() => _fruitBankSettings.OpenAIApiKey; public string GetModelName() => _fruitBankSettings.OpenAIModelName; public void RegisterCallback(Action callback, Action onCompleteCallback, Action onErrorCallback) { _callback = callback; _onComplete = onCompleteCallback; _onError = onErrorCallback; } #region === CHAT (TEXT INPUT) === public async Task GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null) { string modelName = GetModelName(); StringContent requestContent = new(""); if (modelName == "gpt-4.1-mini" || modelName == "gpt-4o-mini" || modelName == "gpt-4.1-nano" || modelName == "gpt-5-nano") { var requestBody = new OpenAIGpt4MiniAIChatRequest { Model = modelName, Temperature = 0.2, Messages = assistantMessage == null || assistantMessage == string.Empty ? new[] { new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } } : new[] { new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } }, Stream = false }; var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); } else { var requestBody = new OpenAIGpt5AIChatRequest { Model = modelName, Temperature = 1, Messages = assistantMessage == null || assistantMessage == string.Empty ? new[] { new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } } : new[] { new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } }, ReasoningEffort = "minimal", Verbosity = "high", Stream = false }; var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); } using var response = await _httpClient.PostAsync(OpenAiEndpoint, requestContent); response.EnsureSuccessStatusCode(); using var responseStream = await response.Content.ReadAsStreamAsync(); using var document = await JsonDocument.ParseAsync(responseStream); var inputTokens = document.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32(); var outputTokens = document.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32(); Console.WriteLine($"USAGE STATS - Tokens: {inputTokens} + {outputTokens} = {inputTokens + outputTokens}"); return document.RootElement .GetProperty("choices")[0] .GetProperty("message") .GetProperty("content") .GetString() ?? "No response"; } #endregion #region === CHAT (STREAMING) === public async Task GetStreamedResponseAsync(string sessionId, string systemMessage, string userMessage, string? assistantMessage = null) { string modelName = GetModelName(); StringContent requestContent = new(""); if (modelName == "gpt-4.1-mini" || modelName == "gpt-4o-mini" || modelName == "gpt-4.1-nano" || modelName == "gpt-5-nano") { var requestBody = new OpenAIGpt4MiniAIChatRequest { Model = modelName, Temperature = 0.2, Messages = assistantMessage == null || assistantMessage == string.Empty ? new[] { new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } } : new[] { new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } }, Stream = true }; var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); } else { var requestBody = new OpenAIGpt5AIChatRequest { Model = modelName, Temperature = 1, Messages = assistantMessage == null || assistantMessage == string.Empty ? new[] { new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "user", Content = userMessage } } : new[] { new AIChatMessage { Role = "system", Content = systemMessage }, new AIChatMessage { Role = "assistant", Content = assistantMessage }, new AIChatMessage { Role = "user", Content = userMessage } }, Stream = true }; var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); } using var httpRequest = new HttpRequestMessage(HttpMethod.Post, OpenAiEndpoint) { Content = requestContent }; using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); var stringBuilder = new StringBuilder(); using var responseStream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(responseStream); try { while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(); if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue; var jsonResponse = line.Substring(6); if (jsonResponse == "[DONE]") { _onComplete?.Invoke(sessionId); break; } try { using var jsonDoc = JsonDocument.Parse(jsonResponse); if (jsonDoc.RootElement.TryGetProperty("choices", out var choices) && choices[0].TryGetProperty("delta", out var delta) && delta.TryGetProperty("content", out var contentElement)) { var content = contentElement.GetString(); if (!string.IsNullOrEmpty(content)) { stringBuilder.Append(content); _callback?.Invoke(sessionId, stringBuilder.ToString()); } } } catch (JsonException ex) { _onError?.Invoke(sessionId, $"Malformed JSON: {ex.Message}"); break; } } if (reader.EndOfStream && !stringBuilder.ToString().EndsWith("[DONE]")) { _onError?.Invoke(sessionId, "Unexpected end of stream"); } } catch (Exception ex) { _onError?.Invoke(sessionId, $"Exception: {ex.Message}"); } return stringBuilder.ToString(); } #endregion #region === IMAGE GENERATION === public async Task GenerateImageAsync(string prompt) { var request = new HttpRequestMessage(HttpMethod.Post, OpenAiImageEndpoint); var requestBody = new { model = "gpt-image-1", prompt = prompt, n = 1, size = "1024x1024" }; request.Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"); var response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { var error = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Image generation failed: {error}"); return null; } using var content = await response.Content.ReadAsStreamAsync(); var json = await JsonDocument.ParseAsync(content); var base64Image = json.RootElement .GetProperty("data")[0] .GetProperty("b64_json") .GetString(); return $"data:image/png;base64,{base64Image}"; } #endregion #region === PDF ANALYSIS (NEW) === private async Task EnsureAssistantAndVectorStoreAsync() { // Find or create vector store if (_vectorStoreId == null) { _vectorStoreId = await FindOrCreateVectorStoreAsync("pdf-analysis-store"); } // Find or create assistant if (_assistantId == null) { _assistantId = await FindOrCreateAssistantAsync("PDF and Image Analyzer Assistant"); } } //TEMPORARY: Cleanup all assistants (for testing purposes) - A. public async Task CleanupAllAssistantsAsync() { Console.WriteLine("Cleaning up all existing assistants..."); var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants"); listRequest.Headers.Add("OpenAI-Beta", "assistants=v2"); var response = await _httpClient.SendAsync(listRequest); if (response.IsSuccessStatusCode) { using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var assistants = json.RootElement.GetProperty("data"); foreach (var assistant in assistants.EnumerateArray()) { var id = assistant.GetProperty("id").GetString(); var name = assistant.GetProperty("name").GetString(); var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"{BaseUrl}/assistants/{id}"); deleteRequest.Headers.Add("OpenAI-Beta", "assistants=v2"); await _httpClient.SendAsync(deleteRequest); Console.WriteLine($"Deleted assistant: {name} ({id})"); } Console.WriteLine("Cleanup complete!"); } // Reset local cache _assistantId = null; } public async Task AnalyzePdfAsync(Stream file, string fileName, string userPrompt) { await EnsureAssistantAndVectorStoreAsync(); var fileId = await UploadFileAsync(file, fileName); var isImage = IsImageFile(fileName); if (!isImage) { await AttachFileToVectorStoreAsync(fileId); } var threadId = await CreateThreadAsync(); if (isImage) { await AddUserMessageWithImageAsync(threadId, userPrompt, fileId); } else { await AddUserMessageAsync(threadId, userPrompt); } var runId = await CreateRunAsync(threadId); await WaitForRunCompletionAsync(threadId, runId); return await GetAssistantResponseAsync(threadId); } private bool IsImageFile(string fileName) { var extension = Path.GetExtension(fileName).ToLowerInvariant(); return extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".gif" || extension == ".webp"; } private async Task UploadFileAsync(Stream file, string fileName) { using var form = new MultipartFormDataContent(); var fileContent = new StreamContent(file); // Determine MIME type based on file extension var extension = Path.GetExtension(fileName).ToLowerInvariant(); fileContent.Headers.ContentType = extension switch { ".pdf" => new MediaTypeHeaderValue("application/pdf"), ".jpg" or ".jpeg" => new MediaTypeHeaderValue("image/jpeg"), ".png" => new MediaTypeHeaderValue("image/png"), ".gif" => new MediaTypeHeaderValue("image/gif"), ".webp" => new MediaTypeHeaderValue("image/webp"), _ => new MediaTypeHeaderValue("application/octet-stream") }; form.Add(fileContent, "file", fileName); form.Add(new StringContent("assistants"), "purpose"); var response = await _httpClient.PostAsync($"{BaseUrl}/files", form); await EnsureSuccessAsync(response, "upload file"); using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); return json.RootElement.GetProperty("id").GetString()!; } private async Task AttachFileToVectorStoreAsync(string fileId) { var body = new { file_id = fileId }; var request = CreateAssistantRequest( HttpMethod.Post, $"{BaseUrl}/vector_stores/{_vectorStoreId}/files", body ); var response = await _httpClient.SendAsync(request); await EnsureSuccessAsync(response, "attach file to vector store"); } private async Task CreateThreadAsync() { var request = CreateAssistantRequest( HttpMethod.Post, $"{BaseUrl}/threads", new { } ); var response = await _httpClient.SendAsync(request); await EnsureSuccessAsync(response, "create thread"); using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); return json.RootElement.GetProperty("id").GetString()!; } private async Task AddUserMessageAsync(string threadId, string userPrompt) { var body = new { role = "user", content = userPrompt }; var request = CreateAssistantRequest( HttpMethod.Post, $"{BaseUrl}/threads/{threadId}/messages", body ); var response = await _httpClient.SendAsync(request); await EnsureSuccessAsync(response, "add user message"); } private async Task AddUserMessageWithImageAsync(string threadId, string userPrompt, string fileId) { var body = new { role = "user", content = new object[] { new { type = "text", text = userPrompt }, new { type = "image_file", image_file = new { file_id = fileId } } } }; var request = CreateAssistantRequest( HttpMethod.Post, $"{BaseUrl}/threads/{threadId}/messages", body ); var response = await _httpClient.SendAsync(request); await EnsureSuccessAsync(response, "add user message with image"); } private async Task CreateRunAsync(string threadId) { var body = new { assistant_id = _assistantId, tool_resources = new { file_search = new { vector_store_ids = new[] { _vectorStoreId } } } }; var request = CreateAssistantRequest( HttpMethod.Post, $"{BaseUrl}/threads/{threadId}/runs", body ); var response = await _httpClient.SendAsync(request); await EnsureSuccessAsync(response, "create run"); using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); return json.RootElement.GetProperty("id").GetString()!; } private async Task WaitForRunCompletionAsync(string threadId, string runId) { const int pollIntervalMs = 1000; const int maxAttempts = 60; // 1 minute timeout int attempts = 0; while (attempts < maxAttempts) { var request = CreateAssistantRequest( HttpMethod.Get, $"{BaseUrl}/threads/{threadId}/runs/{runId}" ); var response = await _httpClient.SendAsync(request); await EnsureSuccessAsync(response, "check run status"); using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var status = json.RootElement.GetProperty("status").GetString()!; if (status == "completed") return; if (status != "in_progress" && status != "queued") throw new Exception($"Run failed with status: {status}"); await Task.Delay(pollIntervalMs); attempts++; } throw new TimeoutException("Run did not complete within the expected time"); } private async Task GetAssistantResponseAsync(string threadId) { var request = CreateAssistantRequest( HttpMethod.Get, $"{BaseUrl}/threads/{threadId}/messages" ); var response = await _httpClient.SendAsync(request); await EnsureSuccessAsync(response, "retrieve messages"); using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var messages = json.RootElement.GetProperty("data"); if (messages.GetArrayLength() == 0) return "No response"; var firstMessage = messages[0] .GetProperty("content")[0] .GetProperty("text") .GetProperty("value") .GetString(); return firstMessage ?? "No response"; } private HttpRequestMessage CreateAssistantRequest(HttpMethod method, string url, object? body = null) { var request = new HttpRequestMessage(method, url); request.Headers.Add("OpenAI-Beta", "assistants=v2"); if (body != null) { var json = JsonSerializer.Serialize(body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); } return request; } private async Task EnsureSuccessAsync(HttpResponseMessage response, string operation) { if (!response.IsSuccessStatusCode) { var errorBody = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Error Status: {response.StatusCode}"); Console.WriteLine($"Error Body: {errorBody}"); throw new Exception($"Failed to {operation}: {errorBody}"); } } private async Task FindOrCreateVectorStoreAsync(string name) { // List existing vector stores var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/vector_stores"); listRequest.Headers.Add("OpenAI-Beta", "assistants=v2"); var response = await _httpClient.SendAsync(listRequest); if (response.IsSuccessStatusCode) { using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var stores = json.RootElement.GetProperty("data"); foreach (var store in stores.EnumerateArray()) { if (store.GetProperty("name").GetString() == name) { return store.GetProperty("id").GetString()!; } } } // Create new if not found var createBody = new { name = name }; var createRequest = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/vector_stores", createBody); var createResponse = await _httpClient.SendAsync(createRequest); await EnsureSuccessAsync(createResponse, "create vector store"); using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync()); return createJson.RootElement.GetProperty("id").GetString()!; } private async Task FindOrCreateAssistantAsync(string name) { // List existing assistants var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants"); listRequest.Headers.Add("OpenAI-Beta", "assistants=v2"); var response = await _httpClient.SendAsync(listRequest); if (response.IsSuccessStatusCode) { using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var assistants = json.RootElement.GetProperty("data"); foreach (var assistant in assistants.EnumerateArray()) { if (assistant.GetProperty("name").GetString() == name) { return assistant.GetProperty("id").GetString()!; } } } // Create new if not found var assistantBody = new { name = name, instructions = "You are an assistant that analyzes uploaded files. When you receive an image, analyze and describe what you see in the image in detail. When you receive a PDF or text document, use the file_search tool to find and analyze relevant information. Always respond directly to the user's question about the file they uploaded.", model = "gpt-4o", tools = new[] { new { type = "file_search" } } }; var request = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/assistants", assistantBody); var createResponse = await _httpClient.SendAsync(request); await EnsureSuccessAsync(createResponse, "create assistant"); using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync()); return createJson.RootElement.GetProperty("id").GetString()!; } #endregion } }