Skip to content

Commit e2c652d

Browse files
authored
Merge pull request #97 from tcdent/frameworks
Frameworks
2 parents b5ef6c8 + 6d3d63a commit e2c652d

37 files changed

+1745
-664
lines changed

agentstack/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
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/agents.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from typing import Optional
2+
import os
3+
from pathlib import Path
4+
import pydantic
5+
from ruamel.yaml import YAML, YAMLError
6+
from ruamel.yaml.scalarstring import FoldedScalarString
7+
from agentstack import ValidationError
8+
9+
10+
AGENTS_FILENAME: Path = Path("src/config/agents.yaml")
11+
12+
yaml = YAML()
13+
yaml.preserve_quotes = True # Preserve quotes in existing data
14+
15+
16+
class AgentConfig(pydantic.BaseModel):
17+
"""
18+
Interface for interacting with an agent configuration.
19+
20+
Multiple agents are stored in a single YAML file, so we always look up the
21+
requested agent by `name`.
22+
23+
Use it as a context manager to make and save edits:
24+
```python
25+
with AgentConfig('agent_name') as config:
26+
config.llm = "openai/gpt-4o"
27+
28+
Config Schema
29+
-------------
30+
name: str
31+
The name of the agent; used for lookup.
32+
role: Optional[str]
33+
The role of the agent.
34+
goal: Optional[str]
35+
The goal of the agent.
36+
backstory: Optional[str]
37+
The backstory of the agent.
38+
llm: Optional[str]
39+
The model this agent should use.
40+
Adheres to the format set by the framework.
41+
"""
42+
43+
name: str
44+
role: Optional[str] = ""
45+
goal: Optional[str] = ""
46+
backstory: Optional[str] = ""
47+
llm: Optional[str] = ""
48+
49+
def __init__(self, name: str, path: Optional[Path] = None):
50+
if not path:
51+
path = Path()
52+
53+
filename = path / AGENTS_FILENAME
54+
if not os.path.exists(filename):
55+
os.makedirs(filename.parent, exist_ok=True)
56+
filename.touch()
57+
58+
try:
59+
with open(filename, 'r') as f:
60+
data = yaml.load(f) or {}
61+
data = data.get(name, {}) or {}
62+
super().__init__(**{**{'name': name}, **data})
63+
except YAMLError as e:
64+
# TODO format MarkedYAMLError lines/messages
65+
raise ValidationError(f"Error parsing agents file: {filename}\n{e}")
66+
except pydantic.ValidationError as e:
67+
error_str = "Error validating agent config:\n"
68+
for error in e.errors():
69+
error_str += f"{' '.join([str(loc) for loc in error['loc']])}: {error['msg']}\n"
70+
raise ValidationError(f"Error loading agent {name} from {filename}.\n{error_str}")
71+
72+
# store the path *after* loading data
73+
self._path = path
74+
75+
def model_dump(self, *args, **kwargs) -> dict:
76+
dump = super().model_dump(*args, **kwargs)
77+
dump.pop('name') # name is the key, so keep it out of the data
78+
# format these as FoldedScalarStrings
79+
for key in ('role', 'goal', 'backstory'):
80+
dump[key] = FoldedScalarString(dump.get(key) or "")
81+
return {self.name: dump}
82+
83+
def write(self):
84+
filename = self._path / AGENTS_FILENAME
85+
86+
with open(filename, 'r') as f:
87+
data = yaml.load(f) or {}
88+
89+
data.update(self.model_dump())
90+
91+
with open(filename, 'w') as f:
92+
yaml.dump(data, f)
93+
94+
def __enter__(self) -> 'AgentConfig':
95+
return self
96+
97+
def __exit__(self, *args):
98+
self.write()

agentstack/cli/__init__.py

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

agentstack/cli/cli.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import time
55
from datetime import datetime
66
from typing import Optional
7+
from pathlib import Path
78
import requests
89
import itertools
910

@@ -21,12 +22,15 @@
2122
)
2223
from agentstack.logger import log
2324
from agentstack.utils import get_package_path
25+
from agentstack.tools import get_all_tools
2426
from agentstack.generation.files import ConfigFile
25-
from agentstack.generation.tool_generation import get_all_tools
26-
from agentstack import packaging, generation
27+
from agentstack import frameworks
28+
from agentstack import packaging
29+
from agentstack import generation
2730
from agentstack.utils import open_json_file, term_color, is_snake_case
2831
from agentstack.update import AGENTSTACK_PACKAGE
2932

33+
3034
PREFERRED_MODELS = [
3135
'openai/gpt-4o',
3236
'anthropic/claude-3-5-sonnet',
@@ -158,13 +162,32 @@ def configure_default_model(path: Optional[str] = None):
158162
)
159163

160164
if model == other_msg: # If the user selects "Other", prompt for a model name
161-
print(f'A list of available models is available at: "https://docs.litellm.ai/docs/providers"')
165+
print('A list of available models is available at: "https://docs.litellm.ai/docs/providers"')
162166
model = inquirer.text(message="Enter the model name")
163167

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

