#!/usr/bin/env python3 """ Server startup script with pre-flight checks. Usage: python scripts/run_server.py python scripts/run_server.py --port 8080 --reload python scripts/run_server.py --host 0.0.0.0 --port 8000 """ import argparse import sys import time from pathlib import Path import requests from rich.console import Console from rich.panel import Panel from rich.table import Table # Add project root to path project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) from config.settings import LLMProvider, Settings console = Console() def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser( description="Start EyeWiki RAG API server with pre-flight checks" ) parser.add_argument( "--host", type=str, default="0.0.0.0", help="Host to bind (default: 0.0.0.0)", ) parser.add_argument( "--port", type=int, default=8000, help="Port number (default: 8000)", ) parser.add_argument( "--reload", action="store_true", help="Enable hot reload for development", ) parser.add_argument( "--skip-checks", action="store_true", help="Skip pre-flight checks (not recommended)", ) return parser.parse_args() def print_header(): """Print welcome header.""" console.print() console.print( Panel.fit( "[bold blue]EyeWiki RAG API Server[/bold blue]\n" "[dim]Retrieval-Augmented Generation for Medical Knowledge[/dim]", border_style="blue", ) ) console.print() def check_ollama(settings: Settings) -> bool: """ Check if Ollama is running and has required models. Args: settings: Application settings Returns: True if check passed, False otherwise """ console.print("[bold cyan]1. Checking Ollama service...[/bold cyan]") try: # Check if Ollama is running response = requests.get(f"{settings.ollama_base_url}/api/tags", timeout=5) response.raise_for_status() models_data = response.json() available_models = [model["name"] for model in models_data.get("models", [])] # Check for required LLM model (embedding model is sentence-transformers, not Ollama) required_models = { "LLM": settings.llm_model, } table = Table(show_header=True, header_style="bold magenta") table.add_column("Model Type", style="cyan") table.add_column("Required Model", style="yellow") table.add_column("Status", style="green") all_found = True for model_type, model_name in required_models.items(): # Check if model name (with or without tag) is in available models found = any( model_name in model or model in model_name for model in available_models ) status = "[green]✓ Found[/green]" if found else "[red]✗ Missing[/red]" table.add_row(model_type, model_name, status) if not found: all_found = False console.print(table) if not all_found: console.print( "\n[red]Error:[/red] Some required models are missing. " "Pull them with:" ) for model_type, model_name in required_models.items(): if not any( model_name in model or model in model_name for model in available_models ): console.print(f" [yellow]ollama pull {model_name}[/yellow]") console.print() return False console.print("[green]✓ Ollama is running with all required models[/green]\n") return True except requests.RequestException as e: console.print(f"[red]✗ Failed to connect to Ollama:[/red] {e}") console.print( f"\nMake sure Ollama is running at [yellow]{settings.ollama_base_url}[/yellow]" ) console.print("Start it with: [yellow]ollama serve[/yellow]\n") return False def check_openai_config(settings: Settings) -> bool: """ Check if OpenAI-compatible API is configured with required API key. Args: settings: Application settings Returns: True if check passed, False otherwise """ console.print("[bold cyan]1. Checking OpenAI-compatible API configuration...[/bold cyan]") table = Table(show_header=True, header_style="bold magenta") table.add_column("Property", style="cyan") table.add_column("Value", style="yellow") table.add_column("Status", style="green") # Check API key has_key = bool(settings.openai_api_key) key_display = f"{settings.openai_api_key[:8]}..." if has_key else "(not set)" key_status = "[green]✓ Set[/green]" if has_key else "[red]✗ Missing[/red]" table.add_row("API Key", key_display, key_status) # Show base URL base_url = settings.openai_base_url or "(OpenAI default)" table.add_row("Base URL", base_url, "[green]✓[/green]") # Show model table.add_row("Model", settings.openai_model, "[green]✓[/green]") console.print(table) if not has_key: console.print( "\n[red]Error:[/red] API key is required for OpenAI-compatible provider." ) console.print( "Set the [yellow]OPENAI_API_KEY[/yellow] environment variable or add it to your [yellow].env[/yellow] file.\n" ) return False console.print("[green]✓ OpenAI-compatible API configuration looks good[/green]\n") return True def check_vector_store(settings: Settings) -> bool: """ Check if vector store exists and has documents. Args: settings: Application settings Returns: True if check passed, False otherwise """ console.print("[bold cyan]2. Checking vector store...[/bold cyan]") qdrant_path = Path(settings.qdrant_path) collection_name = settings.qdrant_collection_name # Check if Qdrant directory exists if not qdrant_path.exists(): console.print(f"[red]✗ Qdrant directory not found:[/red] {qdrant_path}") console.print( "\nRun the indexing pipeline first:\n" " [yellow]python scripts/build_index.py --index-vectors[/yellow]\n" ) return False # Try to connect to Qdrant and check collection try: from qdrant_client import QdrantClient client = QdrantClient(path=str(qdrant_path)) # Check if collection exists collections = client.get_collections().collections collection_names = [col.name for col in collections] if collection_name not in collection_names: console.print( f"[red]✗ Collection '{collection_name}' not found[/red]\n" f"Available collections: {collection_names}" ) console.print( "\nRun the indexing pipeline first:\n" " [yellow]python scripts/build_index.py --index-vectors[/yellow]\n" ) return False # Get collection info collection_info = client.get_collection(collection_name) points_count = collection_info.points_count if points_count == 0: console.print( f"[yellow]⚠ Collection '{collection_name}' exists but is empty[/yellow]" ) console.print( "\nRun the indexing pipeline:\n" " [yellow]python scripts/build_index.py --index-vectors[/yellow]\n" ) return False # Print stats table = Table(show_header=True, header_style="bold magenta") table.add_column("Property", style="cyan") table.add_column("Value", style="yellow") table.add_row("Collection", collection_name) table.add_row("Location", str(qdrant_path)) table.add_row("Documents", f"{points_count:,}") console.print(table) console.print("[green]✓ Vector store is ready[/green]\n") return True except Exception as e: console.print(f"[red]✗ Failed to access vector store:[/red] {e}\n") return False def check_required_files() -> bool: """ Check if all required files exist. Returns: True if all files exist, False otherwise """ console.print("[bold cyan]3. Checking required files...[/bold cyan]") required_files = { "System Prompt": project_root / "prompts" / "system_prompt.txt", "Query Prompt": project_root / "prompts" / "query_prompt.txt", "Medical Disclaimer": project_root / "prompts" / "medical_disclaimer.txt", } table = Table(show_header=True, header_style="bold magenta") table.add_column("File", style="cyan") table.add_column("Path", style="yellow") table.add_column("Status", style="green") all_exist = True for name, path in required_files.items(): exists = path.exists() status = "[green]✓ Found[/green]" if exists else "[red]✗ Missing[/red]" table.add_row(name, str(path.relative_to(project_root)), status) if not exists: all_exist = False console.print(table) if not all_exist: console.print( "\n[red]Error:[/red] Some required files are missing.\n" "Make sure all prompt files are in the [yellow]prompts/[/yellow] directory.\n" ) return False console.print("[green]✓ All required files found[/green]\n") return True def run_preflight_checks(skip_checks: bool = False) -> bool: """ Run all pre-flight checks. Args: skip_checks: Skip all checks if True Returns: True if all checks passed, False otherwise """ if skip_checks: console.print("[yellow]⚠ Skipping pre-flight checks[/yellow]\n") return True console.print("[bold yellow]Running Pre-flight Checks...[/bold yellow]\n") # Load settings try: settings = Settings() except Exception as e: console.print(f"[red]✗ Failed to load settings:[/red] {e}\n") return False console.print(f"[dim]LLM Provider: {settings.llm_provider.value}[/dim]\n") # Check LLM provider (Ollama or OpenAI-compatible) if settings.llm_provider == LLMProvider.OLLAMA: llm_check = check_ollama(settings) else: llm_check = check_openai_config(settings) # Run checks checks = [ llm_check, check_vector_store(settings), check_required_files(), ] if not all(checks): console.print("[bold red]✗ Pre-flight checks failed[/bold red]") console.print("Fix the issues above and try again.\n") return False console.print("[bold green]✓ All pre-flight checks passed![/bold green]\n") return True def print_access_urls(host: str, port: int): """ Print access URLs for the server. Args: host: Server host port: Server port """ # Determine display host display_host = "localhost" if host in ["0.0.0.0", "127.0.0.1"] else host table = Table( show_header=True, header_style="bold magenta", title="[bold green]Server Access URLs[/bold green]", title_style="bold green", ) table.add_column("Service", style="cyan", width=20) table.add_column("URL", style="yellow") table.add_column("Description", style="dim") urls = [ ("API Root", f"http://{display_host}:{port}", "API information"), ("Health Check", f"http://{display_host}:{port}/health", "Service health status"), ( "Interactive Docs", f"http://{display_host}:{port}/docs", "Swagger UI documentation", ), ("ReDoc", f"http://{display_host}:{port}/redoc", "Alternative API docs"), ( "Gradio UI", f"http://{display_host}:{port}/ui", "Web chat interface", ), ] for service, url, description in urls: table.add_row(service, url, description) console.print() console.print(table) console.print() # Print quick start commands console.print("[bold cyan]Quick Test Commands:[/bold cyan]") console.print( f" [dim]# Test health endpoint[/dim]\n" f" [yellow]curl http://{display_host}:{port}/health[/yellow]\n" ) console.print( f" [dim]# Query the API[/dim]\n" f" [yellow]curl -X POST http://{display_host}:{port}/query \\[/yellow]\n" f' [yellow] -H "Content-Type: application/json" \\[/yellow]\n' f' [yellow] -d \'{{"question": "What is glaucoma?"}}\' [/yellow]\n' ) def start_server(host: str, port: int, reload: bool): """ Start the uvicorn server. Args: host: Server host port: Server port reload: Enable hot reload """ console.print("[bold green]Starting server...[/bold green]\n") # Print URLs before starting print_access_urls(host, port) # Import uvicorn here to avoid import errors if not installed try: import uvicorn except ImportError: console.print("[red]Error:[/red] uvicorn is not installed") console.print("Install it with: [yellow]pip install uvicorn[/yellow]\n") sys.exit(1) # Start server try: console.print( f"[dim]Server listening on {host}:{port}[/dim]", f"[dim](Press CTRL+C to stop)[/dim]\n", ) uvicorn.run( "src.api.main:app", host=host, port=port, reload=reload, log_level="info", ) except KeyboardInterrupt: console.print("\n\n[yellow]Server stopped by user[/yellow]") except Exception as e: console.print(f"\n[red]Error starting server:[/red] {e}") sys.exit(1) def main(): """Main entry point.""" args = parse_args() print_header() # Run pre-flight checks if not run_preflight_checks(skip_checks=args.skip_checks): console.print("[red]Startup aborted due to failed checks[/red]\n") sys.exit(1) # Start server start_server(host=args.host, port=args.port, reload=args.reload) if __name__ == "__main__": main()