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 _logger; private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); private Process? _process; private bool _isReady = false; private readonly JsonSerializerOptions _serializeOptions; public GeminiBridgeManager(ILogger 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 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(); } } }