Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agentstack/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .cli import init_project_builder, list_tools, configure_default_model
from .cli import init_project_builder, list_tools, configure_default_model, run_project
23 changes: 22 additions & 1 deletion agentstack/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import time
from datetime import datetime
from typing import Optional
from pathlib import Path
import requests
import itertools

Expand All @@ -18,7 +19,9 @@
from agentstack.utils import get_package_path
from agentstack.generation.files import ConfigFile
from agentstack.generation.tool_generation import get_all_tools
from agentstack import packaging, generation
from agentstack import frameworks
from agentstack import packaging
from agentstack import generation
from agentstack.utils import open_json_file, term_color, is_snake_case
from agentstack.update import AGENTSTACK_PACKAGE

Expand Down Expand Up @@ -151,6 +154,24 @@ def configure_default_model(path: Optional[str] = None):
agentstack_config.default_model = model


def run_project(framework: str, path: str = ''):
"""Validate that the project is ready to run and then run it."""
if not framework in frameworks.SUPPORTED_FRAMEWORKS:
print(term_color(f"Framework {framework} is not supported by agentstack.", 'red'))
sys.exit(1)

try:
frameworks.validate_project(framework, path)
except frameworks.ValidationError as e:
print(term_color("Project validation failed:", 'red'))
print(e)
sys.exit(1)

path = Path(path)
entrypoint = path/frameworks.get_entrypoint_path(framework)
os.system(f'python {entrypoint}')


def ask_framework() -> str:
framework = "CrewAI"
# framework = inquirer.list_input(
Expand Down
41 changes: 41 additions & 0 deletions agentstack/frameworks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Methods for interacting with framework-specific features.

Each framework should have a module in the `frameworks` package which defines the following methods:

- `ENTRYPOINT`: Path: Relative path to the entrypoint file for the framework
- `validate_project(path: Optional[Path] = None) -> None`: Validate that a project is ready to run.
Raises a `ValidationError` if the project is not valid.
"""
from typing import Optional
from importlib import import_module
from pathlib import Path


CREWAI = 'crewai'
SUPPORTED_FRAMEWORKS = [CREWAI, ]

def get_framework_module(framework: str) -> import_module:
"""
Get the module for a framework.
"""
if framework == CREWAI:
from . import crewai
return crewai
else:
raise ValueError(f"Framework {framework} not supported")

def get_entrypoint_path(framework: str) -> Path:
"""
Get the path to the entrypoint file for a framework.
"""
return get_framework_module(framework).ENTRYPOINT

class ValidationError(Exception): pass

def validate_project(framework: str, path: Optional[Path] = None) -> None:
"""
Run the framework specific project validation.
"""
return get_framework_module(framework).validate_project(path)

63 changes: 63 additions & 0 deletions agentstack/frameworks/crewai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from typing import Optional
from pathlib import Path
import ast
from . import SUPPORTED_FRAMEWORKS, ValidationError


ENTRYPOINT: Path = Path('src/crew.py')

def validate_project(path: Optional[Path] = None) -> None:
"""
Validate that a CrewAI project is ready to run.
Raises a frameworks.VaidationError if the project is not valid.
"""
try:
if path is None: path = Path()
with open(path/ENTRYPOINT, 'r') as f:
tree = ast.parse(f.read())
except (FileNotFoundError, SyntaxError) as e:
raise ValidationError(f"Failed to parse {ENTRYPOINT}\n {e}")

# A valid project must have a class in the crew.py file decorated with `@CrewBase`
try:
class_node = _find_class_with_decorator(tree, 'CrewBase')[0]
except IndexError:
raise ValidationError(f"`@CrewBase` decorated class not found in {ENTRYPOINT}")

# The Crew class must have one or more methods decorated with `@agent`
if len(_find_decorated_method_in_class(class_node, 'task')) < 1:
raise ValidationError(
f"`@task` decorated method not found in `{class_node.name}` class in {ENTRYPOINT}.\n"
"Create a new task using `agentstack generate task <task_name>`.")

# The Crew class must have one or more methods decorated with `@agent`
if len(_find_decorated_method_in_class(class_node, 'agent')) < 1:
raise ValidationError(
f"`@agent` decorated method not found in `{class_node.name}` class in {ENTRYPOINT}.\n"
"Create a new agent using `agentstack generate agent <agent_name>`.")

# The Crew class must have one method decorated with `@crew`
if len(_find_decorated_method_in_class(class_node, 'crew')) < 1:
raise ValidationError(f"`@crew` decorated method not found in `{class_node.name}` class in {ENTRYPOINT}")

# TODO move these to a shared AST utility module
def _find_class_with_decorator(tree: ast.AST, decorator_name: str) -> list[ast.ClassDef]:
"""Find a class definition that is marked by a decorator in an AST."""
nodes = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.ClassDef):
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == decorator_name:
nodes.append(node)
return nodes

def _find_decorated_method_in_class(classdef: ast.ClassDef, decorator_name: str) -> list[ast.FunctionDef]:
"""Find all method definitions in a class definition which are decorated with a specific decorator."""
nodes = []
for node in ast.iter_child_nodes(classdef):
if isinstance(node, ast.FunctionDef):
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == decorator_name:
nodes.append(node)
return nodes

13 changes: 4 additions & 9 deletions agentstack/generation/gen_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import sys
from enum import Enum
from typing import Optional, Union, List
from pathlib import Path

from agentstack.utils import term_color
from agentstack import frameworks


def insert_code_after_tag(file_path, tag, code_to_insert, next_line=False):
Expand Down Expand Up @@ -72,14 +74,6 @@ def string_in_file(file_path: str, str_to_match: str) -> bool:
return str_to_match in file_content


def _framework_filename(framework: str, path: str = ''):
if framework == 'crewai':
return f'{path}src/crew.py'

print(term_color(f'Unknown framework: {framework}', 'red'))
sys.exit(1)


class CrewComponent(str, Enum):
AGENT = "agent"
TASK = "task"
Expand All @@ -103,7 +97,8 @@ def get_crew_components(
Returns:
Dictionary with 'agents' and 'tasks' keys containing lists of names
"""
filename = _framework_filename(framework, path)
path = Path(path)
filename = path/frameworks.get_entrypoint_path(framework)

