diff --git a/Makefile b/Makefile index b66018897..9412752d4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,11 @@ .PHONY: all init format lint build build_frontend install_frontend run_frontend run_backend dev help tests coverage all: help +log_level ?= debug +host ?= 0.0.0.0 +port ?= 7860 +env ?= .env +open_browser ?= true setup_poetry: pipx install poetry @@ -69,17 +74,16 @@ endif run_cli: @echo 'Running the CLI' @make install_frontend > /dev/null - @echo 'Building the frontend' - @make build_frontend > /dev/null @echo 'Install backend dependencies' @make install_backend > /dev/null + @echo 'Building the frontend' + @make build_frontend > /dev/null ifdef env - poetry run langflow run --path src/frontend/build --host $(host) --port $(port) --env-file $(env) + @make start env=$(env) host=$(host) port=$(port) log_level=$(log_level) else - poetry run langflow run --path src/frontend/build --host $(host) --port $(port) --env-file .env + @make start host=$(host) port=$(port) log_level=$(log_level) endif - run_cli_debug: @echo 'Running the CLI in debug mode' @make install_frontend > /dev/null @@ -88,11 +92,22 @@ run_cli_debug: @echo 'Install backend dependencies' @make install_backend > /dev/null ifdef env - poetry run langflow run --path src/frontend/build --log-level debug --host $(host) --port $(port) --env-file $(env) + @make start env=$(env) host=$(host) port=$(port) log_level=debug else - poetry run langflow run --path src/frontend/build --log-level debug --host $(host) --port $(port) --env-file .env + @make start host=$(host) port=$(port) log_level=debug endif +start: + @echo 'Running the CLI' + @make install_backend +ifeq ($(open_browser),false) + poetry run langflow run --path src/frontend/build --log-level $(log_level) --host $(host) --port $(port) --env-file $(env) --no-open-browser +else + poetry run langflow run --path src/frontend/build --log-level $(log_level) --host $(host) --port $(port) --env-file $(env) +endif + + + setup_devcontainer: make init make build_frontend @@ -123,7 +138,7 @@ ifeq ($(login),1) poetry run uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --env-file .env --loop asyncio else @echo "Running backend with autologin"; - LANGFLOW_AUTO_LOGIN=True poetry run uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --env-file .env --loop asyncio + LANGFLOW_AUTO_LOGIN=True poetry run uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --env-file .env --loop asyncio endif build_and_run: diff --git a/docs/docs/getting-started/hugging-face-spaces.mdx b/docs/docs/getting-started/hugging-face-spaces.mdx index 4759ea398..dfb377e7f 100644 --- a/docs/docs/getting-started/hugging-face-spaces.mdx +++ b/docs/docs/getting-started/hugging-face-spaces.mdx @@ -1,20 +1,29 @@ # 🤗 HuggingFace Spaces -A fully featured version of Langflow can be accessed via HuggingFace spaces with no installation required. +TLDR; +A fully featured version of Langflow can be accessed via [HuggingFace Spaces](https://huggingface.co/spaces/Logspace/Langflow?duplicate=true) with no installation required. All you gotta do is [duplicate the Space](https://huggingface.co/spaces/Logspace/Langflow?duplicate=true) and you'll have your own copy to play around with! -import ThemedImage from "@theme/ThemedImage"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import ZoomableImage from "/src/theme/ZoomableImage.js"; +--- -{" "} +# 🚀 Getting Started + +HuggingFace provides great support for running Langflow in their Spaces environment. This means you can run Langflow without any installation required. + +The first step is to go to the [Langflow Space](https://huggingface.co/spaces/Logspace/Langflow?duplicate=true). + +You'll be greeted with the following screen: -Check out Langflow on [HuggingFace Spaces](https://huggingface.co/spaces/Logspace/Langflow). +From here, you can rename your Space, define the visibility (Public or Private) and click on the `Duplicate Space` button to start the duplication process. + +import ThemedImage from "@theme/ThemedImage"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import ZoomableImage from "/src/theme/ZoomableImage.js"; diff --git a/docs/static/img/duplicate-space.png b/docs/static/img/duplicate-space.png new file mode 100644 index 000000000..2b342bbcd Binary files /dev/null and b/docs/static/img/duplicate-space.png differ diff --git a/src/backend/base/langflow/__main__.py b/src/backend/base/langflow/__main__.py index c3fb5bd37..6841ae1d1 100644 --- a/src/backend/base/langflow/__main__.py +++ b/src/backend/base/langflow/__main__.py @@ -290,6 +290,7 @@ def run_langflow(host, port, log_level, options, app): host=host, port=port, log_level=log_level.lower(), + loop="asyncio", ) else: from langflow.server import LangflowApplication diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index 5ec169bf3..cdcf40443 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -23,7 +23,7 @@ from langflow.graph.schema import RunOutputs from langflow.interface.custom.custom_component import CustomComponent from langflow.interface.custom.directory_reader import DirectoryReader from langflow.interface.custom.utils import build_custom_component_template -from langflow.processing.process import process_tweaks, run_graph +from langflow.processing.process import process_tweaks, run_graph_internal from langflow.services.auth.utils import api_key_security, get_current_active_user from langflow.services.cache.utils import save_uploaded_file from langflow.services.database.models.flow import Flow @@ -52,7 +52,7 @@ def get_all( @router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True) -async def simplified_run_flow_with_caching( +async def simplified_run_flow( db: Annotated[Session, Depends(get_session)], flow_id: str, input_request: SimplifiedAPIRequest = SimplifiedAPIRequest(), @@ -66,15 +66,24 @@ async def simplified_run_flow_with_caching( ### Parameters: - `db` (Session): Database session for executing queries. - `flow_id` (str): Unique identifier of the flow to be executed. - - `input_request` (SimplifiedAPIRequest): A request model containing: - - `input_value` (Optional[str], default=""): Input value to pass to the flow. - - `input_type` (Optional[Literal["chat", "text", "any"]], default="chat"): Type of the input value, determining how the input is interpreted. - - `output_type` (Optional[Literal["chat", "text", "any", "debug"]], default="chat"): Desired type of output, affecting which components' outputs are included in the response. - - `tweaks` (Optional[Tweaks], default=None): Adjustments to the flow's behavior, allowing for custom execution parameters. - - `session_id` (Optional[str], default=None): An identifier for reusing session data, aiding in performance for subsequent requests. + - `input_request` (SimplifiedAPIRequest): Request object containing input values, types, output selection, tweaks, and session ID. - `api_key_user` (User): User object derived from the provided API key, used for authentication. - `session_service` (SessionService): Service for managing flow sessions, essential for session reuse and caching. + ### SimplifiedAPIRequest: + - `input_value` (Optional[str], default=""): Input value to pass to the flow. + - `input_type` (Optional[Literal["chat", "text", "any"]], default="chat"): Type of the input value, determining how the input is interpreted. + - `output_type` (Optional[Literal["chat", "text", "any", "debug"]], default="chat"): Desired type of output, affecting which components' outputs are included in the response. If set to "debug", all outputs are returned. + - `output_component` (Optional[str], default=None): Specific component output to retrieve. If provided, only the output of the specified component is returned. This overrides the `output_type` parameter. + - `tweaks` (Optional[Tweaks], default=None): Adjustments to the flow's behavior, allowing for custom execution parameters. + - `session_id` (Optional[str], default=None): An identifier for reusing session data, aiding in performance for subsequent requests. + + + ### Tweaks + A dictionary of tweaks to customize the flow execution. The tweaks can be used to modify the flow's parameters and components. Tweaks can be overridden by the input values. + You can use Component's `id` or Display Name as key to tweak a specific component (e.g., `{"Component Name": {"parameter_name": "value"}}`). + You can also use the parameter name as key to tweak all components with that parameter (e.g., `{"parameter_name": "value"}`). + ### Returns: - A `RunResponse` object containing the execution results, including selected (or all, based on `output_type`) outputs of the flow and the session ID, facilitating result retrieval and further interactions in a session context. @@ -128,16 +137,19 @@ async def simplified_run_flow_with_caching( # if the output type is debug, we return all outputs # if the output type is any, we return all outputs that are either chat or text # if the output type is chat or text, we return only the outputs that match the type - outputs = [ - vertex.id - for vertex in graph.vertices - if input_request.output_type == "debug" - or ( - vertex.is_output - and (input_request.output_type == "any" or input_request.output_type in vertex.id.lower()) - ) - ] - task_result, session_id = await run_graph( + if input_request.output_component: + outputs = [input_request.output_component] + else: + outputs = [ + vertex.id + for vertex in graph.vertices + if input_request.output_type == "debug" + or ( + vertex.is_output + and (input_request.output_type == "any" or input_request.output_type in vertex.id.lower()) + ) + ] + task_result, session_id = await run_graph_internal( graph=graph, flow_id=flow_id, session_id=input_request.session_id, @@ -171,7 +183,7 @@ async def simplified_run_flow_with_caching( @router.post("/run/advanced/{flow_id}", response_model=RunResponse, response_model_exclude_none=True) -async def experimental_run_flow_with_caching( +async def experimental_run_flow( session: Annotated[Session, Depends(get_session)], flow_id: str, inputs: Optional[List[InputValueRequest]] = [InputValueRequest(components=[], input_value="")], @@ -243,7 +255,7 @@ async def experimental_run_flow_with_caching( graph_data = flow.data graph_data = process_tweaks(graph_data, tweaks or {}) graph = Graph.from_payload(graph_data, flow_id=flow_id) - task_result, session_id = await run_graph( + task_result, session_id = await run_graph_internal( graph=graph, flow_id=flow_id, session_id=session_id, diff --git a/src/backend/base/langflow/api/v1/schemas.py b/src/backend/base/langflow/api/v1/schemas.py index bd6adb3a6..631c37fbb 100644 --- a/src/backend/base/langflow/api/v1/schemas.py +++ b/src/backend/base/langflow/api/v1/schemas.py @@ -1,7 +1,7 @@ from datetime import datetime from enum import Enum from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, List, Optional, Union from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator, model_serializer @@ -9,7 +9,6 @@ from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator, m from langflow.graph.schema import RunOutputs from langflow.schema import dotdict from langflow.schema.schema import InputType, OutputType -from langflow.schema.schema import InputType, OutputType from langflow.services.database.models.api_key.model import ApiKeyRead from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.flow import FlowCreate, FlowRead @@ -318,5 +317,9 @@ class SimplifiedAPIRequest(BaseModel): input_value: Optional[str] = Field(default="", description="The input value") input_type: Optional[InputType] = Field(default="chat", description="The input type") output_type: Optional[OutputType] = Field(default="chat", description="The output type") + output_component: Optional[str] = Field( + default="", + description="If there are multiple output components, you can specify the component to get the output from.", + ) tweaks: Optional[Tweaks] = Field(default=None, description="The tweaks") session_id: Optional[str] = Field(default=None, description="The session id") diff --git a/src/backend/base/langflow/processing/load.py b/src/backend/base/langflow/processing/load.py index 562a37d08..43d9ecc78 100644 --- a/src/backend/base/langflow/processing/load.py +++ b/src/backend/base/langflow/processing/load.py @@ -1,9 +1,10 @@ import json from pathlib import Path -from typing import Optional, Union +from typing import List, Optional, Union from langflow.graph import Graph -from langflow.processing.process import process_tweaks +from langflow.graph.schema import RunOutputs +from langflow.processing.process import process_tweaks, run_graph def load_flow_from_json(flow: Union[Path, str, dict], tweaks: Optional[dict] = None) -> Graph: @@ -31,3 +32,36 @@ def load_flow_from_json(flow: Union[Path, str, dict], tweaks: Optional[dict] = N graph = Graph.from_payload(graph_data) return graph + + +def run_flow_from_json( + flow: Union[Path, str, dict], + input_value: str, + tweaks: Optional[dict] = None, + input_type: str = "chat", + output_type: str = "chat", + output_component: Optional[str] = None, +) -> List[RunOutputs]: + """ + Runs a JSON flow by loading it from a file or dictionary and executing it with the given input value. + + Args: + flow (Union[Path, str, dict]): The path to the JSON file, or the JSON dictionary representing the flow. + input_value (str): The input value to be processed by the flow. + tweaks (Optional[dict], optional): Optional tweaks to be applied to the flow. Defaults to None. + input_type (str, optional): The type of the input value. Defaults to "chat". + output_type (str, optional): The type of the output value. Defaults to "chat". + output_component (Optional[str], optional): The specific output component to retrieve. Defaults to None. + + Returns: + None: The result of running the flow. + """ + graph = load_flow_from_json(flow, tweaks) + result = run_graph( + graph=graph, + input_value=input_value, + input_type=input_type, + output_type=output_type, + output_component=output_component, + ) + return result diff --git a/src/backend/base/langflow/processing/process.py b/src/backend/base/langflow/processing/process.py index c7e496dd7..c05163933 100644 --- a/src/backend/base/langflow/processing/process.py +++ b/src/backend/base/langflow/processing/process.py @@ -124,10 +124,10 @@ class Result(BaseModel): session_id: str -async def run_graph( +async def run_graph_internal( graph: "Graph", flow_id: str, - stream: bool, + stream: bool = False, session_id: Optional[str] = None, inputs: Optional[List["InputValueRequest"]] = None, outputs: Optional[List[str]] = None, @@ -167,6 +167,58 @@ async def run_graph( return run_outputs, session_id_str +def run_graph( + graph: "Graph", + input_value: str, + input_type: str, + output_type: str, + output_component: Optional[str] = None, +) -> List[RunOutputs]: + """ + Runs the given Langflow Graph with the specified input and returns the outputs. + + Args: + graph (Graph): The graph to be executed. + input_value (str): The input value to be passed to the graph. + input_type (str): The type of the input value. + output_type (str): The type of the desired output. + output_component (Optional[str], optional): The specific output component to retrieve. Defaults to None. + + Returns: + List[RunOutputs]: A list of RunOutputs objects representing the outputs of the graph. + + """ + inputs = [InputValueRequest(components=[], input_value=input_value, type=input_type)] + if output_component: + outputs = [output_component] + else: + outputs = [ + vertex.id + for vertex in graph.vertices + if output_type == "debug" + or (vertex.is_output and (output_type == "any" or output_type in vertex.id.lower())) + ] + components = [] + inputs_list = [] + types = [] + for input_value_request in inputs: + if input_value_request.input_value is None: + logger.warning("InputValueRequest input_value cannot be None, defaulting to an empty string.") + input_value_request.input_value = "" + components.append(input_value_request.components or []) + inputs_list.append({INPUT_FIELD_NAME: input_value_request.input_value}) + types.append(input_value_request.type) + run_outputs = graph.run( + inputs_list, + components, + types, + outputs or [], + stream=False, + session_id="", + ) + return run_outputs + + def validate_input( graph_data: Dict[str, Any], tweaks: Union["Tweaks", Dict[str, Dict[str, Any]]] ) -> List[Dict[str, Any]]: diff --git a/src/backend/base/langflow/server.py b/src/backend/base/langflow/server.py index 9fe432744..8451a9d08 100644 --- a/src/backend/base/langflow/server.py +++ b/src/backend/base/langflow/server.py @@ -1,11 +1,16 @@ from gunicorn.app.base import BaseApplication # type: ignore +from uvicorn.workers import UvicornWorker + + +class LangflowUvicornWorker(UvicornWorker): + CONFIG_KWARGS = {"loop": "asyncio"} class LangflowApplication(BaseApplication): def __init__(self, app, options=None): self.options = options or {} - self.options["worker_class"] = "uvicorn.workers.UvicornWorker" + self.options["worker_class"] = "langflow.server.LangflowUvicornWorker" self.application = app super().__init__() diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 28d28fa90..9ac30a900 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -289,11 +289,10 @@ export function buildTweakObject(tweak: tweakType) { /** * Function to get Chat Input Field - * @param {FlowType} flow - The current flow. * @param {FlowsState} tabsState - The current tabs state. * @returns {string} - The chat input field */ -export function getChatInputField(flow: FlowType, flowState?: FlowState) { +export function getChatInputField(flowState?: FlowState) { let chat_input_field = "text"; if (flowState && flowState.input_keys) { @@ -305,13 +304,14 @@ export function getChatInputField(flow: FlowType, flowState?: FlowState) { /** * Function to get the python code for the API * @param {string} flowId - The id of the flow + * @param {boolean} isAuth - If the API is authenticated + * @param {any[]} tweak - The tweaks * @returns {string} - The python code */ export function getPythonApiCode( flow: FlowType, isAuth: boolean, - tweak?: any[], - flowState?: FlowState + tweak?: any[] ): string { const flowId = flow.id; @@ -320,13 +320,10 @@ export function getPythonApiCode( // node.data.id // } const tweaks = buildTweaks(flow); - const inputs = buildInputs(); return `import requests from typing import Optional -BASE_API_URL = "${window.location.protocol}//${ - window.location.host - }/api/v1/process" +BASE_API_URL = "${window.location.protocol}//${window.location.host}/api/v1/run" FLOW_ID = "${flowId}" # You can tweak the flow by adding a tweaks dictionary # e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}} @@ -336,9 +333,12 @@ TWEAKS = ${ : JSON.stringify(tweaks, null, 2) } -def run_flow(inputs: dict, flow_id: str, tweaks: Optional[dict] = None${ - !isAuth ? `, api_key: Optional[str] = None` : "" - }) -> dict: +def run_flow(message: str, + flow_id: str, + output_type: str = "chat", + input_type: str = "chat", + tweaks: Optional[dict] = None, + api_key: Optional[str] = None) -> dict: """ Run a flow with a given message and optional tweaks. @@ -349,7 +349,11 @@ def run_flow(inputs: dict, flow_id: str, tweaks: Optional[dict] = None${ """ api_url = f"{BASE_API_URL}/{flow_id}" - payload = {"inputs": inputs} + payload = { + "input_value": message, + "output_type": output_type, + "input_type": input_type, + } headers = None if tweaks: payload["tweaks"] = tweaks @@ -359,9 +363,9 @@ def run_flow(inputs: dict, flow_id: str, tweaks: Optional[dict] = None${ return response.json() # Setup any tweaks you want to apply to the flow -inputs = ${inputs} +message = "message" ${!isAuth ? `api_key = ""` : ""} -print(run_flow(inputs, flow_id=FLOW_ID, tweaks=TWEAKS${ +print(run_flow(message=message, flow_id=FLOW_ID, tweaks=TWEAKS${ !isAuth ? `, api_key=api_key` : "" }))`; } @@ -369,28 +373,27 @@ print(run_flow(inputs, flow_id=FLOW_ID, tweaks=TWEAKS${ /** * Function to get the curl code for the API * @param {string} flowId - The id of the flow + * @param {boolean} isAuth - If the API is authenticated * @returns {string} - The curl code */ export function getCurlCode( flow: FlowType, isAuth: boolean, - tweak?: any[], - flowState?: FlowState + tweak?: any[] ): string { const flowId = flow.id; const tweaks = buildTweaks(flow); - const inputs = buildInputs(); - - const arrayOfOutputs = getOutputIds(flow); return `curl -X POST \\ - ${window.location.protocol}//${window.location.host}/api/v1/run/${flowId} \\ + ${window.location.protocol}//${ + window.location.host + }/api/v1/run/${flowId}?stream=false \\ -H 'Content-Type: application/json'\\${ !isAuth ? `\n -H 'x-api-key: '\\` : "" } - -d '{"inputs": [${inputs}], - "outputs": [${arrayOfOutputs}], - "stream": false, + -d '{"input_value": "message", + "output_type": "chat", + "input_type": "chat", "tweaks": ${ tweak && tweak.length > 0 ? buildTweakObject(tweak) @@ -419,26 +422,23 @@ export function getOutputIds(flow) { /** * Function to get the python code for the API * @param {string} flow - The current flow + * @param {any[]} tweak - The tweaks * @returns {string} - The python code */ -export function getPythonCode( - flow: FlowType, - tweak?: any[], - flowState?: FlowState -): string { +export function getPythonCode(flow: FlowType, tweak?: any[]): string { const flowName = flow.name; const tweaks = buildTweaks(flow); - const inputs = buildInputs(); - return `from langflow.load import load_flow_from_json + + return `from langflow.load import run_flow_from_json TWEAKS = ${ tweak && tweak.length > 0 ? buildTweakObject(tweak) : JSON.stringify(tweaks, null, 2) } -flow = load_flow_from_json("${flowName}.json", tweaks=TWEAKS) -# Now you can use it like any chain -inputs = ${inputs} -flow(inputs)`; + +result = run_flow_from_json(flow="${flowName}.json", + input_value="message", + tweaks=TWEAKS)`; } /** @@ -454,7 +454,7 @@ export function getWidgetCode( const flowId = flow.id; const flowName = flow.name; const inputs = buildInputs(); - let chat_input_field = getChatInputField(flow, flowState); + let chat_input_field = getChatInputField(flowState); return `