adding new mindforge applications
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

This commit is contained in:
2026-03-20 22:51:04 -03:00
parent 36e405a9a8
commit 3e09b03753
55 changed files with 4164 additions and 2 deletions

View File

@@ -0,0 +1,202 @@
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
}
}
}
}
}