using AutoGen.Core;
using Mscc.GenerativeAI;
using System.Diagnostics;
using static AiAgent.Core.Constant.ProgramConstants;
using static AiAgent.Core.Helpers.GenericHelper;
using System.Text.Json;
namespace AiAgent.Core.Infrastructure;
///
/// A client for interacting with the Gemini API, providing methods for querying generative models and generating images.
///
/// This class encapsulates functionality to interact with Google's Gemini API, including methods to query
/// generative models with text prompts and input files, generate images based on text prompts, and log usage
/// metadata and errors. It supports multiple API keys and model configurations to optimize for cost and performance.
public class GeminiApiClient
{
private static string[] PaidApiKeys => ["AIzaSyCFVgyBJtrEIe5S7mvttxEzGRYILUzGNhg"]; // Pay
private static string[] FreeApiKeys => ["AIzaSyAsphWA1WPfht3DbL2BBTa5bSknyIU1oHA", "AIzaSyBr7Hz70vQCnFYA7ij7_vmkLCbi2d6B1Pw"]; // Free
private static string[] ImageModels =>
[
//"imagen-4.0-ultra-generate-001",
//"imagen-4.0-generate-001",
"imagen-4.0-fast-generate-001",
//"imagen-3.0-generate-002"
];
public static async Task Query(string prompt, string tag, GenerativeModel model, GenerationConfig generationConfig, string apiKey, List? inputDataFiles = null)
{
for (int keyIndex = 0; keyIndex < 2; keyIndex++)
{
//try
//{
// Make sure all the files exist
foreach (var file in inputDataFiles ?? [])
if (File.Exists(file) is false)
return $"The input file '{file}' is missing";
// Create GoogleAI instance to use for file upload
var googleAI = new GoogleAI(apiKey);
// Create a collection of upload tasks
var uploadTasks = inputDataFiles?.Select(filePath => googleAI.UploadFile(filePath, Path.GetFileName(filePath)))
?? throw new Exception("Problem uploading the files");
var results = await Task.WhenAll(uploadTasks);
var uploadedFiles = results.Select(response => response.File).ToList();
foreach (var file in uploadedFiles)
Log($"- Includes: {file.DisplayName} ({file.MimeType}) {file.SizeBytes} bytes, {file.State} -> {file.Uri}' ");
// Initialising the prompt
var myPrompt = new List { new TextData { Text = prompt } };
// Add a reference to each uploaded file to the prompt.
foreach (var file in uploadedFiles)
myPrompt.Add(new FileData { MimeType = file.MimeType, FileUri = file.Uri });
// Use GenerateContentStreamAsync to get the response as a stream.
Log("- Starting the query...\n");
var fullThinking = new System.Text.StringBuilder();
var fullResponse = new System.Text.StringBuilder();
Stopwatch stopwatch = Stopwatch.StartNew();
model.ApiKey = apiKey;
var stream = model.GenerateContentStream(myPrompt, generationConfig);
UsageMetadata? usage = null;
Candidate? firstCandidate = null;
// Use "await foreach" to process each chunk as it arrives.
await foreach (var res in stream)
{
// Log the chunk to a file
string jsonChunk = JsonSerializer.Serialize(res);
string message = $"------------------\n{DateTime.Now} -> {stopwatch}\n";
//message += string.IsNullOrEmpty(res.Thinking) ? "" : $"Thinking:\n{res.Thinking}\n";
//message += string.IsNullOrEmpty(res.Text) ? "" : $"Text:\n{res.Text}\n";
File.AppendAllText(Path.Combine(Output, $"Chunks - {tag}.txt"), $"{message}{jsonChunk}\n");
if (res is null)
break;
if (res.Candidates?.Count > 0)
{
firstCandidate = res.Candidates[0];
File.AppendAllText(Path.Combine(Output, $"Candidates - {tag} - Index = {keyIndex}.txt"), $"{res.Candidates[0].FinishReason}\n{res.Candidates[0].FinishMessage}\n");
if (res.Candidates[0].FinishReason is not null)
if (res.Candidates[0].FinishReason.HasValue)
{
var candidateJson = JsonSerializer.Serialize(res.Candidates[0]);
File.AppendAllText(Path.Combine(Output, $"Candidates - {tag} - Index = {keyIndex}.txt"), $"{candidateJson}\n");
}
}
try
{
if (res.Thinking is null && res.Text is null)
break;
}
catch
{
break;
}
// First display the thinking
if (res.Thinking is not null)
{
fullThinking.Append(res.Thinking);
File.AppendAllText(Path.Combine(Output, $"Thinking - {tag}.txt"), fullThinking.ToString());
LogNoLine(res.Thinking);
}
// Get the text from the current chunk
var chunkText = res.Text;
usage = res.UsageMetadata;
// THIS is how you see the "thinking" in real-time - handle null case
if (chunkText != null)
LogNoLine(chunkText);
// Append the chunk to our StringBuilder to build the final result
fullResponse.Append(chunkText);
}
// Save the full response to a file
File.WriteAllText(Path.Combine(Output, $"{tag} - Full Response.txt"), $"Thinking:\n{fullThinking}\n\nResponse:\n{fullResponse}");
// Write the usage log
string header = "DateTime,Elapsed,Tag,Model,PromptTokens,ThoughtsTokens,ToolUsePromptTokens,CandidatesTokens,TotalTokens";
var usageMessage = $"{DateTime.Now},{stopwatch.Elapsed},{tag},{model.Model},{usage?.PromptTokenCount},{usage?.ThoughtsTokenCount},{usage?.ToolUsePromptTokenCount},{usage?.CandidatesTokenCount},{usage?.TotalTokenCount}";
LogScv(Output, "gemini_usage.csv", header, usageMessage);
LogScv(Archive, "gemini_usage.csv", header, usageMessage);
// After the loop, the StringBuilder contains the complete text.
return fullResponse.ToString();
//}
//catch (Exception ex)
//{
// await LogError(ex);
//}
}
return "Failed: Please check the Error Logs";
}
///
/// Logs a line of text to a specified file, creating the file with a header if it does not already exist.
///
/// If the specified file does not exist, it will be created, and the provided header will be
/// written as the first line. Subsequent calls will append the specified line to the file.
/// The directory path where the file is located or will be created.
/// The name of the file to which the log entry will be written.
/// The header text to write to the file if it is created.
/// The line of text to append to the file.
public static void LogScv(string path, string file, string header, string line)
{
string fullPath = Path.Combine(path, file);
if (File.Exists(fullPath) is false)
File.WriteAllText(fullPath, header + Environment.NewLine);
File.AppendAllText(fullPath, line + Environment.NewLine);
}
///
/// Generates an image based on the specified prompt and saves it to the specified file path.
///
/// This method attempts to generate an image using a sequence of models, starting with the most
/// advanced. If an error occurs during the process, it retries with progressively less expensive models up to four
/// times. The generated image is saved as a file in the specified location.
/// The text prompt describing the desired image content.
/// The file path where the generated image will be saved. The path must be writable.
/// A representing the asynchronous operation. The task result is a string indicating
/// the outcome: if the image was generated and saved successfully; otherwise, .
public static async Task GenerateImage(string prompt, string outputFile)
{
var keyUsed = PaidApiKeys;
// Cycle through 4 different models until one works
for (int modelIndex = 0; modelIndex < ImageModels.Length; modelIndex++)
for (int keyIndex = 0; keyIndex < keyUsed.Length; keyIndex++)
try
{
// Use cheaper and cheaper models
string imageModel = ImageModels[modelIndex];
string apiKey = keyUsed[keyIndex];
Console.WriteLine($"Generating image with model '{imageModel}'");
// Define the model
var googleAI = new GoogleAI(apiKey);
var model = googleAI.ImageGenerationModel(imageModel);
var requestOptions = new RequestOptions(null, TimeSpan.FromMinutes(2));
// Call the API
var response = await model.GenerateImages(prompt, 1, null, ImageAspectRatio.Ratio1x1, null, null, null, null, null, null, requestOptions);
// Check for failure, maybe censured
if (response.Predictions is null)
continue;
// Save the image to a file
var image = response.Predictions.First();
byte[] imageBytes = Convert.FromBase64String(image.BytesBase64Encoded);
await File.WriteAllBytesAsync(outputFile, imageBytes);
// We can get out now
return "Success";
}
catch (Exception ex)
{
await LogError(ex);
}
return "Failed";
}
///
/// Executes a generative AI query using the specified model, prompt, and input data files, and returns the
/// generated response as a string.
///
/// This method performs the following steps: Uploads the specified
/// input data files to the generative AI model.Constructs a query prompt that includes the provided
/// text prompt and references to the uploaded files.Streams the generated response in real-time,
/// appending each chunk to build the final result.Logs usage metadata, including token counts and
/// elapsed time, to a CSV file. The method attempts to use multiple API keys in sequence if the
/// first attempt fails. If all attempts fail, the method returns a failure message.
/// The text prompt to guide the generative AI model in producing a response.
/// A user-defined tag to categorize or identify the query in logs.
/// The name of the generative AI model to use for the query. This should include the full model identifier.
/// A list of file paths representing input data files to be uploaded and referenced in the query.
/// A representing the asynchronous operation. The task result contains the generated
/// response as a string. If the operation fails, the result will be "Failed: Please check the Error Logs".
public static async Task Query(string prompt, string tag, string modelName, List inputDataFiles)
{
for (int keyIndex = 0; keyIndex < 2; keyIndex++)
try
{
// Use cheaper and cheaper models
string apiKey = FreeApiKeys[keyIndex];
// Get the model core name
string modelCoreName = modelName.Split('/').Last();
Console.WriteLine($"\nStarting Gemini Query with model: {modelCoreName}");
// Define the model
var model = new GenerativeModel()
{
Model = modelCoreName,
ApiKey = apiKey,
UseGoogleSearch = true,
Timeout = TimeSpan.FromMinutes(5)
};
// Generation config
var generationConfig = new GenerationConfig
{
Temperature = 0.0f,
CandidateCount = 1,
ThinkingConfig = new ThinkingConfig
{
ThinkingBudget = 8000,
IncludeThoughts = true,
},
};
// Create a collection of upload tasks
var uploadTasks = inputDataFiles.Select(filePath => model.UploadFile(filePath, Path.GetFileName(filePath)));
// Await all tasks to complete
var results = await Task.WhenAll(uploadTasks);
var uploadedFiles = results.Select(response => response.File).ToList();
foreach (var file in uploadedFiles)
Console.WriteLine($"- Includes: {file.DisplayName} ({file.MimeType}) {file.SizeBytes} bytes, {file.State} -> {file.Uri}' ");
// Initialising the prompt
var myPrompt = new List { new TextData { Text = prompt } };
// Add a reference to each uploaded file to the prompt.
foreach (var file in uploadedFiles)
myPrompt.Add(new FileData { MimeType = file.MimeType, FileUri = file.Uri });
// Prepare the log
var usageLog = Path.Combine(Archive, "gemini_usage.csv");
if (File.Exists(usageLog) is false)
File.WriteAllText(usageLog, "DateTime,Elapsed,Tag,Model,PromptTokens,ThoughtsTokens,ToolUsePromptTokens,CandidatesTokens,TotalTokens\n");
// Use GenerateContentStreamAsync to get the response as a stream.
var fullResponse = new System.Text.StringBuilder();
Stopwatch stopwatch = Stopwatch.StartNew();
var stream = model.GenerateContentStream(myPrompt, generationConfig);
UsageMetadata? usage = null;
// Use "await foreach" to process each chunk as it arrives.
await foreach (var res in stream)
{
// Get the text from the current chunk
var chunkText = res.Text;
usage = res.UsageMetadata;
// THIS is how you see the "thinking" in real-time
Console.Write(chunkText);
// Append the chunk to our StringBuilder to build the final result
fullResponse.Append(chunkText);
}
File.AppendAllText(usageLog, $"{DateTime.Now},{stopwatch.Elapsed},{tag},{model.Model}," +
$"{usage?.PromptTokenCount},{usage?.ThoughtsTokenCount},{usage?.ToolUsePromptTokenCount},{usage?.CandidatesTokenCount},{usage?.TotalTokenCount}\n");
// After the loop, the StringBuilder contains the complete text.
return fullResponse.ToString();
}
catch (Exception ex)
{
await LogError(ex);
}
return "Failed: Please check the Error Logs";
}
public static async Task NanoBanana(string prompt, string tag)
{
try
{
// Define the model
var model = new GenerativeModel()
{
Model = Model.Gemini25FlashImage,
ApiKey = PaidApiKeys.First(),
UseGoogleSearch = false,
Timeout = TimeSpan.FromMinutes(5)
};
// Generation config
var generationConfig = new GenerationConfig
{
Temperature = 0.0f,
CandidateCount = 1,
ResponseModalities = [ResponseModality.Image]
};
Console.WriteLine($"\nStarting Gemini Image Query with model: {model.Model}");
// Initialising the prompt
var myPrompt = new List { new TextData { Text = prompt } };
// Prepare the log
var usageLog = Path.Combine(Archive, "gemini_usage.csv");
if (File.Exists(usageLog) is false)
File.WriteAllText(usageLog, "DateTime,Elapsed,Tag,Model,PromptTokens,ThoughtsTokens,ToolUsePromptTokens,CandidatesTokens,TotalTokens\n");
// Use GenerateContentStreamAsync to get the response as a stream.
var fullResponse = new System.Text.StringBuilder();
Stopwatch stopwatch = Stopwatch.StartNew();
var stream = model.GenerateContentStream(myPrompt, generationConfig);
UsageMetadata? usage = null;
// Use "await foreach" to process each chunk as it arrives.
await foreach (var res in stream)
{
// Get the text from the current chunk
var chunkText = res.Text;
usage = res.UsageMetadata;
// THIS is how you see the "thinking" in real-time
Console.Write(chunkText);
// Append the chunk to our StringBuilder to build the final result
fullResponse.Append(chunkText);
}
File.AppendAllText(usageLog, $"{DateTime.Now},{stopwatch.Elapsed},{tag},{model.Model}," +
$"{usage?.PromptTokenCount},{usage?.ThoughtsTokenCount},{usage?.ToolUsePromptTokenCount},{usage?.CandidatesTokenCount},{usage?.TotalTokenCount}\n");
// After the loop, the StringBuilder contains the complete text.
return fullResponse.ToString();
}
catch (Exception ex)
{
await LogError(ex);
}
return "Failed: Please check the Error Logs";
}
///
/// Logs the details of an exception to the console and appends them to an error log file.
///
/// This method writes the exception message to the console for immediate visibility and appends
/// detailed information, including the stack trace and inner exception (if available), to a log file named
/// "gemini_errors.txt" located in the application's archive directory.
/// The exception to log. Must not be .
public static async Task LogError(Exception ex)
{
Log($"Error during query: {ex.Message}");
var errorFilePath = Path.Combine(Output, "gemini_errors.txt");
await File.AppendAllTextAsync(errorFilePath, $"\n\n\n------------------------------------------------------" +
$"{DateTime.Now}\n{ex.Message}\n\n{ex.InnerException?.Message}\n\n{ex.StackTrace}");
return ex.Message;
}
////private string staticModel = "gemini-2.0-flash";
////private const string model = "gemini-2.0-flash-lite";
////private string model = "gemini-2.0-flash";
////private string model = "gemini-2.5-pro";
////private string model = "gemini-2.5-pro-exp-03-25";
//private const string ApiBaseUrl = "https://generativelanguage.googleapis.com/v1beta/models/";
//private readonly string _apiKey = "AIzaSyAsphWA1WPfht3DbL2BBTa5bSknyIU1oHA";
//private class FileMetadata
//{
// [JsonPropertyName("name")]
// public string Name { get; set; }
// [JsonPropertyName("uploadUri")]
// public string UploadUri { get; set; }
//}
//private class FileApiResponse
//{
// public FileMetadata File { get; set; }
//}
//public GeminiApiClient()
//{
//}
//public static async Task Query(string prompt, string model)
//{
// var client = new GeminiApiClient();
// return await client.GenerateText(prompt, model);
//}
//public async Task GenerateText(string prompt, string model)
//{
// using var httpClient = new HttpClient();
// httpClient.DefaultRequestHeaders.Accept.Clear();
// httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// httpClient.Timeout = TimeSpan.FromMinutes(15);
// //var requestBody = new
// //{
// // contents = new[]
// // {
// // new
// // {
// // parts = new[]
// // {
// // new { text = prompt }
// // }
// // }
// // }
// //};
// var requestBody = new
// {
// contents = new[]
// {
// new
// {
// role = "user",
// parts = new[]
// {
// new { text = prompt }
// }
// }
// },
// generationConfig = new
// {
// temperature = 0
// },
// tools = new[]
// {
// new { googleSearch = new {}, urlContext = new{ } } // The empty object for googleSearch
// }
// };
// //prompt = "What is the weather right now in London UK ?";
// //var jtext = """
// // {
// // "contents": [
// // {
// // "role": "user",
// // "parts": [
// // {
// // "text": "INSERT_INPUT_HERE"
// // },
// // ]
// // },
// // ],
// // "generationConfig": {
// // "thinkingConfig": {
// // "thinkingBudget": -1,
// // },
// // },
// // "tools": [
// // {
// // "urlContext": {}
// // },
// // {
// // "googleSearch": {}
// // },
// // ],
// // }
// // """;
// //var jtext = """
// // {
// // "contents": [
// // {
// // "role": "user",
// // "parts": [
// // {
// // "text": "INSERT_INPUT_HERE"
// // },
// // ]
// // },
// // ],
// // "generationConfig": {
// // "thinkingConfig": {
// // "thinkingBudget": -1,
// // },
// // },
// // "tools": [
// // {
// // "urlContext": {}
// // },
// // {
// // "googleSearch": {}
// // },
// // ]
// // }
// // """;
// //string Q = "\"";
// //jtext = jtext.Replace("INSERT_INPUT_HERE", prompt.Replace(Q, "'").Replace("\r", "").Replace("\n", "\\n"));
// //File.WriteAllText("Debug\\LastPrompt.json", jtext);
// //var requestBody = new
// //{
// // contents = new[]
// // {
// // new
// // {
// // //role = "user",
// // parts = new[]
// // {
// // new { text = prompt }
// // }
// // }
// // },
// // generationConfig = new
// // {
// // temperature = 1.0,
// // thinkingConfig = new
// // {
// // thinkingBudget = 500
// // }
// // }
// // //,
// // //tools = new object[]
// // //{
// // // new { urlContext = new {} },
// // // new { googleSearch = new {} }
// // //}
// //};
// File.WriteAllText("Debug\\gemini_prompt.txt", prompt);
// var jsonRequestBody = JsonSerializer.Serialize(requestBody); //jtext; //
// var content = new StringContent(jsonRequestBody, Encoding.UTF8, "application/json");
// File.WriteAllText("Debug\\gemini_query.json", jsonRequestBody);
// var modelVersion = model.Split("/").Last(); // staticModel; //
// var requestUri = $"{ApiBaseUrl}{modelVersion}:generateContent?key={_apiKey}";
// HttpResponseMessage response = await httpClient.PostAsync(requestUri, content);
// // 4. Read the response content as a string
// string responseContent = await response.Content.ReadAsStringAsync();
// File.WriteAllText("Debug\\gemini_response.json", responseContent);
// File.AppendAllText("Debug\\gemini_response.json", responseContent + Environment.NewLine);
// // Deserialize the response to extract the generated text
// try
// {
// using (JsonDocument doc = JsonDocument.Parse(responseContent))
// {
// JsonElement root = doc.RootElement;
// // Navigate to the usageMetadata object.
// if (root.TryGetProperty("usageMetadata", out JsonElement usageMetadata))
// {
// // Safely extract each value using TryGetProperty and TryGetInt32.
// // This prevents exceptions if a property is missing.
// usageMetadata.TryGetProperty("promptTokenCount", out JsonElement promptTokensElement);
// usageMetadata.TryGetProperty("candidatesTokenCount", out JsonElement candidatesTokensElement);
// usageMetadata.TryGetProperty("totalTokenCount", out JsonElement totalTokensElement);
// usageMetadata.TryGetProperty("toolUsePromptTokenCount", out JsonElement toolUseTokensElement);
// // Extract the model version and response ID from the root.
// root.TryGetProperty("modelVersion", out JsonElement modelVersionElement);
// root.TryGetProperty("responseId", out JsonElement responseIdElement);
// // Get the integer or string values, defaulting to 0 or "N/A" if not found.
// int promptTokens = promptTokensElement.ValueKind == JsonValueKind.Number ? promptTokensElement.GetInt32() : 0;
// int candidatesTokens = candidatesTokensElement.ValueKind == JsonValueKind.Number ? candidatesTokensElement.GetInt32() : 0;
// int totalTokens = totalTokensElement.ValueKind == JsonValueKind.Number ? totalTokensElement.GetInt32() : 0;
// int toolUseTokens = toolUseTokensElement.ValueKind == JsonValueKind.Number ? toolUseTokensElement.GetInt32() : 0;
// string modelVersion2 = modelVersionElement.GetString() ?? "N/A";
// string responseId = responseIdElement.GetString() ?? "N/A";
// string timestamp = DateTime.UtcNow.ToString("o"); // ISO 8601 format
// // --- CSV File Handling ---
// // Check if the file exists to determine if we need to write the header.
// string csvFilePath = "Debug\\gemini_usage_metadata.csv";
// bool fileExists = File.Exists(csvFilePath);
// var csvLine = new StringBuilder();
// // If the file doesn't exist, create it and add the header row.
// if (!fileExists)
// {
// var header = "Timestamp,ResponseId,ModelVersion,PromptTokens,CandidatesTokens,ToolUseTokens,TotalTokens";
// csvLine.AppendLine(header);
// }
// // Format the extracted data as a single CSV line.
// csvLine.AppendFormat("\"{0}\",\"{1}\",\"{2}\",{3},{4},{5},{6}",
// timestamp,
// responseId,
// modelVersion2,
// promptTokens,
// candidatesTokens,
// toolUseTokens,
// totalTokens);
// // Asynchronously append the new line to the CSV file.
// await File.AppendAllTextAsync(csvFilePath, csvLine.ToString() + Environment.NewLine);
// }
// // Navigate to the 'candidates' array as before
// if (root.TryGetProperty("candidates", out JsonElement candidates) && candidates.GetArrayLength() > 0)
// {
// // Navigate to the 'content' and 'parts' of the first candidate
// if (candidates[0].TryGetProperty("content", out JsonElement contentElement) &&
// contentElement.TryGetProperty("parts", out JsonElement parts) &&
// parts.GetArrayLength() > 0)
// {
// // 1. Use a StringBuilder for efficient string concatenation
// var fullText = new System.Text.StringBuilder();
// // 2. Loop through EACH part in the 'parts' array
// foreach (JsonElement part in parts.EnumerateArray())
// {
// // 3. Get the text from the current part and append it
// if (part.TryGetProperty("text", out JsonElement textElement))
// {
// fullText.Append(textElement.GetString());
// }
// }
// // 4. Return the final combined string
// return fullText.ToString();
// }
// }
// }
// }
// catch (JsonException ex)
// {
// return $"Error deserializing response: {ex.Message}";
// }
// return "";
//}
//public async Task GenerateTextWithAttachments(string prompt, string model, List localFilePaths)
//{
// File.WriteAllText("Debug\\gemini_prompt.txt", prompt);
// using var httpClient = new HttpClient();
// httpClient.DefaultRequestHeaders.Accept.Clear();
// httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// httpClient.Timeout = TimeSpan.FromMinutes(5);
// // 2. Create a list of all the parts for the request, starting with the prompt.
// var allParts = new List