218 lines
7.9 KiB
Python
218 lines
7.9 KiB
Python
#!/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()
|