Skip to content

Commit

Permalink
Merge pull request #155 from tcdent/logging
Browse files Browse the repository at this point in the history
Add extensible log handler
  • Loading branch information
bboynton97 authored Jan 10, 2025
2 parents af7403d + 718cd28 commit 5fcb194
Show file tree
Hide file tree
Showing 25 changed files with 538 additions and 196 deletions.
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

0 comments on commit 5fcb194

Please sign in to comment.