mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-27 01:38:29 +00:00
Compare commits
6 Commits
v0.41.1
...
doc_genera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0fc50eab5 | ||
|
|
2d086ab596 | ||
|
|
776c67cc0f | ||
|
|
78ef490646 | ||
|
|
4da5cc9778 | ||
|
|
6930656897 |
@@ -16,7 +16,7 @@ We assume you have already installed CrewAI. If not, please refer to the [instal
|
||||
To create a new project, run the following CLI command:
|
||||
|
||||
```shell
|
||||
$ crewai create my_project
|
||||
$ crewai create <project_name>
|
||||
```
|
||||
|
||||
This command will create a new project folder with the following structure:
|
||||
@@ -79,8 +79,77 @@ research_candidates_task:
|
||||
{job_requirements}
|
||||
expected_output: >
|
||||
A list of 10 potential candidates with their contact information and brief profiles highlighting their suitability.
|
||||
agent: researcher # THIS NEEDS TO MATCH THE AGENT NAME IN THE AGENTS.YAML FILE AND THE AGENT DEFINED IN THE Crew.PY FILE
|
||||
context: # THESE NEED TO MATCH THE TASK NAMES DEFINED ABOVE AND THE TASKS.YAML FILE AND THE TASK DEFINED IN THE Crew.PY FILE
|
||||
- researcher
|
||||
```
|
||||
|
||||
### Referencing Variables:
|
||||
Your defined functions with the same name will be used. For example, you can reference the agent for specific tasks from task.yaml file. Ensure your annotated agent and function name is the same otherwise your task wont recognize the reference properly.
|
||||
|
||||
#### Example References
|
||||
agent.yaml
|
||||
```yaml
|
||||
email_summarizer:
|
||||
role: >
|
||||
Email Summarizer
|
||||
goal: >
|
||||
Summarize emails into a concise and clear summary
|
||||
backstory: >
|
||||
You will create a 5 bullet point summary of the report
|
||||
llm: mixtal_llm
|
||||
```
|
||||
|
||||
task.yaml
|
||||
```yaml
|
||||
email_summarizer_task:
|
||||
description: >
|
||||
Summarize the email into a 5 bullet point summary
|
||||
expected_output: >
|
||||
A 5 bullet point summary of the email
|
||||
agent: email_summarizer
|
||||
context:
|
||||
- reporting_task
|
||||
- research_task
|
||||
```
|
||||
|
||||
Use the annotations are used to properly reference the agent and task in the crew.py file.
|
||||
|
||||
### Annotations include:
|
||||
* @agent
|
||||
* @task
|
||||
* @crew
|
||||
* @llm
|
||||
* @tool
|
||||
* @callback
|
||||
* @output_json
|
||||
* @output_pydantic
|
||||
* @cache_handler
|
||||
|
||||
|
||||
crew.py
|
||||
```py
|
||||
...
|
||||
@llm
|
||||
def mixtal_llm(self):
|
||||
return ChatGroq(temperature=0, model_name="mixtral-8x7b-32768")
|
||||
|
||||
@agent
|
||||
def email_summarizer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config["email_summarizer"],
|
||||
)
|
||||
## ...other tasks defined
|
||||
@task
|
||||
def email_summarizer_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config["email_summarizer_task"],
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
To install the dependencies for your project, you can use Poetry. First, navigate to your project directory:
|
||||
|
||||
@@ -5,11 +5,12 @@ from crewai.memory.storage.kickoff_task_outputs_storage import (
|
||||
KickoffTaskOutputsSQLiteStorage,
|
||||
)
|
||||
|
||||
|
||||
from .create_crew import create_crew
|
||||
from .train_crew import train_crew
|
||||
from .replay_from_task import replay_task_command
|
||||
from .reset_memories_command import reset_memories_command
|
||||
from .test_crew import test_crew
|
||||
from .train_crew import train_crew
|
||||
from .doc_generator import generate_documentation
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -126,5 +127,38 @@ def reset_memories(long, short, entities, kickoff_outputs, all):
|
||||
click.echo(f"An error occurred while resetting memories: {e}", err=True)
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@click.option(
|
||||
"-n",
|
||||
"--n_iterations",
|
||||
type=int,
|
||||
default=3,
|
||||
help="Number of iterations to Test the crew",
|
||||
)
|
||||
@click.option(
|
||||
"-m",
|
||||
"--model",
|
||||
type=str,
|
||||
default="gpt-4o-mini",
|
||||
help="LLM Model to run the tests on the Crew. For now only accepting only OpenAI models.",
|
||||
)
|
||||
def test(n_iterations: int, model: str):
|
||||
"""Test the crew and evaluate the results."""
|
||||
click.echo(f"Testing the crew for {n_iterations} iterations with model {model}")
|
||||
test_crew(n_iterations, model)
|
||||
|
||||
@crewai.command()
|
||||
@click.option('--output', '-o', default='crew_documentation.md', help='Output file for the documentation')
|
||||
@click.option('--format', '-f', default='markdown', help='Output format')
|
||||
def generate_docs(output, format):
|
||||
"""Generate documentation for the current project setup."""
|
||||
try:
|
||||
click.echo(f"Generating documentation in {format} format...")
|
||||
generate_documentation(output, format)
|
||||
click.echo(f"Documentation generated and saved to {output}")
|
||||
except ValueError as e:
|
||||
click.echo(f"Error: {str(e)}", err=True)
|
||||
click.echo("Please ensure you are in the root directory of your CrewAI project.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
crewai()
|
||||
|
||||
204
src/crewai/cli/doc_generator.py
Normal file
204
src/crewai/cli/doc_generator.py
Normal file
@@ -0,0 +1,204 @@
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
|
||||
|
||||
def is_project_root():
|
||||
"""
|
||||
Check if the current directory is the root of a CrewAI project.
|
||||
|
||||
Returns:
|
||||
bool: True if in project root, False otherwise.
|
||||
"""
|
||||
# Check for key indicators of a CrewAI project root
|
||||
indicators = ["pyproject.toml", "poetry.lock", "src"]
|
||||
return all(os.path.exists(indicator) for indicator in indicators)
|
||||
|
||||
|
||||
def generate_documentation(output_file, format):
|
||||
"""
|
||||
Generate documentation for the current CrewAI project setup.
|
||||
|
||||
Args:
|
||||
output_file (str): The path and filename where the generated documentation
|
||||
will be saved.
|
||||
format (str): The desired output format for the documentation.
|
||||
Supported values currently 'markdown'.
|
||||
|
||||
Returns:
|
||||
None: The function writes the generated documentation to the specified
|
||||
output file and doesn't return any value.
|
||||
|
||||
Raises:
|
||||
ValueError: If not in the project root or if an unsupported output format is specified.
|
||||
"""
|
||||
if not is_project_root():
|
||||
raise ValueError(
|
||||
"Not in the root of a CrewAI project."
|
||||
)
|
||||
|
||||
# Load the current project configuration
|
||||
config = load_crew_configuration()
|
||||
|
||||
if config is None:
|
||||
logging.error("Failed to load crew configuration. Exiting.")
|
||||
return
|
||||
|
||||
if format == "markdown":
|
||||
content = generate_markdown(config)
|
||||
else:
|
||||
raise ValueError(f"Unsupported output format: {format}")
|
||||
|
||||
with open(output_file, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
logging.info(f"Documentation generated and saved to {output_file}")
|
||||
|
||||
|
||||
def find_config_dir():
|
||||
"""
|
||||
Find the configuration directory based on the project structure.
|
||||
|
||||
This function attempts to locate the configuration directory for a CrewAI project
|
||||
by assuming a standard project structure. It starts from the current working
|
||||
directory and constructs an expected path to the config directory.
|
||||
|
||||
Returns:
|
||||
str or None: The path to the configuration directory if found, None otherwise.
|
||||
|
||||
The function performs the following steps:
|
||||
1. Gets the current working directory.
|
||||
2. Extracts the project name from the current directory path.
|
||||
3. Constructs the expected config path using the project structure convention.
|
||||
4. Checks if the expected config directory exists.
|
||||
5. Returns the path if found, or None if not found.
|
||||
|
||||
Logging:
|
||||
- Logs debug information about the search process.
|
||||
- Logs the starting directory, the checked path, and the result of the search.
|
||||
|
||||
Note:
|
||||
This function assumes a specific project structure where the config
|
||||
directory is located at 'src/<project_name>/config' relative to the
|
||||
project root.
|
||||
"""
|
||||
current_dir = os.getcwd()
|
||||
logging.debug(f"Starting search from: {current_dir}")
|
||||
|
||||
# Split the path to get the project name
|
||||
path_parts = current_dir.split(os.path.sep)
|
||||
project_name = path_parts[-1]
|
||||
|
||||
# Construct the expected config path
|
||||
expected_config_path = os.path.join(current_dir, "src", project_name, "config")
|
||||
|
||||
logging.debug(f"Checking for config directory: {expected_config_path}")
|
||||
|
||||
if os.path.isdir(expected_config_path):
|
||||
logging.debug(f"Found config directory: {expected_config_path}")
|
||||
return expected_config_path
|
||||
|
||||
logging.debug("Config directory not found in the expected location")
|
||||
return None
|
||||
|
||||
|
||||
def load_crew_configuration():
|
||||
"""
|
||||
Load the crew configuration from YAML files.
|
||||
|
||||
This function attempts to find the configuration directory and load the agents
|
||||
and tasks configurations from their respective YAML files.
|
||||
|
||||
Returns:
|
||||
dict or None: A dictionary containing 'agents' and 'tasks' configurations
|
||||
if successful, None if there was an error.
|
||||
|
||||
The function performs the following steps:
|
||||
1. Finds the configuration directory using find_config_dir().
|
||||
2. Constructs paths to agents.yaml and tasks.yaml files.
|
||||
3. Checks if both files exist.
|
||||
4. Loads and parses the YAML content of both files.
|
||||
5. Returns a dictionary with the parsed configurations.
|
||||
|
||||
Logging:
|
||||
- Logs an error if the configuration directory is not found.
|
||||
- Logs an error if either agents.yaml or tasks.yaml is not found.
|
||||
|
||||
Note:
|
||||
This function assumes that the configuration files are named 'agents.yaml'
|
||||
and 'tasks.yaml' and are located in the directory returned by find_config_dir().
|
||||
"""
|
||||
config_dir = find_config_dir()
|
||||
if not config_dir:
|
||||
logging.error(
|
||||
"Configuration directory not found. Make sure you're in the root of your CrewAI project."
|
||||
)
|
||||
return None
|
||||
|
||||
agents_file = os.path.join(config_dir, "agents.yaml")
|
||||
tasks_file = os.path.join(config_dir, "tasks.yaml")
|
||||
|
||||
if not os.path.exists(agents_file) or not os.path.exists(tasks_file):
|
||||
logging.error(f"agents.yaml or tasks.yaml not found in {config_dir}")
|
||||
return None
|
||||
|
||||
with open(agents_file, "r") as f:
|
||||
agents_config = yaml.safe_load(f)
|
||||
|
||||
with open(tasks_file, "r") as f:
|
||||
tasks_config = yaml.safe_load(f)
|
||||
|
||||
return {"agents": agents_config, "tasks": tasks_config}
|
||||
|
||||
|
||||
def generate_markdown(config):
|
||||
"""
|
||||
Generate Markdown documentation for the CrewAI project configuration.
|
||||
|
||||
This function takes the parsed configuration dictionary and generates
|
||||
a formatted Markdown string containing documentation for the project's
|
||||
agents and tasks.
|
||||
|
||||
Args:
|
||||
config (dict): A dictionary containing the parsed configuration
|
||||
with 'agents' and 'tasks' keys.
|
||||
|
||||
Returns:
|
||||
str: A formatted Markdown string containing the project documentation.
|
||||
If the input config is None, it returns an error message.
|
||||
|
||||
The generated Markdown includes:
|
||||
1. A title for the project documentation.
|
||||
2. A section for Agents, listing each agent's name, role, goal, and backstory.
|
||||
3. A section for Tasks, listing each task's name, description, expected output,
|
||||
and assigned agent.
|
||||
|
||||
Each piece of information is wrapped in code blocks for better readability
|
||||
in rendered Markdown.
|
||||
|
||||
Note:
|
||||
This function assumes that the config dictionary has the correct structure
|
||||
with 'agents' and 'tasks' keys, each containing nested dictionaries of
|
||||
agent and task information respectively.
|
||||
"""
|
||||
if config is None:
|
||||
return "# Error: No crew configuration available"
|
||||
|
||||
md = "# CrewAI Project Documentation\n\n"
|
||||
|
||||
md += "## Agents\n\n"
|
||||
for agent_name, agent_data in config["agents"].items():
|
||||
md += f"### \n```\n{agent_name}\n```\n"
|
||||
md += f"Role: \n```\n{agent_data.get('role', 'Not specified')}\n```\n"
|
||||
md += f"Goal: \n```\n{agent_data.get('goal', 'Not specified')}\n```\n"
|
||||
md += f"Backstory: \n```\n{agent_data.get('backstory', 'Not specified')}\n```\n"
|
||||
md += f""
|
||||
|
||||
md += "## Tasks\n\n"
|
||||
for task_name, task_data in config["tasks"].items():
|
||||
md += f"### {task_name}\n"
|
||||
md += f"Description: \n```\n{task_data.get('description', 'Not specified')}\n```\n"
|
||||
md += f"Expected Output: \n```\n{task_data.get('expected_output', 'Not specified')}\n```\n"
|
||||
md += f"Assigned Agent: \n```\n{task_data.get('agent', 'Not assigned')}\n```\n"
|
||||
|
||||
return md
|
||||
@@ -5,6 +5,7 @@ research_task:
|
||||
the current year is 2024.
|
||||
expected_output: >
|
||||
A list with 10 bullet points of the most relevant information about {topic}
|
||||
agent: researcher
|
||||
|
||||
reporting_task:
|
||||
description: >
|
||||
@@ -13,3 +14,4 @@ reporting_task:
|
||||
expected_output: >
|
||||
A fully fledge reports with the mains topics, each with a full section of information.
|
||||
Formatted as markdown without '```'
|
||||
agent: reporting_analyst
|
||||
|
||||
@@ -32,14 +32,12 @@ class {{crew_name}}Crew():
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['research_task'],
|
||||
agent=self.researcher()
|
||||
)
|
||||
|
||||
@task
|
||||
def reporting_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['reporting_task'],
|
||||
agent=self.reporting_analyst(),
|
||||
output_file='report.md'
|
||||
)
|
||||
|
||||
|
||||
@@ -39,3 +39,16 @@ def replay():
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"An error occurred while replaying the crew: {e}")
|
||||
|
||||
def test():
|
||||
"""
|
||||
Test the crew execution and returns the results.
|
||||
"""
|
||||
inputs = {
|
||||
"topic": "AI LLMs"
|
||||
}
|
||||
try:
|
||||
{{crew_name}}Crew().crew().test(n_iterations=int(sys.argv[1]), model=sys.argv[2], inputs=inputs)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"An error occurred while replaying the crew: {e}")
|
||||
|
||||
@@ -12,6 +12,7 @@ crewai = { extras = ["tools"], version = "^0.41.1" }
|
||||
{{folder_name}} = "{{folder_name}}.main:run"
|
||||
train = "{{folder_name}}.main:train"
|
||||
replay = "{{folder_name}}.main:replay"
|
||||
test = "{{folder_name}}.main:test"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
30
src/crewai/cli/test_crew.py
Normal file
30
src/crewai/cli/test_crew.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def test_crew(n_iterations: int, model: str) -> None:
|
||||
"""
|
||||
Test the crew by running a command in the Poetry environment.
|
||||
|
||||
Args:
|
||||
n_iterations (int): The number of iterations to test the crew.
|
||||
model (str): The model to test the crew with.
|
||||
"""
|
||||
command = ["poetry", "run", "test", str(n_iterations), model]
|
||||
|
||||
try:
|
||||
if n_iterations <= 0:
|
||||
raise ValueError("The number of iterations must be a positive integer.")
|
||||
|
||||
result = subprocess.run(command, capture_output=False, text=True, check=True)
|
||||
|
||||
if result.stderr:
|
||||
click.echo(result.stderr, err=True)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.echo(f"An error occurred while testing the crew: {e}", err=True)
|
||||
click.echo(e.output, err=True)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||
@@ -966,5 +966,11 @@ class Crew(BaseModel):
|
||||
|
||||
return total_usage_metrics
|
||||
|
||||
def test(
|
||||
self, n_iterations: int, model: str, inputs: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""Test the crew with the given inputs."""
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return f"Crew(id={self.id}, process={self.process}, number_of_agents={len(self.agents)}, number_of_tasks={len(self.tasks)})"
|
||||
|
||||
@@ -1,2 +1,25 @@
|
||||
from .annotations import agent, crew, task
|
||||
from .annotations import (
|
||||
agent,
|
||||
crew,
|
||||
task,
|
||||
output_json,
|
||||
output_pydantic,
|
||||
tool,
|
||||
callback,
|
||||
llm,
|
||||
cache_handler,
|
||||
)
|
||||
from .crew_base import CrewBase
|
||||
|
||||
__all__ = [
|
||||
"agent",
|
||||
"crew",
|
||||
"task",
|
||||
"output_json",
|
||||
"output_pydantic",
|
||||
"tool",
|
||||
"callback",
|
||||
"CrewBase",
|
||||
"llm",
|
||||
"cache_handler",
|
||||
]
|
||||
|
||||
@@ -30,6 +30,37 @@ def agent(func):
|
||||
return func
|
||||
|
||||
|
||||
def llm(func):
|
||||
func.is_llm = True
|
||||
func = memoize(func)
|
||||
return func
|
||||
|
||||
|
||||
def output_json(cls):
|
||||
cls.is_output_json = True
|
||||
return cls
|
||||
|
||||
|
||||
def output_pydantic(cls):
|
||||
cls.is_output_pydantic = True
|
||||
return cls
|
||||
|
||||
|
||||
def tool(func):
|
||||
func.is_tool = True
|
||||
return memoize(func)
|
||||
|
||||
|
||||
def callback(func):
|
||||
func.is_callback = True
|
||||
return memoize(func)
|
||||
|
||||
|
||||
def cache_handler(func):
|
||||
func.is_cache_handler = True
|
||||
return memoize(func)
|
||||
|
||||
|
||||
def crew(func):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
instantiated_tasks = []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import inspect
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
@@ -20,11 +21,6 @@ def CrewBase(cls):
|
||||
base_directory = Path(frame_info.filename).parent.resolve()
|
||||
break
|
||||
|
||||
if base_directory is None:
|
||||
raise Exception(
|
||||
"Unable to dynamically determine the project's base directory, you must run it from the project's root directory."
|
||||
)
|
||||
|
||||
original_agents_config_path = getattr(
|
||||
cls, "agents_config", "config/agents.yaml"
|
||||
)
|
||||
@@ -32,12 +28,20 @@ def CrewBase(cls):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.base_directory is None:
|
||||
raise Exception(
|
||||
"Unable to dynamically determine the project's base directory, you must run it from the project's root directory."
|
||||
)
|
||||
|
||||
self.agents_config = self.load_yaml(
|
||||
os.path.join(self.base_directory, self.original_agents_config_path)
|
||||
)
|
||||
self.tasks_config = self.load_yaml(
|
||||
os.path.join(self.base_directory, self.original_tasks_config_path)
|
||||
)
|
||||
self.map_all_agent_variables()
|
||||
self.map_all_task_variables()
|
||||
|
||||
@staticmethod
|
||||
def load_yaml(config_path: str):
|
||||
@@ -45,4 +49,138 @@ def CrewBase(cls):
|
||||
# parsedContent = YamlParser.parse(file) # type: ignore # Argument 1 to "parse" has incompatible type "TextIOWrapper"; expected "YamlParser"
|
||||
return yaml.safe_load(file)
|
||||
|
||||
def _get_all_functions(self):
|
||||
return {
|
||||
name: getattr(self, name)
|
||||
for name in dir(self)
|
||||
if callable(getattr(self, name))
|
||||
}
|
||||
|
||||
def _filter_functions(
|
||||
self, functions: Dict[str, Callable], attribute: str
|
||||
) -> Dict[str, Callable]:
|
||||
return {
|
||||
name: func
|
||||
for name, func in functions.items()
|
||||
if hasattr(func, attribute)
|
||||
}
|
||||
|
||||
def map_all_agent_variables(self) -> None:
|
||||
all_functions = self._get_all_functions()
|
||||
llms = self._filter_functions(all_functions, "is_llm")
|
||||
tool_functions = self._filter_functions(all_functions, "is_tool")
|
||||
cache_handler_functions = self._filter_functions(
|
||||
all_functions, "is_cache_handler"
|
||||
)
|
||||
callbacks = self._filter_functions(all_functions, "is_callback")
|
||||
agents = self._filter_functions(all_functions, "is_agent")
|
||||
|
||||
for agent_name, agent_info in self.agents_config.items():
|
||||
self._map_agent_variables(
|
||||
agent_name,
|
||||
agent_info,
|
||||
agents,
|
||||
llms,
|
||||
tool_functions,
|
||||
cache_handler_functions,
|
||||
callbacks,
|
||||
)
|
||||
|
||||
def _map_agent_variables(
|
||||
self,
|
||||
agent_name: str,
|
||||
agent_info: Dict[str, Any],
|
||||
agents: Dict[str, Callable],
|
||||
llms: Dict[str, Callable],
|
||||
tool_functions: Dict[str, Callable],
|
||||
cache_handler_functions: Dict[str, Callable],
|
||||
callbacks: Dict[str, Callable],
|
||||
) -> None:
|
||||
if llm := agent_info.get("llm"):
|
||||
self.agents_config[agent_name]["llm"] = llms[llm]()
|
||||
|
||||
if tools := agent_info.get("tools"):
|
||||
self.agents_config[agent_name]["tools"] = [
|
||||
tool_functions[tool]() for tool in tools
|
||||
]
|
||||
|
||||
if function_calling_llm := agent_info.get("function_calling_llm"):
|
||||
self.agents_config[agent_name]["function_calling_llm"] = agents[
|
||||
function_calling_llm
|
||||
]()
|
||||
|
||||
if step_callback := agent_info.get("step_callback"):
|
||||
self.agents_config[agent_name]["step_callback"] = callbacks[
|
||||
step_callback
|
||||
]()
|
||||
|
||||
if cache_handler := agent_info.get("cache_handler"):
|
||||
self.agents_config[agent_name]["cache_handler"] = (
|
||||
cache_handler_functions[cache_handler]()
|
||||
)
|
||||
|
||||
def map_all_task_variables(self) -> None:
|
||||
all_functions = self._get_all_functions()
|
||||
agents = self._filter_functions(all_functions, "is_agent")
|
||||
tasks = self._filter_functions(all_functions, "is_task")
|
||||
output_json_functions = self._filter_functions(
|
||||
all_functions, "is_output_json"
|
||||
)
|
||||
tool_functions = self._filter_functions(all_functions, "is_tool")
|
||||
callback_functions = self._filter_functions(all_functions, "is_callback")
|
||||
output_pydantic_functions = self._filter_functions(
|
||||
all_functions, "is_output_pydantic"
|
||||
)
|
||||
|
||||
for task_name, task_info in self.tasks_config.items():
|
||||
self._map_task_variables(
|
||||
task_name,
|
||||
task_info,
|
||||
agents,
|
||||
tasks,
|
||||
output_json_functions,
|
||||
tool_functions,
|
||||
callback_functions,
|
||||
output_pydantic_functions,
|
||||
)
|
||||
|
||||
def _map_task_variables(
|
||||
self,
|
||||
task_name: str,
|
||||
task_info: Dict[str, Any],
|
||||
agents: Dict[str, Callable],
|
||||
tasks: Dict[str, Callable],
|
||||
output_json_functions: Dict[str, Callable],
|
||||
tool_functions: Dict[str, Callable],
|
||||
callback_functions: Dict[str, Callable],
|
||||
output_pydantic_functions: Dict[str, Callable],
|
||||
) -> None:
|
||||
if context_list := task_info.get("context"):
|
||||
self.tasks_config[task_name]["context"] = [
|
||||
tasks[context_task_name]() for context_task_name in context_list
|
||||
]
|
||||
|
||||
if tools := task_info.get("tools"):
|
||||
self.tasks_config[task_name]["tools"] = [
|
||||
tool_functions[tool]() for tool in tools
|
||||
]
|
||||
|
||||
if agent_name := task_info.get("agent"):
|
||||
self.tasks_config[task_name]["agent"] = agents[agent_name]()
|
||||
|
||||
if output_json := task_info.get("output_json"):
|
||||
self.tasks_config[task_name]["output_json"] = output_json_functions[
|
||||
output_json
|
||||
]
|
||||
|
||||
if output_pydantic := task_info.get("output_pydantic"):
|
||||
self.tasks_config[task_name]["output_pydantic"] = (
|
||||
output_pydantic_functions[output_pydantic]
|
||||
)
|
||||
|
||||
if callbacks := task_info.get("callbacks"):
|
||||
self.tasks_config[task_name]["callbacks"] = [
|
||||
callback_functions[callback]() for callback in callbacks
|
||||
]
|
||||
|
||||
return WrappedClass
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest import mock
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from crewai.cli.cli import train, version, reset_memories
|
||||
from crewai.cli.cli import reset_memories, test, train, version
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -133,3 +133,33 @@ def test_version_command_with_tools(runner):
|
||||
"crewai tools version:" in result.output
|
||||
or "crewai tools not installed" in result.output
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai.cli.cli.test_crew")
|
||||
def test_test_default_iterations(test_crew, runner):
|
||||
result = runner.invoke(test)
|
||||
|
||||
test_crew.assert_called_once_with(3, "gpt-4o-mini")
|
||||
assert result.exit_code == 0
|
||||
assert "Testing the crew for 3 iterations with model gpt-4o-mini" in result.output
|
||||
|
||||
|
||||
@mock.patch("crewai.cli.cli.test_crew")
|
||||
def test_test_custom_iterations(test_crew, runner):
|
||||
result = runner.invoke(test, ["--n_iterations", "5", "--model", "gpt-4o"])
|
||||
|
||||
test_crew.assert_called_once_with(5, "gpt-4o")
|
||||
assert result.exit_code == 0
|
||||
assert "Testing the crew for 5 iterations with model gpt-4o" in result.output
|
||||
|
||||
|
||||
@mock.patch("crewai.cli.cli.test_crew")
|
||||
def test_test_invalid_string_iterations(test_crew, runner):
|
||||
result = runner.invoke(test, ["--n_iterations", "invalid"])
|
||||
|
||||
test_crew.assert_not_called()
|
||||
assert result.exit_code == 2
|
||||
assert (
|
||||
"Usage: test [OPTIONS]\nTry 'test --help' for help.\n\nError: Invalid value for '-n' / '--n_iterations': 'invalid' is not a valid integer.\n"
|
||||
in result.output
|
||||
)
|
||||
|
||||
97
tests/cli/test_crew_test.py
Normal file
97
tests/cli/test_crew_test.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import subprocess
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.cli import test_crew
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"n_iterations,model",
|
||||
[
|
||||
(1, "gpt-4o"),
|
||||
(5, "gpt-3.5-turbo"),
|
||||
(10, "gpt-4"),
|
||||
],
|
||||
)
|
||||
@mock.patch("crewai.cli.test_crew.subprocess.run")
|
||||
def test_crew_success(mock_subprocess_run, n_iterations, model):
|
||||
"""Test the crew function for successful execution."""
|
||||
mock_subprocess_run.return_value = subprocess.CompletedProcess(
|
||||
args=f"poetry run test {n_iterations} {model}", returncode=0
|
||||
)
|
||||
result = test_crew.test_crew(n_iterations, model)
|
||||
|
||||
mock_subprocess_run.assert_called_once_with(
|
||||
["poetry", "run", "test", str(n_iterations), model],
|
||||
capture_output=False,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
assert result is None
|
||||
|
||||
|
||||
@mock.patch("crewai.cli.test_crew.click")
|
||||
def test_test_crew_zero_iterations(click):
|
||||
test_crew.test_crew(0, "gpt-4o")
|
||||
click.echo.assert_called_once_with(
|
||||
"An unexpected error occurred: The number of iterations must be a positive integer.",
|
||||
err=True,
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai.cli.test_crew.click")
|
||||
def test_test_crew_negative_iterations(click):
|
||||
test_crew.test_crew(-2, "gpt-4o")
|
||||
click.echo.assert_called_once_with(
|
||||
"An unexpected error occurred: The number of iterations must be a positive integer.",
|
||||
err=True,
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai.cli.test_crew.click")
|
||||
@mock.patch("crewai.cli.test_crew.subprocess.run")
|
||||
def test_test_crew_called_process_error(mock_subprocess_run, click):
|
||||
n_iterations = 5
|
||||
mock_subprocess_run.side_effect = subprocess.CalledProcessError(
|
||||
returncode=1,
|
||||
cmd=["poetry", "run", "test", str(n_iterations), "gpt-4o"],
|
||||
output="Error",
|
||||
stderr="Some error occurred",
|
||||
)
|
||||
test_crew.test_crew(n_iterations, "gpt-4o")
|
||||
|
||||
mock_subprocess_run.assert_called_once_with(
|
||||
["poetry", "run", "test", "5", "gpt-4o"],
|
||||
capture_output=False,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
click.echo.assert_has_calls(
|
||||
[
|
||||
mock.call.echo(
|
||||
"An error occurred while testing the crew: Command '['poetry', 'run', 'test', '5', 'gpt-4o']' returned non-zero exit status 1.",
|
||||
err=True,
|
||||
),
|
||||
mock.call.echo("Error", err=True),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai.cli.test_crew.click")
|
||||
@mock.patch("crewai.cli.test_crew.subprocess.run")
|
||||
def test_test_crew_unexpected_exception(mock_subprocess_run, click):
|
||||
# Arrange
|
||||
n_iterations = 5
|
||||
mock_subprocess_run.side_effect = Exception("Unexpected error")
|
||||
test_crew.test_crew(n_iterations, "gpt-4o")
|
||||
|
||||
mock_subprocess_run.assert_called_once_with(
|
||||
["poetry", "run", "test", "5", "gpt-4o"],
|
||||
capture_output=False,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
click.echo.assert_called_once_with(
|
||||
"An unexpected error occurred: Unexpected error", err=True
|
||||
)
|
||||
Reference in New Issue
Block a user