FlowAPI / FlowAPI.Application /Services /GeminiBridgeManager.cs
danylokhodus's picture
init
b9c7f0e
Raw
History Blame Contribute Delete
7.63 kB
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using System.Threading;
using System.Threading.Tasks;
using FlowAPI.Application.Interfaces;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace FlowAPI.Application.Services
{
public class GeminiBridgeManager : IGeminiBridgeManager, IDisposable
{
private readonly ILogger<GeminiBridgeManager> _logger;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private Process? _process;
private bool _isReady = false;
private readonly JsonSerializerOptions _serializeOptions;
public GeminiBridgeManager(ILogger<GeminiBridgeManager> logger, IHostApplicationLifetime appLifetime)
{
_logger = logger;
_serializeOptions = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
appLifetime.ApplicationStopping.Register(() =>
{
_logger.LogInformation("Application stopping, closing Gemini Bridge.");
Dispose();
});
}
private async Task EnsureProcessStartedAsync()
{
if (_process != null && !_process.HasExited && _isReady)
return;
_logger.LogInformation("Starting persistent Gemini Bridge process...");
string scriptPath = Path.Combine(AppContext.BaseDirectory, "gemini_bridge.py");
if (!File.Exists(scriptPath))
{
scriptPath = Path.Combine(Directory.GetCurrentDirectory(), "gemini_bridge.py");
}
if (!File.Exists(scriptPath))
{
scriptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "gemini_bridge.py");
}
if (!File.Exists(scriptPath))
{
throw new FileNotFoundException("Could not locate gemini_bridge.py in application folders.");
}
var startInfo = new ProcessStartInfo
{
FileName = "python",
Arguments = $"\"{scriptPath}\" --headless",
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(scriptPath)
};
_process = new Process { StartInfo = startInfo };
_process.Start();
// Fire and forget error logging
_ = Task.Run(async () =>
{
while (_process != null && !_process.HasExited && !_process.StandardError.EndOfStream)
{
var errLine = await _process.StandardError.ReadLineAsync();
if (!string.IsNullOrEmpty(errLine))
{
_logger.LogWarning($"[GeminiBridge Python]: {errLine}");
}
}
});
// Wait for bridge to declare itself "ready"
var readyTimeout = Task.Delay(TimeSpan.FromSeconds(45));
var readReadyTask = Task.Run(async () =>
{
while (_process != null && !_process.HasExited && !_process.StandardOutput.EndOfStream)
{
var l = await _process.StandardOutput.ReadLineAsync();
if (l != null && l.Contains("\"type\": \"ready\""))
{
return true;
}
}
return false;
});
var completedTask = await Task.WhenAny(readReadyTask, readyTimeout);
if (completedTask == readyTimeout || !await readReadyTask)
{
try { _process.Kill(); } catch { }
_process = null;
throw new Exception("Timeout or failure waiting for persistent Gemini Browser Bridge to initialize.");
}
_isReady = true;
_logger.LogInformation("Persistent Gemini Bridge initialized successfully.");
}
public async Task<string> SendPromptAsync(string promptText, bool isNewChat, int timeoutSeconds = 120)
{
await _semaphore.WaitAsync();
try
{
await EnsureProcessStartedAsync();
if (_process == null || _process.HasExited)
throw new Exception("Gemini Bridge process is not running.");
var promptObj = new
{
type = "prompt",
id = Guid.NewGuid().ToString(),
new_chat = isNewChat,
content = promptText
};
string promptRequestJson = JsonSerializer.Serialize(promptObj, _serializeOptions);
// Send Prompt to standard input
await _process.StandardInput.WriteLineAsync(promptRequestJson);
await _process.StandardInput.FlushAsync();
// Await Gemini final response
var responseTimeout = Task.Delay(TimeSpan.FromSeconds(timeoutSeconds));
var readResponseTask = Task.Run(async () =>
{
while (_process != null && !_process.HasExited && !_process.StandardOutput.EndOfStream)
{
var l = await _process.StandardOutput.ReadLineAsync();
if (l != null && l.Contains("\"type\": \"response\"") && l.Contains($"\"id\": \"{promptObj.id}\""))
{
return l;
}
}
return null;
});
var responseCompletedTask = await Task.WhenAny(readResponseTask, responseTimeout);
if (responseCompletedTask == responseTimeout)
{
// If timeout occurs, we should probably kill the process to avoid desync
try { _process.Kill(); } catch { }
_process = null;
_isReady = false;
throw new Exception("Timeout waiting for AI response from Gemini Bridge.");
}
string? rawResponseJson = await readResponseTask;
if (string.IsNullOrEmpty(rawResponseJson))
{
throw new Exception("Did not receive a valid response block from the Gemini Bridge.");
}
return rawResponseJson;
}
finally
{
_semaphore.Release();
}
}
public void Dispose()
{
if (_process != null)
{
try
{
if (!_process.HasExited)
{
_process.StandardInput.WriteLine("{\"type\": \"exit\"}");
_process.StandardInput.Flush();
Thread.Sleep(500); // Give it a moment to exit gracefully
if (!_process.HasExited)
{
_process.Kill();
}
}
}
catch { }
_process.Dispose();
_process = null;
}
_semaphore.Dispose();
}
}
}