fix: adds logging to langflow run command (#8466)
* Clean up initialiation of uv run langflow run * sigint fixes * Log level fixes and gunicorn logs removal * sqlalch contraints * Add back banner for windows * [autofix.ci] apply automated fixes * handle sigint sigterms * [autofix.ci] apply automated fixes * handle multiple sigints * more gracefully handle shutdown during init * [autofix.ci] apply automated fixes * swallow cancellation during log flush * fix path import and cleanup * [autofix.ci] apply automated fixes * start with step logs * 📝 (main.py): Comment out logger.info statement to prevent unnecessary logging of static files directory setup information. * 💡 (langflow/__main__.py): Comment out logger.info message for initializing Langflow to reduce noise in logs and improve readability. * uncomment logs, change to debug * 📝 (langflow): remove redundant logger info messages in signal handlers and shutdown process ♻️ (langflow): refactor shutdown process to use a progress indicator for better visibility and control ♻️ (langflow): refactor initialization process to remove redundant logger info messages 📝 (langflow): add methods to print shutdown summary and farewell message after shutdown is complete 📝 (langflow): add method to create a progress indicator for shutdown steps 📝 (langflow): add predefined shutdown steps for Langflow shutdown process 📝 (langflow): add method to create a progress indicator with predefined Langflow initialization steps 📝 (langflow): add method to create a progress indicator with predefined Langflow shutdown steps 📝 (langflow): add predefined shutdown steps in reverse order of initialization 📝 (langflow): add predefined initialization steps for Langflow initialization process 📝 (langflow): add method to print a summary of all completed shutdown steps * 📝 (api/v1/__init__.py): reorganize imports and include_router calls for better readability and maintainability 🔧 (api/v1/__init__.py): update __all__ list to explicitly include all routers for better module encapsulation 💡 (utils.py): add logging statement to indicate building custom components from specified paths * Fix ordering of operations for windows builds * use lowercase log levels for uvicorn * converts log to lower if set from env var * [autofix.ci] apply automated fixes * use a subprocess for windows so progress step can complete * [autofix.ci] apply automated fixes * test subprocess for windows * use subprocess for all architectures * Clean up global vars and simplify shutdown * [autofix.ci] apply automated fixes * 📝 (progress.py): add platform check to use different characters and icons based on the operating system for better compatibility and user experience * 📝 (langflow/__main__.py): use platform-specific characters for icons to prevent encoding issues and improve cross-platform compatibility * test for windows * remove print * move windows blocking call outside of progres step * [autofix.ci] apply automated fixes * log cleanup and timeout of teardown * remove seemingly unnecessary redirect * [autofix.ci] apply automated fixes * ruff * Add init file to cli dir * ruff: : * unused import * mypy fixes * mypy fixes * [autofix.ci] apply automated fixes * ruff * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * Try underscores so ci doesn't remove * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
parent
e5bec89cc8
commit
131322cb9b
15 changed files with 6191 additions and 4124 deletions
|
|
@ -215,8 +215,8 @@ nv-ingest = [
|
|||
]
|
||||
|
||||
postgresql = [
|
||||
"sqlalchemy[postgresql_psycopg2binary]",
|
||||
"sqlalchemy[postgresql_psycopg]",
|
||||
"sqlalchemy[postgresql_psycopg2binary]>=2.0.38,<3.0.0",
|
||||
"sqlalchemy[postgresql_psycopg]>=2.0.38,<3.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from rich.panel import Panel
|
|||
from rich.table import Table
|
||||
from sqlmodel import select
|
||||
|
||||
from langflow.cli.progress import create_langflow_progress
|
||||
from langflow.initial_setup.setup import get_or_create_default_folder
|
||||
from langflow.logging.logger import configure, logger
|
||||
from langflow.main import setup_app
|
||||
|
|
@ -40,6 +41,54 @@ console = Console()
|
|||
app = typer.Typer(no_args_is_help=True)
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""Manages the lifecycle of the backend process."""
|
||||
|
||||
def __init__(self):
|
||||
self.webapp_process = None
|
||||
self.shutdown_in_progress = False
|
||||
|
||||
# params are required for signal handlers, even if they are not used
|
||||
def handle_sigterm(self, _signum: int, _frame) -> None:
|
||||
"""Handle SIGTERM signal gracefully."""
|
||||
if self.shutdown_in_progress:
|
||||
return # Already shutting down, ignore
|
||||
self.shutdown_in_progress = True
|
||||
self.shutdown()
|
||||
|
||||
# params are required for signal handlers, even if they are not used
|
||||
def handle_sigint(self, _signum: int, _frame) -> None:
|
||||
"""Handle SIGINT signal gracefully."""
|
||||
if self.shutdown_in_progress:
|
||||
return # Already shutting down, ignore
|
||||
self.shutdown_in_progress = True
|
||||
self.shutdown()
|
||||
|
||||
def shutdown(self):
|
||||
"""Gracefully shutdown the webapp process."""
|
||||
if self.webapp_process and self.webapp_process.is_alive():
|
||||
# Just terminate the process - the actual shutdown progress is handled
|
||||
# by the FastAPI lifespan context in main.py
|
||||
self.webapp_process.terminate()
|
||||
# The long wait allows the process to finish setup, preventing it from
|
||||
# getting in a state where background tasks continue to do work after termination
|
||||
# is sent.
|
||||
self.webapp_process.join(timeout=30)
|
||||
if self.webapp_process.is_alive():
|
||||
logger.warning("Process didn't terminate gracefully, killing it.")
|
||||
self.webapp_process.kill()
|
||||
self.webapp_process.join()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# Create a single instance of ProcessManager
|
||||
process_manager = ProcessManager()
|
||||
|
||||
# Update signal handlers to use the instance methods
|
||||
signal.signal(signal.SIGTERM, process_manager.handle_sigterm)
|
||||
signal.signal(signal.SIGINT, process_manager.handle_sigint)
|
||||
|
||||
|
||||
def get_number_of_workers(workers=None):
|
||||
if workers == -1 or workers is None:
|
||||
workers = (cpu_count() * 2) + 1
|
||||
|
|
@ -78,11 +127,19 @@ def set_var_for_macos_issue() -> None:
|
|||
logger.debug("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY to YES to avoid error")
|
||||
|
||||
|
||||
def handle_sigterm(signum, frame): # noqa: ARG001
|
||||
"""Handle SIGTERM signal gracefully."""
|
||||
logger.info("Received SIGTERM signal. Performing graceful shutdown...")
|
||||
# Raise SystemExit to trigger graceful shutdown
|
||||
sys.exit(0)
|
||||
def wait_for_server_ready(host, port, protocol) -> None:
|
||||
"""Wait for the server to become ready by polling the health endpoint."""
|
||||
status_code = 0
|
||||
while status_code != httpx.codes.OK:
|
||||
try:
|
||||
status_code = httpx.get(
|
||||
f"{protocol}://{host}:{port}/health", verify=host not in ("127.0.0.1", "localhost")
|
||||
).status_code
|
||||
except HTTPError:
|
||||
time.sleep(1)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.opt(exception=True).debug("Error while waiting for the server to become ready.")
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
|
|
@ -103,7 +160,11 @@ def run(
|
|||
help="Path to the .env file containing environment variables.",
|
||||
show_default=False,
|
||||
),
|
||||
log_level: str | None = typer.Option(None, help="Logging level.", show_default=False),
|
||||
log_level: str | None = typer.Option(
|
||||
None,
|
||||
help="Logging level. One of: [debug, info, warning, error, critical]. Defaults to info.",
|
||||
show_default=False,
|
||||
),
|
||||
log_file: Path | None = typer.Option(None, help="Path to the log file.", show_default=False),
|
||||
cache: str | None = typer.Option( # noqa: ARG001
|
||||
None,
|
||||
|
|
@ -167,127 +228,148 @@ def run(
|
|||
ssl_key_file_path: str | None = typer.Option(None, help="Defines the SSL key file path.", show_default=False),
|
||||
) -> None:
|
||||
"""Run Langflow."""
|
||||
# Register SIGTERM handler
|
||||
signal.signal(signal.SIGTERM, handle_sigterm)
|
||||
|
||||
if env_file:
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
# Set default log level if not provided
|
||||
log_level_str = "info" if log_level is None else log_level.lower()
|
||||
|
||||
# Must set as env var for child process to pick up
|
||||
env_log_level = os.environ.get("LANGFLOW_LOG_LEVEL")
|
||||
if env_log_level is None:
|
||||
os.environ["LANGFLOW_LOG_LEVEL"] = log_level_str
|
||||
else:
|
||||
os.environ["LANGFLOW_LOG_LEVEL"] = env_log_level.lower()
|
||||
|
||||
configure(log_level=log_level, log_file=log_file)
|
||||
logger.debug(f"Loading config from file: '{env_file}'" if env_file else "No env_file provided.")
|
||||
set_var_for_macos_issue()
|
||||
settings_service = get_settings_service()
|
||||
|
||||
for key, value in os.environ.items():
|
||||
new_key = key.replace("LANGFLOW_", "")
|
||||
if hasattr(settings_service.auth_settings, new_key):
|
||||
setattr(settings_service.auth_settings, new_key, value)
|
||||
# Create progress indicator (show verbose timing if log level is DEBUG)
|
||||
verbose = log_level == "debug"
|
||||
progress = create_langflow_progress(verbose=verbose)
|
||||
|
||||
frame = inspect.currentframe()
|
||||
valid_args: list = []
|
||||
values: dict = {}
|
||||
if frame is not None:
|
||||
arguments, _, _, values = inspect.getargvalues(frame)
|
||||
valid_args = [arg for arg in arguments if values[arg] is not None]
|
||||
# Step 0: Initializing Langflow
|
||||
with progress.step(0):
|
||||
logger.debug(f"Loading config from file: '{env_file}'" if env_file else "No env_file provided.")
|
||||
set_var_for_macos_issue()
|
||||
settings_service = get_settings_service()
|
||||
|
||||
for arg in valid_args:
|
||||
if arg == "components_path":
|
||||
settings_service.settings.update_settings(components_path=components_path)
|
||||
elif hasattr(settings_service.settings, arg):
|
||||
settings_service.set(arg, values[arg])
|
||||
elif hasattr(settings_service.auth_settings, arg):
|
||||
settings_service.auth_settings.set(arg, values[arg])
|
||||
logger.debug(f"Loading config from cli parameter '{arg}': '{values[arg]}'")
|
||||
# Step 1: Checking Environment
|
||||
with progress.step(1):
|
||||
for key, value in os.environ.items():
|
||||
new_key = key.replace("LANGFLOW_", "")
|
||||
if hasattr(settings_service.auth_settings, new_key):
|
||||
setattr(settings_service.auth_settings, new_key, value)
|
||||
|
||||
host = settings_service.settings.host
|
||||
port = settings_service.settings.port
|
||||
workers = settings_service.settings.workers
|
||||
worker_timeout = settings_service.settings.worker_timeout
|
||||
log_level = settings_service.settings.log_level
|
||||
frontend_path = settings_service.settings.frontend_path
|
||||
backend_only = settings_service.settings.backend_only
|
||||
ssl_cert_file_path = settings_service.settings.ssl_cert_file if ssl_cert_file_path is None else ssl_cert_file_path
|
||||
ssl_key_file_path = settings_service.settings.ssl_key_file if ssl_key_file_path is None else ssl_key_file_path
|
||||
frame = inspect.currentframe()
|
||||
valid_args: list = []
|
||||
values: dict = {}
|
||||
if frame is not None:
|
||||
arguments, _, _, values = inspect.getargvalues(frame)
|
||||
valid_args = [arg for arg in arguments if values[arg] is not None]
|
||||
|
||||
# create path object if frontend_path is provided
|
||||
static_files_dir: Path | None = Path(frontend_path) if frontend_path else None
|
||||
for arg in valid_args:
|
||||
if arg == "components_path":
|
||||
settings_service.settings.update_settings(components_path=components_path)
|
||||
elif hasattr(settings_service.settings, arg):
|
||||
settings_service.set(arg, values[arg])
|
||||
elif hasattr(settings_service.auth_settings, arg):
|
||||
settings_service.auth_settings.set(arg, values[arg])
|
||||
logger.debug(f"Loading config from cli parameter '{arg}': '{values[arg]}'")
|
||||
|
||||
app = setup_app(static_files_dir=static_files_dir, backend_only=backend_only)
|
||||
# check if port is being used
|
||||
if is_port_in_use(port, host):
|
||||
port = get_free_port(port)
|
||||
# Get final values from settings
|
||||
host = settings_service.settings.host
|
||||
port = settings_service.settings.port
|
||||
workers = settings_service.settings.workers
|
||||
worker_timeout = settings_service.settings.worker_timeout
|
||||
log_level = settings_service.settings.log_level
|
||||
frontend_path = settings_service.settings.frontend_path
|
||||
backend_only = settings_service.settings.backend_only
|
||||
ssl_cert_file_path = (
|
||||
settings_service.settings.ssl_cert_file if ssl_cert_file_path is None else ssl_cert_file_path
|
||||
)
|
||||
ssl_key_file_path = settings_service.settings.ssl_key_file if ssl_key_file_path is None else ssl_key_file_path
|
||||
|
||||
options = {
|
||||
"bind": f"{host}:{port}",
|
||||
"workers": get_number_of_workers(workers),
|
||||
"timeout": worker_timeout,
|
||||
"certfile": ssl_cert_file_path,
|
||||
"keyfile": ssl_key_file_path,
|
||||
}
|
||||
protocol = "https" if options["keyfile"] and options["certfile"] else "http"
|
||||
# create path object if frontend_path is provided
|
||||
static_files_dir: Path | None = Path(frontend_path) if frontend_path else None
|
||||
|
||||
# Define an env variable to know if we are just testing the server
|
||||
if "pytest" in sys.modules:
|
||||
return
|
||||
process: Process | None = None
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
# Run using uvicorn on MacOS and Windows
|
||||
# Windows doesn't support gunicorn
|
||||
# MacOS requires an env variable to be set to use gunicorn
|
||||
run_on_windows(host, port, log_level, options, app, protocol)
|
||||
else:
|
||||
# Run using gunicorn on Linux
|
||||
process = run_on_mac_or_linux(host, port, log_level, options, app, protocol)
|
||||
# Step 2: Starting Core Services
|
||||
with progress.step(2):
|
||||
app = setup_app(static_files_dir=static_files_dir, backend_only=backend_only)
|
||||
|
||||
# Step 3: Connecting Database (this happens inside setup_app via dependencies)
|
||||
with progress.step(3):
|
||||
# check if port is being used
|
||||
if is_port_in_use(port, host):
|
||||
port = get_free_port(port)
|
||||
|
||||
protocol = "https" if ssl_cert_file_path and ssl_key_file_path else "http"
|
||||
|
||||
# Step 4: Loading Components (placeholder for components loading)
|
||||
with progress.step(4):
|
||||
pass # Components are loaded during app startup
|
||||
|
||||
# Step 5: Adding Starter Projects (placeholder for starter projects)
|
||||
if get_settings_service().settings.create_starter_projects:
|
||||
with progress.step(5):
|
||||
pass # Starter projects are added during app startup
|
||||
|
||||
# Step 6: Launching Langflow
|
||||
if platform.system() == "Windows":
|
||||
with progress.step(6):
|
||||
import uvicorn
|
||||
|
||||
# Print summary and banner before starting the server, since uvicorn is a blocking call.
|
||||
# We _may_ be able to subprocess, but with window's spawn behavior, we'd have to move all
|
||||
# non-picklable code to the subprocess.
|
||||
progress.print_summary()
|
||||
print_banner(host, port, protocol)
|
||||
|
||||
# Blocking call, so must be outside of the progress step
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level=log_level,
|
||||
reload=False,
|
||||
workers=get_number_of_workers(workers),
|
||||
loop="asyncio",
|
||||
)
|
||||
else:
|
||||
with progress.step(6):
|
||||
# Use Gunicorn with LangflowUvicornWorker for non-Windows systems
|
||||
from langflow.server import LangflowApplication
|
||||
|
||||
options = {
|
||||
"bind": f"{host}:{port}",
|
||||
"workers": get_number_of_workers(workers),
|
||||
"timeout": worker_timeout,
|
||||
"certfile": ssl_cert_file_path,
|
||||
"keyfile": ssl_key_file_path,
|
||||
"log_level": log_level.lower(),
|
||||
}
|
||||
server = LangflowApplication(app, options)
|
||||
|
||||
# Start the webapp process
|
||||
process_manager.webapp_process = Process(target=server.run)
|
||||
process_manager.webapp_process.start()
|
||||
|
||||
wait_for_server_ready(host, port, protocol)
|
||||
|
||||
# Print summary and banner after server is ready
|
||||
progress.print_summary()
|
||||
print_banner(host, port, protocol)
|
||||
|
||||
# Handle browser opening
|
||||
if open_browser and not backend_only:
|
||||
browser_host = get_best_access_host(host, port)
|
||||
click.launch(f"{protocol}://{browser_host}:{port}")
|
||||
if process:
|
||||
process.join()
|
||||
except (KeyboardInterrupt, SystemExit) as e:
|
||||
logger.info("Shutting down server...")
|
||||
if process is not None:
|
||||
process.terminate()
|
||||
process.join(timeout=15) # Wait up to 15 seconds for process to terminate
|
||||
if process.is_alive():
|
||||
logger.warning("Process did not terminate gracefully, forcing...")
|
||||
process.kill()
|
||||
raise typer.Exit(0) from e
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
if process is not None:
|
||||
process.terminate()
|
||||
raise typer.Exit(1) from e
|
||||
click.launch(f"{protocol}://{host}:{port}")
|
||||
|
||||
|
||||
def wait_for_server_ready(host, port, protocol) -> None:
|
||||
"""Wait for the server to become ready by polling the health endpoint."""
|
||||
status_code = 0
|
||||
while status_code != httpx.codes.OK:
|
||||
try:
|
||||
status_code = httpx.get(
|
||||
f"{protocol}://{host}:{port}/health", verify=host not in ("localhost", "127.0.0.1")
|
||||
).status_code
|
||||
except HTTPError:
|
||||
time.sleep(1)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.opt(exception=True).debug("Error while waiting for the server to become ready.")
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def run_on_mac_or_linux(host, port, log_level, options, app, protocol):
|
||||
webapp_process = Process(target=run_langflow, args=(host, port, log_level, options, app))
|
||||
webapp_process.start()
|
||||
wait_for_server_ready(host, port, protocol)
|
||||
|
||||
print_banner(host, port, protocol)
|
||||
return webapp_process
|
||||
|
||||
|
||||
def run_on_windows(host, port, log_level, options, app, protocol) -> None:
|
||||
"""Run the Langflow server on Windows."""
|
||||
print_banner(host, port, protocol)
|
||||
run_langflow(host, port, log_level, options, app)
|
||||
process_manager.webapp_process.join()
|
||||
except KeyboardInterrupt:
|
||||
# SIGINT should be handled by the signal handler, but leaving here for safety
|
||||
logger.warning("KeyboardInterrupt caught in main thread")
|
||||
finally:
|
||||
process_manager.shutdown()
|
||||
|
||||
|
||||
def is_port_in_use(port, host="localhost"):
|
||||
|
|
@ -466,9 +548,24 @@ def print_banner(host: str, port: int, protocol: str) -> None:
|
|||
)
|
||||
|
||||
title = f"[bold]Welcome to {styled_package_name}[/bold]\n"
|
||||
|
||||
# Use Windows-safe characters to prevent encoding issues
|
||||
import platform
|
||||
|
||||
if platform.system() == "Windows":
|
||||
github_icon = "*"
|
||||
discord_icon = "#"
|
||||
arrow = "->"
|
||||
status_icon = "[OK]"
|
||||
else:
|
||||
github_icon = ":star2:"
|
||||
discord_icon = ":speech_balloon:"
|
||||
arrow = "→"
|
||||
status_icon = "🟢"
|
||||
|
||||
info_text = (
|
||||
":star2: GitHub: Star for updates → https://github.com/langflow-ai/langflow\n"
|
||||
":speech_balloon: Discord: Join for support → https://discord.com/invite/EqksyE2EX9"
|
||||
f"{github_icon} GitHub: Star for updates {arrow} https://github.com/langflow-ai/langflow\n"
|
||||
f"{discord_icon} Discord: Join for support {arrow} https://discord.com/invite/EqksyE2EX9"
|
||||
)
|
||||
telemetry_text = (
|
||||
(
|
||||
|
|
@ -482,54 +579,13 @@ def print_banner(host: str, port: int, protocol: str) -> None:
|
|||
)
|
||||
)
|
||||
access_host = get_best_access_host(host, port)
|
||||
access_link = f"[bold]🟢 Open Langflow →[/bold] [link={protocol}://{access_host}:{port}]{protocol}://{access_host}:{port}[/link]"
|
||||
access_link = f"[bold]{status_icon} Open Langflow {arrow}[/bold] [link={protocol}://{access_host}:{port}]{protocol}://{access_host}:{port}[/link]"
|
||||
|
||||
message = f"{title}\n{info_text}\n\n{telemetry_text}\n\n{access_link}"
|
||||
|
||||
console.print(Panel.fit(message, border_style="#7528FC", padding=(1, 2)))
|
||||
|
||||
|
||||
def run_langflow(host, port, log_level, options, app) -> None:
|
||||
"""Run Langflow server on localhost."""
|
||||
if platform.system() == "Windows":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level=log_level.lower(),
|
||||
loop="asyncio",
|
||||
ssl_keyfile=options["keyfile"],
|
||||
ssl_certfile=options["certfile"],
|
||||
)
|
||||
else:
|
||||
from langflow.server import LangflowApplication
|
||||
|
||||
server = LangflowApplication(app, options)
|
||||
|
||||
def graceful_shutdown(signum, frame): # noqa: ARG001
|
||||
"""Gracefully shutdown the server when receiving SIGTERM."""
|
||||
# Suppress click exceptions during shutdown
|
||||
import click
|
||||
|
||||
click.echo = lambda *args, **kwargs: None # noqa: ARG005
|
||||
|
||||
logger.info("Gracefully shutting down server...")
|
||||
# For Gunicorn workers, we raise SystemExit to trigger graceful shutdown
|
||||
raise SystemExit(0)
|
||||
|
||||
# Register signal handlers
|
||||
signal.signal(signal.SIGTERM, graceful_shutdown)
|
||||
signal.signal(signal.SIGINT, graceful_shutdown)
|
||||
|
||||
try:
|
||||
server.run()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
# Suppress the exception output
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@app.command()
|
||||
def superuser(
|
||||
username: str = typer.Option(..., prompt=True, help="Username for the superuser."),
|
||||
|
|
|
|||
0
src/backend/base/langflow/cli/__init__.py
Normal file
0
src/backend/base/langflow/cli/__init__.py
Normal file
225
src/backend/base/langflow/cli/progress.py
Normal file
225
src/backend/base/langflow/cli/progress.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import platform
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
MIN_DURATION_THRESHOLD = 0.1 # Minimum duration to show in seconds (100ms)
|
||||
|
||||
|
||||
class ProgressIndicator:
|
||||
"""A CLI progress indicator that shows user-friendly step-by-step progress.
|
||||
|
||||
Shows animated loading indicators (□ → ■) for each step of the initialization process.
|
||||
"""
|
||||
|
||||
def __init__(self, *, verbose: bool = False):
|
||||
self.verbose = verbose
|
||||
self.steps: list[dict[str, Any]] = []
|
||||
self.current_step = 0
|
||||
self.running = False
|
||||
self._stop_animation = False
|
||||
self._animation_thread: threading.Thread | None = None
|
||||
|
||||
# Use Windows-safe characters on Windows to prevent encoding issues
|
||||
if platform.system() == "Windows":
|
||||
self._animation_chars = ["-", "\\", "|", "/"] # ASCII spinner
|
||||
self._success_icon = "+" # ASCII plus sign
|
||||
self._failure_icon = "x" # ASCII x
|
||||
self._farewell_emoji = ":)" # ASCII smiley
|
||||
else:
|
||||
self._animation_chars = ["□", "▢", "▣", "■"] # Unicode squares
|
||||
self._success_icon = "✓" # Unicode checkmark
|
||||
self._failure_icon = "✗" # Unicode cross
|
||||
self._farewell_emoji = "👋" # Unicode wave
|
||||
|
||||
self._animation_index = 0
|
||||
|
||||
def add_step(self, title: str, description: str = "") -> None:
|
||||
"""Add a step to the progress indicator."""
|
||||
self.steps.append(
|
||||
{
|
||||
"title": title,
|
||||
"description": description,
|
||||
"status": "pending", # pending, running, completed, failed
|
||||
"start_time": None,
|
||||
"end_time": None,
|
||||
}
|
||||
)
|
||||
|
||||
def _animate_step(self, step_index: int) -> None:
|
||||
"""Animate the current step with rotating square characters."""
|
||||
if step_index >= len(self.steps):
|
||||
return
|
||||
|
||||
step = self.steps[step_index]
|
||||
|
||||
while self.running and step["status"] == "running" and not self._stop_animation:
|
||||
# Clear the current line and move cursor to beginning
|
||||
sys.stdout.write("\r")
|
||||
|
||||
# Show the animated character
|
||||
animation_char = self._animation_chars[self._animation_index]
|
||||
|
||||
# Print the step with animation
|
||||
line = f"{animation_char} {step['title']}..."
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
|
||||
# Update animation
|
||||
self._animation_index = (self._animation_index + 1) % len(self._animation_chars)
|
||||
|
||||
time.sleep(0.15) # Animation speed
|
||||
|
||||
def start_step(self, step_index: int) -> None:
|
||||
"""Start a specific step and begin animation."""
|
||||
if step_index >= len(self.steps):
|
||||
return
|
||||
|
||||
self.current_step = step_index
|
||||
step = self.steps[step_index]
|
||||
step["status"] = "running"
|
||||
step["start_time"] = time.time()
|
||||
|
||||
self.running = True
|
||||
self._stop_animation = False
|
||||
|
||||
# Start animation in a separate thread
|
||||
self._animation_thread = threading.Thread(target=self._animate_step, args=(step_index,))
|
||||
self._animation_thread.daemon = True
|
||||
self._animation_thread.start()
|
||||
|
||||
def complete_step(self, step_index: int, *, success: bool = True) -> None:
|
||||
"""Complete a step and stop its animation."""
|
||||
if step_index >= len(self.steps):
|
||||
return
|
||||
|
||||
step = self.steps[step_index]
|
||||
step["status"] = "completed" if success else "failed"
|
||||
step["end_time"] = time.time()
|
||||
|
||||
# Stop animation
|
||||
self._stop_animation = True
|
||||
if self._animation_thread and self._animation_thread.is_alive():
|
||||
self._animation_thread.join(timeout=0.5)
|
||||
|
||||
self.running = False
|
||||
|
||||
# Clear the current line and print final result
|
||||
sys.stdout.write("\r")
|
||||
|
||||
if success:
|
||||
icon = click.style(self._success_icon, fg="green", bold=True)
|
||||
title = click.style(step["title"], fg="green")
|
||||
else:
|
||||
icon = click.style(self._failure_icon, fg="red", bold=True)
|
||||
title = click.style(step["title"], fg="red")
|
||||
|
||||
duration = ""
|
||||
if step["start_time"] and step["end_time"]:
|
||||
elapsed = step["end_time"] - step["start_time"]
|
||||
if self.verbose and elapsed > MIN_DURATION_THRESHOLD: # Only show duration if verbose and > 100ms
|
||||
duration = click.style(f" ({elapsed:.2f}s)", fg="bright_black")
|
||||
|
||||
line = f"{icon} {title}{duration}"
|
||||
click.echo(line)
|
||||
|
||||
def fail_step(self, step_index: int, error_msg: str = "") -> None:
|
||||
"""Mark a step as failed."""
|
||||
self.complete_step(step_index, success=False)
|
||||
if error_msg and self.verbose:
|
||||
click.echo(click.style(f" Error: {error_msg}", fg="red"))
|
||||
|
||||
@contextmanager
|
||||
def step(self, step_index: int) -> Generator[None, None, None]:
|
||||
"""Context manager for running a step with automatic completion."""
|
||||
try:
|
||||
self.start_step(step_index)
|
||||
yield
|
||||
self.complete_step(step_index, success=True)
|
||||
except Exception as e:
|
||||
error_msg = str(e) if self.verbose else ""
|
||||
self.fail_step(step_index, error_msg)
|
||||
raise
|
||||
|
||||
def print_summary(self) -> None:
|
||||
"""Print a summary of all completed steps."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
completed_steps = [s for s in self.steps if s["status"] in ["completed", "failed"]]
|
||||
if not completed_steps:
|
||||
return
|
||||
|
||||
total_time = sum(
|
||||
(s["end_time"] - s["start_time"]) for s in completed_steps if s["start_time"] and s["end_time"]
|
||||
)
|
||||
|
||||
click.echo()
|
||||
click.echo(click.style(f"Total initialization time: {total_time:.2f}s", fg="bright_black"))
|
||||
|
||||
def print_shutdown_summary(self) -> None:
|
||||
"""Print a summary of all completed shutdown steps."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
completed_steps = [s for s in self.steps if s["status"] in ["completed", "failed"]]
|
||||
if not completed_steps:
|
||||
return
|
||||
|
||||
total_time = sum(
|
||||
(s["end_time"] - s["start_time"]) for s in completed_steps if s["start_time"] and s["end_time"]
|
||||
)
|
||||
|
||||
click.echo()
|
||||
click.echo(click.style(f"Total shutdown time: {total_time:.2f}s", fg="bright_black"))
|
||||
|
||||
def print_farewell_message(self) -> None:
|
||||
"""Print a nice farewell message after shutdown is complete."""
|
||||
click.echo()
|
||||
farewell = click.style(f"{self._farewell_emoji} See you next time!", fg="bright_blue", bold=True)
|
||||
click.echo(farewell)
|
||||
|
||||
|
||||
def create_langflow_progress(*, verbose: bool = False) -> ProgressIndicator:
|
||||
"""Create a progress indicator with predefined Langflow initialization steps."""
|
||||
progress = ProgressIndicator(verbose=verbose)
|
||||
|
||||
# Define the initialization steps matching the order in main.py
|
||||
steps = [
|
||||
("Initializing Langflow", "Setting up basic configuration"),
|
||||
("Checking Environment", "Loading environment variables and settings"),
|
||||
("Starting Core Services", "Initializing database and core services"),
|
||||
("Connecting Database", "Setting up database connection and migrations"),
|
||||
("Loading Components", "Caching component types and custom components"),
|
||||
("Adding Starter Projects", "Creating or updating starter project templates"),
|
||||
("Launching Langflow", "Starting server and final setup"),
|
||||
]
|
||||
|
||||
for title, description in steps:
|
||||
progress.add_step(title, description)
|
||||
|
||||
return progress
|
||||
|
||||
|
||||
def create_langflow_shutdown_progress(*, verbose: bool = False) -> ProgressIndicator:
|
||||
"""Create a progress indicator with predefined Langflow shutdown steps."""
|
||||
progress = ProgressIndicator(verbose=verbose)
|
||||
|
||||
# Define the shutdown steps in reverse order of initialization
|
||||
steps = [
|
||||
("Stopping Server", "Gracefully stopping the web server"),
|
||||
("Cancelling Background Tasks", "Stopping file synchronization and background jobs"),
|
||||
("Cleaning Up Services", "Teardown database connections and services"),
|
||||
("Clearing Temporary Files", "Removing temporary directories and cache"),
|
||||
("Finalizing Shutdown", "Completing cleanup and logging"),
|
||||
]
|
||||
|
||||
for title, description in steps:
|
||||
progress.add_step(title, description)
|
||||
|
||||
return progress
|
||||
|
|
@ -536,6 +536,7 @@ def build_custom_components(components_paths: list[str]):
|
|||
return {}
|
||||
|
||||
logger.info(f"Building custom components from {components_paths}")
|
||||
|
||||
custom_components_from_file: dict = {}
|
||||
processed_paths = set()
|
||||
for path in components_paths:
|
||||
|
|
@ -546,7 +547,7 @@ def build_custom_components(components_paths: list[str]):
|
|||
custom_component_dict = build_custom_component_list_from_path(path_str)
|
||||
if custom_component_dict:
|
||||
category = next(iter(custom_component_dict))
|
||||
logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
||||
logger.debug(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||
custom_components_from_file, custom_component_dict
|
||||
)
|
||||
|
|
@ -560,7 +561,7 @@ async def abuild_custom_components(components_paths: list[str]):
|
|||
if not components_paths:
|
||||
return {}
|
||||
|
||||
logger.info(f"Building custom components from {components_paths}")
|
||||
logger.debug(f"Building custom components from {components_paths}")
|
||||
custom_components_from_file: dict = {}
|
||||
processed_paths = set()
|
||||
for path in components_paths:
|
||||
|
|
@ -571,7 +572,7 @@ async def abuild_custom_components(components_paths: list[str]):
|
|||
custom_component_dict = await abuild_custom_component_list_from_path(path_str)
|
||||
if custom_component_dict:
|
||||
category = next(iter(custom_component_dict))
|
||||
logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
||||
logger.debug(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||
custom_components_from_file, custom_component_dict
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1015,7 +1015,7 @@ async def initialize_super_user_if_needed() -> None:
|
|||
super_user = await create_super_user(db=async_session, username=username, password=password)
|
||||
await get_variable_service().initialize_user_variables(super_user.id, async_session)
|
||||
_ = await get_or_create_default_folder(async_session, super_user.id)
|
||||
logger.info("Super user initialized")
|
||||
logger.debug("Super user initialized")
|
||||
|
||||
|
||||
async def get_or_create_default_folder(session: AsyncSession, user_id: UUID) -> FolderRead:
|
||||
|
|
@ -1058,32 +1058,45 @@ async def get_or_create_default_folder(session: AsyncSession, user_id: UUID) ->
|
|||
async def sync_flows_from_fs():
|
||||
flow_mtimes = {}
|
||||
fs_flows_polling_interval = get_settings_service().settings.fs_flows_polling_interval / 1000
|
||||
while True:
|
||||
try:
|
||||
async with session_scope() as session:
|
||||
stmt = select(Flow).where(col(Flow.fs_path).is_not(None))
|
||||
flows = (await session.exec(stmt)).all()
|
||||
for flow in flows:
|
||||
mtime = flow_mtimes.setdefault(flow.id, 0)
|
||||
path = anyio.Path(flow.fs_path)
|
||||
try:
|
||||
if await path.exists():
|
||||
new_mtime = (await path.stat()).st_mtime
|
||||
if new_mtime > mtime:
|
||||
update_data = orjson.loads(await path.read_text(encoding="utf-8"))
|
||||
try:
|
||||
for field_name in ("name", "description", "data", "locked"):
|
||||
if new_value := update_data.get(field_name):
|
||||
setattr(flow, field_name, new_value)
|
||||
if folder_id := update_data.get("folder_id"):
|
||||
flow.folder_id = UUID(folder_id)
|
||||
await session.commit()
|
||||
await session.refresh(flow)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception(f"Couldn't update flow {flow.id} in database from path {path}")
|
||||
flow_mtimes[flow.id] = new_mtime
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception(f"Error while handling flow file {path}")
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("Error while syncing flows from database")
|
||||
await asyncio.sleep(fs_flows_polling_interval)
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
async with session_scope() as session:
|
||||
stmt = select(Flow).where(col(Flow.fs_path).is_not(None))
|
||||
flows = (await session.exec(stmt)).all()
|
||||
for flow in flows:
|
||||
mtime = flow_mtimes.setdefault(flow.id, 0)
|
||||
path = anyio.Path(flow.fs_path)
|
||||
try:
|
||||
if await path.exists():
|
||||
new_mtime = (await path.stat()).st_mtime
|
||||
if new_mtime > mtime:
|
||||
update_data = orjson.loads(await path.read_text(encoding="utf-8"))
|
||||
try:
|
||||
for field_name in ("name", "description", "data", "locked"):
|
||||
if new_value := update_data.get(field_name):
|
||||
setattr(flow, field_name, new_value)
|
||||
if folder_id := update_data.get("folder_id"):
|
||||
flow.folder_id = UUID(folder_id)
|
||||
await session.commit()
|
||||
await session.refresh(flow)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception(f"Couldn't update flow {flow.id} in database from path {path}")
|
||||
flow_mtimes[flow.id] = new_mtime
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception(f"Error while handling flow file {path}")
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Flow sync cancelled")
|
||||
break
|
||||
except (sa.exc.OperationalError, ValueError) as e:
|
||||
if "no active connection" in str(e) or "connection is closed" in str(e):
|
||||
logger.debug("Database connection lost, assuming shutdown")
|
||||
break # Exit gracefully, don't error
|
||||
raise # Re-raise if it's a real connection problem
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("Error while syncing flows from database")
|
||||
break
|
||||
|
||||
await asyncio.sleep(fs_flows_polling_interval)
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Flow sync task cancelled")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from urllib.parse import urlencode
|
|||
|
||||
import anyio
|
||||
import httpx
|
||||
import sqlalchemy
|
||||
from fastapi import FastAPI, HTTPException, Request, Response, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
|
@ -124,6 +125,7 @@ def get_lifespan(*, fix_migration=False, version=None):
|
|||
|
||||
temp_dirs: list[TemporaryDirectory] = []
|
||||
sync_flows_from_fs_task = None
|
||||
|
||||
try:
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
|
|
@ -180,25 +182,68 @@ def get_lifespan(*, fix_migration=False, version=None):
|
|||
logger.debug(f"Total initialization time: {total_time:.2f}s")
|
||||
yield
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Lifespan received cancellation signal")
|
||||
except Exception as exc:
|
||||
if "langflow migration --fix" not in str(exc):
|
||||
logger.exception(exc)
|
||||
raise
|
||||
finally:
|
||||
# Clean shutdown
|
||||
logger.info("Cleaning up resources...")
|
||||
if sync_flows_from_fs_task:
|
||||
sync_flows_from_fs_task.cancel()
|
||||
await asyncio.wait([sync_flows_from_fs_task])
|
||||
await teardown_services()
|
||||
# Clean shutdown with progress indicator
|
||||
# Create shutdown progress (show verbose timing if log level is DEBUG)
|
||||
from langflow.cli.progress import create_langflow_shutdown_progress
|
||||
|
||||
await asyncio.sleep(0.1) # let logger flush async logs
|
||||
await logger.complete()
|
||||
log_level = os.getenv("LANGFLOW_LOG_LEVEL", "info").lower()
|
||||
shutdown_progress = create_langflow_shutdown_progress(verbose=log_level == "debug")
|
||||
|
||||
temp_dir_cleanups = [asyncio.to_thread(temp_dir.cleanup) for temp_dir in temp_dirs]
|
||||
await asyncio.gather(*temp_dir_cleanups)
|
||||
# Final message
|
||||
logger.debug("Langflow shutdown complete")
|
||||
try:
|
||||
# Step 0: Stopping Server
|
||||
with shutdown_progress.step(0):
|
||||
logger.debug("Stopping server gracefully...")
|
||||
# The actual server stopping is handled by the lifespan context
|
||||
await asyncio.sleep(0.1) # Brief pause for visual effect
|
||||
|
||||
# Step 1: Cancelling Background Tasks
|
||||
with shutdown_progress.step(1):
|
||||
if sync_flows_from_fs_task:
|
||||
sync_flows_from_fs_task.cancel()
|
||||
await asyncio.wait([sync_flows_from_fs_task])
|
||||
|
||||
# Step 2: Cleaning Up Services
|
||||
with shutdown_progress.step(2):
|
||||
try:
|
||||
await asyncio.wait_for(teardown_services(), timeout=10)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Teardown services timed out.")
|
||||
|
||||
# Step 3: Clearing Temporary Files
|
||||
with shutdown_progress.step(3):
|
||||
temp_dir_cleanups = [asyncio.to_thread(temp_dir.cleanup) for temp_dir in temp_dirs]
|
||||
await asyncio.gather(*temp_dir_cleanups)
|
||||
|
||||
# Step 4: Finalizing Shutdown
|
||||
with shutdown_progress.step(4):
|
||||
logger.debug("Langflow shutdown complete")
|
||||
|
||||
# Show completion summary and farewell
|
||||
shutdown_progress.print_shutdown_summary()
|
||||
shutdown_progress.print_farewell_message()
|
||||
|
||||
except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.DBAPIError) as e:
|
||||
# Case where the database connection is closed during shutdown
|
||||
logger.warning(f"Database teardown failed due to closed connection: {e}")
|
||||
except asyncio.CancelledError:
|
||||
# Swallow this - it's normal during shutdown
|
||||
logger.debug("Teardown cancelled during shutdown.")
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.exception(f"Unhandled error during cleanup: {e}")
|
||||
|
||||
try:
|
||||
await asyncio.shield(asyncio.sleep(0.1)) # let logger flush async logs
|
||||
await asyncio.shield(logger.complete())
|
||||
except asyncio.CancelledError:
|
||||
# Cancellation during logger flush is possible during shutdown, so we swallow it
|
||||
pass
|
||||
|
||||
return lifespan
|
||||
|
||||
|
|
@ -369,7 +414,6 @@ def get_static_files_dir():
|
|||
def setup_app(static_files_dir: Path | None = None, *, backend_only: bool = False) -> FastAPI:
|
||||
"""Setup the FastAPI app."""
|
||||
# get the directory of the current file
|
||||
logger.info(f"Setting up app with static files directory {static_files_dir}")
|
||||
if not static_files_dir:
|
||||
static_files_dir = get_static_files_dir()
|
||||
|
||||
|
|
@ -377,6 +421,7 @@ def setup_app(static_files_dir: Path | None = None, *, backend_only: bool = Fals
|
|||
msg = f"Static files directory {static_files_dir} does not exist."
|
||||
raise RuntimeError(msg)
|
||||
app = create_app()
|
||||
|
||||
if not backend_only and static_files_dir is not None:
|
||||
setup_static_files(app, static_files_dir)
|
||||
return app
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ class LangflowUvicornWorker(UvicornWorker):
|
|||
|
||||
def handle_exit(self, sig, frame):
|
||||
if not self._has_exited:
|
||||
print("👋 See you next time!") # noqa: T201
|
||||
self._has_exited = True
|
||||
|
||||
super().handle_exit(sig, frame)
|
||||
|
|
@ -46,6 +45,9 @@ class Logger(glogging.Logger):
|
|||
|
||||
def __init__(self, cfg) -> None:
|
||||
super().__init__(cfg)
|
||||
logging.getLogger("gunicorn.error").setLevel(logging.WARNING)
|
||||
logging.getLogger("gunicorn.access").setLevel(logging.WARNING)
|
||||
|
||||
logging.getLogger("gunicorn.error").handlers = [InterceptHandler()]
|
||||
logging.getLogger("gunicorn.access").handlers = [InterceptHandler()]
|
||||
|
||||
|
|
|
|||
|
|
@ -310,7 +310,6 @@ class DatabaseService(Service):
|
|||
command.ensure_version(alembic_cfg)
|
||||
# alembic_cfg.attributes["connection"].commit()
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
logger.info("Alembic initialized")
|
||||
|
||||
def _run_migrations(self, should_initialize_alembic, fix) -> None:
|
||||
# First we need to check if alembic has been initialized
|
||||
|
|
@ -336,9 +335,7 @@ class DatabaseService(Service):
|
|||
logger.exception(msg)
|
||||
raise RuntimeError(msg) from exc
|
||||
else:
|
||||
logger.info("Alembic initialized")
|
||||
|
||||
logger.info(f"Running DB migrations in {self.script_location}")
|
||||
logger.debug("Alembic initialized")
|
||||
|
||||
try:
|
||||
buffer.write(f"{datetime.now(tz=timezone.utc).astimezone().isoformat()}: Checking migrations\n")
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class ServiceManager:
|
|||
|
||||
async def teardown(self) -> None:
|
||||
"""Teardown all the services."""
|
||||
for service in self.services.values():
|
||||
for service in list(self.services.values()):
|
||||
if service is None:
|
||||
continue
|
||||
logger.debug(f"Teardown service {service.name}")
|
||||
|
|
|
|||
|
|
@ -429,8 +429,14 @@ class Settings(BaseSettings):
|
|||
logger.debug(f"Appending {langflow_component_path} to components_path")
|
||||
|
||||
if not value:
|
||||
value = []
|
||||
logger.debug("Setting empty components path")
|
||||
value = [BASE_COMPONENTS_PATH]
|
||||
logger.debug("Setting default components path to components_path")
|
||||
else:
|
||||
if isinstance(value, Path):
|
||||
value = [str(value)]
|
||||
elif isinstance(value, list):
|
||||
value = [str(p) if isinstance(p, Path) else p for p in value]
|
||||
logger.debug("Adding default components path to components_path")
|
||||
|
||||
logger.debug(f"Components path: {value}")
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -131,17 +131,12 @@ async def teardown_superuser(settings_service, session: AsyncSession) -> None:
|
|||
|
||||
async def teardown_services() -> None:
|
||||
"""Teardown all the services."""
|
||||
try:
|
||||
async with get_db_service().with_session() as session:
|
||||
await teardown_superuser(get_settings_service(), session)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.exception(exc)
|
||||
try:
|
||||
from langflow.services.manager import service_manager
|
||||
async with get_db_service().with_session() as session:
|
||||
await teardown_superuser(get_settings_service(), session)
|
||||
|
||||
await service_manager.teardown()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.exception(exc)
|
||||
from langflow.services.manager import service_manager
|
||||
|
||||
await service_manager.teardown()
|
||||
|
||||
|
||||
def initialize_settings_service() -> None:
|
||||
|
|
|
|||
|
|
@ -29,10 +29,9 @@ class DatabaseVariableService(VariableService, Service):
|
|||
|
||||
async def initialize_user_variables(self, user_id: UUID | str, session: AsyncSession) -> None:
|
||||
if not self.settings_service.settings.store_environment_variables:
|
||||
logger.info("Skipping environment variable storage.")
|
||||
logger.debug("Skipping environment variable storage.")
|
||||
return
|
||||
|
||||
logger.info("Storing environment variables in the database.")
|
||||
for var_name in self.settings_service.settings.variables_to_get_from_environment:
|
||||
if var_name in os.environ and os.environ[var_name].strip():
|
||||
value = os.environ[var_name].strip()
|
||||
|
|
@ -50,7 +49,7 @@ class DatabaseVariableService(VariableService, Service):
|
|||
type_=CREDENTIAL_TYPE,
|
||||
session=session,
|
||||
)
|
||||
logger.info(f"Processed {var_name} variable from environment.")
|
||||
logger.debug(f"Processed {var_name} variable from environment.")
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.exception(f"Error processing {var_name} variable: {e!s}")
|
||||
|
||||
|
|
|
|||
|
|
@ -236,10 +236,10 @@ audio = [
|
|||
]
|
||||
|
||||
postgresql = [
|
||||
"sqlalchemy[postgresql_psycopg2binary]",
|
||||
"sqlalchemy[postgresql_psycopg]",
|
||||
|
||||
"sqlalchemy[postgresql_psycopg2binary]>=2.0.38,<3.0.0",
|
||||
"sqlalchemy[postgresql_psycopg]>=2.0.38,<3.0.0",
|
||||
]
|
||||
|
||||
local = [
|
||||
"llama-cpp-python>=0.2.0",
|
||||
"sentence-transformers>=2.0.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue