This commit is contained in:
Raphaël MANSUY 2025-12-04 19:14:28 +08:00
parent d266d00f3e
commit 49b0953ac1
3 changed files with 156 additions and 83 deletions

View file

@ -365,8 +365,12 @@ def parse_args() -> argparse.Namespace:
# Inject model configuration # Inject model configuration
args.llm_model = get_env_value("LLM_MODEL", "mistral-nemo:latest") args.llm_model = get_env_value("LLM_MODEL", "mistral-nemo:latest")
args.embedding_model = get_env_value("EMBEDDING_MODEL", "bge-m3:latest") # EMBEDDING_MODEL defaults to None - each binding will use its own default model
args.embedding_dim = get_env_value("EMBEDDING_DIM", 1024, int) # e.g., OpenAI uses "text-embedding-3-small", Jina uses "jina-embeddings-v4"
args.embedding_model = get_env_value("EMBEDDING_MODEL", None, special_none=True)
# EMBEDDING_DIM defaults to None - each binding will use its own default dimension
# Value is inherited from provider defaults via wrap_embedding_func_with_attrs decorator
args.embedding_dim = get_env_value("EMBEDDING_DIM", None, int, special_none=True)
args.embedding_send_dim = get_env_value("EMBEDDING_SEND_DIM", False, bool) args.embedding_send_dim = get_env_value("EMBEDDING_SEND_DIM", False, bool)
# Inject chunk configuration # Inject chunk configuration

View file

