diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index 7631a4c2b..abf67b80a 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -624,6 +624,42 @@ def create_crew_knowledge(self) -> Crew: ) return self + @model_validator(mode="after") + def sync_agents_with_tasks(self) -> Self: + """Ensure ``agents`` includes every agent assigned to a task. + + When a ``Task`` has an agent assigned (``task.agent=agent``) but that + agent is not listed in ``Crew.agents``, downstream setup such as + ``agent.crew = crew`` is skipped, which causes features that depend + on the crew reference (e.g. ``input_files`` injection, delegation + tools, crew memory lookups) to silently no-op. + + This validator auto-populates ``self.agents`` with any task agent + that isn't already present so the crew behaves the same whether or + not ``agents=[...]`` was passed explicitly. + """ + if not self.tasks: + return self + + existing: list[BaseAgent] = list(self.agents) + + def _contains(agent: BaseAgent) -> bool: + return any(existing_agent is agent for existing_agent in existing) + + for task in self.tasks: + task_agent = task.agent + if task_agent is None: + continue + if task_agent is self.manager_agent: + continue + if not _contains(task_agent): + existing.append(task_agent) + + if len(existing) != len(self.agents): + self.agents = existing + + return self + @model_validator(mode="after") def check_manager_llm(self) -> Self: """Validates that the language model is set when using hierarchical process.""" diff --git a/lib/crewai/tests/test_crew_multimodal.py b/lib/crewai/tests/test_crew_multimodal.py index d23a80a99..ab910294e 100644 --- a/lib/crewai/tests/test_crew_multimodal.py +++ b/lib/crewai/tests/test_crew_multimodal.py @@ -457,4 +457,112 @@ def test_pdf_upload_anthropic(self, pdf_file: PDFFile) -> None: result = crew.kickoff(input_files={"document": pdf_file}) assert result.raw - assert len(result.raw) > 0 \ No newline at end of file + assert len(result.raw) > 0 + + +class TestCrewMultimodalWithoutExplicitAgents: + """Regression tests for issue #5534. + + ``Task.input_files`` must be propagated even when ``Crew`` is constructed + with only ``tasks=[...]`` and no explicit ``agents=[...]``. Prior to the + fix, ``crew.agents`` was empty in that case, ``setup_agents`` never wired + ``task.agent.crew = crew``, and file injection silently no-op'd. + """ + + @staticmethod + def _build_task_and_crew( + image_file: ImageFile, + ) -> tuple[Agent, Task, Crew]: + llm = LLM(model="openai/gpt-4o-mini") + agent = Agent( + role="File Analyst", + goal="Analyze files", + backstory="Expert analyst.", + llm=llm, + multimodal=True, + verbose=False, + ) + task = Task( + description="Describe the image.", + expected_output="A brief description.", + agent=agent, + input_files={"chart": image_file}, + ) + crew = Crew(tasks=[task], verbose=False) + return agent, task, crew + + def test_crew_agents_auto_populated_from_tasks( + self, image_file: ImageFile + ) -> None: + """Crew.agents should include task.agent even if not passed explicitly.""" + agent, _task, crew = self._build_task_and_crew(image_file) + + assert agent in crew.agents + assert len(crew.agents) == 1 + + def test_crew_agents_not_duplicated_when_provided_explicitly( + self, image_file: ImageFile + ) -> None: + """When agents=[agent] is passed explicitly, no duplicates are added.""" + llm = LLM(model="openai/gpt-4o-mini") + agent = Agent( + role="File Analyst", + goal="Analyze files", + backstory="Expert analyst.", + llm=llm, + multimodal=True, + verbose=False, + ) + task = Task( + description="Describe the image.", + expected_output="A brief description.", + agent=agent, + input_files={"chart": image_file}, + ) + crew = Crew(agents=[agent], tasks=[task], verbose=False) + + assert crew.agents.count(agent) == 1 + + def test_prepare_kickoff_wires_task_agent_to_crew( + self, image_file: ImageFile + ) -> None: + """Task agents not in ``agents=[...]`` should still get ``agent.crew`` + set so downstream file/delegation code can find the crew.""" + from crewai.crews.utils import prepare_kickoff + from crewai.utilities.file_store import clear_files, get_all_files + + agent, task, crew = self._build_task_and_crew(image_file) + + try: + prepare_kickoff(crew, inputs=None, input_files=None) + + assert agent.crew is crew + + # After prepare_kickoff + task._store_input_files, files stored + # at the task level must be retrievable via the crew id. + task._store_input_files() + files = get_all_files(crew.id, task.id) + assert files is not None + assert "chart" in files + finally: + clear_files(crew.id) + + def test_task_prompt_includes_input_files_without_explicit_agents( + self, image_file: ImageFile + ) -> None: + """``task.prompt()`` must reference the input file even when the + crew is built without ``agents=[...]``.""" + from crewai.crews.utils import prepare_kickoff + from crewai.utilities.file_store import clear_files + + _agent, task, crew = self._build_task_and_crew(image_file) + + try: + prepare_kickoff(crew, inputs=None, input_files=None) + task._store_input_files() + + rendered = task.prompt() + + assert "chart" in rendered + finally: + clear_files(crew.id) \ No newline at end of file