diff --git a/src/backend/base/langflow/__main__.py b/src/backend/base/langflow/__main__.py index 4d59b3149..4162629dd 100644 --- a/src/backend/base/langflow/__main__.py +++ b/src/backend/base/langflow/__main__.py @@ -77,7 +77,7 @@ def set_var_for_macos_issue(): def run( host: str = typer.Option("127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST"), workers: int = typer.Option(1, help="Number of worker processes.", envvar="LANGFLOW_WORKERS"), - timeout: int = typer.Option(300, help="Worker timeout in seconds."), + timeout: int = typer.Option(300, help="Worker timeout in seconds.", envvar="LANGFLOW_WORKER_TIMEOUT"), port: int = typer.Option(7860, help="Port to listen on.", envvar="LANGFLOW_PORT"), components_path: Optional[Path] = typer.Option( Path(__file__).parent / "components", @@ -145,6 +145,10 @@ def run( if is_port_in_use(port, host): port = get_free_port(port) + settings_service = get_settings_service() + + settings_service.set("worker_timeout", timeout) + options = { "bind": f"{host}:{port}", "workers": get_number_of_workers(workers), diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index b47774c8b..006099b15 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Annotated, List, Optional, Union +from typing import TYPE_CHECKING, Annotated, List, Optional, Union from uuid import UUID import sqlalchemy as sa @@ -9,6 +9,7 @@ from sqlmodel import Session, select from langflow.api.utils import update_frontend_node_with_template_values from langflow.api.v1.schemas import ( + ConfigResponse, CustomComponentRequest, InputValueRequest, ProcessResponse, @@ -31,7 +32,9 @@ from langflow.services.deps import get_session, get_session_service, get_setting from langflow.services.session.service import SessionService from langflow.services.task.service import TaskService -# build router +if TYPE_CHECKING: + from langflow.services.settings.manager import SettingsService + router = APIRouter(tags=["Base"]) @@ -440,3 +443,15 @@ async def custom_component_update( except Exception as exc: logger.exception(exc) raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@router.get("/config", response_model=ConfigResponse) +def get_config(): + try: + from langflow.services.deps import get_settings_service + + settings_service: "SettingsService" = get_settings_service() + return settings_service.settings.model_dump() + except Exception as exc: + logger.exception(exc) + raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/src/backend/base/langflow/api/v1/schemas.py b/src/backend/base/langflow/api/v1/schemas.py index 2b9eff312..419c3d1f9 100644 --- a/src/backend/base/langflow/api/v1/schemas.py +++ b/src/backend/base/langflow/api/v1/schemas.py @@ -316,3 +316,7 @@ class FlowDataRequest(BaseModel): nodes: List[dict] edges: List[dict] viewport: Optional[dict] = None + + +class ConfigResponse(BaseModel): + frontend_timeout: int diff --git a/src/backend/base/langflow/services/settings/base.py b/src/backend/base/langflow/services/settings/base.py index 05a368c9b..f62ccacd4 100644 --- a/src/backend/base/langflow/services/settings/base.py +++ b/src/backend/base/langflow/services/settings/base.py @@ -104,6 +104,10 @@ class Settings(BaseSettings): """Whether to store environment variables as Global Variables in the database.""" variables_to_get_from_environment: list[str] = VARIABLES_TO_GET_FROM_ENVIRONMENT """List of environment variables to get from the environment and store in the database.""" + worker_timeout: int = 300 + """Timeout for the API calls in seconds.""" + frontend_timeout: int = 0 + """Timeout for the frontend API calls in seconds.""" @field_validator("config_dir", mode="before") def set_langflow_dir(cls, value): diff --git a/src/backend/base/langflow/services/settings/service.py b/src/backend/base/langflow/services/settings/service.py index 0d9d63bc4..f7ef2980d 100644 --- a/src/backend/base/langflow/services/settings/service.py +++ b/src/backend/base/langflow/services/settings/service.py @@ -42,3 +42,7 @@ class SettingsService(Service): CONFIG_DIR=settings.config_dir, ) return cls(settings, auth_settings) + + def set(self, key, value): + setattr(self.settings, key, value) + return self.settings diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 922ec9994..510500e2b 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1,3 +1,4 @@ +import axios from "axios"; import { useContext, useEffect, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { useNavigate } from "react-router-dom"; @@ -15,6 +16,7 @@ import { } from "./constants/constants"; import { AuthContext } from "./contexts/authContext"; import { autoLogin, getGlobalVariables, getHealth } from "./controllers/API"; +import { setupAxiosDefaults } from "./controllers/API/utils"; import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path"; import Router from "./routes"; import useAlertStore from "./stores/alertStore"; @@ -28,10 +30,10 @@ export default function App() { useTrackLastVisitedPath(); const removeFromTempNotificationList = useAlertStore( - (state) => state.removeFromTempNotificationList, + (state) => state.removeFromTempNotificationList ); const tempNotificationList = useAlertStore( - (state) => state.tempNotificationList, + (state) => state.tempNotificationList ); const [fetchError, setFetchError] = useState(false); const isLoading = useFlowsManagerStore((state) => state.isLoading); @@ -49,7 +51,7 @@ export default function App() { const refreshVersion = useDarkStore((state) => state.refreshVersion); const refreshStars = useDarkStore((state) => state.refreshStars); const setGlobalVariables = useGlobalVariablesStore( - (state) => state.setGlobalVariables, + (state) => state.setGlobalVariables ); const checkHasStore = useStoreStore((state) => state.checkHasStore); const navigate = useNavigate(); @@ -114,9 +116,11 @@ export default function App() { return new Promise(async (resolve, reject) => { if (isAuthenticated) { try { + await setupAxiosDefaults(); await getFoldersApi(); await getTypes(); await refreshFlows(); + console.log(axios.defaults); const res = await getGlobalVariables(); setGlobalVariables(res); checkHasStore(); diff --git a/src/frontend/src/controllers/API/api.tsx b/src/frontend/src/controllers/API/api.tsx index 363576ae0..d630c474f 100644 --- a/src/frontend/src/controllers/API/api.tsx +++ b/src/frontend/src/controllers/API/api.tsx @@ -48,7 +48,7 @@ function ApiInterceptor() { } await clearBuildVerticesState(error); return Promise.reject(error); - }, + } ); const isAuthorizedURL = (url) => { @@ -65,10 +65,10 @@ function ApiInterceptor() { const parsedURL = new URL(url); const isDomainAllowed = authorizedDomains.some( - (domain) => parsedURL.origin === new URL(domain).origin, + (domain) => parsedURL.origin === new URL(domain).origin ); const isEndpointAllowed = authorizedEndpoints.some((endpoint) => - parsedURL.pathname.includes(endpoint), + parsedURL.pathname.includes(endpoint) ); return isDomainAllowed || isEndpointAllowed; @@ -112,7 +112,7 @@ function ApiInterceptor() { }, (error) => { return Promise.reject(error); - }, + } ); return () => { @@ -144,7 +144,7 @@ function ApiInterceptor() { if (error?.config?.headers) { delete error.config.headers["Authorization"]; error.config.headers["Authorization"] = `Bearer ${cookies.get( - "access_token_lf", + "access_token_lf" )}`; const response = await axios.request(error.config); return response; diff --git a/src/frontend/src/controllers/API/utils.tsx b/src/frontend/src/controllers/API/utils.tsx new file mode 100644 index 000000000..3991caa2e --- /dev/null +++ b/src/frontend/src/controllers/API/utils.tsx @@ -0,0 +1,32 @@ +import axios from "axios"; +import { BASE_URL_API } from "../../constants/constants"; + +/** + * Fetches the configuration data from the API. + * @returns {Promise} A promise that resolves to the configuration data. + * @throws {Error} If there was an error fetching the configuration data. + */ +export async function fetchConfig() { + try { + const response = await axios.get(`${BASE_URL_API}config`); + return response.data; + } catch (error) { + console.error("Failed to fetch configuration:", error); + throw error; + } +} + +/** + * Sets up default configurations for Axios. + * Fetches the timeout configuration and sets it as the default timeout for Axios requests. + */ +export async function setupAxiosDefaults() { + const config = await fetchConfig(); + // Create Axios instance with the fetched timeout configuration + + const timeoutInMilliseconds = config.frontend_timeout + ? config.frontend_timeout * 1000 + : 30000; + axios.defaults.baseURL = ""; + axios.defaults.timeout = timeoutInMilliseconds; +} diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 1203aab65..cbf941fbb 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -271,7 +271,7 @@ export default function NodeToolbarComponent({ selected && (hasApiKey || hasStore) && (event.ctrlKey || event.metaKey) && - event.key === "u" + event.key.toUpperCase() === "U" ) { event.preventDefault(); handleSelectChange("update"); @@ -280,7 +280,7 @@ export default function NodeToolbarComponent({ selected && isGroup && (event.ctrlKey || event.metaKey) && - event.key === "g" + event.key.toUpperCase() === "G" ) { event.preventDefault(); handleSelectChange("ungroup"); @@ -290,7 +290,7 @@ export default function NodeToolbarComponent({ (hasApiKey || hasStore) && (event.ctrlKey || event.metaKey) && event.shiftKey && - event.key === "S" + event.key.toUpperCase() === "S" ) { event.preventDefault(); setShowconfirmShare((state) => !state); @@ -300,7 +300,7 @@ export default function NodeToolbarComponent({ selected && (event.ctrlKey || event.metaKey) && event.shiftKey && - event.key === "Q" + event.key.toUpperCase() === "Q" ) { event.preventDefault(); if (isMinimal) { @@ -317,7 +317,7 @@ export default function NodeToolbarComponent({ selected && (event.ctrlKey || event.metaKey) && event.shiftKey && - event.key === "U" + event.key.toUpperCase() === "U" ) { event.preventDefault(); if (hasCode) return setOpenModal((state) => !state); @@ -327,12 +327,16 @@ export default function NodeToolbarComponent({ selected && (event.ctrlKey || event.metaKey) && event.shiftKey && - event.key === "A" + event.key.toUpperCase() === "A" ) { event.preventDefault(); setShowModalAdvanced((state) => !state); } - if (selected && (event.ctrlKey || event.metaKey) && event.key === "s") { + if ( + selected && + (event.ctrlKey || event.metaKey) && + event.key.toUpperCase() === "S" + ) { if (isSaved) { event.preventDefault(); return setShowOverrideModal((state) => !state); @@ -347,7 +351,7 @@ export default function NodeToolbarComponent({ selected && (event.ctrlKey || event.metaKey) && event.shiftKey && - event.key === "D" + event.key.toUpperCase() === "D" ) { event.preventDefault(); if (data.node?.documentation) { @@ -357,7 +361,11 @@ export default function NodeToolbarComponent({ title: `${data.id} docs is not available at the moment.`, }); } - if (selected && (event.ctrlKey || event.metaKey) && event.key === "j") { + if ( + selected && + (event.ctrlKey || event.metaKey) && + event.key.toUpperCase() === "J" + ) { event.preventDefault(); downloadNode(flowComponent!); }