diff --git a/src/crewai/cli/authentication/__init__.py b/src/crewai/cli/authentication/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/crewai/cli/deploy/constants.py b/src/crewai/cli/authentication/constants.py similarity index 100% rename from src/crewai/cli/deploy/constants.py rename to src/crewai/cli/authentication/constants.py diff --git a/src/crewai/cli/authentication/main.py b/src/crewai/cli/authentication/main.py new file mode 100644 index 000000000..e18b9bbcf --- /dev/null +++ b/src/crewai/cli/authentication/main.py @@ -0,0 +1,78 @@ +import time +import webbrowser +from typing import Any, Dict, Optional + +import requests +from rich.console import Console + +from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN +from .utils import validate_token + +console = Console() + + +class Authentication: + DEVICE_CODE_URL = f"https://{AUTH0_DOMAIN}/oauth/device/code" + TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token" + + def signup(self) -> Optional[Dict[str, Any]]: + """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 profile email", + "audience": "https://dev-jzsr0j8zs0atl5ha.us.auth0.com/api/v2/", + } + 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] + ) -> Optional[Dict[str, Any]]: + """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"]) + # current_user = jwt.decode( + # token_data["id_token"], + # algorithms=ALGORITHMS, + # options={"verify_signature": False}, + # ) + console.print("\nWelcome to CrewAI+ !!", style="green") + return token_data + + 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..f4d3420ce --- /dev/null +++ b/src/crewai/cli/authentication/utils.py @@ -0,0 +1,21 @@ +from auth0.authentication.token_verifier import ( + AsymmetricSignatureVerifier, + TokenVerifier, +) + +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) diff --git a/src/crewai/cli/cli.py b/src/crewai/cli/cli.py index 77e29ed0b..dec978b7a 100644 --- a/src/crewai/cli/cli.py +++ b/src/crewai/cli/cli.py @@ -9,6 +9,7 @@ from crewai.memory.storage.kickoff_task_outputs_storage import ( KickoffTaskOutputsSQLiteStorage, ) +from .authentication.main import Authentication from .deploy.main import DeployCommand from .evaluate_crew import evaluate_crew from .replay_from_task import replay_task_command @@ -177,23 +178,22 @@ def run(): run_crew() -# Deploy command group +@crewai.command() +def signup(): + """Sign Up/Login to CrewAI+.""" + Authentication().signup() + + +# DEPLOY CREWAI+ COMMANDS @crewai.group() def deploy(): """Deploy the Crew CLI group.""" pass -@deploy.command(name="up") -@click.option("-u", "--uuid", type=str, help="Crew UUID parameter") -def deploy_up(uuid: Optional[str]): - """Deploy the crew.""" - deploy_cmd.deploy(uuid=uuid) - - @deploy.command(name="create") def deploy_create(): - """Create a deployment.""" + """Create a Crew deployment.""" deploy_cmd.create_crew() @@ -203,6 +203,13 @@ def deploy_list(): 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.deploy(uuid=uuid) + + @deploy.command(name="status") @click.option("-u", "--uuid", type=str, help="Crew UUID parameter") def deply_status(uuid: Optional[str]): @@ -224,11 +231,5 @@ def deploy_remove(uuid: Optional[str]): deploy_cmd.remove_crew(uuid=uuid) -@deploy.command(name="signup") -def signup(): - """Sign up for a deployment.""" - deploy_cmd.signup() - - if __name__ == "__main__": crewai() diff --git a/src/crewai/cli/deploy/api.py b/src/crewai/cli/deploy/api.py index 2f80c7e18..ccc0ce61b 100644 --- a/src/crewai/cli/deploy/api.py +++ b/src/crewai/cli/deploy/api.py @@ -8,8 +8,9 @@ class CrewAPI: CrewAPI class to interact with the crewAI+ API. """ - CREW_BASE_URL = getenv("BASE_URL", "http://localhost:3000/crewai_plus/api/v1/crews") - MAIN_BASE_URL = getenv("MAIN_BASE_URL", "http://localhost:3000/crewai_plus/api/v1") + CREWAI_BASE_URL = getenv( + "CREWAI_BASE_URL", "http://localhost:3000/crewai_plus/api/v1/crews" + ) def __init__(self, api_key: str) -> None: self.api_key = api_key @@ -18,24 +19,25 @@ class CrewAPI: "Content-Type": "application/json", } - def _make_request( - self, method: str, endpoint: str, base_url: str = CREW_BASE_URL, **kwargs - ) -> requests.Response: - url = f"{base_url}/{endpoint}" + def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response: + url = f"{self.CREWAI_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: @@ -46,17 +48,17 @@ class CrewAPI: ) -> 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) - - def signup(self) -> requests.Response: - return self._make_request("GET", "signup_link", base_url=self.MAIN_BASE_URL) diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py index 645492b3e..8188e2912 100644 --- a/src/crewai/cli/deploy/main.py +++ b/src/crewai/cli/deploy/main.py @@ -1,50 +1,85 @@ -import time -import webbrowser -from os import getenv -from typing import Optional +from typing import Any, Dict, List, Optional -import requests from rich.console import Console from .api import CrewAPI -from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN from .utils import ( fetch_and_json_env_file, get_auth_token, get_git_remote_url, get_project_name, - validate_token, ) console = Console() class DeployCommand: - BASE_URL = getenv("BASE_URL", "http://localhost:3000/crewai_plus/api") + """ + A class to handle deployment-related operations for CrewAI projects. + """ def __init__(self): + """ + Initialize the DeployCommand with project name and API client. + """ self.project_name = get_project_name() self.client = CrewAPI(api_key=get_auth_token()) - def _handle_error(self, json_response: dict) -> None: - error = json_response.get("error") - message = json_response.get("message") - console.print( - f"Error: {error}", - style="bold red", - ) - console.print( - f"Message: {message}", - style="bold red", - ) + 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.", + "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) @@ -56,29 +91,54 @@ class DeployCommand: json_response = response.json() if response.status_code == 200: - 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']}\"") - + 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(remote_repo_url, env_vars) + + 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" ) - payload = { + + def _create_payload( + self, remote_repo_url: str, env_vars: Dict[str, 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, @@ -86,39 +146,62 @@ class DeployCommand: } } - response = self.client.create_crew(payload) - if response.status_code == 201: - json_response = response.json() - 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 up") - console.print(" or") - console.print(f"crewai deploy --uuid {json_response['uuid']}") - else: - self._handle_error(response.json()) + 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: - for crew_data in json_response: - console.print( - f"- {crew_data['name']} ({crew_data['uuid']}) [blue]{crew_data['status']}[/blue]" - ) + 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( - "You don't have any crews yet. Let's create one!", style="yellow" + f"- {crew_data['name']} ({crew_data['uuid']}) [blue]{crew_data['status']}[/blue]" ) - console.print(" [green]crewai create --name [name][/green]") + + 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) @@ -130,16 +213,29 @@ class DeployCommand: json_response = response.json() if response.status_code == 200: - console.print(f"Name:\t {json_response['name']}") - console.print(f"Status:\t {json_response['status']}") - + self._display_crew_status(json_response) else: self._handle_error(json_response) - def get_crew_logs( - self, uuid: Optional[str], log_type: str = "dExacployment" - ) -> None: - console.print(f"Getting {log_type} logs...", style="bold blue") + 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) @@ -150,15 +246,17 @@ class DeployCommand: return if response.status_code == 200: - log_messages = response.json() - for log_message in log_messages: - console.print( - f"{log_message['timestamp']} - {log_message['level']}: {log_message['message']}" - ) + self._display_logs(response.json()) else: - console.print(response.text, style="bold red") + 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: @@ -177,59 +275,3 @@ class DeployCommand: console.print( f"Failed to remove crew '{self.project_name}'", style="bold red" ) - - def signup(self) -> None: - console.print("Signing Up", style="bold blue") - - device_code_payload = { - "client_id": AUTH0_CLIENT_ID, - "scope": "openid profile email", - "audience": "https://dev-jzsr0j8zs0atl5ha.us.auth0.com/api/v2/", - } - device_code_response = requests.post( - f"https://{AUTH0_DOMAIN}/oauth/device/code", - data=device_code_payload, - ) - - if device_code_response.status_code != 200: - console.print("Error generating the device code") - raise - - device_code_data = device_code_response.json() - 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"]) - - token_payload = { - "grant_type": "urn:ietf:params:oauth:grant-type:device_code", - "device_code": device_code_data["device_code"], - "client_id": AUTH0_CLIENT_ID, - } - - authenticated = False - while not authenticated: - token_response = requests.post( - f"https://{AUTH0_DOMAIN}/oauth/token", data=token_payload - ) - - token_data = token_response.json() - if token_response.status_code == 200: - validate_token(token_data["id_token"]) - - # current_user = jwt.decode( - # token_data["id_token"], - # algorithms=ALGORITHMS, - # options={"verify_signature": False}, - # ) - - authenticated = True - console.print("\nWelcome to CrewAI+ !!", style="green") - - elif token_data["error"] not in ("authorization_pending", "slow_down"): - console.print(token_data["error_description"]) - raise - else: - time.sleep(device_code_data["interval"]) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index f66fe3130..003ac0e85 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -2,12 +2,6 @@ import re import subprocess import tomllib -from auth0.authentication.token_verifier import ( - AsymmetricSignatureVerifier, - TokenVerifier, -) - -from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN def get_git_remote_url(): @@ -87,20 +81,5 @@ def fetch_and_json_env_file(env_file_path: str = ".env") -> dict: return {} -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) - - def get_auth_token(): - return "" + return ""