From 775fec7988976c88e76e6282ad8692fbe66b77e8 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 5 Jan 2026 00:23:48 +0100 Subject: [PATCH] fix(flow): use __router_paths__ for listener routers When a listener method is also a router (e.g., decorated with both @listen and @human_feedback with emit), the router path detection was directly calling get_possible_return_constants() which parses the method's source code via AST. This caused incorrect router paths (parsing return values instead of emit values) and could cause IndentationError when the method had multiple decorators, because inspect.getsource() includes decorators with class-level indentation. The fix aligns listener router handling with start method router handling: check the __router_paths__ attribute first (set by @human_feedback when emit is specified) before falling back to source code parsing. This is a documented use case per human-feedback-in-flows.mdx: "The @human_feedback decorator works with other flow decorators." --- lib/crewai/src/crewai/flow/flow.py | 15 +++- .../tests/test_human_feedback_decorator.py | 85 +++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index ade5b6f1ba..a5e305dc2d 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -445,11 +445,18 @@ def __new__( and attr_value.__is_router__ ): routers.add(attr_name) - possible_returns = get_possible_return_constants(attr_value) - if possible_returns: - router_paths[attr_name] = possible_returns + # Get router paths from the decorator attribute first + if ( + hasattr(attr_value, "__router_paths__") + and attr_value.__router_paths__ + ): + router_paths[attr_name] = attr_value.__router_paths__ else: - router_paths[attr_name] = [] + possible_returns = get_possible_return_constants(attr_value) + if possible_returns: + router_paths[attr_name] = possible_returns + else: + router_paths[attr_name] = [] # Handle start methods that are also routers (e.g., @human_feedback with emit) if ( diff --git a/lib/crewai/tests/test_human_feedback_decorator.py b/lib/crewai/tests/test_human_feedback_decorator.py index 0ae6adbbed..23e31c7782 100644 --- a/lib/crewai/tests/test_human_feedback_decorator.py +++ b/lib/crewai/tests/test_human_feedback_decorator.py @@ -399,3 +399,88 @@ def test_fallback_to_first(self): ) assert result == "approved" # First in list + + +class TestListenerRouterPathDetection: + """Tests for router path detection when combining @listen and @human_feedback.""" + + def test_listener_with_human_feedback_emit_has_router_paths(self): + """Test that @listen + @human_feedback(emit=...) correctly sets router paths. + + This test verifies that the Flow metaclass correctly detects router paths + from the __router_paths__ attribute set by @human_feedback, instead of + trying to parse source code (which fails with IndentationError for + multi-decorated methods). + + Regression test for: combining @listen and @human_feedback(emit=...) caused + IndentationError during Flow class initialization because source parsing + doesn't handle multiple decorators well. + """ + + class TestFlow(Flow): + @start() + def begin(self): + return "start" + + @listen("begin") + @human_feedback( + message="Review this:", + emit=["approved", "rejected", "needs_revision"], + llm="gpt-4o-mini", + ) + def review(self): + return "content to review" + + @listen("approved") + def on_approved(self): + return "published" + + @listen("rejected") + def on_rejected(self): + return "discarded" + + # Verify the Flow class was created without IndentationError + # and router_paths are correctly detected + assert "review" in TestFlow._router_paths + assert TestFlow._router_paths["review"] == [ + "approved", + "rejected", + "needs_revision", + ] + + # Verify the method is registered as both a listener and a router + assert "review" in TestFlow._listeners + assert "review" in TestFlow._routers + + def test_multiple_listener_routers_detected(self): + """Test that multiple @listen + @human_feedback combinations work.""" + + class MultiListenerFlow(Flow): + @start() + @human_feedback( + message="First review:", + emit=["continue", "stop"], + llm="gpt-4o-mini", + ) + def step1(self): + return "step 1" + + @listen("continue") + @human_feedback( + message="Second review:", + emit=["finalize", "revise"], + llm="gpt-4o-mini", + ) + def step2(self): + return "step 2" + + @listen("finalize") + def final(self): + return "done" + + # Both should have their router paths detected + assert "step1" in MultiListenerFlow._router_paths + assert MultiListenerFlow._router_paths["step1"] == ["continue", "stop"] + + assert "step2" in MultiListenerFlow._router_paths + assert MultiListenerFlow._router_paths["step2"] == ["finalize", "revise"]