fix(cli): guard crew chat description helpers against LLM failures
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled

This commit is contained in:
Greyson LaLonde
2026-04-29 10:30:24 +08:00
committed by GitHub
parent 0154d16fd8
commit 07667829e9
3 changed files with 153 additions and 9 deletions

View File

@@ -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()

View File

@@ -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

2
uv.lock generated
View File

@@ -13,7 +13,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-04-28T00:00:00Z"
exclude-newer = "2026-04-27T16:00:00Z"
[manifest]
members = [