mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-20 10:03:37 +00:00
* test(flow): unskip the conversational end-to-end suite The `conversational_graph_broken` marker parked 21 end-to-end conversational tests with the reason "the definition-first start migration intentionally stopped scanning inherited methods, so that graph no longer registers". That is no longer true: `_iter_flow_methods` walks the MRO for `__conversational_only__` methods (dsl/_utils.py:406-420), so a `conversational = True` subclass does register `route_conversation`, `converse_turn`, `end_conversation` and `answer_from_history_turn` — which `test_flow_definition.py:391-407` already asserts. Removing the marker takes the file from 47 passed / 21 skipped to 68 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(flow): make conversational opt-in unmistakable Opting a Flow into chat took two statements, and forgetting one failed silently. With `@ConversationConfig(...)` but no `conversational = True`: `FlowDefinition.conversational` came back `None`, the built-in graph never registered, and `handle_turn()` returned `None` without appending a message or raising — while `chat()` reported "only available on conversational flows" on a class that was literally decorated with a conversational config. Three changes: - `ConversationConfig.__call__` now also sets `conversational = True`. Every field on the config is consumed only by the conversational graph, so a decorated non-conversational Flow could only ever discard it. - `FlowConversationalDefinition.enabled` defaults to True. The block is absent on non-conversational flows, so declaring it is the opt-in; `enabled: false` remains an explicit opt-out. - `handle_turn()` raises like `chat()` and `stream_turn()` already do instead of silently returning `None`. Setting `conversational = True` by hand still works and is still the way to opt in without a config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(flow): let a declaration drive conversational mode `FlowDefinition.conversational` was written by the DSL projection and read by nothing: every conversational gate resolved through `type(self).flow_definition()` — the class projection — instead of `self._definition`, the declaration a flow was actually built from. So `Flow.from_declaration()` on a definition with a conversational block produced a flow that reported itself non-conversational, dropped the user message, and never registered its declared routes. Resolution rules, applied consistently: - Structure (enabled, methods, route labels, builtin/internal routes) comes from `self._definition`, which is the loaded declaration for a declarative flow and the class projection otherwise. Both paths now agree. - Behavior (`conversational_config`) still prefers the class attribute, which can hold live objects — a configured LLM, a custom BaseLLM, a response_format model class — that the serializable definition degrades to a config dict or a `module:qualname` ref. Reading the definition first would silently downgrade every decorated Python flow. A declaration-built flow has no class config, so `_config_from_definition` supplies one, cached for stable identity. - A declared `state:` block is never replaced. `_create_default_extension_state` is consulted before `_create_definition_state`, so returning `ConversationState` there discarded every field the declaration asked for. It now yields to a declared state and only supplies the default when nothing else does. The class-scoped `_is_conversational` / `_conversational_definition` classmethods are gone; the existing instance-scoped `_is_conversational_enabled` is the single gate. A router `response_format` that survived serialization as a ref or schema dict is dropped with a warning rather than handed to `llm.call()`, which needs a real class. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(flow): synthesize the built-in conversational methods for declarations A declaration carrying `conversational: {}` loaded clean and then ran zero methods and returned `None`, because the four built-in graph handlers are inherited from `_ConversationalMixin` and a declaration has nothing to inherit from. Authors had to name `crewai.experimental.conversational_mixin:_Conversational Mixin.route_conversation` and three siblings by hand. `Flow._extend_definition` is a new runtime extension hook, called once `_definition` is resolved and before methods are bound. The conversational mixin overrides it to fill in `route_conversation`, `converse_turn`, `end_conversation` and `answer_from_history_turn` when they are missing, using the same code refs the DSL projection already emits so a declaration and a class projection of the same flow produce identical method definitions. Synthesis is deliberately a runtime concern, not a contract one: `FlowDefinition` stays independent of the authoring layer and of the engine, as `test_flow_definition_contract_is_dsl_agnostic` requires, and a loaded declaration still serializes back to exactly what its author wrote. Route descriptions are now carried by the contract. The DSL projects a handler docstring's first line into `FlowMethodDefinition.description`, and the router catalog reads that before falling back to the live docstring. This also fixes a real defect: for a declarative flow `getattr(type(self), handler_name, None)` is `None`, and the old code read `None.__doc__` — so the router LLM was told a route's description was "The type of the None singleton." Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(flow): let an agent or crew handler reply in a conversation (#7034) `handle_turn` promotes a handler's return value to the assistant message when the handler did not append one itself, but the check required `isinstance(result, str)`. Declarative `agent` and `crew` actions return `LiteAgentOutput` and `CrewOutput`, whose text lives on `.raw` — so the most natural declarative handler was exactly the one whose reply never reached the transcript. `_is_public_turn_result` now unwraps `.raw` before deciding, matching `_stringify_result`, which already did. The routing-artefact guards are applied to the unwrapped text, so an output echoing a route label or this turn's intent is still not promoted. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(flow): keep privately recorded agent results out of the transcript Unwrapping `.raw` in `_is_public_turn_result` made the end-of-turn fallback promote `LiteAgentOutput` / `CrewOutput` objects that the handler had already recorded via `append_agent_result` with the default private visibility. That call does not set `_assistant_reply_appended`, so the fallback republished the very object the handler asked to keep private — defeating `visible_agent_outputs`. Reproduced against `main` for contrast: no leak before the unwrap, leak after. `append_agent_result` now remembers the object it recorded for the duration of the turn, and the fallback skips anything already routed that way. The check is identity-based on purpose: a handler that records scratch work privately and then returns a user-facing summary still gets that summary promoted, which a simple "handler already handled it" flag would have broken. Found by Cursor Bugbot on #7033. * feat(flow): mark a declarative chat flow conversational on the instance A declaration enables chat through `conversational.enabled`, without the `conversational = True` class attribute. Callers outside this package capability-check that attribute -- the AG-UI serving guide states it as a requirement -- so it disagreed with `_is_conversational_enabled()` and a declarative conversational flow looked non-conversational from outside. `_extend_definition` now sets it on the instance when the definition enables chat. Instance-only on purpose: the DSL projection reads the attribute off the *class* to decide whether to emit a conversational block, so setting it there would make every later subclass look conversational. Verified on a real declarative flow: `conversational` and `stream_turn` both now satisfy the documented capability check, while `Flow.conversational` and any later subclass stay False. * refactor(flow): derive routing-artefact labels from the effective routes `_is_public_turn_result` matched a literal set of route labels, duplicating knowledge that `_effective_builtin_routes()` already owns. A declaration that adds a builtin route was not covered, so a handler echoing that label could be promoted into the transcript -- the same class of divergence already fixed for `route_turn`. Verified the derived set is byte-identical to the old literal one for a class-based flow, so this is a pure generalization: `conversation` and `route_to_flow` stay explicit because neither is a route. Also replaces a tuple-index lambda in the chat REPL test with a named `input_fn`; it relied on tuple evaluation order and on the list being mutated before its length was read. Both found by CodeRabbit on #7033. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: ViditOstwal <viditostwal@gmail.com>