Skip to content

Commit 892b25e

Browse files
committed
feat: optimize dropdown filtering and output resolution
misc: remove commented out code feat: add refresh button and sort flows by updated_at date from most to least recent ruff (flow.py imports) improve fn contracts in runflow and improve flow id retrieval logic based on graph exec context add dynamic outputs and optimize db lookups add flow cache and db query for getting a single flow by id or name cache run outputs and add refresh context to build config misc misc use ids for flow retrieval misc fix missing flow_id bug add unit and integration tests add input field flag to persist hidden fields at runtime move unit tests and change input and output display names chore: update component index fix: fix tool mode when flow has multiple inputs by dynamically creating resolvers chore: update component index ruff (run_flow and tests) add resolvers to outputs map for non tool mode runtime fix tests (current flow excluded in db fetch) mypy (helpers/flow.py) chore: update component index remove unused code and clean up comments fix: persist user messages in chat-based flows via session injection chore: update component index empty string fallback for sessionid in chat.py chore: update component index chore: update component index cache invalidation with timestamps misc add cache invalidation chore: update component index chore: update comp idx ruff (run_flow.py) change session_id input type to MessageTextInput chore: update component index chore: update component index chore: update component index
1 parent c74502e commit 892b25e

52 files changed

Lines changed: 3467 additions & 250 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/backend/base/langflow/helpers/flow.py

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from fastapi import HTTPException
77
from lfx.log.logger import logger
88
from pydantic.v1 import BaseModel, Field, create_model
9-
from sqlmodel import select
9+
from sqlalchemy.orm import aliased
10+
from sqlmodel import asc, desc, select
1011

1112
from langflow.schema.schema import INPUT_FIELD_NAME
1213
from langflow.services.database.models.flow.model import Flow, FlowRead
@@ -19,14 +20,17 @@
1920
from lfx.graph.schema import RunOutputs
2021
from lfx.graph.vertex.base import Vertex
2122

22-
from langflow.schema.data import Data
23+
from langflow.schema.data import Data
2324

2425
INPUT_TYPE_MAP = {
2526
"ChatInput": {"type_hint": "Optional[str]", "default": '""'},
2627
"TextInput": {"type_hint": "Optional[str]", "default": '""'},
2728
"JSONInput": {"type_hint": "Optional[dict]", "default": "{}"},
2829
}
29-
30+
SORT_DISPATCHER = {
31+
"asc": asc,
32+
"desc": desc,
33+
}
3034

3135
async def list_flows(*, user_id: str | None = None) -> list[Data]:
3236
if not user_id:
@@ -44,6 +48,122 @@ async def list_flows(*, user_id: str | None = None) -> list[Data]:
4448
raise ValueError(msg) from e
4549

4650

