From 0ea86871d72fa3f8c22ea404ffdcb147acec4f38 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Mon, 8 Dec 2025 15:54:10 -0500 Subject: [PATCH 1/6] Add image pruning options to monitor screen Introduces two async methods to ContainerManager for pruning OpenRAG images: one for removing old/unused images and another for aggressively removing all images after stopping services. Adds a modal dialog for users to select prune options, integrates the prune workflow into the monitor screen, and updates UI controls to include a 'Prune Images' button. --- src/tui/managers/container_manager.py | 276 +++++++++++++++++++++++++ src/tui/screens/monitor.py | 40 +++- src/tui/widgets/prune_options_modal.py | 106 ++++++++++ 3 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 src/tui/widgets/prune_options_modal.py diff --git a/src/tui/managers/container_manager.py b/src/tui/managers/container_manager.py index 666d66bb..75d16ed0 100644 --- a/src/tui/managers/container_manager.py +++ b/src/tui/managers/container_manager.py @@ -1208,3 +1208,279 @@ class ContainerManager: self.platform_detector.check_podman_macos_memory() ) return is_sufficient, message + + async def prune_old_images(self) -> AsyncIterator[tuple[bool, str]]: + """Prune old OpenRAG images and dependencies, keeping only the latest versions. + + This method: + 1. Lists all images + 2. Identifies OpenRAG-related images (openrag-backend, openrag-frontend, langflow, opensearch, dashboards) + 3. For each repository, keeps only the latest/currently used image + 4. Removes old images + 5. Prunes dangling images + + Yields: + Tuples of (success, message) for progress updates + """ + if not self.is_available(): + yield False, "No container runtime available" + return + + yield False, "Scanning for OpenRAG images..." + + # Get list of all images + success, stdout, stderr = await self._run_runtime_command( + ["images", "--format", "{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}"] + ) + + if not success: + yield False, f"Failed to list images: {stderr}" + return + + # Parse images and group by repository + openrag_repos = { + "langflowai/openrag-backend", + "langflowai/openrag-frontend", + "langflowai/openrag-langflow", + "langflowai/openrag-opensearch", + "langflowai/openrag-dashboards", + "langflow/langflow", # Also include base langflow images + "opensearchproject/opensearch", + "opensearchproject/opensearch-dashboards", + } + + images_by_repo = {} + for line in stdout.strip().split("\n"): + if not line.strip(): + continue + + parts = line.split("\t") + if len(parts) < 3: + continue + + image_tag, image_id, created_at = parts[0], parts[1], parts[2] + + # Skip tags (dangling images will be handled separately) + if "" in image_tag: + continue + + # Extract repository name (without tag) + if ":" in image_tag: + repo = image_tag.rsplit(":", 1)[0] + else: + repo = image_tag + + # Check if this is an OpenRAG-related image + if any(openrag_repo in repo for openrag_repo in openrag_repos): + if repo not in images_by_repo: + images_by_repo[repo] = [] + images_by_repo[repo].append({ + "full_tag": image_tag, + "id": image_id, + "created": created_at, + }) + + if not images_by_repo: + yield True, "No OpenRAG images found to prune" + # Still run dangling image cleanup + yield False, "Cleaning up dangling images..." + success, stdout, stderr = await self._run_runtime_command( + ["image", "prune", "-f"] + ) + if success: + yield True, "Dangling images cleaned up" + else: + yield False, f"Failed to prune dangling images: {stderr}" + return + + # Get currently used images (from running/stopped containers) + services = await self.get_service_status(force_refresh=True) + current_images = set() + for service_info in services.values(): + if service_info.image and service_info.image != "N/A": + current_images.add(service_info.image) + + yield False, f"Found {len(images_by_repo)} OpenRAG image repositories" + + # For each repository, remove old images (keep latest and currently used) + total_removed = 0 + for repo, images in images_by_repo.items(): + if len(images) <= 1: + # Only one image for this repo, skip + continue + + # Sort by creation date (newest first) + # Note: This is a simple string comparison which works for ISO dates + images.sort(key=lambda x: x["created"], reverse=True) + + # Keep the newest image and any currently used images + images_to_remove = [] + for i, img in enumerate(images): + # Keep the first (newest) image + if i == 0: + continue + # Keep currently used images + if img["full_tag"] in current_images: + continue + # Mark for removal + images_to_remove.append(img) + + if not images_to_remove: + yield False, f"No old images to remove for {repo}" + continue + + # Remove old images + for img in images_to_remove: + yield False, f"Removing old image: {img['full_tag']}" + success, stdout, stderr = await self._run_runtime_command( + ["rmi", img["id"]] + ) + if success: + total_removed += 1 + yield False, f" โœ“ Removed {img['full_tag']}" + else: + # Don't fail the whole operation if one image fails + # (might be in use by another container) + yield False, f" โš  Could not remove {img['full_tag']}: {stderr.strip()}" + + if total_removed > 0: + yield True, f"Removed {total_removed} old image(s)" + else: + yield True, "No old images were removed" + + # Clean up dangling images (untagged images) + yield False, "Cleaning up dangling images..." + success, stdout, stderr = await self._run_runtime_command( + ["image", "prune", "-f"] + ) + + if success: + # Parse output to see if anything was removed + if stdout.strip(): + yield True, f"Dangling images cleaned: {stdout.strip()}" + else: + yield True, "No dangling images to clean" + else: + yield False, f"Failed to prune dangling images: {stderr}" + + yield True, "Image pruning completed" + + async def prune_all_images(self) -> AsyncIterator[tuple[bool, str]]: + """Stop services and prune ALL OpenRAG images and dependencies. + + This is a more aggressive pruning that: + 1. Stops all running services + 2. Removes ALL OpenRAG-related images (not just old versions) + 3. Prunes dangling images + + This frees up maximum disk space but requires re-downloading images on next start. + + Yields: + Tuples of (success, message) for progress updates + """ + if not self.is_available(): + yield False, "No container runtime available" + return + + # Step 1: Stop all services first + yield False, "Stopping all services..." + async for success, message in self.stop_services(): + yield success, message + if not success and "failed" in message.lower(): + yield False, "Failed to stop services, aborting prune" + return + + # Give services time to fully stop + import asyncio + await asyncio.sleep(2) + + yield False, "Scanning for OpenRAG images..." + + # Get list of all images + success, stdout, stderr = await self._run_runtime_command( + ["images", "--format", "{{.Repository}}:{{.Tag}}\t{{.ID}}"] + ) + + if not success: + yield False, f"Failed to list images: {stderr}" + return + + # Parse images and identify ALL OpenRAG-related images + openrag_repos = { + "langflowai/openrag-backend", + "langflowai/openrag-frontend", + "langflowai/openrag-langflow", + "langflowai/openrag-opensearch", + "langflowai/openrag-dashboards", + "langflow/langflow", + "opensearchproject/opensearch", + "opensearchproject/opensearch-dashboards", + } + + images_to_remove = [] + for line in stdout.strip().split("\n"): + if not line.strip(): + continue + + parts = line.split("\t") + if len(parts) < 2: + continue + + image_tag, image_id = parts[0], parts[1] + + # Skip tags (will be handled by prune) + if "" in image_tag: + continue + + # Extract repository name (without tag) + if ":" in image_tag: + repo = image_tag.rsplit(":", 1)[0] + else: + repo = image_tag + + # Check if this is an OpenRAG-related image + if any(openrag_repo in repo for openrag_repo in openrag_repos): + images_to_remove.append({ + "full_tag": image_tag, + "id": image_id, + }) + + if not images_to_remove: + yield True, "No OpenRAG images found to remove" + else: + yield False, f"Found {len(images_to_remove)} OpenRAG image(s) to remove" + + # Remove all OpenRAG images + total_removed = 0 + for img in images_to_remove: + yield False, f"Removing image: {img['full_tag']}" + success, stdout, stderr = await self._run_runtime_command( + ["rmi", "-f", img["id"]] # Force remove + ) + if success: + total_removed += 1 + yield False, f" โœ“ Removed {img['full_tag']}" + else: + yield False, f" โš  Could not remove {img['full_tag']}: {stderr.strip()}" + + if total_removed > 0: + yield True, f"Removed {total_removed} OpenRAG image(s)" + else: + yield False, "No images were removed" + + # Clean up dangling images + yield False, "Cleaning up dangling images..." + success, stdout, stderr = await self._run_runtime_command( + ["image", "prune", "-f"] + ) + + if success: + if stdout.strip(): + yield True, f"Dangling images cleaned: {stdout.strip()}" + else: + yield True, "No dangling images to clean" + else: + yield False, f"Failed to prune dangling images: {stderr}" + + yield True, "All OpenRAG images removed successfully" + diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index 7c5e0203..3525e898 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -296,6 +296,8 @@ class MonitorScreen(Screen): self.run_worker(self._upgrade_services()) elif button_id.startswith("reset-btn"): self.run_worker(self._reset_services()) + elif button_id.startswith("prune-btn"): + self.run_worker(self._prune_images()) elif button_id.startswith("docling-start-btn"): self.run_worker(self._start_docling_serve()) elif button_id.startswith("docling-stop-btn"): @@ -548,6 +550,39 @@ class MonitorScreen(Screen): yield True, "Factory reset completed successfully" + async def _prune_images(self) -> None: + """Prune old OpenRAG images with progress updates.""" + self.operation_in_progress = True + try: + # Show prune options modal + from tui.widgets.prune_options_modal import PruneOptionsModal + + prune_choice = await self.app.push_screen_wait(PruneOptionsModal()) + + if prune_choice == "cancel": + self.notify("Prune cancelled", severity="information") + return + + # Choose the appropriate pruning method based on user choice + if prune_choice == "all": + # Stop services and prune all images + command_generator = self.container_manager.prune_all_images() + modal_title = "Stopping Services & Pruning All Images" + else: + # Prune only unused images (default) + command_generator = self.container_manager.prune_old_images() + modal_title = "Pruning Unused Images" + + # Show command output in modal dialog + modal = CommandOutputModal( + modal_title, + command_generator, + on_complete=None, # We'll refresh in on_screen_resume instead + ) + self.app.push_screen(modal) + finally: + self.operation_in_progress = False + def _check_flow_backups(self) -> bool: """Check if there are any flow backups in ./flows/backup directory.""" from pathlib import Path @@ -839,10 +874,13 @@ class MonitorScreen(Screen): Button("Start Services", variant="success", id=f"start-btn{suffix}") ) - # Always show upgrade and reset buttons + # Always show upgrade, prune, and reset buttons controls.mount( Button("Upgrade", variant="warning", id=f"upgrade-btn{suffix}") ) + controls.mount( + Button("Prune Images", variant="default", id=f"prune-btn{suffix}") + ) controls.mount(Button("Factory Reset", variant="error", id=f"reset-btn{suffix}")) except Exception as e: diff --git a/src/tui/widgets/prune_options_modal.py b/src/tui/widgets/prune_options_modal.py new file mode 100644 index 00000000..ef8839f4 --- /dev/null +++ b/src/tui/widgets/prune_options_modal.py @@ -0,0 +1,106 @@ +"""Prune options modal for OpenRAG TUI.""" + +from textual.app import ComposeResult +from textual.containers import Container, Horizontal +from textual.screen import ModalScreen +from textual.widgets import Button, Static, Label + + +class PruneOptionsModal(ModalScreen[str]): + """Modal dialog to choose prune options.""" + + DEFAULT_CSS = """ + PruneOptionsModal { + align: center middle; + } + + #dialog { + width: 70; + height: auto; + border: solid #3f3f46; + background: #27272a; + padding: 0; + } + + #title { + background: #ec4899; + color: #fafafa; + padding: 1 2; + text-align: center; + width: 100%; + text-style: bold; + } + + #message { + padding: 2; + color: #fafafa; + text-align: left; + } + + #button-row { + width: 100%; + height: auto; + align: center middle; + padding: 1; + margin-top: 1; + } + + #button-row Button { + margin: 0 1; + min-width: 20; + background: #27272a; + color: #fafafa; + border: round #52525b; + text-style: none; + tint: transparent 0%; + } + + #button-row Button:hover { + background: #27272a !important; + color: #fafafa !important; + border: round #52525b; + tint: transparent 0%; + text-style: none; + } + + #button-row Button:focus { + background: #27272a !important; + color: #fafafa !important; + border: round #ec4899; + tint: transparent 0%; + text-style: none; + } + """ + + def compose(self) -> ComposeResult: + """Create the modal dialog layout.""" + with Container(id="dialog"): + yield Label("๐Ÿ—‘๏ธ Prune Images", id="title") + yield Static( + "Choose how to prune OpenRAG images:\n\n" + "โ€ข Prune Unused Only\n" + " Remove old versions, keep latest and currently used images\n" + " (Services will continue running)\n\n" + "โ€ข Stop & Prune All\n" + " Stop all services and remove ALL OpenRAG images\n" + " (Frees maximum disk space, images will be re-downloaded on next start)\n\n" + "What would you like to do?", + id="message", + ) + with Horizontal(id="button-row"): + yield Button("Cancel", id="cancel-btn") + yield Button("Prune Unused Only", id="prune-unused-btn", variant="primary") + yield Button("Stop & Prune All", id="prune-all-btn", variant="warning") + + def on_mount(self) -> None: + """Focus the prune unused button by default.""" + self.query_one("#prune-unused-btn", Button).focus() + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button presses.""" + if event.button.id == "prune-unused-btn": + self.dismiss("unused") # Prune only unused images + elif event.button.id == "prune-all-btn": + self.dismiss("all") # Stop services and prune all + else: + self.dismiss("cancel") # User cancelled From 4bd321e7c0e62c4aa9572f9940c37c03a730e2e9 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Fri, 19 Dec 2025 15:10:45 -0500 Subject: [PATCH 2/6] Handle legacy TUI config directory cleanup Added support for deleting and recreating the legacy TUI config directory (~/.openrag/tui/config/) during the config reset process. This ensures old configuration files are properly removed alongside the current config directory. --- src/tui/managers/env_manager.py | 1 + src/tui/screens/monitor.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/tui/managers/env_manager.py b/src/tui/managers/env_manager.py index 7724b760..f869e257 100644 --- a/src/tui/managers/env_manager.py +++ b/src/tui/managers/env_manager.py @@ -79,6 +79,7 @@ class EnvConfig: openrag_config_path: str = "$HOME/.openrag/config" openrag_data_path: str = "$HOME/.openrag/data" # Backend data (conversations, tokens, etc.) opensearch_data_path: str = "$HOME/.openrag/data/opensearch-data" + openrag_tui_config_path_legacy: str = "$HOME/.openrag/tui/config" # Container version (linked to TUI version) openrag_version: str = "" diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index 570679d8..80e05f15 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -502,6 +502,16 @@ class MonitorScreen(Screen): # Recreate empty config directory config_path.mkdir(parents=True, exist_ok=True) + # Also delete legacy TUI config folder if it exists (~/.openrag/tui/config/) + tui_config_path = expand_path(env_manager.config.openrag_tui_config_path_legacy) + if tui_config_path.exists(): + success, msg = await self.container_manager.clear_directory_with_container(tui_config_path) + if not success: + # Fallback to regular rmtree if container method fails + shutil.rmtree(tui_config_path) + # Recreate empty config directory + tui_config_path.mkdir(parents=True, exist_ok=True) + # Delete flow backups only if user chose to (and they actually exist) if self._check_flow_backups(): if delete_backups: From b514c8c078528d6f1d0bbbf250cb4e743c18252b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 20:24:48 +0000 Subject: [PATCH 3/6] Initial plan From 17590042dbdf589b7dc5944628e3730768d245f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 20:30:50 +0000 Subject: [PATCH 4/6] Fix duplicate OPENSEARCH_PASSWORD and remove legacy docker-compose-cpu.yml - Remove duplicate OPENSEARCH_PASSWORD in docker-compose.yml line 136 - Delete legacy docker-compose-cpu.yml file - Update Makefile to use base docker-compose.yml for CPU mode - Update Makefile to use docker-compose.yml + docker-compose.gpu.yml for GPU mode - Update documentation to reflect new architecture - Remove all references to docker-compose-cpu.yml from docs Co-authored-by: phact <1313220+phact@users.noreply.github.com> --- Makefile | 22 ++--- docker-compose-cpu.yml | 143 ----------------------------- docker-compose.yml | 1 - docs/docs/get-started/docker.mdx | 13 +-- docs/docs/support/troubleshoot.mdx | 2 +- 5 files changed, 16 insertions(+), 165 deletions(-) delete mode 100644 docker-compose-cpu.yml diff --git a/Makefile b/Makefile index d49228e2..a6e791c7 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ help: # Development environments dev: @echo "๐Ÿš€ Starting OpenRAG with GPU support..." - docker compose up -d + docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d @echo "โœ… Services started!" @echo " Backend: http://localhost:8000" @echo " Frontend: http://localhost:3000" @@ -65,7 +65,7 @@ dev: dev-cpu: @echo "๐Ÿš€ Starting OpenRAG with CPU only..." - docker compose -f docker-compose-cpu.yml up -d + docker compose up -d @echo "โœ… Services started!" @echo " Backend: http://localhost:8000" @echo " Frontend: http://localhost:3000" @@ -93,7 +93,7 @@ infra: infra-cpu: @echo "๐Ÿ”ง Starting infrastructure services only..." - docker-compose -f docker-compose-cpu.yml up -d opensearch dashboards langflow + docker compose up -d opensearch dashboards langflow @echo "โœ… Infrastructure services started!" @echo " Langflow: http://localhost:7860" @echo " OpenSearch: http://localhost:9200" @@ -103,14 +103,12 @@ infra-cpu: stop: @echo "๐Ÿ›‘ Stopping all containers..." docker compose down - docker compose -f docker-compose-cpu.yml down 2>/dev/null || true restart: stop dev clean: stop @echo "๐Ÿงน Cleaning up containers and volumes..." docker compose down -v --remove-orphans - docker compose -f docker-compose-cpu.yml down -v --remove-orphans 2>/dev/null || true docker system prune -f # Local development @@ -210,13 +208,13 @@ test-ci: chmod 644 keys/public_key.pem 2>/dev/null || true; \ fi; \ echo "Cleaning up old containers and volumes..."; \ - docker compose -f docker-compose-cpu.yml down -v 2>/dev/null || true; \ + docker compose down -v 2>/dev/null || true; \ echo "Pulling latest images..."; \ - docker compose -f docker-compose-cpu.yml pull; \ + docker compose pull; \ echo "Building OpenSearch image override..."; \ docker build --no-cache -t langflowai/openrag-opensearch:latest -f Dockerfile .; \ echo "Starting infra (OpenSearch + Dashboards + Langflow + Backend + Frontend) with CPU containers"; \ - docker compose -f docker-compose-cpu.yml up -d opensearch dashboards langflow openrag-backend openrag-frontend; \ + docker compose up -d opensearch dashboards langflow openrag-backend openrag-frontend; \ echo "Starting docling-serve..."; \ DOCLING_ENDPOINT=$$(uv run python scripts/docling_ctl.py start --port 5001 | grep "Endpoint:" | awk '{print $$2}'); \ echo "Docling-serve started at $$DOCLING_ENDPOINT"; \ @@ -288,7 +286,7 @@ test-ci: echo ""; \ echo "Tearing down infra"; \ uv run python scripts/docling_ctl.py stop || true; \ - docker compose -f docker-compose-cpu.yml down -v 2>/dev/null || true; \ + docker compose down -v 2>/dev/null || true; \ exit $$TEST_RESULT # CI-friendly integration test target with local builds: builds all images, brings up infra, waits, runs tests, tears down @@ -305,14 +303,14 @@ test-ci-local: chmod 644 keys/public_key.pem 2>/dev/null || true; \ fi; \ echo "Cleaning up old containers and volumes..."; \ - docker compose -f docker-compose-cpu.yml down -v 2>/dev/null || true; \ + docker compose down -v 2>/dev/null || true; \ echo "Building all images locally..."; \ docker build -t langflowai/openrag-opensearch:latest -f Dockerfile .; \ docker build -t langflowai/openrag-backend:latest -f Dockerfile.backend .; \ docker build -t langflowai/openrag-frontend:latest -f Dockerfile.frontend .; \ docker build -t langflowai/openrag-langflow:latest -f Dockerfile.langflow .; \ echo "Starting infra (OpenSearch + Dashboards + Langflow + Backend + Frontend) with CPU containers"; \ - docker compose -f docker-compose-cpu.yml up -d opensearch dashboards langflow openrag-backend openrag-frontend; \ + docker compose up -d opensearch dashboards langflow openrag-backend openrag-frontend; \ echo "Starting docling-serve..."; \ DOCLING_ENDPOINT=$$(uv run python scripts/docling_ctl.py start --port 5001 | grep "Endpoint:" | awk '{print $$2}'); \ echo "Docling-serve started at $$DOCLING_ENDPOINT"; \ @@ -394,7 +392,7 @@ test-ci-local: fi; \ echo "Tearing down infra"; \ uv run python scripts/docling_ctl.py stop || true; \ - docker compose -f docker-compose-cpu.yml down -v 2>/dev/null || true; \ + docker compose down -v 2>/dev/null || true; \ exit $$TEST_RESULT # SDK integration tests (requires running OpenRAG instance) diff --git a/docker-compose-cpu.yml b/docker-compose-cpu.yml deleted file mode 100644 index 58e75268..00000000 --- a/docker-compose-cpu.yml +++ /dev/null @@ -1,143 +0,0 @@ -services: - opensearch: - image: langflowai/openrag-opensearch:${OPENRAG_VERSION:-latest} - #build: - # context: . - # dockerfile: Dockerfile - container_name: os - depends_on: - - openrag-backend - environment: - - discovery.type=single-node - - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_PASSWORD} - # Run security setup in background after OpenSearch starts - command: > - bash -c " - # Ensure data directory has correct permissions - sudo chown -R opensearch:opensearch /usr/share/opensearch/data || true - - # Start OpenSearch in background - /usr/share/opensearch/opensearch-docker-entrypoint.sh opensearch & - - # Wait a bit for OpenSearch to start, then apply security config - sleep 10 && /usr/share/opensearch/setup-security.sh & - - # Wait for background processes - wait - " - ports: - - "9200:9200" - - "9600:9600" - volumes: - - ${OPENSEARCH_DATA_PATH:-./opensearch-data}:/usr/share/opensearch/data:Z - - dashboards: - image: opensearchproject/opensearch-dashboards:3.0.0 - container_name: osdash - depends_on: - - opensearch - environment: - OPENSEARCH_HOSTS: '["https://opensearch:9200"]' - OPENSEARCH_USERNAME: "admin" - OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD} - ports: - - "5601:5601" - - openrag-backend: - image: langflowai/openrag-backend:${OPENRAG_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.backend - container_name: openrag-backend - depends_on: - - langflow - environment: - - OPENSEARCH_HOST=opensearch - - LANGFLOW_URL=http://langflow:7860 - - LANGFLOW_PUBLIC_URL=${LANGFLOW_PUBLIC_URL} - - LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN} - - LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY} - - LANGFLOW_SUPERUSER=${LANGFLOW_SUPERUSER} - - LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD} - - LANGFLOW_CHAT_FLOW_ID=${LANGFLOW_CHAT_FLOW_ID} - - LANGFLOW_INGEST_FLOW_ID=${LANGFLOW_INGEST_FLOW_ID} - - LANGFLOW_URL_INGEST_FLOW_ID=${LANGFLOW_URL_INGEST_FLOW_ID} - - DISABLE_INGEST_WITH_LANGFLOW=${DISABLE_INGEST_WITH_LANGFLOW:-false} - - NUDGES_FLOW_ID=${NUDGES_FLOW_ID} - - OPENSEARCH_PORT=9200 - - OPENSEARCH_USERNAME=admin - - OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - - WATSONX_API_KEY=${WATSONX_API_KEY} - - WATSONX_ENDPOINT=${WATSONX_ENDPOINT} - - WATSONX_PROJECT_ID=${WATSONX_PROJECT_ID} - - OLLAMA_ENDPOINT=${OLLAMA_ENDPOINT} - - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID} - - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET} - - MICROSOFT_GRAPH_OAUTH_CLIENT_ID=${MICROSOFT_GRAPH_OAUTH_CLIENT_ID} - - MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET=${MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET} - - WEBHOOK_BASE_URL=${WEBHOOK_BASE_URL} - - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - volumes: - - ./openrag-documents:/app/openrag-documents:Z - - ./keys:/app/keys:Z - - ./flows:/app/flows:U,z - - ./config:/app/config:Z - - openrag-frontend: - image: langflowai/openrag-frontend:${OPENRAG_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.frontend - container_name: openrag-frontend - depends_on: - - openrag-backend - environment: - - OPENRAG_BACKEND_HOST=openrag-backend - ports: - - "3000:3000" - - langflow: - volumes: - - ./flows:/app/flows:U,z - image: langflowai/openrag-langflow:${LANGFLOW_VERSION:-latest} - # build: - # context: . - # dockerfile: Dockerfile.langflow - container_name: langflow - ports: - - "7860:7860" - environment: - - LANGFLOW_DEACTIVATE_TRACING=true - - OPENAI_API_KEY=${OPENAI_API_KEY} - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - - WATSONX_API_KEY=${WATSONX_API_KEY} - - WATSONX_ENDPOINT=${WATSONX_ENDPOINT} - - WATSONX_PROJECT_ID=${WATSONX_PROJECT_ID} - - OLLAMA_BASE_URL=${OLLAMA_ENDPOINT} - - LANGFLOW_LOAD_FLOWS_PATH=/app/flows - - LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY} - - JWT=None - - OWNER=None - - OWNER_NAME=None - - OWNER_EMAIL=None - - CONNECTOR_TYPE=system - - CONNECTOR_TYPE_URL=url - - OPENRAG-QUERY-FILTER="{}" - - OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD} - - FILENAME=None - - MIMETYPE=None - - FILESIZE=0 - - SELECTED_EMBEDDING_MODEL=${SELECTED_EMBEDDING_MODEL:-} - - LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL - - LANGFLOW_LOG_LEVEL=DEBUG - - LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN} - - LANGFLOW_SUPERUSER=${LANGFLOW_SUPERUSER} - - LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD} - - LANGFLOW_NEW_USER_IS_ACTIVE=${LANGFLOW_NEW_USER_IS_ACTIVE} - - LANGFLOW_ENABLE_SUPERUSER_CLI=${LANGFLOW_ENABLE_SUPERUSER_CLI} - # - DEFAULT_FOLDER_NAME=OpenRAG - - HIDE_GETTING_STARTED_PROGRESS=true - diff --git a/docker-compose.yml b/docker-compose.yml index 3058bc71..79826b3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -133,7 +133,6 @@ services: - MIMETYPE=None - FILESIZE=0 - SELECTED_EMBEDDING_MODEL=${SELECTED_EMBEDDING_MODEL:-} - - OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD} - LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,ANTHROPIC_API_KEY,WATSONX_API_KEY,WATSONX_ENDPOINT,WATSONX_PROJECT_ID,OLLAMA_BASE_URL - LANGFLOW_LOG_LEVEL=DEBUG - LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN} diff --git a/docs/docs/get-started/docker.mdx b/docs/docs/get-started/docker.mdx index f0ac6d03..14147e0f 100644 --- a/docs/docs/get-started/docker.mdx +++ b/docs/docs/get-started/docker.mdx @@ -114,29 +114,26 @@ The following variables are required or recommended: PID: 27746 ``` -3. Deploy the OpenRAG containers locally using the appropriate Docker Compose file for your environment. -Both files deploy the same services. +3. Deploy the OpenRAG containers locally using the appropriate Docker Compose configuration for your environment. - * [`docker-compose.yml`](https://github.com/langflow-ai/openrag/blob/main/docker-compose.yml): If your host machine has an NVIDIA GPU with CUDA support and compatible NVIDIA drivers, you can use this file to deploy OpenRAG with accelerated processing. + * **CPU-only deployment** (default): If your host machine doesn't have NVIDIA GPU support, use the base `docker-compose.yml` file. ```bash title="Docker" - docker compose build docker compose up -d ``` ```bash title="Podman" - podman compose build podman compose up -d ``` - * [`docker-compose-cpu.yml`](https://github.com/langflow-ai/openrag/blob/main/docker-compose-cpu.yml): If your host machine doesn't have NVIDIA GPU support, use this file for a CPU-only OpenRAG deployment. + * **GPU-accelerated deployment**: If your host machine has an NVIDIA GPU with CUDA support and compatible NVIDIA drivers, use the base `docker-compose.yml` file with the `docker-compose.gpu.yml` override. ```bash title="Docker" - docker compose -f docker-compose-cpu.yml up -d + docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d ``` ```bash title="Podman" - podman compose -f docker-compose-cpu.yml up -d + podman compose -f docker-compose.yml -f docker-compose.gpu.yml up -d ``` 4. Wait for the OpenRAG containers to start, and then confirm that all containers are running: diff --git a/docs/docs/support/troubleshoot.mdx b/docs/docs/support/troubleshoot.mdx index b26d2fbd..24a7d71e 100644 --- a/docs/docs/support/troubleshoot.mdx +++ b/docs/docs/support/troubleshoot.mdx @@ -38,7 +38,7 @@ For more information about this variable and how this variable controls Langflow ## Container out of memory errors -Increase Docker memory allocation or use [docker-compose-cpu.yml](https://github.com/langflow-ai/openrag/blob/main/docker-compose-cpu.yml) to deploy OpenRAG. +Increase Docker memory allocation or use the CPU-only deployment (base `docker-compose.yml` without GPU override) to reduce memory usage. ## Memory issue with Podman on macOS From 6812e525101d75a12f4cfab875a523073486b58a Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Fri, 19 Dec 2025 15:35:26 -0500 Subject: [PATCH 5/6] Add refresh action to WelcomeScreen Introduces a new 'r' key binding to refresh the service state and update the welcome screen. The refresh action asynchronously checks the status of container and native services, updates OAuth config state, and refreshes the UI to ensure consistency between displayed text and button states. --- src/tui/screens/welcome.py | 65 +++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/src/tui/screens/welcome.py b/src/tui/screens/welcome.py index dc017a3a..7c85808c 100644 --- a/src/tui/screens/welcome.py +++ b/src/tui/screens/welcome.py @@ -23,6 +23,7 @@ class WelcomeScreen(Screen): BINDINGS = [ ("q", "quit", "Quit"), + ("r", "refresh", "Refresh"), ] def __init__(self): @@ -305,16 +306,10 @@ class WelcomeScreen(Screen): else: self.default_button_id = "basic-setup-btn" - # Update the welcome text - try: - welcome_widget = self.query_one("#welcome-text") - welcome_widget.update(self._create_welcome_text()) - except: - pass # Widget might not be mounted yet - - # Focus the appropriate button (the buttons are created correctly in compose, - # the issue was they weren't being updated after service operations) - self.call_after_refresh(self._focus_appropriate_button) + # Refresh the welcome text AND buttons based on the updated async state + # This ensures buttons match the actual service state (fixes issue where + # text showed "All services running" but buttons weren't updated) + await self._refresh_welcome_content() def _focus_appropriate_button(self) -> None: """Focus the appropriate button based on current state.""" @@ -341,8 +336,22 @@ class WelcomeScreen(Screen): os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID") ) - # Re-detect service state - self._detect_services_sync() + # Re-detect container services using async method for accuracy + if self.container_manager.is_available(): + services = await self.container_manager.get_service_status(force_refresh=True) + expected = set(self.container_manager.expected_services) + running_services = [ + s.name for s in services.values() if s.status == ServiceStatus.RUNNING + ] + starting_services = [ + s.name for s in services.values() if s.status == ServiceStatus.STARTING + ] + self.services_running = len(running_services) == len(expected) and len(starting_services) == 0 + else: + self.services_running = False + + # Re-detect native service state + self.docling_running = self.docling_manager.is_running() # Refresh the welcome content and buttons await self._refresh_welcome_content() @@ -397,6 +406,38 @@ class WelcomeScreen(Screen): self.app.push_screen(DiagnosticsScreen()) + def action_refresh(self) -> None: + """Refresh service state and update welcome screen.""" + self.run_worker(self._refresh_state()) + + async def _refresh_state(self) -> None: + """Async refresh of service state.""" + # Re-detect container services using async method for accuracy + if self.container_manager.is_available(): + services = await self.container_manager.get_service_status(force_refresh=True) + expected = set(self.container_manager.expected_services) + running_services = [ + s.name for s in services.values() if s.status == ServiceStatus.RUNNING + ] + starting_services = [ + s.name for s in services.values() if s.status == ServiceStatus.STARTING + ] + self.services_running = len(running_services) == len(expected) and len(starting_services) == 0 + else: + self.services_running = False + + # Re-detect native service state + self.docling_running = self.docling_manager.is_running() + + # Update OAuth config state + self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool( + os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID") + ) + + # Refresh the welcome content and buttons + await self._refresh_welcome_content() + self.notify("Refreshed", severity="information", timeout=2) + def action_start_all_services(self) -> None: """Start all services (native first, then containers).""" self.run_worker(self._start_all_services()) From 8af1059720069827c2eacf9feaeabc75e9fdf168 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:25:25 +0000 Subject: [PATCH 6/6] Swap GPU and CPU deployment sections in documentation Per review feedback, reorder deployment sections to show GPU first, CPU second. Line 132 now correctly shows CPU deployment with single command (no explicit -f flags). Co-authored-by: phact <1313220+phact@users.noreply.github.com> --- docs/docs/get-started/docker.mdx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/docs/get-started/docker.mdx b/docs/docs/get-started/docker.mdx index 14147e0f..9de7dd38 100644 --- a/docs/docs/get-started/docker.mdx +++ b/docs/docs/get-started/docker.mdx @@ -116,16 +116,6 @@ The following variables are required or recommended: 3. Deploy the OpenRAG containers locally using the appropriate Docker Compose configuration for your environment. - * **CPU-only deployment** (default): If your host machine doesn't have NVIDIA GPU support, use the base `docker-compose.yml` file. - - ```bash title="Docker" - docker compose up -d - ``` - - ```bash title="Podman" - podman compose up -d - ``` - * **GPU-accelerated deployment**: If your host machine has an NVIDIA GPU with CUDA support and compatible NVIDIA drivers, use the base `docker-compose.yml` file with the `docker-compose.gpu.yml` override. ```bash title="Docker" @@ -136,6 +126,16 @@ The following variables are required or recommended: podman compose -f docker-compose.yml -f docker-compose.gpu.yml up -d ``` + * **CPU-only deployment** (default): If your host machine doesn't have NVIDIA GPU support, use the base `docker-compose.yml` file. + + ```bash title="Docker" + docker compose up -d + ``` + + ```bash title="Podman" + podman compose up -d + ``` + 4. Wait for the OpenRAG containers to start, and then confirm that all containers are running: ```bash title="Docker"