Fix CI issues and implement PR feedback improvements

Co-Authored-By: Joe Moura <joao@crewai.com>
This commit is contained in:
Devin AI
2025-03-21 19:44:28 +00:00
parent 9315610cc6
commit 6708d47d39
10 changed files with 198 additions and 44 deletions

View File

@@ -18,6 +18,7 @@ Create a file named `deployment.yaml`:
# CrewAI Deployment Configuration # CrewAI Deployment Configuration
name: my-crewai-app name: my-crewai-app
port: 8000 port: 8000
host: 127.0.0.1 # Default to localhost for security
# Crews configuration # Crews configuration
crews: crews:

View File

@@ -1,6 +1,7 @@
# CrewAI Deployment Configuration # CrewAI Deployment Configuration
name: analysis-app name: analysis-app
port: 8000 port: 8000
host: 127.0.0.1 # Default to localhost for security
# Flows configuration # Flows configuration
flows: flows:

View File

@@ -21,6 +21,12 @@ def create_deployment(config_path):
deployment = Deployment(config_path) deployment = Deployment(config_path)
deployment.prepare() deployment.prepare()
console.print(f"Deployment prepared at {deployment.deployment_dir}", style="bold green") console.print(f"Deployment prepared at {deployment.deployment_dir}", style="bold green")
console.print(f"Configuration:", style="bold blue")
console.print(f" Name: {deployment.config.name}")
console.print(f" Port: {deployment.config.port}")
console.print(f" Host: {deployment.config.host}")
console.print(f" Crews: {[c.name for c in deployment.config.crews]}")
console.print(f" Flows: {[f.name for f in deployment.config.flows]}")
except Exception as e: except Exception as e:
console.print(f"Error creating deployment: {e}", style="bold red") console.print(f"Error creating deployment: {e}", style="bold red")
@@ -57,7 +63,7 @@ def start_deployment(deployment_name):
deployment = Deployment(config_path) deployment = Deployment(config_path)
deployment.start() deployment.start()
console.print(f"Deployment {deployment_name} started", style="bold green") console.print(f"Deployment {deployment_name} started", style="bold green")
console.print(f"API server running at http://localhost:{deployment.config.port}") console.print(f"API server running at http://{deployment.config.host}:{deployment.config.port}")
except Exception as e: except Exception as e:
console.print(f"Error starting deployment: {e}", style="bold red") console.print(f"Error starting deployment: {e}", style="bold red")

View File

@@ -1,41 +1,93 @@
from pathlib import Path import os
from typing import Any, Dict, List, Optional
import yaml import yaml
from pathlib import Path
from typing import Dict, List, Optional, Union, Any
from pydantic import BaseModel, Field, validator
class CrewConfig(BaseModel):
"""Configuration for a crew in a deployment."""
name: str = Field(..., min_length=1)
module_path: str = Field(..., min_length=1)
class_name: str = Field(..., min_length=1)
class FlowConfig(BaseModel):
"""Configuration for a flow in a deployment."""
name: str = Field(..., min_length=1)
module_path: str = Field(..., min_length=1)
class_name: str = Field(..., min_length=1)
class DeploymentConfig(BaseModel):
"""Main configuration for a CrewAI deployment."""
name: str = Field(..., min_length=1)
port: int = Field(..., gt=0, lt=65536)
host: Optional[str] = Field(default="127.0.0.1")
crews: List[CrewConfig] = Field(default_factory=list)
flows: List[FlowConfig] = Field(default_factory=list)
environment: List[str] = Field(default_factory=list)
@validator('environment', pre=True)
def parse_environment(cls, v):
if not v:
return []
return v
class Config: class Config:
""" """
Configuration for CrewAI deployments. Configuration manager for CrewAI deployments.
""" """
def __init__(self, config_path: str): def __init__(self, config_path: str):
self.config_path = Path(config_path) self.config_path = Path(config_path)
self.config_data = self._load_config() self._config_data = self._load_config()
self.config = self._validate_config()
def _load_config(self) -> Dict[str, Any]: def _load_config(self) -> Dict[str, Any]:
"""Load configuration from YAML file.""" """Load configuration from YAML file."""
if not self.config_path.exists(): if not self.config_path.exists():
raise FileNotFoundError(f"Config file {self.config_path} not found") raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
with open(self.config_path, "r") as f: with open(self.config_path, "r") as f:
return yaml.safe_load(f) try:
return yaml.safe_load(f)
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML in configuration file: {e}")
def _validate_config(self) -> DeploymentConfig:
"""Validate configuration using Pydantic."""
try:
return DeploymentConfig(**self._config_data)
except Exception as e:
raise ValueError(f"Invalid configuration: {e}")
@property @property
def name(self) -> str: def name(self) -> str:
"""Get deployment name.""" """Get deployment name."""
return self.config_data.get("name", "crewai-deployment") return self.config.name
@property @property
def port(self) -> int: def port(self) -> int:
"""Get server port.""" """Get server port."""
return int(self.config_data.get("port", 8000)) return self.config.port
@property @property
def crews(self) -> List[Dict[str, Any]]: def host(self) -> str:
"""Get host configuration."""
return self.config.host
@property
def crews(self) -> List[CrewConfig]:
"""Get crews configuration.""" """Get crews configuration."""
return self.config_data.get("crews", []) return self.config.crews
@property @property
def flows(self) -> List[Dict[str, Any]]: def flows(self) -> List[FlowConfig]:
"""Get flows configuration.""" """Get flows configuration."""
return self.config_data.get("flows", []) return self.config.flows
@property
def environment(self) -> List[str]:
"""Get environment variables configuration."""
return self.config.environment

