mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-09 08:55:09 +00:00
Compare commits
3 Commits
devin/1746
...
devin/1745
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0360988835 | ||
|
|
fa39ce9db2 | ||
|
|
2f66aa0efc |
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user