diff --git a/src/crewai/crew.py b/src/crewai/crew.py index d488783ea..7d3158a48 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -516,6 +516,7 @@ class Crew(BaseModel): """Starts the crew to work on its assigned tasks.""" self._execution_span = self._telemetry.crew_execution_span(self, inputs) + self._log_crew_start() self._task_output_handler.reset() self._logging_color = "bold_purple" @@ -562,6 +563,7 @@ class Crew(BaseModel): for metric in metrics: self.usage_metrics.add_usage_metrics(metric) + self._log_crew_finish() return result def kickoff_for_each(self, inputs: List[Dict[str, Any]]) -> List[CrewOutput]: @@ -707,6 +709,7 @@ class Crew(BaseModel): task_outputs: List[TaskOutput] = [] futures: List[Tuple[Task, Future[TaskOutput], int]] = [] last_sync_output: Optional[TaskOutput] = None + previous_agent_role: Optional[str] = None for task_index, task in enumerate(tasks): if start_index is not None and task_index < start_index: @@ -716,6 +719,8 @@ class Crew(BaseModel): else: task_outputs = [task.output] last_sync_output = task.output + if task.agent is not None: + previous_agent_role = task.agent.role continue agent_to_use = self._get_agent_to_use(task) @@ -724,6 +729,11 @@ class Crew(BaseModel): f"No agent available for task: {task.description}. Ensure that either the task has an assigned agent or a manager agent is provided." ) + if previous_agent_role and previous_agent_role != agent_to_use.role: + self._log_context_received( + agent_to_use.role, previous_agent_role + ) + # Determine which tools to use - task tools take precedence over agent tools tools_for_task = task.tools or agent_to_use.tools or [] tools_for_task = self._prepare_tools( @@ -741,6 +751,8 @@ class Crew(BaseModel): if skipped_task_output: continue + next_agent_role = self._get_next_agent_role(tasks, task_index) + if task.async_execution: context = self._get_context( task, [last_sync_output] if last_sync_output else [] @@ -765,6 +777,11 @@ class Crew(BaseModel): task_outputs = [task_output] self._process_task_result(task, task_output) self._store_execution_log(task, task_output, task_index, was_replayed) + self._log_task_completion( + agent_to_use.role, task, next_agent_role + ) + + previous_agent_role = agent_to_use.role if futures: task_outputs = self._process_async_tasks(futures, was_replayed) @@ -859,12 +876,67 @@ class Crew(BaseModel): tools = self._inject_delegation_tools(tools, task.agent, agents_for_delegation) return tools + def _log_crew_start(self): + crew_name = self.name or "Crew" + self._logger.log( + "info", + f"[{crew_name}] Starting crew execution", + color="bold_purple", + ) + + def _log_crew_finish(self): + crew_name = self.name or "Crew" + self._logger.log( + "info", + f"[{crew_name}] Crew execution completed", + color="bold_purple", + ) + def _log_task_start(self, task: Task, role: str = "None"): + task_name = task.name or task.description + self._logger.log( + "info", + f"[Agent: {role}] Starting task: {task_name}", + color="bold_blue", + ) if self.output_log_file: self._file_handler.log( task_name=task.name, task=task.description, agent=role, status="started" ) + def _log_task_completion( + self, role: str, task: Task, next_agent_role: Optional[str] = None + ): + task_name = task.name or task.description + if next_agent_role and next_agent_role != role: + self._logger.log( + "info", + f"[Agent: {role}] Task complete: {task_name}, passing to: {next_agent_role}", + color="bold_blue", + ) + else: + self._logger.log( + "info", + f"[Agent: {role}] Task complete: {task_name}", + color="bold_blue", + ) + + def _log_context_received(self, role: str, from_role: str): + self._logger.log( + "info", + f"[Agent: {role}] Received context from {from_role}", + color="bold_blue", + ) + + def _get_next_agent_role( + self, tasks: List[Task], current_index: int + ) -> Optional[str]: + for i in range(current_index + 1, len(tasks)): + next_agent = self._get_agent_to_use(tasks[i]) + if next_agent is not None: + return next_agent.role + return None + def _update_manager_tools(self, task: Task, tools: List[Tool]): if self.manager_agent: if task.agent: diff --git a/tests/crew_test.py b/tests/crew_test.py index 2003ddada..bb9c9c339 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -1123,7 +1123,7 @@ def test_kickoff_for_each_empty_input(): assert results == [] -@pytest.mark.vcr(filter_headers=["authorization"]) +@pytest.mark.vcr(filter_headeruvs=["authorization"]) def test_kickoff_for_each_invalid_input(): """Tests if kickoff_for_each raises TypeError for invalid input types.""" @@ -3125,4 +3125,238 @@ def test_multimodal_agent_live_image_analysis(): # Verify we got a meaningful response assert isinstance(result.raw, str) assert len(result.raw) > 100 # Expecting a detailed analysis - assert "error" not in result.raw.lower() # No error messages in response \ No newline at end of file + assert "error" not in result.raw.lower() # No error messages in response + + +def test_verbose_crew_start_and_finish_logs(capsys): + """Test that crew start/finish logs are emitted when verbose=True.""" + tasks = [ + Task( + description="Research AI advancements.", + expected_output="A full report on AI advancements.", + agent=researcher, + ), + ] + + crew = Crew( + agents=[researcher], + tasks=tasks, + process=Process.sequential, + verbose=True, + ) + + mock_task_output = TaskOutput( + description="Research AI advancements.", + raw="AI advancements report", + agent="Researcher", + ) + with patch.object(Task, "execute_sync", return_value=mock_task_output): + crew.kickoff() + captured = capsys.readouterr() + assert "[Crew] Starting crew execution" in captured.out + assert "[Crew] Crew execution completed" in captured.out + + +def test_verbose_crew_start_and_finish_not_logged_when_not_verbose(capsys): + """Test that crew start/finish logs are NOT emitted when verbose=False.""" + tasks = [ + Task( + description="Research AI advancements.", + expected_output="A full report on AI advancements.", + agent=researcher, + ), + ] + + crew = Crew( + agents=[researcher], + tasks=tasks, + process=Process.sequential, + verbose=False, + ) + + mock_task_output = TaskOutput( + description="Research AI advancements.", + raw="AI advancements report", + agent="Researcher", + ) + with patch.object(Task, "execute_sync", return_value=mock_task_output): + crew.kickoff() + captured = capsys.readouterr() + assert "[Crew] Starting crew execution" not in captured.out + assert "[Crew] Crew execution completed" not in captured.out + + +def test_verbose_task_start_and_completion_logs(capsys): + """Test that per-task start/complete logs are emitted when verbose=True.""" + tasks = [ + Task( + description="Research AI advancements.", + expected_output="A full report on AI advancements.", + agent=researcher, + ), + Task( + description="Write about AI in healthcare.", + expected_output="A 4 paragraph article about AI.", + agent=writer, + ), + ] + + crew = Crew( + agents=[researcher, writer], + tasks=tasks, + process=Process.sequential, + verbose=True, + ) + + mock_task_output1 = TaskOutput( + description="Research AI advancements.", + raw="AI advancements report", + agent="Researcher", + ) + mock_task_output2 = TaskOutput( + description="Write about AI in healthcare.", + raw="Article about AI in healthcare", + agent="Senior Writer", + ) + with patch.object( + Task, "execute_sync", side_effect=[mock_task_output1, mock_task_output2] + ): + crew.kickoff() + captured = capsys.readouterr() + assert "[Agent: Researcher] Starting task:" in captured.out + assert "[Agent: Senior Writer] Starting task:" in captured.out + assert "[Agent: Researcher] Task complete:" in captured.out + assert "[Agent: Senior Writer] Task complete:" in captured.out + + +def test_verbose_handoff_logs_between_agents(capsys): + """Test that handoff logs appear when tasks pass between different agents.""" + tasks = [ + Task( + description="Research AI advancements.", + expected_output="A full report on AI advancements.", + agent=researcher, + ), + Task( + description="Write about AI in healthcare.", + expected_output="A 4 paragraph article about AI.", + agent=writer, + ), + ] + + crew = Crew( + agents=[researcher, writer], + tasks=tasks, + process=Process.sequential, + verbose=True, + ) + + mock_task_output1 = TaskOutput( + description="Research AI advancements.", + raw="AI advancements report", + agent="Researcher", + ) + mock_task_output2 = TaskOutput( + description="Write about AI in healthcare.", + raw="Article about AI in healthcare", + agent="Senior Writer", + ) + with patch.object( + Task, "execute_sync", side_effect=[mock_task_output1, mock_task_output2] + ): + crew.kickoff() + captured = capsys.readouterr() + assert "passing to: Senior Writer" in captured.out + assert "[Agent: Senior Writer] Received context from Researcher" in captured.out + + +def test_verbose_no_handoff_when_same_agent(capsys): + """Test that no handoff log is emitted when the same agent runs consecutive tasks.""" + tasks = [ + Task( + description="Research AI advancements.", + expected_output="A full report on AI advancements.", + agent=researcher, + ), + Task( + description="Research more about AI.", + expected_output="Extended AI report.", + agent=researcher, + ), + ] + + crew = Crew( + agents=[researcher], + tasks=tasks, + process=Process.sequential, + verbose=True, + ) + + mock_task_output1 = TaskOutput( + description="Research AI advancements.", + raw="AI advancements report", + agent="Researcher", + ) + mock_task_output2 = TaskOutput( + description="Research more about AI.", + raw="Extended AI report", + agent="Researcher", + ) + with patch.object( + Task, "execute_sync", side_effect=[mock_task_output1, mock_task_output2] + ): + crew.kickoff() + captured = capsys.readouterr() + assert "Received context from" not in captured.out + assert "passing to:" not in captured.out + + +def test_verbose_crew_name_in_logs(capsys): + """Test that the crew name appears in start/finish logs when set.""" + crew = Crew( + name="TestCrew", + agents=[researcher], + tasks=[ + Task( + description="Research AI advancements.", + expected_output="A full report on AI advancements.", + agent=researcher, + ), + ], + process=Process.sequential, + verbose=True, + ) + + crew._log_crew_start() + crew._log_crew_finish() + captured = capsys.readouterr() + assert "[TestCrew] Starting crew execution" in captured.out + assert "[TestCrew] Crew execution completed" in captured.out + + +def test_verbose_task_name_in_logs(capsys): + """Test that the task name is used in logs when available, falling back to description.""" + named_task = Task( + name="research_task", + description="Research AI advancements.", + expected_output="A full report on AI advancements.", + agent=researcher, + ) + unnamed_task = Task( + description="Write about AI.", + expected_output="A short article.", + agent=writer, + ) + + crew = Crew( + agents=[researcher, writer], + tasks=[named_task, unnamed_task], + process=Process.sequential, + verbose=True, + ) + + crew._log_task_start(named_task, "Researcher") + crew._log_task_start(unnamed_task, "Senior Writer") + captured = capsys.readouterr() + assert "[Agent: Researcher] Starting task: research_task" in captured.out + assert "[Agent: Senior Writer] Starting task: Write about AI." in captured.out