167171

172+
def run_project(framework: str, path: str = ''):
173+
"""Validate that the project is ready to run and then run it."""
174+
if framework not in frameworks.SUPPORTED_FRAMEWORKS:
175+
print(term_color(f"Framework {framework} is not supported by agentstack.", 'red'))
176+
sys.exit(1)
177+
178+
_path = Path(path)
179+
180+
try:
181+
frameworks.validate_project(framework, _path)
182+
except frameworks.ValidationError as e:
183+
print(term_color("Project validation failed:", 'red'))
184+
print(e)
185+
sys.exit(1)
186+
187+
entrypoint = _path / frameworks.get_entrypoint_path(framework)
188+
os.system(f'python {entrypoint}')
189+
190+
168191
def ask_framework() -> str:
169192
framework = "CrewAI"
170193
# framework = inquirer.list_input(

agentstack/frameworks/__init__.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from typing import Optional, Protocol
2+
from types import ModuleType
3+
from importlib import import_module
4+
from pathlib import Path
5+
from agentstack import ValidationError
6+
from agentstack.tools import ToolConfig
7+
from agentstack.agents import AgentConfig
8+
from agentstack.tasks import TaskConfig
9+
10+
11+
CREWAI = 'crewai'
12+
SUPPORTED_FRAMEWORKS = [CREWAI, ]
13+
14+
class FrameworkModule(Protocol):
15+
"""
16+
Protocol spec for a framework implementation module.
17+
"""
18+
ENTRYPOINT: Path
19+
"""
20+
Relative path to the entrypoint file for the framework in the user's project.
21+
ie. `src/crewai.py`
22+
"""
23+
24+
def validate_project(self, path: Optional[Path] = None) -> None:
25+
"""
26+
Validate that a user's project is ready to run.
27+
Raises a `ValidationError` if the project is not valid.
28+
"""
29+
...
30+
31+
def add_tool(self, tool: ToolConfig, agent_name: str, path: Optional[Path] = None) -> None:
32+
"""
33+
Add a tool to an agent in the user's project.
34+
"""
35+
...
36+
37+
def remove_tool(self, tool: ToolConfig, agent_name: str, path: Optional[Path] = None) -> None:
38+
"""
39+
Remove a tool from an agent in user's project.
40+
"""
41+
...
42+
43+
def get_agent_names(self, path: Optional[Path] = None) -> list[str]:
44+
"""
45+
Get a list of agent names in the user's project.
46+
"""
47+
...
48+
49+
def add_agent(self, agent: AgentConfig, path: Optional[Path] = None) -> None:
50+
"""
51+
Add an agent to the user's project.
52+
"""
53+
...
54+
55+
def add_task(self, task: TaskConfig, path: Optional[Path] = None) -> None:
56+
"""
57+
Add a task to the user's project.
58+
"""
59+
...
60+
61+
62+
def get_framework_module(framework: str) -> FrameworkModule:
63+
"""
64+
Get the module for a framework.
65+
"""
66+
try:
67+
return import_module(f".{framework}", package=__package__)
68+
except ImportError:
69+
raise Exception(f"Framework {framework} could not be imported.")
70+
71+
def get_entrypoint_path(framework: str, path: Optional[Path] = None) -> Path:
72+
"""
73+
Get the path to the entrypoint file for a framework.
74+
"""
75+
if path is None:
76+
path = Path()
77+
return path / get_framework_module(framework).ENTRYPOINT
78+
79+
def validate_project(framework: str, path: Optional[Path] = None):
80+
"""
81+
Validate that the user's project is ready to run.
82+
"""
83+
return get_framework_module(framework).validate_project(path)
84+
85+
def add_tool(framework: str, tool: ToolConfig, agent_name: str, path: Optional[Path] = None):
86+
"""
87+
Add a tool to the user's project.
88+
The tool will have aready been installed in the user's application and have
89+
all dependencies installed. We're just handling code generation here.
90+
"""
91+
return get_framework_module(framework).add_tool(tool, agent_name, path)
92+
93+
def remove_tool(framework: str, tool: ToolConfig, agent_name: str, path: Optional[Path] = None):
94+
"""
95+
Remove a tool from the user's project.
96+
"""
97+
return get_framework_module(framework).remove_tool(tool, agent_name, path)
98+
99+
def get_agent_names(framework: str, path: Optional[Path] = None) -> list[str]:
100+
"""
101+
Get a list of agent names in the user's project.
102+
"""
103+
return get_framework_module(framework).get_agent_names(path)
104+
105+
def add_agent(framework: str, agent: AgentConfig, path: Optional[Path] = None):
106+
"""
107+
Add an agent to the user's project.
108+
"""
109+
return get_framework_module(framework).add_agent(agent, path)
110+
111+
def add_task(framework: str, task: TaskConfig, path: Optional[Path] = None):
112+
"""
113+
Add a task to the user's project.
114+
"""
115+
return get_framework_module(framework).add_task(task, path)
116+

0 commit comments

Comments
 (0)