@ -56,7 +56,8 @@ from lightrag.api.routers.ollama_api import OllamaAPI
from lightrag.utils import logger, set_verbose_debug from lightrag.utils import logger, set_verbose_debug
from lightrag.kg.shared_storage import ( from lightrag.kg.shared_storage import (
get_namespace_data, get_namespace_data,
initialize_pipeline_status, get_default_workspace,
# set_default_workspace,
cleanup_keyed_lock, cleanup_keyed_lock,
finalize_share_data, finalize_share_data,
) )
@ -158,19 +159,22 @@ def check_frontend_build():
"""Check if frontend is built and optionally check if source is up-to-date """Check if frontend is built and optionally check if source is up-to-date
Returns: Returns:
bool: True if frontend is outdated, False if up-to-date or production environment tuple: (assets_exist: bool, is_outdated: bool)
- assets_exist: True if WebUI build files exist
- is_outdated: True if source is newer than build (only in dev environment)
""" """
webui_dir = Path(__file__).parent / "webui" webui_dir = Path(__file__).parent / "webui"
index_html = webui_dir / "index.html" index_html = webui_dir / "index.html"
# 1. Check if build files exist (required) # 1. Check if build files exist
if not index_html.exists(): if not index_html.exists():
ASCIIColors.red("\n" + "=" * 80) ASCIIColors.yellow("\n" + "=" * 80)
ASCIIColors.red("ERROR: Frontend Not Built") ASCIIColors.yellow("WARNING: Frontend Not Built")
ASCIIColors.red("=" * 80) ASCIIColors.yellow("=" * 80)
ASCIIColors.yellow("The WebUI frontend has not been built yet.") ASCIIColors.yellow("The WebUI frontend has not been built yet.")
ASCIIColors.yellow("The API server will start without the WebUI interface.")
ASCIIColors.yellow( ASCIIColors.yellow(
"Please build the frontend code first using the following commands:\n" "\nTo enable WebUI, build the frontend using these commands:\n"
) )
ASCIIColors.cyan(" cd lightrag_webui") ASCIIColors.cyan(" cd lightrag_webui")
ASCIIColors.cyan(" bun install --frozen-lockfile") ASCIIColors.cyan(" bun install --frozen-lockfile")
@ -180,8 +184,8 @@ def check_frontend_build():
ASCIIColors.cyan( ASCIIColors.cyan(
"Note: Make sure you have Bun installed. Visit https://bun.sh for installation." "Note: Make sure you have Bun installed. Visit https://bun.sh for installation."
) )
ASCIIColors.red("=" * 80 + "\n") ASCIIColors.yellow("=" * 80 + "\n")
sys.exit(1) # Exit immediately return (False, False) # Assets don't exist, not outdated
# 2. Check if this is a development environment (source directory exists) # 2. Check if this is a development environment (source directory exists)
try: try:
@ -194,7 +198,7 @@ def check_frontend_build():
logger.debug( logger.debug(
"Production environment detected, skipping source freshness check" "Production environment detected, skipping source freshness check"
) )
return False return (True, False) # Assets exist, not outdated (prod environment)
# Development environment, perform source code timestamp check # Development environment, perform source code timestamp check
logger.debug("Development environment detected, checking source freshness") logger.debug("Development environment detected, checking source freshness")
@ -269,20 +273,20 @@ def check_frontend_build():
ASCIIColors.cyan(" cd ..") ASCIIColors.cyan(" cd ..")
ASCIIColors.yellow("\nThe server will continue with the current build.") ASCIIColors.yellow("\nThe server will continue with the current build.")
ASCIIColors.yellow("=" * 80 + "\n") ASCIIColors.yellow("=" * 80 + "\n")
return True # Frontend is outdated return (True, True) # Assets exist, outdated
else: else:
logger.info("Frontend build is up-to-date") logger.info("Frontend build is up-to-date")
return False # Frontend is up-to-date return (True, False) # Assets exist, up-to-date
except Exception as e: except Exception as e:
# If check fails, log warning but don't affect startup # If check fails, log warning but don't affect startup
logger.warning(f"Failed to check frontend source freshness: {e}") logger.warning(f"Failed to check frontend source freshness: {e}")
return False # Assume up-to-date on error return (True, False) # Assume assets exist and up-to-date on error
def create_app(args): def create_app(args):
# Check frontend build first and get outdated status # Check frontend build first and get status
is_frontend_outdated = check_frontend_build() webui_assets_exist, is_frontend_outdated = check_frontend_build()
# Create unified API version display with warning symbol if frontend is outdated # Create unified API version display with warning symbol if frontend is outdated
api_version_display = ( api_version_display = (
@ -350,8 +354,8 @@ def create_app(args):
try: try:
# Initialize database connections # Initialize database connections
# Note: initialize_storages() now auto-initializes pipeline_status for rag.workspace
await rag.initialize_storages() await rag.initialize_storages()
await initialize_pipeline_status()
# Data migration regardless of storage implementation # Data migration regardless of storage implementation
await rag.check_and_migrate_data() await rag.check_and_migrate_data()
@ -452,7 +456,7 @@ def create_app(args):
# Create combined auth dependency for all endpoints # Create combined auth dependency for all endpoints
combined_auth = get_combined_auth_dependency(api_key) combined_auth = get_combined_auth_dependency(api_key)
def get_workspace_from_request(request: Request) -> str: def get_workspace_from_request(request: Request) -> str | None:
""" """
Extract workspace from HTTP request header or use default. Extract workspace from HTTP request header or use default.
@ -469,9 +473,8 @@ def create_app(args):
# Check custom header first # Check custom header first
workspace = request.headers.get("LIGHTRAG-WORKSPACE", "").strip() workspace = request.headers.get("LIGHTRAG-WORKSPACE", "").strip()
# Fall back to server default if header not provided
if not workspace: if not workspace:
workspace = args.workspace workspace = None
return workspace return workspace
@ -710,6 +713,7 @@ def create_app(args):
) )
# Step 3: Create optimized embedding function (calls underlying function directly) # Step 3: Create optimized embedding function (calls underlying function directly)
# Note: When model is None, each binding will use its own default model
async def optimized_embedding_function(texts, embedding_dim=None): async def optimized_embedding_function(texts, embedding_dim=None):
try: try:
if binding == "lollms": if binding == "lollms":
@ -721,9 +725,9 @@ def create_app(args):
if isinstance(lollms_embed, EmbeddingFunc) if isinstance(lollms_embed, EmbeddingFunc)
else lollms_embed else lollms_embed
) )
return await actual_func( # lollms embed_model is not used (server uses configured vectorizer)
texts, embed_model=model, host=host, api_key=api_key # Only pass base_url and api_key
) return await actual_func(texts, base_url=host, api_key=api_key)
elif binding == "ollama": elif binding == "ollama":
from lightrag.llm.ollama import ollama_embed from lightrag.llm.ollama import ollama_embed
@ -742,13 +746,16 @@ def create_app(args):
ollama_options = OllamaEmbeddingOptions.options_dict(args) ollama_options = OllamaEmbeddingOptions.options_dict(args)
return await actual_func( # Pass embed_model only if provided, let function use its default (bge-m3:latest)
texts, kwargs = {
embed_model=model, "texts": texts,
host=host, "host": host,
api_key=api_key, "api_key": api_key,
options=ollama_options, "options": ollama_options,
) }
if model:
kwargs["embed_model"] = model
return await actual_func(**kwargs)
elif binding == "azure_openai": elif binding == "azure_openai":
from lightrag.llm.azure_openai import azure_openai_embed from lightrag.llm.azure_openai import azure_openai_embed
@ -757,7 +764,11 @@ def create_app(args):
if isinstance(azure_openai_embed, EmbeddingFunc) if isinstance(azure_openai_embed, EmbeddingFunc)
else azure_openai_embed else azure_openai_embed
) )
return await actual_func(texts, model=model, api_key=api_key) # Pass model only if provided, let function use its default otherwise
kwargs = {"texts": texts, "api_key": api_key}
if model:
kwargs["model"] = model
return await actual_func(**kwargs)
elif binding == "aws_bedrock": elif binding == "aws_bedrock":
from lightrag.llm.bedrock import bedrock_embed from lightrag.llm.bedrock import bedrock_embed
@ -766,7 +777,11 @@ def create_app(args):
if isinstance(bedrock_embed, EmbeddingFunc) if isinstance(bedrock_embed, EmbeddingFunc)
else bedrock_embed else bedrock_embed
) )
return await actual_func(texts, model=model) # Pass model only if provided, let function use its default otherwise
kwargs = {"texts": texts}
if model:
kwargs["model"] = model
return await actual_func(**kwargs)
elif binding == "jina": elif binding == "jina":
from lightrag.llm.jina import jina_embed from lightrag.llm.jina import jina_embed
@ -775,12 +790,16 @@ def create_app(args):
if isinstance(jina_embed, EmbeddingFunc) if isinstance(jina_embed, EmbeddingFunc)
else jina_embed else jina_embed
) )
return await actual_func( # Pass model only if provided, let function use its default (jina-embeddings-v4)
texts, kwargs = {
embedding_dim=embedding_dim, "texts": texts,
base_url=host, "embedding_dim": embedding_dim,
api_key=api_key, "base_url": host,
) "api_key": api_key,
}
if model:
kwargs["model"] = model
return await actual_func(**kwargs)
elif binding == "gemini": elif binding == "gemini":
from lightrag.llm.gemini import gemini_embed from lightrag.llm.gemini import gemini_embed
@ -798,14 +817,19 @@ def create_app(args):
gemini_options = GeminiEmbeddingOptions.options_dict(args) gemini_options = GeminiEmbeddingOptions.options_dict(args)
return await actual_func( # Pass model only if provided, let function use its default (gemini-embedding-001)
texts, kwargs = {
model=model, "texts": texts,
base_url=host, "base_url": host,
api_key=api_key, "api_key": api_key,
embedding_dim=embedding_dim, "embedding_dim": embedding_dim,
task_type=gemini_options.get("task_type", "RETRIEVAL_DOCUMENT"), "task_type": gemini_options.get(
) "task_type", "RETRIEVAL_DOCUMENT"
),
}
if model:
kwargs["model"] = model
return await actual_func(**kwargs)
else: # openai and compatible else: # openai and compatible
from lightrag.llm.openai import openai_embed from lightrag.llm.openai import openai_embed
@ -814,13 +838,16 @@ def create_app(args):
if isinstance(openai_embed, EmbeddingFunc) if isinstance(openai_embed, EmbeddingFunc)
else openai_embed else openai_embed
) )
return await actual_func( # Pass model only if provided, let function use its default (text-embedding-3-small)
texts, kwargs = {
model=model, "texts": texts,
base_url=host, "base_url": host,
api_key=api_key, "api_key": api_key,
embedding_dim=embedding_dim, "embedding_dim": embedding_dim,
) }
if model:
kwargs["model"] = model
return await actual_func(**kwargs)
except ImportError as e: except ImportError as e:
raise Exception(f"Failed to import {binding} embedding: {e}") raise Exception(f"Failed to import {binding} embedding: {e}")
@ -929,11 +956,6 @@ def create_app(args):
else: else:
logger.info("Embedding max_token_size: not set (90% token warning disabled)") logger.info("Embedding max_token_size: not set (90% token warning disabled)")
# Set max_token_size if EMBEDDING_TOKEN_LIMIT is provided
if args.embedding_token_limit is not None:
embedding_func.max_token_size = args.embedding_token_limit
logger.info(f"Set embedding max_token_size to {args.embedding_token_limit}")
# Configure rerank function based on args.rerank_bindingparameter # Configure rerank function based on args.rerank_bindingparameter
rerank_model_func = None rerank_model_func = None
if args.rerank_binding != "null": if args.rerank_binding != "null":
@ -1072,8 +1094,11 @@ def create_app(args):
@app.get("/") @app.get("/")
async def redirect_to_webui(): async def redirect_to_webui():
"""Redirect root path to /webui""" """Redirect root path based on WebUI availability"""
return RedirectResponse(url="/webui") if webui_assets_exist:
return RedirectResponse(url="/webui")
else:
return RedirectResponse(url="/docs")
@app.get("/auth-status") @app.get("/auth-status")
async def get_auth_status(): async def get_auth_status():
@ -1140,18 +1165,49 @@ def create_app(args):
"webui_description": webui_description, "webui_description": webui_description,
} }
@app.get("/health", dependencies=[Depends(combined_auth)]) @app.get(
"/health",
dependencies=[Depends(combined_auth)],
summary="Get system health and configuration status",
description="Returns comprehensive system status including WebUI availability, configuration, and operational metrics",
response_description="System health status with configuration details",
responses={
200: {
"description": "Successful response with system status",
"content": {
"application/json": {
"example": {
"status": "healthy",
"webui_available": True,
"working_directory": "/path/to/working/dir",
"input_directory": "/path/to/input/dir",
"configuration": {
"llm_binding": "openai",
"llm_model": "gpt-4",
"embedding_binding": "openai",
"embedding_model": "text-embedding-ada-002",
"workspace": "default",
},
"auth_mode": "enabled",
"pipeline_busy": False,
"core_version": "0.0.1",
"api_version": "0.0.1",
}
}
},
}
},
)
async def get_status(request: Request): async def get_status(request: Request):
"""Get current system status""" """Get current system status including WebUI availability"""
try: try:
# Extract workspace from request header or use default
workspace = get_workspace_from_request(request) workspace = get_workspace_from_request(request)
default_workspace = get_default_workspace()
# Construct namespace (following GraphDB pattern) if workspace is None:
namespace = f"{workspace}:pipeline" if workspace else "pipeline_status" workspace = default_workspace
pipeline_status = await get_namespace_data(
# Get workspace-specific pipeline status "pipeline_status", workspace=workspace
pipeline_status = await get_namespace_data(namespace) )
if not auth_configured: if not auth_configured:
auth_mode = "disabled" auth_mode = "disabled"
@ -1163,6 +1219,7 @@ def create_app(args):
return { return {
"status": "healthy", "status": "healthy",
"webui_available": webui_assets_exist,
"working_directory": str(args.working_dir), "working_directory": str(args.working_dir),
"input_directory": str(args.input_dir), "input_directory": str(args.input_dir),
"configuration": { "configuration": {
@ -1182,8 +1239,7 @@ def create_app(args):
"vector_storage": args.vector_storage, "vector_storage": args.vector_storage,
"enable_llm_cache_for_extract": args.enable_llm_cache_for_extract, "enable_llm_cache_for_extract": args.enable_llm_cache_for_extract,
"enable_llm_cache": args.enable_llm_cache, "enable_llm_cache": args.enable_llm_cache,
"workspace": workspace, "workspace": default_workspace,
"default_workspace": args.workspace,
"max_graph_nodes": args.max_graph_nodes, "max_graph_nodes": args.max_graph_nodes,
# Rerank configuration # Rerank configuration
"enable_rerank": rerank_model_func is not None, "enable_rerank": rerank_model_func is not None,
@ -1253,16 +1309,27 @@ def create_app(args):
name="swagger-ui-static", name="swagger-ui-static",
) )
# Webui mount webui/index.html # Conditionally mount WebUI only if assets exist
static_dir = Path(__file__).parent / "webui" if webui_assets_exist:
static_dir.mkdir(exist_ok=True) static_dir = Path(__file__).parent / "webui"
app.mount( static_dir.mkdir(exist_ok=True)
"/webui", app.mount(
SmartStaticFiles( "/webui",
directory=static_dir, html=True, check_dir=True SmartStaticFiles(
), # Use SmartStaticFiles directory=static_dir, html=True, check_dir=True
name="webui", ), # Use SmartStaticFiles
) name="webui",
)
logger.info("WebUI assets mounted at /webui")
else:
logger.info("WebUI assets not available, /webui route not mounted")
# Add redirect for /webui when assets are not available
@app.get("/webui")
@app.get("/webui/")
async def webui_redirect_to_docs():
"""Redirect /webui to /docs when WebUI is not available"""
return RedirectResponse(url="/docs")
return app return app

View file

@ -173,7 +173,9 @@ async def ollama_model_complete(
@wrap_embedding_func_with_attrs(embedding_dim=1024, max_token_size=8192) @wrap_embedding_func_with_attrs(embedding_dim=1024, max_token_size=8192)
async def ollama_embed(texts: list[str], embed_model, **kwargs) -> np.ndarray: async def ollama_embed(
texts: list[str], embed_model: str = "bge-m3:latest", **kwargs
) -> np.ndarray:
api_key = kwargs.pop("api_key", None) api_key = kwargs.pop("api_key", None)
if not api_key: if not api_key:
api_key = os.getenv("OLLAMA_API_KEY") api_key = os.getenv("OLLAMA_API_KEY")