Skip to content

Commit f9b049f

Browse files
authored
Merge pull request agentstack-ai#122 from tcdent/inputs-file
Store inputs in project YAML file, accept CLI flag input data
2 parents b85589c + 8976f2d commit f9b049f

File tree

18 files changed

+346
-107
lines changed

18 files changed

+346
-107
lines changed

agentstack/__init__.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
"""
2+
This it the beginning of the agentstack public API.
13
4+
Methods that have been imported into this file are expected to be used by the
5+
end user inside of their project.
6+
"""
7+
from agentstack.exceptions import ValidationError
8+
from agentstack.inputs import get_inputs
9+
10+
___all___ = [
11+
"ValidationError",
12+
"get_inputs",
13+
]
214

3-
class ValidationError(Exception):
4-
"""
5-
Raised when a validation error occurs ie. a file does not meet the required
6-
format or a syntax error is found.
7-
"""
8-
pass

agentstack/cli/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
from .cli import init_project_builder, list_tools, configure_default_model, run_project, export_template
1+
from .cli import init_project_builder, list_tools, configure_default_model, export_template
2+
from .run import run_project

agentstack/cli/agentstack_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class ProjectStructure:
5454
def __init__(self):
5555
self.agents = []
5656
self.tasks = []
57-
self.inputs = []
57+
self.inputs = {}
5858

5959
def add_agent(self, agent):
6060
self.agents.append(agent)

agentstack/cli/cli.py

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
1-
import json
2-
import shutil
3-
import sys
1+
from typing import Optional
2+
import os, sys
43
import time
54
from datetime import datetime
6-
from typing import Optional
75
from pathlib import Path
8-
import requests
6+
7+
import json
8+
import shutil
99
import itertools
1010

1111
from art import text2art
1212
import inquirer
13-
import os
14-
import importlib.resources
1513
from cookiecutter.main import cookiecutter
16-
from dotenv import load_dotenv
17-
import subprocess
18-
from packaging.metadata import Metadata
1914

