feat: update code and decouple auth

This commit is contained in:
Eduardo Chiarotti
2024-08-21 16:11:16 -03:00
parent 3b0286f592
commit 414a9ba07e
8 changed files with 290 additions and 167 deletions

View File

@@ -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"
)

View File

@@ -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)

View File

@@ -9,6 +9,7 @@ from crewai.memory.storage.kickoff_task_outputs_storage import (
KickoffTaskOutputsSQLiteStorage, KickoffTaskOutputsSQLiteStorage,
) )
from .authentication.main import Authentication
from .deploy.main import DeployCommand from .deploy.main import DeployCommand
from .evaluate_crew import evaluate_crew from .evaluate_crew import evaluate_crew
from .replay_from_task import replay_task_command from .replay_from_task import replay_task_command
@@ -177,23 +178,22 @@ def run():
run_crew() run_crew()
# Deploy command group @crewai.command()
def signup():
"""Sign Up/Login to CrewAI+."""
Authentication().signup()
# DEPLOY CREWAI+ COMMANDS
@crewai.group() @crewai.group()
def deploy(): def deploy():
"""Deploy the Crew CLI group.""" """Deploy the Crew CLI group."""
pass 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") @deploy.command(name="create")
def deploy_create(): def deploy_create():
"""Create a deployment.""" """Create a Crew deployment."""
deploy_cmd.create_crew() deploy_cmd.create_crew()
@@ -203,6 +203,13 @@ def deploy_list():
deploy_cmd.list_crews() 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") @deploy.command(name="status")
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter") @click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
def deply_status(uuid: Optional[str]): def deply_status(uuid: Optional[str]):
@@ -224,11 +231,5 @@ def deploy_remove(uuid: Optional[str]):
deploy_cmd.remove_crew(uuid=uuid) deploy_cmd.remove_crew(uuid=uuid)
@deploy.command(name="signup")
def signup():
"""Sign up for a deployment."""
deploy_cmd.signup()
if __name__ == "__main__": if __name__ == "__main__":
crewai() crewai()

View File

@@ -8,8 +8,9 @@ class CrewAPI:
CrewAPI class to interact with the crewAI+ API. CrewAPI class to interact with the crewAI+ API.
""" """
CREW_BASE_URL = getenv("BASE_URL", "http://localhost:3000/crewai_plus/api/v1/crews") CREWAI_BASE_URL = getenv(
MAIN_BASE_URL = getenv("MAIN_BASE_URL", "http://localhost:3000/crewai_plus/api/v1") "CREWAI_BASE_URL", "http://localhost:3000/crewai_plus/api/v1/crews"
)
def __init__(self, api_key: str) -> None: def __init__(self, api_key: str) -> None:
self.api_key = api_key self.api_key = api_key
@@ -18,24 +19,25 @@ class CrewAPI:
"Content-Type": "application/json", "Content-Type": "application/json",
} }
def _make_request( def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
self, method: str, endpoint: str, base_url: str = CREW_BASE_URL, **kwargs url = f"{self.CREWAI_BASE_URL}/{endpoint}"
) -> requests.Response:
url = f"{base_url}/{endpoint}"
return requests.request(method, url, headers=self.headers, **kwargs) return requests.request(method, url, headers=self.headers, **kwargs)
# Deploy
def deploy_by_name(self, project_name: str) -> requests.Response: def deploy_by_name(self, project_name: str) -> requests.Response:
return self._make_request("POST", f"by-name/{project_name}/deploy") return self._make_request("POST", f"by-name/{project_name}/deploy")
def deploy_by_uuid(self, uuid: str) -> requests.Response: def deploy_by_uuid(self, uuid: str) -> requests.Response:
return self._make_request("POST", f"{uuid}/deploy") return self._make_request("POST", f"{uuid}/deploy")
# Status
def status_by_name(self, project_name: str) -> requests.Response: def status_by_name(self, project_name: str) -> requests.Response:
return self._make_request("GET", f"by-name/{project_name}/status") return self._make_request("GET", f"by-name/{project_name}/status")
def status_by_uuid(self, uuid: str) -> requests.Response: def status_by_uuid(self, uuid: str) -> requests.Response:
return self._make_request("GET", f"{uuid}/status") return self._make_request("GET", f"{uuid}/status")
# Logs
def logs_by_name( def logs_by_name(
self, project_name: str, log_type: str = "deployment" self, project_name: str, log_type: str = "deployment"
) -> requests.Response: ) -> requests.Response:
@@ -46,17 +48,17 @@ class CrewAPI:
) -> requests.Response: ) -> requests.Response:
return self._make_request("GET", f"{uuid}/logs/{log_type}") return self._make_request("GET", f"{uuid}/logs/{log_type}")
# Delete
def delete_by_name(self, project_name: str) -> requests.Response: def delete_by_name(self, project_name: str) -> requests.Response:
return self._make_request("DELETE", f"by-name/{project_name}") return self._make_request("DELETE", f"by-name/{project_name}")
def delete_by_uuid(self, uuid: str) -> requests.Response: def delete_by_uuid(self, uuid: str) -> requests.Response:
return self._make_request("DELETE", f"{uuid}") return self._make_request("DELETE", f"{uuid}")
# List
def list_crews(self) -> requests.Response: def list_crews(self) -> requests.Response:
return self._make_request("GET", "") return self._make_request("GET", "")
# Create
def create_crew(self, payload) -> requests.Response: def create_crew(self, payload) -> requests.Response:
return self._make_request("POST", "", json=payload) 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)

