From 09cba0135e875fc0a38e20dd2073149b6b9e335a Mon Sep 17 00:00:00 2001 From: Shu Huang <44169687+ShuHuang@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:39:15 +0100 Subject: [PATCH 01/20] Bugfix: Update LLM-Connections.md The original code doesn't work due to a comma --- docs/how-to/LLM-Connections.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/LLM-Connections.md b/docs/how-to/LLM-Connections.md index 4acdbb3e3..1f0eafd5e 100644 --- a/docs/how-to/LLM-Connections.md +++ b/docs/how-to/LLM-Connections.md @@ -88,7 +88,7 @@ There are a couple of different ways you can use HuggingFace to host your LLM. ### Your own HuggingFace endpoint ```python -from langchain_huggingface import HuggingFaceEndpoint, +from langchain_huggingface import HuggingFaceEndpoint llm = HuggingFaceEndpoint( repo_id="microsoft/Phi-3-mini-4k-instruct", @@ -194,4 +194,4 @@ azure_agent = Agent( ``` ## Conclusion -Integrating CrewAI with different LLMs expands the framework's versatility, allowing for customized, efficient AI solutions across various domains and platforms. \ No newline at end of file +Integrating CrewAI with different LLMs expands the framework's versatility, allowing for customized, efficient AI solutions across various domains and platforms. From f5246039e59dd608d5ab281602fbaf8292b16b3f Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Fri, 23 Aug 2024 10:20:03 -0300 Subject: [PATCH 02/20] Feat/cli deploy (#1240) * feat: set basic structure deploy commands * feat: add first iteration of CLI Deploy * feat: some minor refactor * feat: Add api, Deploy command and update cli * feat: Remove test token * feat: add auth0 lib, update cli and improve code * feat: update code and decouple auth * fix: parts of the code * feat: Add token manager to encrypt access token and get and save tokens * feat: add audience to costants * feat: add subsystem saving credentials and remove comment of type hinting * feat: add get crew version to send on header of request * feat: add docstrings * feat: add tests for authentication module * feat: add tests for utils * feat: add unit tests for cl * feat: add tests * feat: add deploy man tests * feat: fix type checking issue * feat: rename tests to pass ci * feat: fix pr issues * feat: fix get crewai versoin * fix: add timeout for tests.yml --- .github/workflows/tests.yml | 1 + poetry.lock | 117 +++++++-- pyproject.toml | 1 + src/crewai/cli/authentication/__init__.py | 3 + src/crewai/cli/authentication/constants.py | 4 + src/crewai/cli/authentication/main.py | 75 ++++++ src/crewai/cli/authentication/utils.py | 144 ++++++++++ src/crewai/cli/cli.py | 69 +++++ src/crewai/cli/deploy/__init__.py | 0 src/crewai/cli/deploy/api.py | 63 +++++ src/crewai/cli/deploy/main.py | 289 +++++++++++++++++++++ src/crewai/cli/deploy/utils.py | 117 +++++++++ src/crewai/memory/memory.py | 2 +- tests/cli/authentication/test_auth_main.py | 94 +++++++ tests/cli/authentication/test_utils.py | 147 +++++++++++ tests/cli/cli_test.py | 118 ++++++++- tests/cli/deploy/test_api.py | 102 ++++++++ tests/cli/deploy/test_deploy_main.py | 153 +++++++++++ 18 files changed, 1481 insertions(+), 18 deletions(-) create mode 100644 src/crewai/cli/authentication/__init__.py create mode 100644 src/crewai/cli/authentication/constants.py create mode 100644 src/crewai/cli/authentication/main.py create mode 100644 src/crewai/cli/authentication/utils.py create mode 100644 src/crewai/cli/deploy/__init__.py create mode 100644 src/crewai/cli/deploy/api.py create mode 100644 src/crewai/cli/deploy/main.py create mode 100644 src/crewai/cli/deploy/utils.py create mode 100644 tests/cli/authentication/test_auth_main.py create mode 100644 tests/cli/authentication/test_utils.py create mode 100644 tests/cli/deploy/test_api.py create mode 100644 tests/cli/deploy/test_deploy_main.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d99c19524..dcd3e2f1e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,7 @@ env: jobs: deploy: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout code diff --git a/poetry.lock b/poetry.lock index 701a7e146..f98e4d9d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -253,6 +253,24 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +[[package]] +name = "auth0-python" +version = "4.7.1" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "auth0_python-4.7.1-py3-none-any.whl", hash = "sha256:5bdbefd582171f398c2b686a19fb5e241a2fa267929519a0c02e33e5932fa7b8"}, + {file = "auth0_python-4.7.1.tar.gz", hash = "sha256:5cf8be11aa807d54e19271a990eb92bea1863824e4863c7fc8493c6f15a597f1"}, +] + +[package.dependencies] +aiohttp = ">=3.8.5,<4.0.0" +cryptography = ">=42.0.4,<43.0.0" +pyjwt = ">=2.8.0,<3.0.0" +requests = ">=2.31.0,<3.0.0" +urllib3 = ">=2.0.7,<3.0.0" + [[package]] name = "autoflake" version = "2.3.1" @@ -851,6 +869,60 @@ pytube = ">=15.0.0,<16.0.0" requests = ">=2.31.0,<3.0.0" selenium = ">=4.18.1,<5.0.0" +[[package]] +name = "cryptography" +version = "42.0.8" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "cssselect2" version = "0.7.0" @@ -4230,6 +4302,23 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyjwt" +version = "2.9.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pylance" version = "0.9.18" @@ -5478,22 +5567,23 @@ files = [ [[package]] name = "urllib3" -version = "1.26.19" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, - {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.dependencies] -PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} [package.extras] -brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" @@ -5567,23 +5657,20 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "vcrpy" -version = "6.0.1" +version = "5.1.0" description = "Automatically mock your HTTP interactions to simplify and speed up testing" optional = false python-versions = ">=3.8" files = [ - {file = "vcrpy-6.0.1.tar.gz", hash = "sha256:9e023fee7f892baa0bbda2f7da7c8ac51165c1c6e38ff8688683a12a4bde9278"}, + {file = "vcrpy-5.1.0-py2.py3-none-any.whl", hash = "sha256:605e7b7a63dcd940db1df3ab2697ca7faf0e835c0852882142bafb19649d599e"}, + {file = "vcrpy-5.1.0.tar.gz", hash = "sha256:bbf1532f2618a04f11bce2a99af3a9647a32c880957293ff91e0a5f187b6b3d2"}, ] [package.dependencies] PyYAML = "*" -urllib3 = {version = "<2", markers = "platform_python_implementation == \"PyPy\""} wrapt = "*" yarl = "*" -[package.extras] -tests = ["Werkzeug (==2.0.3)", "aiohttp", "boto3", "httplib2", "httpx", "pytest", "pytest-aiohttp", "pytest-asyncio", "pytest-cov", "pytest-httpbin", "requests (>=2.22.0)", "tornado", "urllib3"] - [[package]] name = "virtualenv" version = "20.26.3" @@ -6073,4 +6160,4 @@ tools = ["crewai-tools"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<=3.13" -content-hash = "91ba982ea96ca7be017d536784223d4ef83e86de05d11eb1c3ce0fc1b726f283" +content-hash = "8327a37f807d35d0851e9cc46960e8df0d06924938b2c5354b09951fa54f15e3" diff --git a/pyproject.toml b/pyproject.toml index e438f6574..6cb50c771 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ jsonref = "^1.1.0" agentops = { version = "^0.3.0", optional = true } embedchain = "^0.1.114" json-repair = "^0.25.2" +auth0-python = "^4.7.1" [tool.poetry.extras] tools = ["crewai-tools"] diff --git a/src/crewai/cli/authentication/__init__.py b/src/crewai/cli/authentication/__init__.py new file mode 100644 index 000000000..484453771 --- /dev/null +++ b/src/crewai/cli/authentication/__init__.py @@ -0,0 +1,3 @@ +from .main import AuthenticationCommand + +__all__ = ["AuthenticationCommand"] diff --git a/src/crewai/cli/authentication/constants.py b/src/crewai/cli/authentication/constants.py new file mode 100644 index 000000000..9418087aa --- /dev/null +++ b/src/crewai/cli/authentication/constants.py @@ -0,0 +1,4 @@ +ALGORITHMS = ["RS256"] +AUTH0_DOMAIN = "dev-jzsr0j8zs0atl5ha.us.auth0.com" +AUTH0_CLIENT_ID = "CZtyRHuVW80HbLSjk4ggXNzjg4KAt7Oe" +AUTH0_AUDIENCE = "https://dev-jzsr0j8zs0atl5ha.us.auth0.com/api/v2/" diff --git a/src/crewai/cli/authentication/main.py b/src/crewai/cli/authentication/main.py new file mode 100644 index 000000000..331b583e8 --- /dev/null +++ b/src/crewai/cli/authentication/main.py @@ -0,0 +1,75 @@ +import time +import webbrowser +from typing import Any, Dict + +import requests +from rich.console import Console + +from .constants import AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_DOMAIN +from .utils import TokenManager, validate_token + +console = Console() + + +class AuthenticationCommand: + DEVICE_CODE_URL = f"https://{AUTH0_DOMAIN}/oauth/device/code" + TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token" + + def __init__(self): + self.token_manager = TokenManager() + + def signup(self) -> None: + """Sign up to CrewAI+""" + console.print("Signing Up to CrewAI+ \n", style="bold blue") + device_code_data = self._get_device_code() + self._display_auth_instructions(device_code_data) + + return self._poll_for_token(device_code_data) + + def _get_device_code(self) -> Dict[str, Any]: + """Get the device code to authenticate the user.""" + + device_code_payload = { + "client_id": AUTH0_CLIENT_ID, + "scope": "openid", + "audience": AUTH0_AUDIENCE, + } + response = requests.post(url=self.DEVICE_CODE_URL, data=device_code_payload) + response.raise_for_status() + return response.json() + + def _display_auth_instructions(self, device_code_data: Dict[str, str]) -> None: + """Display the authentication instructions to the user.""" + console.print("1. Navigate to: ", device_code_data["verification_uri_complete"]) + console.print("2. Enter the following code: ", device_code_data["user_code"]) + webbrowser.open(device_code_data["verification_uri_complete"]) + + def _poll_for_token(self, device_code_data: Dict[str, Any]) -> None: + """Poll the server for the token.""" + token_payload = { + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + "device_code": device_code_data["device_code"], + "client_id": AUTH0_CLIENT_ID, + } + + attempts = 0 + while True and attempts < 5: + response = requests.post(self.TOKEN_URL, data=token_payload) + token_data = response.json() + + if response.status_code == 200: + validate_token(token_data["id_token"]) + expires_in = 360000 # Token expiration time in seconds + self.token_manager.save_tokens(token_data["access_token"], expires_in) + console.print("\nWelcome to CrewAI+ !!", style="green") + return + + if token_data["error"] not in ("authorization_pending", "slow_down"): + raise requests.HTTPError(token_data["error_description"]) + + time.sleep(device_code_data["interval"]) + attempts += 1 + + console.print( + "Timeout: Failed to get the token. Please try again.", style="bold red" + ) diff --git a/src/crewai/cli/authentication/utils.py b/src/crewai/cli/authentication/utils.py new file mode 100644 index 000000000..09e7491b1 --- /dev/null +++ b/src/crewai/cli/authentication/utils.py @@ -0,0 +1,144 @@ +import json +import os +import sys +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional + +from auth0.authentication.token_verifier import ( + AsymmetricSignatureVerifier, + TokenVerifier, +) +from cryptography.fernet import Fernet + +from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN + + +def validate_token(id_token: str) -> None: + """ + Verify the token and its precedence + + :param id_token: + """ + jwks_url = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json" + issuer = f"https://{AUTH0_DOMAIN}/" + signature_verifier = AsymmetricSignatureVerifier(jwks_url) + token_verifier = TokenVerifier( + signature_verifier=signature_verifier, issuer=issuer, audience=AUTH0_CLIENT_ID + ) + token_verifier.verify(id_token) + + +class TokenManager: + def __init__(self, file_path: str = "tokens.enc") -> None: + """ + Initialize the TokenManager class. + + :param file_path: The file path to store the encrypted tokens. Default is "tokens.enc". + """ + self.file_path = file_path + self.key = self._get_or_create_key() + self.fernet = Fernet(self.key) + + def _get_or_create_key(self) -> bytes: + """ + Get or create the encryption key. + + :return: The encryption key. + """ + key_filename = "secret.key" + key = self.read_secure_file(key_filename) + + if key is not None: + return key + + new_key = Fernet.generate_key() + self.save_secure_file(key_filename, new_key) + return new_key + + def save_tokens(self, access_token: str, expires_in: int) -> None: + """ + Save the access token and its expiration time. + + :param access_token: The access token to save. + :param expires_in: The expiration time of the access token in seconds. + """ + expiration_time = datetime.now() + timedelta(seconds=expires_in) + data = { + "access_token": access_token, + "expiration": expiration_time.isoformat(), + } + encrypted_data = self.fernet.encrypt(json.dumps(data).encode()) + self.save_secure_file(self.file_path, encrypted_data) + + def get_token(self) -> Optional[str]: + """ + Get the access token if it is valid and not expired. + + :return: The access token if valid and not expired, otherwise None. + """ + encrypted_data = self.read_secure_file(self.file_path) + + decrypted_data = self.fernet.decrypt(encrypted_data) + data = json.loads(decrypted_data) + + expiration = datetime.fromisoformat(data["expiration"]) + if expiration <= datetime.now(): + return None + + return data["access_token"] + + def get_secure_storage_path(self) -> Path: + """ + Get the secure storage path based on the operating system. + + :return: The secure storage path. + """ + if sys.platform == "win32": + # Windows: Use %LOCALAPPDATA% + base_path = os.environ.get("LOCALAPPDATA") + elif sys.platform == "darwin": + # macOS: Use ~/Library/Application Support + base_path = os.path.expanduser("~/Library/Application Support") + else: + # Linux and other Unix-like: Use ~/.local/share + base_path = os.path.expanduser("~/.local/share") + + app_name = "crewai/credentials" + storage_path = Path(base_path) / app_name + + storage_path.mkdir(parents=True, exist_ok=True) + + return storage_path + + def save_secure_file(self, filename: str, content: bytes) -> None: + """ + Save the content to a secure file. + + :param filename: The name of the file. + :param content: The content to save. + """ + storage_path = self.get_secure_storage_path() + file_path = storage_path / filename + + with open(file_path, "wb") as f: + f.write(content) + + # Set appropriate permissions (read/write for owner only) + os.chmod(file_path, 0o600) + + def read_secure_file(self, filename: str) -> Optional[bytes]: + """ + Read the content of a secure file. + + :param filename: The name of the file. + :return: The content of the file if it exists, otherwise None. + """ + storage_path = self.get_secure_storage_path() + file_path = storage_path / filename + + if not file_path.exists(): + return None + + with open(file_path, "rb") as f: + return f.read() diff --git a/src/crewai/cli/cli.py b/src/crewai/cli/cli.py index 2ca400000..cf1e7584b 100644 --- a/src/crewai/cli/cli.py +++ b/src/crewai/cli/cli.py @@ -1,3 +1,5 @@ +from typing import Optional + import click import pkg_resources @@ -7,6 +9,8 @@ from crewai.memory.storage.kickoff_task_outputs_storage import ( KickoffTaskOutputsSQLiteStorage, ) +from .authentication.main import AuthenticationCommand +from .deploy.main import DeployCommand from .evaluate_crew import evaluate_crew from .install_crew import install_crew from .replay_from_task import replay_task_command @@ -179,5 +183,70 @@ def run(): run_crew() +@crewai.command() +def signup(): + """Sign Up/Login to CrewAI+.""" + AuthenticationCommand().signup() + + +@crewai.command() +def login(): + """Sign Up/Login to CrewAI+.""" + AuthenticationCommand().signup() + + +# DEPLOY CREWAI+ COMMANDS +@crewai.group() +def deploy(): + """Deploy the Crew CLI group.""" + pass + + +@deploy.command(name="create") +def deploy_create(): + """Create a Crew deployment.""" + deploy_cmd = DeployCommand() + deploy_cmd.create_crew() + + +@deploy.command(name="list") +def deploy_list(): + """List all deployments.""" + deploy_cmd = DeployCommand() + deploy_cmd.list_crews() + + +@deploy.command(name="push") +@click.option("-u", "--uuid", type=str, help="Crew UUID parameter") +def deploy_push(uuid: Optional[str]): + """Deploy the Crew.""" + deploy_cmd = DeployCommand() + deploy_cmd.deploy(uuid=uuid) + + +@deploy.command(name="status") +@click.option("-u", "--uuid", type=str, help="Crew UUID parameter") +def deply_status(uuid: Optional[str]): + """Get the status of a deployment.""" + deploy_cmd = DeployCommand() + deploy_cmd.get_crew_status(uuid=uuid) + + +@deploy.command(name="logs") +@click.option("-u", "--uuid", type=str, help="Crew UUID parameter") +def deploy_logs(uuid: Optional[str]): + """Get the logs of a deployment.""" + deploy_cmd = DeployCommand() + deploy_cmd.get_crew_logs(uuid=uuid) + + +@deploy.command(name="remove") +@click.option("-u", "--uuid", type=str, help="Crew UUID parameter") +def deploy_remove(uuid: Optional[str]): + """Remove a deployment.""" + deploy_cmd = DeployCommand() + deploy_cmd.remove_crew(uuid=uuid) + + if __name__ == "__main__": crewai() diff --git a/src/crewai/cli/deploy/__init__.py b/src/crewai/cli/deploy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/crewai/cli/deploy/api.py b/src/crewai/cli/deploy/api.py new file mode 100644 index 000000000..942fc487e --- /dev/null +++ b/src/crewai/cli/deploy/api.py @@ -0,0 +1,63 @@ +from os import getenv + +import requests + + +class CrewAPI: + """ + CrewAPI class to interact with the crewAI+ API. + """ + + def __init__(self, api_key: str) -> None: + self.api_key = api_key + self.headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + self.base_url = getenv( + "CREWAI_BASE_URL", "https://dev.crewai.com/crewai_plus/api/v1/crews" + ) + + def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response: + url = f"{self.base_url}/{endpoint}" + return requests.request(method, url, headers=self.headers, **kwargs) + + # Deploy + def deploy_by_name(self, project_name: str) -> requests.Response: + return self._make_request("POST", f"by-name/{project_name}/deploy") + + def deploy_by_uuid(self, uuid: str) -> requests.Response: + return self._make_request("POST", f"{uuid}/deploy") + + # Status + def status_by_name(self, project_name: str) -> requests.Response: + return self._make_request("GET", f"by-name/{project_name}/status") + + def status_by_uuid(self, uuid: str) -> requests.Response: + return self._make_request("GET", f"{uuid}/status") + + # Logs + def logs_by_name( + self, project_name: str, log_type: str = "deployment" + ) -> requests.Response: + return self._make_request("GET", f"by-name/{project_name}/logs/{log_type}") + + def logs_by_uuid( + self, uuid: str, log_type: str = "deployment" + ) -> requests.Response: + return self._make_request("GET", f"{uuid}/logs/{log_type}") + + # Delete + def delete_by_name(self, project_name: str) -> requests.Response: + return self._make_request("DELETE", f"by-name/{project_name}") + + def delete_by_uuid(self, uuid: str) -> requests.Response: + return self._make_request("DELETE", f"{uuid}") + + # List + def list_crews(self) -> requests.Response: + return self._make_request("GET", "") + + # Create + def create_crew(self, payload) -> requests.Response: + return self._make_request("POST", "", json=payload) diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py new file mode 100644 index 000000000..d67e1cdc8 --- /dev/null +++ b/src/crewai/cli/deploy/main.py @@ -0,0 +1,289 @@ +from typing import Any, Dict, List, Optional + +from rich.console import Console + +from .api import CrewAPI +from .utils import ( + fetch_and_json_env_file, + get_auth_token, + get_git_remote_url, + get_project_name, +) + +console = Console() + + +class DeployCommand: + """ + A class to handle deployment-related operations for CrewAI projects. + """ + + def __init__(self): + """ + Initialize the DeployCommand with project name and API client. + """ + try: + access_token = get_auth_token() + except Exception: + console.print( + "Please sign up/login to CrewAI+ before using the CLI.", + style="bold red", + ) + console.print("Run 'crewai signup' to sign up/login.", style="bold green") + raise SystemExit + + self.project_name = get_project_name() + self.client = CrewAPI(api_key=access_token) + + def _handle_error(self, json_response: Dict[str, Any]) -> None: + """ + Handle and display error messages from API responses. + + Args: + json_response (Dict[str, Any]): The JSON response containing error information. + """ + error = json_response.get("error", "Unknown error") + message = json_response.get("message", "No message provided") + console.print(f"Error: {error}", style="bold red") + console.print(f"Message: {message}", style="bold red") + + def _standard_no_param_error_message(self) -> None: + """ + Display a standard error message when no UUID or project name is available. + """ + console.print( + "No UUID provided, project pyproject.toml not found or with error.", + style="bold red", + ) + + def _display_deployment_info(self, json_response: Dict[str, Any]) -> None: + """ + Display deployment information. + + Args: + json_response (Dict[str, Any]): The deployment information to display. + """ + console.print("Deploying the crew...\n", style="bold blue") + for key, value in json_response.items(): + console.print(f"{key.title()}: [green]{value}[/green]") + console.print("\nTo check the status of the deployment, run:") + console.print("crewai deploy status") + console.print(" or") + console.print(f"crewai deploy status --uuid \"{json_response['uuid']}\"") + + def _display_logs(self, log_messages: List[Dict[str, Any]]) -> None: + """ + Display log messages. + + Args: + log_messages (List[Dict[str, Any]]): The log messages to display. + """ + for log_message in log_messages: + console.print( + f"{log_message['timestamp']} - {log_message['level']}: {log_message['message']}" + ) + + def deploy(self, uuid: Optional[str] = None) -> None: + """ + Deploy a crew using either UUID or project name. + + Args: + uuid (Optional[str]): The UUID of the crew to deploy. + """ + console.print("Starting deployment...", style="bold blue") + if uuid: + response = self.client.deploy_by_uuid(uuid) + elif self.project_name: + response = self.client.deploy_by_name(self.project_name) + else: + self._standard_no_param_error_message() + return + + json_response = response.json() + if response.status_code == 200: + self._display_deployment_info(json_response) + else: + self._handle_error(json_response) + + def create_crew(self) -> None: + """ + Create a new crew deployment. + """ + console.print("Creating deployment...", style="bold blue") + env_vars = fetch_and_json_env_file() + remote_repo_url = get_git_remote_url() + + self._confirm_input(env_vars, remote_repo_url) + payload = self._create_payload(env_vars, remote_repo_url) + + response = self.client.create_crew(payload) + if response.status_code == 201: + self._display_creation_success(response.json()) + else: + self._handle_error(response.json()) + + def _confirm_input(self, env_vars: Dict[str, str], remote_repo_url: str) -> None: + """ + Confirm input parameters with the user. + + Args: + env_vars (Dict[str, str]): Environment variables. + remote_repo_url (str): Remote repository URL. + """ + input(f"Press Enter to continue with the following Env vars: {env_vars}") + input( + f"Press Enter to continue with the following remote repository: {remote_repo_url}\n" + ) + + def _create_payload( + self, + env_vars: Dict[str, str], + remote_repo_url: str, + ) -> Dict[str, Any]: + """ + Create the payload for crew creation. + + Args: + remote_repo_url (str): Remote repository URL. + env_vars (Dict[str, str]): Environment variables. + + Returns: + Dict[str, Any]: The payload for crew creation. + """ + return { + "deploy": { + "name": self.project_name, + "repo_clone_url": remote_repo_url, + "env": env_vars, + } + } + + def _display_creation_success(self, json_response: Dict[str, Any]) -> None: + """ + Display success message after crew creation. + + Args: + json_response (Dict[str, Any]): The response containing crew information. + """ + console.print("Deployment created successfully!\n", style="bold green") + console.print( + f"Name: {self.project_name} ({json_response['uuid']})", style="bold green" + ) + console.print(f"Status: {json_response['status']}", style="bold green") + console.print("\nTo (re)deploy the crew, run:") + console.print("crewai deploy push") + console.print(" or") + console.print(f"crewai deploy push --uuid {json_response['uuid']}") + + def list_crews(self) -> None: + """ + List all available crews. + """ + console.print("Listing all Crews\n", style="bold blue") + + response = self.client.list_crews() + json_response = response.json() + if response.status_code == 200: + self._display_crews(json_response) + else: + self._display_no_crews_message() + + def _display_crews(self, crews_data: List[Dict[str, Any]]) -> None: + """ + Display the list of crews. + + Args: + crews_data (List[Dict[str, Any]]): List of crew data to display. + """ + for crew_data in crews_data: + console.print( + f"- {crew_data['name']} ({crew_data['uuid']}) [blue]{crew_data['status']}[/blue]" + ) + + def _display_no_crews_message(self) -> None: + """ + Display a message when no crews are available. + """ + console.print("You don't have any Crews yet. Let's create one!", style="yellow") + console.print(" crewai create crew ", style="green") + + def get_crew_status(self, uuid: Optional[str] = None) -> None: + """ + Get the status of a crew. + + Args: + uuid (Optional[str]): The UUID of the crew to check. + """ + console.print("Fetching deployment status...", style="bold blue") + if uuid: + response = self.client.status_by_uuid(uuid) + elif self.project_name: + response = self.client.status_by_name(self.project_name) + else: + self._standard_no_param_error_message() + return + + json_response = response.json() + if response.status_code == 200: + self._display_crew_status(json_response) + else: + self._handle_error(json_response) + + def _display_crew_status(self, status_data: Dict[str, str]) -> None: + """ + Display the status of a crew. + + Args: + status_data (Dict[str, str]): The status data to display. + """ + console.print(f"Name:\t {status_data['name']}") + console.print(f"Status:\t {status_data['status']}") + + def get_crew_logs(self, uuid: Optional[str], log_type: str = "deployment") -> None: + """ + Get logs for a crew. + + Args: + uuid (Optional[str]): The UUID of the crew to get logs for. + log_type (str): The type of logs to retrieve (default: "deployment"). + """ + console.print(f"Fetching {log_type} logs...", style="bold blue") + + if uuid: + response = self.client.logs_by_uuid(uuid, log_type) + elif self.project_name: + response = self.client.logs_by_name(self.project_name, log_type) + else: + self._standard_no_param_error_message() + return + + if response.status_code == 200: + self._display_logs(response.json()) + else: + self._handle_error(response.json()) + + def remove_crew(self, uuid: Optional[str]) -> None: + """ + Remove a crew deployment. + + Args: + uuid (Optional[str]): The UUID of the crew to remove. + """ + console.print("Removing deployment...", style="bold blue") + + if uuid: + response = self.client.delete_by_uuid(uuid) + elif self.project_name: + response = self.client.delete_by_name(self.project_name) + else: + self._standard_no_param_error_message() + return + + if response.status_code == 204: + console.print( + f"Crew '{self.project_name}' removed successfully.", style="green" + ) + else: + console.print( + f"Failed to remove crew '{self.project_name}'", style="bold red" + ) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py new file mode 100644 index 000000000..8fe1851df --- /dev/null +++ b/src/crewai/cli/deploy/utils.py @@ -0,0 +1,117 @@ +import re +import subprocess + +import tomllib + +from ..authentication.utils import TokenManager + + +def get_git_remote_url() -> str: + """Get the Git repository's remote URL.""" + try: + # Run the git remote -v command + result = subprocess.run( + ["git", "remote", "-v"], capture_output=True, text=True, check=True + ) + + # Get the output + output = result.stdout + + # Parse the output to find the origin URL + matches = re.findall(r"origin\s+(.*?)\s+\(fetch\)", output) + + if matches: + return matches[0] # Return the first match (origin URL) + else: + print("No origin remote found.") + return "No remote URL found" + + except subprocess.CalledProcessError as e: + return f"Error running trying to fetch the Git Repository: {e}" + except FileNotFoundError: + return "Git command not found. Make sure Git is installed and in your PATH." + + +def get_project_name(pyproject_path: str = "pyproject.toml"): + """Get the project name from the pyproject.toml file.""" + try: + # Read the pyproject.toml file + with open(pyproject_path, "rb") as f: + pyproject_content = tomllib.load(f) + + # Extract the project name + project_name = pyproject_content["tool"]["poetry"]["name"] + + if "crewai" not in pyproject_content["tool"]["poetry"]["dependencies"]: + raise Exception("crewai is not in the dependencies.") + + return project_name + + except FileNotFoundError: + print(f"Error: {pyproject_path} not found.") + except KeyError: + print(f"Error: {pyproject_path} is not a valid pyproject.toml file.") + except tomllib.TOMLDecodeError: + print(f"Error: {pyproject_path} is not a valid TOML file.") + except Exception as e: + print(f"Error reading the pyproject.toml file: {e}") + + return None + + +def get_crewai_version(pyproject_path: str = "pyproject.toml") -> str: + """Get the version number of crewai from the pyproject.toml file.""" + try: + # Read the pyproject.toml file + with open("pyproject.toml", "rb") as f: + pyproject_content = tomllib.load(f) + + # Extract the version number of crewai + crewai_version = pyproject_content["tool"]["poetry"]["dependencies"]["crewai"][ + "version" + ] + + return crewai_version + + except FileNotFoundError: + print(f"Error: {pyproject_path} not found.") + except KeyError: + print(f"Error: {pyproject_path} is not a valid pyproject.toml file.") + except tomllib.TOMLDecodeError: + print(f"Error: {pyproject_path} is not a valid TOML file.") + except Exception as e: + print(f"Error reading the pyproject.toml file: {e}") + + return "no-version-found" + + +def fetch_and_json_env_file(env_file_path: str = ".env") -> dict: + """Fetch the environment variables from a .env file and return them as a dictionary.""" + try: + # Read the .env file + with open(env_file_path, "r") as f: + env_content = f.read() + + # Parse the .env file content to a dictionary + env_dict = {} + for line in env_content.splitlines(): + if line.strip() and not line.strip().startswith("#"): + key, value = line.split("=", 1) + env_dict[key.strip()] = value.strip() + + return env_dict + + except FileNotFoundError: + print(f"Error: {env_file_path} not found.") + except Exception as e: + print(f"Error reading the .env file: {e}") + + return {} + + +def get_auth_token() -> str: + """Get the authentication token.""" + access_token = TokenManager().get_token() + if not access_token: + raise Exception() + return access_token diff --git a/src/crewai/memory/memory.py b/src/crewai/memory/memory.py index 4b8c687c0..9df09d3c7 100644 --- a/src/crewai/memory/memory.py +++ b/src/crewai/memory/memory.py @@ -21,7 +21,7 @@ class Memory: if agent: metadata["agent"] = agent - self.storage.save(value, metadata) # type: ignore # Maybe BUG? Should be self.storage.save(key, value, metadata) + self.storage.save(value, metadata) def search(self, query: str) -> Dict[str, Any]: return self.storage.search(query) diff --git a/tests/cli/authentication/test_auth_main.py b/tests/cli/authentication/test_auth_main.py new file mode 100644 index 000000000..c56968aab --- /dev/null +++ b/tests/cli/authentication/test_auth_main.py @@ -0,0 +1,94 @@ +import unittest +from unittest.mock import MagicMock, patch + +import requests +from crewai.cli.authentication.main import AuthenticationCommand + + +class TestAuthenticationCommand(unittest.TestCase): + def setUp(self): + self.auth_command = AuthenticationCommand() + + @patch("crewai.cli.authentication.main.requests.post") + def test_get_device_code(self, mock_post): + mock_response = MagicMock() + mock_response.json.return_value = { + "device_code": "123456", + "user_code": "ABCDEF", + "verification_uri_complete": "https://example.com", + "interval": 5, + } + mock_post.return_value = mock_response + + device_code_data = self.auth_command._get_device_code() + + self.assertEqual(device_code_data["device_code"], "123456") + self.assertEqual(device_code_data["user_code"], "ABCDEF") + self.assertEqual( + device_code_data["verification_uri_complete"], "https://example.com" + ) + self.assertEqual(device_code_data["interval"], 5) + + @patch("crewai.cli.authentication.main.console.print") + @patch("crewai.cli.authentication.main.webbrowser.open") + def test_display_auth_instructions(self, mock_open, mock_print): + device_code_data = { + "verification_uri_complete": "https://example.com", + "user_code": "ABCDEF", + } + + self.auth_command._display_auth_instructions(device_code_data) + + mock_print.assert_any_call("1. Navigate to: ", "https://example.com") + mock_print.assert_any_call("2. Enter the following code: ", "ABCDEF") + mock_open.assert_called_once_with("https://example.com") + + @patch("crewai.cli.authentication.main.requests.post") + @patch("crewai.cli.authentication.main.validate_token") + @patch("crewai.cli.authentication.main.console.print") + def test_poll_for_token_success(self, mock_print, mock_validate_token, mock_post): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "id_token": "TOKEN", + "access_token": "ACCESS_TOKEN", + } + mock_post.return_value = mock_response + + self.auth_command._poll_for_token({"device_code": "123456"}) + + mock_validate_token.assert_called_once_with("TOKEN") + mock_print.assert_called_once_with("\nWelcome to CrewAI+ !!", style="green") + + @patch("crewai.cli.authentication.main.requests.post") + @patch("crewai.cli.authentication.main.console.print") + def test_poll_for_token_error(self, mock_print, mock_post): + mock_response = MagicMock() + mock_response.status_code = 400 + mock_response.json.return_value = { + "error": "invalid_request", + "error_description": "Invalid request", + } + mock_post.return_value = mock_response + + with self.assertRaises(requests.HTTPError): + self.auth_command._poll_for_token({"device_code": "123456"}) + + mock_print.assert_not_called() + + @patch("crewai.cli.authentication.main.requests.post") + @patch("crewai.cli.authentication.main.console.print") + def test_poll_for_token_timeout(self, mock_print, mock_post): + mock_response = MagicMock() + mock_response.status_code = 400 + mock_response.json.return_value = { + "error": "authorization_pending", + "error_description": "Authorization pending", + } + mock_post.return_value = mock_response + + self.auth_command._poll_for_token({"device_code": "123456", "interval": 0.01}) + + mock_print.assert_called_once_with( + "Timeout: Failed to get the token. Please try again.", style="bold red" + ) diff --git a/tests/cli/authentication/test_utils.py b/tests/cli/authentication/test_utils.py new file mode 100644 index 000000000..b04dceede --- /dev/null +++ b/tests/cli/authentication/test_utils.py @@ -0,0 +1,147 @@ +import json +import unittest +from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch + +from crewai.cli.authentication.utils import TokenManager, validate_token +from cryptography.fernet import Fernet + + +class TestValidateToken(unittest.TestCase): + @patch("crewai.cli.authentication.utils.AsymmetricSignatureVerifier") + @patch("crewai.cli.authentication.utils.TokenVerifier") + def test_validate_token(self, mock_token_verifier, mock_asymmetric_verifier): + mock_verifier_instance = mock_token_verifier.return_value + mock_id_token = "mock_id_token" + + validate_token(mock_id_token) + + mock_asymmetric_verifier.assert_called_once_with( + "https://dev-jzsr0j8zs0atl5ha.us.auth0.com/.well-known/jwks.json" + ) + mock_token_verifier.assert_called_once_with( + signature_verifier=mock_asymmetric_verifier.return_value, + issuer="https://dev-jzsr0j8zs0atl5ha.us.auth0.com/", + audience="CZtyRHuVW80HbLSjk4ggXNzjg4KAt7Oe", + ) + mock_verifier_instance.verify.assert_called_once_with(mock_id_token) + + +class TestTokenManager(unittest.TestCase): + def setUp(self): + self.token_manager = TokenManager() + + @patch("crewai.cli.authentication.utils.TokenManager.read_secure_file") + @patch("crewai.cli.authentication.utils.TokenManager.save_secure_file") + @patch("crewai.cli.authentication.utils.TokenManager._get_or_create_key") + def test_get_or_create_key_existing(self, mock_get_or_create, mock_save, mock_read): + mock_key = Fernet.generate_key() + mock_get_or_create.return_value = mock_key + + token_manager = TokenManager() + result = token_manager.key + + self.assertEqual(result, mock_key) + + @patch("crewai.cli.authentication.utils.Fernet.generate_key") + @patch("crewai.cli.authentication.utils.TokenManager.read_secure_file") + @patch("crewai.cli.authentication.utils.TokenManager.save_secure_file") + def test_get_or_create_key_new(self, mock_save, mock_read, mock_generate): + mock_key = b"new_key" + mock_read.return_value = None + mock_generate.return_value = mock_key + + result = self.token_manager._get_or_create_key() + + self.assertEqual(result, mock_key) + mock_read.assert_called_once_with("secret.key") + mock_generate.assert_called_once() + mock_save.assert_called_once_with("secret.key", mock_key) + + @patch("crewai.cli.authentication.utils.TokenManager.save_secure_file") + def test_save_tokens(self, mock_save): + access_token = "test_token" + expires_in = 3600 + + self.token_manager.save_tokens(access_token, expires_in) + + mock_save.assert_called_once() + args = mock_save.call_args[0] + self.assertEqual(args[0], "tokens.enc") + decrypted_data = self.token_manager.fernet.decrypt(args[1]) + data = json.loads(decrypted_data) + self.assertEqual(data["access_token"], access_token) + expiration = datetime.fromisoformat(data["expiration"]) + self.assertAlmostEqual( + expiration, + datetime.now() + timedelta(seconds=expires_in), + delta=timedelta(seconds=1), + ) + + @patch("crewai.cli.authentication.utils.TokenManager.read_secure_file") + def test_get_token_valid(self, mock_read): + access_token = "test_token" + expiration = (datetime.now() + timedelta(hours=1)).isoformat() + data = {"access_token": access_token, "expiration": expiration} + encrypted_data = self.token_manager.fernet.encrypt(json.dumps(data).encode()) + mock_read.return_value = encrypted_data + + result = self.token_manager.get_token() + + self.assertEqual(result, access_token) + + @patch("crewai.cli.authentication.utils.TokenManager.read_secure_file") + def test_get_token_expired(self, mock_read): + access_token = "test_token" + expiration = (datetime.now() - timedelta(hours=1)).isoformat() + data = {"access_token": access_token, "expiration": expiration} + encrypted_data = self.token_manager.fernet.encrypt(json.dumps(data).encode()) + mock_read.return_value = encrypted_data + + result = self.token_manager.get_token() + + self.assertIsNone(result) + + @patch("crewai.cli.authentication.utils.TokenManager.get_secure_storage_path") + @patch("builtins.open", new_callable=unittest.mock.mock_open) + @patch("crewai.cli.authentication.utils.os.chmod") + def test_save_secure_file(self, mock_chmod, mock_open, mock_get_path): + mock_path = MagicMock() + mock_get_path.return_value = mock_path + filename = "test_file.txt" + content = b"test_content" + + self.token_manager.save_secure_file(filename, content) + + mock_path.__truediv__.assert_called_once_with(filename) + mock_open.assert_called_once_with(mock_path.__truediv__.return_value, "wb") + mock_open().write.assert_called_once_with(content) + mock_chmod.assert_called_once_with(mock_path.__truediv__.return_value, 0o600) + + @patch("crewai.cli.authentication.utils.TokenManager.get_secure_storage_path") + @patch( + "builtins.open", new_callable=unittest.mock.mock_open, read_data=b"test_content" + ) + def test_read_secure_file_exists(self, mock_open, mock_get_path): + mock_path = MagicMock() + mock_get_path.return_value = mock_path + mock_path.__truediv__.return_value.exists.return_value = True + filename = "test_file.txt" + + result = self.token_manager.read_secure_file(filename) + + self.assertEqual(result, b"test_content") + mock_path.__truediv__.assert_called_once_with(filename) + mock_open.assert_called_once_with(mock_path.__truediv__.return_value, "rb") + + @patch("crewai.cli.authentication.utils.TokenManager.get_secure_storage_path") + def test_read_secure_file_not_exists(self, mock_get_path): + mock_path = MagicMock() + mock_get_path.return_value = mock_path + mock_path.__truediv__.return_value.exists.return_value = False + filename = "test_file.txt" + + result = self.token_manager.read_secure_file(filename) + + self.assertIsNone(result) + mock_path.__truediv__.assert_called_once_with(filename) diff --git a/tests/cli/cli_test.py b/tests/cli/cli_test.py index 4f606e213..b2fb8d0e5 100644 --- a/tests/cli/cli_test.py +++ b/tests/cli/cli_test.py @@ -2,8 +2,19 @@ from unittest import mock import pytest from click.testing import CliRunner - -from crewai.cli.cli import reset_memories, test, train, version +from crewai.cli.cli import ( + deploy_create, + deploy_list, + deploy_logs, + deploy_push, + deploy_remove, + deply_status, + reset_memories, + signup, + test, + train, + version, +) @pytest.fixture @@ -163,3 +174,106 @@ def test_test_invalid_string_iterations(evaluate_crew, runner): "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 ) + + +@mock.patch("crewai.cli.cli.AuthenticationCommand") +def test_signup(command, runner): + mock_auth = command.return_value + result = runner.invoke(signup) + + assert result.exit_code == 0 + mock_auth.signup.assert_called_once() + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_create(command, runner): + mock_deploy = command.return_value + result = runner.invoke(deploy_create) + + assert result.exit_code == 0 + mock_deploy.create_crew.assert_called_once() + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_list(command, runner): + mock_deploy = command.return_value + result = runner.invoke(deploy_list) + + assert result.exit_code == 0 + mock_deploy.list_crews.assert_called_once() + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_push(command, runner): + mock_deploy = command.return_value + uuid = "test-uuid" + result = runner.invoke(deploy_push, ["-u", uuid]) + + assert result.exit_code == 0 + mock_deploy.deploy.assert_called_once_with(uuid=uuid) + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_push_no_uuid(command, runner): + mock_deploy = command.return_value + result = runner.invoke(deploy_push) + + assert result.exit_code == 0 + mock_deploy.deploy.assert_called_once_with(uuid=None) + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_status(command, runner): + mock_deploy = command.return_value + uuid = "test-uuid" + result = runner.invoke(deply_status, ["-u", uuid]) + + assert result.exit_code == 0 + mock_deploy.get_crew_status.assert_called_once_with(uuid=uuid) + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_status_no_uuid(command, runner): + mock_deploy = command.return_value + result = runner.invoke(deply_status) + + assert result.exit_code == 0 + mock_deploy.get_crew_status.assert_called_once_with(uuid=None) + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_logs(command, runner): + mock_deploy = command.return_value + uuid = "test-uuid" + result = runner.invoke(deploy_logs, ["-u", uuid]) + + assert result.exit_code == 0 + mock_deploy.get_crew_logs.assert_called_once_with(uuid=uuid) + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_logs_no_uuid(command, runner): + mock_deploy = command.return_value + result = runner.invoke(deploy_logs) + + assert result.exit_code == 0 + mock_deploy.get_crew_logs.assert_called_once_with(uuid=None) + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_remove(command, runner): + mock_deploy = command.return_value + uuid = "test-uuid" + result = runner.invoke(deploy_remove, ["-u", uuid]) + + assert result.exit_code == 0 + mock_deploy.remove_crew.assert_called_once_with(uuid=uuid) + + +@mock.patch("crewai.cli.cli.DeployCommand") +def test_deploy_remove_no_uuid(command, runner): + mock_deploy = command.return_value + result = runner.invoke(deploy_remove) + + assert result.exit_code == 0 + mock_deploy.remove_crew.assert_called_once_with(uuid=None) diff --git a/tests/cli/deploy/test_api.py b/tests/cli/deploy/test_api.py new file mode 100644 index 000000000..f1a6c573d --- /dev/null +++ b/tests/cli/deploy/test_api.py @@ -0,0 +1,102 @@ +import unittest +from os import environ +from unittest.mock import MagicMock, patch + +from crewai.cli.deploy.api import CrewAPI + + +class TestCrewAPI(unittest.TestCase): + def setUp(self): + self.api_key = "test_api_key" + self.api = CrewAPI(self.api_key) + + def test_init(self): + self.assertEqual(self.api.api_key, self.api_key) + self.assertEqual( + self.api.headers, + { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + }, + ) + + @patch("crewai.cli.deploy.api.requests.request") + def test_make_request(self, mock_request): + mock_response = MagicMock() + mock_request.return_value = mock_response + + response = self.api._make_request("GET", "test_endpoint") + + mock_request.assert_called_once_with( + "GET", f"{self.api.base_url}/test_endpoint", headers=self.api.headers + ) + self.assertEqual(response, mock_response) + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_deploy_by_name(self, mock_make_request): + self.api.deploy_by_name("test_project") + mock_make_request.assert_called_once_with("POST", "by-name/test_project/deploy") + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_deploy_by_uuid(self, mock_make_request): + self.api.deploy_by_uuid("test_uuid") + mock_make_request.assert_called_once_with("POST", "test_uuid/deploy") + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_status_by_name(self, mock_make_request): + self.api.status_by_name("test_project") + mock_make_request.assert_called_once_with("GET", "by-name/test_project/status") + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_status_by_uuid(self, mock_make_request): + self.api.status_by_uuid("test_uuid") + mock_make_request.assert_called_once_with("GET", "test_uuid/status") + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_logs_by_name(self, mock_make_request): + self.api.logs_by_name("test_project") + mock_make_request.assert_called_once_with( + "GET", "by-name/test_project/logs/deployment" + ) + + self.api.logs_by_name("test_project", "custom_log") + mock_make_request.assert_called_with( + "GET", "by-name/test_project/logs/custom_log" + ) + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_logs_by_uuid(self, mock_make_request): + self.api.logs_by_uuid("test_uuid") + mock_make_request.assert_called_once_with("GET", "test_uuid/logs/deployment") + + self.api.logs_by_uuid("test_uuid", "custom_log") + mock_make_request.assert_called_with("GET", "test_uuid/logs/custom_log") + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_delete_by_name(self, mock_make_request): + self.api.delete_by_name("test_project") + mock_make_request.assert_called_once_with("DELETE", "by-name/test_project") + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_delete_by_uuid(self, mock_make_request): + self.api.delete_by_uuid("test_uuid") + mock_make_request.assert_called_once_with("DELETE", "test_uuid") + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_list_crews(self, mock_make_request): + self.api.list_crews() + mock_make_request.assert_called_once_with("GET", "") + + @patch("crewai.cli.deploy.api.CrewAPI._make_request") + def test_create_crew(self, mock_make_request): + payload = {"name": "test_crew"} + self.api.create_crew(payload) + mock_make_request.assert_called_once_with("POST", "", json=payload) + + @patch.dict(environ, {"CREWAI_BASE_URL": "https://custom-url.com/api"}) + def test_custom_base_url(self): + custom_api = CrewAPI("test_key") + self.assertEqual( + custom_api.base_url, + "https://custom-url.com/api", + ) diff --git a/tests/cli/deploy/test_deploy_main.py b/tests/cli/deploy/test_deploy_main.py new file mode 100644 index 000000000..f4b08d877 --- /dev/null +++ b/tests/cli/deploy/test_deploy_main.py @@ -0,0 +1,153 @@ +import unittest +from io import StringIO +from unittest.mock import MagicMock, patch + +from crewai.cli.deploy.main import DeployCommand + + +class TestDeployCommand(unittest.TestCase): + @patch("crewai.cli.deploy.main.get_auth_token") + @patch("crewai.cli.deploy.main.get_project_name") + @patch("crewai.cli.deploy.main.CrewAPI") + def setUp(self, mock_crew_api, mock_get_project_name, mock_get_auth_token): + self.mock_get_auth_token = mock_get_auth_token + self.mock_get_project_name = mock_get_project_name + self.mock_crew_api = mock_crew_api + + self.mock_get_auth_token.return_value = "test_token" + self.mock_get_project_name.return_value = "test_project" + + self.deploy_command = DeployCommand() + self.mock_client = self.deploy_command.client + + def test_init_success(self): + self.assertEqual(self.deploy_command.project_name, "test_project") + self.mock_crew_api.assert_called_once_with(api_key="test_token") + + @patch("crewai.cli.deploy.main.get_auth_token") + def test_init_failure(self, mock_get_auth_token): + mock_get_auth_token.side_effect = Exception("Auth failed") + + with self.assertRaises(SystemExit): + DeployCommand() + + def test_handle_error(self): + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command._handle_error( + {"error": "Test error", "message": "Test message"} + ) + self.assertIn("Error: Test error", fake_out.getvalue()) + self.assertIn("Message: Test message", fake_out.getvalue()) + + def test_standard_no_param_error_message(self): + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command._standard_no_param_error_message() + self.assertIn("No UUID provided", fake_out.getvalue()) + + def test_display_deployment_info(self): + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command._display_deployment_info( + {"uuid": "test-uuid", "status": "deployed"} + ) + self.assertIn("Deploying the crew...", fake_out.getvalue()) + self.assertIn("test-uuid", fake_out.getvalue()) + self.assertIn("deployed", fake_out.getvalue()) + + def test_display_logs(self): + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command._display_logs( + [{"timestamp": "2023-01-01", "level": "INFO", "message": "Test log"}] + ) + self.assertIn("2023-01-01 - INFO: Test log", fake_out.getvalue()) + + @patch("crewai.cli.deploy.main.DeployCommand._display_deployment_info") + def test_deploy_with_uuid(self, mock_display): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"uuid": "test-uuid"} + self.mock_client.deploy_by_uuid.return_value = mock_response + + self.deploy_command.deploy(uuid="test-uuid") + + self.mock_client.deploy_by_uuid.assert_called_once_with("test-uuid") + mock_display.assert_called_once_with({"uuid": "test-uuid"}) + + @patch("crewai.cli.deploy.main.DeployCommand._display_deployment_info") + def test_deploy_with_project_name(self, mock_display): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"uuid": "test-uuid"} + self.mock_client.deploy_by_name.return_value = mock_response + + self.deploy_command.deploy() + + self.mock_client.deploy_by_name.assert_called_once_with("test_project") + mock_display.assert_called_once_with({"uuid": "test-uuid"}) + + @patch("crewai.cli.deploy.main.fetch_and_json_env_file") + @patch("crewai.cli.deploy.main.get_git_remote_url") + @patch("builtins.input") + def test_create_crew(self, mock_input, mock_get_git_remote_url, mock_fetch_env): + mock_fetch_env.return_value = {"ENV_VAR": "value"} + mock_get_git_remote_url.return_value = "https://github.com/test/repo.git" + mock_input.return_value = "" + + mock_response = MagicMock() + mock_response.status_code = 201 + mock_response.json.return_value = {"uuid": "new-uuid", "status": "created"} + self.mock_client.create_crew.return_value = mock_response + + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command.create_crew() + self.assertIn("Deployment created successfully!", fake_out.getvalue()) + self.assertIn("new-uuid", fake_out.getvalue()) + + def test_list_crews(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = [ + {"name": "Crew1", "uuid": "uuid1", "status": "active"}, + {"name": "Crew2", "uuid": "uuid2", "status": "inactive"}, + ] + self.mock_client.list_crews.return_value = mock_response + + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command.list_crews() + self.assertIn("Crew1 (uuid1) active", fake_out.getvalue()) + self.assertIn("Crew2 (uuid2) inactive", fake_out.getvalue()) + + def test_get_crew_status(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"name": "TestCrew", "status": "active"} + self.mock_client.status_by_name.return_value = mock_response + + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command.get_crew_status() + self.assertIn("TestCrew", fake_out.getvalue()) + self.assertIn("active", fake_out.getvalue()) + + def test_get_crew_logs(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = [ + {"timestamp": "2023-01-01", "level": "INFO", "message": "Log1"}, + {"timestamp": "2023-01-02", "level": "ERROR", "message": "Log2"}, + ] + self.mock_client.logs_by_name.return_value = mock_response + + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command.get_crew_logs(None) + self.assertIn("2023-01-01 - INFO: Log1", fake_out.getvalue()) + self.assertIn("2023-01-02 - ERROR: Log2", fake_out.getvalue()) + + def test_remove_crew(self): + mock_response = MagicMock() + mock_response.status_code = 204 + self.mock_client.delete_by_name.return_value = mock_response + + with patch("sys.stdout", new=StringIO()) as fake_out: + self.deploy_command.remove_crew(None) + self.assertIn( + "Crew 'test_project' removed successfully", fake_out.getvalue() + ) From 782ce22d9951a7f969f9301f015e5d0d5886c44c Mon Sep 17 00:00:00 2001 From: Paul Nugent Date: Fri, 23 Aug 2024 14:39:06 +0100 Subject: [PATCH 03/20] Update LLM-Connections.md (#1190) Added missing quotes around os.environ Co-authored-by: Eduardo Chiarotti --- docs/how-to/LLM-Connections.md | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/how-to/LLM-Connections.md b/docs/how-to/LLM-Connections.md index 1f0eafd5e..222554535 100644 --- a/docs/how-to/LLM-Connections.md +++ b/docs/how-to/LLM-Connections.md @@ -112,30 +112,30 @@ Switch between APIs and models seamlessly using environment variables, supportin ### Configuration Examples #### FastChat ```sh -os.environ[OPENAI_API_BASE]="http://localhost:8001/v1" -os.environ[OPENAI_MODEL_NAME]='oh-2.5m7b-q51' -os.environ[OPENAI_API_KEY]=NA +os.environ["OPENAI_API_BASE"]='http://localhost:8001/v1' +os.environ["OPENAI_MODEL_NAME"]='oh-2.5m7b-q51' +os.environ[OPENAI_API_KEY]='NA' ``` #### LM Studio Launch [LM Studio](https://lmstudio.ai) and go to the Server tab. Then select a model from the dropdown menu and wait for it to load. Once it's loaded, click the green Start Server button and use the URL, port, and API key that's shown (you can modify them). Below is an example of the default settings as of LM Studio 0.2.19: ```sh -os.environ[OPENAI_API_BASE]="http://localhost:1234/v1" -os.environ[OPENAI_API_KEY]="lm-studio" +os.environ["OPENAI_API_BASE"]='http://localhost:1234/v1' +os.environ["OPENAI_API_KEY"]='lm-studio' ``` #### Groq API ```sh -os.environ[OPENAI_API_KEY]=your-groq-api-key -os.environ[OPENAI_MODEL_NAME]='llama3-8b-8192' -os.environ[OPENAI_API_BASE]=https://api.groq.com/openai/v1 +os.environ["OPENAI_API_KEY"]='your-groq-api-key' +os.environ["OPENAI_MODEL_NAME"]='llama3-8b-8192' +os.environ["OPENAI_API_BASE"]='https://api.groq.com/openai/v1' ``` #### Mistral API ```sh -os.environ[OPENAI_API_KEY]=your-mistral-api-key -os.environ[OPENAI_API_BASE]=https://api.mistral.ai/v1 -os.environ[OPENAI_MODEL_NAME]="mistral-small" +os.environ["OPENAI_API_KEY"]='your-mistral-api-key' +os.environ["OPENAI_API_BASE"]='https://api.mistral.ai/v1' +os.environ["OPENAI_MODEL_NAME"]='mistral-small' ``` ### Solar @@ -143,8 +143,8 @@ os.environ[OPENAI_MODEL_NAME]="mistral-small" from langchain_community.chat_models.solar import SolarChat ``` ```sh -os.environ[SOLAR_API_BASE]="https://api.upstage.ai/v1/solar" -os.environ[SOLAR_API_KEY]="your-solar-api-key" +os.environ["SOLAR_API_BASE"]='https://api.upstage.ai/v1/solar' +os.environ["SOLAR_API_KEY"]='your-solar-api-key' ``` # Free developer API key available here: https://console.upstage.ai/services/solar @@ -155,7 +155,7 @@ os.environ[SOLAR_API_KEY]="your-solar-api-key" ```python from langchain_cohere import ChatCohere # Initialize language model -os.environ["COHERE_API_KEY"] = "your-cohere-api-key" +os.environ["COHERE_API_KEY"]='your-cohere-api-key' llm = ChatCohere() # Free developer API key available here: https://cohere.com/ @@ -166,10 +166,10 @@ llm = ChatCohere() For Azure OpenAI API integration, set the following environment variables: ```sh -os.environ[AZURE_OPENAI_DEPLOYMENT] = "Your deployment" -os.environ["OPENAI_API_VERSION"] = "2023-12-01-preview" -os.environ["AZURE_OPENAI_ENDPOINT"] = "Your Endpoint" -os.environ["AZURE_OPENAI_API_KEY"] = "" +os.environ["AZURE_OPENAI_DEPLOYMENT"]='Your deployment' +os.environ["OPENAI_API_VERSION"]='2023-12-01-preview' +os.environ["AZURE_OPENAI_ENDPOINT"]='Your Endpoint' +os.environ["AZURE_OPENAI_API_KEY"]='Your API Key' ``` ### Example Agent with Azure LLM From f777c1c2e07f113f9b649757e5b1ff4638cbe654 Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Fri, 23 Aug 2024 10:52:36 -0300 Subject: [PATCH 04/20] fix: All files pre commit (#1249) --- src/crewai/agents/__init__.py | 2 ++ src/crewai/agents/cache/__init__.py | 2 ++ src/crewai/crews/__init__.py | 2 ++ src/crewai/memory/__init__.py | 2 ++ src/crewai/memory/storage/rag_storage.py | 9 +++++---- src/crewai/pipeline/__init__.py | 2 ++ src/crewai/routers/__init__.py | 2 ++ src/crewai/task.py | 4 +++- src/crewai/telemetry/__init__.py | 2 ++ src/crewai/utilities/parser.py | 5 ++++- tests/pipeline/test_pipeline.py | 4 +++- 11 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/crewai/agents/__init__.py b/src/crewai/agents/__init__.py index 2bcc6f28a..cd6883fa5 100644 --- a/src/crewai/agents/__init__.py +++ b/src/crewai/agents/__init__.py @@ -2,3 +2,5 @@ from .cache.cache_handler import CacheHandler from .executor import CrewAgentExecutor from .parser import CrewAgentParser from .tools_handler import ToolsHandler + +__all__ = ["CacheHandler", "CrewAgentExecutor", "CrewAgentParser", "ToolsHandler"] diff --git a/src/crewai/agents/cache/__init__.py b/src/crewai/agents/cache/__init__.py index c91d30c8b..6b4d20081 100644 --- a/src/crewai/agents/cache/__init__.py +++ b/src/crewai/agents/cache/__init__.py @@ -1 +1,3 @@ from .cache_handler import CacheHandler + +__all__ = ["CacheHandler"] diff --git a/src/crewai/crews/__init__.py b/src/crewai/crews/__init__.py index 32dd1158d..92f297d9f 100644 --- a/src/crewai/crews/__init__.py +++ b/src/crewai/crews/__init__.py @@ -1 +1,3 @@ from .crew_output import CrewOutput + +__all__ = ["CrewOutput"] diff --git a/src/crewai/memory/__init__.py b/src/crewai/memory/__init__.py index 91d4db9e8..8182bede7 100644 --- a/src/crewai/memory/__init__.py +++ b/src/crewai/memory/__init__.py @@ -1,3 +1,5 @@ from .entity.entity_memory import EntityMemory from .long_term.long_term_memory import LongTermMemory from .short_term.short_term_memory import ShortTermMemory + +__all__ = ["EntityMemory", "LongTermMemory", "ShortTermMemory"] diff --git a/src/crewai/memory/storage/rag_storage.py b/src/crewai/memory/storage/rag_storage.py index 7d3758ab7..6af196370 100644 --- a/src/crewai/memory/storage/rag_storage.py +++ b/src/crewai/memory/storage/rag_storage.py @@ -5,13 +5,14 @@ import os import shutil from typing import Any, Dict, List, Optional -from crewai.memory.storage.interface import Storage -from crewai.utilities.paths import db_storage_path from embedchain import App from embedchain.llm.base import BaseLlm from embedchain.models.data_type import DataType from embedchain.vectordb.chroma import InvalidDimensionException +from crewai.memory.storage.interface import Storage +from crewai.utilities.paths import db_storage_path + @contextlib.contextmanager def suppress_logging( @@ -77,12 +78,12 @@ class RAGStorage(Storage): self.app.llm = FakeLLM() if allow_reset: self.app.reset() - + def _sanitize_role(self, role: str) -> str: """ Sanitizes agent roles to ensure valid directory names. """ - return role.replace('\n', '').replace(' ', '_').replace('/', '_') + return role.replace("\n", "").replace(" ", "_").replace("/", "_") def save(self, value: Any, metadata: Dict[str, Any]) -> None: self._generate_embedding(value, metadata) diff --git a/src/crewai/pipeline/__init__.py b/src/crewai/pipeline/__init__.py index 573154b25..d9821e34c 100644 --- a/src/crewai/pipeline/__init__.py +++ b/src/crewai/pipeline/__init__.py @@ -1,3 +1,5 @@ from crewai.pipeline.pipeline import Pipeline from crewai.pipeline.pipeline_kickoff_result import PipelineKickoffResult from crewai.pipeline.pipeline_output import PipelineOutput + +__all__ = ["Pipeline", "PipelineKickoffResult", "PipelineOutput"] diff --git a/src/crewai/routers/__init__.py b/src/crewai/routers/__init__.py index b21d76bd2..c53802bef 100644 --- a/src/crewai/routers/__init__.py +++ b/src/crewai/routers/__init__.py @@ -1 +1,3 @@ from crewai.routers.router import Router + +__all__ = ["Router"] diff --git a/src/crewai/task.py b/src/crewai/task.py index ea292772a..573e83c7d 100644 --- a/src/crewai/task.py +++ b/src/crewai/task.py @@ -273,7 +273,9 @@ class Task(BaseModel): content = ( json_output if json_output - else pydantic_output.model_dump_json() if pydantic_output else result + else pydantic_output.model_dump_json() + if pydantic_output + else result ) self._save_file(content) diff --git a/src/crewai/telemetry/__init__.py b/src/crewai/telemetry/__init__.py index 6caed962a..1556f4fa5 100644 --- a/src/crewai/telemetry/__init__.py +++ b/src/crewai/telemetry/__init__.py @@ -1 +1,3 @@ from .telemetry import Telemetry + +__all__ = ["Telemetry"] diff --git a/src/crewai/utilities/parser.py b/src/crewai/utilities/parser.py index 8d286170e..c19cc1133 100644 --- a/src/crewai/utilities/parser.py +++ b/src/crewai/utilities/parser.py @@ -1,5 +1,6 @@ import re + class YamlParser: @staticmethod def parse(file): @@ -16,7 +17,9 @@ class YamlParser: # Replace single { and } with doubled ones, while leaving already doubled ones intact and the other special characters {# and {% modified_content = re.sub(r"(? 80 - else "route2" if x.get("score", 0) > 50 else "default" + else "route2" + if x.get("score", 0) > 50 + else "default" ), ) ) From c012e0ff8df9f381f47f4c48c3532be54182bd0b Mon Sep 17 00:00:00 2001 From: "Brandon Hancock (bhancock_ai)" <109994880+bhancockio@users.noreply.github.com> Date: Fri, 23 Aug 2024 12:51:58 -0400 Subject: [PATCH 05/20] Update async docs with more examples (#1254) * Update async docs with more examples * Add use cases --- docs/how-to/Kickoff-async.md | 73 +++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/docs/how-to/Kickoff-async.md b/docs/how-to/Kickoff-async.md index 0a2d10344..b080a4e35 100644 --- a/docs/how-to/Kickoff-async.md +++ b/docs/how-to/Kickoff-async.md @@ -4,9 +4,11 @@ description: Kickoff a Crew Asynchronously --- ## Introduction + CrewAI provides the ability to kickoff a crew asynchronously, allowing you to start the crew execution in a non-blocking manner. This feature is particularly useful when you want to run multiple crews concurrently or when you need to perform other tasks while the crew is executing. ## Asynchronous Crew Execution + To kickoff a crew asynchronously, use the `kickoff_async()` method. This method initiates the crew execution in a separate thread, allowing the main thread to continue executing other tasks. ### Method Signature @@ -23,10 +25,20 @@ def kickoff_async(self, inputs: dict) -> CrewOutput: - `CrewOutput`: An object representing the result of the crew execution. -## Example -Here's an example of how to kickoff a crew asynchronously: +## Potential Use Cases + +- **Parallel Content Generation**: Kickoff multiple independent crews asynchronously, each responsible for generating content on different topics. For example, one crew might research and draft an article on AI trends, while another crew generates social media posts about a new product launch. Each crew operates independently, allowing content production to scale efficiently. + +- **Concurrent Market Research Tasks**: Launch multiple crews asynchronously to conduct market research in parallel. One crew might analyze industry trends, while another examines competitor strategies, and yet another evaluates consumer sentiment. Each crew independently completes its task, enabling faster and more comprehensive insights. + +- **Independent Travel Planning Modules**: Execute separate crews to independently plan different aspects of a trip. One crew might handle flight options, another handles accommodation, and a third plans activities. Each crew works asynchronously, allowing various components of the trip to be planned simultaneously and independently for faster results. + +## Example: Single Asynchronous Crew Execution + +Here's an example of how to kickoff a crew asynchronously using asyncio and awaiting the result: ```python +import asyncio from crewai import Crew, Agent, Task # Create an agent with code execution enabled @@ -49,6 +61,57 @@ analysis_crew = Crew( tasks=[data_analysis_task] ) -# Execute the crew asynchronously -result = analysis_crew.kickoff_async(inputs={"ages": [25, 30, 35, 40, 45]}) -``` \ No newline at end of file +# Async function to kickoff the crew asynchronously +async def async_crew_execution(): + result = await analysis_crew.kickoff_async(inputs={"ages": [25, 30, 35, 40, 45]}) + print("Crew Result:", result) + +# Run the async function +asyncio.run(async_crew_execution()) +``` + +## Example: Multiple Asynchronous Crew Executions + +In this example, we'll show how to kickoff multiple crews asynchronously and wait for all of them to complete using asyncio.gather(): + +```python +import asyncio +from crewai import Crew, Agent, Task + +# Create an agent with code execution enabled +coding_agent = Agent( + role="Python Data Analyst", + goal="Analyze data and provide insights using Python", + backstory="You are an experienced data analyst with strong Python skills.", + allow_code_execution=True +) + +# Create tasks that require code execution +task_1 = Task( + description="Analyze the first dataset and calculate the average age of participants. Ages: {ages}", + agent=coding_agent +) + +task_2 = Task( + description="Analyze the second dataset and calculate the average age of participants. Ages: {ages}", + agent=coding_agent +) + +# Create two crews and add tasks +crew_1 = Crew(agents=[coding_agent], tasks=[task_1]) +crew_2 = Crew(agents=[coding_agent], tasks=[task_2]) + +# Async function to kickoff multiple crews asynchronously and wait for all to finish +async def async_multiple_crews(): + result_1 = crew_1.kickoff_async(inputs={"ages": [25, 30, 35, 40, 45]}) + result_2 = crew_2.kickoff_async(inputs={"ages": [20, 22, 24, 28, 30]}) + + # Wait for both crews to finish + results = await asyncio.gather(result_1, result_2) + + for i, result in enumerate(results, 1): + print(f"Crew {i} Result:", result) + +# Run the async function +asyncio.run(async_multiple_crews()) +``` From 5ff178084e4675fe0a536f28fd763445545e84bc Mon Sep 17 00:00:00 2001 From: "Brandon Hancock (bhancock_ai)" <109994880+bhancockio@users.noreply.github.com> Date: Fri, 23 Aug 2024 12:58:37 -0400 Subject: [PATCH 06/20] Fix deployment name issue to support Azure (#1253) * Fix deployment name issue to support Azure * More carefully check atters on llm --- src/crewai/agent.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/crewai/agent.py b/src/crewai/agent.py index e0b193a01..0711c741d 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -117,16 +117,21 @@ class Agent(BaseAgent): def post_init_setup(self): self.agent_ops_agent_name = self.role - if hasattr(self.llm, "model_name"): - self._setup_llm_callbacks() + # Different llms store the model name in different attributes + model_name = getattr(self.llm, "model_name", None) or getattr( + self.llm, "deployment_name", None + ) + + if model_name: + self._setup_llm_callbacks(model_name) if not self.agent_executor: self._setup_agent_executor() return self - def _setup_llm_callbacks(self): - token_handler = TokenCalcHandler(self.llm.model_name, self._token_process) + def _setup_llm_callbacks(self, model_name: str): + token_handler = TokenCalcHandler(model_name, self._token_process) if not isinstance(self.llm.callbacks, list): self.llm.callbacks = [] From 172758020cfdd4625585213113a0a513760cc0bf Mon Sep 17 00:00:00 2001 From: mvanwyk <2493311+mvanwyk@users.noreply.github.com> Date: Sat, 24 Aug 2024 20:45:59 +0200 Subject: [PATCH 07/20] bug: fix incorrect mkdocs site_url (#1238) * bug: fix incorrect mkdocs site_url * bug: fix incorrect mkdocs repo_url Co-authored-by: Eduardo Chiarotti --------- Co-authored-by: Eduardo Chiarotti --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 8d11351ea..cc5b4a97d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,8 +2,8 @@ site_name: crewAI site_author: crewAI, Inc site_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. repo_name: crewAI -repo_url: https://github.com/joaomdmoura/crewai/ -site_url: https://crewai.com +repo_url: https://github.com/crewAIInc/crewAI +site_url: https://docs.crewai.com edit_uri: edit/main/docs/ copyright: Copyright © 2024 crewAI, Inc From fa937bf3a79c8bf4f9f3fba45c86eebc0cf98330 Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 10:22:54 -0300 Subject: [PATCH 08/20] Add Python 3.10 support to CLI --- src/crewai/cli/deploy/utils.py | 55 ++++++++++++++++++++---- tests/cli/deploy/test_deploy_main.py | 62 +++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 8fe1851df..5a701e675 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -1,11 +1,40 @@ +import sys import re import subprocess -import tomllib - from ..authentication.utils import TokenManager +if sys.version_info >= (3, 11): + import tomllib + + +def simple_toml_parser(content): + result = {} + current_section = result + for line in content.split('\n'): + line = line.strip() + if line.startswith('[') and line.endswith(']'): + # New section + section = line[1:-1].split('.') + current_section = result + for key in section: + current_section = current_section.setdefault(key, {}) + elif '=' in line: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip().strip('"') + current_section[key] = value + return result + + +def parse_toml(content): + if sys.version_info >= (3, 11): + return tomllib.loads(content) + else: + return simple_toml_parser(content) + + def get_git_remote_url() -> str: """Get the Git repository's remote URL.""" try: @@ -37,7 +66,7 @@ def get_project_name(pyproject_path: str = "pyproject.toml"): try: # Read the pyproject.toml file with open(pyproject_path, "rb") as f: - pyproject_content = tomllib.load(f) + pyproject_content = parse_toml(f.read()) # Extract the project name project_name = pyproject_content["tool"]["poetry"]["name"] @@ -51,8 +80,12 @@ def get_project_name(pyproject_path: str = "pyproject.toml"): print(f"Error: {pyproject_path} not found.") except KeyError: print(f"Error: {pyproject_path} is not a valid pyproject.toml file.") - except tomllib.TOMLDecodeError: - print(f"Error: {pyproject_path} is not a valid TOML file.") + except tomllib.TOMLDecodeError if sys.version_info >= (3, 11) else Exception as e: + print( + f"Error: {pyproject_path} is not a valid TOML file." + if sys.version_info >= (3, 11) + else f"Error reading the pyproject.toml file: {e}" + ) except Exception as e: print(f"Error reading the pyproject.toml file: {e}") @@ -63,8 +96,8 @@ def get_crewai_version(pyproject_path: str = "pyproject.toml") -> str: """Get the version number of crewai from the pyproject.toml file.""" try: # Read the pyproject.toml file - with open("pyproject.toml", "rb") as f: - pyproject_content = tomllib.load(f) + with open(pyproject_path, "rb") as f: + pyproject_content = parse_toml(f.read()) # Extract the version number of crewai crewai_version = pyproject_content["tool"]["poetry"]["dependencies"]["crewai"][ @@ -77,8 +110,12 @@ def get_crewai_version(pyproject_path: str = "pyproject.toml") -> str: print(f"Error: {pyproject_path} not found.") except KeyError: print(f"Error: {pyproject_path} is not a valid pyproject.toml file.") - except tomllib.TOMLDecodeError: - print(f"Error: {pyproject_path} is not a valid TOML file.") + except tomllib.TOMLDecodeError if sys.version_info >= (3, 11) else Exception as e: + print( + f"Error: {pyproject_path} is not a valid TOML file." + if sys.version_info >= (3, 11) + else f"Error reading the pyproject.toml file: {e}" + ) except Exception as e: print(f"Error reading the pyproject.toml file: {e}") diff --git a/tests/cli/deploy/test_deploy_main.py b/tests/cli/deploy/test_deploy_main.py index f4b08d877..52171459a 100644 --- a/tests/cli/deploy/test_deploy_main.py +++ b/tests/cli/deploy/test_deploy_main.py @@ -1,9 +1,10 @@ import unittest from io import StringIO from unittest.mock import MagicMock, patch +import sys from crewai.cli.deploy.main import DeployCommand - +from crewai.cli.deploy.utils import parse_toml class TestDeployCommand(unittest.TestCase): @patch("crewai.cli.deploy.main.get_auth_token") @@ -151,3 +152,62 @@ class TestDeployCommand(unittest.TestCase): self.assertIn( "Crew 'test_project' removed successfully", fake_out.getvalue() ) + + @patch('crewai.cli.deploy.utils.sys.version_info', (3, 10)) + def test_parse_toml_python_310(self): + toml_content = """ + [tool.poetry] + name = "test_project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.10" + crewai = "^0.1.0" + """ + parsed = parse_toml(toml_content) + self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') + self.assertEqual(parsed['tool']['poetry']['dependencies']['crewai'], '^0.1.0') + + @unittest.skipIf(sys.version_info < (3, 11), "Requires Python 3.11+") + def test_parse_toml_python_311_plus(self): + toml_content = """ + [tool.poetry] + name = "test_project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.11" + crewai = "^0.1.0" + """ + parsed = parse_toml(toml_content) + self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') + self.assertEqual(parsed['tool']['poetry']['dependencies']['crewai'], '^0.1.0') + + @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data=""" + [tool.poetry] + name = "test_project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.10" + crewai = "^0.1.0" + """) + def test_get_project_name_python_310(self, mock_open): + from crewai.cli.deploy.utils import get_project_name + project_name = get_project_name() + self.assertEqual(project_name, 'test_project') + + @unittest.skipIf(sys.version_info < (3, 11), "Requires Python 3.11+") + @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data=""" + [tool.poetry] + name = "test_project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.11" + crewai = "^0.1.0" + """) + def test_get_project_name_python_311_plus(self, mock_open): + from crewai.cli.deploy.utils import get_project_name + project_name = get_project_name() + self.assertEqual(project_name, 'test_project') From 345f1eacde618bd7f78701051e2fc5f5c499ea90 Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 11:14:04 -0300 Subject: [PATCH 09/20] Get current crewai version from poetry.lock --- src/crewai/cli/deploy/utils.py | 38 +++++++++++------------- tests/cli/deploy/test_deploy_main.py | 44 ++++++++++++++++------------ 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 5a701e675..a7534fc48 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -1,6 +1,7 @@ import sys import re import subprocess +import ast from ..authentication.utils import TokenManager @@ -92,32 +93,27 @@ def get_project_name(pyproject_path: str = "pyproject.toml"): return None -def get_crewai_version(pyproject_path: str = "pyproject.toml") -> str: - """Get the version number of crewai from the pyproject.toml file.""" +def get_crewai_version(poetry_lock_path: str = "poetry.lock") -> str: + """Get the version number of crewai from the poetry.lock file.""" try: - # Read the pyproject.toml file - with open(pyproject_path, "rb") as f: - pyproject_content = parse_toml(f.read()) + with open(poetry_lock_path, "r") as f: + lock_content = f.read() - # Extract the version number of crewai - crewai_version = pyproject_content["tool"]["poetry"]["dependencies"]["crewai"][ - "version" - ] - - return crewai_version + 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: {pyproject_path} not found.") - except KeyError: - print(f"Error: {pyproject_path} is not a valid pyproject.toml file.") - except tomllib.TOMLDecodeError if sys.version_info >= (3, 11) else Exception as e: - print( - f"Error: {pyproject_path} is not a valid TOML file." - if sys.version_info >= (3, 11) - else f"Error reading the pyproject.toml file: {e}" - ) + print(f"Error: {poetry_lock_path} not found.") except Exception as e: - print(f"Error reading the pyproject.toml file: {e}") + print(f"Error reading the poetry.lock file: {e}") return "no-version-found" diff --git a/tests/cli/deploy/test_deploy_main.py b/tests/cli/deploy/test_deploy_main.py index 52171459a..a63ea7110 100644 --- a/tests/cli/deploy/test_deploy_main.py +++ b/tests/cli/deploy/test_deploy_main.py @@ -153,21 +153,6 @@ class TestDeployCommand(unittest.TestCase): "Crew 'test_project' removed successfully", fake_out.getvalue() ) - @patch('crewai.cli.deploy.utils.sys.version_info', (3, 10)) - def test_parse_toml_python_310(self): - toml_content = """ - [tool.poetry] - name = "test_project" - version = "0.1.0" - - [tool.poetry.dependencies] - python = "^3.10" - crewai = "^0.1.0" - """ - parsed = parse_toml(toml_content) - self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') - self.assertEqual(parsed['tool']['poetry']['dependencies']['crewai'], '^0.1.0') - @unittest.skipIf(sys.version_info < (3, 11), "Requires Python 3.11+") def test_parse_toml_python_311_plus(self): toml_content = """ @@ -177,11 +162,10 @@ class TestDeployCommand(unittest.TestCase): [tool.poetry.dependencies] python = "^3.11" - crewai = "^0.1.0" + crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } """ parsed = parse_toml(toml_content) self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') - self.assertEqual(parsed['tool']['poetry']['dependencies']['crewai'], '^0.1.0') @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data=""" [tool.poetry] @@ -190,7 +174,7 @@ class TestDeployCommand(unittest.TestCase): [tool.poetry.dependencies] python = "^3.10" - crewai = "^0.1.0" + crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } """) def test_get_project_name_python_310(self, mock_open): from crewai.cli.deploy.utils import get_project_name @@ -205,9 +189,31 @@ class TestDeployCommand(unittest.TestCase): [tool.poetry.dependencies] python = "^3.11" - crewai = "^0.1.0" + crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } """) def test_get_project_name_python_311_plus(self, mock_open): from crewai.cli.deploy.utils import get_project_name project_name = get_project_name() self.assertEqual(project_name, 'test_project') + + @patch('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.deploy.utils import get_crewai_version + version = get_crewai_version() + 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.deploy.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()) From 9a10cc15f4bd0838f4d8d6eb22699bc52317c35c Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 14:37:34 -0300 Subject: [PATCH 10/20] Add python 3.10 support back to CLI +fixes --- src/crewai/cli/deploy/api.py | 3 +++ src/crewai/cli/deploy/main.py | 5 +++++ src/crewai/cli/deploy/utils.py | 12 +++++++----- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/crewai/cli/deploy/api.py b/src/crewai/cli/deploy/api.py index 942fc487e..1037744af 100644 --- a/src/crewai/cli/deploy/api.py +++ b/src/crewai/cli/deploy/api.py @@ -2,6 +2,8 @@ from os import getenv import requests +from crewai.cli.deploy.utils import get_crewai_version + class CrewAPI: """ @@ -13,6 +15,7 @@ class CrewAPI: self.headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", + "User-Agent": f"CrewAI-CLI/{get_crewai_version()}", } self.base_url = getenv( "CREWAI_BASE_URL", "https://dev.crewai.com/crewai_plus/api/v1/crews" diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py index d67e1cdc8..9cb6f6c8c 100644 --- a/src/crewai/cli/deploy/main.py +++ b/src/crewai/cli/deploy/main.py @@ -113,6 +113,11 @@ class DeployCommand: env_vars = fetch_and_json_env_file() remote_repo_url = get_git_remote_url() + if remote_repo_url is None: + console.print("No remote repository URL found.", style="bold red") + console.print("Please ensure your project has a valid remote repository.", style="yellow") + return + self._confirm_input(env_vars, remote_repo_url) payload = self._create_payload(env_vars, remote_repo_url) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index a7534fc48..e55309cfa 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -1,10 +1,13 @@ import sys import re import subprocess -import ast + +from rich.console import Console from ..authentication.utils import TokenManager +console = Console() + if sys.version_info >= (3, 11): import tomllib @@ -53,13 +56,12 @@ def get_git_remote_url() -> str: if matches: return matches[0] # Return the first match (origin URL) else: - print("No origin remote found.") - return "No remote URL found" + console.print("No origin remote found.", style="bold red") except subprocess.CalledProcessError as e: - return f"Error running trying to fetch the Git Repository: {e}" + console.print(f"Error running trying to fetch the Git Repository: {e}", style="bold red") except FileNotFoundError: - return "Git command not found. Make sure Git is installed and in your PATH." + console.print("Git command not found. Make sure Git is installed and in your PATH.", style="bold red") def get_project_name(pyproject_path: str = "pyproject.toml"): From c8c0a89dc64447fb8b630edee72d0eee63ea4ecb Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 15:02:19 -0300 Subject: [PATCH 11/20] Fix type checking + lint --- src/crewai/cli/deploy/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index e55309cfa..89bb89536 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -39,7 +39,7 @@ def parse_toml(content): return simple_toml_parser(content) -def get_git_remote_url() -> str: +def get_git_remote_url() -> str | None: """Get the Git repository's remote URL.""" try: # Run the git remote -v command @@ -63,6 +63,8 @@ def get_git_remote_url() -> str: except FileNotFoundError: console.print("Git command not found. Make sure Git is installed and in your PATH.", style="bold red") + return None + def get_project_name(pyproject_path: str = "pyproject.toml"): """Get the project name from the pyproject.toml file.""" @@ -83,7 +85,7 @@ def get_project_name(pyproject_path: str = "pyproject.toml"): print(f"Error: {pyproject_path} not found.") except KeyError: print(f"Error: {pyproject_path} is not a valid pyproject.toml file.") - except tomllib.TOMLDecodeError if sys.version_info >= (3, 11) else Exception as e: + except tomllib.TOMLDecodeError if sys.version_info >= (3, 11) else Exception as e: # type: ignore print( f"Error: {pyproject_path} is not a valid TOML file." if sys.version_info >= (3, 11) From cda1900b14d19e1e101d4b50d2f04b7d50e0e335 Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 15:17:51 -0300 Subject: [PATCH 12/20] Read as `str` no `bytes` +handle when project_name is None (fails, basically) --- src/crewai/cli/deploy/main.py | 4 ++++ src/crewai/cli/deploy/utils.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py index 9cb6f6c8c..51afdb5da 100644 --- a/src/crewai/cli/deploy/main.py +++ b/src/crewai/cli/deploy/main.py @@ -33,6 +33,10 @@ class DeployCommand: raise SystemExit self.project_name = get_project_name() + if self.project_name is None: + console.print("No project name found. Please ensure your project has a valid pyproject.toml file.", style="bold red") + raise SystemExit + self.client = CrewAPI(api_key=access_token) def _handle_error(self, json_response: Dict[str, Any]) -> None: diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 89bb89536..140e486a8 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -66,11 +66,11 @@ def get_git_remote_url() -> str | None: return None -def get_project_name(pyproject_path: str = "pyproject.toml"): +def get_project_name(pyproject_path: str = "pyproject.toml") -> str | None: """Get the project name from the pyproject.toml file.""" try: # Read the pyproject.toml file - with open(pyproject_path, "rb") as f: + with open(pyproject_path, "r") as f: pyproject_content = parse_toml(f.read()) # Extract the project name From 5f359b14f74560aecf27930e5042adc04ba6399f Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 15:58:47 -0300 Subject: [PATCH 13/20] Fix test --- tests/cli/deploy/test_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/cli/deploy/test_api.py b/tests/cli/deploy/test_api.py index f1a6c573d..616a9ff3d 100644 --- a/tests/cli/deploy/test_api.py +++ b/tests/cli/deploy/test_api.py @@ -17,6 +17,7 @@ class TestCrewAPI(unittest.TestCase): { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", + "User-Agent": "CrewAI-CLI/no-version-found" }, ) From 67f19f79c2828996a09e92514c2821245fcef2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Thu, 29 Aug 2024 23:35:05 -0300 Subject: [PATCH 14/20] removing base_model from telemetry --- src/crewai/telemetry/telemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crewai/telemetry/telemetry.py b/src/crewai/telemetry/telemetry.py index afff13051..5e5acb3d7 100644 --- a/src/crewai/telemetry/telemetry.py +++ b/src/crewai/telemetry/telemetry.py @@ -462,7 +462,7 @@ class Telemetry: pass def _safe_llm_attributes(self, llm): - attributes = ["name", "model_name", "base_url", "model", "top_k", "temperature"] + attributes = ["name", "model_name", "model", "top_k", "temperature"] if llm: safe_attributes = {k: v for k, v in vars(llm).items() if k in attributes} safe_attributes["class"] = llm.__class__.__name__ From d861cb0d747ac8fb7469aa69f4e160198f5472ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Fri, 30 Aug 2024 00:15:06 -0300 Subject: [PATCH 15/20] updating docs --- docs/telemetry/Telemetry.md | 46 ++++++++++++------- src/crewai/telemetry/telemetry.py | 76 +++++++++++++++++++++++++------ 2 files changed, 93 insertions(+), 29 deletions(-) diff --git a/docs/telemetry/Telemetry.md b/docs/telemetry/Telemetry.md index 63d5f5905..9588c3cb0 100644 --- a/docs/telemetry/Telemetry.md +++ b/docs/telemetry/Telemetry.md @@ -5,24 +5,38 @@ description: Understanding the telemetry data collected by CrewAI and how it con ## Telemetry -CrewAI utilizes anonymous telemetry to gather usage statistics with the primary goal of enhancing the library. Our focus is on improving and developing the features, integrations, and tools most utilized by our users. We don't offer a way to disable it now, but we will in the future. +!!! note "Personal Information" + By default, we collect no data that would be considered personal information under GDPR and other privacy regulations. + We do collect Tool's names and Agent's roles, so be advised not to include any personal information in the tool's names or the Agent's roles. + Because no personal information is collected, it's not necessary to worry about data residency. + When `share_crew` is enabled, additional data is collected which may contain personal information if included by the user. Users should exercise caution when enabling this feature to ensure compliance with privacy regulations. -It's pivotal to understand that **NO data is collected** concerning prompts, task descriptions, agents' backstories or goals, usage of tools, API calls, responses, any data processed by the agents, or secrets and environment variables, with the exception of the conditions mentioned. When the `share_crew` feature is enabled, detailed data including task descriptions, agents' backstories or goals, and other specific attributes are collected to provide deeper insights while respecting user privacy. +CrewAI utilizes anonymous telemetry to gather usage statistics with the primary goal of enhancing the library. Our focus is on improving and developing the features, integrations, and tools most utilized by our users. -### Data Collected Includes: -- **Version of CrewAI**: Assessing the adoption rate of our latest version helps us understand user needs and guide our updates. -- **Python Version**: Identifying the Python versions our users operate with assists in prioritizing our support efforts for these versions. -- **General OS Information**: Details like the number of CPUs and the operating system type (macOS, Windows, Linux) enable us to focus our development on the most used operating systems and explore the potential for OS-specific features. -- **Number of Agents and Tasks in a Crew**: Ensures our internal testing mirrors real-world scenarios, helping us guide users towards best practices. -- **Crew Process Utilization**: Understanding how crews are utilized aids in directing our development focus. -- **Memory and Delegation Use by Agents**: Insights into how these features are used help evaluate their effectiveness and future. -- **Task Execution Mode**: Knowing whether tasks are executed in parallel or sequentially influences our emphasis on enhancing parallel execution capabilities. -- **Language Model Utilization**: Supports our goal to improve support for the most popular languages among our users. -- **Roles of Agents within a Crew**: Understanding the various roles agents play aids in crafting better tools, integrations, and examples. -- **Tool Usage**: Identifying which tools are most frequently used allows us to prioritize improvements in those areas. +It's pivotal to understand that by default, **NO personal data is collected** concerning prompts, task descriptions, agents' backstories or goals, usage of tools, API calls, responses, any data processed by the agents, or secrets and environment variables. +When the `share_crew` feature is enabled, detailed data including task descriptions, agents' backstories or goals, and other specific attributes are collected to provide deeper insights. This expanded data collection may include personal information if users have incorporated it into their crews or tasks. Users should carefully consider the content of their crews and tasks before enabling `share_crew`. Users can disable telemetry by setting the environment variable OTEL_SDK_DISABLED to true. + +### Data Explanation: +| Defaulted | Data | Reason and Specifics | +|-----------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------| +| Yes | CrewAI and Python Version | Tracks software versions. Example: CrewAI v1.2.3, Python 3.8.10. No personal data. | +| Yes | Crew Metadata | Includes: randomly generated key and ID, process type (e.g., 'sequential', 'parallel'), boolean flag for memory usage (true/false), count of tasks, count of agents. All non-personal. | +| Yes | Agent Data | Includes: randomly generated key and ID, role name (should not include personal info), boolean settings (verbose, delegation enabled, code execution allowed), max iterations, max RPM, max retry limit, LLM info (see LLM Attributes), list of tool names (should not include personal info). No personal data. | +| Yes | Task Metadata | Includes: randomly generated key and ID, boolean execution settings (async_execution, human_input), associated agent's role and key, list of tool names. All non-personal. | +| Yes | Tool Usage Statistics | Includes: tool name (should not include personal info), number of usage attempts (integer), LLM attributes used. No personal data. | +| Yes | Test Execution Data | Includes: crew's randomly generated key and ID, number of iterations, model name used, quality score (float), execution time (in seconds). All non-personal. | +| Yes | Task Lifecycle Data | Includes: creation and execution start/end times, crew and task identifiers. Stored as spans with timestamps. No personal data. | +| Yes | LLM Attributes | Includes: name, model_name, model, top_k, temperature, and class name of the LLM. All technical, non-personal data. | +| No | Agent's Expanded Data | Includes: goal description, backstory text, i18n prompt file identifier. Users should ensure no personal info is included in text fields. | +| No | Detailed Task Information | Includes: task description, expected output description, context references. Users should ensure no personal info is included in these fields. | +| No | Environment Information | Includes: platform, release, system, version, and CPU count. Example: 'Windows 10', 'x86_64'. No personal data. | +| No | Crew and Task Inputs and Outputs | Includes: input parameters and output results as non-identifiable data. Users should ensure no personal info is included. | +| No | Comprehensive Crew Execution Data | Includes: detailed logs of crew operations, all agents and tasks data, final output. All non-personal and technical in nature. | + +Note: "No" in the "Defaulted" column indicates that this data is only collected when `share_crew` is set to `true`. ### Opt-In Further Telemetry Sharing -Users can choose to share their complete telemetry data by enabling the `share_crew` attribute to `True` in their crew configurations. Enabling `share_crew` results in the collection of detailed crew and task execution data, including `goal`, `backstory`, `context`, and `output` of tasks. This enables a deeper insight into usage patterns while respecting the user's choice to share. +Users can choose to share their complete telemetry data by enabling the `share_crew` attribute to `True` in their crew configurations. Enabling `share_crew` results in the collection of detailed crew and task execution data, including `goal`, `backstory`, `context`, and `output` of tasks. This enables a deeper insight into usage patterns. -### Updates and Revisions -We are committed to maintaining the accuracy and transparency of our documentation. Regular reviews and updates are performed to ensure our documentation accurately reflects the latest developments of our codebase and telemetry practices. Users are encouraged to review this section for the most current information on our data collection practices and how they contribute to the improvement of CrewAI. \ No newline at end of file +!!! warning "Potential Personal Information" + If you enable `share_crew`, the collected data may include personal information if it has been incorporated into crew configurations, task descriptions, or outputs. Users should carefully review their data and ensure compliance with GDPR and other applicable privacy regulations before enabling this feature. \ No newline at end of file diff --git a/src/crewai/telemetry/telemetry.py b/src/crewai/telemetry/telemetry.py index 5e5acb3d7..88cb560d9 100644 --- a/src/crewai/telemetry/telemetry.py +++ b/src/crewai/telemetry/telemetry.py @@ -28,18 +28,6 @@ class Telemetry: agents backstories or goals nor responses or any data that is being processed by the agents, nor any secrets and env vars. - Data collected includes: - - Version of crewAI - - Version of Python - - General OS (e.g. number of CPUs, macOS/Windows/Linux) - - Number of agents and tasks in a crew - - Crew Process being used - - If Agents are using memory or allowing delegation - - If Tasks are being executed in parallel or sequentially - - Language model being used - - Roles of agents in a crew - - Tools names available - Users can opt-in to sharing more complete data using the `share_crew` attribute in the Crew class. """ @@ -114,10 +102,17 @@ class Telemetry: "max_iter": agent.max_iter, "max_rpm": agent.max_rpm, "i18n": agent.i18n.prompt_file, + "function_calling_llm": json.dumps( + self._safe_llm_attributes( + agent.function_calling_llm + ) + ), "llm": json.dumps( self._safe_llm_attributes(agent.llm) ), "delegation_enabled?": agent.allow_delegation, + "allow_code_execution?": agent.allow_code_execution, + "max_retry_limit": agent.max_retry_limit, "tools_names": [ tool.name.casefold() for tool in agent.tools or [] @@ -165,7 +160,62 @@ class Telemetry: self._add_attribute( span, "crew_inputs", json.dumps(inputs) if inputs else None ) - + else: + self._add_attribute( + span, + "crew_agents", + json.dumps( + [ + { + "key": agent.key, + "id": str(agent.id), + "role": agent.role, + "verbose?": agent.verbose, + "max_iter": agent.max_iter, + "max_rpm": agent.max_rpm, + "function_calling_llm": json.dumps( + self._safe_llm_attributes( + agent.function_calling_llm + ) + ), + "llm": json.dumps( + self._safe_llm_attributes(agent.llm) + ), + "delegation_enabled?": agent.allow_delegation, + "allow_code_execution?": agent.allow_code_execution, + "max_retry_limit": agent.max_retry_limit, + "tools_names": [ + tool.name.casefold() + for tool in agent.tools or [] + ], + } + for agent in crew.agents + ] + ), + ) + self._add_attribute( + span, + "crew_tasks", + json.dumps( + [ + { + "key": task.key, + "id": str(task.id), + "async_execution?": task.async_execution, + "human_input?": task.human_input, + "agent_role": task.agent.role + if task.agent + else "None", + "agent_key": task.agent.key if task.agent else None, + "tools_names": [ + tool.name.casefold() + for tool in task.tools or [] + ], + } + for task in crew.tasks + ] + ), + ) span.set_status(Status(StatusCode.OK)) span.end() except Exception: From d4791bef28440eeb74145adaa0709a20497b52f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Fri, 30 Aug 2024 00:32:18 -0300 Subject: [PATCH 16/20] updating deployment cli with --- docs/telemetry/Telemetry.md | 1 + src/crewai/cli/deploy/main.py | 14 ++++++++ src/crewai/telemetry/telemetry.py | 59 ++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/telemetry/Telemetry.md b/docs/telemetry/Telemetry.md index 9588c3cb0..51b1ad8e9 100644 --- a/docs/telemetry/Telemetry.md +++ b/docs/telemetry/Telemetry.md @@ -27,6 +27,7 @@ When the `share_crew` feature is enabled, detailed data including task descripti | Yes | Test Execution Data | Includes: crew's randomly generated key and ID, number of iterations, model name used, quality score (float), execution time (in seconds). All non-personal. | | Yes | Task Lifecycle Data | Includes: creation and execution start/end times, crew and task identifiers. Stored as spans with timestamps. No personal data. | | Yes | LLM Attributes | Includes: name, model_name, model, top_k, temperature, and class name of the LLM. All technical, non-personal data. | +| Yes | Crew Deployment attempt using crewAI CLI | Includes: The fact a deploy is being made and crew id, and if it's trying to pull logs, no other data. | | No | Agent's Expanded Data | Includes: goal description, backstory text, i18n prompt file identifier. Users should ensure no personal info is included in text fields. | | No | Detailed Task Information | Includes: task description, expected output description, context references. Users should ensure no personal info is included in these fields. | | No | Environment Information | Includes: platform, release, system, version, and CPU count. Example: 'Windows 10', 'x86_64'. No personal data. | diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py index d67e1cdc8..67b69a0ee 100644 --- a/src/crewai/cli/deploy/main.py +++ b/src/crewai/cli/deploy/main.py @@ -2,6 +2,7 @@ from typing import Any, Dict, List, Optional from rich.console import Console +from crewai.telemetry import Telemetry from .api import CrewAPI from .utils import ( fetch_and_json_env_file, @@ -23,8 +24,13 @@ class DeployCommand: Initialize the DeployCommand with project name and API client. """ try: + self._telemetry = Telemetry() + self._telemetry.set_tracer() access_token = get_auth_token() except Exception: + self._deploy_signup_error_span = self._telemetry.deploy_signup_error_span( + self + ) console.print( "Please sign up/login to CrewAI+ before using the CLI.", style="bold red", @@ -90,6 +96,7 @@ class DeployCommand: Args: uuid (Optional[str]): The UUID of the crew to deploy. """ + self._start_deployment_span = self._telemetry.start_deployment_span(self, uuid) console.print("Starting deployment...", style="bold blue") if uuid: response = self.client.deploy_by_uuid(uuid) @@ -109,6 +116,9 @@ class DeployCommand: """ Create a new crew deployment. """ + self._create_crew_deployment_span = self._telemetry.create_crew_deployment_span( + self + ) console.print("Creating deployment...", style="bold blue") env_vars = fetch_and_json_env_file() remote_repo_url = get_git_remote_url() @@ -247,6 +257,9 @@ class DeployCommand: uuid (Optional[str]): The UUID of the crew to get logs for. log_type (str): The type of logs to retrieve (default: "deployment"). """ + self._get_crew_logs_span = self._telemetry.get_crew_logs_span( + self, uuid, log_type + ) console.print(f"Fetching {log_type} logs...", style="bold blue") if uuid: @@ -269,6 +282,7 @@ class DeployCommand: Args: uuid (Optional[str]): The UUID of the crew to remove. """ + self._remove_crew_span = self._telemetry.remove_crew_span(self, uuid) console.print("Removing deployment...", style="bold blue") if uuid: diff --git a/src/crewai/telemetry/telemetry.py b/src/crewai/telemetry/telemetry.py index 88cb560d9..21d629ad3 100644 --- a/src/crewai/telemetry/telemetry.py +++ b/src/crewai/telemetry/telemetry.py @@ -4,7 +4,7 @@ import asyncio import json import os import platform -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional import pkg_resources from opentelemetry import trace @@ -399,6 +399,63 @@ class Telemetry: except Exception: pass + def deploy_signup_error_span(self): + if self.ready: + try: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Deploy Signup Error") + span.set_status(Status(StatusCode.OK)) + span.end() + except Exception: + pass + + def start_deployment_span(self, uuid: Optional[str] = None): + if self.ready: + try: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Start Deployment") + if uuid: + self._add_attribute(span, "uuid", uuid) + span.set_status(Status(StatusCode.OK)) + span.end() + except Exception: + pass + + def create_crew_deployment_span(self): + if self.ready: + try: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Create Crew Deployment") + span.set_status(Status(StatusCode.OK)) + span.end() + except Exception: + pass + + def get_crew_logs_span(self, uuid: Optional[str], log_type: str = "deployment"): + if self.ready: + try: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Get Crew Logs") + self._add_attribute(span, "log_type", log_type) + if uuid: + self._add_attribute(span, "uuid", uuid) + span.set_status(Status(StatusCode.OK)) + span.end() + except Exception: + pass + + def remove_crew_span(self, uuid: Optional[str] = None): + if self.ready: + try: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Remove Crew") + if uuid: + self._add_attribute(span, "uuid", uuid) + span.set_status(Status(StatusCode.OK)) + span.end() + except Exception: + pass + def crew_execution_span(self, crew: Crew, inputs: dict[str, Any] | None): """Records the complete execution of a crew. This is only collected if the user has opted-in to share the crew. From 79b5248b83fa3284f152713e596db4448ac35b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Fri, 30 Aug 2024 00:33:51 -0300 Subject: [PATCH 17/20] preparing new version --- pyproject.toml | 2 +- src/crewai/cli/templates/crew/pyproject.toml | 2 +- src/crewai/cli/templates/pipeline/pyproject.toml | 2 +- src/crewai/cli/templates/pipeline_router/pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6cb50c771..9efd732cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "crewai" -version = "0.51.1" +version = "0.54.0" 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 "] readme = "README.md" diff --git a/src/crewai/cli/templates/crew/pyproject.toml b/src/crewai/cli/templates/crew/pyproject.toml index 1783b351e..14487974e 100644 --- a/src/crewai/cli/templates/crew/pyproject.toml +++ b/src/crewai/cli/templates/crew/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = ">=3.10,<=3.13" -crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } +crewai = { extras = ["tools"], version = ">=0.54.0,<1.0.0" } [tool.poetry.scripts] diff --git a/src/crewai/cli/templates/pipeline/pyproject.toml b/src/crewai/cli/templates/pipeline/pyproject.toml index b72cebd6a..3512c0842 100644 --- a/src/crewai/cli/templates/pipeline/pyproject.toml +++ b/src/crewai/cli/templates/pipeline/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = ">=3.10,<=3.13" -crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } +crewai = { extras = ["tools"], version = ">=0.54.0,<1.0.0" } asyncio = "*" [tool.poetry.scripts] diff --git a/src/crewai/cli/templates/pipeline_router/pyproject.toml b/src/crewai/cli/templates/pipeline_router/pyproject.toml index ab00bd355..a43f5d31a 100644 --- a/src/crewai/cli/templates/pipeline_router/pyproject.toml +++ b/src/crewai/cli/templates/pipeline_router/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = ">=3.10,<=3.13" -crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } +crewai = { extras = ["tools"], version = ">=0.54.0,<1.0.0" } [tool.poetry.scripts] From f2208f5f8ee6ed23ac0419d4255b751b57fa0d00 Mon Sep 17 00:00:00 2001 From: Astha Puri Date: Fri, 30 Aug 2024 06:54:34 -0400 Subject: [PATCH 18/20] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index adb4b83ac..65cf8601d 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ os.environ["SERPER_API_KEY"] = "Your Key" # serper.dev API key # You can pass an optional llm attribute specifying what model you wanna use. # It can be a local model through Ollama / LM Studio or a remote # model like OpenAI, Mistral, Antrophic or others (https://docs.crewai.com/how-to/LLM-Connections/) +# If you don't specify a model, the default is OpenAI gpt-4 # # import os # os.environ['OPENAI_MODEL_NAME'] = 'gpt-3.5-turbo' From 5b3f7be1c4e21cbb6f69c73222f9832ec2815907 Mon Sep 17 00:00:00 2001 From: Astha Puri Date: Fri, 30 Aug 2024 06:55:31 -0400 Subject: [PATCH 19/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65cf8601d..fb91fd19a 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ os.environ["SERPER_API_KEY"] = "Your Key" # serper.dev API key # You can pass an optional llm attribute specifying what model you wanna use. # It can be a local model through Ollama / LM Studio or a remote # model like OpenAI, Mistral, Antrophic or others (https://docs.crewai.com/how-to/LLM-Connections/) -# If you don't specify a model, the default is OpenAI gpt-4 +# If you don't specify a model, the default is OpenAI gpt-4o # # import os # os.environ['OPENAI_MODEL_NAME'] = 'gpt-3.5-turbo' From 0b9e753c2f3b90e5a44e202a4b8604e67d8c4879 Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Fri, 30 Aug 2024 11:52:53 -0300 Subject: [PATCH 20/20] Add comment to warn about dro simple_toml_parser --- src/crewai/cli/deploy/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 140e486a8..7579785df 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -13,6 +13,7 @@ if sys.version_info >= (3, 11): import tomllib +# Drop the simple_toml_parser when we move to python3.11 def simple_toml_parser(content): result = {} current_section = result