Compare commits

...

8 Commits

Author SHA1 Message Date
Eduardo Chiarotti
835f0065f1 test: change tests and gh action file 2024-05-16 21:15:20 -03:00
Eduardo Chiarotti
5865a2b899 fix: removed fix since it didnt changed the test 2024-05-16 21:07:39 -03:00
Eduardo Chiarotti
9629337f17 fix: fix test 2024-05-16 21:04:15 -03:00
Eduardo Chiarotti
2186f5c968 fix: test.yml 2024-05-16 20:49:58 -03:00
Eduardo Chiarotti
d34c2a2672 fix: fix typing hinting issue on code 2024-05-16 19:48:42 -03:00
Eduardo Chiarotti
2520f389f2 feat: add the tests 2024-05-16 19:43:42 -03:00
Eduardo Chiarotti
a958b31768 feat: add crewai train CLI command 2024-05-16 19:43:33 -03:00
Eduardo Chiarotti
5de494c99b fix: fix crewai-tools cli command 2024-05-15 19:54:26 -03:00
16 changed files with 284 additions and 52 deletions

View File

@@ -19,14 +19,13 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.11"
- name: Install Requirements - name: Install Requirements
run: | run: |
sudo apt-get update && pip install poetry
pip install poetry &&
poetry lock && poetry lock &&
poetry install poetry install
- name: Run tests - name: Run tests
run: poetry run pytest run: poetry run pytest tests

View File

@@ -2,6 +2,8 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4 rev: v0.4.4
hooks: hooks:
# Run the linter.
- id: ruff - id: ruff
args: [--fix] args: ["--fix"]
exclude: "templates"
- id: ruff-format
exclude: "templates"

View File

@@ -2,6 +2,7 @@ import click
import pkg_resources import pkg_resources
from .create_crew import create_crew from .create_crew import create_crew
from .train_crew import train_crew
@click.group() @click.group()
@@ -27,11 +28,25 @@ def version(tools):
if tools: if tools:
try: try:
tools_version = pkg_resources.get_distribution("crewai[tools]").version tools_version = pkg_resources.get_distribution("crewai-tools").version
click.echo(f"crewai tools version: {tools_version}") click.echo(f"crewai tools version: {tools_version}")
except pkg_resources.DistributionNotFound: except pkg_resources.DistributionNotFound:
click.echo("crewai tools not installed") click.echo("crewai tools not installed")
@crewai.command()
@click.option(
"-n",
"--n_iterations",
type=int,
default=5,
help="Number of iterations to train the crew",
)
def train(n_iterations: int):
"""Train the crew."""
click.echo(f"Training the crew for {n_iterations} iterations")
train_crew(n_iterations)
if __name__ == "__main__": if __name__ == "__main__":
crewai() crewai()

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys
from {{folder_name}}.crew import {{crew_name}}Crew from {{folder_name}}.crew import {{crew_name}}Crew
@@ -8,3 +9,14 @@ def run():
'topic': 'AI LLMs' 'topic': 'AI LLMs'
} }
{{crew_name}}Crew().crew().kickoff(inputs=inputs) {{crew_name}}Crew().crew().kickoff(inputs=inputs)
def train():
"""
Train the crew for a given number of iterations.
"""
try:
{{crew_name}}Crew().crew().train(n_iterations=int(sys.argv[1]))
except Exception as e:
raise Exception(f"An error occurred while training the crew: {e}")

View File

@@ -6,10 +6,11 @@ authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.10,<=3.13" python = ">=3.10,<=3.13"
crewai = {extras = ["tools"], version = "^0.30.11"} crewai = { extras = ["tools"], version = "^0.30.11" }
[tool.poetry.scripts] [tool.poetry.scripts]
{{folder_name}} = "{{folder_name}}.main:run" {{folder_name}} = "{{folder_name}}.main:run"
train = "{{folder_name}}.main:train"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

View File

@@ -0,0 +1,29 @@
import subprocess
import click
def train_crew(n_iterations: int) -> None:
"""
Train the crew by running a command in the Poetry environment.
Args:
n_iterations (int): The number of iterations to train the crew.
"""
command = ["poetry", "run", "train", str(n_iterations)]
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 training 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)

