mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-11 00:58:30 +00:00
Merge branch 'main' into tm-basic-event-structure
This commit is contained in:
48
poetry.lock
generated
48
poetry.lock
generated
@@ -6103,6 +6103,54 @@ description = "Database Abstraction Library"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp311-cp311-win32.whl", hash = "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp311-cp311-win_amd64.whl", hash = "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp312-cp312-win32.whl", hash = "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp312-cp312-win_amd64.whl", hash = "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f021d334f2ca692523aaf7bbf7592ceff70c8594fad853416a81d66b35e3abf9"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05c3f58cf91683102f2f0265c0db3bd3892e9eedabe059720492dbaa4f922da1"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:032d979ce77a6c2432653322ba4cbeabf5a6837f704d16fa38b5a05d8e21fa00"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:2e795c2f7d7249b75bb5f479b432a51b59041580d20599d4e112b5f2046437a3"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:cc32b2990fc34380ec2f6195f33a76b6cdaa9eecf09f0c9404b74fc120aef36f"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp37-cp37m-win32.whl", hash = "sha256:9509c4123491d0e63fb5e16199e09f8e262066e58903e84615c301dde8fa2e87"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp37-cp37m-win_amd64.whl", hash = "sha256:3655af10ebcc0f1e4e06c5900bb33e080d6a1fa4228f502121f28a3b1753cde5"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4c31943b61ed8fdd63dfd12ccc919f2bf95eefca133767db6fbbd15da62078ec"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a62dd5d7cc8626a3634208df458c5fe4f21200d96a74d122c83bc2015b333bc1"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0630774b0977804fba4b6bbea6852ab56c14965a2b0c7fc7282c5f7d90a1ae72"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d625eddf7efeba2abfd9c014a22c0f6b3796e0ffb48f5d5ab106568ef01ff5a"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ada603db10bb865bbe591939de854faf2c60f43c9b763e90f653224138f910d9"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c41411e192f8d3ea39ea70e0fae48762cd11a2244e03751a98bd3c0ca9a4e936"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp38-cp38-win32.whl", hash = "sha256:d299797d75cd747e7797b1b41817111406b8b10a4f88b6e8fe5b5e59598b43b0"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp38-cp38-win_amd64.whl", hash = "sha256:0375a141e1c0878103eb3d719eb6d5aa444b490c96f3fedab8471c7f6ffe70ee"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp39-cp39-win32.whl", hash = "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-cp39-cp39-win_amd64.whl", hash = "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f"},
|
||||||
|
{file = "SQLAlchemy-2.0.35-py3-none-any.whl", hash = "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1"},
|
||||||
{file = "sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f"},
|
{file = "sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "crewai"
|
name = "crewai"
|
||||||
version = "0.66.0"
|
version = "0.67.1"
|
||||||
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
|
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
|
||||||
authors = ["Joao Moura <joao@crewai.com>"]
|
authors = ["Joao Moura <joao@crewai.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -260,10 +260,18 @@ def deploy_remove(uuid: Optional[str]):
|
|||||||
deploy_cmd.remove_crew(uuid=uuid)
|
deploy_cmd.remove_crew(uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@tool.command(name="create")
|
||||||
|
@click.argument("handle")
|
||||||
|
def tool_create(handle: str):
|
||||||
|
tool_cmd = ToolCommand()
|
||||||
|
tool_cmd.create(handle)
|
||||||
|
|
||||||
|
|
||||||
@tool.command(name="install")
|
@tool.command(name="install")
|
||||||
@click.argument("handle")
|
@click.argument("handle")
|
||||||
def tool_install(handle: str):
|
def tool_install(handle: str):
|
||||||
tool_cmd = ToolCommand()
|
tool_cmd = ToolCommand()
|
||||||
|
tool_cmd.login()
|
||||||
tool_cmd.install(handle)
|
tool_cmd.install(handle)
|
||||||
|
|
||||||
|
|
||||||
@@ -272,6 +280,7 @@ def tool_install(handle: str):
|
|||||||
@click.option("--private", "is_public", flag_value=False)
|
@click.option("--private", "is_public", flag_value=False)
|
||||||
def tool_publish(is_public: bool):
|
def tool_publish(is_public: bool):
|
||||||
tool_cmd = ToolCommand()
|
tool_cmd = ToolCommand()
|
||||||
|
tool_cmd.login()
|
||||||
tool_cmd.publish(is_public)
|
tool_cmd.publish(is_public)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import Dict, Any
|
import requests
|
||||||
|
from requests.exceptions import JSONDecodeError
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from crewai.cli.plus_api import PlusAPI
|
from crewai.cli.plus_api import PlusAPI
|
||||||
from crewai.cli.utils import get_auth_token
|
from crewai.cli.utils import get_auth_token
|
||||||
@@ -27,14 +28,44 @@ class PlusAPIMixin:
|
|||||||
console.print("Run 'crewai signup' to sign up/login.", style="bold green")
|
console.print("Run 'crewai signup' to sign up/login.", style="bold green")
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
|
|
||||||
def _handle_plus_api_error(self, json_response: Dict[str, Any]) -> None:
|
def _validate_response(self, response: requests.Response) -> None:
|
||||||
"""
|
"""
|
||||||
Handle and display error messages from API responses.
|
Handle and display error messages from API responses.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
json_response (Dict[str, Any]): The JSON response containing error information.
|
response (requests.Response): The response from the Plus API
|
||||||
"""
|
"""
|
||||||
error = json_response.get("error", "Unknown error")
|
try:
|
||||||
message = json_response.get("message", "No message provided")
|
json_response = response.json()
|
||||||
console.print(f"Error: {error}", style="bold red")
|
except (JSONDecodeError, ValueError):
|
||||||
console.print(f"Message: {message}", style="bold red")
|
console.print(
|
||||||
|
"Failed to parse response from Enterprise API failed. Details:",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
console.print(f"Status Code: {response.status_code}")
|
||||||
|
console.print(f"Response:\n{response.content}")
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
if response.status_code == 422:
|
||||||
|
console.print(
|
||||||
|
"Failed to complete operation. Please fix the following errors:",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
for field, messages in json_response.items():
|
||||||
|
for message in messages:
|
||||||
|
console.print(
|
||||||
|
f"* [bold red]{field.capitalize()}[/bold red] {message}"
|
||||||
|
)
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
console.print(
|
||||||
|
"Request to Enterprise API failed. Details:", style="bold red"
|
||||||
|
)
|
||||||
|
details = (
|
||||||
|
json_response.get("error")
|
||||||
|
or json_response.get("message")
|
||||||
|
or response.content
|
||||||
|
)
|
||||||
|
console.print(f"{details}")
|
||||||
|
raise SystemExit
|
||||||
|
|||||||
@@ -79,11 +79,8 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
|||||||
self._standard_no_param_error_message()
|
self._standard_no_param_error_message()
|
||||||
return
|
return
|
||||||
|
|
||||||
json_response = response.json()
|
self._validate_response(response)
|
||||||
if response.status_code == 200:
|
self._display_deployment_info(response.json())
|
||||||
self._display_deployment_info(json_response)
|
|
||||||
else:
|
|
||||||
self._handle_plus_api_error(json_response)
|
|
||||||
|
|
||||||
def create_crew(self, confirm: bool = False) -> None:
|
def create_crew(self, confirm: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -106,12 +103,10 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
|||||||
|
|
||||||
self._confirm_input(env_vars, remote_repo_url, confirm)
|
self._confirm_input(env_vars, remote_repo_url, confirm)
|
||||||
payload = self._create_payload(env_vars, remote_repo_url)
|
payload = self._create_payload(env_vars, remote_repo_url)
|
||||||
|
|
||||||
response = self.plus_api_client.create_crew(payload)
|
response = self.plus_api_client.create_crew(payload)
|
||||||
if response.status_code == 201:
|
|
||||||
self._display_creation_success(response.json())
|
self._validate_response(response)
|
||||||
else:
|
self._display_creation_success(response.json())
|
||||||
self._handle_plus_api_error(response.json())
|
|
||||||
|
|
||||||
def _confirm_input(
|
def _confirm_input(
|
||||||
self, env_vars: Dict[str, str], remote_repo_url: str, confirm: bool
|
self, env_vars: Dict[str, str], remote_repo_url: str, confirm: bool
|
||||||
@@ -218,11 +213,8 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
|||||||
self._standard_no_param_error_message()
|
self._standard_no_param_error_message()
|
||||||
return
|
return
|
||||||
|
|
||||||
json_response = response.json()
|
self._validate_response(response)
|
||||||
if response.status_code == 200:
|
self._display_crew_status(response.json())
|
||||||
self._display_crew_status(json_response)
|
|
||||||
else:
|
|
||||||
self._handle_plus_api_error(json_response)
|
|
||||||
|
|
||||||
def _display_crew_status(self, status_data: Dict[str, str]) -> None:
|
def _display_crew_status(self, status_data: Dict[str, str]) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -253,10 +245,8 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
|||||||
self._standard_no_param_error_message()
|
self._standard_no_param_error_message()
|
||||||
return
|
return
|
||||||
|
|
||||||
if response.status_code == 200:
|
self._validate_response(response)
|
||||||
self._display_logs(response.json())
|
self._display_logs(response.json())
|
||||||
else:
|
|
||||||
self._handle_plus_api_error(response.json())
|
|
||||||
|
|
||||||
def remove_crew(self, uuid: Optional[str]) -> None:
|
def remove_crew(self, uuid: Optional[str]) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class PlusAPI:
|
|||||||
url = urljoin(self.base_url, endpoint)
|
url = urljoin(self.base_url, endpoint)
|
||||||
return requests.request(method, url, headers=self.headers, **kwargs)
|
return requests.request(method, url, headers=self.headers, **kwargs)
|
||||||
|
|
||||||
|
def login_to_tool_repository(self):
|
||||||
|
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login")
|
||||||
|
|
||||||
def get_tool(self, handle: str):
|
def get_tool(self, handle: str):
|
||||||
return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}")
|
return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}")
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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.66.0,<1.0.0" }
|
crewai = { extras = ["tools"], version = ">=0.67.1,<1.0.0" }
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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.66.0,<1.0.0" }
|
crewai = { extras = ["tools"], version = ">=0.67.1,<1.0.0" }
|
||||||
asyncio = "*"
|
asyncio = "*"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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.66.0,<1.0.0" }
|
crewai = { extras = ["tools"], version = ">=0.67.1,<1.0.0" }
|
||||||
asyncio = "*"
|
asyncio = "*"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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.66.0,<1.0.0" }
|
crewai = { extras = ["tools"], version = ">=0.67.1,<1.0.0" }
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
|
|||||||
48
src/crewai/cli/templates/tool/README.md
Normal file
48
src/crewai/cli/templates/tool/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# {{folder_name}}
|
||||||
|
|
||||||
|
{{folder_name}} is a CrewAI Tool. This template is designed to help you create
|
||||||
|
custom tools to power up your crews.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
Ensure you have Python >=3.10 <=3.13 installed on your system. This project
|
||||||
|
uses [Poetry](https://python-poetry.org/) for dependency management and package
|
||||||
|
handling, offering a seamless setup and execution experience.
|
||||||
|
|
||||||
|
First, if you haven't already, install Poetry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install poetry
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, navigate to your project directory and install the dependencies with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crewai install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
Collaborate by sharing tools within your organization, or publish them publicly
|
||||||
|
to contribute with the community.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crewai tool publish {{tool_name}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Others may install your tool in their crews running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crewai tool install {{tool_name}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For support, questions, or feedback regarding the {{crew_name}} tool or CrewAI.
|
||||||
|
|
||||||
|
- Visit our [documentation](https://docs.crewai.com)
|
||||||
|
- Reach out to us through our [GitHub repository](https://github.com/joaomdmoura/crewai)
|
||||||
|
- [Join our Discord](https://discord.com/invite/X4JWnZnxPb)
|
||||||
|
- [Chat with our docs](https://chatg.pt/DWjSBZn)
|
||||||
|
|
||||||
|
Let's create wonders together with the power and simplicity of crewAI.
|
||||||
14
src/crewai/cli/templates/tool/pyproject.toml
Normal file
14
src/crewai/cli/templates/tool/pyproject.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "{{folder_name}}"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Power up your crews with {{folder_name}}"
|
||||||
|
authors = ["Your Name <you@example.com>"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = ">=3.10,<=3.13"
|
||||||
|
crewai = { extras = ["tools"], version = ">=0.64.0,<1.0.0" }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from crewai_tools import BaseTool
|
||||||
|
|
||||||
|
class {{class_name}}(BaseTool):
|
||||||
|
name: str = "Name of my tool"
|
||||||
|
description: str = "What this tool does. It's vital for effective utilization."
|
||||||
|
|
||||||
|
def _run(self, argument: str) -> str:
|
||||||
|
# Your tool's logic here
|
||||||
|
return "Tool's result"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
import click
|
import click
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -9,6 +10,8 @@ from crewai.cli.utils import (
|
|||||||
get_project_name,
|
get_project_name,
|
||||||
get_project_description,
|
get_project_description,
|
||||||
get_project_version,
|
get_project_version,
|
||||||
|
tree_copy,
|
||||||
|
tree_find_and_replace,
|
||||||
)
|
)
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
@@ -24,6 +27,37 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
BaseCommand.__init__(self)
|
BaseCommand.__init__(self)
|
||||||
PlusAPIMixin.__init__(self, telemetry=self._telemetry)
|
PlusAPIMixin.__init__(self, telemetry=self._telemetry)
|
||||||
|
|
||||||
|
def create(self, handle: str):
|
||||||
|
self._ensure_not_in_project()
|
||||||
|
|
||||||
|
folder_name = handle.replace(" ", "_").replace("-", "_").lower()
|
||||||
|
class_name = handle.replace("_", " ").replace("-", " ").title().replace(" ", "")
|
||||||
|
|
||||||
|
project_root = Path(folder_name)
|
||||||
|
if project_root.exists():
|
||||||
|
click.secho(f"Folder {folder_name} already exists.", fg="red")
|
||||||
|
raise SystemExit
|
||||||
|
else:
|
||||||
|
os.makedirs(project_root)
|
||||||
|
|
||||||
|
click.secho(f"Creating custom tool {folder_name}...", fg="green", bold=True)
|
||||||
|
|
||||||
|
template_dir = Path(__file__).parent.parent / "templates" / "tool"
|
||||||
|
tree_copy(template_dir, project_root)
|
||||||
|
tree_find_and_replace(project_root, "{{folder_name}}", folder_name)
|
||||||
|
tree_find_and_replace(project_root, "{{class_name}}", class_name)
|
||||||
|
|
||||||
|
old_directory = os.getcwd()
|
||||||
|
os.chdir(project_root)
|
||||||
|
try:
|
||||||
|
self.login()
|
||||||
|
subprocess.run(["git", "init"], check=True)
|
||||||
|
console.print(
|
||||||
|
f"[green]Created custom tool [bold]{folder_name}[/bold]. Run [bold]cd {project_root}[/bold] to start working.[/green]"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
os.chdir(old_directory)
|
||||||
|
|
||||||
def publish(self, is_public: bool):
|
def publish(self, is_public: bool):
|
||||||
project_name = get_project_name(require=True)
|
project_name = get_project_name(require=True)
|
||||||
assert isinstance(project_name, str)
|
assert isinstance(project_name, str)
|
||||||
@@ -64,23 +98,8 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
description=project_description,
|
description=project_description,
|
||||||
encoded_file=f"data:application/x-gzip;base64,{encoded_tarball}",
|
encoded_file=f"data:application/x-gzip;base64,{encoded_tarball}",
|
||||||
)
|
)
|
||||||
if publish_response.status_code == 422:
|
|
||||||
console.print(
|
|
||||||
"[bold red]Failed to publish tool. Please fix the following errors:[/bold red]"
|
|
||||||
)
|
|
||||||
for field, messages in publish_response.json().items():
|
|
||||||
for message in messages:
|
|
||||||
console.print(
|
|
||||||
f"* [bold red]{field.capitalize()}[/bold red] {message}"
|
|
||||||
)
|
|
||||||
|
|
||||||
raise SystemExit
|
self._validate_response(publish_response)
|
||||||
elif publish_response.status_code != 200:
|
|
||||||
self._handle_plus_api_error(publish_response.json())
|
|
||||||
console.print(
|
|
||||||
"Failed to publish tool. Please try again later.", style="bold red"
|
|
||||||
)
|
|
||||||
raise SystemExit
|
|
||||||
|
|
||||||
published_handle = publish_response.json()["handle"]
|
published_handle = publish_response.json()["handle"]
|
||||||
console.print(
|
console.print(
|
||||||
@@ -103,15 +122,32 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
)
|
)
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
|
|
||||||
self._add_repository_to_poetry(get_response.json())
|
|
||||||
self._add_package(get_response.json())
|
self._add_package(get_response.json())
|
||||||
|
|
||||||
console.print(f"Succesfully installed {handle}", style="bold green")
|
console.print(f"Succesfully installed {handle}", style="bold green")
|
||||||
|
|
||||||
def _add_repository_to_poetry(self, tool_details):
|
def login(self):
|
||||||
repository_handle = f"crewai-{tool_details['repository']['handle']}"
|
login_response = self.plus_api_client.login_to_tool_repository()
|
||||||
repository_url = tool_details["repository"]["url"]
|
|
||||||
repository_credentials = tool_details["repository"]["credentials"]
|
if login_response.status_code != 200:
|
||||||
|
console.print(
|
||||||
|
"Failed to authenticate to the tool repository. Make sure you have the access to tools.",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
login_response_json = login_response.json()
|
||||||
|
for repository in login_response_json["repositories"]:
|
||||||
|
self._add_repository_to_poetry(
|
||||||
|
repository, login_response_json["credential"]
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
"Succesfully authenticated to the tool repository.", style="bold green"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_repository_to_poetry(self, repository, credentials):
|
||||||
|
repository_handle = f"crewai-{repository['handle']}"
|
||||||
|
|
||||||
add_repository_command = [
|
add_repository_command = [
|
||||||
"poetry",
|
"poetry",
|
||||||
@@ -119,7 +155,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
"add",
|
"add",
|
||||||
"--priority=explicit",
|
"--priority=explicit",
|
||||||
repository_handle,
|
repository_handle,
|
||||||
repository_url,
|
repository["url"],
|
||||||
]
|
]
|
||||||
add_repository_result = subprocess.run(
|
add_repository_result = subprocess.run(
|
||||||
add_repository_command, text=True, check=True
|
add_repository_command, text=True, check=True
|
||||||
@@ -133,8 +169,8 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
"poetry",
|
"poetry",
|
||||||
"config",
|
"config",
|
||||||
f"http-basic.{repository_handle}",
|
f"http-basic.{repository_handle}",
|
||||||
repository_credentials,
|
credentials["username"],
|
||||||
'""',
|
credentials["password"],
|
||||||
]
|
]
|
||||||
add_repository_credentials_result = subprocess.run(
|
add_repository_credentials_result = subprocess.run(
|
||||||
add_repository_credentials_command,
|
add_repository_credentials_command,
|
||||||
@@ -166,3 +202,16 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
if add_package_result.stderr:
|
if add_package_result.stderr:
|
||||||
click.echo(add_package_result.stderr, err=True)
|
click.echo(add_package_result.stderr, err=True)
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
|
|
||||||
|
def _ensure_not_in_project(self):
|
||||||
|
if os.path.isfile("./pyproject.toml"):
|
||||||
|
console.print(
|
||||||
|
"[bold red]Oops! It looks like you're inside a project.[/bold red]"
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
"You can't create a new tool while inside an existing project."
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
"[bold yellow]Tip:[/bold yellow] Navigate to a different directory and try again."
|
||||||
|
)
|
||||||
|
raise SystemExit
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
import click
|
import click
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import importlib.metadata
|
||||||
|
|
||||||
from crewai.cli.authentication.utils import TokenManager
|
from crewai.cli.authentication.utils import TokenManager
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
@@ -162,29 +165,9 @@ def _get_nested_value(data: Dict[str, Any], keys: List[str]) -> Any:
|
|||||||
return reduce(dict.__getitem__, keys, data)
|
return reduce(dict.__getitem__, keys, data)
|
||||||
|
|
||||||
|
|
||||||
def get_crewai_version(poetry_lock_path: str = "poetry.lock") -> str:
|
def get_crewai_version() -> str:
|
||||||
"""Get the version number of crewai from the poetry.lock file."""
|
"""Get the version number of CrewAI running the CLI"""
|
||||||
try:
|
return importlib.metadata.version("crewai")
|
||||||
with open(poetry_lock_path, "r") as f:
|
|
||||||
lock_content = f.read()
|
|
||||||
|
|
||||||
match = re.search(
|
|
||||||
r'\[\[package\]\]\s*name\s*=\s*"crewai"\s*version\s*=\s*"([^"]+)"',
|
|
||||||
lock_content,
|
|
||||||
re.DOTALL,
|
|
||||||
)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
else:
|
|
||||||
print("crewai package not found in poetry.lock")
|
|
||||||
return "no-version-found"
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"Error: {poetry_lock_path} not found.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reading the poetry.lock file: {e}")
|
|
||||||
|
|
||||||
return "no-version-found"
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
|
def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
|
||||||
@@ -217,3 +200,40 @@ def get_auth_token() -> str:
|
|||||||
if not access_token:
|
if not access_token:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
|
def tree_copy(source, destination):
|
||||||
|
"""Copies the entire directory structure from the source to the destination."""
|
||||||
|
for item in os.listdir(source):
|
||||||
|
source_item = os.path.join(source, item)
|
||||||
|
destination_item = os.path.join(destination, item)
|
||||||
|
if os.path.isdir(source_item):
|
||||||
|
shutil.copytree(source_item, destination_item)
|
||||||
|
else:
|
||||||
|
shutil.copy2(source_item, destination_item)
|
||||||
|
|
||||||
|
|
||||||
|
def tree_find_and_replace(directory, find, replace):
|
||||||
|
"""Recursively searches through a directory, replacing a target string in
|
||||||
|
both file contents and filenames with a specified replacement string.
|
||||||
|
"""
|
||||||
|
for path, dirs, files in os.walk(os.path.abspath(directory), topdown=False):
|
||||||
|
for filename in files:
|
||||||
|
filepath = os.path.join(path, filename)
|
||||||
|
|
||||||
|
with open(filepath, "r") as file:
|
||||||
|
contents = file.read()
|
||||||
|
with open(filepath, "w") as file:
|
||||||
|
file.write(contents.replace(find, replace))
|
||||||
|
|
||||||
|
if find in filename:
|
||||||
|
new_filename = filename.replace(find, replace)
|
||||||
|
new_filepath = os.path.join(path, new_filename)
|
||||||
|
os.rename(filepath, new_filepath)
|
||||||
|
|
||||||
|
for dirname in dirs:
|
||||||
|
if find in dirname:
|
||||||
|
new_dirname = dirname.replace(find, replace)
|
||||||
|
new_dirpath = os.path.join(path, new_dirname)
|
||||||
|
old_dirpath = os.path.join(path, dirname)
|
||||||
|
os.rename(old_dirpath, new_dirpath)
|
||||||
|
|||||||
@@ -103,10 +103,12 @@ def convert_to_model(
|
|||||||
return handle_partial_json(
|
return handle_partial_json(
|
||||||
result, model, bool(output_json), agent, converter_cls
|
result, model, bool(output_json), agent, converter_cls
|
||||||
)
|
)
|
||||||
|
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return handle_partial_json(
|
return handle_partial_json(
|
||||||
result, model, bool(output_json), agent, converter_cls
|
result, model, bool(output_json), agent, converter_cls
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Printer().print(
|
Printer().print(
|
||||||
content=f"Unexpected error during model conversion: {type(e).__name__}: {e}. Returning original result.",
|
content=f"Unexpected error during model conversion: {type(e).__name__}: {e}. Returning original result.",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Type, get_args, get_origin
|
from typing import Type, get_args, get_origin, Union
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -36,7 +36,14 @@ class PydanticSchemaParser(BaseModel):
|
|||||||
return f"List[\n{nested_schema}\n{' ' * 4 * depth}]"
|
return f"List[\n{nested_schema}\n{' ' * 4 * depth}]"
|
||||||
else:
|
else:
|
||||||
return f"List[{list_item_type.__name__}]"
|
return f"List[{list_item_type.__name__}]"
|
||||||
elif issubclass(field_type, BaseModel):
|
elif get_origin(field_type) is Union:
|
||||||
|
union_args = get_args(field_type)
|
||||||
|
if type(None) in union_args:
|
||||||
|
non_none_type = next(arg for arg in union_args if arg is not type(None))
|
||||||
|
return f"Optional[{self._get_field_type(field.__class__(annotation=non_none_type), depth)}]"
|
||||||
|
else:
|
||||||
|
return f"Union[{', '.join(arg.__name__ for arg in union_args)}]"
|
||||||
|
elif isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
||||||
return self._get_model_schema(field_type, depth)
|
return self._get_model_schema(field_type, depth)
|
||||||
else:
|
else:
|
||||||
return field_type.__name__
|
return getattr(field_type, "__name__", str(field_type))
|
||||||
|
|||||||
0
tests/cli/__init__.py
Normal file
0
tests/cli/__init__.py
Normal file
0
tests/cli/authentication/__init__.py
Normal file
0
tests/cli/authentication/__init__.py
Normal file
@@ -1,7 +1,11 @@
|
|||||||
import unittest
|
import pytest
|
||||||
from io import StringIO
|
import requests
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
import sys
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
from requests.exceptions import JSONDecodeError
|
||||||
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
from crewai.cli.deploy.main import DeployCommand
|
from crewai.cli.deploy.main import DeployCommand
|
||||||
from crewai.cli.utils import parse_toml
|
from crewai.cli.utils import parse_toml
|
||||||
@@ -33,13 +37,65 @@ class TestDeployCommand(unittest.TestCase):
|
|||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
DeployCommand()
|
DeployCommand()
|
||||||
|
|
||||||
def test_handle_plus_api_error(self):
|
def test_validate_response_successful_response(self):
|
||||||
|
mock_response = Mock(spec=requests.Response)
|
||||||
|
mock_response.json.return_value = {"message": "Success"}
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.ok = True
|
||||||
|
|
||||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
self.deploy_command._handle_plus_api_error(
|
self.deploy_command._validate_response(mock_response)
|
||||||
{"error": "Test error", "message": "Test message"}
|
assert fake_out.getvalue() == ""
|
||||||
|
|
||||||
|
def test_validate_response_json_decode_error(self):
|
||||||
|
mock_response = Mock(spec=requests.Response)
|
||||||
|
mock_response.json.side_effect = JSONDecodeError("Decode error", "", 0)
|
||||||
|
mock_response.status_code = 500
|
||||||
|
mock_response.content = b"Invalid JSON"
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
self.deploy_command._validate_response(mock_response)
|
||||||
|
output = fake_out.getvalue()
|
||||||
|
assert (
|
||||||
|
"Failed to parse response from Enterprise API failed. Details:"
|
||||||
|
in output
|
||||||
)
|
)
|
||||||
self.assertIn("Error: Test error", fake_out.getvalue())
|
assert "Status Code: 500" in output
|
||||||
self.assertIn("Message: Test message", fake_out.getvalue())
|
assert "Response:\nb'Invalid JSON'" in output
|
||||||
|
|
||||||
|
def test_validate_response_422_error(self):
|
||||||
|
mock_response = Mock(spec=requests.Response)
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"field1": ["Error message 1"],
|
||||||
|
"field2": ["Error message 2"],
|
||||||
|
}
|
||||||
|
mock_response.status_code = 422
|
||||||
|
mock_response.ok = False
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
self.deploy_command._validate_response(mock_response)
|
||||||
|
output = fake_out.getvalue()
|
||||||
|
assert (
|
||||||
|
"Failed to complete operation. Please fix the following errors:"
|
||||||
|
in output
|
||||||
|
)
|
||||||
|
assert "Field1 Error message 1" in output
|
||||||
|
assert "Field2 Error message 2" in output
|
||||||
|
|
||||||
|
def test_validate_response_other_error(self):
|
||||||
|
mock_response = Mock(spec=requests.Response)
|
||||||
|
mock_response.json.return_value = {"error": "Something went wrong"}
|
||||||
|
mock_response.status_code = 500
|
||||||
|
mock_response.ok = False
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
self.deploy_command._validate_response(mock_response)
|
||||||
|
output = fake_out.getvalue()
|
||||||
|
assert "Request to Enterprise API failed. Details:" in output
|
||||||
|
assert "Details:\nSomething went wrong" in output
|
||||||
|
|
||||||
def test_standard_no_param_error_message(self):
|
def test_standard_no_param_error_message(self):
|
||||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
@@ -207,30 +263,7 @@ class TestDeployCommand(unittest.TestCase):
|
|||||||
project_name = get_project_name()
|
project_name = get_project_name()
|
||||||
self.assertEqual(project_name, "test_project")
|
self.assertEqual(project_name, "test_project")
|
||||||
|
|
||||||
@patch(
|
def test_get_crewai_version(self):
|
||||||
"builtins.open",
|
|
||||||
new_callable=unittest.mock.mock_open,
|
|
||||||
read_data="""
|
|
||||||
[[package]]
|
|
||||||
name = "crewai"
|
|
||||||
version = "0.51.1"
|
|
||||||
description = "Some description"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.10,<4.0"
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
def test_get_crewai_version(self, mock_open):
|
|
||||||
from crewai.cli.utils import get_crewai_version
|
from crewai.cli.utils import get_crewai_version
|
||||||
|
|
||||||
version = get_crewai_version()
|
assert isinstance(get_crewai_version(), str)
|
||||||
self.assertEqual(version, "0.51.1")
|
|
||||||
|
|
||||||
@patch("builtins.open", side_effect=FileNotFoundError)
|
|
||||||
def test_get_crewai_version_file_not_found(self, mock_open):
|
|
||||||
from crewai.cli.utils import get_crewai_version
|
|
||||||
|
|
||||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
|
||||||
version = get_crewai_version()
|
|
||||||
self.assertEqual(version, "no-version-found")
|
|
||||||
self.assertIn("Error: poetry.lock not found.", fake_out.getvalue())
|
|
||||||
|
|||||||
@@ -11,15 +11,22 @@ class TestPlusAPI(unittest.TestCase):
|
|||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
self.assertEqual(self.api.api_key, self.api_key)
|
self.assertEqual(self.api.api_key, self.api_key)
|
||||||
self.assertEqual(
|
self.assertEqual(self.api.headers["Authorization"], f"Bearer {self.api_key}")
|
||||||
self.api.headers,
|
self.assertEqual(self.api.headers["Content-Type"], "application/json")
|
||||||
{
|
self.assertTrue("CrewAI-CLI/" in self.api.headers["User-Agent"])
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
self.assertTrue(self.api.headers["X-Crewai-Version"])
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": "CrewAI-CLI/no-version-found",
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
||||||
"X-Crewai-Version": "no-version-found",
|
def test_login_to_tool_repository(self, mock_make_request):
|
||||||
},
|
mock_response = MagicMock()
|
||||||
|
mock_make_request.return_value = mock_response
|
||||||
|
|
||||||
|
response = self.api.login_to_tool_repository()
|
||||||
|
|
||||||
|
mock_make_request.assert_called_once_with(
|
||||||
|
"POST", "/crewai_plus/api/v1/tools/login"
|
||||||
)
|
)
|
||||||
|
self.assertEqual(response, mock_response)
|
||||||
|
|
||||||
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
||||||
def test_get_tool(self, mock_make_request):
|
def test_get_tool(self, mock_make_request):
|
||||||
|
|||||||
100
tests/cli/test_utils.py
Normal file
100
tests/cli/test_utils.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import pytest
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
from crewai.cli import utils
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_tree():
|
||||||
|
root_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
create_file(os.path.join(root_dir, "file1.txt"), "Hello, world!")
|
||||||
|
create_file(os.path.join(root_dir, "file2.txt"), "Another file")
|
||||||
|
os.mkdir(os.path.join(root_dir, "empty_dir"))
|
||||||
|
nested_dir = os.path.join(root_dir, "nested_dir")
|
||||||
|
os.mkdir(nested_dir)
|
||||||
|
create_file(os.path.join(nested_dir, "nested_file.txt"), "Nested content")
|
||||||
|
|
||||||
|
yield root_dir
|
||||||
|
|
||||||
|
shutil.rmtree(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def create_file(path, content):
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_find_and_replace_file_content(temp_tree):
|
||||||
|
utils.tree_find_and_replace(temp_tree, "world", "universe")
|
||||||
|
with open(os.path.join(temp_tree, "file1.txt"), "r") as f:
|
||||||
|
assert f.read() == "Hello, universe!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_find_and_replace_file_name(temp_tree):
|
||||||
|
old_path = os.path.join(temp_tree, "file2.txt")
|
||||||
|
new_path = os.path.join(temp_tree, "file2_renamed.txt")
|
||||||
|
os.rename(old_path, new_path)
|
||||||
|
utils.tree_find_and_replace(temp_tree, "renamed", "modified")
|
||||||
|
assert os.path.exists(os.path.join(temp_tree, "file2_modified.txt"))
|
||||||
|
assert not os.path.exists(new_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_find_and_replace_directory_name(temp_tree):
|
||||||
|
utils.tree_find_and_replace(temp_tree, "empty", "renamed")
|
||||||
|
assert os.path.exists(os.path.join(temp_tree, "renamed_dir"))
|
||||||
|
assert not os.path.exists(os.path.join(temp_tree, "empty_dir"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_find_and_replace_nested_content(temp_tree):
|
||||||
|
utils.tree_find_and_replace(temp_tree, "Nested", "Updated")
|
||||||
|
with open(os.path.join(temp_tree, "nested_dir", "nested_file.txt"), "r") as f:
|
||||||
|
assert f.read() == "Updated content"
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_find_and_replace_no_matches(temp_tree):
|
||||||
|
utils.tree_find_and_replace(temp_tree, "nonexistent", "replacement")
|
||||||
|
assert set(os.listdir(temp_tree)) == {
|
||||||
|
"file1.txt",
|
||||||
|
"file2.txt",
|
||||||
|
"empty_dir",
|
||||||
|
"nested_dir",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_copy_full_structure(temp_tree):
|
||||||
|
dest_dir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
utils.tree_copy(temp_tree, dest_dir)
|
||||||
|
assert set(os.listdir(dest_dir)) == set(os.listdir(temp_tree))
|
||||||
|
assert os.path.isfile(os.path.join(dest_dir, "file1.txt"))
|
||||||
|
assert os.path.isfile(os.path.join(dest_dir, "file2.txt"))
|
||||||
|
assert os.path.isdir(os.path.join(dest_dir, "empty_dir"))
|
||||||
|
assert os.path.isdir(os.path.join(dest_dir, "nested_dir"))
|
||||||
|
assert os.path.isfile(os.path.join(dest_dir, "nested_dir", "nested_file.txt"))
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(dest_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_copy_preserve_content(temp_tree):
|
||||||
|
dest_dir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
utils.tree_copy(temp_tree, dest_dir)
|
||||||
|
with open(os.path.join(dest_dir, "file1.txt"), "r") as f:
|
||||||
|
assert f.read() == "Hello, world!"
|
||||||
|
with open(os.path.join(dest_dir, "nested_dir", "nested_file.txt"), "r") as f:
|
||||||
|
assert f.read() == "Nested content"
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(dest_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tree_copy_to_existing_directory(temp_tree):
|
||||||
|
dest_dir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
create_file(os.path.join(dest_dir, "existing_file.txt"), "I was here first")
|
||||||
|
utils.tree_copy(temp_tree, dest_dir)
|
||||||
|
assert os.path.isfile(os.path.join(dest_dir, "existing_file.txt"))
|
||||||
|
assert os.path.isfile(os.path.join(dest_dir, "file1.txt"))
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(dest_dir)
|
||||||
@@ -1,11 +1,59 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
|
import os
|
||||||
from crewai.cli.tools.main import ToolCommand
|
from crewai.cli.tools.main import ToolCommand
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
class TestToolCommand(unittest.TestCase):
|
class TestToolCommand(unittest.TestCase):
|
||||||
|
@contextmanager
|
||||||
|
def in_temp_dir(self):
|
||||||
|
original_dir = os.getcwd()
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
os.chdir(temp_dir)
|
||||||
|
try:
|
||||||
|
yield temp_dir
|
||||||
|
finally:
|
||||||
|
os.chdir(original_dir)
|
||||||
|
|
||||||
|
@patch("crewai.cli.tools.main.subprocess.run")
|
||||||
|
def test_create_success(self, mock_subprocess):
|
||||||
|
with self.in_temp_dir():
|
||||||
|
tool_command = ToolCommand()
|
||||||
|
|
||||||
|
with patch.object(tool_command, "login") as mock_login, patch(
|
||||||
|
"sys.stdout", new=StringIO()
|
||||||
|
) as fake_out:
|
||||||
|
tool_command.create("test-tool")
|
||||||
|
output = fake_out.getvalue()
|
||||||
|
|
||||||
|
self.assertTrue(os.path.isdir("test_tool"))
|
||||||
|
|
||||||
|
self.assertTrue(os.path.isfile(os.path.join("test_tool", "README.md")))
|
||||||
|
self.assertTrue(os.path.isfile(os.path.join("test_tool", "pyproject.toml")))
|
||||||
|
self.assertTrue(
|
||||||
|
os.path.isfile(
|
||||||
|
os.path.join("test_tool", "src", "test_tool", "__init__.py")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
os.path.isfile(os.path.join("test_tool", "src", "test_tool", "tool.py"))
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(
|
||||||
|
os.path.join("test_tool", "src", "test_tool", "tool.py"), "r"
|
||||||
|
) as f:
|
||||||
|
content = f.read()
|
||||||
|
self.assertIn("class TestTool", content)
|
||||||
|
|
||||||
|
mock_login.assert_called_once()
|
||||||
|
mock_subprocess.assert_called_once_with(["git", "init"], check=True)
|
||||||
|
|
||||||
|
self.assertIn("Creating custom tool test_tool...", output)
|
||||||
|
|
||||||
@patch("crewai.cli.tools.main.subprocess.run")
|
@patch("crewai.cli.tools.main.subprocess.run")
|
||||||
@patch("crewai.cli.plus_api.PlusAPI.get_tool")
|
@patch("crewai.cli.plus_api.PlusAPI.get_tool")
|
||||||
def test_install_success(self, mock_get, mock_subprocess_run):
|
def test_install_success(self, mock_get, mock_subprocess_run):
|
||||||
@@ -13,11 +61,7 @@ class TestToolCommand(unittest.TestCase):
|
|||||||
mock_get_response.status_code = 200
|
mock_get_response.status_code = 200
|
||||||
mock_get_response.json.return_value = {
|
mock_get_response.json.return_value = {
|
||||||
"handle": "sample-tool",
|
"handle": "sample-tool",
|
||||||
"repository": {
|
"repository": {"handle": "sample-repo", "url": "https://example.com/repo"},
|
||||||
"handle": "sample-repo",
|
|
||||||
"url": "https://example.com/repo",
|
|
||||||
"credentials": "my_very_secret",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
mock_get.return_value = mock_get_response
|
mock_get.return_value = mock_get_response
|
||||||
mock_subprocess_run.return_value = MagicMock(stderr=None)
|
mock_subprocess_run.return_value = MagicMock(stderr=None)
|
||||||
@@ -29,30 +73,6 @@ class TestToolCommand(unittest.TestCase):
|
|||||||
output = fake_out.getvalue()
|
output = fake_out.getvalue()
|
||||||
|
|
||||||
mock_get.assert_called_once_with("sample-tool")
|
mock_get.assert_called_once_with("sample-tool")
|
||||||
mock_subprocess_run.assert_any_call(
|
|
||||||
[
|
|
||||||
"poetry",
|
|
||||||
"source",
|
|
||||||
"add",
|
|
||||||
"--priority=explicit",
|
|
||||||
"crewai-sample-repo",
|
|
||||||
"https://example.com/repo",
|
|
||||||
],
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
mock_subprocess_run.assert_any_call(
|
|
||||||
[
|
|
||||||
"poetry",
|
|
||||||
"config",
|
|
||||||
"http-basic.crewai-sample-repo",
|
|
||||||
"my_very_secret",
|
|
||||||
'""',
|
|
||||||
],
|
|
||||||
capture_output=False,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
mock_subprocess_run.assert_any_call(
|
mock_subprocess_run.assert_any_call(
|
||||||
["poetry", "add", "--source", "crewai-sample-repo", "sample-tool"],
|
["poetry", "add", "--source", "crewai-sample-repo", "sample-tool"],
|
||||||
capture_output=False,
|
capture_output=False,
|
||||||
@@ -182,7 +202,7 @@ class TestToolCommand(unittest.TestCase):
|
|||||||
output = fake_out.getvalue()
|
output = fake_out.getvalue()
|
||||||
|
|
||||||
mock_publish.assert_called_once()
|
mock_publish.assert_called_once()
|
||||||
self.assertIn("Failed to publish tool", output)
|
self.assertIn("Failed to complete operation", output)
|
||||||
self.assertIn("Name is already taken", output)
|
self.assertIn("Name is already taken", output)
|
||||||
|
|
||||||
@patch("crewai.cli.tools.main.get_project_name", return_value="sample-tool")
|
@patch("crewai.cli.tools.main.get_project_name", return_value="sample-tool")
|
||||||
@@ -210,9 +230,11 @@ class TestToolCommand(unittest.TestCase):
|
|||||||
mock_get_project_version,
|
mock_get_project_version,
|
||||||
mock_get_project_name,
|
mock_get_project_name,
|
||||||
):
|
):
|
||||||
mock_publish_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_publish_response.status_code = 500
|
mock_response.status_code = 500
|
||||||
mock_publish.return_value = mock_publish_response
|
mock_response.json.return_value = {"error": "Internal Server Error"}
|
||||||
|
mock_response.ok = False
|
||||||
|
mock_publish.return_value = mock_response
|
||||||
|
|
||||||
tool_command = ToolCommand()
|
tool_command = ToolCommand()
|
||||||
|
|
||||||
@@ -222,8 +244,55 @@ class TestToolCommand(unittest.TestCase):
|
|||||||
output = fake_out.getvalue()
|
output = fake_out.getvalue()
|
||||||
|
|
||||||
mock_publish.assert_called_once()
|
mock_publish.assert_called_once()
|
||||||
self.assertIn("Failed to publish tool", output)
|
self.assertIn("Request to Enterprise API failed", output)
|
||||||
|
|
||||||
|
@patch("crewai.cli.plus_api.PlusAPI.login_to_tool_repository")
|
||||||
|
@patch("crewai.cli.tools.main.subprocess.run")
|
||||||
|
def test_login_success(self, mock_subprocess_run, mock_login):
|
||||||
|
mock_login_response = MagicMock()
|
||||||
|
mock_login_response.status_code = 200
|
||||||
|
mock_login_response.json.return_value = {
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"handle": "tools",
|
||||||
|
"url": "https://example.com/repo",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credential": {"username": "user", "password": "pass"},
|
||||||
|
}
|
||||||
|
mock_login.return_value = mock_login_response
|
||||||
|
|
||||||
if __name__ == "__main__":
|
mock_subprocess_run.return_value = MagicMock(stderr=None)
|
||||||
unittest.main()
|
|
||||||
|
tool_command = ToolCommand()
|
||||||
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
|
tool_command.login()
|
||||||
|
output = fake_out.getvalue()
|
||||||
|
|
||||||
|
mock_login.assert_called_once()
|
||||||
|
mock_subprocess_run.assert_any_call(
|
||||||
|
[
|
||||||
|
"poetry",
|
||||||
|
"source",
|
||||||
|
"add",
|
||||||
|
"--priority=explicit",
|
||||||
|
"crewai-tools",
|
||||||
|
"https://example.com/repo",
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
mock_subprocess_run.assert_any_call(
|
||||||
|
[
|
||||||
|
"poetry",
|
||||||
|
"config",
|
||||||
|
"http-basic.crewai-tools",
|
||||||
|
"user",
|
||||||
|
"pass",
|
||||||
|
],
|
||||||
|
capture_output=False,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
self.assertIn("Succesfully authenticated to the tool repository", output)
|
||||||
|
|||||||
Reference in New Issue
Block a user