View File

@@ -3,6 +3,7 @@ import shutil
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from crewai.deployment.docker.exceptions import DockerBuildError, DockerRunError, DockerComposeError
class DockerContainer: class DockerContainer:
@@ -15,7 +16,7 @@ class DockerContainer:
self.dockerfile_path = self.deployment_dir / "Dockerfile" self.dockerfile_path = self.deployment_dir / "Dockerfile"
self.compose_path = self.deployment_dir / "docker-compose.yml" self.compose_path = self.deployment_dir / "docker-compose.yml"
def generate_dockerfile(self, requirements: List[str] = None): def generate_dockerfile(self, requirements: Optional[List[str]] = None):
"""Generate a Dockerfile for the deployment.""" """Generate a Dockerfile for the deployment."""
template_dir = Path(__file__).parent / "templates" template_dir = Path(__file__).parent / "templates"
dockerfile_template = template_dir / "Dockerfile" dockerfile_template = template_dir / "Dockerfile"
@@ -46,20 +47,32 @@ class DockerContainer:
def build(self): def build(self):
"""Build the Docker image.""" """Build the Docker image."""
cmd = ["docker", "build", "-t", f"crewai-{self.name}", "."] try:
subprocess.run(cmd, check=True, cwd=self.deployment_dir) cmd = ["docker", "build", "-t", f"crewai-{self.name}", "."]
subprocess.run(cmd, check=True, cwd=self.deployment_dir)
except subprocess.CalledProcessError as e:
raise DockerBuildError(f"Failed to build Docker image: {e}")
def start(self): def start(self):
"""Start the Docker containers using docker-compose.""" """Start the Docker containers using docker-compose."""
cmd = ["docker-compose", "up", "-d"] try:
subprocess.run(cmd, check=True, cwd=self.deployment_dir) cmd = ["docker-compose", "up", "-d"]
subprocess.run(cmd, check=True, cwd=self.deployment_dir)
except subprocess.CalledProcessError as e:
raise DockerRunError(f"Failed to start Docker containers: {e}")
def stop(self): def stop(self):
"""Stop the Docker containers.""" """Stop the Docker containers."""
cmd = ["docker-compose", "down"] try:
subprocess.run(cmd, check=True, cwd=self.deployment_dir) cmd = ["docker-compose", "down"]
subprocess.run(cmd, check=True, cwd=self.deployment_dir)
except subprocess.CalledProcessError as e:
raise DockerComposeError(f"Failed to stop Docker containers: {e}")
def logs(self): def logs(self):
"""Get container logs.""" """Get container logs."""
cmd = ["docker-compose", "logs"] try:
subprocess.run(cmd, check=True, cwd=self.deployment_dir) cmd = ["docker-compose", "logs"]
subprocess.run(cmd, check=True, cwd=self.deployment_dir)
except subprocess.CalledProcessError as e:
raise DockerComposeError(f"Failed to get Docker logs: {e}")

View File

@@ -0,0 +1,18 @@
class DockerError(Exception):
"""Base exception for Docker-related errors in CrewAI deployments."""
pass
class DockerBuildError(DockerError):
"""Exception raised when Docker build fails."""
pass
class DockerRunError(DockerError):
"""Exception raised when Docker container fails to run."""
pass
class DockerComposeError(DockerError):
"""Exception raised when docker-compose commands fail."""
pass

View File