# Convert single component type to list for consistent handling
if isinstance(component_type, CrewComponent):
Expand Down
20 changes: 4 additions & 16 deletions agentstack/generation/tool_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Optional, List, Dict, Union

from . import get_agent_names
from .gen_utils import insert_code_after_tag, string_in_file, _framework_filename
from .gen_utils import insert_code_after_tag, string_in_file
from ..utils import open_json_file, get_framework, term_color
import os
import shutil
Expand All @@ -19,25 +19,12 @@
from agentstack import packaging
from agentstack.utils import get_package_path
from agentstack.generation.files import ConfigFile, EnvFile
from agentstack import frameworks
from .gen_utils import insert_code_after_tag, string_in_file
from ..utils import open_json_file, get_framework, term_color


TOOL_INIT_FILENAME = "src/tools/__init__.py"
FRAMEWORK_FILENAMES: dict[str, str] = {
'crewai': 'src/crew.py',
}

def get_framework_filename(framework: str, path: str = ''):
if path:
path = path.endswith('/') and path or path + '/'
else:
path = './'
try:
return f"{path}{FRAMEWORK_FILENAMES[framework]}"
except KeyError:
print(term_color(f'Unknown framework: {framework}', 'red'))
sys.exit(1)

class ToolConfig(BaseModel):
name: str
Expand Down Expand Up @@ -375,7 +362,8 @@ def modify_agent_tools(
print(term_color(f"Agent '{agent}' not found in the project.", 'red'))
sys.exit(1)

filename = _framework_filename(framework, path)
path = Path(path)
filename = path/frameworks.get_entrypoint_path(framework)

with open(filename, 'r', encoding='utf-8') as f:
source_lines = f.readlines()
Expand Down
5 changes: 2 additions & 3 deletions agentstack/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import sys

from agentstack.cli import init_project_builder, list_tools, configure_default_model
from agentstack.cli import init_project_builder, list_tools, configure_default_model, run_project
from agentstack.telemetry import track_cli_command
from agentstack.utils import get_version, get_framework
import agentstack.generation as generation
Expand Down Expand Up @@ -102,8 +102,7 @@ def main():
init_project_builder(args.slug_name, args.template, args.wizard)
elif args.command in ['run', 'r']:
framework = get_framework()
if framework == "crewai":
os.system('python src/main.py')
run_project(framework)
elif args.command in ['generate', 'g']:
if args.generate_command in ['agent', 'a']:
if not args.llm:
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/agentstack.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"framework": "crewai",
"tools": ["tool1", "tool2"]
"tools": []
}
26 changes: 23 additions & 3 deletions tests/test_cli_loads.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import subprocess
import sys
import os, sys
import unittest
from pathlib import Path
import shutil

BASE_PATH = Path(__file__).parent

class TestAgentStackCLI(unittest.TestCase):
CLI_ENTRY = [sys.executable, "-m", "agentstack.main"] # Replace with your actual CLI entry point if different
Expand Down Expand Up @@ -31,19 +32,38 @@ def test_invalid_command(self):

def test_init_command(self):
"""Test the 'init' command to create a project directory."""
test_dir = Path("test_project")
test_dir = Path(BASE_PATH/'tmp/test_project')

# Ensure the directory doesn't exist from previous runs
if test_dir.exists():
shutil.rmtree(test_dir)

os.makedirs(test_dir)

os.chdir(test_dir)
result = self.run_cli("init", str(test_dir))
self.assertEqual(result.returncode, 0)
self.assertTrue(test_dir.exists())

# Clean up
shutil.rmtree(test_dir)

def test_run_command_invalid_project(self):
"""Test the 'run' command on an invalid project."""
test_dir = Path(BASE_PATH/'tmp/test_project')
if test_dir.exists():
shutil.rmtree(test_dir)
os.makedirs(test_dir)

# Write a basic agentstack.json file
with (test_dir/'agentstack.json').open('w') as f:
f.write(open(BASE_PATH/'fixtures/agentstack.json', 'r').read())

os.chdir(test_dir)
result = self.run_cli('run')
self.assertNotEqual(result.returncode, 0)
self.assertIn("Project validation failed", result.stdout)

shutil.rmtree(test_dir)

if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion tests/test_generation_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class GenerationFilesTest(unittest.TestCase):
def test_read_config(self):
config = ConfigFile(BASE_PATH / "fixtures") # + agentstack.json
assert config.framework == "crewai"
assert config.tools == ["tool1", "tool2"]
assert config.tools == []
assert config.telemetry_opt_out is None
assert config.default_model is None

Expand Down
Loading