diff --git a/Mindforge.API/Models/Enums/LlmProvider.cs b/Mindforge.API/Models/Enums/LlmProvider.cs deleted file mode 100644 index 83eb027..0000000 --- a/Mindforge.API/Models/Enums/LlmProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Mindforge.API.Models.Enums -{ - public enum LlmProvider - { - OpenAI, - Gemini - } -} diff --git a/Mindforge.API/Program.cs b/Mindforge.API/Program.cs index 1c0966f..092d49f 100644 --- a/Mindforge.API/Program.cs +++ b/Mindforge.API/Program.cs @@ -34,7 +34,6 @@ builder.Services.AddHttpClient(); // Register Providers builder.Services.AddScoped(); -builder.Services.AddScoped(); // Register Services builder.Services.AddScoped(); @@ -59,30 +58,26 @@ app.UseAuthorization(); app.MapControllers(); // Check for env vars -var openAiKey = builder.Configuration["OPENAI_API_KEY"]; -var geminiKey = builder.Configuration["GEMINI_API_KEY"]; +var openAiApiUrl = builder.Configuration["OPENAI_API_URL"]; +var openAiToken = builder.Configuration["OPENAI_TOKEN"]; +var openAiModel = builder.Configuration["OPENAI_MODEL"]; -if (string.IsNullOrEmpty(openAiKey)) -{ - app.Logger.LogWarning("OPENAI_API_KEY not found in configuration."); -} +if (string.IsNullOrEmpty(openAiApiUrl)) + app.Logger.LogWarning("OPENAI_API_URL not found in configuration."); -if (string.IsNullOrEmpty(geminiKey)) -{ - app.Logger.LogWarning("GEMINI_API_KEY not found in configuration."); -} +if (string.IsNullOrEmpty(openAiToken)) + app.Logger.LogWarning("OPENAI_TOKEN not found in configuration."); + +if (string.IsNullOrEmpty(openAiModel)) + app.Logger.LogWarning("OPENAI_MODEL not found in configuration."); var giteaRepoUrl = builder.Configuration["GITEA_REPO_URL"]; var giteaAccessToken = builder.Configuration["GITEA_ACCESS_TOKEN"]; if (string.IsNullOrEmpty(giteaRepoUrl)) -{ app.Logger.LogWarning("GITEA_REPO_URL not found in configuration. Repository features will not work."); -} if (string.IsNullOrEmpty(giteaAccessToken)) -{ app.Logger.LogWarning("GITEA_ACCESS_TOKEN not found in configuration. Repository features will not work."); -} app.Run(); diff --git a/Mindforge.API/Providers/GeminiApiProvider.cs b/Mindforge.API/Providers/GeminiApiProvider.cs deleted file mode 100644 index d57f9bf..0000000 --- a/Mindforge.API/Providers/GeminiApiProvider.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; - -namespace Mindforge.API.Providers -{ - public class GeminiApiProvider : ILlmApiProvider - { - private readonly HttpClient _httpClient; - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - - public GeminiApiProvider(HttpClient httpClient, IConfiguration configuration, ILogger logger) - { - _httpClient = httpClient; - _httpClient.Timeout = TimeSpan.FromMinutes(5); - _configuration = configuration; - _logger = logger; - } - - public async Task SendRequestAsync(string systemPrompt, string userPrompt, string model) - { - var apiKey = _configuration["GEMINI_API_KEY"]; - if (string.IsNullOrEmpty(apiKey)) - { - throw new Exception("GEMINI_API_KEY not found in configuration."); - } - - var apiBase = "https://generativelanguage.googleapis.com/v1beta"; - var url = $"{apiBase.TrimEnd('/')}/models/{model}:generateContent?key={apiKey}"; - - var reqBody = new - { - system_instruction = string.IsNullOrEmpty(systemPrompt) ? null : new - { - parts = new[] { new { text = systemPrompt } } - }, - contents = new[] - { - new - { - role = "user", - parts = new[] { new { text = userPrompt } } - } - } - }; - - var jsonBody = JsonSerializer.Serialize(reqBody, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); - - using var request = new HttpRequestMessage(HttpMethod.Post, url); - request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); - - var response = await _httpClient.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) - { - throw new Exception($"Gemini API error status {(int)response.StatusCode}: {responseBody}"); - } - - var result = JsonSerializer.Deserialize(responseBody); - if (result.TryGetProperty("candidates", out var candidates) && candidates.GetArrayLength() > 0) - { - var content = candidates[0].GetProperty("content"); - if (content.TryGetProperty("parts", out var parts) && parts.GetArrayLength() > 0) - { - return parts[0].GetProperty("text").GetString() ?? string.Empty; - } - } - - throw new Exception("empty response from Gemini API"); - } - - public async Task SendRequestBatchAsync(string systemPrompt, string userPrompt, string model) - { - var apiKey = _configuration["GEMINI_API_KEY"]; - if (string.IsNullOrEmpty(apiKey)) - throw new Exception("GEMINI_API_KEY not found in configuration."); - - var apiBase = "https://generativelanguage.googleapis.com/v1beta"; - var jsonOptions = new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }; - - // Build single inline request - var batchBody = new - { - batch = new - { - display_name = "mindforge-batch", - input_config = new - { - requests = new - { - requests = new[] - { - new - { - request = new - { - system_instruction = string.IsNullOrEmpty(systemPrompt) ? null : new - { - parts = new[] { new { text = systemPrompt } } - }, - contents = new[] - { - new - { - role = "user", - parts = new[] { new { text = userPrompt } } - } - } - }, - metadata = new { key = "request-1" } - } - } - } - } - } - }; - - // Submit batch job - var createUrl = $"{apiBase}/models/{model}:batchGenerateContent?key={apiKey}"; - using var createReq = new HttpRequestMessage(HttpMethod.Post, createUrl); - createReq.Content = new StringContent(JsonSerializer.Serialize(batchBody, jsonOptions), Encoding.UTF8, "application/json"); - - var createResp = await _httpClient.SendAsync(createReq); - var createBody = await createResp.Content.ReadAsStringAsync(); - if (!createResp.IsSuccessStatusCode) - throw new Exception($"Gemini Batch API error creating job {(int)createResp.StatusCode}: {createBody}"); - - _logger.LogInformation("Gemini Batch API job created"); - - var createResult = JsonSerializer.Deserialize(createBody); - if (!createResult.TryGetProperty("name", out var nameEl)) - throw new Exception("Gemini Batch API did not return a job name."); - - var batchName = nameEl.GetString()!; - var pollUrl = $"{apiBase}/{batchName}?key={apiKey}"; - - _logger.LogInformation("Gemini Batch API job name: {BatchName}", batchName); - - // Poll until terminal state - while (true) - { - await Task.Delay(TimeSpan.FromSeconds(10)); - - using var pollReq = new HttpRequestMessage(HttpMethod.Get, pollUrl); - var pollResp = await _httpClient.SendAsync(pollReq); - var pollBody = await pollResp.Content.ReadAsStringAsync(); - - if (!pollResp.IsSuccessStatusCode) - throw new Exception($"Gemini Batch API error polling status {(int)pollResp.StatusCode}: {pollBody}"); - - var pollResult = JsonSerializer.Deserialize(pollBody); - var metadata = pollResult.GetProperty("metadata"); - var state = metadata.GetProperty("state"); - - _logger.LogInformation("Gemini Batch API job state: {State}", state.GetString()); - - switch (state.GetString()) - { - case "BATCH_STATE_SUCCEEDED": - if (pollResult.TryGetProperty("response", out var batchResponse) && - batchResponse.TryGetProperty("inlinedResponses", out var inlinedResponses) && - inlinedResponses.TryGetProperty("inlinedResponses", out var inlinedResponsesInternal) && - inlinedResponsesInternal.GetArrayLength() > 0) - { - _logger.LogInformation("Gemini Batch API job succeeded"); - - var first = inlinedResponsesInternal[0]; - if (first.TryGetProperty("error", out var reqError)) - throw new Exception($"Gemini Batch request error: {reqError}"); - - if (first.TryGetProperty("response", out var innerResponse) && - innerResponse.TryGetProperty("candidates", out var candidates) && - candidates.GetArrayLength() > 0) - { - var content = candidates[0].GetProperty("content"); - if (content.TryGetProperty("parts", out var parts) && parts.GetArrayLength() > 0) - return parts[0].GetProperty("text").GetString() ?? string.Empty; - } - } - throw new Exception("Gemini Batch job succeeded but returned no content."); - - case "BATCH_STATE_FAILED": - throw new Exception($"Gemini Batch job failed: {pollBody}"); - - case "BATCH_STATE_CANCELLED": - throw new Exception("Gemini Batch job was cancelled."); - - case "BATCH_STATE_EXPIRED": - throw new Exception("Gemini Batch job expired before completing."); - - // BATCH_STATE_PENDING / BATCH_STATE_RUNNING — keep polling - } - } - } - } -} diff --git a/Mindforge.API/Providers/ILlmApiProvider.cs b/Mindforge.API/Providers/ILlmApiProvider.cs index 2bd756a..6b58efe 100644 --- a/Mindforge.API/Providers/ILlmApiProvider.cs +++ b/Mindforge.API/Providers/ILlmApiProvider.cs @@ -4,7 +4,6 @@ namespace Mindforge.API.Providers { public interface ILlmApiProvider { - Task SendRequestAsync(string systemPrompt, string userPrompt, string model); - Task SendRequestBatchAsync(string systemPrompt, string userPrompt, string model); + Task SendRequestAsync(string systemPrompt, string userPrompt); } } diff --git a/Mindforge.API/Providers/OpenAIApiProvider.cs b/Mindforge.API/Providers/OpenAIApiProvider.cs index 3870430..d98bf61 100644 --- a/Mindforge.API/Providers/OpenAIApiProvider.cs +++ b/Mindforge.API/Providers/OpenAIApiProvider.cs @@ -22,28 +22,29 @@ namespace Mindforge.API.Providers _logger = logger; } - public async Task SendRequestAsync(string systemPrompt, string userPrompt, string model) + public async Task SendRequestAsync(string systemPrompt, string userPrompt) { - var apiKey = _configuration["OPENAI_API_KEY"]; - if (string.IsNullOrEmpty(apiKey)) - { - throw new Exception("OPENAI_API_KEY not found in configuration."); - } + var apiUrl = _configuration["OPENAI_API_URL"]; + if (string.IsNullOrEmpty(apiUrl)) + throw new Exception("OPENAI_API_URL not found in configuration."); - var apiBase = "https://api.openai.com/v1"; - var url = $"{apiBase.TrimEnd('/')}/responses"; + var token = _configuration["OPENAI_TOKEN"]; + if (string.IsNullOrEmpty(token)) + throw new Exception("OPENAI_TOKEN not found in configuration."); + + var model = _configuration["OPENAI_MODEL"]; + if (string.IsNullOrEmpty(model)) + throw new Exception("OPENAI_MODEL not found in configuration."); + + var url = $"{apiUrl.TrimEnd('/')}/chat/completions"; var reqBody = new { model = model, - input = new[] + messages = new[] { - new { role = "developer", content = systemPrompt }, + new { role = "system", content = systemPrompt }, new { role = "user", content = userPrompt } - }, - reasoning = new - { - effort = "low" } }; @@ -54,7 +55,7 @@ namespace Mindforge.API.Providers for (int i = 0; i < 5; i++) { using var request = new HttpRequestMessage(HttpMethod.Post, url); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); try @@ -64,47 +65,30 @@ namespace Mindforge.API.Providers if (!response.IsSuccessStatusCode) { - lastErr = new Exception($"OpenAI API error status {(int)response.StatusCode}: {responseBody}"); + lastErr = new Exception($"API error status {(int)response.StatusCode}: {responseBody}"); await Task.Delay(TimeSpan.FromSeconds(1 << i)); continue; } var result = JsonSerializer.Deserialize(responseBody); - if (result.TryGetProperty("output", out var outputArray)) + if (result.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0) { - foreach (var outputItem in outputArray.EnumerateArray()) - { - if (outputItem.TryGetProperty("content", out var contentArray)) - { - foreach (var contentItem in contentArray.EnumerateArray()) - { - if (contentItem.TryGetProperty("text", out var textContent)) - { - return textContent.GetString() ?? string.Empty; - } - } - } - } + var message = choices[0].GetProperty("message"); + return message.GetProperty("content").GetString() ?? string.Empty; } - _logger.LogWarning("OpenAI API raw response: {responseBody}", responseBody); - - throw new Exception("empty response from OpenAI API"); + _logger.LogWarning("API raw response: {responseBody}", responseBody); + throw new Exception("Empty response from API."); } catch (Exception ex) { - _logger.LogError(ex, "Error in OpenAI API request"); + _logger.LogError(ex, "Error in API request"); lastErr = ex; await Task.Delay(TimeSpan.FromSeconds(1 << i)); } } - throw new Exception($"failed to get OpenAI response after 5 attempts. Last error: {lastErr?.Message}", lastErr); - } - - public async Task SendRequestBatchAsync(string systemPrompt, string userPrompt, string model) - { - throw new NotImplementedException(); + throw new Exception($"Failed to get response after 5 attempts. Last error: {lastErr?.Message}", lastErr); } } } diff --git a/Mindforge.API/Services/AgentService.cs b/Mindforge.API/Services/AgentService.cs index ec0ab52..2d4133b 100644 --- a/Mindforge.API/Services/AgentService.cs +++ b/Mindforge.API/Services/AgentService.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using Mindforge.API.Models.Enums; using Mindforge.API.Providers; using Mindforge.API.Services.Interfaces; @@ -10,39 +6,16 @@ namespace Mindforge.API.Services { public class AgentService : IAgentService { - private readonly IEnumerable _providers; + private readonly ILlmApiProvider _provider; - public AgentService(IEnumerable providers) + public AgentService(ILlmApiProvider provider) { - _providers = providers; + _provider = provider; } - public Task ProcessRequestAsync(LlmProvider providerEnum, string systemPrompt, string userPrompt, string model) + public Task ProcessRequestAsync(string systemPrompt, string userPrompt) { - ILlmApiProvider provider = providerEnum switch - { - LlmProvider.OpenAI => _providers.OfType().FirstOrDefault() - ?? throw new Exception("OpenAI provider not registered"), - LlmProvider.Gemini => _providers.OfType().FirstOrDefault() - ?? throw new Exception("Gemini provider not registered"), - _ => throw new Exception("Unknown provider") - }; - - return provider.SendRequestAsync(systemPrompt, userPrompt, model); - } - - public Task ProcessRequestBatchAsync(LlmProvider providerEnum, string systemPrompt, string userPrompt, string model) - { - ILlmApiProvider provider = providerEnum switch - { - LlmProvider.OpenAI => _providers.OfType().FirstOrDefault() - ?? throw new Exception("OpenAI provider not registered"), - LlmProvider.Gemini => _providers.OfType().FirstOrDefault() - ?? throw new Exception("Gemini provider not registered"), - _ => throw new Exception("Unknown provider") - }; - - return provider.SendRequestBatchAsync(systemPrompt, userPrompt, model); + return _provider.SendRequestAsync(systemPrompt, userPrompt); } } } diff --git a/Mindforge.API/Services/FileService.cs b/Mindforge.API/Services/FileService.cs index 226e6dd..f2bbc2a 100644 --- a/Mindforge.API/Services/FileService.cs +++ b/Mindforge.API/Services/FileService.cs @@ -1,6 +1,4 @@ -using System; using System.Threading.Tasks; -using Mindforge.API.Models.Enums; using Mindforge.API.Models.Requests; using Mindforge.API.Services.Interfaces; using Mindforge.API.Exceptions; @@ -11,9 +9,6 @@ namespace Mindforge.API.Services { private readonly IAgentService _agentService; - private const LlmProvider DefaultProvider = LlmProvider.OpenAI; - private const string DefaultModel = "gpt-5-mini"; - public FileService(IAgentService agentService) { _agentService = agentService; @@ -55,7 +50,7 @@ Os resumos serão utilizados para concursos públicos, principalmente para a ban string userPrompt = $"Arquivo: {request.FileName}\nConteúdo:\n{request.FileContent}"; - return await _agentService.ProcessRequestAsync(DefaultProvider, systemPrompt, userPrompt, DefaultModel); + return await _agentService.ProcessRequestAsync(systemPrompt, userPrompt); } } } diff --git a/Mindforge.API/Services/FlashcardService.cs b/Mindforge.API/Services/FlashcardService.cs index 4c5172b..a5f187e 100644 --- a/Mindforge.API/Services/FlashcardService.cs +++ b/Mindforge.API/Services/FlashcardService.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Mindforge.API.Models.Enums; using Mindforge.API.Models.Requests; using Mindforge.API.Services.Interfaces; @@ -10,9 +9,6 @@ namespace Mindforge.API.Services private readonly IAgentService _agentService; private readonly ILogger _logger; - private const LlmProvider DefaultProvider = LlmProvider.Gemini; - private string DefaultModel = "gemini-3.1-flash-image-preview"; - public FlashcardService(IAgentService agentService, ILogger logger) { _agentService = agentService; @@ -21,25 +17,12 @@ namespace Mindforge.API.Services public async Task GenerateFlashcardsAsync(FlashcardGenerateRequest request) { - var extraPrompt = ""; - - switch (request.Mode) + var extraPrompt = request.Mode switch { - case FlashcardMode.Basic: - DefaultModel = "gemini-3.1-flash-lite-preview"; - break; - case FlashcardMode.Simple: - DefaultModel = "gemini-3.1-flash-image-preview"; - break; - case FlashcardMode.Detailed: - DefaultModel = "gemini-3.1-flash-image-preview"; - extraPrompt = "Crie flashcards mais detalhados."; - break; - case FlashcardMode.Hyper: - DefaultModel = "gemini-3.1-pro-preview"; - extraPrompt = "Adicione também pequenas questões para fixação, para que o usuário possa testar seus conhecimentos. As questões devem ser curtas e objetivas, como se fosse cobradas em prova mesmo."; - break; - } + FlashcardMode.Detailed => "Crie flashcards mais detalhados.", + FlashcardMode.Hyper => "Adicione também pequenas questões para fixação, para que o usuário possa testar seus conhecimentos. As questões devem ser curtas e objetivas, como se fosse cobradas em prova mesmo.", + _ => "" + }; string systemPrompt = $@"Você é um assistente educacional especializado em criar flashcards para o Anki. Baseado no texto fornecido, crie exatamente {request.Amount} flashcards que focam nos conceitos mais importantes e difíceis. @@ -57,8 +40,7 @@ Com base no arquivo fornecido, crie exatamente {request.Amount} flashcards que f string userPrompt = $"Arquivo: {request.FileName}\nConteúdo:\n{request.FileContent}"; - //var result = await _agentService.ProcessRequestAsync(DefaultProvider, systemPrompt, userPrompt, DefaultModel); - var result = await _agentService.ProcessRequestBatchAsync(DefaultProvider, systemPrompt, userPrompt, DefaultModel); + var result = await _agentService.ProcessRequestAsync(systemPrompt, userPrompt); var lines = result.Split('\n'); diff --git a/Mindforge.API/Services/Interfaces/IAgentService.cs b/Mindforge.API/Services/Interfaces/IAgentService.cs index 55293cc..5beb1e8 100644 --- a/Mindforge.API/Services/Interfaces/IAgentService.cs +++ b/Mindforge.API/Services/Interfaces/IAgentService.cs @@ -1,11 +1,9 @@ using System.Threading.Tasks; -using Mindforge.API.Models.Enums; namespace Mindforge.API.Services.Interfaces { public interface IAgentService { - Task ProcessRequestAsync(LlmProvider provider, string systemPrompt, string userPrompt, string model); - Task ProcessRequestBatchAsync(LlmProvider provider, string systemPrompt, string userPrompt, string model); + Task ProcessRequestAsync(string systemPrompt, string userPrompt); } } diff --git a/Mindforge.API/appsettings.json b/Mindforge.API/appsettings.json index be62f02..85d8112 100644 --- a/Mindforge.API/appsettings.json +++ b/Mindforge.API/appsettings.json @@ -6,8 +6,9 @@ } }, "AllowedHosts": "*", - "OPENAI_API_KEY": "", - "GEMINI_API_KEY": "", + "OPENAI_API_URL": "https://openrouter.ai/api/v1", + "OPENAI_TOKEN": "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", + "OPENAI_MODEL": "openai/gpt-5.4-mini", "GITEA_REPO_URL": "", "GITEA_ACCESS_TOKEN": "" -} +} \ No newline at end of file diff --git a/Mindforge.API/deploy/mindforge-api.yaml b/Mindforge.API/deploy/mindforge-api.yaml index 1642a15..5462c09 100644 --- a/Mindforge.API/deploy/mindforge-api.yaml +++ b/Mindforge.API/deploy/mindforge-api.yaml @@ -20,16 +20,15 @@ spec: ports: - containerPort: 8080 env: - - name: OPENAI_API_KEY + - name: OPENAI_TOKEN valueFrom: secretKeyRef: name: mindforge-secrets - key: OPENAI_API_KEY - - name: GEMINI_API_KEY - valueFrom: - secretKeyRef: - name: mindforge-secrets - key: GEMINI_API_KEY + key: OPENAI_TOKEN + - name: OPENAI_API_URL + value: https://openrouter.ai/api/v1 + - name: OPENAI_MODEL + value: openai/gpt-5.4-mini - name: GITEA_REPO_URL valueFrom: secretKeyRef: