From 07667829e95a116d44d2e5053d03ea04c6bce1b8 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 29 Apr 2026 10:30:24 +0800 Subject: [PATCH] fix(cli): guard crew chat description helpers against LLM failures --- lib/crewai/src/crewai/cli/crew_chat.py | 44 ++++++++-- lib/crewai/tests/cli/test_crew_chat.py | 116 +++++++++++++++++++++++++ uv.lock | 2 +- 3 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 lib/crewai/tests/cli/test_crew_chat.py diff --git a/lib/crewai/src/crewai/cli/crew_chat.py b/lib/crewai/src/crewai/cli/crew_chat.py index 61d9b4d9e..c5e170eb1 100644 --- a/lib/crewai/src/crewai/cli/crew_chat.py +++ b/lib/crewai/src/crewai/cli/crew_chat.py @@ -25,6 +25,9 @@ from crewai.utilities.version import get_crewai_version MIN_REQUIRED_VERSION: Final[Literal["0.98.0"]] = "0.98.0" +DEFAULT_INPUT_DESCRIPTION: Final[str] = "Input value for the crew's tasks and agents." +DEFAULT_CREW_DESCRIPTION: Final[str] = "A CrewAI crew." + def check_conversational_crews_version( crewai_version: str, pyproject_data: dict[str, Any] @@ -381,7 +384,10 @@ def load_crew_and_name() -> tuple[Crew, str]: def generate_crew_chat_inputs( - crew: Crew, crew_name: str, chat_llm: LLM | BaseLLM + crew: Crew, + crew_name: str, + chat_llm: LLM | BaseLLM, + generate_descriptions: bool = True, ) -> ChatInputs: """ Generates the ChatInputs required for the crew by analyzing the tasks and agents. @@ -390,21 +396,28 @@ def generate_crew_chat_inputs( crew (Crew): The crew object containing tasks and agents. crew_name (str): The name of the crew. chat_llm: The chat language model to use for AI calls. + generate_descriptions: When True (default), use the LLM to generate + input and crew descriptions. When False, skip all LLM calls and + return static defaults. Production callers that invoke this at + startup should pass ``False`` to avoid blocking on the LLM. Returns: ChatInputs: An object containing the crew's name, description, and input fields. """ - # Extract placeholders from tasks and agents required_inputs = fetch_required_inputs(crew) - # Generate descriptions for each input using AI input_fields = [] for input_name in required_inputs: - description = generate_input_description_with_ai(input_name, crew, chat_llm) + if generate_descriptions: + description = generate_input_description_with_ai(input_name, crew, chat_llm) + else: + description = DEFAULT_INPUT_DESCRIPTION input_fields.append(ChatInputField(name=input_name, description=description)) - # Generate crew description using AI - crew_description = generate_crew_description_with_ai(crew, chat_llm) + if generate_descriptions: + crew_description = generate_crew_description_with_ai(crew, chat_llm) + else: + crew_description = DEFAULT_CREW_DESCRIPTION return ChatInputs( crew_name=crew_name, crew_description=crew_description, inputs=input_fields @@ -482,7 +495,15 @@ def generate_input_description_with_ai( "Context:\n" f"{context}" ) - response = chat_llm.call(messages=[{"role": "user", "content": prompt}]) + try: + response = chat_llm.call(messages=[{"role": "user", "content": prompt}]) + except Exception as exc: + click.secho( + f"Warning: failed to generate input description for '{input_name}' " + f"({exc}); using default.", + fg="yellow", + ) + return DEFAULT_INPUT_DESCRIPTION return str(response).strip() @@ -532,5 +553,12 @@ def generate_crew_description_with_ai(crew: Crew, chat_llm: LLM | BaseLLM) -> st "Context:\n" f"{context}" ) - response = chat_llm.call(messages=[{"role": "user", "content": prompt}]) + try: + response = chat_llm.call(messages=[{"role": "user", "content": prompt}]) + except Exception as exc: + click.secho( + f"Warning: failed to generate crew description ({exc}); using default.", + fg="yellow", + ) + return DEFAULT_CREW_DESCRIPTION return str(response).strip() diff --git a/lib/crewai/tests/cli/test_crew_chat.py b/lib/crewai/tests/cli/test_crew_chat.py new file mode 100644 index 000000000..b4498c7f4 --- /dev/null +++ b/lib/crewai/tests/cli/test_crew_chat.py @@ -0,0 +1,116 @@ +"""Tests for ``crewai.cli.crew_chat`` startup-safety helpers.""" + +from unittest import mock + +from crewai.cli.crew_chat import ( + DEFAULT_CREW_DESCRIPTION, + DEFAULT_INPUT_DESCRIPTION, + generate_crew_chat_inputs, + generate_crew_description_with_ai, + generate_input_description_with_ai, +) + + +def _make_crew( + *, + task_description: str = "", + expected_output: str = "", + agent_role: str = "", + agent_goal: str = "", + agent_backstory: str = "", + inputs: set[str] | None = None, +) -> mock.Mock: + task = mock.Mock() + task.description = task_description + task.expected_output = expected_output + + agent = mock.Mock() + agent.role = agent_role + agent.goal = agent_goal + agent.backstory = agent_backstory + + crew = mock.Mock() + crew.tasks = [task] + crew.agents = [agent] + crew.fetch_inputs = mock.Mock(return_value=inputs or set()) + return crew + + +def test_generate_input_description_falls_back_on_llm_failure() -> None: + crew = _make_crew(task_description="Summarize {topic} for the team.") + chat_llm = mock.Mock() + chat_llm.call.side_effect = RuntimeError("APIConnectionError") + + description = generate_input_description_with_ai("topic", crew, chat_llm) + + assert description == DEFAULT_INPUT_DESCRIPTION + chat_llm.call.assert_called_once() + + +def test_generate_crew_description_falls_back_on_llm_failure() -> None: + crew = _make_crew(task_description="Summarize topic for the team.") + chat_llm = mock.Mock() + chat_llm.call.side_effect = RuntimeError("APIConnectionError") + + description = generate_crew_description_with_ai(crew, chat_llm) + + assert description == DEFAULT_CREW_DESCRIPTION + chat_llm.call.assert_called_once() + + +def test_generate_input_description_returns_llm_response_on_success() -> None: + crew = _make_crew(task_description="Summarize {topic} for the team.") + chat_llm = mock.Mock() + chat_llm.call.return_value = " the subject to summarize " + + description = generate_input_description_with_ai("topic", crew, chat_llm) + + assert description == "the subject to summarize" + + +def test_generate_crew_chat_inputs_skips_llm_when_descriptions_disabled() -> None: + crew = _make_crew( + task_description="Summarize {topic} for the team.", + inputs={"topic"}, + ) + chat_llm = mock.Mock() + + chat_inputs = generate_crew_chat_inputs( + crew, "demo-crew", chat_llm, generate_descriptions=False + ) + + assert chat_inputs.crew_name == "demo-crew" + assert chat_inputs.crew_description == DEFAULT_CREW_DESCRIPTION + assert len(chat_inputs.inputs) == 1 + assert chat_inputs.inputs[0].name == "topic" + assert chat_inputs.inputs[0].description == DEFAULT_INPUT_DESCRIPTION + chat_llm.call.assert_not_called() + + +def test_generate_crew_chat_inputs_uses_llm_by_default() -> None: + crew = _make_crew( + task_description="Summarize {topic} for the team.", + inputs={"topic"}, + ) + chat_llm = mock.Mock() + chat_llm.call.side_effect = ["the subject to summarize", "summarize topics"] + + chat_inputs = generate_crew_chat_inputs(crew, "demo-crew", chat_llm) + + assert chat_inputs.crew_description == "summarize topics" + assert chat_inputs.inputs[0].description == "the subject to summarize" + assert chat_llm.call.call_count == 2 + + +def test_generate_crew_chat_inputs_falls_back_when_llm_fails_mid_run() -> None: + crew = _make_crew( + task_description="Summarize {topic} for the team.", + inputs={"topic"}, + ) + chat_llm = mock.Mock() + chat_llm.call.side_effect = RuntimeError("APIConnectionError") + + chat_inputs = generate_crew_chat_inputs(crew, "demo-crew", chat_llm) + + assert chat_inputs.crew_description == DEFAULT_CREW_DESCRIPTION + assert chat_inputs.inputs[0].description == DEFAULT_INPUT_DESCRIPTION \ No newline at end of file diff --git a/uv.lock b/uv.lock index f6c4363e5..4c56c5035 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-28T00:00:00Z" +exclude-newer = "2026-04-27T16:00:00Z" [manifest] members = [