@@ -1,11 +1,20 @@
import json import json
import os import os
import shutil import shutil
import logging
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from crewai.deployment.config import Config from crewai.deployment.config import Config
from crewai.deployment.docker.container import DockerContainer from crewai.deployment.docker.container import DockerContainer
from crewai.deployment.docker.exceptions import DockerError
# Configure structured logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("crewai.deployment")
class Deployment: class Deployment:
@@ -13,6 +22,7 @@ class Deployment:
Handles the deployment of CrewAI crews and flows. Handles the deployment of CrewAI crews and flows.
""" """
def __init__(self, config_path: str): def __init__(self, config_path: str):
logger.info(f"Initializing deployment from config: {config_path}")
self.config = Config(config_path) self.config = Config(config_path)
self.deployment_dir = Path(f"./deployments/{self.config.name}") self.deployment_dir = Path(f"./deployments/{self.config.name}")
self.docker = DockerContainer( self.docker = DockerContainer(
@@ -22,6 +32,8 @@ class Deployment:
def prepare(self): def prepare(self):
"""Prepare the deployment directory and files.""" """Prepare the deployment directory and files."""
logger.info(f"Preparing deployment: {self.config.name}")
# Create deployment directory # Create deployment directory
os.makedirs(self.deployment_dir, exist_ok=True) os.makedirs(self.deployment_dir, exist_ok=True)
@@ -29,22 +41,27 @@ class Deployment:
deployment_config = { deployment_config = {
"name": self.config.name, "name": self.config.name,
"port": self.config.port, "port": self.config.port,
"host": self.config.host,
"crews": [], "crews": [],
"flows": [] "flows": []
} }
# Process crews # Process crews
for crew_config in self.config.crews: for crew_config in self.config.crews:
name = crew_config["name"] name = crew_config.name
module_path = crew_config["module_path"] module_path = crew_config.module_path
class_name = crew_config["class_name"] class_name = crew_config.class_name
logger.info(f"Processing crew: {name}")
# Copy crew module to deployment directory # Copy crew module to deployment directory
source_path = Path(module_path) source_path = Path(module_path)
dest_path = self.deployment_dir / source_path.name dest_path = self.deployment_dir / source_path.name
if source_path.exists(): if source_path.exists():
shutil.copy(source_path, dest_path) shutil.copy(source_path, dest_path)
logger.debug(f"Copied {source_path} to {dest_path}")
else: else:
logger.warning(f"Crew module not found: {source_path}")
# For testing purposes, create an empty file # For testing purposes, create an empty file
with open(dest_path, 'w') as f: with open(dest_path, 'w') as f:
pass pass
@@ -58,16 +75,20 @@ class Deployment:
# Process flows # Process flows
for flow_config in self.config.flows: for flow_config in self.config.flows:
name = flow_config["name"] name = flow_config.name
module_path = flow_config["module_path"] module_path = flow_config.module_path
class_name = flow_config["class_name"] class_name = flow_config.class_name
logger.info(f"Processing flow: {name}")
# Copy flow module to deployment directory # Copy flow module to deployment directory
source_path = Path(module_path) source_path = Path(module_path)
dest_path = self.deployment_dir / source_path.name dest_path = self.deployment_dir / source_path.name
if source_path.exists(): if source_path.exists():
shutil.copy(source_path, dest_path) shutil.copy(source_path, dest_path)
logger.debug(f"Copied {source_path} to {dest_path}")
else: else:
logger.warning(f"Flow module not found: {source_path}")
# For testing purposes, create an empty file # For testing purposes, create an empty file
with open(dest_path, 'w') as f: with open(dest_path, 'w') as f:
pass pass
@@ -80,29 +101,61 @@ class Deployment:
}) })
# Write deployment config # Write deployment config
with open(self.deployment_dir / "deployment_config.json", "w") as f: config_file = self.deployment_dir / "deployment_config.json"
with open(config_file, "w") as f:
json.dump(deployment_config, f, indent=2) json.dump(deployment_config, f, indent=2)
logger.info(f"Created deployment config: {config_file}")
# Copy server template # Copy server template
server_template = Path(__file__).parent / "templates" / "server.py" server_template = Path(__file__).parent / "templates" / "server.py"
shutil.copy(server_template, self.deployment_dir / "server.py") server_dest = self.deployment_dir / "server.py"
shutil.copy(server_template, server_dest)
logger.info(f"Copied server template to {server_dest}")
# Generate Docker files # Generate Docker files
self.docker.generate_dockerfile() try:
self.docker.generate_compose_file(port=self.config.port) self.docker.generate_dockerfile()
self.docker.generate_compose_file(port=self.config.port)
logger.info("Generated Docker configuration files")
except Exception as e:
logger.error(f"Failed to generate Docker files: {e}")
raise
def build(self): def build(self):
"""Build the Docker image for the deployment.""" """Build the Docker image for the deployment."""
self.docker.build() logger.info(f"Building Docker image for {self.config.name}")
try:
self.docker.build()
logger.info("Docker image built successfully")
except DockerError as e:
logger.error(f"Failed to build Docker image: {e}")
raise
def start(self): def start(self):
"""Start the deployment.""" """Start the deployment."""
self.docker.start() logger.info(f"Starting deployment {self.config.name}")
try:
self.docker.start()
logger.info(f"Deployment started at http://{self.config.host}:{self.config.port}")
except DockerError as e:
logger.error(f"Failed to start deployment: {e}")
raise
def stop(self): def stop(self):
"""Stop the deployment.""" """Stop the deployment."""
self.docker.stop() logger.info(f"Stopping deployment {self.config.name}")
try:
self.docker.stop()
logger.info("Deployment stopped")
except DockerError as e:
logger.error(f"Failed to stop deployment: {e}")
raise
def logs(self): def logs(self):
"""Get deployment logs.""" """Get deployment logs."""
self.docker.logs() logger.info(f"Fetching logs for {self.config.name}")
try:
self.docker.logs()
except DockerError as e:
logger.error(f"Failed to fetch logs: {e}")
raise