View File

@@ -164,7 +164,9 @@ class Crew(BaseModel):
"""Set private attributes.""" """Set private attributes."""
if self.memory: if self.memory:
self._long_term_memory = LongTermMemory() self._long_term_memory = LongTermMemory()
self._short_term_memory = ShortTermMemory(crew=self, embedder_config=self.embedder) self._short_term_memory = ShortTermMemory(
crew=self, embedder_config=self.embedder
)
self._entity_memory = EntityMemory(crew=self, embedder_config=self.embedder) self._entity_memory = EntityMemory(crew=self, embedder_config=self.embedder)
return self return self
@@ -280,6 +282,10 @@ class Crew(BaseModel):
return result return result
def train(self, n_iterations: int) -> None:
# TODO: Implement training
pass
def _run_sequential_process(self) -> str: def _run_sequential_process(self) -> str:
"""Executes tasks sequentially and returns the final output.""" """Executes tasks sequentially and returns the final output."""
task_output = "" task_output = ""

View File

@@ -12,7 +12,10 @@ class EntityMemory(Memory):
def __init__(self, crew=None, embedder_config=None): def __init__(self, crew=None, embedder_config=None):
storage = RAGStorage( storage = RAGStorage(
type="entities", allow_reset=False, embedder_config=embedder_config, crew=crew type="entities",
allow_reset=False,
embedder_config=embedder_config,
crew=crew,
) )
super().__init__(storage) super().__init__(storage)

View File

@@ -13,7 +13,9 @@ class ShortTermMemory(Memory):
""" """
def __init__(self, crew=None, embedder_config=None): def __init__(self, crew=None, embedder_config=None):
storage = RAGStorage(type="short_term", embedder_config=embedder_config, crew=crew) storage = RAGStorage(
type="short_term", embedder_config=embedder_config, crew=crew
)
super().__init__(storage) super().__init__(storage)
def save(self, item: ShortTermMemoryItem) -> None: # type: ignore # BUG?: Signature of "save" incompatible with supertype "Memory" def save(self, item: ShortTermMemoryItem) -> None: # type: ignore # BUG?: Signature of "save" incompatible with supertype "Memory"

View File

@@ -1,18 +1,18 @@
import inspect import inspect
import yaml
import os import os
from pathlib import Path from pathlib import Path
import yaml
from dotenv import load_dotenv
from pydantic import ConfigDict from pydantic import ConfigDict
from dotenv import load_dotenv
load_dotenv() load_dotenv()
def CrewBase(cls): def CrewBase(cls):
class WrappedClass(cls): class WrappedClass(cls):
model_config = ConfigDict(arbitrary_types_allowed=True) model_config = ConfigDict(arbitrary_types_allowed=True)
is_crew_class: bool = True is_crew_class: bool = True # type: ignore
base_directory = None base_directory = None
for frame_info in inspect.stack(): for frame_info in inspect.stack():

View File

@@ -305,7 +305,7 @@ class Task(BaseModel):
if directory and not os.path.exists(directory): if directory and not os.path.exists(directory):
os.makedirs(directory) os.makedirs(directory)
with open(self.output_file, "w", encoding='utf-8') as file: # type: ignore # Argument 1 to "open" has incompatible type "str | None"; expected "int | str | bytes | PathLike[str] | PathLike[bytes]" with open(self.output_file, "w", encoding="utf-8") as file: # type: ignore # Argument 1 to "open" has incompatible type "str | None"; expected "int | str | bytes | PathLike[str] | PathLike[bytes]"
file.write(result) file.write(result)
return None return None

View File

