Skip to content

Commit 5fcb194

Browse files
authored
Merge pull request #155 from tcdent/logging
Add extensible log handler
2 parents af7403d + 718cd28 commit 5fcb194

File tree

25 files changed

+538
-196
lines changed

25 files changed

+538
-196
lines changed

agentstack/agents.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pydantic
55
from ruamel.yaml import YAML, YAMLError
66
from ruamel.yaml.scalarstring import FoldedScalarString
7-
from agentstack import conf
7+
from agentstack import conf, log
88
from agentstack.exceptions import ValidationError
99

1010

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

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

8182
with open(filename, 'r') as f:
@@ -96,6 +97,7 @@ def __exit__(self, *args):
9697
def get_all_agent_names() -> list[str]:
9798
filename = conf.PATH / AGENTS_FILENAME
9899
if not os.path.exists(filename):
100+
log.debug(f"Project does not have an {AGENTS_FILENAME} file.")
99101
return []
100102
with open(filename, 'r') as f:
101103
data = yaml.load(f) or {}

agentstack/auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import inquirer
1111
from appdirs import user_data_dir
12-
from agentstack.logger import log
12+
from agentstack import log
1313

1414

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

@@ -120,7 +120,7 @@ def login():
120120
server.shutdown()
121121
server_thread.join()
122122

123-
print("🔐 Authentication successful! Token has been stored.")
123+
log.success("🔐 Authentication successful! Token has been stored.")
124124
return True
125125

126126
except Exception as e:

agentstack/cli/agentstack_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Optional
44

55
from agentstack.utils import clean_input, get_version
6-
from agentstack.logger import log
6+
from agentstack import log
77

88

99
class ProjectMetadata:

