#!/usr/bin/env python3 """ Unified Single-Command Project Launcher for Brand Catalog Product LLM & RAG Intelligence Engine. Usage: python run_project.py What this script automates: 1. Checks & launches PostgreSQL via Docker Compose (`docker compose up -d`). 2. Starts FastAPI backend server on http://localhost:8000 (with background auto-seeding). 3. Waits until backend port 8000 is live & ready to receive connections. 4. Starts Vite frontend dev server on http://localhost:5173. 5. Keeps both servers running concurrently with instant hot-reloading & single Ctrl+C exit. """ import os import sys import time import subprocess import signal import shutil import urllib.request from pathlib import Path if sys.platform == "win32": try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") sys.stderr.reconfigure(encoding="utf-8", errors="replace") except Exception: pass ROOT_DIR = Path(__file__).resolve().parent BACKEND_DIR = ROOT_DIR / "backend" FRONTEND_DIR = ROOT_DIR / "frontend" VENV_PYTHON = BACKEND_DIR / "venv" / "Scripts" / "python.exe" if not VENV_PYTHON.exists(): VENV_PYTHON = Path(sys.executable) def print_banner(): print("\033[96m" + "=" * 70) print(" šŸš€ BRAND CATALOG PRODUCT LLM & RAG INTELLIGENCE ENGINE") print("=" * 70 + "\033[0m") def start_docker(): print("\nšŸ“¦ Checking PostgreSQL Docker container...") compose_file = ROOT_DIR / "docker-compose.yml" if not compose_file.exists(): compose_file = BACKEND_DIR / "docker-compose.yml" if not shutil.which("docker"): print("ā„¹ļø Docker CLI not found. Skipping container management.") return # 1. Check if container is already running try: check_running = subprocess.run( ["docker", "ps", "--filter", "name=catalog_rag_postgres", "--format", "{{.Names}}"], capture_output=True, text=True ) if "catalog_rag_postgres" in check_running.stdout: print("āœ… PostgreSQL container 'catalog_rag_postgres' is running on port 5432 (pgvector ready).") return except Exception: pass # 2. Check if stopped container exists and start it try: check_all = subprocess.run( ["docker", "ps", "-a", "--filter", "name=catalog_rag_postgres", "--format", "{{.Names}}"], capture_output=True, text=True ) if "catalog_rag_postgres" in check_all.stdout: print("šŸ”„ Starting existing 'catalog_rag_postgres' container...") start_res = subprocess.run(["docker", "start", "catalog_rag_postgres"], capture_output=True, text=True) if start_res.returncode == 0: print("āœ… PostgreSQL container started on port 5432 (pgvector ready).") return except Exception: pass # 3. Fallback to Docker Compose if compose_file.exists(): try: res = subprocess.run( ["docker", "compose", "-f", str(compose_file), "up", "-d", "--remove-orphans"], capture_output=True, text=True ) if res.returncode == 0: print("āœ… PostgreSQL container is running on port 5432 (pgvector ready).") else: stderr_msg = res.stderr.strip() if "Conflict" in stderr_msg or "already in use" in stderr_msg: print("āš ļø Container name conflict detected. Automatically resetting container...") subprocess.run(["docker", "rm", "-f", "catalog_rag_postgres"], capture_output=True, text=True) retry_res = subprocess.run( ["docker", "compose", "-f", str(compose_file), "up", "-d"], capture_output=True, text=True ) if retry_res.returncode == 0: print("āœ… PostgreSQL container successfully recreated on port 5432 (pgvector ready).") return print("āš ļø Docker notice:", stderr_msg or "Ensure Docker Desktop is running if using Docker Postgres.") except Exception as e: print(f"āš ļø Docker check skipped: {e}") def start_ollama(): print("\nšŸ¦™ Checking Ollama LLM service...") try: req = urllib.request.urlopen("http://localhost:11434/api/tags", timeout=2) if req.getcode() == 200: print("āœ… Ollama LLM server is running on http://localhost:11434.") return None except Exception: pass if not shutil.which("ollama"): print("ā„¹ļø Ollama CLI not installed/found. Backend will use catalog-grounded fallback responses.") return None try: p = subprocess.Popen(["ollama", "serve"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print("šŸš€ Automatically launched Ollama LLM server background process.") return p except Exception as e: print(f"āš ļø Ollama auto-launch note: {e} (App will use catalog grounded fallback).") return None def wait_for_backend_ready(): print("ā³ Waiting for backend API to initialize on port 8000...") for _ in range(30): try: req = urllib.request.urlopen("http://127.0.0.1:8000/api/system/status", timeout=1) if req.getcode() == 200: print("āœ… Backend API is live and accepting connections on port 8000.") return True except Exception: time.sleep(0.5) return False def main(): print_banner() start_docker() p_ollama = start_ollama() processes = [] if p_ollama: processes.append(p_ollama) try: # 1. Start Backend FastAPI Uvicorn print("\n⚔ Starting Backend FastAPI Server (http://localhost:8000)...") backend_cmd = [ str(VENV_PYTHON), "-m", "uvicorn", "app.main:app", "--reload", "--host", "0.0.0.0", "--port", "8000" ] env_backend = os.environ.copy() env_backend["PYTHONPATH"] = str(BACKEND_DIR) env_backend["PYTHONIOENCODING"] = "utf-8" p_backend = subprocess.Popen( backend_cmd, cwd=str(BACKEND_DIR), env=env_backend ) processes.append(p_backend) # 2. Wait until Backend is ready before starting Vite frontend wait_for_backend_ready() # 3. Start Frontend Vite Dev Server print("šŸŽØ Starting Frontend Vite Server (http://localhost:5173)...") npm_cmd = "npm.cmd" if sys.platform == "win32" else "npm" p_frontend = subprocess.Popen( [npm_cmd, "run", "dev"], cwd=str(FRONTEND_DIR), shell=(sys.platform == "win32") ) processes.append(p_frontend) print("\n\033[92m" + "=" * 70) print(" šŸŽ‰ APPLICATION IS LIVE AND READY!") print("=" * 70 + "\033[0m") print(" šŸ‘‰ Web App UI: \033[94mhttp://localhost:5173\033[0m") print(" šŸ‘‰ Backend API: \033[94mhttp://localhost:8000\033[0m") print(" šŸ‘‰ API Docs: \033[94mhttp://localhost:8000/docs\033[0m") print(" šŸ‘‰ System Status: \033[94mhttp://localhost:8000/api/system/status\033[0m") print("=" * 70) print(" (Press Ctrl+C to stop all servers gracefully)\n") # Monitor core processes (backend & frontend) core_processes = [p_backend, p_frontend] while True: time.sleep(1) for p in core_processes: if p.poll() is not None: print(f"āš ļø Core process PID {p.pid} exited with code {p.returncode}") return except KeyboardInterrupt: print("\nšŸ›‘ Stopping all services...") finally: for p in processes: try: p.terminate() p.wait(timeout=3) except Exception: try: p.kill() except Exception: pass print("šŸ‘‹ Goodbye! All processes terminated.") if __name__ == "__main__": main()