51+
async def list_flows_by_flow_folder(
52+
*,
53+
user_id: str | None = None,
54+
flow_id: str | None = None,
55+
order_params: dict | None = {"column": "updated_at", "direction": "desc"} # noqa: B006
56+
) -> list[Data]:
57+
if not user_id:
58+
msg = "Session is invalid"
59+
raise ValueError(msg)
60+
if not flow_id:
61+
msg = "Flow ID is required"
62+
raise ValueError(msg)
63+
try:
64+
async with session_scope() as session:
65+
uuid_user_id = UUID(user_id) if isinstance(user_id, str) else user_id
66+
uuid_flow_id = UUID(flow_id) if isinstance(flow_id, str) else flow_id
67+
# get all flows belonging to the specified user
68+
# and inside the same folder as the specified flow
69+
flow_ = aliased(Flow) # flow table alias, used to retrieve the folder
70+
stmt = (select(Flow.id, Flow.name, Flow.updated_at)
71+
.join(flow_, Flow.folder_id == flow_.folder_id)
72+
.where(flow_.id == uuid_flow_id)
73+
.where(flow_.user_id == uuid_user_id)
74+
.where(Flow.user_id == uuid_user_id)
75+
.where(Flow.id != uuid_flow_id)
76+
)
77+
# sort flows by the specified column and direction
78+
if order_params is not None:
79+
sort_col = getattr(Flow, order_params.get("column", "updated_at"), Flow.updated_at)
80+
sort_dir = SORT_DISPATCHER.get(order_params.get("direction", "desc"), desc)
81+
stmt = stmt.order_by(sort_dir(sort_col))
82+
83+
flows = (await session.exec(stmt)).all()
84+
return [Data(data=dict(flow._mapping)) for flow in flows] # noqa: SLF001
85+
except Exception as e:
86+
msg = f"Error listing flows: {e}"
87+
raise ValueError(msg) from e
88+
89+
90+
async def list_flows_by_folder_id(
91+
*,
92+
user_id: str | None = None,
93+
folder_id: str | None = None,
94+
order_params: dict | None = {"column": "updated_at", "direction": "desc"} # noqa: B006
95+
) -> list[Data]:
96+
if not user_id:
97+
msg = "Session is invalid"
98+
raise ValueError(msg)
99+
if not folder_id:
100+
msg = "Folder ID is required"
101+
raise ValueError(msg)
102+
103+
try:
104+
async with session_scope() as session:
105+
uuid_user_id = UUID(user_id) if isinstance(user_id, str) else user_id
106+
uuid_folder_id = UUID(folder_id) if isinstance(folder_id, str) else folder_id
107+
stmt = (select(Flow.id, Flow.name, Flow.updated_at)
108+
.where(Flow.user_id == uuid_user_id)
109+
.where(Flow.folder_id == uuid_folder_id)
110+
)
111+
if order_params is not None:
112+
sort_col = getattr(Flow, order_params.get("column", "updated_at"), Flow.updated_at)
113+
sort_dir = SORT_DISPATCHER.get(order_params.get("direction", "desc"), desc)
114+
stmt = stmt.order_by(sort_dir(sort_col))
115+
116+
flows = (await session.exec(stmt)).all()
117+
return [Data(data=dict(flow._mapping)) for flow in flows] # noqa: SLF001
118+
except Exception as e:
119+
msg = f"Error listing flows: {e}"
120+
raise ValueError(msg) from e
121+
122+
123+
async def get_flow_by_id_or_name(
124+
*,
125+
user_id: str | None = None,
126+
flow_id: str | None = None,
127+
flow_name: str | None = None,
128+
) -> Data | None:
129+
if not user_id:
130+
msg = "Session is invalid"
131+
raise ValueError(msg)
132+
if not (flow_id or flow_name):
133+
msg = "Flow ID or Flow Name is required"
134+
raise ValueError(msg)
135+
136+
# set user provided flow id or flow name.
137+
# if both are provided, flow_id is used.
138+
attr, val = None, None
139+
if flow_name:
140+
attr = "name"
141+
val = flow_name
142+
if flow_id:
143+
attr = "id"
144+
val = flow_id
145+
if not (attr and val):
146+
msg = "Flow id or Name is required"
147+
raise ValueError(msg)
148+
try:
149+
async with session_scope() as session:
150+
uuid_user_id = UUID(user_id) if isinstance(user_id, str) else user_id # type: ignore[assignment]
151+
uuid_flow_id_or_name = val # type: ignore[assignment]
152+
if isinstance(val, str) and attr == "id":
153+
uuid_flow_id_or_name = UUID(val) # type: ignore[assignment]
154+
stmt = (
155+
select(Flow)
156+
.where(Flow.user_id == uuid_user_id)
157+
.where(getattr(Flow, attr) == uuid_flow_id_or_name)
158+
)
159+
flow = (await session.exec(stmt)).first()
160+
return flow.to_data() if flow else None
161+
162+
except Exception as e:
163+
msg = f"Error getting flow by id: {e}"
164+
raise ValueError(msg) from e
165+
166+
47167
async def load_flow(
48168
user_id: str, flow_id: str | None = None, flow_name: str | None = None, tweaks: dict | None = None
49169
) -> Graph:

src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@
362362
"legacy": false,
363363
"lf_version": "1.5.0",
364364
"metadata": {
365-
"code_hash": "0014a5b41817",
365+
"code_hash": "edef7741db98",
366366
"dependencies": {
367367
"dependencies": [
368368
{
@@ -410,7 +410,7 @@
410410
"show": true,
411411
"title_case": false,
412412
"type": "code",
413-
"value": "from lfx.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.inputs.inputs import BoolInput\nfrom lfx.io import (\n DropdownInput,\n FileInput,\n MessageTextInput,\n MultilineInput,\n Output,\n)\nfrom lfx.schema.message import Message\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_USER,\n MESSAGE_SENDER_USER,\n)\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n documentation: str = \"https://docs.langflow.org/components-io#chat-input\"\n icon = \"MessagesSquare\"\n name = \"ChatInput\"\n minimized = True\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n input_types=[],\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_USER,\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_USER,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n temp_file=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Chat Message\", name=\"message\", method=\"message_response\"),\n ]\n\n async def message_response(self) -> Message:\n # Ensure files is a list and filter out empty/None values\n files = self.files if self.files else []\n if files and not isinstance(files, list):\n files = [files]\n # Filter out None/empty values\n files = [f for f in files if f is not None and f != \"\"]\n\n message = await Message.create(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=self.session_id,\n context_id=self.context_id,\n files=files,\n )\n if self.session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = await self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n"
413+
"value": "from lfx.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.inputs.inputs import BoolInput\nfrom lfx.io import (\n DropdownInput,\n FileInput,\n MessageTextInput,\n MultilineInput,\n Output,\n)\nfrom lfx.schema.message import Message\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_USER,\n MESSAGE_SENDER_USER,\n)\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n documentation: str = \"https://docs.langflow.org/components-io#chat-input\"\n icon = \"MessagesSquare\"\n name = \"ChatInput\"\n minimized = True\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n input_types=[],\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_USER,\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_USER,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n temp_file=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Chat Message\", name=\"message\", method=\"message_response\"),\n ]\n\n async def message_response(self) -> Message:\n # Ensure files is a list and filter out empty/None values\n files = self.files if self.files else []\n if files and not isinstance(files, list):\n files = [files]\n # Filter out None/empty values\n files = [f for f in files if f is not None and f != \"\"]\n\n session_id = self.session_id or self.graph.session_id or \"\"\n message = await Message.create(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=session_id,\n context_id=self.context_id,\n files=files,\n )\n if session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = await self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n"
414414
},
415415
"context_id": {
416416
"_input_type": "MessageTextInput",

0 commit comments

Comments
 (0)