From 14c3d30853243d8429781bd74f6dd511fc0a7769 Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Wed, 10 Sep 2025 14:15:31 +0100 Subject: [PATCH 1/9] feat: add support to start ui programmatically from cognee package --- cognee/__init__.py | 1 + cognee/api/v1/ui/__init__.py | 1 + cognee/api/v1/ui/ui.py | 500 +++++++++++++++++++++++++++++++++++ 3 files changed, 502 insertions(+) create mode 100644 cognee/api/v1/ui/__init__.py create mode 100644 cognee/api/v1/ui/ui.py diff --git a/cognee/__init__.py b/cognee/__init__.py index be5a16b3b..27dea1ad5 100644 --- a/cognee/__init__.py +++ b/cognee/__init__.py @@ -27,6 +27,7 @@ from .api.v1.visualize import visualize_graph, start_visualization_server from cognee.modules.visualization.cognee_network_visualization import ( cognee_network_visualization, ) +from .api.v1.ui import start_ui # Pipelines from .modules import pipelines diff --git a/cognee/api/v1/ui/__init__.py b/cognee/api/v1/ui/__init__.py new file mode 100644 index 000000000..f268a2e54 --- /dev/null +++ b/cognee/api/v1/ui/__init__.py @@ -0,0 +1 @@ +from .ui import start_ui, stop_ui, ui diff --git a/cognee/api/v1/ui/ui.py b/cognee/api/v1/ui/ui.py new file mode 100644 index 000000000..bd201d174 --- /dev/null +++ b/cognee/api/v1/ui/ui.py @@ -0,0 +1,500 @@ +import os +import subprocess +import threading +import time +import webbrowser +import zipfile +import requests +from pathlib import Path +from typing import Optional, Tuple +import tempfile +import shutil + +from cognee.shared.logging_utils import get_logger +from cognee.version import get_cognee_version + +logger = get_logger() + + +def get_frontend_cache_dir() -> Path: + """ + Get the directory where downloaded frontend assets are cached. + Uses user's home directory to persist across package updates. + """ + cache_dir = Path.home() / ".cognee" / "ui-cache" + cache_dir.mkdir(parents=True, exist_ok=True) + return cache_dir + + +def get_frontend_download_info() -> Tuple[str, str]: + """ + Get the download URL and version for the actual cognee-frontend source. + Downloads the real frontend from GitHub releases, matching the installed version. + """ + version = get_cognee_version() + + # Clean up version string (remove -local suffix for development) + clean_version = version.replace('-local', '') + + # Download from specific release tag to ensure version compatibility + download_url = f"https://github.com/topoteretes/cognee/archive/refs/tags/v{clean_version}.zip" + + return download_url, version + + +def download_frontend_assets(force: bool = False) -> bool: + """ + Download and cache frontend assets. + + Args: + force: If True, re-download even if already cached + + Returns: + bool: True if successful, False otherwise + """ + cache_dir = get_frontend_cache_dir() + frontend_dir = cache_dir / "frontend" + version_file = cache_dir / "version.txt" + + # Check if already downloaded and up to date + if not force and frontend_dir.exists() and version_file.exists(): + try: + cached_version = version_file.read_text().strip() + current_version = get_cognee_version() + if cached_version == current_version: + logger.debug(f"Frontend assets already cached for version {current_version}") + return True + except Exception as e: + logger.debug(f"Error checking cached version: {e}") + + download_url, version = get_frontend_download_info() + + logger.info("Downloading cognee frontend assets...") + logger.info("This is a one-time download and will be cached for future use.") + + try: + # Create a temporary directory for download + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + archive_path = temp_path / "cognee-main.zip" + + # Download the actual cognee repository from releases + logger.info(f"Downloading cognee v{version.replace('-local', '')} from GitHub releases...") + logger.info(f"URL: {download_url}") + response = requests.get(download_url, stream=True, timeout=60) + response.raise_for_status() + + with open(archive_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + # Extract the archive and find the cognee-frontend directory + if frontend_dir.exists(): + shutil.rmtree(frontend_dir) + + with zipfile.ZipFile(archive_path, 'r') as zip_file: + # Extract to temp directory first + extract_dir = temp_path / "extracted" + zip_file.extractall(extract_dir) + + # Find the cognee-frontend directory in the extracted content + # The archive structure will be: cognee-{version}/cognee-frontend/ + cognee_frontend_source = None + for root, dirs, files in os.walk(extract_dir): + if "cognee-frontend" in dirs: + cognee_frontend_source = Path(root) / "cognee-frontend" + break + + if not cognee_frontend_source or not cognee_frontend_source.exists(): + logger.error("Could not find cognee-frontend directory in downloaded release archive") + logger.error("This might indicate a version mismatch or missing release.") + return False + + # Copy the cognee-frontend to our cache + shutil.copytree(cognee_frontend_source, frontend_dir) + logger.debug(f"Frontend extracted to: {frontend_dir}") + + # Write version info + version_file.write_text(version) + + logger.info(f"✓ Cognee frontend v{version.replace('-local', '')} downloaded and cached successfully!") + return True + + except requests.exceptions.RequestException as e: + if "404" in str(e): + logger.error(f"Release v{version.replace('-local', '')} not found on GitHub.") + logger.error("This version might not have been released yet, or you're using a development version.") + logger.error("Try using a stable release version of cognee.") + else: + logger.error(f"Failed to download from GitHub: {str(e)}") + logger.error("You can still use cognee without the UI functionality.") + return False + except Exception as e: + logger.error(f"Failed to download frontend assets: {str(e)}") + logger.error("You can still use cognee without the UI functionality.") + return False + + + + +def find_frontend_path() -> Optional[Path]: + """ + Find the cognee-frontend directory. + Checks both development location and cached download location. + """ + current_file = Path(__file__) + + # First, try development paths (for contributors/developers) + dev_search_paths = [ + current_file.parents[4] / "cognee-frontend", # from cognee/api/v1/ui/ui.py to project root + current_file.parents[3] / "cognee-frontend", # fallback path + current_file.parents[2] / "cognee-frontend", # another fallback + ] + + for path in dev_search_paths: + if path.exists() and (path / "package.json").exists(): + logger.debug(f"Found development frontend at: {path}") + return path + + # Then try cached download location (for pip-installed users) + cache_dir = get_frontend_cache_dir() + cached_frontend = cache_dir / "frontend" + + if cached_frontend.exists() and (cached_frontend / "package.json").exists(): + logger.debug(f"Found cached frontend at: {cached_frontend}") + return cached_frontend + + return None + + +def check_node_npm() -> tuple[bool, str]: + """ + Check if Node.js and npm are available. + Returns (is_available, error_message) + """ + try: + # Check Node.js + result = subprocess.run(["node", "--version"], + capture_output=True, text=True, timeout=10) + if result.returncode != 0: + return False, "Node.js is not installed or not in PATH" + + node_version = result.stdout.strip() + logger.debug(f"Found Node.js version: {node_version}") + + # Check npm + result = subprocess.run(["npm", "--version"], + capture_output=True, text=True, timeout=10) + if result.returncode != 0: + return False, "npm is not installed or not in PATH" + + npm_version = result.stdout.strip() + logger.debug(f"Found npm version: {npm_version}") + + return True, f"Node.js {node_version}, npm {npm_version}" + + except subprocess.TimeoutExpired: + return False, "Timeout checking Node.js/npm installation" + except FileNotFoundError: + return False, "Node.js/npm not found. Please install Node.js from https://nodejs.org/" + except Exception as e: + return False, f"Error checking Node.js/npm: {str(e)}" + + +def install_frontend_dependencies(frontend_path: Path) -> bool: + """ + Install frontend dependencies if node_modules doesn't exist. + This is needed for both development and downloaded frontends since both use npm run dev. + """ + node_modules = frontend_path / "node_modules" + if node_modules.exists(): + logger.debug("Frontend dependencies already installed") + return True + + logger.info("Installing frontend dependencies (this may take a few minutes)...") + + try: + result = subprocess.run( + ["npm", "install"], + cwd=frontend_path, + capture_output=True, + text=True, + timeout=300 # 5 minutes timeout + ) + + if result.returncode == 0: + logger.info("Frontend dependencies installed successfully") + return True + else: + logger.error(f"Failed to install dependencies: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + logger.error("Timeout installing frontend dependencies") + return False + except Exception as e: + logger.error(f"Error installing frontend dependencies: {str(e)}") + return False + + +def is_development_frontend(frontend_path: Path) -> bool: + """ + Check if this is a development frontend (has Next.js) vs downloaded assets. + """ + package_json_path = frontend_path / "package.json" + if not package_json_path.exists(): + return False + + try: + import json + with open(package_json_path) as f: + package_data = json.load(f) + + # Development frontend has Next.js as dependency + dependencies = package_data.get("dependencies", {}) + dev_dependencies = package_data.get("devDependencies", {}) + + return "next" in dependencies or "next" in dev_dependencies + except Exception: + return False + + +def start_python_server(frontend_path: Path, host: str, port: int) -> Optional[subprocess.Popen]: + """ + Start a Python HTTP server to serve downloaded frontend assets. + This doesn't require Node.js and works with pre-built assets. + """ + try: + # Change to the frontend directory + original_cwd = os.getcwd() + os.chdir(frontend_path) + + # Use subprocess to run the server so we can return a process handle + cmd = [ + "python", "-m", "http.server", str(port), + "--bind", host + ] + + process = subprocess.Popen( + cmd, + cwd=frontend_path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Restore original directory + os.chdir(original_cwd) + + # Give it a moment to start + time.sleep(2) + + # Check if process is still running + if process.poll() is not None: + stdout, stderr = process.communicate() + logger.error("Python HTTP server failed to start:") + logger.error(f"stdout: {stdout}") + logger.error(f"stderr: {stderr}") + return None + + return process + + except Exception as e: + logger.error(f"Failed to start Python HTTP server: {str(e)}") + # Restore original directory on error + try: + os.chdir(original_cwd) + except OSError: + pass + return None + + +def prompt_user_for_download() -> bool: + """ + Ask user if they want to download the frontend assets. + Returns True if user consents, False otherwise. + """ + try: + print("\n" + "="*60) + print("🎨 Cognee UI Setup Required") + print("="*60) + print("The cognee frontend is not available on your system.") + print("This is required to use the web interface.") + print("\nWhat will happen:") + print("• Download the actual cognee-frontend from GitHub") + print("• Cache it in your home directory (~/.cognee/ui-cache/)") + print("• Install dependencies with npm (requires Node.js)") + print("• This is a one-time setup per cognee version") + print("\nThe frontend will then be available offline for future use.") + + response = input("\nWould you like to download the frontend now? (y/N): ").strip().lower() + return response in ['y', 'yes'] + except (KeyboardInterrupt, EOFError): + print("\nOperation cancelled by user.") + return False + + +def start_ui(host: str = "localhost", port: int = 3000, open_browser: bool = True, auto_download: bool = False) -> Optional[subprocess.Popen]: + """ + Start the cognee frontend UI server. + + This function will: + 1. Find the cognee-frontend directory (development) or download it (pip install) + 2. Check if Node.js and npm are available (for development mode) + 3. Install dependencies if needed (development mode) + 4. Start the appropriate server + 5. Optionally open the browser + + Args: + host: Host to bind the server to (default: localhost) + port: Port to run the server on (default: 3000) + open_browser: Whether to open the browser automatically (default: True) + auto_download: If True, download frontend without prompting (default: False) + + Returns: + subprocess.Popen object representing the running server, or None if failed + + Example: + >>> import cognee + >>> server = cognee.start_ui() + >>> # UI will be available at http://localhost:3000 + >>> # To stop the server later: + >>> server.terminate() + """ + logger.info("Starting cognee UI...") + + # Find frontend directory + frontend_path = find_frontend_path() + + if not frontend_path: + logger.info("Frontend not found locally. This is normal for pip-installed cognee.") + + # Offer to download the frontend + if auto_download or prompt_user_for_download(): + if download_frontend_assets(): + frontend_path = find_frontend_path() + if not frontend_path: + logger.error("Download succeeded but frontend still not found. This is unexpected.") + return None + else: + logger.error("Failed to download frontend assets.") + return None + else: + logger.info("Frontend download declined. UI functionality not available.") + logger.info("You can still use all other cognee features without the web interface.") + return None + + # Check Node.js and npm + node_available, node_message = check_node_npm() + if not node_available: + logger.error(f"Cannot start UI: {node_message}") + logger.error("Please install Node.js from https://nodejs.org/ to use the UI functionality") + return None + + logger.debug(f"Environment check passed: {node_message}") + + # Install dependencies if needed + if not install_frontend_dependencies(frontend_path): + logger.error("Failed to install frontend dependencies") + return None + + # Prepare environment variables + env = os.environ.copy() + env['HOST'] = host + env['PORT'] = str(port) + + # Start the development server + logger.info(f"Starting frontend server at http://{host}:{port}") + logger.info("This may take a moment to compile and start...") + + try: + process = subprocess.Popen( + ["npm", "run", "dev"], + cwd=frontend_path, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Give it a moment to start up + time.sleep(3) + + # Check if process is still running + if process.poll() is not None: + stdout, stderr = process.communicate() + logger.error("Frontend server failed to start:") + logger.error(f"stdout: {stdout}") + logger.error(f"stderr: {stderr}") + return None + + # Open browser if requested + if open_browser: + def open_browser_delayed(): + time.sleep(5) # Give Next.js time to fully start + try: + webbrowser.open(f"http://{host}:{port}") # TODO: use dashboard url? + except Exception as e: + logger.warning(f"Could not open browser automatically: {e}") + + browser_thread = threading.Thread(target=open_browser_delayed, daemon=True) + browser_thread.start() + + logger.info("✓ Cognee UI is starting up...") + logger.info(f"✓ Open your browser to: http://{host}:{port}") + logger.info("✓ The UI will be available once Next.js finishes compiling") + + return process + + except Exception as e: + logger.error(f"Failed to start frontend server: {str(e)}") + return None + + +def stop_ui(process: subprocess.Popen) -> bool: + """ + Stop a running UI server process. + + Args: + process: The subprocess.Popen object returned by start_ui() + + Returns: + bool: True if stopped successfully, False otherwise + """ + if not process: + return False + + try: + process.terminate() + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + logger.warning("Process didn't terminate gracefully, forcing kill") + process.kill() + process.wait() + + logger.info("UI server stopped") + return True + + except Exception as e: + logger.error(f"Error stopping UI server: {str(e)}") + return False + + +# Convenience function similar to DuckDB's approach +def ui() -> Optional[subprocess.Popen]: + """ + Convenient alias for start_ui() with default parameters. + Similar to how DuckDB provides simple ui() function. + """ + return start_ui() + + +if __name__ == "__main__": + # Test the UI startup + server = start_ui() + if server: + try: + input("Press Enter to stop the server...") + finally: + stop_ui(server) From 88ac0fc46c67f8eab4d524c96b1387432449ab7d Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Wed, 10 Sep 2025 14:15:48 +0100 Subject: [PATCH 2/9] examples: add start_ui_example.py --- examples/start_ui_example.py | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 examples/start_ui_example.py diff --git a/examples/start_ui_example.py b/examples/start_ui_example.py new file mode 100644 index 000000000..b01c9f150 --- /dev/null +++ b/examples/start_ui_example.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Example showing how to use cognee.start_ui() to launch the frontend. + +This demonstrates the new UI functionality that works similar to DuckDB's start_ui(). +""" + +import asyncio +import cognee +import time + + +async def main(): + # First, let's add some data to cognee for the UI to display + print("Adding sample data to cognee...") + await cognee.add("Natural language processing (NLP) is an interdisciplinary subfield of computer science and information retrieval.") + await cognee.add("Machine learning (ML) is a subset of artificial intelligence that focuses on algorithms and statistical models.") + + # Generate the knowledge graph + print("Generating knowledge graph...") + await cognee.cognify() + + print("\n" + "="*60) + print("Starting cognee UI...") + print("="*60) + + # Start the UI server + server = cognee.start_ui( + host="localhost", + port=3000, + open_browser=True # This will automatically open your browser + ) + + if server: + print("UI server started successfully!") + print("The interface will be available at: http://localhost:3000") + print("\nPress Ctrl+C to stop the server when you're done...") + + try: + # Keep the server running + while server.poll() is None: # While process is still running + time.sleep(1) + except KeyboardInterrupt: + print("\nStopping UI server...") + server.terminate() + server.wait() # Wait for process to finish + print("UI server stopped.") + else: + print("Failed to start UI server. Check the logs above for details.") + + +if __name__ == "__main__": + asyncio.run(main()) From db705f75ab8fbbf5e7f16b3b6d3b765439bf0890 Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Wed, 10 Sep 2025 14:16:02 +0100 Subject: [PATCH 3/9] format: ruff format --- cognee/api/v1/ui/ui.py | 210 ++++++++++++++++++----------------- examples/start_ui_example.py | 28 +++-- 2 files changed, 124 insertions(+), 114 deletions(-) diff --git a/cognee/api/v1/ui/ui.py b/cognee/api/v1/ui/ui.py index bd201d174..36a4f4305 100644 --- a/cognee/api/v1/ui/ui.py +++ b/cognee/api/v1/ui/ui.py @@ -32,30 +32,30 @@ def get_frontend_download_info() -> Tuple[str, str]: Downloads the real frontend from GitHub releases, matching the installed version. """ version = get_cognee_version() - + # Clean up version string (remove -local suffix for development) - clean_version = version.replace('-local', '') - + clean_version = version.replace("-local", "") + # Download from specific release tag to ensure version compatibility download_url = f"https://github.com/topoteretes/cognee/archive/refs/tags/v{clean_version}.zip" - + return download_url, version def download_frontend_assets(force: bool = False) -> bool: """ Download and cache frontend assets. - + Args: force: If True, re-download even if already cached - + Returns: bool: True if successful, False otherwise """ cache_dir = get_frontend_cache_dir() frontend_dir = cache_dir / "frontend" version_file = cache_dir / "version.txt" - + # Check if already downloaded and up to date if not force and frontend_dir.exists() and version_file.exists(): try: @@ -66,37 +66,39 @@ def download_frontend_assets(force: bool = False) -> bool: return True except Exception as e: logger.debug(f"Error checking cached version: {e}") - + download_url, version = get_frontend_download_info() - + logger.info("Downloading cognee frontend assets...") logger.info("This is a one-time download and will be cached for future use.") - + try: # Create a temporary directory for download with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) archive_path = temp_path / "cognee-main.zip" - + # Download the actual cognee repository from releases - logger.info(f"Downloading cognee v{version.replace('-local', '')} from GitHub releases...") + logger.info( + f"Downloading cognee v{version.replace('-local', '')} from GitHub releases..." + ) logger.info(f"URL: {download_url}") response = requests.get(download_url, stream=True, timeout=60) response.raise_for_status() - - with open(archive_path, 'wb') as f: + + with open(archive_path, "wb") as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) - + # Extract the archive and find the cognee-frontend directory if frontend_dir.exists(): shutil.rmtree(frontend_dir) - - with zipfile.ZipFile(archive_path, 'r') as zip_file: + + with zipfile.ZipFile(archive_path, "r") as zip_file: # Extract to temp directory first extract_dir = temp_path / "extracted" zip_file.extractall(extract_dir) - + # Find the cognee-frontend directory in the extracted content # The archive structure will be: cognee-{version}/cognee-frontend/ cognee_frontend_source = None @@ -104,26 +106,32 @@ def download_frontend_assets(force: bool = False) -> bool: if "cognee-frontend" in dirs: cognee_frontend_source = Path(root) / "cognee-frontend" break - + if not cognee_frontend_source or not cognee_frontend_source.exists(): - logger.error("Could not find cognee-frontend directory in downloaded release archive") + logger.error( + "Could not find cognee-frontend directory in downloaded release archive" + ) logger.error("This might indicate a version mismatch or missing release.") return False - + # Copy the cognee-frontend to our cache shutil.copytree(cognee_frontend_source, frontend_dir) logger.debug(f"Frontend extracted to: {frontend_dir}") - + # Write version info version_file.write_text(version) - - logger.info(f"✓ Cognee frontend v{version.replace('-local', '')} downloaded and cached successfully!") + + logger.info( + f"✓ Cognee frontend v{version.replace('-local', '')} downloaded and cached successfully!" + ) return True - + except requests.exceptions.RequestException as e: if "404" in str(e): logger.error(f"Release v{version.replace('-local', '')} not found on GitHub.") - logger.error("This version might not have been released yet, or you're using a development version.") + logger.error( + "This version might not have been released yet, or you're using a development version." + ) logger.error("Try using a stable release version of cognee.") else: logger.error(f"Failed to download from GitHub: {str(e)}") @@ -135,35 +143,33 @@ def download_frontend_assets(force: bool = False) -> bool: return False - - def find_frontend_path() -> Optional[Path]: """ Find the cognee-frontend directory. Checks both development location and cached download location. """ current_file = Path(__file__) - + # First, try development paths (for contributors/developers) dev_search_paths = [ current_file.parents[4] / "cognee-frontend", # from cognee/api/v1/ui/ui.py to project root - current_file.parents[3] / "cognee-frontend", # fallback path + current_file.parents[3] / "cognee-frontend", # fallback path current_file.parents[2] / "cognee-frontend", # another fallback ] - + for path in dev_search_paths: if path.exists() and (path / "package.json").exists(): logger.debug(f"Found development frontend at: {path}") return path - + # Then try cached download location (for pip-installed users) cache_dir = get_frontend_cache_dir() cached_frontend = cache_dir / "frontend" - + if cached_frontend.exists() and (cached_frontend / "package.json").exists(): logger.debug(f"Found cached frontend at: {cached_frontend}") return cached_frontend - + return None @@ -174,25 +180,23 @@ def check_node_npm() -> tuple[bool, str]: """ try: # Check Node.js - result = subprocess.run(["node", "--version"], - capture_output=True, text=True, timeout=10) + result = subprocess.run(["node", "--version"], capture_output=True, text=True, timeout=10) if result.returncode != 0: return False, "Node.js is not installed or not in PATH" - + node_version = result.stdout.strip() logger.debug(f"Found Node.js version: {node_version}") - + # Check npm - result = subprocess.run(["npm", "--version"], - capture_output=True, text=True, timeout=10) + result = subprocess.run(["npm", "--version"], capture_output=True, text=True, timeout=10) if result.returncode != 0: return False, "npm is not installed or not in PATH" - + npm_version = result.stdout.strip() logger.debug(f"Found npm version: {npm_version}") - + return True, f"Node.js {node_version}, npm {npm_version}" - + except subprocess.TimeoutExpired: return False, "Timeout checking Node.js/npm installation" except FileNotFoundError: @@ -210,25 +214,25 @@ def install_frontend_dependencies(frontend_path: Path) -> bool: if node_modules.exists(): logger.debug("Frontend dependencies already installed") return True - + logger.info("Installing frontend dependencies (this may take a few minutes)...") - + try: result = subprocess.run( ["npm", "install"], cwd=frontend_path, capture_output=True, text=True, - timeout=300 # 5 minutes timeout + timeout=300, # 5 minutes timeout ) - + if result.returncode == 0: logger.info("Frontend dependencies installed successfully") return True else: logger.error(f"Failed to install dependencies: {result.stderr}") return False - + except subprocess.TimeoutExpired: logger.error("Timeout installing frontend dependencies") return False @@ -244,16 +248,17 @@ def is_development_frontend(frontend_path: Path) -> bool: package_json_path = frontend_path / "package.json" if not package_json_path.exists(): return False - + try: import json + with open(package_json_path) as f: package_data = json.load(f) - + # Development frontend has Next.js as dependency dependencies = package_data.get("dependencies", {}) dev_dependencies = package_data.get("devDependencies", {}) - + return "next" in dependencies or "next" in dev_dependencies except Exception: return False @@ -268,27 +273,20 @@ def start_python_server(frontend_path: Path, host: str, port: int) -> Optional[s # Change to the frontend directory original_cwd = os.getcwd() os.chdir(frontend_path) - + # Use subprocess to run the server so we can return a process handle - cmd = [ - "python", "-m", "http.server", str(port), - "--bind", host - ] - + cmd = ["python", "-m", "http.server", str(port), "--bind", host] + process = subprocess.Popen( - cmd, - cwd=frontend_path, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True + cmd, cwd=frontend_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) - + # Restore original directory os.chdir(original_cwd) - + # Give it a moment to start time.sleep(2) - + # Check if process is still running if process.poll() is not None: stdout, stderr = process.communicate() @@ -296,9 +294,9 @@ def start_python_server(frontend_path: Path, host: str, port: int) -> Optional[s logger.error(f"stdout: {stdout}") logger.error(f"stderr: {stderr}") return None - + return process - + except Exception as e: logger.error(f"Failed to start Python HTTP server: {str(e)}") # Restore original directory on error @@ -315,9 +313,9 @@ def prompt_user_for_download() -> bool: Returns True if user consents, False otherwise. """ try: - print("\n" + "="*60) + print("\n" + "=" * 60) print("🎨 Cognee UI Setup Required") - print("="*60) + print("=" * 60) print("The cognee frontend is not available on your system.") print("This is required to use the web interface.") print("\nWhat will happen:") @@ -326,34 +324,39 @@ def prompt_user_for_download() -> bool: print("• Install dependencies with npm (requires Node.js)") print("• This is a one-time setup per cognee version") print("\nThe frontend will then be available offline for future use.") - + response = input("\nWould you like to download the frontend now? (y/N): ").strip().lower() - return response in ['y', 'yes'] + return response in ["y", "yes"] except (KeyboardInterrupt, EOFError): print("\nOperation cancelled by user.") return False -def start_ui(host: str = "localhost", port: int = 3000, open_browser: bool = True, auto_download: bool = False) -> Optional[subprocess.Popen]: +def start_ui( + host: str = "localhost", + port: int = 3000, + open_browser: bool = True, + auto_download: bool = False, +) -> Optional[subprocess.Popen]: """ Start the cognee frontend UI server. - + This function will: 1. Find the cognee-frontend directory (development) or download it (pip install) 2. Check if Node.js and npm are available (for development mode) 3. Install dependencies if needed (development mode) 4. Start the appropriate server 5. Optionally open the browser - + Args: host: Host to bind the server to (default: localhost) port: Port to run the server on (default: 3000) open_browser: Whether to open the browser automatically (default: True) auto_download: If True, download frontend without prompting (default: False) - + Returns: subprocess.Popen object representing the running server, or None if failed - + Example: >>> import cognee >>> server = cognee.start_ui() @@ -362,19 +365,21 @@ def start_ui(host: str = "localhost", port: int = 3000, open_browser: bool = Tru >>> server.terminate() """ logger.info("Starting cognee UI...") - + # Find frontend directory frontend_path = find_frontend_path() - + if not frontend_path: logger.info("Frontend not found locally. This is normal for pip-installed cognee.") - + # Offer to download the frontend if auto_download or prompt_user_for_download(): if download_frontend_assets(): frontend_path = find_frontend_path() if not frontend_path: - logger.error("Download succeeded but frontend still not found. This is unexpected.") + logger.error( + "Download succeeded but frontend still not found. This is unexpected." + ) return None else: logger.error("Failed to download frontend assets.") @@ -383,30 +388,30 @@ def start_ui(host: str = "localhost", port: int = 3000, open_browser: bool = Tru logger.info("Frontend download declined. UI functionality not available.") logger.info("You can still use all other cognee features without the web interface.") return None - + # Check Node.js and npm node_available, node_message = check_node_npm() if not node_available: logger.error(f"Cannot start UI: {node_message}") logger.error("Please install Node.js from https://nodejs.org/ to use the UI functionality") return None - + logger.debug(f"Environment check passed: {node_message}") - + # Install dependencies if needed if not install_frontend_dependencies(frontend_path): logger.error("Failed to install frontend dependencies") return None - + # Prepare environment variables env = os.environ.copy() - env['HOST'] = host - env['PORT'] = str(port) - + env["HOST"] = host + env["PORT"] = str(port) + # Start the development server logger.info(f"Starting frontend server at http://{host}:{port}") logger.info("This may take a moment to compile and start...") - + try: process = subprocess.Popen( ["npm", "run", "dev"], @@ -414,12 +419,12 @@ def start_ui(host: str = "localhost", port: int = 3000, open_browser: bool = Tru env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True + text=True, ) - + # Give it a moment to start up time.sleep(3) - + # Check if process is still running if process.poll() is not None: stdout, stderr = process.communicate() @@ -427,25 +432,26 @@ def start_ui(host: str = "localhost", port: int = 3000, open_browser: bool = Tru logger.error(f"stdout: {stdout}") logger.error(f"stderr: {stderr}") return None - + # Open browser if requested if open_browser: + def open_browser_delayed(): time.sleep(5) # Give Next.js time to fully start try: - webbrowser.open(f"http://{host}:{port}") # TODO: use dashboard url? + webbrowser.open(f"http://{host}:{port}") # TODO: use dashboard url? except Exception as e: logger.warning(f"Could not open browser automatically: {e}") - + browser_thread = threading.Thread(target=open_browser_delayed, daemon=True) browser_thread.start() - + logger.info("✓ Cognee UI is starting up...") logger.info(f"✓ Open your browser to: http://{host}:{port}") logger.info("✓ The UI will be available once Next.js finishes compiling") - + return process - + except Exception as e: logger.error(f"Failed to start frontend server: {str(e)}") return None @@ -454,16 +460,16 @@ def start_ui(host: str = "localhost", port: int = 3000, open_browser: bool = Tru def stop_ui(process: subprocess.Popen) -> bool: """ Stop a running UI server process. - + Args: process: The subprocess.Popen object returned by start_ui() - + Returns: bool: True if stopped successfully, False otherwise """ if not process: return False - + try: process.terminate() try: @@ -472,10 +478,10 @@ def stop_ui(process: subprocess.Popen) -> bool: logger.warning("Process didn't terminate gracefully, forcing kill") process.kill() process.wait() - + logger.info("UI server stopped") return True - + except Exception as e: logger.error(f"Error stopping UI server: {str(e)}") return False diff --git a/examples/start_ui_example.py b/examples/start_ui_example.py index b01c9f150..55796727b 100644 --- a/examples/start_ui_example.py +++ b/examples/start_ui_example.py @@ -13,29 +13,33 @@ import time async def main(): # First, let's add some data to cognee for the UI to display print("Adding sample data to cognee...") - await cognee.add("Natural language processing (NLP) is an interdisciplinary subfield of computer science and information retrieval.") - await cognee.add("Machine learning (ML) is a subset of artificial intelligence that focuses on algorithms and statistical models.") - + await cognee.add( + "Natural language processing (NLP) is an interdisciplinary subfield of computer science and information retrieval." + ) + await cognee.add( + "Machine learning (ML) is a subset of artificial intelligence that focuses on algorithms and statistical models." + ) + # Generate the knowledge graph print("Generating knowledge graph...") await cognee.cognify() - - print("\n" + "="*60) + + print("\n" + "=" * 60) print("Starting cognee UI...") - print("="*60) - + print("=" * 60) + # Start the UI server server = cognee.start_ui( - host="localhost", - port=3000, - open_browser=True # This will automatically open your browser + host="localhost", + port=3000, + open_browser=True, # This will automatically open your browser ) - + if server: print("UI server started successfully!") print("The interface will be available at: http://localhost:3000") print("\nPress Ctrl+C to stop the server when you're done...") - + try: # Keep the server running while server.poll() is None: # While process is still running From b3b9b1390c1448cfbf89161a9b5c8e1667db15f6 Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Thu, 11 Sep 2025 10:34:30 +0100 Subject: [PATCH 4/9] ui.py: clean up function meant to serve static frontend --- cognee/api/v1/ui/ui.py | 43 ------------------------------------------ 1 file changed, 43 deletions(-) diff --git a/cognee/api/v1/ui/ui.py b/cognee/api/v1/ui/ui.py index 36a4f4305..2e73e2eaf 100644 --- a/cognee/api/v1/ui/ui.py +++ b/cognee/api/v1/ui/ui.py @@ -264,49 +264,6 @@ def is_development_frontend(frontend_path: Path) -> bool: return False -def start_python_server(frontend_path: Path, host: str, port: int) -> Optional[subprocess.Popen]: - """ - Start a Python HTTP server to serve downloaded frontend assets. - This doesn't require Node.js and works with pre-built assets. - """ - try: - # Change to the frontend directory - original_cwd = os.getcwd() - os.chdir(frontend_path) - - # Use subprocess to run the server so we can return a process handle - cmd = ["python", "-m", "http.server", str(port), "--bind", host] - - process = subprocess.Popen( - cmd, cwd=frontend_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True - ) - - # Restore original directory - os.chdir(original_cwd) - - # Give it a moment to start - time.sleep(2) - - # Check if process is still running - if process.poll() is not None: - stdout, stderr = process.communicate() - logger.error("Python HTTP server failed to start:") - logger.error(f"stdout: {stdout}") - logger.error(f"stderr: {stderr}") - return None - - return process - - except Exception as e: - logger.error(f"Failed to start Python HTTP server: {str(e)}") - # Restore original directory on error - try: - os.chdir(original_cwd) - except OSError: - pass - return None - - def prompt_user_for_download() -> bool: """ Ask user if they want to download the frontend assets. From 26f1ca65c70080a2626e844783ac62acc6b20d21 Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Thu, 11 Sep 2025 11:57:16 +0100 Subject: [PATCH 5/9] nit: make programmatically started ui use port 3001 --- cognee/api/v1/ui/ui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cognee/api/v1/ui/ui.py b/cognee/api/v1/ui/ui.py index 2e73e2eaf..84fa088bf 100644 --- a/cognee/api/v1/ui/ui.py +++ b/cognee/api/v1/ui/ui.py @@ -291,7 +291,7 @@ def prompt_user_for_download() -> bool: def start_ui( host: str = "localhost", - port: int = 3000, + port: int = 3001, open_browser: bool = True, auto_download: bool = False, ) -> Optional[subprocess.Popen]: @@ -307,7 +307,7 @@ def start_ui( Args: host: Host to bind the server to (default: localhost) - port: Port to run the server on (default: 3000) + port: Port to run the server on (default: 3001) open_browser: Whether to open the browser automatically (default: True) auto_download: If True, download frontend without prompting (default: False) @@ -317,7 +317,7 @@ def start_ui( Example: >>> import cognee >>> server = cognee.start_ui() - >>> # UI will be available at http://localhost:3000 + >>> # UI will be available at http://localhost:3001 >>> # To stop the server later: >>> server.terminate() """ From 527db2abdf27401567e96f74ffe9d21375a9e504 Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Thu, 11 Sep 2025 15:08:53 +0100 Subject: [PATCH 6/9] cognee cli - add "-ui" flag to start ui server --- cognee/cli/_cognee.py | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/cognee/cli/_cognee.py b/cognee/cli/_cognee.py index f8ade67dd..d889cc78d 100644 --- a/cognee/cli/_cognee.py +++ b/cognee/cli/_cognee.py @@ -51,6 +51,31 @@ class DebugAction(argparse.Action): fmt.note("Debug mode enabled. Full stack traces will be shown.") +class UiAction(argparse.Action): + def __init__( + self, + option_strings: Sequence[str], + dest: Any = argparse.SUPPRESS, + default: Any = argparse.SUPPRESS, + help: str = None, + ) -> None: + super(UiAction, self).__init__( + option_strings=option_strings, dest=dest, default=default, nargs=0, help=help + ) + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str = None, + ) -> None: + # Set a flag to indicate UI should be started + global ACTION_EXECUTED + ACTION_EXECUTED = True + namespace.start_ui = True + + # Debug functionality is now in cognee.cli.debug module @@ -97,6 +122,11 @@ def _create_parser() -> tuple[argparse.ArgumentParser, Dict[str, SupportsCliComm action=DebugAction, help="Enable debug mode to show full stack traces on exceptions", ) + parser.add_argument( + "-ui", + action=UiAction, + help="Start the cognee web UI interface", + ) subparsers = parser.add_subparsers(title="Available commands", dest="command") @@ -140,6 +170,44 @@ def main() -> int: parser, installed_commands = _create_parser() args = parser.parse_args() + # Handle UI flag + if hasattr(args, 'start_ui') and args.start_ui: + try: + from cognee import start_ui + fmt.echo("Starting cognee UI...") + server = start_ui( + host="localhost", + port=3001, + open_browser=True + ) + + if server: + fmt.success("UI server started successfully!") + fmt.echo("The interface is available at: http://localhost:3001") + fmt.note("Press Ctrl+C to stop the server...") + + try: + # Keep the server running + import time + while server.poll() is None: # While process is still running + time.sleep(1) + except KeyboardInterrupt: + fmt.echo("\nStopping UI server...") + server.terminate() + server.wait() + fmt.success("UI server stopped.") + + return 0 + else: + fmt.error("Failed to start UI server. Check the logs above for details.") + return 1 + + except Exception as ex: + fmt.error(f"Error starting UI: {str(ex)}") + if debug.is_debug_enabled(): + raise ex + return 1 + if cmd := installed_commands.get(args.command): try: cmd.execute(args) From d93f31ad30fd17b0035bcdb7a6e411bf4763645d Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Thu, 11 Sep 2025 15:14:17 +0100 Subject: [PATCH 7/9] frontend auto-update for pip-based installs --- cognee/api/v1/ui/ui.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/cognee/api/v1/ui/ui.py b/cognee/api/v1/ui/ui.py index 84fa088bf..db876978e 100644 --- a/cognee/api/v1/ui/ui.py +++ b/cognee/api/v1/ui/ui.py @@ -16,10 +16,22 @@ from cognee.version import get_cognee_version logger = get_logger() +def normalize_version_for_comparison(version: str) -> str: + """ + Normalize version string for comparison. + Handles development versions and edge cases. + """ + # Remove common development suffixes for comparison + normalized = version.replace("-local", "").replace("-dev", "").replace("-alpha", "").replace("-beta", "") + return normalized.strip() + + def get_frontend_cache_dir() -> Path: """ Get the directory where downloaded frontend assets are cached. Uses user's home directory to persist across package updates. + Each cached frontend is version-specific and will be re-downloaded + when the cognee package version changes. """ cache_dir = Path.home() / ".cognee" / "ui-cache" cache_dir.mkdir(parents=True, exist_ok=True) @@ -61,16 +73,34 @@ def download_frontend_assets(force: bool = False) -> bool: try: cached_version = version_file.read_text().strip() current_version = get_cognee_version() - if cached_version == current_version: + + # Compare normalized versions to handle development versions + cached_normalized = normalize_version_for_comparison(cached_version) + current_normalized = normalize_version_for_comparison(current_version) + + if cached_normalized == current_normalized: logger.debug(f"Frontend assets already cached for version {current_version}") return True + else: + logger.info(f"Version mismatch detected: cached={cached_version}, current={current_version}") + logger.info("Updating frontend cache to match current cognee version...") + # Clear the old cached version + if frontend_dir.exists(): + shutil.rmtree(frontend_dir) + if version_file.exists(): + version_file.unlink() except Exception as e: logger.debug(f"Error checking cached version: {e}") + # Clear potentially corrupted cache + if frontend_dir.exists(): + shutil.rmtree(frontend_dir) + if version_file.exists(): + version_file.unlink() download_url, version = get_frontend_download_info() - logger.info("Downloading cognee frontend assets...") - logger.info("This is a one-time download and will be cached for future use.") + logger.info(f"Downloading cognee frontend assets for version {version}...") + logger.info("This will be cached and reused until the cognee version changes.") try: # Create a temporary directory for download @@ -118,8 +148,9 @@ def download_frontend_assets(force: bool = False) -> bool: shutil.copytree(cognee_frontend_source, frontend_dir) logger.debug(f"Frontend extracted to: {frontend_dir}") - # Write version info + # Write version info for future cache validation version_file.write_text(version) + logger.debug(f"Cached frontend for cognee version: {version}") logger.info( f"✓ Cognee frontend v{version.replace('-local', '')} downloaded and cached successfully!" From 7220052ca64c5ab28762d1144428db551d93d320 Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Thu, 11 Sep 2025 15:21:31 +0100 Subject: [PATCH 8/9] handle cli started ui closure gracefully --- cognee/api/v1/ui/ui.py | 35 ++++++++++++++++++++++++++++++++--- cognee/cli/_cognee.py | 41 ++++++++++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/cognee/api/v1/ui/ui.py b/cognee/api/v1/ui/ui.py index db876978e..6806e97ce 100644 --- a/cognee/api/v1/ui/ui.py +++ b/cognee/api/v1/ui/ui.py @@ -1,4 +1,5 @@ import os +import signal import subprocess import threading import time @@ -401,6 +402,7 @@ def start_ui( logger.info("This may take a moment to compile and start...") try: + # Use process group to ensure all child processes get terminated together process = subprocess.Popen( ["npm", "run", "dev"], cwd=frontend_path, @@ -408,6 +410,7 @@ def start_ui( stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + preexec_fn=os.setsid if hasattr(os, 'setsid') else None, # Create new process group on Unix ) # Give it a moment to start up @@ -447,7 +450,7 @@ def start_ui( def stop_ui(process: subprocess.Popen) -> bool: """ - Stop a running UI server process. + Stop a running UI server process and all its children. Args: process: The subprocess.Popen object returned by start_ui() @@ -459,12 +462,38 @@ def stop_ui(process: subprocess.Popen) -> bool: return False try: - process.terminate() + # Try to terminate the process group (includes child processes like Next.js) + if hasattr(os, 'killpg'): + try: + # Kill the entire process group + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + logger.debug("Sent SIGTERM to process group") + except (OSError, ProcessLookupError): + # Fall back to terminating just the main process + process.terminate() + logger.debug("Terminated main process only") + else: + process.terminate() + logger.debug("Terminated main process (Windows)") + try: process.wait(timeout=10) + logger.info("UI server stopped gracefully") except subprocess.TimeoutExpired: logger.warning("Process didn't terminate gracefully, forcing kill") - process.kill() + + # Force kill the process group + if hasattr(os, 'killpg'): + try: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + logger.debug("Sent SIGKILL to process group") + except (OSError, ProcessLookupError): + process.kill() + logger.debug("Force killed main process only") + else: + process.kill() + logger.debug("Force killed main process (Windows)") + process.wait() logger.info("UI server stopped") diff --git a/cognee/cli/_cognee.py b/cognee/cli/_cognee.py index d889cc78d..dab35aeb5 100644 --- a/cognee/cli/_cognee.py +++ b/cognee/cli/_cognee.py @@ -1,6 +1,8 @@ import sys import os import argparse +import signal +import subprocess from typing import Any, Sequence, Dict, Type, cast, List import click @@ -172,16 +174,43 @@ def main() -> int: # Handle UI flag if hasattr(args, 'start_ui') and args.start_ui: + server_process = None + + def signal_handler(signum, frame): + """Handle Ctrl+C and other termination signals""" + nonlocal server_process + fmt.echo("\nShutting down UI server...") + if server_process: + try: + # Try graceful termination first + server_process.terminate() + try: + server_process.wait(timeout=5) + fmt.success("UI server stopped gracefully.") + except subprocess.TimeoutExpired: + # If graceful termination fails, force kill + fmt.echo("Force stopping UI server...") + server_process.kill() + server_process.wait() + fmt.success("UI server stopped.") + except Exception as e: + fmt.warning(f"Error stopping server: {e}") + sys.exit(0) + + # Set up signal handlers + signal.signal(signal.SIGINT, signal_handler) # Ctrl+C + signal.signal(signal.SIGTERM, signal_handler) # Termination request + try: from cognee import start_ui fmt.echo("Starting cognee UI...") - server = start_ui( + server_process = start_ui( host="localhost", port=3001, open_browser=True ) - if server: + if server_process: fmt.success("UI server started successfully!") fmt.echo("The interface is available at: http://localhost:3001") fmt.note("Press Ctrl+C to stop the server...") @@ -189,13 +218,11 @@ def main() -> int: try: # Keep the server running import time - while server.poll() is None: # While process is still running + while server_process.poll() is None: # While process is still running time.sleep(1) except KeyboardInterrupt: - fmt.echo("\nStopping UI server...") - server.terminate() - server.wait() - fmt.success("UI server stopped.") + # This shouldn't happen now due to signal handler, but kept for safety + signal_handler(signal.SIGINT, None) return 0 else: From 151e5ea6ea6b53b9f587b8219ba2e773bdf8a472 Mon Sep 17 00:00:00 2001 From: Daulet Amirkhanov Date: Thu, 11 Sep 2025 15:21:41 +0100 Subject: [PATCH 9/9] ruff format --- cognee/api/v1/ui/ui.py | 26 ++++++++++++++++---------- cognee/cli/_cognee.py | 24 +++++++++++------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/cognee/api/v1/ui/ui.py b/cognee/api/v1/ui/ui.py index 6806e97ce..aae0ad5a9 100644 --- a/cognee/api/v1/ui/ui.py +++ b/cognee/api/v1/ui/ui.py @@ -23,7 +23,9 @@ def normalize_version_for_comparison(version: str) -> str: Handles development versions and edge cases. """ # Remove common development suffixes for comparison - normalized = version.replace("-local", "").replace("-dev", "").replace("-alpha", "").replace("-beta", "") + normalized = ( + version.replace("-local", "").replace("-dev", "").replace("-alpha", "").replace("-beta", "") + ) return normalized.strip() @@ -74,16 +76,18 @@ def download_frontend_assets(force: bool = False) -> bool: try: cached_version = version_file.read_text().strip() current_version = get_cognee_version() - + # Compare normalized versions to handle development versions cached_normalized = normalize_version_for_comparison(cached_version) current_normalized = normalize_version_for_comparison(current_version) - + if cached_normalized == current_normalized: logger.debug(f"Frontend assets already cached for version {current_version}") return True else: - logger.info(f"Version mismatch detected: cached={cached_version}, current={current_version}") + logger.info( + f"Version mismatch detected: cached={cached_version}, current={current_version}" + ) logger.info("Updating frontend cache to match current cognee version...") # Clear the old cached version if frontend_dir.exists(): @@ -410,7 +414,9 @@ def start_ui( stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, - preexec_fn=os.setsid if hasattr(os, 'setsid') else None, # Create new process group on Unix + preexec_fn=os.setsid + if hasattr(os, "setsid") + else None, # Create new process group on Unix ) # Give it a moment to start up @@ -463,7 +469,7 @@ def stop_ui(process: subprocess.Popen) -> bool: try: # Try to terminate the process group (includes child processes like Next.js) - if hasattr(os, 'killpg'): + if hasattr(os, "killpg"): try: # Kill the entire process group os.killpg(os.getpgid(process.pid), signal.SIGTERM) @@ -475,15 +481,15 @@ def stop_ui(process: subprocess.Popen) -> bool: else: process.terminate() logger.debug("Terminated main process (Windows)") - + try: process.wait(timeout=10) logger.info("UI server stopped gracefully") except subprocess.TimeoutExpired: logger.warning("Process didn't terminate gracefully, forcing kill") - + # Force kill the process group - if hasattr(os, 'killpg'): + if hasattr(os, "killpg"): try: os.killpg(os.getpgid(process.pid), signal.SIGKILL) logger.debug("Sent SIGKILL to process group") @@ -493,7 +499,7 @@ def stop_ui(process: subprocess.Popen) -> bool: else: process.kill() logger.debug("Force killed main process (Windows)") - + process.wait() logger.info("UI server stopped") diff --git a/cognee/cli/_cognee.py b/cognee/cli/_cognee.py index dab35aeb5..1c9406143 100644 --- a/cognee/cli/_cognee.py +++ b/cognee/cli/_cognee.py @@ -173,9 +173,9 @@ def main() -> int: args = parser.parse_args() # Handle UI flag - if hasattr(args, 'start_ui') and args.start_ui: + if hasattr(args, "start_ui") and args.start_ui: server_process = None - + def signal_handler(signum, frame): """Handle Ctrl+C and other termination signals""" nonlocal server_process @@ -196,39 +196,37 @@ def main() -> int: except Exception as e: fmt.warning(f"Error stopping server: {e}") sys.exit(0) - + # Set up signal handlers signal.signal(signal.SIGINT, signal_handler) # Ctrl+C signal.signal(signal.SIGTERM, signal_handler) # Termination request - + try: from cognee import start_ui + fmt.echo("Starting cognee UI...") - server_process = start_ui( - host="localhost", - port=3001, - open_browser=True - ) - + server_process = start_ui(host="localhost", port=3001, open_browser=True) + if server_process: fmt.success("UI server started successfully!") fmt.echo("The interface is available at: http://localhost:3001") fmt.note("Press Ctrl+C to stop the server...") - + try: # Keep the server running import time + while server_process.poll() is None: # While process is still running time.sleep(1) except KeyboardInterrupt: # This shouldn't happen now due to signal handler, but kept for safety signal_handler(signal.SIGINT, None) - + return 0 else: fmt.error("Failed to start UI server. Check the logs above for details.") return 1 - + except Exception as ex: fmt.error(f"Error starting UI: {str(ex)}") if debug.is_debug_enabled():