agentstack/cli/cli.py

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
ProjectStructure,
1717
CookiecutterData,
1818
)
19-
from agentstack.logger import log
20-
from agentstack import conf
19+
from agentstack import conf, log
2120
from agentstack.conf import ConfigFile
2221
from agentstack.utils import get_package_path
2322
from agentstack.generation.files import ProjectFile
@@ -60,14 +59,12 @@ def init_project_builder(
6059
try:
6160
template_data = TemplateConfig.from_url(template)
6261
except Exception as e:
63-
print(term_color(f"Failed to fetch template data from {template}.\n{e}", 'red'))
64-
sys.exit(1)
62+
raise Exception(f"Failed to fetch template data from {template}.\n{e}")
6563
else:
6664
try:
6765
template_data = TemplateConfig.from_template_name(template)
6866
except Exception as e:
69-
print(term_color(f"Failed to load template {template}.\n{e}", 'red'))
70-
sys.exit(1)
67+
raise Exception(f"Failed to load template {template}.\n{e}")
7168

7269
if template_data:
7370
project_details = {
@@ -118,35 +115,37 @@ def init_project_builder(
118115

119116

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

126123
# Print the welcome message with ASCII art
127-
print(title)
128-
print(border)
129-
print(tagline)
130-
print(border)
124+
log.info(title)
125+
log.info(border)
126+
log.info(tagline)
127+
log.info(border)
131128

132129

133130
def configure_default_model():
134131
"""Set the default model"""
135132
agentstack_config = ConfigFile()
136133
if agentstack_config.default_model:
134+
log.debug("Using default model from project config.")
137135
return # Default model already set
138136

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

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

148+
log.debug("Writing default model to project config.")
150149
with ConfigFile() as agentstack_config:
151150
agentstack_config.default_model = model
152151

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

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

177176
return framework
178177

@@ -192,16 +191,13 @@ def get_validated_input(
192191
snake_case: Whether to enforce snake_case naming
193192
"""
194193
while True:
195-
try:
196-
value = inquirer.text(
197-
message=message,
198-
validate=validate_func or validator_not_empty(min_length) if min_length else None,
199-
)
200-
if snake_case and not is_snake_case(value):
201-
raise ValidationError("Input must be in snake_case")
202-
return value
203-
except ValidationError as e:
204-
print(term_color(f"Error: {str(e)}", 'red'))
194+
value = inquirer.text(
195+
message=message,
196+
validate=validate_func or validator_not_empty(min_length) if min_length else None,
197+
)
198+
if snake_case and not is_snake_case(value):
199+
raise ValidationError("Input must be in snake_case")
200+
return value
205201

206202

207203
def ask_agent_details():
@@ -331,10 +327,10 @@ def ask_tools() -> list:
331327

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

334-
print("Adding tools:")
330+
log.info("Adding tools:")
335331
for t in tools_to_add:
336-
print(f' - {t}')
337-
print('')
332+
log.info(f' - {t}')
333+
log.info('')
338334
adding_tools = inquirer.confirm("Add another tool?")
339335

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

346342
if not is_snake_case(name):
347-
print(term_color("Project name must be snake case", 'red'))
343+
log.error("Project name must be snake case")
348344
return ask_project_details(slug_name)
349345

350346
questions = inquirer.prompt(
@@ -404,16 +400,7 @@ def insert_template(
404400
f'{template_path}/{"{{cookiecutter.project_metadata.project_slug}}"}/.env.example',
405401
f'{template_path}/{"{{cookiecutter.project_metadata.project_slug}}"}/.env',
406402
)
407-
408-
# if os.path.isdir(project_details['name']):
409-
# print(
410-
# term_color(
411-
# f"Directory {template_path} already exists. Please check this and try again",
412-
# "red",
413-
# )
414-
# )
415-
# sys.exit(1)
416-
403+
417404
cookiecutter(str(template_path), no_input=True, extra_context=None)
418405

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

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

498484
try:
499485
template.write_to_file(conf.PATH / output_filename)
500-
print(term_color(f"Template saved to: {conf.PATH / output_filename}", 'green'))
486+
log.success(f"Template saved to: {conf.PATH / output_filename}")
501487
except Exception as e:
502-
print(term_color(f"Failed to write template to file: {e}", 'red'))
503-
sys.exit(1)
488+
raise Exception(f"Failed to write template to file: {e}")

agentstack/cli/init.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os, sys
22
from typing import Optional
33
from pathlib import Path
4-
from agentstack import conf
4+
from agentstack import conf, log
5+
from agentstack.exceptions import EnvironmentError
56
from agentstack import packaging
67
from agentstack.cli import welcome_message, init_project_builder
78
from agentstack.utils import term_color
@@ -15,14 +16,14 @@ def require_uv():
1516
uv_bin = packaging.get_uv_bin()
1617
assert os.path.exists(uv_bin)
1718
except (AssertionError, ImportError):
18-
print(term_color("Error: uv is not installed.", 'red'))
19-
print("Full installation instructions at: https://docs.astral.sh/uv/getting-started/installation")
19+
message = "Error: uv is not installed.\n"
20+
message += "Full installation instructions at: https://docs.astral.sh/uv/getting-started/installation\n"
2021
match sys.platform:
2122
case 'linux' | 'darwin':
22-
print("Hint: run `curl -LsSf https://astral.sh/uv/install.sh | sh`")
23+
message += "Hint: run `curl -LsSf https://astral.sh/uv/install.sh | sh`\n"
2324
case _:
2425
pass
25-
sys.exit(1)
26+
raise EnvironmentError(message)
2627

2728

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

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

5452
welcome_message()
55-
print(term_color("🦾 Creating a new AgentStack project...", 'blue'))
56-
print(f"Using project directory: {conf.PATH.absolute()}")
53+
log.notify("🦾 Creating a new AgentStack project...")
54+
log.info(f"Using project directory: {conf.PATH.absolute()}")
5755

5856
# copy the project skeleton, create a virtual environment, and install dependencies
5957
init_project_builder(slug_name, template, use_wizard)
6058
packaging.create_venv()
6159
packaging.install_project()
6260

63-
print(
64-
"\n"
65-
"🚀 \033[92mAgentStack project generated successfully!\033[0m\n\n"
61+
log.success("🚀 AgentStack project generated successfully!\n")
62+
log.info(
6663
" To get started, activate the virtual environment with:\n"
6764
f" cd {conf.PATH}\n"
6865
" source .venv/bin/activate\n\n"

agentstack/cli/run.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
import importlib.util
66
from dotenv import load_dotenv
77

8-
from agentstack import conf
8+
from agentstack import conf, log
99
from agentstack.exceptions import ValidationError
1010
from agentstack import inputs
1111
from agentstack import frameworks
12-
from agentstack.utils import term_color, get_framework
12+
from agentstack.utils import term_color, get_framework, verify_agentstack_project
1313

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

1717

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

7377

7478
def _import_project_module(path: Path):
@@ -89,41 +93,36 @@ def _import_project_module(path: Path):
8993
return project_module
9094

9195

92-
def run_project(command: str = 'run', debug: bool = False, cli_args: Optional[str] = None):
96+
def run_project(command: str = 'run', cli_args: Optional[str] = None):
9397
"""Validate that the project is ready to run and then run it."""
98+
verify_agentstack_project()
99+
94100
if conf.get_framework() not in frameworks.SUPPORTED_FRAMEWORKS:
95-
print(term_color(f"Framework {conf.get_framework()} is not supported by agentstack.", 'red'))
96-
sys.exit(1)
101+
raise ValidationError(f"Framework {conf.get_framework()} is not supported by agentstack.")
97102

98103
try:
99104
frameworks.validate_project()
100105
except ValidationError as e:
101-
print(term_color(f"Project validation failed:\n{e}", 'red'))
102-
sys.exit(1)
106+
raise e
103107

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

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

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

0 commit comments

Comments
 (0)