Skip to content

Commit

Permalink
Merge pull request #132 from tcdent/path
Browse files Browse the repository at this point in the history
Make PATH part of global state.
  • Loading branch information
tcdent authored Dec 13, 2024
2 parents f9b049f + ad45c87 commit 37f3a44
Show file tree
Hide file tree
Showing 31 changed files with 447 additions and 437 deletions.
5 changes: 3 additions & 2 deletions agentstack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
Methods that have been imported into this file are expected to be used by the
end user inside of their project.
"""
from agentstack.exceptions import ValidationError
from pathlib import Path
from agentstack import conf
from agentstack.inputs import get_inputs

___all___ = [
"ValidationError",
"conf",
"get_inputs",
]

25 changes: 9 additions & 16 deletions agentstack/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import pydantic
from ruamel.yaml import YAML, YAMLError
from ruamel.yaml.scalarstring import FoldedScalarString
from agentstack import ValidationError
from agentstack import conf
from agentstack.exceptions import ValidationError


AGENTS_FILENAME: Path = Path("src/config/agents.yaml")
Expand Down Expand Up @@ -46,11 +47,8 @@ class AgentConfig(pydantic.BaseModel):
backstory: str = ""
llm: str = ""

def __init__(self, name: str, path: Optional[Path] = None):
if not path:
path = Path()

filename = path / AGENTS_FILENAME
def __init__(self, name: str):
filename = conf.PATH / AGENTS_FILENAME
if not os.path.exists(filename):
os.makedirs(filename.parent, exist_ok=True)
filename.touch()
Expand All @@ -69,9 +67,6 @@ def __init__(self, name: str, path: Optional[Path] = None):
error_str += f"{' '.join([str(loc) for loc in error['loc']])}: {error['msg']}\n"
raise ValidationError(f"Error loading agent {name} from {filename}.\n{error_str}")

# store the path *after* loading data
self._path = path

def model_dump(self, *args, **kwargs) -> dict:
dump = super().model_dump(*args, **kwargs)
dump.pop('name') # name is the key, so keep it out of the data
Expand All @@ -81,7 +76,7 @@ def model_dump(self, *args, **kwargs) -> dict:
return {self.name: dump}

def write(self):
filename = self._path / AGENTS_FILENAME
filename = conf.PATH / AGENTS_FILENAME

with open(filename, 'r') as f:
data = yaml.load(f) or {}
Expand All @@ -98,16 +93,14 @@ def __exit__(self, *args):
self.write()


def get_all_agent_names(path: Optional[Path] = None) -> list[str]:
if not path:
path = Path()
filename = path / AGENTS_FILENAME
def get_all_agent_names() -> list[str]:
filename = conf.PATH / AGENTS_FILENAME
if not os.path.exists(filename):
return []
with open(filename, 'r') as f:
data = yaml.load(f) or {}
return list(data.keys())


def get_all_agents(path: Optional[Path] = None) -> list[AgentConfig]:
return [AgentConfig(name, path) for name in get_all_agent_names(path)]
def get_all_agents() -> list[AgentConfig]:
return [AgentConfig(name) for name in get_all_agent_names()]
39 changes: 21 additions & 18 deletions agentstack/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
CookiecutterData,
)
from agentstack.logger import log
from agentstack import conf
from agentstack.conf import ConfigFile
from agentstack.utils import get_package_path
from agentstack.tools import get_all_tools
from agentstack.generation.files import ConfigFile, ProjectFile
from agentstack.generation.files import ProjectFile
from agentstack import frameworks
from agentstack import generation
from agentstack import inputs
Expand Down Expand Up @@ -117,9 +119,12 @@ def init_project_builder(

log.debug(f"project_details: {project_details}" f"framework: {framework}" f"design: {design}")
insert_template(project_details, framework, design, template_data)
path = Path(project_details['name'])

# we have an agentstack.json file in the directory now
conf.set_path(project_details['name'])

for tool_data in tools:
generation.add_tool(tool_data['name'], agents=tool_data['agents'], path=path)
generation.add_tool(tool_data['name'], agents=tool_data['agents'])


def welcome_message():
Expand All @@ -135,9 +140,9 @@ def welcome_message():
print(border)


def configure_default_model(path: Optional[str] = None):
def configure_default_model():
"""Set the default model"""
agentstack_config = ConfigFile(path)
agentstack_config = ConfigFile()
if agentstack_config.default_model:
return # Default model already set

Expand All @@ -152,7 +157,7 @@ def configure_default_model(path: Optional[str] = None):
print('A list of available models is available at: "https://docs.litellm.ai/docs/providers"')
model = inquirer.text(message="Enter the model name")

with ConfigFile(path) as agentstack_config:
with ConfigFile() as agentstack_config:
agentstack_config.default_model = model


Expand Down Expand Up @@ -385,6 +390,7 @@ def insert_template(
template_path = get_package_path() / f'templates/{framework.name}'
with open(f"{template_path}/cookiecutter.json", "w") as json_file:
json.dump(cookiecutter_data.to_dict(), json_file)
# TODO this should not be written to the package directory

# copy .env.example to .env
shutil.copy(
Expand Down Expand Up @@ -453,22 +459,19 @@ def list_tools():
print(" https://docs.agentstack.sh/tools/core")


def export_template(output_filename: str, path: str = ''):
def export_template(output_filename: str):
"""
Export the current project as a template.
"""
_path = Path(path)
framework = get_framework(_path)

try:
metadata = ProjectFile(_path)
metadata = ProjectFile()
except Exception as e:
print(term_color(f"Failed to load project metadata: {e}", 'red'))
sys.exit(1)

# Read all the agents from the project's agents.yaml file
agents: list[TemplateConfig.Agent] = []
for agent in get_all_agents(_path):
for agent in get_all_agents():
agents.append(
TemplateConfig.Agent(
name=agent.name,
Expand All @@ -481,7 +484,7 @@ def export_template(output_filename: str, path: str = ''):

# Read all the tasks from the project's tasks.yaml file
tasks: list[TemplateConfig.Task] = []
for task in get_all_tasks(_path):
for task in get_all_tasks():
tasks.append(
TemplateConfig.Task(
name=task.name,
Expand All @@ -493,8 +496,8 @@ def export_template(output_filename: str, path: str = ''):

# Export all of the configured tools from the project
tools_agents: dict[str, list[str]] = {}
for agent_name in frameworks.get_agent_names(framework, _path):
for tool_name in frameworks.get_agent_tool_names(framework, agent_name, _path):
for agent_name in frameworks.get_agent_names():
for tool_name in frameworks.get_agent_tool_names(agent_name):
if not tool_name:
continue
if tool_name not in tools_agents:
Expand All @@ -514,7 +517,7 @@ def export_template(output_filename: str, path: str = ''):
template_version=2,
name=metadata.project_name,
description=metadata.project_description,
framework=framework,
framework=get_framework(),
method="sequential", # TODO this needs to be stored in the project somewhere
agents=agents,
tasks=tasks,
Expand All @@ -523,8 +526,8 @@ def export_template(output_filename: str, path: str = ''):
)

try:
template.write_to_file(_path / output_filename)
print(term_color(f"Template saved to: {_path / output_filename}", 'green'))
template.write_to_file(conf.PATH / output_filename)
print(term_color(f"Template saved to: {conf.PATH / output_filename}", 'green'))
except Exception as e:
print(term_color(f"Failed to write template to file: {e}", 'red'))
sys.exit(1)
18 changes: 8 additions & 10 deletions agentstack/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import importlib.util
from dotenv import load_dotenv

from agentstack import ValidationError
from agentstack import conf
from agentstack.exceptions import ValidationError
from agentstack import inputs
from agentstack import frameworks
from agentstack.utils import term_color, get_framework
Expand All @@ -31,17 +32,14 @@ def _import_project_module(path: Path):
return project_module


def run_project(command: str = 'run', path: Optional[str] = None, 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."""
_path = Path(path) if path else Path.cwd()
framework = get_framework(_path)

