diff --git a/CLAUDE.md b/CLAUDE.md index ac3cab9..ebfb035 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,8 +21,23 @@ uv sync **Run the application:** +Development mode (with auto-reload): ```bash -uv run python main.py +uv run run_dev.py +``` + +Production mode (with Gunicorn WSGI server): +```bash +# First install production dependencies +uv sync --extra prod + +# Then run in production mode +uv run run_prod.py +``` + +Legacy mode (basic Dash server): +```bash +uv run main.py ``` The app will be available at http://127.0.0.1:8050 diff --git a/Dockerfile b/Dockerfile index 002130c..f968012 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,11 +21,13 @@ COPY pyproject.toml uv.lock ./ # Copy source code (needed for editable install) COPY src/ src/ COPY main.py . +COPY wsgi.py . +COPY run_prod.py . COPY assets/ assets/ -# Create virtual environment and install dependencies +# Create virtual environment and install dependencies (including production extras) RUN uv venv .venv -RUN uv sync --frozen +RUN uv sync --frozen --extra prod # Stage 2: Runtime FROM python:3.11-slim as runtime @@ -45,6 +47,8 @@ COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app/src /app/src COPY --from=builder /app/main.py /app/main.py COPY --from=builder /app/assets /app/assets +COPY --from=builder /app/wsgi.py /app/wsgi.py +COPY --from=builder /app/run_prod.py /app/run_prod.py # Make sure the virtual environment is in PATH ENV PATH="/app/.venv/bin:$PATH" @@ -55,7 +59,8 @@ ENV PYTHONPATH="/app/src:$PYTHONPATH" # Environment variables for production ENV EMBEDDINGBUDDY_HOST=0.0.0.0 ENV EMBEDDINGBUDDY_PORT=8050 -ENV EMBEDDINGBUDDY_DEBUG=False +ENV EMBEDDINGBUDDY_DEBUG=false +ENV EMBEDDINGBUDDY_ENV=production # Expose port EXPOSE 8050 @@ -64,5 +69,5 @@ EXPOSE 8050 HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD python -c "import requests; requests.get('http://localhost:8050/', timeout=5)" || exit 1 -# Run application -CMD ["python", "main.py"] \ No newline at end of file +# Run application with Gunicorn in production +CMD ["python", "run_prod.py"] \ No newline at end of file diff --git a/README.md b/README.md index 507961c..1e42d41 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ EmbeddingBuddy provides an intuitive web interface for analyzing high-dimensiona embedding vectors by applying various dimensionality reduction algorithms and visualizing the results in interactive 2D and 3D plots. The application features a clean, modular architecture that makes it easy to test, maintain, and extend -with new features. It supports dual dataset visualization, allowing you to compare +with new features. It supports dual dataset visualization, allowing you to compare documents and prompts to understand how queries relate to your content. ## Features @@ -73,17 +73,77 @@ uv sync 2. **Run the application:** +**Development mode** (with auto-reload): + ```bash -uv run python main.py +uv run run_dev.py ``` -3. **Open your browser** to http://127.0.0.1:8050 +**Production mode** (with Gunicorn WSGI server): + +```bash +# Install production dependencies +uv sync --extra prod + +# Run in production mode +uv run run_prod.py +``` + +**Legacy mode** (basic Dash server): + +```bash +uv run main.py +``` + +3. **Open your browser** to 4. **Test with sample data**: - Upload `sample_data.ndjson` (documents) - Upload `sample_prompts.ndjson` (prompts) to see dual visualization - Use the "Show prompts" toggle to compare how prompts relate to documents +## Docker + +You can also run EmbeddingBuddy using Docker: + +### Basic Usage + +```bash +# Run in the background +docker compose up -d +``` + +The application will be available at + +### With OpenSearch + +To run with OpenSearch for enhanced search capabilities: + +```bash +# Run in the background with OpenSearch +docker compose --profile opensearch up -d +``` + +This will start both the EmbeddingBuddy application and an OpenSearch instance. +OpenSearch will be available at + +### Docker Commands + +```bash +# Stop all services +docker compose down + +# Stop and remove volumes +docker compose down -v + +# View logs +docker compose logs embeddingbuddy +docker compose logs opensearch + +# Rebuild containers +docker compose build +``` + ## Development ### Project Structure diff --git a/docker-compose.yml b/docker-compose.yml index 2a2bee8..488be2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: opensearch: - image: opensearchproject/opensearch:2.13.0 + image: opensearchproject/opensearch:2 container_name: embeddingbuddy-opensearch profiles: - opensearch diff --git a/pyproject.toml b/pyproject.toml index d3fbf7b..cbc385c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ "scikit-learn>=1.3.2", "dash-bootstrap-components>=1.5.0", "umap-learn>=0.5.8", - "numba>=0.56.4", "openTSNE>=1.0.0", "mypy>=1.17.1", "opensearch-py>=3.0.0", @@ -32,11 +31,14 @@ security = [ "safety>=2.3.0", "pip-audit>=2.6.0", ] +prod = [ + "gunicorn>=21.2.0", +] dev = [ "embeddingbuddy[test,lint,security]", ] all = [ - "embeddingbuddy[test,lint,security]", + "embeddingbuddy[test,lint,security,prod]", ] [build-system] diff --git a/run_dev.py b/run_dev.py new file mode 100644 index 0000000..525efea --- /dev/null +++ b/run_dev.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +""" +Development runner with auto-reload enabled. +This runs the Dash development server with hot reloading. +""" +import os +from src.embeddingbuddy.app import create_app, run_app + +def main(): + """Run the application in development mode with auto-reload.""" + # Force development settings + os.environ["EMBEDDINGBUDDY_ENV"] = "development" + os.environ["EMBEDDINGBUDDY_DEBUG"] = "true" + + print("šŸš€ Starting EmbeddingBuddy in development mode...") + print("šŸ“ Auto-reload enabled - changes will trigger restart") + print("🌐 Server will be available at http://127.0.0.1:8050") + print("ā¹ļø Press Ctrl+C to stop") + + app = create_app() + + # Run with development server (includes auto-reload when debug=True) + run_app(app, debug=True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/run_prod.py b/run_prod.py new file mode 100644 index 0000000..f0b3251 --- /dev/null +++ b/run_prod.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +""" +Production runner using Gunicorn WSGI server. +This provides better performance and stability for production deployments. +""" +import os +import subprocess +import sys +from src.embeddingbuddy.config.settings import AppSettings + +def main(): + """Run the application in production mode with Gunicorn.""" + # Force production settings + os.environ["EMBEDDINGBUDDY_ENV"] = "production" + os.environ["EMBEDDINGBUDDY_DEBUG"] = "false" + + print("šŸš€ Starting EmbeddingBuddy in production mode...") + print(f"āš™ļø Workers: {AppSettings.GUNICORN_WORKERS}") + print(f"🌐 Server will be available at http://{AppSettings.GUNICORN_BIND}") + print("ā¹ļø Press Ctrl+C to stop") + + # Gunicorn command + cmd = [ + "gunicorn", + "--workers", str(AppSettings.GUNICORN_WORKERS), + "--bind", AppSettings.GUNICORN_BIND, + "--timeout", str(AppSettings.GUNICORN_TIMEOUT), + "--keepalive", str(AppSettings.GUNICORN_KEEPALIVE), + "--access-logfile", "-", + "--error-logfile", "-", + "--log-level", "info", + "wsgi:application" + ] + + try: + subprocess.run(cmd, check=True) + except KeyboardInterrupt: + print("\nšŸ›‘ Shutting down...") + sys.exit(0) + except subprocess.CalledProcessError as e: + print(f"āŒ Error running Gunicorn: {e}") + sys.exit(1) + except FileNotFoundError: + print("āŒ Gunicorn not found. Install it with: uv add gunicorn") + print("šŸ’” Or run in development mode with: python run_dev.py") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/embeddingbuddy/config/settings.py b/src/embeddingbuddy/config/settings.py index 3f60ab2..0459084 100644 --- a/src/embeddingbuddy/config/settings.py +++ b/src/embeddingbuddy/config/settings.py @@ -72,6 +72,15 @@ class AppSettings: DEBUG = os.getenv("EMBEDDINGBUDDY_DEBUG", "True").lower() == "true" HOST = os.getenv("EMBEDDINGBUDDY_HOST", "127.0.0.1") PORT = int(os.getenv("EMBEDDINGBUDDY_PORT", "8050")) + + # Environment Configuration + ENVIRONMENT = os.getenv("EMBEDDINGBUDDY_ENV", "development") # development, production + + # WSGI Server Configuration (for production) + GUNICORN_WORKERS = int(os.getenv("GUNICORN_WORKERS", "4")) + GUNICORN_BIND = os.getenv("GUNICORN_BIND", f"{HOST}:{PORT}") + GUNICORN_TIMEOUT = int(os.getenv("GUNICORN_TIMEOUT", "120")) + GUNICORN_KEEPALIVE = int(os.getenv("GUNICORN_KEEPALIVE", "5")) # OpenSearch Configuration OPENSEARCH_DEFAULT_SIZE = 100 diff --git a/uv.lock b/uv.lock index 0bff82e..c3e13b7 100644 --- a/uv.lock +++ b/uv.lock @@ -418,7 +418,6 @@ dependencies = [ { name = "dash" }, { name = "dash-bootstrap-components" }, { name = "mypy" }, - { name = "numba" }, { name = "numpy" }, { name = "opensearch-py" }, { name = "opentsne" }, @@ -431,6 +430,7 @@ dependencies = [ [package.optional-dependencies] all = [ { name = "bandit" }, + { name = "gunicorn" }, { name = "mypy" }, { name = "pip-audit" }, { name = "pytest" }, @@ -451,6 +451,9 @@ lint = [ { name = "mypy" }, { name = "ruff" }, ] +prod = [ + { name = "gunicorn" }, +] security = [ { name = "bandit" }, { name = "pip-audit" }, @@ -466,11 +469,11 @@ requires-dist = [ { name = "bandit", extras = ["toml"], marker = "extra == 'security'", specifier = ">=1.7.5" }, { name = "dash", specifier = ">=2.17.1" }, { name = "dash-bootstrap-components", specifier = ">=1.5.0" }, - { name = "embeddingbuddy", extras = ["test", "lint", "security"], marker = "extra == 'all'" }, { name = "embeddingbuddy", extras = ["test", "lint", "security"], marker = "extra == 'dev'" }, + { name = "embeddingbuddy", extras = ["test", "lint", "security", "prod"], marker = "extra == 'all'" }, + { name = "gunicorn", marker = "extra == 'prod'", specifier = ">=21.2.0" }, { name = "mypy", specifier = ">=1.17.1" }, { name = "mypy", marker = "extra == 'lint'", specifier = ">=1.5.0" }, - { name = "numba", specifier = ">=0.56.4" }, { name = "numpy", specifier = ">=1.24.4" }, { name = "opensearch-py", specifier = ">=3.0.0" }, { name = "opentsne", specifier = ">=1.0.0" }, @@ -484,7 +487,7 @@ requires-dist = [ { name = "scikit-learn", specifier = ">=1.3.2" }, { name = "umap-learn", specifier = ">=0.5.8" }, ] -provides-extras = ["test", "lint", "security", "dev", "all"] +provides-extras = ["test", "lint", "security", "prod", "dev", "all"] [[package]] name = "events" @@ -520,6 +523,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" }, ] +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + [[package]] name = "h11" version = "0.16.0" diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..0c26210 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,20 @@ +""" +WSGI entry point for production deployment. +Use this with a production WSGI server like Gunicorn. +""" +from src.embeddingbuddy.app import create_app + +# Create the application instance +application = create_app() + +# For compatibility with different WSGI servers +app = application + +if __name__ == "__main__": + # This won't be used in production, but useful for testing + from src.embeddingbuddy.config.settings import AppSettings + application.run( + host=AppSettings.HOST, + port=AppSettings.PORT, + debug=False + ) \ No newline at end of file