+ {/* Right panel - Summary (TODO), Technical details, */}
+ {chunks.length > 0 && (
+
+
+
Technical details
+
+
+
-
+ Total chunks
+
+ -
+ {chunks.length}
+
+
+
+
-
+ Avg length
+
+ -
+ {averageChunkLength.toFixed(0)} chars
+
+
+ {/* TODO: Uncomment after data is available */}
+ {/*
- Process time
-
@@ -276,54 +270,55 @@ function ChunksPageContent() {
-
*/}
-
-
-
-
- Original document
-
-
- {/*
+
+
+
+
+ Original document
+
+
+ {/*
- Name
-
{fileData?.filename}
*/}
-
-
- Type
- -
- {fileData ? getFileTypeLabel(fileData.mimetype) : "Unknown"}
-
-
-
-
- Size
- -
- {fileData?.size
- ? `${Math.round(fileData.size / 1024)} KB`
- : "Unknown"}
-
-
- {/*
+
+
- Type
+ -
+ {fileData ? getFileTypeLabel(fileData.mimetype) : "Unknown"}
+
+
+
+
- Size
+ -
+ {fileData?.size
+ ? `${Math.round(fileData.size / 1024)} KB`
+ : "Unknown"}
+
+
+ {/*
- Uploaded
-
N/A
*/}
- {/* TODO: Uncomment after data is available */}
- {/*
+ {/* TODO: Uncomment after data is available */}
+ {/*
- Source
*/}
- {/*
+ {/*
- Updated
-
N/A
*/}
-
+
+
-
- )}
+ )}
+
);
}
diff --git a/pyproject.toml b/pyproject.toml
index cbdd7be4..0b3c447f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,10 @@
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
[project]
name = "openrag"
-version = "0.1.15"
+version = "0.1.18"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
diff --git a/src/tui/_assets/docker-compose-cpu.yml b/src/tui/_assets/docker-compose-cpu.yml
deleted file mode 100644
index 4a1125f8..00000000
--- a/src/tui/_assets/docker-compose-cpu.yml
+++ /dev/null
@@ -1,122 +0,0 @@
-services:
- opensearch:
- image: phact/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 "
- # 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"
-
- 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: phact/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_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}
- - NVIDIA_DRIVER_CAPABILITIES=compute,utility
- - NVIDIA_VISIBLE_DEVICES=all
- - 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:
- - ./documents:/app/documents:Z
- - ./keys:/app/keys:Z
- - ./flows:/app/flows:Z
-
- openrag-frontend:
- image: phact/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:Z
- image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest}
- container_name: langflow
- ports:
- - "7860:7860"
- environment:
- - OPENAI_API_KEY=${OPENAI_API_KEY}
- - 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
- - LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,FILENAME,MIMETYPE,FILESIZE
- - 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/src/tui/_assets/docker-compose-cpu.yml b/src/tui/_assets/docker-compose-cpu.yml
new file mode 120000
index 00000000..557e48b7
--- /dev/null
+++ b/src/tui/_assets/docker-compose-cpu.yml
@@ -0,0 +1 @@
+../docker-compose-cpu.yml
\ No newline at end of file
diff --git a/src/tui/_assets/docker-compose.yml b/src/tui/_assets/docker-compose.yml
deleted file mode 100644
index 6cac6506..00000000
--- a/src/tui/_assets/docker-compose.yml
+++ /dev/null
@@ -1,122 +0,0 @@
-services:
- opensearch:
- image: phact/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 "
- # 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"
-
- 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: phact/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_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}
- - NVIDIA_DRIVER_CAPABILITIES=compute,utility
- - NVIDIA_VISIBLE_DEVICES=all
- - 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:
- - ./documents:/app/documents:Z
- - ./keys:/app/keys:Z
- - ./flows:/app/flows:Z
- gpus: all
-
- openrag-frontend:
- image: phact/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:Z
- image: phact/openrag-langflow:${LANGFLOW_VERSION:-latest}
- container_name: langflow
- ports:
- - "7860:7860"
- environment:
- - OPENAI_API_KEY=${OPENAI_API_KEY}
- - 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
- - LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,FILENAME,MIMETYPE,FILESIZE
- - 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/src/tui/_assets/docker-compose.yml b/src/tui/_assets/docker-compose.yml
new file mode 120000
index 00000000..5c8318ef
--- /dev/null
+++ b/src/tui/_assets/docker-compose.yml
@@ -0,0 +1 @@
+../docker-compose.yml
\ No newline at end of file
diff --git a/src/tui/_assets/documents/2506.08231v1.pdf b/src/tui/_assets/documents/2506.08231v1.pdf
deleted file mode 100644
index 61e83265..00000000
Binary files a/src/tui/_assets/documents/2506.08231v1.pdf and /dev/null differ
diff --git a/src/tui/_assets/documents/2506.08231v1.pdf b/src/tui/_assets/documents/2506.08231v1.pdf
new file mode 120000
index 00000000..079e1ace
--- /dev/null
+++ b/src/tui/_assets/documents/2506.08231v1.pdf
@@ -0,0 +1 @@
+../../../../documents/2506.08231v1.pdf
\ No newline at end of file
diff --git a/src/tui/_assets/documents/ai-human-resources.pdf b/src/tui/_assets/documents/ai-human-resources.pdf
deleted file mode 100644
index 5e36eab4..00000000
Binary files a/src/tui/_assets/documents/ai-human-resources.pdf and /dev/null differ
diff --git a/src/tui/_assets/documents/ai-human-resources.pdf b/src/tui/_assets/documents/ai-human-resources.pdf
new file mode 120000
index 00000000..ba76acc5
--- /dev/null
+++ b/src/tui/_assets/documents/ai-human-resources.pdf
@@ -0,0 +1 @@
+../../../../documents/ai-human-resources.pdf
\ No newline at end of file
diff --git a/src/tui/_assets/documents/warmup_ocr.pdf b/src/tui/_assets/documents/warmup_ocr.pdf
deleted file mode 100644
index 8b17f8b2..00000000
Binary files a/src/tui/_assets/documents/warmup_ocr.pdf and /dev/null differ
diff --git a/src/tui/_assets/documents/warmup_ocr.pdf b/src/tui/_assets/documents/warmup_ocr.pdf
new file mode 120000
index 00000000..10a7670a
--- /dev/null
+++ b/src/tui/_assets/documents/warmup_ocr.pdf
@@ -0,0 +1 @@
+../../../../documents/warmup_ocr.pdf
\ No newline at end of file
diff --git a/src/tui/_assets/flows/components/ollama_embedding.json b/src/tui/_assets/flows/components/ollama_embedding.json
new file mode 120000
index 00000000..0e3a7516
--- /dev/null
+++ b/src/tui/_assets/flows/components/ollama_embedding.json
@@ -0,0 +1 @@
+../../../../../flows/components/ollama_embedding.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/components/ollama_llm.json b/src/tui/_assets/flows/components/ollama_llm.json
new file mode 120000
index 00000000..30c18f43
--- /dev/null
+++ b/src/tui/_assets/flows/components/ollama_llm.json
@@ -0,0 +1 @@
+../../../../../flows/components/ollama_llm.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/components/ollama_llm_text.json b/src/tui/_assets/flows/components/ollama_llm_text.json
new file mode 120000
index 00000000..1b55fd42
--- /dev/null
+++ b/src/tui/_assets/flows/components/ollama_llm_text.json
@@ -0,0 +1 @@
+../../../../../flows/components/ollama_llm_text.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/components/watsonx_embedding.json b/src/tui/_assets/flows/components/watsonx_embedding.json
new file mode 120000
index 00000000..3d349dac
--- /dev/null
+++ b/src/tui/_assets/flows/components/watsonx_embedding.json
@@ -0,0 +1 @@
+../../../../../flows/components/watsonx_embedding.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/components/watsonx_llm.json b/src/tui/_assets/flows/components/watsonx_llm.json
new file mode 120000
index 00000000..d19d7004
--- /dev/null
+++ b/src/tui/_assets/flows/components/watsonx_llm.json
@@ -0,0 +1 @@
+../../../../../flows/components/watsonx_llm.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/components/watsonx_llm_text.json b/src/tui/_assets/flows/components/watsonx_llm_text.json
new file mode 120000
index 00000000..8f760b2d
--- /dev/null
+++ b/src/tui/_assets/flows/components/watsonx_llm_text.json
@@ -0,0 +1 @@
+../../../../../flows/components/watsonx_llm_text.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/ingestion_flow.json b/src/tui/_assets/flows/ingestion_flow.json
new file mode 120000
index 00000000..6a00e536
--- /dev/null
+++ b/src/tui/_assets/flows/ingestion_flow.json
@@ -0,0 +1 @@
+../../../../flows/ingestion_flow.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/openrag_agent.json b/src/tui/_assets/flows/openrag_agent.json
new file mode 120000
index 00000000..fab81ca0
--- /dev/null
+++ b/src/tui/_assets/flows/openrag_agent.json
@@ -0,0 +1 @@
+../../../../flows/openrag_agent.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/openrag_ingest_docling.json b/src/tui/_assets/flows/openrag_ingest_docling.json
new file mode 120000
index 00000000..a23a93dc
--- /dev/null
+++ b/src/tui/_assets/flows/openrag_ingest_docling.json
@@ -0,0 +1 @@
+../../../../flows/openrag_ingest_docling.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/openrag_nudges.json b/src/tui/_assets/flows/openrag_nudges.json
new file mode 120000
index 00000000..b343ba5d
--- /dev/null
+++ b/src/tui/_assets/flows/openrag_nudges.json
@@ -0,0 +1 @@
+../../../../flows/openrag_nudges.json
\ No newline at end of file
diff --git a/src/tui/_assets/flows/openrag_url_mcp.json b/src/tui/_assets/flows/openrag_url_mcp.json
new file mode 120000
index 00000000..afb2d00e
--- /dev/null
+++ b/src/tui/_assets/flows/openrag_url_mcp.json
@@ -0,0 +1 @@
+../../../../flows/openrag_url_mcp.json
\ No newline at end of file
diff --git a/src/tui/main.py b/src/tui/main.py
index b68293fe..beee4497 100644
--- a/src/tui/main.py
+++ b/src/tui/main.py
@@ -2,6 +2,7 @@
import sys
from pathlib import Path
+from typing import Iterable, Optional
from textual.app import App, ComposeResult
from utils.logging_config import get_logger
try:
@@ -305,41 +306,103 @@ class OpenRAGTUI(App):
return True, "Runtime requirements satisfied"
-def copy_sample_documents():
+def _copy_assets(resource_tree, destination: Path, allowed_suffixes: Optional[Iterable[str]] = None, *, force: bool = False) -> None:
+ """Copy packaged assets into destination and optionally overwrite existing files.
+
+ When ``force`` is True, files are refreshed if the packaged bytes differ.
+ """
+ destination.mkdir(parents=True, exist_ok=True)
+
+ for resource in resource_tree.iterdir():
+ target_path = destination / resource.name
+
+ if resource.is_dir():
+ _copy_assets(resource, target_path, allowed_suffixes, force=force)
+ continue
+
+ if allowed_suffixes and not any(resource.name.endswith(suffix) for suffix in allowed_suffixes):
+ continue
+ resource_bytes = resource.read_bytes()
+
+ if target_path.exists():
+ if not force:
+ continue
+
+ try:
+ if target_path.read_bytes() == resource_bytes:
+ continue
+ except Exception as read_error:
+ logger.debug(f"Failed to read existing asset {target_path}: {read_error}")
+
+ target_path.write_bytes(resource_bytes)
+ logger.info(f"Copied bundled asset: {target_path}")
+
+
+def copy_sample_documents(*, force: bool = False) -> None:
"""Copy sample documents from package to current directory if they don't exist."""
documents_dir = Path("documents")
- # Check if documents directory already exists and has files
- if documents_dir.exists() and any(documents_dir.glob("*.pdf")):
- return # Documents already exist, don't overwrite
-
try:
- # Get sample documents from package assets
assets_files = files("tui._assets.documents")
-
- # Create documents directory if it doesn't exist
- documents_dir.mkdir(exist_ok=True)
-
- # Copy each sample document
- for resource in assets_files.iterdir():
- if resource.is_file() and resource.name.endswith('.pdf'):
- dest_path = documents_dir / resource.name
- if not dest_path.exists():
- content = resource.read_bytes()
- dest_path.write_bytes(content)
- logger.info(f"Copied sample document: {resource.name}")
-
+ _copy_assets(assets_files, documents_dir, allowed_suffixes=(".pdf",), force=force)
except Exception as e:
logger.debug(f"Could not copy sample documents: {e}")
# This is not a critical error - the app can work without sample documents
+def copy_sample_flows(*, force: bool = False) -> None:
+ """Copy sample flows from package to current directory if they don't exist."""
+ flows_dir = Path("flows")
+
+ try:
+ assets_files = files("tui._assets.flows")
+ _copy_assets(assets_files, flows_dir, allowed_suffixes=(".json",), force=force)
+ except Exception as e:
+ logger.debug(f"Could not copy sample flows: {e}")
+ # The app can proceed without bundled flows
+
+
+def copy_compose_files(*, force: bool = False) -> None:
+ """Copy docker-compose templates into the workspace if they are missing."""
+ try:
+ assets_root = files("tui._assets")
+ except Exception as e:
+ logger.debug(f"Could not access compose assets: {e}")
+ return
+
+ for filename in ("docker-compose.yml", "docker-compose-cpu.yml"):
+ destination = Path(filename)
+ if destination.exists() and not force:
+ continue
+
+ try:
+ resource = assets_root.joinpath(filename)
+ if not resource.is_file():
+ logger.debug(f"Compose template not found in assets: {filename}")
+ continue
+
+ resource_bytes = resource.read_bytes()
+ if destination.exists():
+ try:
+ if destination.read_bytes() == resource_bytes:
+ continue
+ except Exception as read_error:
+ logger.debug(f"Failed to read existing compose file {destination}: {read_error}")
+
+ destination.write_bytes(resource_bytes)
+ logger.info(f"Copied docker-compose template: {filename}")
+ except Exception as error:
+ logger.debug(f"Could not copy compose file {filename}: {error}")
+
+
def run_tui():
"""Run the OpenRAG TUI application."""
app = None
try:
- # Copy sample documents on first run
- copy_sample_documents()
+ # Keep bundled assets aligned with the packaged versions
+ copy_sample_documents(force=True)
+ copy_sample_flows(force=True)
+ copy_compose_files(force=True)
app = OpenRAGTUI()
app.run()
diff --git a/src/utils/container_utils.py b/src/utils/container_utils.py
index 14222c84..746379e8 100644
--- a/src/utils/container_utils.py
+++ b/src/utils/container_utils.py
@@ -157,10 +157,22 @@ def guess_host_ip_for_containers(logger=None) -> str:
import logging
import re
import shutil
+ import socket
import subprocess
log = logger or logging.getLogger(__name__)
+ def can_bind_to_address(ip_addr: str) -> bool:
+ """Test if we can bind to the given IP address."""
+ try:
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind((ip_addr, 0)) # Port 0 = let OS choose a free port
+ return True
+ except (OSError, socket.error) as e:
+ log.debug("Cannot bind to %s: %s", ip_addr, e)
+ return False
+
def run(cmd, timeout=2, text=True):
return subprocess.run(cmd, capture_output=True, text=text, timeout=timeout)
@@ -261,10 +273,23 @@ def guess_host_ip_for_containers(logger=None) -> str:
"Container-reachable host IP candidates: %s",
", ".join(ordered_candidates),
)
- else:
- log.info("Container-reachable host IP: %s", ordered_candidates[0])
- return ordered_candidates[0]
+ # Try each candidate and return the first one we can bind to
+ for ip_addr in ordered_candidates:
+ if can_bind_to_address(ip_addr):
+ if len(ordered_candidates) > 1:
+ log.info("Selected bindable host IP: %s", ip_addr)
+ else:
+ log.info("Container-reachable host IP: %s", ip_addr)
+ return ip_addr
+ log.debug("Skipping %s (cannot bind)", ip_addr)
+
+ # None of the candidates were bindable, fall back to 127.0.0.1
+ log.warning(
+ "None of the discovered IPs (%s) can be bound; falling back to 127.0.0.1",
+ ", ".join(ordered_candidates),
+ )
+ return "127.0.0.1"
log.warning(
"No container bridge IP found. For rootless Podman (slirp4netns) there may be no host bridge; publish ports or use 10.0.2.2 from the container."
diff --git a/uv.lock b/uv.lock
index 8b795659..cefa4fdf 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2352,7 +2352,7 @@ wheels = [
[[package]]
name = "openrag"
-version = "0.1.15"
+version = "0.1.18"
source = { editable = "." }
dependencies = [
{ name = "agentd" },