fix(api/v2): apply request tweaks on the streaming and background paths

The v2 workflows endpoint applied `tweaks` only on mode=sync. The stream
and background paths build the graph via the v1 build-vertex loop
(`generate_flow_events`), which never received the tweaks, so they were
silently dropped. The confusing symptom: a model passed via tweaks
surfaced as "A model selection is required", and any per-component
override was ignored on non-sync runs.

Thread `parsed.tweaks` into `generate_flow_events` and apply them to the
built graph via `vertex.update_raw_params`. We do not use the lfx
`process_tweaks_on_graph` helper because it only sets `vertex.params`,
which does not persist to runtime (the same bug
`lfx.base.tools.run_flow._process_tweaks_on_graph` works around). No-tweaks
runs are unchanged (guarded by `if tweaks`). Adds a streaming regression
test that overrides ChatInput via tweaks and asserts the value drives the run.
This commit is contained in:
ogabrielluiz
2026-06-16 17:35:25 -03:00
parent 98aa98df97
commit 02e06fe6f3
3 changed files with 76 additions and 0 deletions

View File

@ -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)

View File

@ -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

View File

@ -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."""