mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-12 06:35:44 +08:00
fix(agent): clear component inputs on canvas re-run (#16790)
### What problem does this PR solve? Issue [#16758](https://github.com/infiniflow/ragflow/issues/16758) — clicking a chunk whose data references a single-line variable from an Await-Response (UserFillUp) component, the Agent's `user_prompt` is being resolved against the **previous** canvas run's captured value instead of the current run's value. The system-prompt path works only because the system prompt is computed upstream and re-reads the value on the new run. ### Root cause `Canvas._run_impl` reset every path component with `only_output=True`, so `_param.inputs` was never cleared between runs. `ComponentBase.get_input()` calls `set_input_value(var, resolved)` at line 482, which writes the resolved variable into `self._param.inputs[var]["value"]`. On the next canvas run, that input was never cleared, so the previous run's resolved value stuck around. The Agent's `kwargs.get("user_prompt")` then read the stale string and forwarded it to the LLM, which produced the "Understood. Please provide the text..." fallback because the prompt looked empty. ### What changed? - `agent/canvas.py` — differentiate `begin` (still `only_output=True`, since it has no inputs and the webhook payload branch below populates `request` explicitly) from non-begin path components (reset with `only_output=False`, which clears both `inputs` and `outputs`). - `test/unit_test/agent/test_canvas_input_reset.py` — new pytest module. Pinned the contract: non-begin path components receive `only_output=False`. The fix is small enough to verify with a stub canvas rather than a full canvas-runtime test (the existing agent conftest hits an unrelated `scholarly` import on Python 3.13, so a real canvas import would require fixing that first). ### Backward compatibility - `Begin` behaviour unchanged. - All non-begin path components: previously persisted inputs across runs (the bug); now reset between runs. Components that were relying on stale inputs (none found in the existing test suite) would lose that as a side effect, but that is the entire point of the fix. - No API surface change. No backend change. ### Testing ``` $ uv run pytest test/unit_test/agent/test_canvas_input_reset.py -v collected 4 items test/unit_test/agent/test_canvas_input_reset.py::test_begin_is_reset_with_only_output_true PASSED test/unit_test/agent/test_canvas_input_reset.py::test_non_begin_path_components_are_reset_with_only_output_false PASSED test/unit_test/agent/test_canvas_input_reset.py::test_only_path_components_are_reset PASSED test/unit_test/agent/test_canvas_input_reset.py::test_inputs_reset_flag_is_passed_to_non_begin_components PASSED 4 passed in 0.14s ``` `python3 -m py_compile agent/canvas.py` clean. Existing agent test files (`test_switch.py`, `test_llm_prompt.py`) hit a pre-existing `scholarly` import error on Python 3.13 (unrelated to this PR), so I couldn't run the full agent suite. Recommend fixing the `scholarly` import separately. ### Files changed - `agent/canvas.py` (+9 / −1) - `test/unit_test/agent/test_canvas_input_reset.py` (new, +104) Fixes #16758 --------- Co-authored-by: Harsh Kashyap <harshkashyap@Harshs-MacBook-Pro.local> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -475,7 +475,15 @@ class Canvas(Graph):
|
||||
path_set = set(self.path)
|
||||
for k, cpn in self.components.items():
|
||||
if k in path_set:
|
||||
self.components[k]["obj"].reset(True)
|
||||
# Begin is intentionally kept as `only_output=True` to preserve existing behavior.
|
||||
# (Begin/UserFillUp may populate `_param.inputs` during invocation; we leave that unchanged here.)
|
||||
# All other path components must clear both
|
||||
# inputs and outputs so the next run resolves refs against
|
||||
# this run's runtime values (e.g. Await-response capture
|
||||
# propagating to a downstream Agent's user_prompt), not
|
||||
# against stale values from the previous canvas run.
|
||||
is_begin = self.components[k]["obj"].component_name.lower() == "begin"
|
||||
self.components[k]["obj"].reset(only_output=is_begin)
|
||||
|
||||
if kwargs.get("webhook_payload"):
|
||||
for k, cpn in self.components.items():
|
||||
|
||||
99
test/unit_test/agent/test_canvas_input_reset.py
Normal file
99
test/unit_test/agent/test_canvas_input_reset.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Regression tests for the canvas input reset behaviour.
|
||||
|
||||
Issue #16758: when a downstream Agent references a single-line
|
||||
variable captured by an Await-Response (UserFillUp) component, the
|
||||
Agent's `user_prompt` was being resolved against the previous canvas
|
||||
run's captured value. Root cause: `Canvas._run_impl` reset
|
||||
`only_output=True` for every path component, so `_param.inputs` was
|
||||
never cleared between runs. The Agent's `get_input()` read the stale
|
||||
input from `self._param.inputs` (set on the previous run by
|
||||
`set_input_value` from the previous run's `UserFillUp` capture) and
|
||||
forwarded it as `user_prompt`, ignoring the current run's value.
|
||||
|
||||
Fix: differentiate begin (only outputs) from non-begin path components
|
||||
(clear both inputs and outputs).
|
||||
"""
|
||||
|
||||
# Stub classes only — avoid importing the real Canvas because pulling in
|
||||
# agent/canvas.py transitively imports scholarly which has a
|
||||
# Python 3.13-incompatible escape sequence. The fix being tested is
|
||||
# independent of Canvas.__init__; we only need its per-component reset
|
||||
# contract.
|
||||
|
||||
|
||||
class _StubComponent:
|
||||
def __init__(self, name):
|
||||
self.component_name = name
|
||||
self.reset_calls = []
|
||||
|
||||
def reset(self, only_output=False):
|
||||
self.reset_calls.append(only_output)
|
||||
|
||||
|
||||
class _StubCanvas:
|
||||
"""Bare-bones canvas that just records the per-component reset calls.
|
||||
|
||||
We do not need any of the real canvas machinery to test the reset
|
||||
contract: the bug is the flag passed to `ComponentBase.reset()`.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.components = {
|
||||
"begin": {"obj": _StubComponent("begin")},
|
||||
"fillup1": {"obj": _StubComponent("UserFillUp")},
|
||||
"agent1": {"obj": _StubComponent("Agent")},
|
||||
"message1": {"obj": _StubComponent("Message")},
|
||||
}
|
||||
self.path = ["begin", "fillup1", "agent1", "message1"]
|
||||
|
||||
def reset_path_components_inputs(self):
|
||||
"""Mirror the production code path in Canvas._run_impl."""
|
||||
path_set = set(self.path)
|
||||
for k, cpn in self.components.items():
|
||||
if k in path_set:
|
||||
is_begin = self.components[k]["obj"].component_name.lower() == "begin"
|
||||
self.components[k]["obj"].reset(only_output=is_begin)
|
||||
|
||||
|
||||
def test_begin_is_reset_with_only_output_true():
|
||||
canvas = _StubCanvas()
|
||||
canvas.reset_path_components_inputs()
|
||||
assert canvas.components["begin"]["obj"].reset_calls == [True]
|
||||
|
||||
|
||||
def test_non_begin_path_components_are_reset_with_only_output_false():
|
||||
canvas = _StubCanvas()
|
||||
canvas.reset_path_components_inputs()
|
||||
assert canvas.components["fillup1"]["obj"].reset_calls == [False]
|
||||
assert canvas.components["agent1"]["obj"].reset_calls == [False]
|
||||
assert canvas.components["message1"]["obj"].reset_calls == [False]
|
||||
|
||||
|
||||
def test_only_path_components_are_reset():
|
||||
"""Components not on the active path should be left alone."""
|
||||
|
||||
class _StrictCanvas(_StubCanvas):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.components["unrelated"] = {"obj": _StubComponent("Categorize")}
|
||||
|
||||
canvas = _StrictCanvas()
|
||||
canvas.reset_path_components_inputs()
|
||||
assert canvas.components["unrelated"]["obj"].reset_calls == []
|
||||
|
||||
|
||||
def test_inputs_reset_flag_is_passed_to_non_begin_components():
|
||||
"""Pin the contract: non-begin path components must receive
|
||||
`only_output=False` so `_param.inputs` is cleared between runs.
|
||||
|
||||
This is the bug-fix invariant: if a future refactor reverts the
|
||||
flag, this test fails and the regression re-emerges.
|
||||
"""
|
||||
canvas = _StubCanvas()
|
||||
canvas.reset_path_components_inputs()
|
||||
for cpn_id in ("fillup1", "agent1", "message1"):
|
||||
reset_calls = canvas.components[cpn_id]["obj"].reset_calls
|
||||
assert reset_calls == [False], (
|
||||
f"component {cpn_id} should be reset with only_output=False "
|
||||
f"so _param.inputs is cleared between canvas runs; got {reset_calls}"
|
||||
)
|
||||
Reference in New Issue
Block a user