@@ -33,17 +33,23 @@ class AgentTools(BaseModel):
] ]
return tools return tools
def delegate_work(self, task: str, context: str, coworker: Union[str, None] = None, **kwargs): def delegate_work(
self, task: str, context: str, coworker: Union[str, None] = None, **kwargs
):
"""Useful to delegate a specific task to a co-worker passing all necessary context and names.""" """Useful to delegate a specific task to a co-worker passing all necessary context and names."""
coworker = coworker or kwargs.get("co_worker") or kwargs.get("co-worker") coworker = coworker or kwargs.get("co_worker") or kwargs.get("co-worker")
if coworker is not None:
is_list = coworker.startswith("[") and coworker.endswith("]") is_list = coworker.startswith("[") and coworker.endswith("]")
if is_list: if is_list:
coworker = coworker[1:-1].split(",")[0] coworker = coworker[1:-1].split(",")[0]
return self._execute(coworker, task, context) return self._execute(coworker, task, context)
def ask_question(self, question: str, context: str, coworker: Union[str, None] = None, **kwargs): def ask_question(
self, question: str, context: str, coworker: Union[str, None] = None, **kwargs
):
"""Useful to ask a question, opinion or take from a co-worker passing all necessary context and names.""" """Useful to ask a question, opinion or take from a co-worker passing all necessary context and names."""
coworker = coworker or kwargs.get("co_worker") or kwargs.get("co-worker") coworker = coworker or kwargs.get("co_worker") or kwargs.get("co-worker")
if coworker is not None:
is_list = coworker.startswith("[") and coworker.endswith("]") is_list = coworker.startswith("[") and coworker.endswith("]")
if is_list: if is_list:
coworker = coworker[1:-1].split(",")[0] coworker = coworker[1:-1].split(",")[0]

59
tests/cli/cli_test.py Normal file
View File

@@ -0,0 +1,59 @@
from unittest import mock
import pytest
from click.testing import CliRunner
from crewai.cli.cli import train, version
@pytest.fixture
def runner():
return CliRunner()
@mock.patch("crewai.cli.cli.train_crew")
def test_train_default_iterations(train_crew, runner):
result = runner.invoke(train)
train_crew.assert_called_once_with(5)
assert result.exit_code == 0
assert "Training the crew for 5 iterations" in result.output
@mock.patch("crewai.cli.cli.train_crew")
def test_train_custom_iterations(train_crew, runner):
result = runner.invoke(train, ["--n_iterations", "10"])
train_crew.assert_called_once_with(10)
assert result.exit_code == 0
assert "Training the crew for 10 iterations" in result.output
@mock.patch("crewai.cli.cli.train_crew")
def test_train_invalid_string_iterations(train_crew, runner):
result = runner.invoke(train, ["--n_iterations", "invalid"])
train_crew.assert_not_called()
assert result.exit_code == 2
assert (
"Usage: train [OPTIONS]\nTry 'train --help' for help.\n\nError: Invalid value for '-n' / '--n_iterations': 'invalid' is not a valid integer.\n"
in result.output
)
def test_version_command(runner):
result = runner.invoke(version)
assert result.exit_code == 0
assert "crewai version:" in result.output
def test_version_command_with_tools(runner):
result = runner.invoke(version, ["--tools"])
assert result.exit_code == 0
assert "crewai version:" in result.output
assert (
"crewai tools version:" in result.output
or "crewai tools not installed" in result.output
)

View File

