Skip to content

Commit 27b2330

Browse files
authored
Merge pull request #72 from tcdent/updater
Updater and abstraction for package management
2 parents f09dde7 + 545fe02 commit 27b2330

10 files changed

Lines changed: 791 additions & 829 deletions

File tree

agentstack/cli/cli.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
from agentstack.utils import get_package_path
1919
from agentstack.generation.files import ConfigFile
2020
from agentstack.generation.tool_generation import get_all_tools
21-
from .. import generation
22-
from ..utils import open_json_file, term_color, is_snake_case
21+
from agentstack import packaging, generation
22+
from agentstack.utils import open_json_file, term_color, is_snake_case
23+
from agentstack.update import AGENTSTACK_PACKAGE
2324

2425
PREFERRED_MODELS = [
2526
'openai/gpt-4o',
@@ -91,7 +92,7 @@ def init_project_builder(slug_name: Optional[str] = None, template: Optional[str
9192
"license": "MIT"
9293
}
9394

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

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

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

114120
def welcome_message():
115121
os.system("cls" if os.name == "nt" else "clear")

agentstack/generation/tool_generation.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
import ast
1717
from pydantic import BaseModel, ValidationError
1818

19+
from agentstack import packaging
1920
from agentstack.utils import get_package_path
2021
from agentstack.generation.files import ConfigFile, EnvFile
2122
from .gen_utils import insert_code_after_tag, string_in_file
2223
from ..utils import open_json_file, get_framework, term_color
2324

2425

2526
TOOL_INIT_FILENAME = "src/tools/__init__.py"
26-
2727
FRAMEWORK_FILENAMES: dict[str, str] = {
2828
'crewai': 'src/crew.py',
2929
}
@@ -106,9 +106,8 @@ def add_tool(tool_name: str, path: Optional[str] = None, agents: Optional[List[s
106106
tool_data = ToolConfig.from_tool_name(tool_name)
107107
tool_file_path = tool_data.get_impl_file_path(framework)
108108

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

148147
tool_data = ToolConfig.from_tool_name(tool_name)
149148
if tool_data.packages:
150-
os.system(f"poetry remove {' '.join(tool_data.packages)}") # Uninstall packages
149+
packaging.remove(' '.join(tool_data.packages))
151150
try:
152151
os.remove(f'{path}src/tools/{tool_name}_tool.py')
153152
except FileNotFoundError:

agentstack/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from agentstack.telemetry import track_cli_command
77
from agentstack.utils import get_version, get_framework
88
import agentstack.generation as generation
9+
from agentstack.update import check_for_updates
910

1011
import webbrowser
1112

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

81+
update = subparsers.add_parser('update', aliases=['u'], help='Check for updates')
82+
8083
# Parse arguments
8184
args = parser.parse_args()
8285

@@ -86,6 +89,7 @@ def main():
8689
return
8790

8891
track_cli_command(args.command)
92+
check_for_updates(update_requested=args.command in ('update', 'u'))
8993

9094
# Handle commands
9195
if args.command in ['docs']:
@@ -120,6 +124,8 @@ def main():
120124
generation.remove_tool(args.name)
121125
else:
122126
tools_parser.print_help()
127+
elif args.command in ['update', 'u']:
128+
pass # Update check already done
123129
else:
124130
parser.print_help()
125131

agentstack/packaging.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import os
2+
from typing import Optional
3+
4+
PACKAGING_CMD = "poetry"
5+
6+
def install(package: str, path: Optional[str] = None):
7+
if path:
8+
os.chdir(path)
9+
os.system(f"{PACKAGING_CMD} add {package}")
10+
11+
def remove(package: str):
12+
os.system(f"{PACKAGING_CMD} remove {package}")
13+
14+
def upgrade(package: str):
15+
os.system(f"{PACKAGING_CMD} add {package}")

agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/pyproject.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ license = "{{cookiecutter.project_metadata.license}}"
77

88
[tool.poetry.dependencies]
99
python = ">=3.10,<=3.13"
10-
agentops = "^0.3.12"
11-
crewai = "^0.63.6"
12-
crewai-tools= "0.12.1"
13-
python-dotenv="1.0.1"
1410

1511
[project.scripts]
1612
{{cookiecutter.project_metadata.project_name}} = "{{cookiecutter.project_metadata.project_name}}.main:run"

agentstack/update.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import json
2+
import os, sys
3+
import time
4+
from pathlib import Path
5+
from packaging.version import parse as parse_version, Version
6+
import inquirer
7+
from agentstack.utils import term_color, get_version, get_framework
8+
from agentstack import packaging
9+
from appdirs import user_data_dir
10+
11+
AGENTSTACK_PACKAGE = 'agentstack'
12+
13+
14+
def _is_ci_environment():
15+
"""Detect if we're running in a CI environment"""
16+
ci_env_vars = [
17+
'CI',
18+
'GITHUB_ACTIONS',
19+
'GITLAB_CI',
20+
'TRAVIS',
21+
'CIRCLECI',
22+
'JENKINS_URL',
23+
'TEAMCITY_VERSION'
24+
]
25+
return any(os.getenv(var) for var in ci_env_vars)
26+
27+
28+
# Try to get appropriate directory for storing update file
29+
try:
30+
base_dir = Path(user_data_dir("agentstack", "agency"))
31+
# Test if we can write to directory
32+
test_file = base_dir / '.test_write_permission'
33+
test_file.touch()
34+
test_file.unlink()
35+
except (RuntimeError, OSError, PermissionError):
36+
# In CI or when directory is not writable, use temp directory
37+
base_dir = Path(os.getenv('TEMP', '/tmp'))
38+
39+
LAST_CHECK_FILE_PATH = base_dir / ".cli-last-update"
40+
INSTALL_PATH = Path(sys.executable).parent.parent
41+
ENDPOINT_URL = "https://pypi.org/simple"
42+
CHECK_EVERY = 3600 # hour
43+
44+
45+
def get_latest_version(package: str) -> Version:
46+
"""Get version information from PyPi to save a full package manager invocation"""
47+
import requests # defer import until we know we need it
48+
response = requests.get(f"{ENDPOINT_URL}/{package}/", headers={"Accept": "application/vnd.pypi.simple.v1+json"})
49+
if response.status_code != 200:
50+
raise Exception(f"Failed to fetch package data from pypi.")
51+
data = response.json()
52+
return parse_version(data['versions'][-1])
53+
54+
55+
def load_update_data():
56+
"""Load existing update data or return empty dict if file doesn't exist"""
57+
if Path(LAST_CHECK_FILE_PATH).exists():
58+
try:
59+
with open(LAST_CHECK_FILE_PATH, 'r') as f:
60+
return json.load(f)
61+
except (json.JSONDecodeError, PermissionError):
62+
return {}
63+
return {}
64+
65+
66+
def should_update() -> bool:
67+
"""Has it been longer than CHECK_EVERY since the last update check?"""
68+
# Always check for updates in CI
69+
if _is_ci_environment():
70+
return True
71+
72+
data = load_update_data()
73+
last_check = data.get(str(INSTALL_PATH))
74+
75+
if not last_check:
76+
return True
77+
78+
return time.time() - float(last_check) > CHECK_EVERY
79+
80+
81+
def record_update_check():
82+
"""Save current timestamp for this installation"""
83+
# Don't record updates in CI
84+
if _is_ci_environment():
85+
return
86+
87+
try:
88+
data = load_update_data()
89+
data[str(INSTALL_PATH)] = time.time()
90+
91+
# Create directory if it doesn't exist
92+
LAST_CHECK_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
93+
94+
with open(LAST_CHECK_FILE_PATH, 'w') as f:
95+
json.dump(data, f, indent=2)
96+
except (OSError, PermissionError):
97+
# Silently fail in CI or when we can't write
98+
pass
99+
100+
101+
def check_for_updates(update_requested: bool = False):
102+
"""
103+
`update_requested` indicates the user has explicitly requested an update.
104+
"""
105+
if not update_requested and not should_update():
106+
return
107+
108+
print("Checking for updates...\n")
109+
110+
try:
111+
latest_version: Version = get_latest_version(AGENTSTACK_PACKAGE)
112+
except Exception as e:
113+
print(term_color("Failed to retrieve package index.", 'red'))
114+
return
115+
116+
installed_version: Version = parse_version(get_version(AGENTSTACK_PACKAGE))
117+
if latest_version > installed_version:
118+
print('') # newline
119+
if inquirer.confirm(f"New version of {AGENTSTACK_PACKAGE} available: {latest_version}! Do you want to install?"):
120+
packaging.upgrade(f'{AGENTSTACK_PACKAGE}[{get_framework()}]')
121+
print(term_color(f"{AGENTSTACK_PACKAGE} updated. Re-run your command to use the latest version.", 'green'))
122+
sys.exit(0)
123+
else:
124+
print(term_color("Skipping update. Run `agentstack update` to install the latest version.", 'blue'))
125+
else:
126+
print(f"{AGENTSTACK_PACKAGE} is up to date ({installed_version})")
127+
128+
record_update_check()
129+

agentstack/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
from pathlib import Path
99
import importlib.resources
1010

11-
def get_version():
11+
def get_version(package: str = 'agentstack'):
1212
try:
13-
return version('agentstack')
13+
return version(package)
1414
except (KeyError, FileNotFoundError) as e:
1515
print(e)
1616
return "Unknown version"

0 commit comments

Comments
 (0)