Skip to content
Merged
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
48 changes: 37 additions & 11 deletions agentstack/cli/init.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
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
from agentstack.utils import is_snake_case
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"
Expand All @@ -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,
Expand Down Expand Up @@ -61,18 +86,19 @@ 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)
elif template:
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()}")

Expand All @@ -81,15 +107,15 @@ 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)
log.info("Creating virtual environment...")
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.
Expand Down
1 change: 1 addition & 0 deletions agentstack/frameworks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
CREWAI,
LANGGRAPH,
]
DEFAULT_FRAMEWORK = CREWAI


class FrameworkModule(Protocol):
Expand Down
14 changes: 7 additions & 7 deletions agentstack/proj_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion agentstack/templates/proj_templates/hello_alex.json
Original file line number Diff line number Diff line change
@@ -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": [{
Expand Down
6 changes: 4 additions & 2 deletions tests/test_cli_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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())
Loading