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
12 changes: 9 additions & 3 deletions agentstack/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
from agentstack.utils import get_package_path
from agentstack.generation.files import ConfigFile
from agentstack.generation.tool_generation import get_all_tools
from .. import generation
from ..utils import open_json_file, term_color, is_snake_case
from agentstack import packaging, generation
from agentstack.utils import open_json_file, term_color, is_snake_case
from agentstack.update import AGENTSTACK_PACKAGE

PREFERRED_MODELS = [
'openai/gpt-4o',
Expand Down Expand Up @@ -91,7 +92,7 @@ def init_project_builder(slug_name: Optional[str] = None, template: Optional[str
"license": "MIT"
}

framework = "CrewAI" # TODO: if --no-wizard, require a framework flag
framework = "crewai" # TODO: if --no-wizard, require a framework flag

design = {
'agents': [],
Expand All @@ -110,6 +111,11 @@ def init_project_builder(slug_name: Optional[str] = None, template: Optional[str
for tool_data in tools:
generation.add_tool(tool_data['name'], agents=tool_data['agents'], path=project_details['name'])

try:
packaging.install(f'{AGENTSTACK_PACKAGE}[{framework}]', path=slug_name)
except Exception as e:
print(term_color(f"Failed to install dependencies for {slug_name}. Please try again by running `agentstack update`", 'red'))


def welcome_message():
os.system("cls" if os.name == "nt" else "clear")
Expand Down
7 changes: 3 additions & 4 deletions agentstack/generation/tool_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
import ast
from pydantic import BaseModel, ValidationError

from agentstack import packaging
from agentstack.utils import get_package_path
from agentstack.generation.files import ConfigFile, EnvFile
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',
}
Expand Down Expand Up @@ -106,9 +106,8 @@ def add_tool(tool_name: str, path: Optional[str] = None, agents: Optional[List[s
tool_data = ToolConfig.from_tool_name(tool_name)
tool_file_path = tool_data.get_impl_file_path(framework)


if tool_data.packages:
os.system(f"poetry add {' '.join(tool_data.packages)}") # Install packages
packaging.install(' '.join(tool_data.packages))
shutil.copy(tool_file_path, f'{path}src/tools/{tool_name}_tool.py') # Move tool from package to project
add_tool_to_tools_init(tool_data, path) # Export tool from tools dir
add_tool_to_agent_definition(framework=framework, tool_data=tool_data, path=path, agents=agents) # Add tool to agent definition
Expand Down Expand Up @@ -147,7 +146,7 @@ def remove_tool(tool_name: str, path: Optional[str] = None):

tool_data = ToolConfig.from_tool_name(tool_name)
if tool_data.packages:
os.system(f"poetry remove {' '.join(tool_data.packages)}") # Uninstall packages
packaging.remove(' '.join(tool_data.packages))
try:
os.remove(f'{path}src/tools/{tool_name}_tool.py')
except FileNotFoundError:
Expand Down
6 changes: 6 additions & 0 deletions agentstack/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from agentstack.telemetry import track_cli_command
from agentstack.utils import get_version, get_framework
import agentstack.generation as generation
from agentstack.update import check_for_updates

import webbrowser

Expand Down Expand Up @@ -77,6 +78,8 @@ def main():
tools_remove_parser = tools_subparsers.add_parser('remove', aliases=['r'], help='Remove a tool')
tools_remove_parser.add_argument('name', help='Name of the tool to remove')

update = subparsers.add_parser('update', aliases=['u'], help='Check for updates')

# Parse arguments
args = parser.parse_args()

Expand All @@ -86,6 +89,7 @@ def main():
return

track_cli_command(args.command)
check_for_updates(update_requested=args.command in ('update', 'u'))

# Handle commands
if args.command in ['docs']:
Expand Down Expand Up @@ -120,6 +124,8 @@ def main():
generation.remove_tool(args.name)
else:
tools_parser.print_help()
elif args.command in ['update', 'u']:
pass # Update check already done
else:
parser.print_help()

Expand Down
15 changes: 15 additions & 0 deletions agentstack/packaging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os
from typing import Optional

PACKAGING_CMD = "poetry"

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

def remove(package: str):
os.system(f"{PACKAGING_CMD} remove {package}")

def upgrade(package: str):
os.system(f"{PACKAGING_CMD} add {package}")
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ license = "{{cookiecutter.project_metadata.license}}"

[tool.poetry.dependencies]
python = ">=3.10,<=3.13"
agentops = "^0.3.12"
crewai = "^0.63.6"
crewai-tools= "0.12.1"
python-dotenv="1.0.1"

[project.scripts]
{{cookiecutter.project_metadata.project_name}} = "{{cookiecutter.project_metadata.project_name}}.main:run"
Expand Down
129 changes: 129 additions & 0 deletions agentstack/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import json
import os, sys
import time
from pathlib import Path
from packaging.version import parse as parse_version, Version
import inquirer
from agentstack.utils import term_color, get_version, get_framework
from agentstack import packaging
from appdirs import user_data_dir

AGENTSTACK_PACKAGE = 'agentstack'


def _is_ci_environment():
"""Detect if we're running in a CI environment"""
ci_env_vars = [
'CI',
'GITHUB_ACTIONS',
'GITLAB_CI',
'TRAVIS',
'CIRCLECI',
'JENKINS_URL',
'TEAMCITY_VERSION'
]
return any(os.getenv(var) for var in ci_env_vars)


# Try to get appropriate directory for storing update file
try:
base_dir = Path(user_data_dir("agentstack", "agency"))
# Test if we can write to directory
test_file = base_dir / '.test_write_permission'
test_file.touch()
test_file.unlink()
except (RuntimeError, OSError, PermissionError):
# In CI or when directory is not writable, use temp directory
base_dir = Path(os.getenv('TEMP', '/tmp'))

LAST_CHECK_FILE_PATH = base_dir / ".cli-last-update"
INSTALL_PATH = Path(sys.executable).parent.parent
ENDPOINT_URL = "https://pypi.org/simple"
CHECK_EVERY = 3600 # hour


def get_latest_version(package: str) -> Version:
"""Get version information from PyPi to save a full package manager invocation"""
import requests # defer import until we know we need it
response = requests.get(f"{ENDPOINT_URL}/{package}/", headers={"Accept": "application/vnd.pypi.simple.v1+json"})
if response.status_code != 200:
raise Exception(f"Failed to fetch package data from pypi.")
data = response.json()
return parse_version(data['versions'][-1])


def load_update_data():
"""Load existing update data or return empty dict if file doesn't exist"""
if Path(LAST_CHECK_FILE_PATH).exists():
try:
with open(LAST_CHECK_FILE_PATH, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, PermissionError):
return {}
return {}


def should_update() -> bool:
"""Has it been longer than CHECK_EVERY since the last update check?"""
# Always check for updates in CI
if _is_ci_environment():
return True

data = load_update_data()
last_check = data.get(str(INSTALL_PATH))

if not last_check:
return True

return time.time() - float(last_check) > CHECK_EVERY


def record_update_check():
"""Save current timestamp for this installation"""
# Don't record updates in CI
if _is_ci_environment():
return

try:
data = load_update_data()
data[str(INSTALL_PATH)] = time.time()

# Create directory if it doesn't exist
LAST_CHECK_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)

with open(LAST_CHECK_FILE_PATH, 'w') as f:
json.dump(data, f, indent=2)
except (OSError, PermissionError):
# Silently fail in CI or when we can't write
pass


def check_for_updates(update_requested: bool = False):
"""
`update_requested` indicates the user has explicitly requested an update.
"""
if not update_requested and not should_update():
return

print("Checking for updates...\n")

try:
latest_version: Version = get_latest_version(AGENTSTACK_PACKAGE)
except Exception as e:
print(term_color("Failed to retrieve package index.", 'red'))
return

installed_version: Version = parse_version(get_version(AGENTSTACK_PACKAGE))
if latest_version > installed_version:
print('') # newline
if inquirer.confirm(f"New version of {AGENTSTACK_PACKAGE} available: {latest_version}! Do you want to install?"):
packaging.upgrade(f'{AGENTSTACK_PACKAGE}[{get_framework()}]')
print(term_color(f"{AGENTSTACK_PACKAGE} updated. Re-run your command to use the latest version.", 'green'))
sys.exit(0)
else:
print(term_color("Skipping update. Run `agentstack update` to install the latest version.", 'blue'))
else:
print(f"{AGENTSTACK_PACKAGE} is up to date ({installed_version})")

record_update_check()

4 changes: 2 additions & 2 deletions agentstack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
from pathlib import Path
import importlib.resources

def get_version():
def get_version(package: str = 'agentstack'):
try:
return version('agentstack')
return version(package)
except (KeyError, FileNotFoundError) as e:
print(e)
return "Unknown version"
Expand Down
Loading
Loading