View File

@@ -1,6 +1,7 @@
# CrewAI Deployment Configuration # CrewAI Deployment Configuration
name: my-crewai-app name: my-crewai-app
port: 8000 port: 8000
host: 127.0.0.1 # Default to localhost for security
# Crews configuration # Crews configuration
crews: crews:

View File

@@ -58,10 +58,13 @@ for flow_config in config.get("flows", []):
def read_root(): def read_root():
return {"status": "running", "crews": list(crews.keys()), "flows": list(flows.keys())} return {"status": "running", "crews": list(crews.keys()), "flows": list(flows.keys())}
@app.post("/run/crew/{crew_name}") @app.post("/run/crew/{crew_name}", response_model=RunResponse)
def run_crew(crew_name: str, request: RunRequest): def run_crew(crew_name: str, request: RunRequest):
if crew_name not in crews: if crew_name not in crews:
raise HTTPException(status_code=404, detail=f"Crew '{crew_name}' not found") raise HTTPException(
status_code=404,
detail=f"Crew '{crew_name}' not found. Available crews: {list(crews.keys())}"
)
try: try:
crew_instance = crews[crew_name].crew() crew_instance = crews[crew_name].crew()
@@ -70,18 +73,22 @@ def run_crew(crew_name: str, request: RunRequest):
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.post("/run/flow/{flow_name}") @app.post("/run/flow/{flow_name}", response_model=RunResponse)
def run_flow(flow_name: str, request: RunRequest): def run_flow(flow_name: str, request: RunRequest):
if flow_name not in flows: if flow_name not in flows:
raise HTTPException(status_code=404, detail=f"Flow '{flow_name}' not found") raise HTTPException(
status_code=404,
detail=f"Flow '{flow_name}' not found. Available flows: {list(flows.keys())}"
)
try: try:
flow_instance = flows[flow_name] flow_instance = flows[flow_name]
result = flow_instance.kickoff(inputs=request.inputs) result = flow_instance.kickoff(inputs=request.inputs)
return {"result": {"value": str(result)}} return {"result": {"value": str(result)}}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=f"Error running flow: {str(e)}")
if __name__ == "__main__": if __name__ == "__main__":
port = int(os.environ.get("PORT", 8000)) port = int(os.environ.get("PORT", 8000))
uvicorn.run(app, host="0.0.0.0", port=port) host = os.environ.get("HOST", "127.0.0.1") # Default to localhost instead of 0.0.0.0
uvicorn.run(app, host=host, port=port)

View File

@@ -18,6 +18,7 @@ class TestDeployment(unittest.TestCase):
f.write(""" f.write("""
name: test-deployment name: test-deployment
port: 8000 port: 8000
host: 127.0.0.1
crews: crews:
- name: test_crew - name: test_crew
@@ -42,8 +43,9 @@ class TestDeployment(unittest.TestCase):
config = Config(self.config_path) config = Config(self.config_path)
self.assertEqual(config.name, "test-deployment") self.assertEqual(config.name, "test-deployment")
self.assertEqual(config.port, 8000) self.assertEqual(config.port, 8000)
self.assertEqual(config.host, "127.0.0.1")
self.assertEqual(len(config.crews), 1) self.assertEqual(len(config.crews), 1)
self.assertEqual(config.crews[0]["name"], "test_crew") self.assertEqual(config.crews[0].name, "test_crew")
@mock.patch("crewai.deployment.docker.container.DockerContainer.generate_dockerfile") @mock.patch("crewai.deployment.docker.container.DockerContainer.generate_dockerfile")
@mock.patch("crewai.deployment.docker.container.DockerContainer.generate_compose_file") @mock.patch("crewai.deployment.docker.container.DockerContainer.generate_compose_file")