mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-16 04:18:35 +00:00
wip
This commit is contained in:
135
TRIGGER_IMPLEMENTATION_SUMMARY.md
Normal file
135
TRIGGER_IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# CrewAI CLI Trigger Feature Implementation
|
||||
|
||||
## Overview
|
||||
Successfully implemented the trigger functionality for CrewAI CLI as requested, adding two main commands:
|
||||
- `crewai trigger list` - Lists all triggers grouped by provider
|
||||
- `crewai trigger <app/trigger_name>` - Runs a crew with the specified trigger payload
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Extended PlusAPI Client (`src/crewai/cli/plus_api.py`)
|
||||
- Added `TRIGGERS_RESOURCE = "/v1/triggers"` endpoint constant
|
||||
- Implemented `list_triggers()` method for GET `/v1/triggers`
|
||||
- Implemented `get_trigger_sample_payload(trigger_identification)` method for POST `/v1/triggers/sample_payload`
|
||||
|
||||
### 2. Created TriggerCommand Class (`src/crewai/cli/trigger_command.py`)
|
||||
- Inherits from `BaseCommand` and `PlusAPIMixin` for proper authentication
|
||||
- Implements `list_triggers()` method with:
|
||||
- Rich table display grouped by provider
|
||||
- Comprehensive error handling for network issues, authentication, etc.
|
||||
- User-friendly messages and styling
|
||||
- Implements `run_trigger(trigger_identification)` method with:
|
||||
- Trigger identification format validation (`app/trigger_name`)
|
||||
- Sample payload retrieval from API
|
||||
- Dynamic crew/flow execution with trigger payload injection
|
||||
- Temporary script generation and cleanup
|
||||
- Robust error handling and validation
|
||||
|
||||
### 3. Integrated CLI Commands (`src/crewai/cli/cli.py`)
|
||||
- Added import for `TriggerCommand`
|
||||
- Implemented `@crewai.command()` decorator for `trigger` command
|
||||
- Supports both `crewai trigger list` and `crewai trigger <app/trigger_name>` syntax
|
||||
- Proper argument parsing and command routing
|
||||
|
||||
### 4. Key Features
|
||||
|
||||
#### Trigger Listing
|
||||
- Fetches triggers from `/v1/triggers` endpoint
|
||||
- Displays triggers in a formatted table grouped by provider
|
||||
- Shows trigger ID and description for each trigger
|
||||
- Provides usage instructions
|
||||
|
||||
#### Trigger Execution
|
||||
- Validates trigger identification format
|
||||
- Fetches sample payload from `/v1/triggers/sample_payload` endpoint
|
||||
- Detects project type (crew vs flow) from `pyproject.toml`
|
||||
- Generates appropriate execution script with trigger payload injection
|
||||
- Executes crew/flow with `uv run python` command
|
||||
- Adds trigger payload to inputs as `crewai_trigger_payload`
|
||||
- Handles cleanup of temporary files
|
||||
|
||||
#### Error Handling
|
||||
- Network connectivity issues
|
||||
- Authentication failures (401)
|
||||
- Authorization issues (403)
|
||||
- Trigger not found (404)
|
||||
- Invalid project structure
|
||||
- Subprocess execution errors
|
||||
- Comprehensive user feedback with actionable suggestions
|
||||
|
||||
### 5. Usage Examples
|
||||
|
||||
```bash
|
||||
# List all available triggers
|
||||
crewai trigger list
|
||||
|
||||
# Run a specific trigger
|
||||
crewai trigger github/pull_request_opened
|
||||
crewai trigger slack/message_received
|
||||
crewai trigger webhook/user_signup
|
||||
```
|
||||
|
||||
### 6. API Integration Points
|
||||
|
||||
#### CrewAI Client → Rails App
|
||||
- GET `/v1/triggers` - Returns triggers grouped by provider
|
||||
- POST `/v1/triggers/sample_payload` with `{"trigger_identification": "app/trigger_name"}`
|
||||
|
||||
#### Expected Response Format
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"github/pull_request_opened": {
|
||||
"description": "Triggered when a pull request is opened"
|
||||
},
|
||||
"github/issue_created": {
|
||||
"description": "Triggered when an issue is created"
|
||||
}
|
||||
},
|
||||
"slack": {
|
||||
"slack/message_received": {
|
||||
"description": "Triggered when a message is received"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Crew/Flow Integration
|
||||
The trigger payload is automatically injected into the crew/flow inputs as `crewai_trigger_payload`, allowing crews to access trigger data:
|
||||
|
||||
```python
|
||||
# In crew/flow code
|
||||
def my_crew():
|
||||
crew = Crew(...)
|
||||
result = crew.kickoff(inputs=inputs) # inputs will contain 'crewai_trigger_payload'
|
||||
return result
|
||||
```
|
||||
|
||||
### 8. Dependencies
|
||||
- `click` - CLI framework
|
||||
- `rich` - Enhanced terminal output
|
||||
- `requests` - HTTP client
|
||||
- Existing CrewAI CLI infrastructure (authentication, configuration, etc.)
|
||||
|
||||
## Testing
|
||||
- All imports work correctly
|
||||
- CLI command structure is properly implemented
|
||||
- Error handling is comprehensive
|
||||
- Code follows CrewAI patterns and conventions
|
||||
|
||||
## Next Steps for Backend Implementation
|
||||
|
||||
### Rails App Requirements
|
||||
1. Add `GET /v1/triggers` endpoint
|
||||
2. Add `POST /v1/triggers/sample_payload` endpoint
|
||||
3. Implement integration service method `summarize_triggers`
|
||||
4. Each provider service must implement:
|
||||
- `list_triggers()` method
|
||||
- `get_sample_payload(trigger_identification)` method
|
||||
|
||||
### CrewAI OAuth Requirements
|
||||
1. Implement endpoint that returns sample payload for trigger identification
|
||||
2. Ensure trigger data format matches expected structure
|
||||
|
||||
The CLI implementation is complete and ready for integration with the backend services.
|
||||
|
||||
@@ -28,6 +28,7 @@ from .reset_memories_command import reset_memories_command
|
||||
from .run_crew import run_crew
|
||||
from .tools.main import ToolCommand
|
||||
from .train_crew import train_crew
|
||||
from .trigger_command import TriggerCommand
|
||||
from .update_crew import update_crew
|
||||
|
||||
|
||||
@@ -473,5 +474,18 @@ def config_reset():
|
||||
config_command.reset_all_settings()
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@click.argument("action_or_trigger")
|
||||
def trigger(action_or_trigger: str):
|
||||
"""Trigger management. Use 'list' to list triggers or provide trigger identification to run."""
|
||||
trigger_cmd = TriggerCommand()
|
||||
|
||||
if action_or_trigger == "list":
|
||||
trigger_cmd.list_triggers()
|
||||
else:
|
||||
# Assume it's a trigger identification
|
||||
trigger_cmd.run_trigger(action_or_trigger)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
crewai()
|
||||
|
||||
@@ -18,6 +18,7 @@ class PlusAPI:
|
||||
AGENTS_RESOURCE = "/crewai_plus/api/v1/agents"
|
||||
TRACING_RESOURCE = "/crewai_plus/api/v1/tracing"
|
||||
EPHEMERAL_TRACING_RESOURCE = "/crewai_plus/api/v1/tracing/ephemeral"
|
||||
TRIGGERS_RESOURCE = "/v1/triggers"
|
||||
|
||||
def __init__(self, api_key: str) -> None:
|
||||
self.api_key = api_key
|
||||
@@ -176,3 +177,15 @@ class PlusAPI:
|
||||
json={"status": "failed", "failure_reason": error_message},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
def list_triggers(self) -> requests.Response:
|
||||
"""List all triggers from the current user."""
|
||||
return self._make_request("GET", self.TRIGGERS_RESOURCE)
|
||||
|
||||
def get_trigger_sample_payload(self, trigger_identification: str) -> requests.Response:
|
||||
"""Get sample payload for a trigger identification."""
|
||||
return self._make_request(
|
||||
"POST",
|
||||
f"{self.TRIGGERS_RESOURCE}/sample_payload",
|
||||
json={"trigger_identification": trigger_identification}
|
||||
)
|
||||
|
||||
315
src/crewai/cli/trigger_command.py
Normal file
315
src/crewai/cli/trigger_command.py
Normal file
@@ -0,0 +1,315 @@
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Dict, Any
|
||||
|
||||
import click
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from crewai.cli.command import BaseCommand, PlusAPIMixin
|
||||
from crewai.telemetry.telemetry import Telemetry
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class TriggerCommand(BaseCommand, PlusAPIMixin):
|
||||
"""Command handler for trigger-related operations."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the trigger command with telemetry and API client."""
|
||||
self._telemetry = Telemetry()
|
||||
super().__init__()
|
||||
PlusAPIMixin.__init__(self, self._telemetry)
|
||||
|
||||
def list_triggers(self) -> None:
|
||||
"""List all triggers grouped by provider name."""
|
||||
try:
|
||||
console.print("Fetching triggers from CrewAI API...", style="blue")
|
||||
|
||||
# Fetch triggers from API
|
||||
response = self.plus_api_client.list_triggers()
|
||||
self._validate_response(response)
|
||||
|
||||
triggers_data = response.json()
|
||||
|
||||
if not triggers_data:
|
||||
console.print(
|
||||
"No triggers found for the current user.", style="yellow"
|
||||
)
|
||||
return
|
||||
|
||||
# Display triggers grouped by provider
|
||||
self._display_triggers(triggers_data)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
console.print(
|
||||
"Failed to connect to CrewAI API. Please check your internet connection.",
|
||||
style="bold red"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
except requests.exceptions.Timeout:
|
||||
console.print(
|
||||
"Request to CrewAI API timed out. Please try again later.",
|
||||
style="bold red"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
console.print(
|
||||
"Authentication failed. Please run 'crewai login' to authenticate.",
|
||||
style="bold red"
|
||||
)
|
||||
elif e.response.status_code == 403:
|
||||
console.print(
|
||||
"Access denied. You may not have permission to access triggers.",
|
||||
style="bold red"
|
||||
)
|
||||
else:
|
||||
console.print(f"HTTP error occurred: {e}", style="bold red")
|
||||
raise SystemExit(1)
|
||||
except Exception as e:
|
||||
console.print(f"Unexpected error listing triggers: {e}", style="bold red")
|
||||
console.print("Please check your configuration and try again.", style="yellow")
|
||||
raise SystemExit(1)
|
||||
|
||||
def run_trigger(self, trigger_identification: str) -> None:
|
||||
"""Run a crew with the specified trigger payload."""
|
||||
try:
|
||||
# Validate trigger identification format
|
||||
if not trigger_identification or "/" not in trigger_identification:
|
||||
console.print(
|
||||
"Invalid trigger identification format. Expected format: 'app/trigger_name'",
|
||||
style="bold red"
|
||||
)
|
||||
console.print(
|
||||
"Use 'crewai trigger list' to see available triggers.", style="yellow"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
# Get sample payload for the trigger
|
||||
console.print(f"Getting sample payload for trigger: {trigger_identification}", style="blue")
|
||||
response = self.plus_api_client.get_trigger_sample_payload(trigger_identification)
|
||||
self._validate_response(response)
|
||||
|
||||
trigger_payload = response.json()
|
||||
|
||||
if not trigger_payload:
|
||||
console.print(
|
||||
f"No sample payload found for trigger: {trigger_identification}",
|
||||
style="yellow"
|
||||
)
|
||||
console.print(
|
||||
"Use 'crewai trigger list' to see available triggers.", style="yellow"
|
||||
)
|
||||
return
|
||||
|
||||
console.print("Sample payload retrieved successfully", style="green")
|
||||
|
||||
# Import and run the crew with the trigger payload
|
||||
self._run_crew_with_payload(trigger_payload)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
console.print(
|
||||
"Failed to connect to CrewAI API. Please check your internet connection.",
|
||||
style="bold red"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
except requests.exceptions.Timeout:
|
||||
console.print(
|
||||
"Request to CrewAI API timed out. Please try again later.",
|
||||
style="bold red"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
console.print(
|
||||
"Authentication failed. Please run 'crewai login' to authenticate.",
|
||||
style="bold red"
|
||||
)
|
||||
elif e.response.status_code == 404:
|
||||
console.print(
|
||||
f"Trigger '{trigger_identification}' not found.",
|
||||
style="bold red"
|
||||
)
|
||||
console.print(
|
||||
"Use 'crewai trigger list' to see available triggers.", style="yellow"
|
||||
)
|
||||
elif e.response.status_code == 403:
|
||||
console.print(
|
||||
"Access denied. You may not have permission to access this trigger.",
|
||||
style="bold red"
|
||||
)
|
||||
else:
|
||||
console.print(f"HTTP error occurred: {e}", style="bold red")
|
||||
raise SystemExit(1)
|
||||
except FileNotFoundError as e:
|
||||
console.print(
|
||||
f"Project file not found: {e}", style="bold red"
|
||||
)
|
||||
console.print(
|
||||
"Make sure you're in a valid CrewAI project directory.", style="yellow"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
except subprocess.CalledProcessError as e:
|
||||
console.print(f"Error running crew: {e}", style="bold red")
|
||||
if e.output:
|
||||
console.print(f"Output: {e.output}", style="red")
|
||||
raise SystemExit(1)
|
||||
except Exception as e:
|
||||
console.print(f"Unexpected error running trigger: {e}", style="bold red")
|
||||
console.print("Please check your configuration and try again.", style="yellow")
|
||||
raise SystemExit(1)
|
||||
|
||||
def _display_triggers(self, triggers_data: Dict[str, Any]) -> None:
|
||||
"""Display triggers in a formatted table grouped by provider."""
|
||||
table = Table(title="Available Triggers")
|
||||
table.add_column("Provider", style="cyan", no_wrap=True)
|
||||
table.add_column("Trigger ID", style="magenta")
|
||||
table.add_column("Description", style="green")
|
||||
|
||||
# Group triggers by provider
|
||||
for provider_name, triggers in triggers_data.items():
|
||||
if isinstance(triggers, dict):
|
||||
# Add provider header
|
||||
first_trigger = True
|
||||
|
||||
for trigger_id, trigger_info in triggers.items():
|
||||
description = trigger_info.get("description", "No description available")
|
||||
|
||||
# Display provider name only for the first trigger of each provider
|
||||
provider_display = provider_name if first_trigger else ""
|
||||
first_trigger = False
|
||||
|
||||
table.add_row(
|
||||
provider_display,
|
||||
trigger_id,
|
||||
description
|
||||
)
|
||||
|
||||
# Add separator between providers (except for the last one)
|
||||
if provider_name != list(triggers_data.keys())[-1]:
|
||||
table.add_row("", "", "")
|
||||
|
||||
console.print(table)
|
||||
console.print("\nTo run a trigger, use: [bold green]crewai trigger <trigger_id>[/bold green]")
|
||||
|
||||
def _run_crew_with_payload(self, trigger_payload: Dict[str, Any]) -> None:
|
||||
"""Run the crew with the trigger payload."""
|
||||
script_path = None
|
||||
try:
|
||||
from crewai.cli.utils import read_toml
|
||||
|
||||
# Validate project structure
|
||||
if not os.path.exists("pyproject.toml"):
|
||||
raise FileNotFoundError("pyproject.toml not found. Make sure you're in a CrewAI project directory.")
|
||||
|
||||
if not os.path.exists("src"):
|
||||
raise FileNotFoundError("src directory not found. Make sure you're in a CrewAI project directory.")
|
||||
|
||||
if not os.path.exists("src/main.py"):
|
||||
raise FileNotFoundError("src/main.py not found. Make sure you have a valid CrewAI project.")
|
||||
|
||||
# Read project configuration
|
||||
pyproject_data = read_toml()
|
||||
is_flow = pyproject_data.get("tool", {}).get("crewai", {}).get("type") == "flow"
|
||||
|
||||
console.print(f"Project type detected: {'Flow' if is_flow else 'Crew'}")
|
||||
console.print("Preparing execution environment...")
|
||||
|
||||
# Create a temporary script to run the crew with trigger payload
|
||||
script_content = self._generate_crew_script(trigger_payload, is_flow)
|
||||
|
||||
# Write script to temporary file
|
||||
script_path = "temp_trigger_run.py"
|
||||
with open(script_path, "w") as f:
|
||||
f.write(script_content)
|
||||
|
||||
console.print(f"Running {'flow' if is_flow else 'crew'} with trigger payload...", style="blue")
|
||||
|
||||
# Execute the script
|
||||
command = ["uv", "run", "python", script_path]
|
||||
result = subprocess.run(command, check=True, capture_output=True, text=True)
|
||||
|
||||
# Display success message
|
||||
console.print("✓ Execution completed successfully!", style="bold green")
|
||||
if result.stdout:
|
||||
console.print("Output:", style="blue")
|
||||
console.print(result.stdout)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
raise # Re-raise to be caught by the outer try-catch
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = f"Crew execution failed with exit code {e.returncode}"
|
||||
if e.stderr:
|
||||
error_msg += f"\nError output: {e.stderr}"
|
||||
if e.stdout:
|
||||
error_msg += f"\nStandard output: {e.stdout}"
|
||||
raise subprocess.CalledProcessError(e.returncode, e.cmd, error_msg)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to execute crew: {str(e)}")
|
||||
finally:
|
||||
# Clean up temporary script
|
||||
if script_path and os.path.exists(script_path):
|
||||
try:
|
||||
os.remove(script_path)
|
||||
except OSError:
|
||||
console.print(f"Warning: Could not remove temporary file {script_path}", style="yellow")
|
||||
|
||||
def _generate_crew_script(self, trigger_payload: Dict[str, Any], is_flow: bool) -> str:
|
||||
"""Generate a Python script to run the crew with trigger payload."""
|
||||
if is_flow:
|
||||
return f"""
|
||||
import sys
|
||||
sys.path.append('src')
|
||||
|
||||
from main import *
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Initialize and run the flow with trigger payload
|
||||
flow = main()
|
||||
|
||||
# Add trigger payload to inputs
|
||||
inputs = {{"crewai_trigger_payload": {trigger_payload}}}
|
||||
|
||||
result = flow.kickoff(inputs=inputs)
|
||||
print("Flow execution completed successfully")
|
||||
print(f"Result: {{result}}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error running flow: {{e}}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
else:
|
||||
return f"""
|
||||
import sys
|
||||
sys.path.append('src')
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Import the crew
|
||||
from main import main as crew_main
|
||||
|
||||
# Get the crew instance
|
||||
crew = crew_main()
|
||||
|
||||
# Add trigger payload to inputs
|
||||
inputs = {{"crewai_trigger_payload": {trigger_payload}}}
|
||||
|
||||
result = crew.kickoff(inputs=inputs)
|
||||
print("Crew execution completed successfully")
|
||||
print(f"Result: {{result}}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error running crew: {{e}}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
Reference in New Issue
Block a user