diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index f22dd4b72..02900834d 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -350,6 +350,8 @@ class Crew(FlowTrackable, BaseModel): @model_validator(mode="after") def set_private_attrs(self) -> Crew: """set private attributes.""" + from crewai.utilities.logger_utils import should_enable_verbose + self._cache_handler = CacheHandler() event_listener = EventListener() @@ -357,12 +359,15 @@ class Crew(FlowTrackable, BaseModel): tracing_enabled = should_enable_tracing(override=self.tracing) set_tracing_enabled(tracing_enabled) + # Determine verbose setting (respects CREWAI_VERBOSE env var) + effective_verbose = should_enable_verbose(override=self.verbose) + # Always setup trace listener - actual execution control is via contextvar trace_listener = TraceCollectionListener() trace_listener.setup_listeners(crewai_event_bus) - event_listener.verbose = self.verbose - event_listener.formatter.verbose = self.verbose - self._logger = Logger(verbose=self.verbose) + event_listener.verbose = effective_verbose + event_listener.formatter.verbose = effective_verbose + self._logger = Logger(verbose=effective_verbose) if self.output_log_file: self._file_handler = FileHandler(self.output_log_file) self._rpm_controller = RPMController(max_rpm=self.max_rpm, logger=self._logger) diff --git a/lib/crewai/src/crewai/events/utils/console_formatter.py b/lib/crewai/src/crewai/events/utils/console_formatter.py index 4aaec2cca..c14433b57 100644 --- a/lib/crewai/src/crewai/events/utils/console_formatter.py +++ b/lib/crewai/src/crewai/events/utils/console_formatter.py @@ -119,14 +119,11 @@ To enable tracing, do any one of these: self, content: Text, title: str, style: str = "blue", is_flow: bool = False ) -> None: """Print a panel with consistent formatting if verbose is enabled.""" + if not self.verbose: + return panel = self.create_panel(content, title, style) - if is_flow: - self.print(panel) - self.print() - else: - if self.verbose: - self.print(panel) - self.print() + self.print(panel) + self.print() def handle_crew_status( self, diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index b92d10d2d..7b0ec78ec 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -559,6 +559,7 @@ class Flow(Generic[T], metaclass=FlowMeta): name: str | None = None tracing: bool | None = None stream: bool = False + verbose: bool = True def __class_getitem__(cls: type[Flow[T]], item: type[T]) -> type[Flow[T]]: class _FlowGeneric(cls): # type: ignore @@ -572,6 +573,7 @@ class Flow(Generic[T], metaclass=FlowMeta): persistence: FlowPersistence | None = None, tracing: bool | None = None, suppress_flow_events: bool = False, + verbose: bool | None = None, **kwargs: Any, ) -> None: """Initialize a new Flow instance. @@ -580,8 +582,12 @@ class Flow(Generic[T], metaclass=FlowMeta): persistence: Optional persistence backend for storing flow states tracing: Whether to enable tracing. True=always enable, False=always disable, None=check environment/user settings suppress_flow_events: Whether to suppress flow event emissions (internal use) + verbose: Whether to enable verbose logging output. True=always enable, False=always disable, + None=check CREWAI_VERBOSE environment variable (defaults to True if not set). **kwargs: Additional state values to initialize or override """ + from crewai.events.event_listener import EventListener + # Initialize basic instance attributes self._methods: dict[FlowMethodName, FlowMethod[Any, Any]] = {} self._method_execution_counts: dict[FlowMethodName, int] = {} @@ -605,6 +611,14 @@ class Flow(Generic[T], metaclass=FlowMeta): self._pending_feedback_context: PendingFeedbackContext | None = None self.suppress_flow_events: bool = suppress_flow_events + # Set verbose and configure event listener + from crewai.utilities.logger_utils import should_enable_verbose + + self.verbose = should_enable_verbose(override=verbose) + event_listener = EventListener() + event_listener.verbose = self.verbose + event_listener.formatter.verbose = self.verbose + # Initialize state with initial values self._state = self._create_initial_state() self.tracing = tracing diff --git a/lib/crewai/src/crewai/utilities/logger_utils.py b/lib/crewai/src/crewai/utilities/logger_utils.py index f59865578..a7a29e37c 100644 --- a/lib/crewai/src/crewai/utilities/logger_utils.py +++ b/lib/crewai/src/crewai/utilities/logger_utils.py @@ -4,6 +4,7 @@ from collections.abc import Generator import contextlib import io import logging +import os import warnings @@ -56,3 +57,39 @@ def suppress_warnings() -> Generator[None, None, None]: "ignore", message="open_text is deprecated*", category=DeprecationWarning ) yield + + +def should_enable_verbose(*, override: bool | None = None) -> bool: + """Determine if verbose logging should be enabled. + + This is the single source of truth for verbose logging enablement. + Priority order: + 1. Explicit override (e.g., Crew.verbose=True/False or Flow.verbose=True/False) + 2. Environment variable CREWAI_VERBOSE + + Args: + override: Explicit override for verbose (True=always enable, False=always disable, + None=check environment variable, defaults to True if not set) + + Returns: + True if verbose logging should be enabled, False otherwise. + + Example: + # Disable verbose logging globally via environment variable + export CREWAI_VERBOSE=false + + # Or in code + flow = Flow(verbose=False) + crew = Crew(verbose=False) + """ + if override is not None: + return override + + env_value = os.getenv("CREWAI_VERBOSE", "").lower() + if env_value in ("false", "0"): + return False + if env_value in ("true", "1"): + return True + + # Default to True if not set + return True diff --git a/lib/crewai/tests/test_verbose_control.py b/lib/crewai/tests/test_verbose_control.py new file mode 100644 index 000000000..017339be8 --- /dev/null +++ b/lib/crewai/tests/test_verbose_control.py @@ -0,0 +1,261 @@ +"""Test verbose control for Flow and Crew.""" + +import os +from io import StringIO +from unittest.mock import patch + +import pytest + +from crewai.events.event_listener import EventListener +from crewai.flow.flow import Flow, start, listen +from crewai.utilities.logger_utils import should_enable_verbose + + +class TestShouldEnableVerbose: + """Test the should_enable_verbose utility function.""" + + def test_override_true_returns_true(self): + """Test that explicit override=True always returns True.""" + assert should_enable_verbose(override=True) is True + + def test_override_false_returns_false(self): + """Test that explicit override=False always returns False.""" + assert should_enable_verbose(override=False) is False + + def test_env_var_false_disables_verbose(self): + """Test that CREWAI_VERBOSE=false disables verbose.""" + with patch.dict(os.environ, {"CREWAI_VERBOSE": "false"}): + assert should_enable_verbose() is False + + def test_env_var_0_disables_verbose(self): + """Test that CREWAI_VERBOSE=0 disables verbose.""" + with patch.dict(os.environ, {"CREWAI_VERBOSE": "0"}): + assert should_enable_verbose() is False + + def test_env_var_true_enables_verbose(self): + """Test that CREWAI_VERBOSE=true enables verbose.""" + with patch.dict(os.environ, {"CREWAI_VERBOSE": "true"}): + assert should_enable_verbose() is True + + def test_env_var_1_enables_verbose(self): + """Test that CREWAI_VERBOSE=1 enables verbose.""" + with patch.dict(os.environ, {"CREWAI_VERBOSE": "1"}): + assert should_enable_verbose() is True + + def test_no_env_var_defaults_to_true(self): + """Test that no CREWAI_VERBOSE env var defaults to True.""" + with patch.dict(os.environ, {}, clear=True): + # Remove CREWAI_VERBOSE if it exists + os.environ.pop("CREWAI_VERBOSE", None) + assert should_enable_verbose() is True + + def test_override_takes_precedence_over_env_var(self): + """Test that explicit override takes precedence over env var.""" + with patch.dict(os.environ, {"CREWAI_VERBOSE": "false"}): + assert should_enable_verbose(override=True) is True + + with patch.dict(os.environ, {"CREWAI_VERBOSE": "true"}): + assert should_enable_verbose(override=False) is False + + +class TestFlowVerboseControl: + """Test verbose control in Flow class.""" + + def test_flow_verbose_default_is_true(self): + """Test that Flow verbose defaults to True when no env var is set.""" + # Remove CREWAI_VERBOSE if it exists + os.environ.pop("CREWAI_VERBOSE", None) + + class SimpleFlow(Flow): + @start() + def step_1(self): + return "done" + + flow = SimpleFlow() + assert flow.verbose is True + + def test_flow_verbose_false_disables_logging(self): + """Test that Flow with verbose=False disables logging.""" + + class SimpleFlow(Flow): + @start() + def step_1(self): + return "done" + + flow = SimpleFlow(verbose=False) + assert flow.verbose is False + + # Verify EventListener is also set to verbose=False + event_listener = EventListener() + assert event_listener.verbose is False + assert event_listener.formatter.verbose is False + + def test_flow_verbose_true_enables_logging(self): + """Test that Flow with verbose=True enables logging.""" + + class SimpleFlow(Flow): + @start() + def step_1(self): + return "done" + + flow = SimpleFlow(verbose=True) + assert flow.verbose is True + + # Verify EventListener is also set to verbose=True + event_listener = EventListener() + assert event_listener.verbose is True + assert event_listener.formatter.verbose is True + + def test_flow_respects_env_var_false(self): + """Test that Flow respects CREWAI_VERBOSE=false env var.""" + + class SimpleFlow(Flow): + @start() + def step_1(self): + return "done" + + with patch.dict(os.environ, {"CREWAI_VERBOSE": "false"}, clear=False): + flow = SimpleFlow() + assert flow.verbose is False + + def test_flow_respects_env_var_true(self): + """Test that Flow respects CREWAI_VERBOSE=true env var.""" + + class SimpleFlow(Flow): + @start() + def step_1(self): + return "done" + + with patch.dict(os.environ, {"CREWAI_VERBOSE": "true"}, clear=False): + flow = SimpleFlow() + assert flow.verbose is True + + def test_flow_explicit_verbose_overrides_env_var(self): + """Test that explicit verbose parameter overrides env var.""" + + class SimpleFlow(Flow): + @start() + def step_1(self): + return "done" + + # Explicit verbose=True overrides CREWAI_VERBOSE=false + with patch.dict(os.environ, {"CREWAI_VERBOSE": "false"}, clear=False): + flow = SimpleFlow(verbose=True) + assert flow.verbose is True + + # Explicit verbose=False overrides CREWAI_VERBOSE=true + with patch.dict(os.environ, {"CREWAI_VERBOSE": "true"}, clear=False): + flow = SimpleFlow(verbose=False) + assert flow.verbose is False + + +class TestFlowVerboseExecution: + """Test that verbose setting actually suppresses output during Flow execution.""" + + def test_flow_verbose_false_suppresses_console_output(self): + """Test that Flow with verbose=False suppresses console output.""" + execution_order = [] + + class SimpleFlow(Flow): + @start() + def step_1(self): + execution_order.append("step_1") + return "step_1_done" + + @listen(step_1) + def step_2(self): + execution_order.append("step_2") + return "step_2_done" + + # Create flow with verbose=False + flow = SimpleFlow(verbose=False) + + # Verify the formatter's verbose is False + event_listener = EventListener() + assert event_listener.formatter.verbose is False + + # Execute the flow + result = flow.kickoff() + + # Flow should still execute correctly + assert execution_order == ["step_1", "step_2"] + assert result == "step_2_done" + + def test_flow_verbose_true_allows_console_output(self): + """Test that Flow with verbose=True allows console output.""" + execution_order = [] + + class SimpleFlow(Flow): + @start() + def step_1(self): + execution_order.append("step_1") + return "step_1_done" + + @listen(step_1) + def step_2(self): + execution_order.append("step_2") + return "step_2_done" + + # Create flow with verbose=True + flow = SimpleFlow(verbose=True) + + # Verify the formatter's verbose is True + event_listener = EventListener() + assert event_listener.formatter.verbose is True + + # Execute the flow + result = flow.kickoff() + + # Flow should execute correctly + assert execution_order == ["step_1", "step_2"] + assert result == "step_2_done" + + +class TestConsoleFormatterVerbose: + """Test that ConsoleFormatter respects verbose setting.""" + + def test_console_formatter_print_panel_respects_verbose_false(self): + """Test that print_panel does not print when verbose=False.""" + from rich.text import Text + from crewai.events.utils.console_formatter import ConsoleFormatter + + formatter = ConsoleFormatter(verbose=False) + + # Create a mock to capture print calls + with patch.object(formatter, "print") as mock_print: + content = Text("Test content") + formatter.print_panel(content, "Test Title", "blue", is_flow=True) + + # print should not be called when verbose=False + mock_print.assert_not_called() + + def test_console_formatter_print_panel_respects_verbose_true(self): + """Test that print_panel prints when verbose=True.""" + from rich.text import Text + from crewai.events.utils.console_formatter import ConsoleFormatter + + formatter = ConsoleFormatter(verbose=True) + + # Create a mock to capture print calls + with patch.object(formatter, "print") as mock_print: + content = Text("Test content") + formatter.print_panel(content, "Test Title", "blue", is_flow=True) + + # print should be called when verbose=True + assert mock_print.call_count >= 1 + + def test_console_formatter_flow_events_respect_verbose_false(self): + """Test that flow events are suppressed when verbose=False.""" + from rich.text import Text + from crewai.events.utils.console_formatter import ConsoleFormatter + + formatter = ConsoleFormatter(verbose=False) + + # Create a mock to capture print calls + with patch.object(formatter, "print") as mock_print: + content = Text("Flow event content") + # is_flow=True should still respect verbose=False + formatter.print_panel(content, "Flow Event", "blue", is_flow=True) + + # print should not be called even for flow events when verbose=False + mock_print.assert_not_called()