diff --git a/src/backend/base/langflow/api/build.py b/src/backend/base/langflow/api/build.py index 223363c60c..a702d94d8e 100644 --- a/src/backend/base/langflow/api/build.py +++ b/src/backend/base/langflow/api/build.py @@ -8,6 +8,7 @@ from collections.abc import AsyncIterator from fastapi import BackgroundTasks, HTTPException, Response from lfx.graph.graph.base import Graph from lfx.graph.utils import log_vertex_build +from lfx.graph.vertex.base import Vertex from lfx.log.logger import logger from lfx.schema.schema import InputValueRequest from sqlmodel import select @@ -365,6 +366,7 @@ async def generate_flow_events( source_flow_id: uuid.UUID | None = None, run_id: str | None = None, track_job_status: bool = True, + tweaks: dict | None = None, ) -> None: """Generate events for flow building process. @@ -393,6 +395,22 @@ async def generate_flow_events( async with session_scope() as fresh_session: graph = await create_graph(fresh_session, flow_id_str, flow_name) + # Apply request tweaks to the built graph. The sync path applies + # tweaks before Graph construction; the streaming/background path + # builds from the DB (or request data), so tweaks must be applied + # to the built graph here or they are silently dropped. We use + # ``update_raw_params`` rather than the lfx ``process_tweaks_on_graph`` + # helper because that helper only sets ``vertex.params`` and does not + # persist the override to runtime (mirrors the workaround in + # ``lfx.base.tools.run_flow._process_tweaks_on_graph``). + if tweaks: + for vertex in graph.vertices: + if not (isinstance(vertex, Vertex) and isinstance(vertex.id, str)): + continue + if node_tweaks := tweaks.get(vertex.id): + node_tweaks = {k: v for k, v in node_tweaks.items() if k != "code"} + vertex.update_raw_params(node_tweaks, overwrite=True) + graph.set_run_id(build_run_id) first_layer = sort_vertices(graph) diff --git a/src/backend/base/langflow/api/v2/workflow.py b/src/backend/base/langflow/api/v2/workflow.py index c5395b4ef9..1ec7be2162 100644 --- a/src/backend/base/langflow/api/v2/workflow.py +++ b/src/backend/base/langflow/api/v2/workflow.py @@ -637,6 +637,7 @@ async def _stream_event_frames( flow_name=flow_name, source_flow_id=source_flow_id, track_job_status=track_job_status, + tweaks=parsed.tweaks, ) except asyncio.CancelledError: raise diff --git a/src/backend/tests/unit/api/v2/test_workflow_agui.py b/src/backend/tests/unit/api/v2/test_workflow_agui.py index ca318c64c5..22dcb5f0b5 100644 --- a/src/backend/tests/unit/api/v2/test_workflow_agui.py +++ b/src/backend/tests/unit/api/v2/test_workflow_agui.py @@ -305,6 +305,63 @@ class TestAGUIStreaming: if flow: await session.delete(flow) + async def test_stream_applies_request_tweaks( + self, + client: AsyncClient, + created_api_key, + json_memory_chatbot_no_llm, + ): + """Request ``tweaks`` must reach the graph on the streaming path. + + Regression: the stream/background path builds the graph via the v1 + build-vertex loop (``generate_flow_events``) and previously dropped + ``tweaks`` entirely, so only ``mode=sync`` applied them. Here we + override the ChatInput's ``input_value`` via tweaks (with no top-level + input to override it) and assert the tweaked text drives the run. + Stream and background share ``generate_flow_events``, so this covers + both non-sync paths. + """ + raw = json.loads(json_memory_chatbot_no_llm) + flow_data = raw.get("data", raw) + chat_input_id = next(n["id"] for n in flow_data["nodes"] if n.get("data", {}).get("type") == "ChatInput") + flow_id = uuid4() + async with session_scope() as session: + flow = Flow( + id=flow_id, + name="AG-UI Tweaks Flow", + data=flow_data, + user_id=created_api_key.user_id, + ) + session.add(flow) + await session.flush() + + tweaked = "TWEAKED_VIA_TWEAKS_123" + try: + response = await client.post( + "api/v2/workflows", + json={ + "flow_id": str(flow_id), + "mode": "stream", + "stream_protocol": "agui", + # No top-level input_value and no session_id, so the build + # loop receives no chat-input override and the tweak is the + # only source of the ChatInput value. Without the fix the + # flow default is used and the tweaked text never appears. + "tweaks": {chat_input_id: {"input_value": tweaked}}, + }, + headers={"x-api-key": created_api_key.api_key}, + ) + + assert response.status_code == 200 + body = response.text + assert "RUN_ERROR" not in body + assert tweaked in body + finally: + async with session_scope() as session: + flow = await session.get(Flow, flow_id) + if flow: + await session.delete(flow) + class TestAGUISyncExecution: """mode=sync runs the flow inline and folds outputs into the response."""