diff --git a/agentstack/cli/init.py b/agentstack/cli/init.py index b84035c1..b471c716 100644 --- a/agentstack/cli/init.py +++ b/agentstack/cli/init.py @@ -1,6 +1,8 @@ import os, sys from typing import Optional from pathlib import Path +import inquirer +from textwrap import shorten from agentstack import conf, log from agentstack.exceptions import EnvironmentError @@ -8,22 +10,23 @@ from agentstack import packaging from agentstack import frameworks from agentstack import generation -from agentstack.proj_templates import TemplateConfig +from agentstack.proj_templates import get_all_templates, TemplateConfig from agentstack.cli import welcome_message from agentstack.cli.wizard import run_wizard from agentstack.cli.templates import insert_template -DEFAULT_TEMPLATE_NAME: str = "hello_alex" - def require_uv(): try: uv_bin = packaging.get_uv_bin() assert os.path.exists(uv_bin) except (AssertionError, ImportError): - message = "Error: uv is not installed.\n" - message += "Full installation instructions at: https://docs.astral.sh/uv/getting-started/installation\n" + message = ( + "Error: uv is not installed.\n" + "Full installation instructions at: " + "https://docs.astral.sh/uv/getting-started/installation\n" + ) match sys.platform: case 'linux' | 'darwin': message += "Hint: run `curl -LsSf https://astral.sh/uv/install.sh | sh`\n" @@ -32,6 +35,28 @@ def require_uv(): raise EnvironmentError(message) +def select_template(slug_name: str, framework: Optional[str] = None) -> TemplateConfig: + """Let the user select a template from the ones available.""" + templates: list[TemplateConfig] = get_all_templates() + template_names = [shorten(f"⚡️ {t.name} - {t.description}", 80) for t in templates] + + empty_msg = "🆕 Empty Project" + template_choice = inquirer.list_input( + message="Do you want to start with a template?", + choices=[empty_msg] + template_names, + ) + template_name = template_choice.split("⚡️ ")[1].split(" - ")[0] + + if template_name == empty_msg: + return TemplateConfig( + name=slug_name, + description="", + framework=framework or frameworks.DEFAULT_FRAMEWORK, + ) + + return TemplateConfig.from_template_name(template_name) + + def init_project( slug_name: Optional[str] = None, template: Optional[str] = None, @@ -61,7 +86,9 @@ def init_project( if template and use_wizard: raise Exception("Template and wizard flags cannot be used together") - + + welcome_message() + if use_wizard: log.debug("Initializing new project with wizard.") template_data = run_wizard(slug_name) @@ -69,10 +96,9 @@ def init_project( log.debug(f"Initializing new project with template: {template}") template_data = TemplateConfig.from_user_input(template) else: - log.debug(f"Initializing new project with default template: {DEFAULT_TEMPLATE_NAME}") - template_data = TemplateConfig.from_template_name(DEFAULT_TEMPLATE_NAME) + log.debug("Initializing new project with template selection.") + template_data = select_template(slug_name, framework) - welcome_message() log.notify("🦾 Creating a new AgentStack project...") log.info(f"Using project directory: {conf.PATH.absolute()}") @@ -81,7 +107,7 @@ def init_project( if not framework in frameworks.SUPPORTED_FRAMEWORKS: raise Exception(f"Framework '{framework}' is not supported.") log.info(f"Using framework: {framework}") - + # copy the project skeleton, create a virtual environment, and install dependencies # project template is populated before the venv is created so we have a working directory insert_template(name=slug_name, template=template_data, framework=framework) @@ -89,7 +115,7 @@ def init_project( packaging.create_venv() log.info("Installing dependencies...") packaging.install_project() - + # now we can interact with the project and add Agents, Tasks, and Tools # we allow dependencies to be installed along with these, so the project must # be fully initialized first. diff --git a/agentstack/frameworks/__init__.py b/agentstack/frameworks/__init__.py index 6f738b7c..035a4f8e 100644 --- a/agentstack/frameworks/__init__.py +++ b/agentstack/frameworks/__init__.py @@ -20,6 +20,7 @@ CREWAI, LANGGRAPH, ] +DEFAULT_FRAMEWORK = CREWAI class FrameworkModule(Protocol): diff --git a/agentstack/proj_templates.py b/agentstack/proj_templates.py index a0b1531b..e678a9af 100644 --- a/agentstack/proj_templates.py +++ b/agentstack/proj_templates.py @@ -196,15 +196,15 @@ class Node(pydantic.BaseModel): name: str description: str - template_version: Literal[4] + template_version: Literal[4] = CURRENT_VERSION framework: str - method: str + method: str = "sequential" manager_agent: Optional[str] = None - agents: list[Agent] - tasks: list[Task] - tools: list[Tool] - graph: list[list[Node]] - inputs: dict[str, str] = {} + agents: list[Agent] = pydantic.Field(default_factory=list) + tasks: list[Task] = pydantic.Field(default_factory=list) + tools: list[Tool] = pydantic.Field(default_factory=list) + graph: list[list[Node]] = pydantic.Field(default_factory=list) + inputs: dict[str, str] = pydantic.Field(default_factory=dict) @pydantic.field_validator('graph') @classmethod diff --git a/agentstack/templates/proj_templates/graph.json b/agentstack/templates/proj_templates/graph.json.example similarity index 100% rename from agentstack/templates/proj_templates/graph.json rename to agentstack/templates/proj_templates/graph.json.example diff --git a/agentstack/templates/proj_templates/hello_alex.json b/agentstack/templates/proj_templates/hello_alex.json index 7f8f0164..6f7d7d22 100644 --- a/agentstack/templates/proj_templates/hello_alex.json +++ b/agentstack/templates/proj_templates/hello_alex.json @@ -1,6 +1,6 @@ { "name": "hello_alex", - "description": "This is the start of your AgentStack project.", + "description": "A simple example that opens the README and offers suggestions.", "template_version": 1, "framework": "crewai", "agents": [{ diff --git a/tests/test_cli_init.py b/tests/test_cli_init.py index 02267d35..0be94e69 100644 --- a/tests/test_cli_init.py +++ b/tests/test_cli_init.py @@ -4,6 +4,7 @@ from pathlib import Path import shutil from cli_test_utils import run_cli +from agentstack.proj_templates import get_all_templates BASE_PATH = Path(__file__).parent @@ -19,8 +20,9 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.project_dir, ignore_errors=True) - def test_init_command(self): + @parameterized.expand([(template.name, ) for template in get_all_templates()]) + def test_init_command(self, template_name: str): """Test the 'init' command to create a project directory.""" - result = run_cli('init', 'test_project') + result = run_cli('init', 'test_project', '--template', template_name) self.assertEqual(result.returncode, 0) self.assertTrue((self.project_dir / 'test_project').exists())