add container utils (#151)

* add container utils

* added localhost url to settings

* added localhost_url as a constant

* added localhost_url to get settings query

* make ollama onboarding have localhost url by default

* make endpoint be changed in models service and in onboarding backend instead of onboarding screen

* fixed embedding dimensions to get stripped model

* make config come as localhost but global variable be set as the transformed endpoint

* remove setting ollama url since it comes from the global variable

* use localhost again on ollama

---------

Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
This commit is contained in:
Mike Fortman 2025-09-30 14:34:55 -05:00 committed by GitHub
parent f54479cf48
commit d015ed1b0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 152 additions and 8 deletions

View file

@ -37,6 +37,7 @@ export interface Settings {
separator?: string;
embeddingModel?: string;
};
localhost_url?: string;
}
export const useGetSettingsQuery = (

View file

@ -19,7 +19,7 @@ export function OllamaOnboarding({
sampleDataset: boolean;
setSampleDataset: (dataset: boolean) => void;
}) {
const [endpoint, setEndpoint] = useState("http://localhost:11434");
const [endpoint, setEndpoint] = useState(`http://localhost:11434`);
const [showConnecting, setShowConnecting] = useState(false);
const debouncedEndpoint = useDebouncedValue(endpoint, 500);

View file

@ -1,6 +1,7 @@
import json
import platform
from starlette.responses import JSONResponse
from utils.container_utils import transform_localhost_url
from utils.logging_config import get_logger
from config.settings import (
LANGFLOW_URL,
@ -8,6 +9,7 @@ from config.settings import (
LANGFLOW_INGEST_FLOW_ID,
LANGFLOW_PUBLIC_URL,
DOCLING_COMPONENT_ID,
LOCALHOST_URL,
clients,
get_openrag_config,
config_manager,
@ -74,6 +76,7 @@ async def get_settings(request, session_manager):
"llm_model": agent_config.llm_model,
"system_prompt": agent_config.system_prompt,
},
"localhost_url": LOCALHOST_URL,
}
# Only expose edit URLs when a public URL is configured
@ -570,7 +573,8 @@ async def onboarding(request, flows_service):
# Set base URL for Ollama provider
if provider == "ollama" and "endpoint" in body:
endpoint = body["endpoint"]
endpoint = transform_localhost_url(body["endpoint"])
await clients._create_langflow_global_variable(
"OLLAMA_BASE_URL", endpoint, modify=True
)

View file

@ -9,6 +9,7 @@ from openai import AsyncOpenAI
from opensearchpy import AsyncOpenSearch
from opensearchpy._async.http_aiohttp import AIOHttpConnection
from utils.container_utils import get_container_host
from utils.document_processing import create_document_converter
from utils.logging_config import get_logger
@ -575,6 +576,8 @@ OLLAMA_LLM_TEXT_COMPONENT_ID = os.getenv(
# Docling component ID for ingest flow
DOCLING_COMPONENT_ID = os.getenv("DOCLING_COMPONENT_ID", "DoclingRemote-78KoX")
LOCALHOST_URL = get_container_host() or "localhost"
# Global clients instance
clients = AppClients()

View file

@ -836,10 +836,5 @@ class FlowsService:
template["url"]["value"] = endpoint
template["url"]["options"] = [endpoint]
updated = True
elif provider == "ollama" and "base_url" in template:
# Ollama uses "base_url" field
template["base_url"]["value"] = endpoint
# Note: base_url is typically a MessageTextInput, not dropdown, so no options field
updated = True
return updated

View file

@ -1,5 +1,6 @@
import httpx
from typing import Dict, List
from utils.container_utils import transform_localhost_url
from utils.logging_config import get_logger
logger = get_logger(__name__)
@ -95,7 +96,7 @@ class ModelsService:
"""Fetch available models from Ollama API with tool calling capabilities for language models"""
try:
# Use provided endpoint or default
ollama_url = endpoint
ollama_url = transform_localhost_url(endpoint)
# API endpoints
tags_url = f"{ollama_url}/api/tags"

View file

@ -0,0 +1,138 @@
"""Utilities for detecting and working with container environments."""
import os
from pathlib import Path
def detect_container_environment() -> str | None:
"""Detect if running in a container and return the appropriate container type.
Returns:
'docker' if running in Docker, 'podman' if running in Podman, None otherwise.
"""
# Check for .dockerenv file (Docker)
if Path("/.dockerenv").exists():
return "docker"
# Check cgroup for container indicators
try:
with Path("/proc/self/cgroup").open() as f:
content = f.read()
if "docker" in content:
return "docker"
if "podman" in content:
return "podman"
except (FileNotFoundError, PermissionError):
pass
# Check environment variables (lowercase 'container' is the standard for Podman)
if os.getenv("container") == "podman": # noqa: SIM112
return "podman"
return None
def get_container_host() -> str | None:
"""Get the hostname to access host services from within a container.
Tries multiple methods to find the correct hostname:
1. host.containers.internal (Podman) or host.docker.internal (Docker)
2. Gateway IP from routing table (fallback for Linux)
Returns:
The hostname or IP to use, or None if not in a container.
"""
import socket
# Check if we're in a container first
container_type = detect_container_environment()
if not container_type:
return None
# Try container-specific hostnames first based on detected type
if container_type == "podman":
# Podman: try host.containers.internal first
try:
socket.getaddrinfo("host.containers.internal", None)
except socket.gaierror:
pass
else:
return "host.containers.internal"
# Fallback to host.docker.internal (for Podman Desktop on macOS)
try:
socket.getaddrinfo("host.docker.internal", None)
except socket.gaierror:
pass
else:
return "host.docker.internal"
else:
# Docker: try host.docker.internal first
try:
socket.getaddrinfo("host.docker.internal", None)
except socket.gaierror:
pass
else:
return "host.docker.internal"
# Fallback to host.containers.internal (unlikely but possible)
try:
socket.getaddrinfo("host.containers.internal", None)
except socket.gaierror:
pass
else:
return "host.containers.internal"
# Fallback: try to get gateway IP from routing table (Linux containers)
try:
with Path("/proc/net/route").open() as f:
for line in f:
fields = line.strip().split()
min_field_count = 3 # Minimum fields needed: interface, destination, gateway
if len(fields) >= min_field_count and fields[1] == "00000000": # Default route
# Gateway is in hex format (little-endian)
gateway_hex = fields[2]
# Convert hex to IP address
# The hex is in little-endian format, so we read it backwards in pairs
octets = [gateway_hex[i : i + 2] for i in range(0, 8, 2)]
return ".".join(str(int(octet, 16)) for octet in reversed(octets))
except (FileNotFoundError, PermissionError, IndexError, ValueError):
pass
return None
def transform_localhost_url(url: str) -> str:
"""Transform localhost URLs to container-accessible hosts when running in a container.
Automatically detects if running inside a container and finds the appropriate host
address to replace localhost/127.0.0.1. Tries in order:
- host.docker.internal (if resolvable)
- host.containers.internal (if resolvable)
- Gateway IP from routing table (fallback)
Args:
url: The original URL
Returns:
Transformed URL with container-accessible host if applicable, otherwise the original URL.
Example:
>>> transform_localhost_url("http://localhost:5001")
# Returns "http://host.docker.internal:5001" if running in Docker and hostname resolves
# Returns "http://172.17.0.1:5001" if running in Docker on Linux (gateway IP fallback)
# Returns "http://localhost:5001" if not in a container
"""
container_host = get_container_host()
if not container_host:
return url
# Replace localhost and 127.0.0.1 with the container host
localhost_patterns = ["localhost", "127.0.0.1"]
for pattern in localhost_patterns:
if pattern in url:
return url.replace(pattern, container_host)
return url

View file

@ -10,6 +10,8 @@ def get_embedding_dimensions(model_name: str) -> int:
# Check all model dictionaries
all_models = {**OPENAI_EMBEDDING_DIMENSIONS, **OLLAMA_EMBEDDING_DIMENSIONS, **WATSONX_EMBEDDING_DIMENSIONS}
model_name = model_name.lower().strip().split(":")[0]
if model_name in all_models:
dimensions = all_models[model_name]
logger.info(f"Found dimensions for model '{model_name}': {dimensions}")