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