Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b37f6f5
Add extensible log handler, redirect logs to a file in the user's pro…
tcdent Dec 19, 2024
0de9e47
Fix custom log levels, expand & cleanup tests.
tcdent Dec 20, 2024
8b712cb
Thoughts on additional log levels.
tcdent Dec 20, 2024
575cf98
Merge branch 'main' into logging
bboynton97 Dec 22, 2024
fe9172d
merge telemetry and logging
bboynton97 Dec 22, 2024
d0d8177
No more sys.exit outside of main.py. Raise exceptions on internal err…
tcdent Dec 30, 2024
e69384a
repo org update
bboynton97 Dec 22, 2024
773e017
sticker pack
bboynton97 Dec 22, 2024
38223e2
authenticate CLI with agentstack account
bboynton97 Dec 22, 2024
3e8e4a1
Fixed a bug, if entered agent name was empty.
tkrevh Dec 17, 2024
c677b33
Added tests for new functions.
tkrevh Dec 17, 2024
dcd9814
Moved CLI_ENTRY out of individual test files to cli_test_utils as it …
tkrevh Dec 19, 2024
3ecedb7
Environment variables are written commented-out if no value is set.
tcdent Dec 23, 2024
e1d2733
Update tool configs to use null values for placeholder environment va…
tcdent Dec 23, 2024
25aaa3d
Don't override `false`` values as None when re-parsing env vars
tcdent Dec 23, 2024
a88dba9
Document for project structure and tasks leading to 0.3 release
tcdent Dec 11, 2024
2e27aa0
Update project structure docs with progress made and future plans
tcdent Dec 13, 2024
b716ec8
Update v0.3 roadmap.
tcdent Dec 20, 2024
a0983d3
telem with user token
bboynton97 Dec 26, 2024
eef36ad
update footer social links to point to agentstack socials
tnguyen21 Dec 29, 2024
eae05ba
Merge regression
tcdent Dec 31, 2024
c795fab
Merge branch 'main' into logging
tcdent Dec 31, 2024
39e75ba
Missing imports
tcdent Dec 31, 2024
bfe6b0f
Correct main interaction to support installed binary (`main.main` get…
tcdent Dec 31, 2024
295b573
Prevent loggers from other modules from affecting the agenrstack logg…
tcdent Dec 31, 2024
4e80428
Resolve #175
tcdent Dec 31, 2024
29a562d
Fix main entrypoint to have congruency between module usage and bin s…
tcdent Dec 31, 2024
6081e04
Comments cleanup
tcdent Dec 31, 2024
c93b58e
Merge branch 'main' into logging
tcdent Jan 10, 2025
7b4bc54
Only write to log files that already exist. This prevents us from pre…
tcdent Jan 10, 2025
b6790fd
Migrate print statements to use agentstack.log
tcdent Jan 10, 2025
75b9411
Typo.
tcdent Jan 10, 2025
718cd28
Error message newlines.
tcdent Jan 10, 2025
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
4 changes: 3 additions & 1 deletion agentstack/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pydantic
from ruamel.yaml import YAML, YAMLError
from ruamel.yaml.scalarstring import FoldedScalarString
from agentstack import conf
from agentstack import conf, log
from agentstack.exceptions import ValidationError


Expand Down Expand Up @@ -76,6 +76,7 @@ def model_dump(self, *args, **kwargs) -> dict:
return {self.name: dump}

def write(self):
log.debug(f"Writing agent {self.name} to {AGENTS_FILENAME}")
filename = conf.PATH / AGENTS_FILENAME

with open(filename, 'r') as f:
Expand All @@ -96,6 +97,7 @@ def __exit__(self, *args):
def get_all_agent_names() -> list[str]:
filename = conf.PATH / AGENTS_FILENAME
if not os.path.exists(filename):
log.debug(f"Project does not have an {AGENTS_FILENAME} file.")
return []
with open(filename, 'r') as f:
data = yaml.load(f) or {}
Expand Down
6 changes: 3 additions & 3 deletions agentstack/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import inquirer
from appdirs import user_data_dir
from agentstack.logger import log
from agentstack import log


try:
Expand Down Expand Up @@ -95,7 +95,7 @@ def login():
# check if already logged in
token = get_stored_token()
if token:
print("You are already authenticated!")
log.success("You are already authenticated!")
if not inquirer.confirm('Would you like to log in with a different account?'):
return

Expand All @@ -120,7 +120,7 @@ def login():
server.shutdown()
server_thread.join()

print("🔐 Authentication successful! Token has been stored.")
log.success("🔐 Authentication successful! Token has been stored.")
return True

except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion agentstack/cli/agentstack_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Optional

from agentstack.utils import clean_input, get_version
from agentstack.logger import log
from agentstack import log


class ProjectMetadata:
Expand Down
71 changes: 28 additions & 43 deletions agentstack/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
ProjectStructure,
CookiecutterData,
)
from agentstack.logger import log
from agentstack import conf
from agentstack import conf, log
from agentstack.conf import ConfigFile
from agentstack.utils import get_package_path
from agentstack.generation.files import ProjectFile
Expand Down Expand Up @@ -60,14 +59,12 @@ def init_project_builder(
try:
template_data = TemplateConfig.from_url(template)
except Exception as e:
print(term_color(f"Failed to fetch template data from {template}.\n{e}", 'red'))
sys.exit(1)
raise Exception(f"Failed to fetch template data from {template}.\n{e}")
else:
try:
template_data = TemplateConfig.from_template_name(template)
except Exception as e:
print(term_color(f"Failed to load template {template}.\n{e}", 'red'))
sys.exit(1)
raise Exception(f"Failed to load template {template}.\n{e}")

if template_data:
project_details = {
Expand Down Expand Up @@ -118,35 +115,37 @@ def init_project_builder(


def welcome_message():
os.system("cls" if os.name == "nt" else "clear")
#os.system("cls" if os.name == "nt" else "clear")
title = text2art("AgentStack", font="smisome1")
tagline = "The easiest way to build a robust agent application!"
border = "-" * len(tagline)

# Print the welcome message with ASCII art
print(title)
print(border)
print(tagline)
print(border)
log.info(title)
log.info(border)
log.info(tagline)
log.info(border)


def configure_default_model():
"""Set the default model"""
agentstack_config = ConfigFile()
if agentstack_config.default_model:
log.debug("Using default model from project config.")
return # Default model already set

print("Project does not have a default model configured.")
log.info("Project does not have a default model configured.")
other_msg = "Other (enter a model name)"
model = inquirer.list_input(
message="Which model would you like to use?",
choices=PREFERRED_MODELS + [other_msg],
)

if model == other_msg: # If the user selects "Other", prompt for a model name
print('A list of available models is available at: "https://docs.litellm.ai/docs/providers"')
log.info('A list of available models is available at: "https://docs.litellm.ai/docs/providers"')
model = inquirer.text(message="Enter the model name")

log.debug("Writing default model to project config.")
with ConfigFile() as agentstack_config:
agentstack_config.default_model = model

Expand All @@ -172,7 +171,7 @@ def ask_framework() -> str:
# choices=["CrewAI", "Autogen", "LiteLLM"],
# )

print("Congrats! Your project is ready to go! Quickly add features now or skip to do it later.\n\n")
log.success("Congrats! Your project is ready to go! Quickly add features now or skip to do it later.\n\n")

return framework

Expand All @@ -192,16 +191,13 @@ def get_validated_input(
snake_case: Whether to enforce snake_case naming
"""
while True:
try:
value = inquirer.text(
message=message,
validate=validate_func or validator_not_empty(min_length) if min_length else None,
)
if snake_case and not is_snake_case(value):
raise ValidationError("Input must be in snake_case")
return value
except ValidationError as e:
print(term_color(f"Error: {str(e)}", 'red'))
value = inquirer.text(
message=message,
validate=validate_func or validator_not_empty(min_length) if min_length else None,
)
if snake_case and not is_snake_case(value):
raise ValidationError("Input must be in snake_case")
return value


def ask_agent_details():
Expand Down Expand Up @@ -331,10 +327,10 @@ def ask_tools() -> list:

tools_to_add.append(tool_selection.split(' - ')[0])

print("Adding tools:")
log.info("Adding tools:")
for t in tools_to_add:
print(f' - {t}')
print('')
log.info(f' - {t}')
log.info('')
adding_tools = inquirer.confirm("Add another tool?")

return tools_to_add
Expand All @@ -344,7 +340,7 @@ def ask_project_details(slug_name: Optional[str] = None) -> dict:
name = inquirer.text(message="What's the name of your project (snake_case)", default=slug_name or '')

if not is_snake_case(name):
print(term_color("Project name must be snake case", 'red'))
log.error("Project name must be snake case")
return ask_project_details(slug_name)

questions = inquirer.prompt(
Expand Down Expand Up @@ -404,16 +400,7 @@ def insert_template(
f'{template_path}/{"{{cookiecutter.project_metadata.project_slug}}"}/.env.example',
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)


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

# TODO: inits a git repo in the directory the command was run in
Expand All @@ -434,8 +421,7 @@ def export_template(output_filename: str):
try:
metadata = ProjectFile()
except Exception as e:
print(term_color(f"Failed to load project metadata: {e}", 'red'))
sys.exit(1)
raise Exception(f"Failed to load project metadata: {e}")

# Read all the agents from the project's agents.yaml file
agents: list[TemplateConfig.Agent] = []
Expand Down Expand Up @@ -497,7 +483,6 @@ def export_template(output_filename: str):

try:
template.write_to_file(conf.PATH / output_filename)
print(term_color(f"Template saved to: {conf.PATH / output_filename}", 'green'))
log.success(f"Template saved to: {conf.PATH / output_filename}")
except Exception as e:
print(term_color(f"Failed to write template to file: {e}", 'red'))
sys.exit(1)
raise Exception(f"Failed to write template to file: {e}")
27 changes: 12 additions & 15 deletions agentstack/cli/init.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os, sys
from typing import Optional
from pathlib import Path
from agentstack import conf
from agentstack import conf, log
from agentstack.exceptions import EnvironmentError
from agentstack import packaging
from agentstack.cli import welcome_message, init_project_builder
from agentstack.utils import term_color
Expand All @@ -15,14 +16,14 @@ def require_uv():
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")
message = "Error: uv is not installed.\n"
message += "Full installation instructions at: https://docs.astral.sh/uv/getting-started/installation\n"
match sys.platform:
case 'linux' | 'darwin':
print("Hint: run `curl -LsSf https://astral.sh/uv/install.sh | sh`")
message += "Hint: run `curl -LsSf https://astral.sh/uv/install.sh | sh`\n"
case _:
pass
sys.exit(1)
raise EnvironmentError(message)


def init_project(
Expand All @@ -43,26 +44,22 @@ def init_project(
if slug_name:
conf.set_path(conf.PATH / slug_name)
else:
print("Error: No project directory specified.")
print("Run `agentstack init <project_name>`")
sys.exit(1)
raise Exception("Error: No project directory specified.\n Run `agentstack init <project_name>`")

if os.path.exists(conf.PATH): # cookiecutter requires the directory to not exist
print(f"Error: Directory already exists: {conf.PATH}")
sys.exit(1)
raise Exception(f"Error: Directory already exists: {conf.PATH}")

welcome_message()
print(term_color("🦾 Creating a new AgentStack project...", 'blue'))
print(f"Using project directory: {conf.PATH.absolute()}")
log.notify("🦾 Creating a new AgentStack project...")
log.info(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_project()

print(
"\n"
"🚀 \033[92mAgentStack project generated successfully!\033[0m\n\n"
log.success("🚀 AgentStack project generated successfully!\n")
log.info(
" To get started, activate the virtual environment with:\n"
f" cd {conf.PATH}\n"
" source .venv/bin/activate\n\n"
Expand Down
37 changes: 18 additions & 19 deletions agentstack/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
import importlib.util
from dotenv import load_dotenv

from agentstack import conf
from agentstack import conf, log
from agentstack.exceptions import ValidationError
from agentstack import inputs
from agentstack import frameworks
from agentstack.utils import term_color, get_framework
from agentstack.utils import term_color, get_framework, verify_agentstack_project

MAIN_FILENAME: Path = Path("src/main.py")
MAIN_MODULE_NAME = "main"


def _format_friendy_error_message(exception: Exception):
def _format_friendly_error_message(exception: Exception):
"""
Projects will throw various errors, especially on first runs, so we catch
them here and print a more helpful message.
Expand Down Expand Up @@ -68,7 +68,11 @@ def _format_friendy_error_message(exception: Exception):
"Ensure all tasks referenced in your code are defined in the tasks.yaml file."
)
case (_, _, _):
return f"{name}: {message}, {tracebacks[-1]}"
log.debug(
f"Unhandled exception; if this is a common error, consider adding it to "
f"`cli.run._format_friendly_error_message`. Exception: {exception}"
)
raise exception # re-raise the original exception so we preserve context


def _import_project_module(path: Path):
Expand All @@ -89,41 +93,36 @@ def _import_project_module(path: Path):
return project_module


def run_project(command: str = 'run', debug: bool = False, cli_args: Optional[str] = None):
def run_project(command: str = 'run', cli_args: Optional[str] = None):
"""Validate that the project is ready to run and then run it."""
verify_agentstack_project()

if conf.get_framework() not in frameworks.SUPPORTED_FRAMEWORKS:
print(term_color(f"Framework {conf.get_framework()} is not supported by agentstack.", 'red'))
sys.exit(1)
raise ValidationError(f"Framework {conf.get_framework()} is not supported by agentstack.")

try:
frameworks.validate_project()
except ValidationError as e:
print(term_color(f"Project validation failed:\n{e}", 'red'))
sys.exit(1)
raise e

# Parse extra --input-* arguments for runtime overrides of the project's inputs
if cli_args:
for arg in cli_args:
if not arg.startswith('--input-'):
continue
key, value = arg[len('--input-') :].split('=')
log.debug(f"Using CLI input override: {key}={value}")
inputs.add_input_for_run(key, value)

load_dotenv(Path.home() / '.env') # load the user's .env file
load_dotenv(conf.PATH / '.env', override=True) # load the project's .env file

# import src/main.py from the project path and run `command` from the project's main.py
try:
print("Running your agent...")
log.notify("Running your agent...")
project_main = _import_project_module(conf.PATH)
getattr(project_main, command)()
except ImportError as e:
print(term_color(f"Failed to import project. Does '{MAIN_FILENAME}' exist?:\n{e}", 'red'))
sys.exit(1)
except Exception as exception:
if debug:
raise exception
print(term_color("\nAn error occurred while running your project:\n", 'red'))
print(_format_friendy_error_message(exception))
print(term_color("\nRun `agentstack run --debug` for a full traceback.", 'blue'))
sys.exit(1)
raise ValidationError(f"Failed to import project. Does '{MAIN_FILENAME}' exist?:\n{e}")
except Exception as e:
raise Exception(_format_friendly_error_message(e))
Loading
Loading