Skip to content

Commit 37f3a44

Browse files
authored
Merge pull request agentstack-ai#132 from tcdent/path
Make PATH part of global state.
2 parents f9b049f + ad45c87 commit 37f3a44

31 files changed

+447
-437
lines changed

agentstack/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
Methods that have been imported into this file are expected to be used by the
55
end user inside of their project.
66
"""
7-
from agentstack.exceptions import ValidationError
7+
from pathlib import Path
8+
from agentstack import conf
89
from agentstack.inputs import get_inputs
910

1011
___all___ = [
11-
"ValidationError",
12+
"conf",
1213
"get_inputs",
1314
]
1415

agentstack/agents.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import pydantic
55
from ruamel.yaml import YAML, YAMLError
66
from ruamel.yaml.scalarstring import FoldedScalarString
7-
from agentstack import ValidationError
7+
from agentstack import conf
8+
from agentstack.exceptions import ValidationError
89

910

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

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

72-
# store the path *after* loading data
73-
self._path = path
74-
7570
def model_dump(self, *args, **kwargs) -> dict:
7671
dump = super().model_dump(*args, **kwargs)
7772
dump.pop('name') # name is the key, so keep it out of the data
@@ -81,7 +76,7 @@ def model_dump(self, *args, **kwargs) -> dict:
8176
return {self.name: dump}
8277

8378
def write(self):
84-
filename = self._path / AGENTS_FILENAME
79+
filename = conf.PATH / AGENTS_FILENAME
8580

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

10095

101-
def get_all_agent_names(path: Optional[Path] = None) -> list[str]:
102-
if not path:
103-
path = Path()
104-
filename = path / AGENTS_FILENAME
96+
def get_all_agent_names() -> list[str]:
97+
filename = conf.PATH / AGENTS_FILENAME
10598
if not os.path.exists(filename):
10699
return []
107100
with open(filename, 'r') as f:
108101
data = yaml.load(f) or {}
109102
return list(data.keys())
110103

111104

112-
def get_all_agents(path: Optional[Path] = None) -> list[AgentConfig]:
113-
return [AgentConfig(name, path) for name in get_all_agent_names(path)]
105+
def get_all_agents() -> list[AgentConfig]:
106+
return [AgentConfig(name) for name in get_all_agent_names()]

agentstack/cli/cli.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
CookiecutterData,
2020
)
2121
from agentstack.logger import log
22+
from agentstack import conf
23+
from agentstack.conf import ConfigFile
2224
from agentstack.utils import get_package_path
2325
from agentstack.tools import get_all_tools
24-
from agentstack.generation.files import ConfigFile, ProjectFile
26+
from agentstack.generation.files import ProjectFile
2527
from agentstack import frameworks
2628
from agentstack import generation
2729
from agentstack import inputs
@@ -117,9 +119,12 @@ def init_project_builder(
117119

118120
log.debug(f"project_details: {project_details}" f"framework: {framework}" f"design: {design}")
119121
insert_template(project_details, framework, design, template_data)
120-
path = Path(project_details['name'])
122+
123+
# we have an agentstack.json file in the directory now
124+
conf.set_path(project_details['name'])
125+
121126
for tool_data in tools:
122-
generation.add_tool(tool_data['name'], agents=tool_data['agents'], path=path)
127+
generation.add_tool(tool_data['name'], agents=tool_data['agents'])
123128

124129

125130
def welcome_message():
@@ -135,9 +140,9 @@ def welcome_message():
135140
print(border)
136141

137142

138-
def configure_default_model(path: Optional[str] = None):
143+
def configure_default_model():
139144
"""Set the default model"""
140-
agentstack_config = ConfigFile(path)
145+
agentstack_config = ConfigFile()
141146
if agentstack_config.default_model:
142147
return # Default model already set
143148

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

155-
with ConfigFile(path) as agentstack_config:
160+
with ConfigFile() as agentstack_config:
156161
agentstack_config.default_model = model
157162

158163

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

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

455461

456-
def export_template(output_filename: str, path: str = ''):
462+
def export_template(output_filename: str):
457463
"""
458464
Export the current project as a template.
459465
"""
460-
_path = Path(path)
461-
framework = get_framework(_path)
462-
463466
try:
464-
metadata = ProjectFile(_path)
467+
metadata = ProjectFile()
465468
except Exception as e:
466469
print(term_color(f"Failed to load project metadata: {e}", 'red'))
467470
sys.exit(1)
468471

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

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

494497
# Export all of the configured tools from the project
495498
tools_agents: dict[str, list[str]] = {}
496-
for agent_name in frameworks.get_agent_names(framework, _path):
497-
for tool_name in frameworks.get_agent_tool_names(framework, agent_name, _path):
499+
for agent_name in frameworks.get_agent_names():
500+
for tool_name in frameworks.get_agent_tool_names(agent_name):
498501
if not tool_name:
499502
continue
500503
if tool_name not in tools_agents:
@@ -514,7 +517,7 @@ def export_template(output_filename: str, path: str = ''):
514517
template_version=2,
515518
name=metadata.project_name,
516519
description=metadata.project_description,
517-
framework=framework,
520+
framework=get_framework(),
518521
method="sequential", # TODO this needs to be stored in the project somewhere
519522
agents=agents,
520523
tasks=tasks,
@@ -523,8 +526,8 @@ def export_template(output_filename: str, path: str = ''):
523526
)
524527

525528
try:
526-
template.write_to_file(_path / output_filename)
527-
print(term_color(f"Template saved to: {_path / output_filename}", 'green'))
529+
template.write_to_file(conf.PATH / output_filename)
530+
print(term_color(f"Template saved to: {conf.PATH / output_filename}", 'green'))
528531
except Exception as e:
529532
print(term_color(f"Failed to write template to file: {e}", 'red'))
530533
sys.exit(1)

agentstack/cli/run.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import importlib.util
55
from dotenv import load_dotenv
66

7-
from agentstack import ValidationError
7+
from agentstack import conf
8+
from agentstack.exceptions import ValidationError
89
from agentstack import inputs
910
from agentstack import frameworks
1011
from agentstack.utils import term_color, get_framework
@@ -31,17 +32,14 @@ def _import_project_module(path: Path):
3132
return project_module
3233

3334

34-
def run_project(command: str = 'run', path: Optional[str] = None, cli_args: Optional[str] = None):
35+
def run_project(command: str = 'run', cli_args: Optional[str] = None):
3536
"""Validate that the project is ready to run and then run it."""
36-
_path = Path(path) if path else Path.cwd()
37-
framework = get_framework(_path)
38-
39-
if framework not in frameworks.SUPPORTED_FRAMEWORKS:
40-
print(term_color(f"Framework {framework} is not supported by agentstack.", 'red'))
37+
if conf.get_framework() not in frameworks.SUPPORTED_FRAMEWORKS:
38+
print(term_color(f"Framework {conf.get_framework()} is not supported by agentstack.", 'red'))
4139
sys.exit(1)
4240

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

5755
load_dotenv(Path.home() / '.env') # load the user's .env file
58-
load_dotenv(_path / '.env', override=True) # load the project's .env file
56+
load_dotenv(conf.PATH / '.env', override=True) # load the project's .env file
5957

6058
# import src/main.py from the project path
6159
try:
62-
project_main = _import_project_module(_path)
60+
project_main = _import_project_module(conf.PATH)
6361
except ImportError as e:
6462
print(term_color(f"Failed to import project. Does '{MAIN_FILENAME}' exist?:\n{e}", 'red'))
6563
sys.exit(1)

agentstack/conf.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import Optional, Union
2+
import os, sys
3+
import json
4+
from pathlib import Path
5+
from pydantic import BaseModel
6+
from agentstack.utils import get_version
7+
8+
9+
DEFAULT_FRAMEWORK = "crewai"
10+
CONFIG_FILENAME = "agentstack.json"
11+
12+
PATH: Path = Path()
13+
14+
15+
def set_path(path: Union[str, Path, None]):
16+
"""Set the path to the project directory."""
17+
global PATH
18+
PATH = Path(path) if path else Path()
19+
20+
21+
def get_framework() -> Optional[str]:
22+
"""The framework used in the project. Will be available after PATH has been set
23+
and if we are inside a project directory.
24+
"""
25+
try:
26+
config = ConfigFile()
27+
return config.framework
28+
except FileNotFoundError:
29+
return None # not in a project directory; that's okay
30+
31+
32+
class ConfigFile(BaseModel):
33+
"""
34+
Interface for interacting with the agentstack.json file inside a project directory.
35+
Handles both data validation and file I/O.
36+
37+
Use it as a context manager to make and save edits:
38+
```python
39+
with ConfigFile() as config:
40+
config.tools.append('tool_name')
41+
```
42+
43+
Config Schema
44+
-------------
45+
framework: str
46+
The framework used in the project. Defaults to 'crewai'.
47+
tools: list[str]
48+
A list of tools that are currently installed in the project.
49+
telemetry_opt_out: Optional[bool]
50+
Whether the user has opted out of telemetry.
51+
default_model: Optional[str]
52+
The default model to use when generating agent configurations.
53+
agentstack_version: Optional[str]
54+
The version of agentstack used to generate the project.
55+
template: Optional[str]
56+
The template used to generate the project.
57+
template_version: Optional[str]
58+
The version of the template system used to generate the project.
59+
"""
60+
61+
framework: str = DEFAULT_FRAMEWORK # TODO this should probably default to None
62+
tools: list[str] = []
63+
telemetry_opt_out: Optional[bool] = None
64+
default_model: Optional[str] = None
65+
agentstack_version: Optional[str] = get_version()
66+
template: Optional[str] = None
67+
template_version: Optional[str] = None
68+
69+
def __init__(self):
70+
if os.path.exists(PATH / CONFIG_FILENAME):
71+
with open(PATH / CONFIG_FILENAME, 'r') as f:
72+
super().__init__(**json.loads(f.read()))
73+
else:
74+
raise FileNotFoundError(f"File {PATH / CONFIG_FILENAME} does not exist.")
75+
76+
def model_dump(self, *args, **kwargs) -> dict:
77+
# Ignore None values
78+
dump = super().model_dump(*args, **kwargs)
79+
return {key: value for key, value in dump.items() if value is not None}
80+
81+
def write(self):
82+
with open(PATH / CONFIG_FILENAME, 'w') as f:
83+
f.write(json.dumps(self.model_dump(), indent=4))
84+
85+
def __enter__(self) -> 'ConfigFile':
86+
return self
87+
88+
def __exit__(self, *args):
89+
self.write()

0 commit comments

Comments
 (0)