2015
from .agentstack_data import (
2116
FrameworkData,
@@ -28,12 +23,11 @@
2823
from agentstack.tools import get_all_tools
2924
from agentstack.generation.files import ConfigFile, ProjectFile
3025
from agentstack import frameworks
31-
from agentstack import packaging
3226
from agentstack import generation
27+
from agentstack import inputs
3328
from agentstack.agents import get_all_agents
3429
from agentstack.tasks import get_all_tasks
3530
from agentstack.utils import open_json_file, term_color, is_snake_case, get_framework
36-
from agentstack.update import AGENTSTACK_PACKAGE
3731
from agentstack.proj_templates import TemplateConfig
3832

3933

@@ -162,27 +156,6 @@ def configure_default_model(path: Optional[str] = None):
162156
agentstack_config.default_model = model
163157

164158

165-
def run_project(framework: str, path: str = ''):
166-
"""Validate that the project is ready to run and then run it."""
167-
if framework not in frameworks.SUPPORTED_FRAMEWORKS:
168-
print(term_color(f"Framework {framework} is not supported by agentstack.", 'red'))
169-
sys.exit(1)
170-
171-
_path = Path(path)
172-
173-
try:
174-
frameworks.validate_project(framework, _path)
175-
except frameworks.ValidationError as e:
176-
print(term_color("Project validation failed:", 'red'))
177-
print(e)
178-
sys.exit(1)
179-
180-
load_dotenv(Path.home() / '.env') # load the user's .env file
181-
load_dotenv(_path / '.env', override=True) # load the project's .env file
182-
print("Running your agent...")
183-
subprocess.run(['python', 'src/main.py'], env=os.environ)
184-
185-
186159
def ask_framework() -> str:
187160
framework = "CrewAI"
188161
# framework = inquirer.list_input(
@@ -401,7 +374,7 @@ def insert_template(
401374
project_structure = ProjectStructure()
402375
project_structure.agents = design["agents"]
403376
project_structure.tasks = design["tasks"]
404-
project_structure.set_inputs(design["inputs"])
377+
project_structure.inputs = design["inputs"]
405378

406379
cookiecutter_data = CookiecutterData(
407380
project_metadata=project_metadata,
@@ -537,21 +510,16 @@ def export_template(output_filename: str, path: str = ''):
537510
)
538511
)
539512

540-
inputs: list[str] = []
541-
# TODO extract inputs from project
542-
# for input in frameworks.get_input_names():
543-
# inputs.append(input)
544-
545513
template = TemplateConfig(
546-
template_version=1,
514+
template_version=2,
547515
name=metadata.project_name,
548516
description=metadata.project_description,
549517
framework=framework,
550518
method="sequential", # TODO this needs to be stored in the project somewhere
551519
agents=agents,
552520
tasks=tasks,
553521
tools=tools,
554-
inputs=inputs,
522+
inputs=inputs.get_inputs(),
555523
)
556524

557525
try:

agentstack/cli/run.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from typing import Optional
2+
import sys
3+
from pathlib import Path
4+
import importlib.util
5+
from dotenv import load_dotenv
6+
7+
from agentstack import ValidationError
8+
from agentstack import inputs
9+
from agentstack import frameworks
10+
from agentstack.utils import term_color, get_framework
11+
12+
MAIN_FILENAME: Path = Path("src/main.py")
13+
MAIN_MODULE_NAME = "main"
14+
15+
16+
def _import_project_module(path: Path):
17+
"""
18+
Import `main` from the project path.
19+
20+
We do it this way instead of spawning a subprocess so that we can share
21+
state with the user's project.
22+
"""
23+
spec = importlib.util.spec_from_file_location(MAIN_MODULE_NAME, str(path / MAIN_FILENAME))
24+
25+
assert spec is not None # appease type checker
26+
assert spec.loader is not None # appease type checker
27+
28+
project_module = importlib.util.module_from_spec(spec)
29+
sys.path.append(str((path / MAIN_FILENAME).parent))
30+
spec.loader.exec_module(project_module)
31+
return project_module
32+
33+
34+
def run_project(command: str = 'run', path: Optional[str] = None, cli_args: Optional[str] = None):
35+
"""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'))
41+
sys.exit(1)
42+
43+
try:
44+
frameworks.validate_project(framework, _path)
45+
except ValidationError as e:
46+
print(term_color(f"Project validation failed:\n{e}", 'red'))
47+
sys.exit(1)
48+
49+
# Parse extra --input-* arguments for runtime overrides of the project's inputs
50+
if cli_args:
51+
for arg in cli_args:
52+
if not arg.startswith('--input-'):
53+
continue
54+
key, value = arg[len('--input-') :].split('=')
55+
inputs.add_input_for_run(key, value)
56+
57+
load_dotenv(Path.home() / '.env') # load the user's .env file
58+
load_dotenv(_path / '.env', override=True) # load the project's .env file
59+
60+
# import src/main.py from the project path
61+
try:
62+
project_main = _import_project_module(_path)
63+
except ImportError as e:
64+
print(term_color(f"Failed to import project. Does '{MAIN_FILENAME}' exist?:\n{e}", 'red'))
65+
sys.exit(1)
66+
67+
# run `command` from the project's main.py
68+
# TODO try/except this and print detailed information with a --debug flag
69+
print("Running your agent...")
70+
return getattr(project_main, command)()

agentstack/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class ValidationError(Exception):
2+
"""
3+
Raised when a validation error occurs ie. a file does not meet the required
4+
format or a syntax error is found.
5+
"""
6+
7+
pass

agentstack/inputs.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from typing import Optional
2+
import os
3+
from pathlib import Path
4+
from ruamel.yaml import YAML, YAMLError
5+
from ruamel.yaml.scalarstring import FoldedScalarString
6+
from agentstack import ValidationError
7+
8+
9+
INPUTS_FILENAME: Path = Path("src/config/inputs.yaml")
10+
11+
yaml = YAML()
12+
yaml.preserve_quotes = True # Preserve quotes in existing data
13+
14+
# run_inputs are set at the beginning of the run and are not saved
15+
run_inputs: dict[str, str] = {}
16+
17+
18+
class InputsConfig:
19+
"""
20+
Interface for interacting with inputs configuration.
21+
22+
Use it as a context manager to make and save edits:
23+
```python
24+
with InputsConfig() as inputs:
25+
inputs.topic = "Open Source Aritifical Intelligence"
26+
```
27+
"""
28+
29+
_attributes: dict[str, str]
30+
31+
def __init__(self, path: Optional[Path] = None):
32+
self.path = path if path else Path()
33+
filename = self.path / INPUTS_FILENAME
34+
35+
if not os.path.exists(filename):
36+
os.makedirs(filename.parent, exist_ok=True)
37+
filename.touch()
38+
39+
try:
40+
with open(filename, 'r') as f:
41+
self._attributes = yaml.load(f) or {}
42+
except YAMLError as e:
43+
# TODO format MarkedYAMLError lines/messages
44+
raise ValidationError(f"Error parsing inputs file: {filename}\n{e}")
45+
46+
def __getitem__(self, key: str) -> str:
47+
return self._attributes[key]
48+
49+
def __setitem__(self, key: str, value: str):
50+
self._attributes[key] = value
51+
52+
def __contains__(self, key: str) -> bool:
53+
return key in self._attributes
54+
55+
def to_dict(self) -> dict[str, str]:
56+
return self._attributes
57+
58+
def model_dump(self) -> dict:
59+
dump = {}
60+
for key, value in self._attributes.items():
61+
dump[key] = FoldedScalarString(value)
62+
return dump
63+
64+
def write(self):
65+
with open(self.path / INPUTS_FILENAME, 'w') as f:
66+
yaml.dump(self.model_dump(), f)
67+
68+
def __enter__(self) -> 'InputsConfig':
69+
return self
70+
71+
def __exit__(self, *args):
72+
self.write()
73+
74+
75+
def get_inputs(path: Optional[Path] = None) -> dict:
76+
"""
77+
Get the inputs configuration file and override with run_inputs.
78+
"""
79+
path = path if path else Path()
80+
config = InputsConfig(path).to_dict()
81+
# run_inputs override saved inputs
82+
for key, value in run_inputs.items():
83+
config[key] = value
84+
return config
85+
86+
87+
def add_input_for_run(key: str, value: str):
88+
"""
89+
Add an input override for the current run.
90+
This is used by the CLI to allow inputs to be set at runtime.
91+
"""
92+
run_inputs[key] = value

agentstack/main.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import argparse
2-
import os
32
import sys
43

54
from agentstack.cli import (
@@ -10,7 +9,7 @@
109
export_template,
1110
)
1211
from agentstack.telemetry import track_cli_command
13-
from agentstack.utils import get_version, get_framework
12+
from agentstack.utils import get_version
1413
from agentstack import generation
1514
from agentstack.update import check_for_updates
1615

@@ -43,7 +42,30 @@ def main():
4342
init_parser.add_argument("--template", "-t", help="Agent template to use")
4443

4544
# 'run' command
46-
_ = subparsers.add_parser("run", aliases=["r"], help="Run your agent")
45+
run_parser = subparsers.add_parser(
46+
"run",
47+
aliases=["r"],
48+
help="Run your agent",
49+
formatter_class=argparse.RawDescriptionHelpFormatter,
50+
epilog='''
51+
--input-<key>=VALUE Specify inputs to be passed to the run.
52+
These will override the inputs in the project's inputs.yaml file.
53+
Examples: --input-topic=Sports --input-content-type=News
54+
''',
55+
)
56+
run_parser.add_argument(
57+
"--function",
58+
"-f",
59+
help="Function to call in main.py, defaults to 'run'",
60+
default="run",
61+
dest="function",
62+
)
63+
run_parser.add_argument(
64+
"--path",
65+
"-p",
66+
help="Path to the project directory, defaults to current working directory",
67+
dest="path",
68+
)
4769

4870
# 'generate' command
4971
generate_parser = subparsers.add_parser("generate", aliases=["g"], help="Generate agents or tasks")
@@ -94,8 +116,8 @@ def main():
94116

95117
update = subparsers.add_parser('update', aliases=['u'], help='Check for updates')
96118

97-
# Parse arguments
98-
args = parser.parse_args()
119+
# Parse known args and store unknown args in extras; some commands use them later on
120+
args, extra_args = parser.parse_known_args()
99121

100122
# Handle version
101123
if args.version:
@@ -115,8 +137,7 @@ def main():
115137
elif args.command in ["init", "i"]:
116138
init_project_builder(args.slug_name, args.template, args.wizard)
117139
elif args.command in ["run", "r"]:
118-
framework = get_framework()
119-
run_project(framework)
140+
run_project(command=args.function, path=args.path, cli_args=extra_args)
120141
elif args.command in ['generate', 'g']:
121142
if args.generate_command in ['agent', 'a']:
122143
if not args.llm:

0 commit comments

Comments
 (0)