All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 1m8s
Mindforge Cronjob Build and Deploy / Build Mindforge Cronjob Image (push) Successful in 1m19s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 11s
Mindforge Cronjob Build and Deploy / Deploy Mindforge Cronjob (internal) (push) Successful in 10s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 2m25s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 12s
203 lines
9.0 KiB
C#
203 lines
9.0 KiB
C#
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<GeminiApiProvider> _logger;
|
|
|
|
public GeminiApiProvider(HttpClient httpClient, IConfiguration configuration, ILogger<GeminiApiProvider> logger)
|
|
{
|
|
_httpClient = httpClient;
|
|
_httpClient.Timeout = TimeSpan.FromMinutes(5);
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<string> 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<JsonElement>(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<string> 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<JsonElement>(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<JsonElement>(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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|