Spaces:
Running
Running
| 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(); | |
| } | |
| } | |
| } | |