| | |
| | """ |
| | 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 |
| |
|
| | |
| | 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: |
| | |
| | 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", [])] |
| |
|
| | |
| | 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(): |
| | |
| | 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") |
| |
|
| | |
| | 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) |
| |
|
| | |
| | base_url = settings.openai_base_url or "(OpenAI default)" |
| | table.add_row("Base URL", base_url, "[green]✓[/green]") |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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: |
| | from qdrant_client import QdrantClient |
| |
|
| | client = QdrantClient(path=str(qdrant_path)) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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") |
| |
|
| | |
| | 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") |
| |
|
| | |
| | if settings.llm_provider == LLMProvider.OLLAMA: |
| | llm_check = check_ollama(settings) |
| | else: |
| | llm_check = check_openai_config(settings) |
| |
|
| | |
| | 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 |
| | """ |
| | |
| | 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() |
| |
|
| | |
| | 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_access_urls(host, port) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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() |
| |
|
| | |
| | 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(host=args.host, port=args.port, reload=args.reload) |
| |
|
| |
|
| | if __name__ == "__main__": |
| | main() |
| |
|