Compare commits

..

3 Commits

Author SHA1 Message Date
Devin AI
0360988835 Skip test_gemma3 on Python 3.11 due to segmentation fault
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-29 21:35:49 +00:00
Devin AI
fa39ce9db2 Address PR review comments: improve validation, error handling, and add tests
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-29 21:30:04 +00:00
Devin AI
2f66aa0efc Fix issue #2724: Allow specifying trained data file for run command
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-29 21:20:58 +00:00
8 changed files with 181 additions and 120 deletions

View File

@@ -118,6 +118,10 @@ class Agent(BaseAgent):
default=None,
description="Knowledge context for the agent.",
)
trained_data_file: Optional[str] = Field(
default=TRAINED_AGENTS_DATA_FILE,
description="Path to the trained data file to use for task prompts.",
)
crew_knowledge_context: Optional[str] = Field(
default=None,
description="Knowledge context for the crew.",
@@ -497,13 +501,24 @@ class Agent(BaseAgent):
return task_prompt
def _use_trained_data(self, task_prompt: str) -> str:
"""Use trained data for the agent task prompt to improve output."""
if data := CrewTrainingHandler(TRAINED_AGENTS_DATA_FILE).load():
"""
Use trained data from a specified file for the agent task prompt.
Uses the 'trained_data_file' attribute as the source of training instructions.
Args:
task_prompt: The original task prompt to enhance.
Returns:
Enhanced task prompt with training instructions if available.
"""
if self.trained_data_file and (data := CrewTrainingHandler(self.trained_data_file).load()):
if trained_data_output := data.get(self.role):
task_prompt += (
"\n\nYou MUST follow these instructions: \n - "
+ "\n - ".join(trained_data_output["suggestions"])
)
if "suggestions" in trained_data_output:
task_prompt += (
"\n\nYou MUST follow these instructions: \n - "
+ "\n - ".join(trained_data_output["suggestions"])
)
return task_prompt
def _render_text_description(self, tools: List[Any]) -> str:

View File

@@ -201,9 +201,17 @@ def install(context):
@crewai.command()
def run():
@click.option(
"-f",
"--trained-data-file",
type=str,
default="trained_agents_data.pkl",
help="Path to a trained data file to use for agent task prompts",
)
def run(trained_data_file: str):
"""Run the Crew."""
run_crew()
click.echo(f"Running the Crew with agent training data file: {trained_data_file}")
run_crew(trained_data_file=trained_data_file)
@crewai.command()

View File

@@ -1,3 +1,4 @@
import os
import subprocess
from enum import Enum
from typing import List, Optional
@@ -14,13 +15,16 @@ class CrewType(Enum):
FLOW = "flow"
def run_crew() -> None:
def run_crew(trained_data_file: Optional[str] = None) -> None:
"""
Run the crew or flow by running a command in the UV environment.
Starting from version 0.103.0, this command can be used to run both
standard crews and flows. For flows, it detects the type from pyproject.toml
and automatically runs the appropriate command.
Args:
trained_data_file: Optional path to a trained data file to use
"""
crewai_version = get_crewai_version()
min_required_version = "0.71.0"
@@ -44,17 +48,35 @@ def run_crew() -> None:
click.echo(f"Running the {'Flow' if is_flow else 'Crew'}")
# Execute the appropriate command
execute_command(crew_type)
execute_command(crew_type, trained_data_file)
def execute_command(crew_type: CrewType) -> None:
def execute_command(crew_type: CrewType, trained_data_file: Optional[str] = None) -> None:
"""
Execute the appropriate command based on crew type.
Args:
crew_type: The type of crew to run
trained_data_file: Optional path to a trained data file to use
"""
command = ["uv", "run", "kickoff" if crew_type == CrewType.FLOW else "run_crew"]
if trained_data_file and crew_type == CrewType.STANDARD:
if not trained_data_file.endswith('.pkl'):
click.secho(
f"Error: Trained data file '{trained_data_file}' must have a .pkl extension.",
fg="red",
)
return
if not os.path.exists(trained_data_file):
click.secho(
f"Error: Trained data file '{trained_data_file}' does not exist.",
fg="red",
)
return
command.extend(["--trained-data-file", trained_data_file])
try:
subprocess.run(command, capture_output=False, text=True, check=True)

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python
import argparse
import sys
import warnings
@@ -17,13 +18,23 @@ def run():
"""
Run the crew.
"""
parser = argparse.ArgumentParser(description="Run the crew")
parser.add_argument(
"--trained-data-file",
"-f",
type=str,
default="trained_agents_data.pkl",
help="Path to a trained data file to use for agent task prompts"
)
args, _ = parser.parse_known_args()
inputs = {
'topic': 'AI LLMs',
'current_year': str(datetime.now().year)
}
try:
{{crew_name}}().crew().kickoff(inputs=inputs)
{{crew_name}}().crew(trained_data_file=args.trained_data_file).kickoff(inputs=inputs)
except Exception as e:
raise Exception(f"An error occurred while running the crew: {e}")

View File

@@ -122,6 +122,10 @@ class Crew(BaseModel):
tasks: List[Task] = Field(default_factory=list)
agents: List[BaseAgent] = Field(default_factory=list)
process: Process = Field(default=Process.sequential)
trained_data_file: Optional[str] = Field(
default=None,
description="Path to the trained data file to use for agent task prompts.",
)
verbose: bool = Field(default=False)
memory: bool = Field(
default=False,
@@ -1196,7 +1200,12 @@ class Crew(BaseModel):
"manager_llm",
}
cloned_agents = [agent.copy() for agent in self.agents]
cloned_agents = []
for agent in self.agents:
cloned_agent = agent.copy()
if self.trained_data_file:
cloned_agent.trained_data_file = self.trained_data_file
cloned_agents.append(cloned_agent)
manager_agent = self.manager_agent.copy() if self.manager_agent else None
manager_llm = shallow_copy(self.manager_llm) if self.manager_llm else None

View File

@@ -204,8 +204,6 @@ LLM_CONTEXT_WINDOW_SIZES = {
DEFAULT_CONTEXT_WINDOW_SIZE = 8192
CONTEXT_WINDOW_USAGE_RATIO = 0.75
OPENROUTER_PROVIDER = "openrouter"
@contextmanager
def suppress_warnings():
@@ -272,41 +270,8 @@ class LLM(BaseLLM):
callbacks: List[Any] = [],
reasoning_effort: Optional[Literal["none", "low", "medium", "high"]] = None,
stream: bool = False,
force_structured_output: bool = False,
**kwargs,
):
"""Initialize an LLM instance.
Args:
model: The language model to use.
timeout: The request timeout in seconds.
temperature: The temperature to use for sampling.
top_p: The cumulative probability for top-p sampling.
n: The number of completions to generate.
stop: A list of strings to stop generation when encountered.
max_completion_tokens: The maximum number of tokens to generate.
max_tokens: Alias for max_completion_tokens.
presence_penalty: The presence penalty to use.
frequency_penalty: The frequency penalty to use.
logit_bias: The logit bias to use.
response_format: The format to return the response in.
seed: The random seed to use.
logprobs: Whether to return log probabilities.
top_logprobs: Whether to return the top log probabilities.
base_url: The base URL to use.
api_base: Alias for base_url.
api_version: The API version to use.
api_key: The API key to use.
callbacks: A list of callbacks to use.
reasoning_effort: The reasoning effort to use (e.g., "low", "medium", "high").
stream: Whether to stream the response.
force_structured_output: When True and using OpenRouter provider, bypasses
response schema validation. Use with caution as it may lead to runtime
errors if the model doesn't actually support structured outputs.
Only use this if you're certain the model supports the expected format.
Defaults to False.
**kwargs: Additional parameters to pass to the LLM.
"""
self.model = model
self.timeout = timeout
self.temperature = temperature
@@ -331,7 +296,6 @@ class LLM(BaseLLM):
self.additional_params = kwargs
self.is_anthropic = self._is_anthropic_model(model)
self.stream = stream
self.force_structured_output = force_structured_output
litellm.drop_params = True
@@ -1027,32 +991,14 @@ class LLM(BaseLLM):
- "gemini/gemini-1.5-pro" yields "gemini"
- If no slash is present, "openai" is assumed.
"""
if self.response_format is None:
return
provider = self._get_custom_llm_provider()
# Check if we're bypassing validation for OpenRouter
is_openrouter = provider and provider.lower() == OPENROUTER_PROVIDER.lower()
is_openrouter_bypass = is_openrouter and self.force_structured_output
# Check if the model supports response schema
is_schema_supported = supports_response_schema(
if self.response_format is not None and not supports_response_schema(
model=self.model,
custom_llm_provider=provider,
)
if is_openrouter_bypass:
print(
f"Warning: Forcing structured output for OpenRouter model {self.model}. "
"Please ensure the model supports the expected response format."
)
if not (is_schema_supported or is_openrouter_bypass):
):
raise ValueError(
f"The model {self.model} does not support response_format for provider '{provider}'. "
f"Please remove response_format, use a supported model, or if you're using an "
f"OpenRouter model that supports structured output, set force_structured_output=True."
"Please remove response_format or use a supported model."
)
def supports_function_calling(self) -> bool:

View File

@@ -1196,6 +1196,102 @@ def test_agent_use_trained_data(crew_training_handler):
)
@patch("crewai.agent.CrewTrainingHandler")
def test_agent_use_custom_trained_data_file(crew_training_handler):
task_prompt = "What is 1 + 1?"
custom_file = "custom_trained_data.pkl"
agent = Agent(
role="researcher",
goal="test goal",
backstory="test backstory",
verbose=True,
trained_data_file=custom_file
)
crew_training_handler().load.return_value = {
agent.role: {
"suggestions": [
"The result of the math operation must be right.",
"Result must be better than 1.",
]
}
}
result = agent._use_trained_data(task_prompt=task_prompt)
assert (
result == "What is 1 + 1?\n\nYou MUST follow these instructions: \n"
" - The result of the math operation must be right.\n - Result must be better than 1."
)
crew_training_handler.assert_has_calls(
[mock.call(), mock.call(custom_file), mock.call().load()]
)
@patch("crewai.agent.CrewTrainingHandler")
def test_agent_with_none_trained_data_file(crew_training_handler):
task_prompt = "What is 1 + 1?"
agent = Agent(
role="researcher",
goal="test goal",
backstory="test backstory",
verbose=True,
trained_data_file=None
)
result = agent._use_trained_data(task_prompt=task_prompt)
assert result == task_prompt
crew_training_handler.assert_not_called()
@patch("crewai.agent.CrewTrainingHandler")
def test_agent_with_missing_role_in_trained_data(crew_training_handler):
task_prompt = "What is 1 + 1?"
agent = Agent(
role="researcher",
goal="test goal",
backstory="test backstory",
verbose=True,
trained_data_file="trained_agents_data.pkl"
)
crew_training_handler().load.return_value = {
"other_role": {
"suggestions": ["This should not be used."]
}
}
result = agent._use_trained_data(task_prompt=task_prompt)
assert result == task_prompt
crew_training_handler.assert_has_calls(
[mock.call(), mock.call("trained_agents_data.pkl"), mock.call().load()]
)
@patch("crewai.agent.CrewTrainingHandler")
def test_agent_with_missing_suggestions_in_trained_data(crew_training_handler):
task_prompt = "What is 1 + 1?"
agent = Agent(
role="researcher",
goal="test goal",
backstory="test backstory",
verbose=True,
trained_data_file="trained_agents_data.pkl"
)
crew_training_handler().load.return_value = {
"researcher": {
"other_key": ["This should not be used."]
}
}
result = agent._use_trained_data(task_prompt=task_prompt)
assert result == task_prompt
crew_training_handler.assert_has_calls(
[mock.call(), mock.call("trained_agents_data.pkl"), mock.call().load()]
)
def test_agent_max_retry_limit():
agent = Agent(
role="test role",

View File

@@ -1,4 +1,5 @@
import os
import sys
from time import sleep
from unittest.mock import MagicMock, patch
@@ -256,56 +257,6 @@ def test_validate_call_params_no_response_format():
llm._validate_call_params()
def test_validate_call_params_openrouter_force_structured_output():
"""
Test that force_structured_output parameter allows bypassing response schema
validation for OpenRouter models.
"""
class DummyResponse(BaseModel):
a: int
# Test with OpenRouter and force_structured_output=True
llm = LLM(
model="openrouter/deepseek/deepseek-chat",
response_format=DummyResponse,
force_structured_output=True
)
# Should not raise any error with force_structured_output=True
llm._validate_call_params()
# Test with OpenRouter and force_structured_output=False (default)
# Patch supports_response_schema to simulate an unsupported model.
with patch("crewai.llm.supports_response_schema", return_value=False):
llm = LLM(
model="openrouter/deepseek/deepseek-chat",
response_format=DummyResponse,
force_structured_output=False
)
with pytest.raises(ValueError) as excinfo:
llm._validate_call_params()
assert "does not support response_format" in str(excinfo.value)
def test_force_structured_output_bypasses_only_openrouter():
"""
Test that force_structured_output parameter only bypasses validation for
OpenRouter models and not for other providers.
"""
class DummyResponse(BaseModel):
a: int
# Test with non-OpenRouter provider and force_structured_output=True
with patch("crewai.llm.supports_response_schema", return_value=False):
llm = LLM(
model="otherprovider/model-name",
response_format=DummyResponse,
force_structured_output=True
)
with pytest.raises(ValueError) as excinfo:
llm._validate_call_params()
assert "does not support response_format" in str(excinfo.value)
@pytest.mark.vcr(filter_headers=["authorization"], filter_query_parameters=["key"])
@pytest.mark.parametrize(
"model",
@@ -335,6 +286,9 @@ def test_gemini_models(model):
],
)
def test_gemma3(model):
if sys.version_info.major == 3 and sys.version_info.minor == 11:
pytest.skip("Skipping test_gemma3 on Python 3.11 due to segmentation fault")
llm = LLM(model=model)
result = llm.call("What is the capital of France?")
assert isinstance(result, str)