View File

@@ -1,50 +1,85 @@
import time from typing import Any, Dict, List, Optional
import webbrowser
from os import getenv
from typing import Optional
import requests
from rich.console import Console from rich.console import Console
from .api import CrewAPI from .api import CrewAPI
from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN
from .utils import ( from .utils import (
fetch_and_json_env_file, fetch_and_json_env_file,
get_auth_token, get_auth_token,
get_git_remote_url, get_git_remote_url,
get_project_name, get_project_name,
validate_token,
) )
console = Console() console = Console()
class DeployCommand: 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): def __init__(self):
"""
Initialize the DeployCommand with project name and API client.
"""
self.project_name = get_project_name() self.project_name = get_project_name()
self.client = CrewAPI(api_key=get_auth_token()) self.client = CrewAPI(api_key=get_auth_token())
def _handle_error(self, json_response: dict) -> None: def _handle_error(self, json_response: Dict[str, Any]) -> None:
error = json_response.get("error") """
message = json_response.get("message") Handle and display error messages from API responses.
console.print(
f"Error: {error}", Args:
style="bold red", json_response (Dict[str, Any]): The JSON response containing error information.
) """
console.print( error = json_response.get("error", "Unknown error")
f"Message: {message}", message = json_response.get("message", "No message provided")
style="bold red", console.print(f"Error: {error}", style="bold red")
) console.print(f"Message: {message}", style="bold red")
def _standard_no_param_error_message(self) -> None: def _standard_no_param_error_message(self) -> None:
"""
Display a standard error message when no UUID or project name is available.
"""
console.print( 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", 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: 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") console.print("Starting deployment...", style="bold blue")
if uuid: if uuid:
response = self.client.deploy_by_uuid(uuid) response = self.client.deploy_by_uuid(uuid)
@@ -56,29 +91,54 @@ class DeployCommand:
json_response = response.json() json_response = response.json()
if response.status_code == 200: if response.status_code == 200:
console.print("Deploying the crew...\n", style="bold blue") self._display_deployment_info(json_response)
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']}\"")
else: else:
self._handle_error(json_response) self._handle_error(json_response)
def create_crew(self) -> None: def create_crew(self) -> None:
"""
Create a new crew deployment.
"""
console.print("Creating deployment...", style="bold blue") console.print("Creating deployment...", style="bold blue")
env_vars = fetch_and_json_env_file() env_vars = fetch_and_json_env_file()
remote_repo_url = get_git_remote_url() 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 Env vars: {env_vars}")
input( input(
f"Press Enter to continue with the following remote repository: {remote_repo_url}\n" 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": { "deploy": {
"name": self.project_name, "name": self.project_name,
"repo_clone_url": remote_repo_url, "repo_clone_url": remote_repo_url,
@@ -86,39 +146,62 @@ class DeployCommand:
} }
} }
response = self.client.create_crew(payload) def _display_creation_success(self, json_response: Dict[str, Any]) -> None:
if response.status_code == 201: """
json_response = response.json() Display success message after crew creation.
console.print("Deployment created successfully!\n", style="bold green")
console.print( Args:
f"Name: {self.project_name} ({json_response['uuid']})", json_response (Dict[str, Any]): The response containing crew information.
style="bold green", """
) console.print("Deployment created successfully!\n", style="bold green")
console.print(f"Status: {json_response['status']}", style="bold green") console.print(
console.print("\nTo (re)deploy the crew, run:") f"Name: {self.project_name} ({json_response['uuid']})", style="bold green"
console.print("crewai deploy up") )
console.print(" or") console.print(f"Status: {json_response['status']}", style="bold green")
console.print(f"crewai deploy --uuid {json_response['uuid']}") console.print("\nTo (re)deploy the crew, run:")
else: console.print("crewai deploy push")
self._handle_error(response.json()) console.print(" or")
console.print(f"crewai deploy push --uuid {json_response['uuid']}")
def list_crews(self) -> None: def list_crews(self) -> None:
"""
List all available crews.
"""
console.print("Listing all Crews\n", style="bold blue") console.print("Listing all Crews\n", style="bold blue")
response = self.client.list_crews() response = self.client.list_crews()
json_response = response.json() json_response = response.json()
if response.status_code == 200: if response.status_code == 200:
for crew_data in json_response: self._display_crews(json_response)
console.print(
f"- {crew_data['name']} ({crew_data['uuid']}) [blue]{crew_data['status']}[/blue]"
)
else: 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( 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 <crew_name>", style="green")
def get_crew_status(self, uuid: Optional[str] = None) -> None: 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") console.print("Fetching deployment status...", style="bold blue")
if uuid: if uuid:
response = self.client.status_by_uuid(uuid) response = self.client.status_by_uuid(uuid)
@@ -130,16 +213,29 @@ class DeployCommand:
json_response = response.json() json_response = response.json()
if response.status_code == 200: if response.status_code == 200:
console.print(f"Name:\t {json_response['name']}") self._display_crew_status(json_response)
console.print(f"Status:\t {json_response['status']}")
else: else:
self._handle_error(json_response) self._handle_error(json_response)
def get_crew_logs( def _display_crew_status(self, status_data: Dict[str, str]) -> None:
self, uuid: Optional[str], log_type: str = "dExacployment" """
) -> None: Display the status of a crew.
console.print(f"Getting {log_type} logs...", style="bold blue")
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: if uuid:
response = self.client.logs_by_uuid(uuid, log_type) response = self.client.logs_by_uuid(uuid, log_type)
@@ -150,15 +246,17 @@ class DeployCommand:
return return
if response.status_code == 200: if response.status_code == 200:
log_messages = response.json() self._display_logs(response.json())
for log_message in log_messages:
console.print(
f"{log_message['timestamp']} - {log_message['level']}: {log_message['message']}"
)
else: else:
console.print(response.text, style="bold red") self._handle_error(response.json())
def remove_crew(self, uuid: Optional[str]) -> None: 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") console.print("Removing deployment...", style="bold blue")
if uuid: if uuid:
@@ -177,59 +275,3 @@ class DeployCommand:
console.print( console.print(
f"Failed to remove crew '{self.project_name}'", style="bold red" 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"])

View File

@@ -2,12 +2,6 @@ import re
import subprocess import subprocess
import tomllib import tomllib
from auth0.authentication.token_verifier import (
AsymmetricSignatureVerifier,
TokenVerifier,
)
from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN
def get_git_remote_url(): def get_git_remote_url():
@@ -87,20 +81,5 @@ def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
return {} 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(): def get_auth_token():
return "" return "<token>"