@@ -0,0 +1,87 @@
import subprocess
from unittest import mock
from crewai.cli.train_crew import train_crew
@mock.patch("crewai.cli.train_crew.subprocess.run")
def test_train_crew_positive_iterations(mock_subprocess_run):
# Arrange
n_iterations = 5
mock_subprocess_run.return_value = subprocess.CompletedProcess(
args=["poetry", "run", "train", str(n_iterations)],
returncode=0,
stdout="Success",
stderr="",
)
# Act
train_crew(n_iterations)
# Assert
mock_subprocess_run.assert_called_once_with(
["poetry", "run", "train", str(n_iterations)],
capture_output=False,
text=True,
check=True,
)
@mock.patch("crewai.cli.train_crew.click")
def test_train_crew_zero_iterations(click):
train_crew(0)
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.train_crew.click")
def test_train_crew_negative_iterations(click):
train_crew(-2)
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.train_crew.click")
@mock.patch("crewai.cli.train_crew.subprocess.run")
def test_train_crew_called_process_error(mock_subprocess_run, click):
n_iterations = 5
mock_subprocess_run.side_effect = subprocess.CalledProcessError(
returncode=1,
cmd=["poetry", "run", "train", str(n_iterations)],
output="Error",
stderr="Some error occurred",
)
train_crew(n_iterations)
mock_subprocess_run.assert_called_once_with(
["poetry", "run", "train", "5"], capture_output=False, text=True, check=True
)
click.echo.assert_has_calls(
[
mock.call.echo(
"An error occurred while training the crew: Command '['poetry', 'run', 'train', '5']' returned non-zero exit status 1.",
err=True,
),
mock.call.echo("Error", err=True),
]
)
@mock.patch("crewai.cli.train_crew.click")
@mock.patch("crewai.cli.train_crew.subprocess.run")
def test_train_crew_unexpected_exception(mock_subprocess_run, click):
# Arrange
n_iterations = 5
mock_subprocess_run.side_effect = Exception("Unexpected error")
train_crew(n_iterations)
mock_subprocess_run.assert_called_once_with(
["poetry", "run", "train", "5"], capture_output=False, text=True, check=True
)
click.echo.assert_called_once_with(
"An unexpected error occurred: Unexpected error", err=True
)

View File

@@ -1,20 +0,0 @@
from click.testing import CliRunner
from crewai.cli.cli import version
def test_version_command():
runner = CliRunner()
result = runner.invoke(version)
assert result.exit_code == 0
assert "crewai version:" in result.output
def test_version_command_with_tools():
runner = CliRunner()
result = runner.invoke(version, ["--tools"])
assert result.exit_code == 0
assert "crewai version:" in result.output
assert (
"crewai tools version:" in result.output
or "crewai tools not installed" in result.output
)

View File

@@ -437,6 +437,7 @@ def test_async_task_execution():
process=Process.sequential, process=Process.sequential,
tasks=[list_ideas, list_important_history, write_article], tasks=[list_ideas, list_important_history, write_article],
) )
output = TaskOutput(description="A 4 paragraph article about AI.", raw_output="ok")
with patch.object(Agent, "execute_task") as execute: with patch.object(Agent, "execute_task") as execute:
execute.return_value = "ok" execute.return_value = "ok"
@@ -444,13 +445,11 @@ def test_async_task_execution():
thread = threading.Thread(target=lambda: None, args=()).start() thread = threading.Thread(target=lambda: None, args=()).start()
start.return_value = thread start.return_value = thread
with patch.object(threading.Thread, "join", wraps=thread.join()) as join: with patch.object(threading.Thread, "join", wraps=thread.join()) as join:
list_ideas.output = TaskOutput( list_ideas.output = output
description="A 4 paragraph article about AI.", raw_output="ok" list_important_history.output = output
)
list_important_history.output = TaskOutput(
description="A 4 paragraph article about AI.", raw_output="ok"
)
crew.kickoff() crew.kickoff()
start.assert_called() start.assert_called()
join.assert_called() join.assert_called()
@@ -993,3 +992,35 @@ def test_manager_agent_with_tools_raises_exception():
with pytest.raises(Exception): with pytest.raises(Exception):
crew.kickoff() crew.kickoff()
def test_crew_train_success():
task = Task(
description="Come up with a list of 5 interesting ideas to explore for an article, then write one amazing paragraph highlight for each idea that showcases how good an article about this topic could be. Return the list of ideas with their paragraph and your notes.",
expected_output="5 bullet points with a paragraph for each idea.",
)
crew = Crew(
agents=[researcher, writer],
tasks=[task],
)
crew.train(n_iterations=2)
def test_crew_train_error():
task = Task(
description="Come up with a list of 5 interesting ideas to explore for an article",
expected_output="5 bullet points with a paragraph for each idea.",
)
crew = Crew(
agents=[researcher, writer],
tasks=[task],
)
with pytest.raises(TypeError) as e:
crew.train()
assert "train() missing 1 required positional argument: 'n_iterations'" in str(
e
)