if framework not in frameworks.SUPPORTED_FRAMEWORKS:
print(term_color(f"Framework {framework} is not supported by agentstack.", 'red'))
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)

try:
frameworks.validate_project(framework, _path)
frameworks.validate_project()
except ValidationError as e:
print(term_color(f"Project validation failed:\n{e}", 'red'))
sys.exit(1)
Expand All @@ -55,11 +53,11 @@ def run_project(command: str = 'run', path: Optional[str] = None, cli_args: Opti
inputs.add_input_for_run(key, value)

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

# import src/main.py from the project path
try:
project_main = _import_project_module(_path)
project_main = _import_project_module(conf.PATH)
except ImportError as e:
print(term_color(f"Failed to import project. Does '{MAIN_FILENAME}' exist?:\n{e}", 'red'))
sys.exit(1)
Expand Down
89 changes: 89 additions & 0 deletions agentstack/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from typing import Optional, Union
import os, sys
import json
from pathlib import Path
from pydantic import BaseModel
from agentstack.utils import get_version


DEFAULT_FRAMEWORK = "crewai"
CONFIG_FILENAME = "agentstack.json"

PATH: Path = Path()


def set_path(path: Union[str, Path, None]):
"""Set the path to the project directory."""
global PATH
PATH = Path(path) if path else Path()


def get_framework() -> Optional[str]:
"""The framework used in the project. Will be available after PATH has been set
and if we are inside a project directory.
"""
try:
config = ConfigFile()
return config.framework
except FileNotFoundError:
return None # not in a project directory; that's okay


class ConfigFile(BaseModel):
"""
Interface for interacting with the agentstack.json file inside a project directory.
Handles both data validation and file I/O.
Use it as a context manager to make and save edits:
```python
with ConfigFile() as config:
config.tools.append('tool_name')
```
Config Schema
-------------
framework: str
The framework used in the project. Defaults to 'crewai'.
tools: list[str]
A list of tools that are currently installed in the project.
telemetry_opt_out: Optional[bool]
Whether the user has opted out of telemetry.
default_model: Optional[str]
The default model to use when generating agent configurations.
agentstack_version: Optional[str]
The version of agentstack used to generate the project.
template: Optional[str]
The template used to generate the project.
template_version: Optional[str]
The version of the template system used to generate the project.
"""

framework: str = DEFAULT_FRAMEWORK # TODO this should probably default to None
tools: list[str] = []
telemetry_opt_out: Optional[bool] = None
default_model: Optional[str] = None
agentstack_version: Optional[str] = get_version()
template: Optional[str] = None
template_version: Optional[str] = None

def __init__(self):
if os.path.exists(PATH / CONFIG_FILENAME):
with open(PATH / CONFIG_FILENAME, 'r') as f:
super().__init__(**json.loads(f.read()))
else:
raise FileNotFoundError(f"File {PATH / CONFIG_FILENAME} does not exist.")

def model_dump(self, *args, **kwargs) -> dict:
# Ignore None values
dump = super().model_dump(*args, **kwargs)
return {key: value for key, value in dump.items() if value is not None}

def write(self):
with open(PATH / CONFIG_FILENAME, 'w') as f:
f.write(json.dumps(self.model_dump(), indent=4))

def __enter__(self) -> 'ConfigFile':
return self

def __exit__(self, *args):
self.write()
Loading

0 comments on commit 37f3a44

Please sign in to comment.