Skip to content

Commit

Permalink
Initialize project now creates a virtual enviroment for you.
Browse files Browse the repository at this point in the history
All package management done with `uv`.

TODO: `packaging.remove` and `packaging.upgrade` still need to be implemented.
  • Loading branch information
tcdent committed Dec 17, 2024
1 parent c936e58 commit 3248719
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 54 deletions.
3 changes: 2 additions & 1 deletion agentstack/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .cli import init_project_builder, configure_default_model, export_template
from .cli import init_project_builder, configure_default_model, export_template, welcome_message
from .init import init_project
from .tools import list_tools, add_tool
from .run import run_project
51 changes: 23 additions & 28 deletions agentstack/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,13 @@ def init_project_builder(
tools = [tools.model_dump() for tools in template_data.tools]

elif use_wizard:
welcome_message()
project_details = ask_project_details(slug_name)
welcome_message()
framework = ask_framework()
design = ask_design()
tools = ask_tools()

else:
welcome_message()
# the user has started a new project; let's give them something to work with
default_project = TemplateConfig.from_template_name('hello_alex')
project_details = {
Expand All @@ -117,9 +115,6 @@ def init_project_builder(
log.debug(f"project_details: {project_details}" f"framework: {framework}" f"design: {design}")
insert_template(project_details, framework, design, template_data)

# we have an agentstack.json file in the directory now
conf.set_path(project_details['name'])

for tool_data in tools:
generation.add_tool(tool_data['name'], agents=tool_data['agents'])

Expand Down Expand Up @@ -395,14 +390,14 @@ def insert_template(
f'{template_path}/{"{{cookiecutter.project_metadata.project_slug}}"}/.env',
)

if os.path.isdir(project_details['name']):
print(
term_color(
f"Directory {template_path} already exists. Please check this and try again",
"red",
)
)
sys.exit(1)
# if os.path.isdir(project_details['name']):
# print(
# term_color(
# f"Directory {template_path} already exists. Please check this and try again",
# "red",
# )
# )
# sys.exit(1)

cookiecutter(str(template_path), no_input=True, extra_context=None)

Expand All @@ -420,21 +415,21 @@ def insert_template(
# os.system("poetry install")
# os.system("cls" if os.name == "nt" else "clear")
# TODO: add `agentstack docs` command
print(
"\n"
"🚀 \033[92mAgentStack project generated successfully!\033[0m\n\n"
" Next, run:\n"
f" cd {project_metadata.project_slug}\n"
" python -m venv .venv\n"
" source .venv/bin/activate\n\n"
" Make sure you have the latest version of poetry installed:\n"
" pip install -U poetry\n\n"
" You'll need to install the project's dependencies with:\n"
" poetry install\n\n"
" Finally, try running your agent with:\n"
" agentstack run\n\n"
" Run `agentstack quickstart` or `agentstack docs` for next steps.\n"
)
# print(
# "\n"
# "🚀 \033[92mAgentStack project generated successfully!\033[0m\n\n"
# " Next, run:\n"
# f" cd {project_metadata.project_slug}\n"
# " python -m venv .venv\n"
# " source .venv/bin/activate\n\n"
# " Make sure you have the latest version of poetry installed:\n"
# " pip install -U poetry\n\n"
# " You'll need to install the project's dependencies with:\n"
# " poetry install\n\n"
# " Finally, try running your agent with:\n"
# " agentstack run\n\n"
# " Run `agentstack quickstart` or `agentstack docs` for next steps.\n"
# )


def export_template(output_filename: str):
Expand Down
70 changes: 70 additions & 0 deletions agentstack/cli/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os, sys
from typing import Optional
from pathlib import Path
from agentstack import conf
from agentstack import packaging
from agentstack.cli import welcome_message, init_project_builder
from agentstack.utils import term_color


# TODO move the rest of the CLI init tooling into this file


def require_uv():
try:
uv_bin = packaging.get_uv_bin()
assert os.path.exists(uv_bin)
except (AssertionError, ImportError):
print(term_color("Error: uv is not installed.", 'red'))
print("Full installation instructions at: https://docs.astral.sh/uv/getting-started/installation")
match sys.platform:
case 'linux' | 'darwin':
print("Hint: run `curl -LsSf https://astral.sh/uv/install.sh | sh`")
case _:
pass
sys.exit(1)


def init_project(
slug_name: Optional[str] = None,
template: Optional[str] = None,
use_wizard: bool = False,
):
"""
Initialize a new project in the current directory.
- create a new virtual environment
- copy project skeleton
- install dependencies
"""
welcome_message()

# conf.PATH may have been set by the argument parser, but if not, use the slug_name
if slug_name:
conf.set_path(conf.PATH / slug_name)
else:
print("Error: No project directory specified.")
print("Run `agentstack init <project_name> or use the --path flag.")
sys.exit(1)

if os.path.exists(conf.PATH):
print(f"Error: Directory already exists: {conf.PATH}")
sys.exit(1)

print(term_color("🦾 Creating a new AgentStack project...", 'blue'))
print(f"Using project directory: {conf.PATH.absolute()}")
# copy the project skeleton, create a virtual environment, and install dependencies
init_project_builder(slug_name, template, use_wizard)
packaging.create_venv()
packaging.install('.')

print(
"\n"
"🚀 \033[92mAgentStack project generated successfully!\033[0m\n\n"
" To get started, activate the virtual environment with:\n"
f" cd {conf.PATH}\n"
" source .venv/bin/activate\n\n"
" Run your new agent with:\n"
" agentstack run\n\n"
" Or, run `agentstack quickstart` or `agentstack docs` for more next steps.\n"
)
1 change: 1 addition & 0 deletions agentstack/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def _import_project_module(path: Path):
assert spec is not None # appease type checker
assert spec.loader is not None # appease type checker

print('dev version')
project_module = importlib.util.module_from_spec(spec)
sys.path.append(str((path / MAIN_FILENAME).parent))
spec.loader.exec_module(project_module)
Expand Down
9 changes: 9 additions & 0 deletions agentstack/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ class ValidationError(Exception):
"""

pass


class EnvironmentError(Exception):
"""
Raised when an error occurs in the execution environment ie. a command is
not present or the environment is not configured as expected.
"""

pass
4 changes: 2 additions & 2 deletions agentstack/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from agentstack import conf
from agentstack.cli import (
init_project_builder,
init_project,
add_tool,
list_tools,
configure_default_model,
Expand Down Expand Up @@ -162,7 +162,7 @@ def main():
elif args.command in ["templates"]:
webbrowser.open("https://docs.agentstack.sh/quickstart")
elif args.command in ["init", "i"]:
init_project_builder(args.slug_name, args.template, args.wizard)
init_project(args.slug_name, args.template, args.wizard)
elif args.command in ["run", "r"]:
run_project(command=args.function, debug=args.debug, cli_args=extra_args)
elif args.command in ['generate', 'g']:
Expand Down
131 changes: 122 additions & 9 deletions agentstack/packaging.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,131 @@
import os
from typing import Optional
import os, sys
from typing import Optional, Callable
from pathlib import Path
import re
import subprocess
import select
from agentstack import conf
from agentstack.exceptions import EnvironmentError

PACKAGING_CMD = "poetry"

DEFAULT_PYTHON_VERSION = "3.12"
VENV_DIR_NAME: Path = Path(".venv")

def install(package: str, path: Optional[str] = None):
if path:
os.chdir(path)
os.system(f"{PACKAGING_CMD} add {package}")

def install(package: str):
"""
Install a package with `uv`.
Filter output to only show useful progress messages.
"""
RE_USEFUL_PROGRESS = re.compile(r'^(Resolved|Prepared|Installed|Audited)')

def on_progress(line: str):
# only print these four messages:
# Resolved 78 packages in 225ms
# Prepared 12 packages in 915ms
# Installed 78 packages in 65ms
# Audited 1 package in 28ms
if RE_USEFUL_PROGRESS.match(line):
print(line.strip())

def on_error(line: str):
print(f"uv: [error]\n {line.strip()}")

# explicitly specify the --python executable to use so that the packages
# are installed into the correct virtual environment
_wrap_command_with_callbacks(
[get_uv_bin(), 'pip', 'install', '--python', '.venv/bin/python', package],
on_progress=on_progress,
on_error=on_error,
)


def remove(package: str):
os.system(f"{PACKAGING_CMD} remove {package}")
raise NotImplementedError("TODO `packaging.remove`")


def upgrade(package: str):
os.system(f"{PACKAGING_CMD} add {package}")
raise NotImplementedError("TODO `packaging.upgrade`")


def create_venv(python_version: str = DEFAULT_PYTHON_VERSION):
"""Intialize a virtual environment in the project directory of one does not exist."""
if os.path.exists(conf.PATH / VENV_DIR_NAME):
return # venv already exists

RE_USEFUL_PROGRESS = re.compile(r'^(Using|Creating)')

def on_progress(line: str):
if RE_USEFUL_PROGRESS.match(line):
print(line.strip())

def on_error(line: str):
print(f"uv: [error]\n {line.strip()}")

_wrap_command_with_callbacks(
[get_uv_bin(), 'venv', '--python', python_version],
on_progress=on_progress,
on_error=on_error,
)


def get_uv_bin() -> str:
"""Find the path to the uv binary."""
try:
import uv

return uv.find_uv_bin()
except ImportError as e:
raise e


def _setup_env() -> dict[str, str]:
"""Copy the current environment and add the virtual environment path for use by a subprocess."""
env = os.environ.copy()
env.setdefault("VIRTUAL_ENV", VENV_DIR_NAME)
env["UV_INTERNAL__PARENT_INTERPRETER"] = sys.executable
return env


def _wrap_command_with_callbacks(
command: list[str],
on_progress: Callable[[str], None] = lambda x: None,
on_complete: Callable[[str], None] = lambda x: None,
on_error: Callable[[str], None] = lambda x: None,
) -> None:
"""Run a command with progress callbacks."""
try:
all_lines = ''
process = subprocess.Popen(
command,
cwd=conf.PATH.absolute(),
env=_setup_env(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
assert process.stdout and process.stderr # appease type checker

readable = [process.stdout, process.stderr]
while readable:
ready, _, _ = select.select(readable, [], [])
for fd in ready:
line = fd.readline()
if not line:
readable.remove(fd)
continue

on_progress(line)
all_lines += line

if process.wait() == 0: # return code: success
on_complete(all_lines)
else:
on_error(all_lines)
except Exception as e:
on_error(str(e))
finally:
try:
process.terminate()
except:
pass
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
[tool.poetry]
[project]
name = "{{cookiecutter.project_metadata.project_name}}"
version = "{{cookiecutter.project_metadata.version}}"
description = "{{cookiecutter.project_metadata.description}}"
authors = ["{{cookiecutter.project_metadata.author_name}}"]
license = "{{cookiecutter.project_metadata.license}}"
package-mode = false
authors = [
{ name = "{{cookiecutter.project_metadata.author_name}}" }
]
license = { text = "{{cookiecutter.project_metadata.license}}" }
requires-python = ">=3.10"

[tool.poetry.dependencies]
python = ">=3.10,<=3.13"
agentstack = {extras = ["{{cookiecutter.framework}}"], version="{{cookiecutter.project_metadata.agentstack_version}}"}

[project.scripts]
{{cookiecutter.project_metadata.project_name}} = "{{cookiecutter.project_metadata.project_name}}.main:run"
run_crew = "{{cookiecutter.project_metadata.project_name}}.main:run"
train = "{{cookiecutter.project_metadata.project_name}}.main:train"
replay = "{{cookiecutter.project_metadata.project_name}}.main:replay"
test = "{{cookiecutter.project_metadata.project_name}}.main:test"
dependencies = [
"agentstack[{{cookiecutter.framework}}]=={{cookiecutter.project_metadata.agentstack_version}}",
]
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies = [
"requests>=2.32",
"appdirs>=1.4.4",
"python-dotenv>=1.0.1",
"uv>=0.5.6",
]

[project.optional-dependencies]
Expand Down

0 comments on commit 3248719

Please sign in to comment.