diff --git a/api_endpoint/logging_server.py b/api_endpoint/logging_server.py index 3d769a37e90..861b469fdb5 100644 --- a/api_endpoint/logging_server.py +++ b/api_endpoint/logging_server.py @@ -33,6 +33,12 @@ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import uvicorn +from dotenv import load_dotenv + +load_dotenv() + + + # Configuration LOGS_DIR = Path(__file__).parent / "logs" / "realtime" diff --git a/api_endpoint/websocket_connection_pool.py b/api_endpoint/websocket_connection_pool.py index 16ca0974459..efb3349653d 100644 --- a/api_endpoint/websocket_connection_pool.py +++ b/api_endpoint/websocket_connection_pool.py @@ -347,36 +347,111 @@ def connect_sync(server_url: str = "ws://localhost:8000/ws") -> bool: """ Synchronous connect - handles event loop internally. - Agent code should use this instead of directly managing event loops. - This ensures the connection pool maintains full control over its lifecycle. + Creates a persistent event loop in a background thread if needed. + This is thread-safe and can be called from any thread (including agent background threads). """ - try: - loop = asyncio.get_event_loop() - if loop.is_closed(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + import threading - return loop.run_until_complete(ws_pool.connect(server_url)) + # If pool doesn't have a loop yet or it's closed, we need to start one + if not ws_pool.loop or ws_pool.loop.is_closed(): + # Start connection in a background thread with its own loop + result_container = {"success": False, "error": None, "connected": False} + + def run_in_thread(): + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + ws_pool.loop = loop # Store the loop in the pool + + # Connect to WebSocket + result_container["success"] = loop.run_until_complete(ws_pool.connect(server_url)) + result_container["connected"] = True + + # Keep loop running forever for future send_event calls + # This is critical - the loop must stay alive for run_coroutine_threadsafe to work + loop.run_forever() + + except Exception as e: + result_container["error"] = str(e) + print(f"❌ Error in WebSocket connection thread: {e}") + finally: + # Clean up if loop stops + if loop.is_running(): + loop.close() + + thread = threading.Thread(target=run_in_thread, daemon=True, name="WebSocket-EventLoop") + thread.start() + + # Wait for connection to complete (but not for loop to exit - it runs forever) + import time + timeout = 10.0 + start = time.time() + while not result_container["connected"] and (time.time() - start) < timeout: + time.sleep(0.1) + + if result_container["error"]: + print(f"⚠️ Connection failed: {result_container['error']}") + + return result_container["success"] + else: + # Pool already has a loop, use run_coroutine_threadsafe + try: + future = asyncio.run_coroutine_threadsafe( + ws_pool.connect(server_url), + ws_pool.loop + ) + return future.result(timeout=10.0) + except Exception as e: + print(f"⚠️ Connection failed: {e}") + return False def send_event_sync(event_type: str, session_id: str, data: Dict[str, Any]) -> bool: """ Synchronous send event - handles event loop internally. - Agent code should use this instead of managing event loops. - This ensures the connection pool maintains full control over its lifecycle. + Uses the WebSocket pool's own event loop to avoid loop conflicts. + This is critical when called from background threads (like agent execution). + This is thread-safe and works correctly even when agent runs in a different thread. """ - try: - loop = asyncio.get_event_loop() - if loop.is_closed(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + if not ws_pool.loop or not ws_pool.loop.is_running(): + # No event loop running - can't send + print("⚠️ WebSocket pool has no running event loop") + return False - return loop.run_until_complete(ws_pool.send_event(event_type, session_id, data)) + try: + # Use run_coroutine_threadsafe to submit to the WebSocket pool's loop + # This works across threads - submits the coroutine to the correct loop + future = asyncio.run_coroutine_threadsafe( + ws_pool.send_event(event_type, session_id, data), + ws_pool.loop # ← Use the pool's loop, not current thread's loop + ) + + # Wait for completion (with timeout to avoid hanging) + return future.result(timeout=5.0) + + except TimeoutError: + print(f"⚠️ Timeout sending event {event_type}") + return False + except Exception as e: + print(f"⚠️ Error sending event: {e}") + return False + +def disconnect_sync(): + """ + Synchronous disconnect - handles event loop internally. + + Thread-safe disconnect that works from any thread. + """ + if ws_pool.loop and ws_pool.loop.is_running(): + try: + future = asyncio.run_coroutine_threadsafe( + ws_pool.disconnect(), + ws_pool.loop + ) + return future.result(timeout=5.0) + except Exception as e: + print(f"⚠️ Error disconnecting: {e}") + return False + return True diff --git a/mixture_of_agents_tool.py b/mixture_of_agents_tool.py index 206b1496328..1634dce89a3 100644 --- a/mixture_of_agents_tool.py +++ b/mixture_of_agents_tool.py @@ -50,13 +50,16 @@ import os import asyncio import uuid import datetime +from dotenv import load_dotenv from pathlib import Path from typing import Dict, Any, List, Optional from openai import AsyncOpenAI +load_dotenv() + # Initialize Nous Research API client for MoA processing nous_client = AsyncOpenAI( - api_key=os.getenv("NOUS_API_KEY"), + api_key="sk-_yoJ_CBLbSNN2R5rGZ_rpg", base_url="https://inference-api.nousresearch.com/v1" ) diff --git a/ui/hermes_ui.py b/ui/hermes_ui.py index 6285f2a6aa4..dd204b46cc5 100644 --- a/ui/hermes_ui.py +++ b/ui/hermes_ui.py @@ -11,6 +11,7 @@ Features: - Real-time event display via WebSocket - Beautiful, responsive UI with dark theme - Session history +- Safe exit handling (no segfaults) Usage: python hermes_ui.py @@ -19,22 +20,25 @@ Usage: import sys import json import signal -import asyncio +import os import requests from datetime import datetime from typing import Dict, Any, List, Optional +# Suppress Qt logging warnings BEFORE importing Qt +os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' + from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLabel, QLineEdit, QComboBox, QCheckBox, - QGroupBox, QScrollArea, QSplitter, QListWidget, QListWidgetItem, - QTextBrowser, QTabWidget, QSpinBox, QMessageBox, QProgressBar + QGroupBox, QSplitter, QListWidget, QListWidgetItem, + QTextBrowser, QSpinBox, QMessageBox ) from PySide6.QtCore import ( - Qt, Signal, Slot, QThread, QObject, QTimer + Qt, Signal, Slot, QObject, QTimer ) from PySide6.QtGui import ( - QFont, QColor, QPalette, QTextCursor, QTextCharFormat + QFont, QTextCursor ) # WebSocket imports @@ -92,7 +96,7 @@ class WebSocketClient(QObject): ) # Run forever with reconnection - self.ws.run_forever(ping_interval=30, ping_timeout=10) + self.ws.run_forever(ping_interval=300, ping_timeout=60) except Exception as e: self.error.emit(f"WebSocket error: {str(e)}") @@ -142,10 +146,16 @@ class EventDisplayWidget(QWidget): header.setFont(QFont("Arial", 12, QFont.Bold)) layout.addWidget(header) - # Event display (rich text browser) + # Event display (rich text browser) - use system monospace font self.event_display = QTextBrowser() self.event_display.setOpenExternalLinks(False) - self.event_display.setFont(QFont("Monaco", 10)) + + # Use system monospace font instead of hardcoded "Monaco" or "Courier" + font = QFont() + font.setStyleHint(QFont.Monospace) + font.setPointSize(10) + self.event_display.setFont(font) + layout.addWidget(self.event_display) # Clear button @@ -269,6 +279,7 @@ class HermesMainWindow(QMainWindow): self.ws_client = None self.current_session_id = None self.available_toolsets = [] + self.is_closing = False # Flag to prevent reconnection during shutdown self.init_ui() self.setup_websocket() @@ -445,6 +456,10 @@ class HermesMainWindow(QMainWindow): @Slot() def on_ws_disconnected(self): """Called when WebSocket connection is lost.""" + # Don't attempt reconnection if we're closing the application + if self.is_closing: + return + self.connection_status.setText("🔴 Disconnected") self.connection_status.setStyleSheet("QLabel { padding: 5px; background-color: #F44336; color: white; border-radius: 3px; }") self.statusBar().showMessage("WebSocket disconnected - attempting reconnect...") @@ -583,15 +598,61 @@ class HermesMainWindow(QMainWindow): # Re-enable button after short delay (UI feedback) QTimer.singleShot(2000, lambda: self.submit_btn.setText("🚀 Submit Query")) - def closeEvent(self, event): - """Handle window close event.""" + def cleanup(self): + """Clean up resources before exit.""" + print("Cleaning up resources...") + self.is_closing = True + if self.ws_client: - self.ws_client.disconnect() + try: + self.ws_client.disconnect() + except Exception as e: + print(f"Error disconnecting WebSocket: {e}") + + def closeEvent(self, event): + """Handle window close event - ensures clean shutdown.""" + print("Closing application...") + self.cleanup() event.accept() +def setup_signal_handlers(app: QApplication) -> QTimer: + """ + Setup signal handlers for graceful shutdown on Ctrl+C. + + This prevents segmentation faults by: + 1. Catching SIGINT/SIGTERM signals + 2. Creating a timer that keeps Python responsive to signals + 3. Calling app.quit() for proper Qt cleanup + + Args: + app: The QApplication instance + + Returns: + Timer that keeps Python interpreter responsive to signals + """ + def signal_handler(signum, frame): + """Handle interrupt signals gracefully.""" + print("\n🛑 Interrupt received, shutting down gracefully...") + app.quit() + + # Register signal handlers + signal.signal(signal.SIGINT, signal_handler) # Ctrl+C + signal.signal(signal.SIGTERM, signal_handler) # Termination signal + + # CRITICAL: Create a timer to wake up Python interpreter periodically + # This allows Python to process signals while Qt's event loop is running + # Without this, Ctrl+C will not work and may cause segfaults + timer = QTimer() + timer.timeout.connect(lambda: None) # Empty callback just to wake up Python + timer.start(100) # Check every 100ms + + return timer + + def main(): """Main entry point for the application.""" + # Create application app = QApplication(sys.argv) # Set application metadata @@ -599,6 +660,9 @@ def main(): app.setOrganizationName("Hermes") app.setApplicationVersion("1.0.0") + # Setup signal handlers for safe Ctrl+C handling (prevents segfaults!) + timer = setup_signal_handlers(app) + # Apply dark theme (optional) # Uncomment to enable dark mode # app.setStyle("Fusion") @@ -611,10 +675,15 @@ def main(): window = HermesMainWindow() window.show() + print("✨ Hermes Agent UI started") + print(" Press Ctrl+C to exit gracefully") + # Start event loop - sys.exit(app.exec()) + exit_code = app.exec() + + print("👋 Hermes Agent UI closed") + sys.exit(exit_code) if __name__ == "__main__": - main() - + main() \ No newline at end of file diff --git a/vision_tools.py b/vision_tools.py index 3183713bd64..e51dba22a04 100644 --- a/vision_tools.py +++ b/vision_tools.py @@ -29,11 +29,14 @@ import json import os import asyncio import uuid +from dotenv import load_dotenv import datetime from pathlib import Path from typing import Dict, Any, Optional from openai import AsyncOpenAI +load_dotenv() + # Initialize Nous Research API client for vision processing nous_client = AsyncOpenAI( api_key=os.getenv("NOUS_API_KEY"), diff --git a/web_tools.py b/web_tools.py index 4c6a8ad7a2c..d09fb79a329 100644 --- a/web_tools.py +++ b/web_tools.py @@ -42,6 +42,7 @@ Usage: import json import os +from dotenv import load_dotenv import re import asyncio import uuid @@ -51,6 +52,9 @@ from typing import List, Dict, Any, Optional from firecrawl import Firecrawl from openai import AsyncOpenAI + +load_dotenv() + # Initialize Firecrawl client once at module level firecrawl_client = Firecrawl(api_key=os.getenv("FIRECRAWL_API_KEY")) @@ -61,7 +65,7 @@ nous_client = AsyncOpenAI( ) # Configuration for LLM processing -DEFAULT_SUMMARIZER_MODEL = "gemini-2.5-flash" +DEFAULT_SUMMARIZER_MODEL = "Hermes-4-70B" DEFAULT_MIN_LENGTH_FOR_SUMMARIZATION = 5000 # Debug mode configuration