diff --git a/lib/crewai/src/crewai/experimental/conversational_mixin.py b/lib/crewai/src/crewai/experimental/conversational_mixin.py index 46e83134b..f960db30c 100644 --- a/lib/crewai/src/crewai/experimental/conversational_mixin.py +++ b/lib/crewai/src/crewai/experimental/conversational_mixin.py @@ -46,7 +46,9 @@ from crewai.flow.conversation import ( get_conversation_messages, receive_user_message as _receive_user_message, ) -from crewai.flow.dsl import listen, router, start +from crewai.flow.dsl import listen, start +from crewai.flow.dsl._utils import _set_flow_method_definition +from crewai.flow.flow_definition import FlowMethodDefinition from crewai.utilities.types import LLMMessage @@ -72,6 +74,15 @@ def _iter_condition_labels(condition: Any) -> set[str]: return set() +def _conversation_start_router(func: Callable[..., Any]) -> Any: + wrapper = start()(func) + _set_flow_method_definition( + wrapper, + FlowMethodDefinition(start=True, router=True), + ) + return wrapper + + class _ConversationalMixin: """Experimental conversational graph for ``Flow``. @@ -85,10 +96,7 @@ class _ConversationalMixin: conversational: ClassVar[bool] = False conversational_config: ClassVar[ConversationConfig | None] = None builtin_routes: ClassVar[tuple[str, ...]] = ("converse", "end") - internal_routes: ClassVar[tuple[str, ...]] = ( - "answer_from_history", - "conversation_start", - ) + internal_routes: ClassVar[tuple[str, ...]] = ("answer_from_history",) builtin_route_descriptions: ClassVar[dict[str, str]] = { "converse": ( "Ordinary chat, follow-ups, summaries, clarifications, and " @@ -138,23 +146,21 @@ class _ConversationalMixin: def kickoff(self, *args: Any, **kwargs: Any) -> Any: pass - @start() - @_conversational_only def conversation_start(self) -> str | None: - """Internal Flow entrypoint that hands the user message to the router. + """Return the current user message for conversational route selection. - In conversational mode, ``Flow.kickoff_async`` runs all ``@start`` - methods sequentially and this one is registered last, so any user - ``@start`` methods (e.g. permission loading) have already finished - before the returned value triggers ``route_conversation``. + This remains as a plain overridable helper for compatibility. It is not + registered as a Flow method; ``route_conversation`` is the synthetic + built-in start/router that begins a conversational turn. """ state = cast(ConversationState, self.state) return state.current_user_message - @router(conversation_start) + @_conversation_start_router @_conversational_only def route_conversation(self) -> str: """Route the current turn to a listener label.""" + self.conversation_start() state = cast(ConversationState, self.state) context = self.build_router_context() previous_intent = state.last_intent @@ -651,16 +657,16 @@ class _ConversationalMixin: if not type(self)._is_conversational(): return start_methods, False - conversation_start = "conversation_start" - if conversation_start not in {str(method) for method in start_methods}: + route_conversation = "route_conversation" + if route_conversation not in {str(method) for method in start_methods}: return start_methods, False ordered_starts = [ - method for method in start_methods if str(method) != conversation_start + method for method in start_methods if str(method) != route_conversation ] ordered_starts.append( next( - method for method in start_methods if str(method) == conversation_start + method for method in start_methods if str(method) == route_conversation ) ) return ordered_starts, True diff --git a/lib/crewai/src/crewai/flow/conversational_definition.py b/lib/crewai/src/crewai/flow/conversational_definition.py index 8673bbb3a..75a4a689b 100644 --- a/lib/crewai/src/crewai/flow/conversational_definition.py +++ b/lib/crewai/src/crewai/flow/conversational_definition.py @@ -39,9 +39,7 @@ class FlowConversationalDefinition(BaseModel): visible_agent_outputs: list[str] | Literal["all"] | None = None defer_trace_finalization: bool = True builtin_routes: list[str] = Field(default_factory=lambda: ["converse", "end"]) - internal_routes: list[str] = Field( - default_factory=lambda: ["answer_from_history", "conversation_start"] - ) + internal_routes: list[str] = Field(default_factory=lambda: ["answer_from_history"]) __all__ = [ diff --git a/lib/crewai/src/crewai/flow/dsl/_utils.py b/lib/crewai/src/crewai/flow/dsl/_utils.py index c4b9a4c92..119173500 100644 --- a/lib/crewai/src/crewai/flow/dsl/_utils.py +++ b/lib/crewai/src/crewai/flow/dsl/_utils.py @@ -313,7 +313,7 @@ def _build_conversational_definition( internal_routes = getattr( flow_class, "internal_routes", - ("answer_from_history", "conversation_start"), + ("answer_from_history",), ) if config is None: return FlowConversationalDefinition( diff --git a/lib/crewai/tests/test_flow_conversation.py b/lib/crewai/tests/test_flow_conversation.py index 122ad0009..eed0212f6 100644 --- a/lib/crewai/tests/test_flow_conversation.py +++ b/lib/crewai/tests/test_flow_conversation.py @@ -598,9 +598,9 @@ class TestConversationalFlow: """Conversational flows: user ``@start`` methods finish before router fires. Non-chat flows run ``@start`` methods in parallel via ``asyncio.gather``, - which would race with ``conversation_start`` and let the router fire + which would race with ``route_conversation`` and let the router fire before user setup finished. In conversational mode the framework runs - them sequentially, with ``conversation_start`` last. + them sequentially, with ``route_conversation`` last. """ order: list[str] = [] @@ -643,15 +643,10 @@ class TestConversationalFlow: assert "attach_bus" in order # still fires every turn assert "route_turn" in order - def test_subclass_can_override_conversation_start_without_redecorating( + def test_subclass_can_override_conversation_start_helper( self, ) -> None: - """Overriding an inherited ``@start`` method must not unregister it. - - Before the metaclass fix, subclasses had to re-apply ``@start()`` on - every override or the parent's ``conversation_start`` would silently - drop out of the start registry — leaving the flow with nothing to fire. - """ + """The compatibility helper remains overridable without adding a Flow node.""" bootstrap_calls: list[str] = [] @@ -673,6 +668,10 @@ class TestConversationalFlow: flow.handle_turn("hi") assert bootstrap_calls == ["ran"] + assert "conversation_start" not in BootstrapFlow.flow_definition().methods + route_definition = BootstrapFlow.flow_definition().methods["route_conversation"] + assert route_definition.start is True + assert route_definition.router is True assert flow.state.messages[-1].content == "worked" @conversational_graph_broken diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index da7908798..f79917369 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -223,10 +223,11 @@ def test_flow_definition_includes_conversational_builtins_when_enabled(): assert definition.conversational.enabled is True assert definition.conversational.defer_trace_finalization is True assert definition.conversational.builtin_routes == ["converse", "end"] - assert "conversation_start" in methods + assert "conversation_start" not in methods assert "route_conversation" in methods assert "converse_turn" in methods - assert methods["conversation_start"].start is True + assert methods["route_conversation"].start is True + assert methods["route_conversation"].router is True def test_flow_definition_serializes_conversational_config(): @@ -260,7 +261,7 @@ def test_flow_definition_serializes_conversational_config(): assert conversational.router.fallback_intent == "end" -def test_flow_definition_preserves_undecorated_conversational_override(): +def test_flow_definition_uses_collapsed_conversational_router_start(): class ChatFlow(Flow): conversational = True @@ -269,8 +270,10 @@ def test_flow_definition_preserves_undecorated_conversational_override(): methods = ChatFlow.flow_definition().methods - assert methods["conversation_start"].start is True + assert "conversation_start" not in methods assert "route_conversation" in methods + assert methods["route_conversation"].start is True + assert methods["route_conversation"].router is True def test_flow_definition_serializes_human_feedback_metadata():