Skip to content

Commit 4d0e1e2

Browse files
committed
Merge branch 'main' into improve-template-arg
2 parents 4ea727c + b744d27 commit 4d0e1e2

File tree

15 files changed

+404
-63
lines changed

15 files changed

+404
-63
lines changed

agentstack/agents.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,22 @@ class AgentConfig(pydantic.BaseModel):
2929
-------------
3030
name: str
3131
The name of the agent; used for lookup.
32-
role: Optional[str]
32+
role: str
3333
The role of the agent.
34-
goal: Optional[str]
34+
goal: str
3535
The goal of the agent.
36-
backstory: Optional[str]
36+
backstory: str
3737
The backstory of the agent.
38-
llm: Optional[str]
38+
llm: str
3939
The model this agent should use.
4040
Adheres to the format set by the framework.
4141
"""
4242

4343
name: str
44-
role: Optional[str] = ""
45-
goal: Optional[str] = ""
46-
backstory: Optional[str] = ""
47-
llm: Optional[str] = ""
44+
role: str = ""
45+
goal: str = ""
46+
backstory: str = ""
47+
llm: str = ""
4848

4949
def __init__(self, name: str, path: Optional[Path] = None):
5050
if not path:
@@ -96,3 +96,18 @@ def __enter__(self) -> 'AgentConfig':
9696

9797
def __exit__(self, *args):
9898
self.write()
99+
100+
101+
def get_all_agent_names(path: Optional[Path] = None) -> list[str]:
102+
if not path:
103+
path = Path()
104+
filename = path / AGENTS_FILENAME
105+
if not os.path.exists(filename):
106+
return []
107+
with open(filename, 'r') as f:
108+
data = yaml.load(f) or {}
109+
return list(data.keys())
110+
111+
112+
def get_all_agents(path: Optional[Path] = None) -> list[AgentConfig]:
113+
return [AgentConfig(name, path) for name in get_all_agent_names(path)]

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, run_project
1+
from .cli import init_project_builder, list_tools, configure_default_model, run_project, export_template

agentstack/cli/cli.py

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from cookiecutter.main import cookiecutter
1616
from dotenv import load_dotenv
1717
import subprocess
18+
from packaging.metadata import Metadata
1819

1920
from .agentstack_data import (
2021
FrameworkData,
@@ -25,11 +26,13 @@
2526
from agentstack.logger import log
2627
from agentstack.utils import get_package_path
2728
from agentstack.tools import get_all_tools
28-
from agentstack.generation.files import ConfigFile
29+
from agentstack.generation.files import ConfigFile, ProjectFile
2930
from agentstack import frameworks
3031
from agentstack import packaging
3132
from agentstack import generation
32-
from agentstack.utils import open_json_file, term_color, is_snake_case
33+
from agentstack.agents import get_all_agents
34+
from agentstack.tasks import get_all_tasks
35+
from agentstack.utils import open_json_file, term_color, is_snake_case, get_framework
3336
from agentstack.update import AGENTSTACK_PACKAGE
3437
from agentstack.proj_templates import TemplateConfig
3538

@@ -62,13 +65,13 @@ def init_project_builder(
6265
try:
6366
template_data = TemplateConfig.from_url(template)
6467
except Exception as e:
65-
print(term_color(f"Failed to fetch template data from {template}", 'red'))
68+
print(term_color(f"Failed to fetch template data from {template}.\n{e}", 'red'))
6669
sys.exit(1)
6770
else:
6871
try:
6972
template_data = TemplateConfig.from_template_name(template)
7073
except Exception as e:
71-
print(term_color(f"Failed to load template {template}", 'red'))
74+
print(term_color(f"Failed to load template {template}.\n{e}", 'red'))
7275
sys.exit(1)
7376

7477
if template_data:
@@ -81,11 +84,11 @@ def init_project_builder(
8184
}
8285
framework = template_data.framework
8386
design = {
84-
'agents': template_data.agents,
85-
'tasks': template_data.tasks,
87+
'agents': [agent.model_dump() for agent in template_data.agents],
88+
'tasks': [task.model_dump() for task in template_data.tasks],
8689
'inputs': template_data.inputs,
8790
}
88-
tools = template_data.tools
91+
tools = [tools.model_dump() for tools in template_data.tools]
8992

9093
elif use_wizard:
9194
welcome_message()
@@ -465,3 +468,85 @@ def list_tools():
465468

466469
print("\n\n✨ Add a tool with: agentstack tools add <tool_name>")
467470
print(" https://docs.agentstack.sh/tools/core")
471+
472+
473+
def export_template(output_filename: str, path: str = ''):
474+
"""
475+
Export the current project as a template.
476+
"""
477+
_path = Path(path)
478+
framework = get_framework(_path)
479+
480+
try:
481+
metadata = ProjectFile(_path)
482+
except Exception as e:
483+
print(term_color(f"Failed to load project metadata: {e}", 'red'))
484+
sys.exit(1)
485+
486+
# Read all the agents from the project's agents.yaml file
487+
agents: list[TemplateConfig.Agent] = []
488+
for agent in get_all_agents(_path):
489+
agents.append(
490+
TemplateConfig.Agent(
491+
name=agent.name,
492+
role=agent.role,
493+
goal=agent.goal,
494+
backstory=agent.backstory,
495+
model=agent.llm, # TODO consistent naming (llm -> model)
496+
)
497+
)
498+
499+
# Read all the tasks from the project's tasks.yaml file
500+
tasks: list[TemplateConfig.Task] = []
501+
for task in get_all_tasks(_path):
502+
tasks.append(
503+
TemplateConfig.Task(
504+
name=task.name,
505+
description=task.description,
506+
expected_output=task.expected_output,
507+
agent=task.agent,
508+
)
509+
)
510+
511+
# Export all of the configured tools from the project
512+
tools_agents: dict[str, list[str]] = {}
513+
for agent_name in frameworks.get_agent_names(framework, _path):
514+
for tool_name in frameworks.get_agent_tool_names(framework, agent_name, _path):
515+
if not tool_name:
516+
continue
517+
if tool_name not in tools_agents:
518+
tools_agents[tool_name] = []
519+
tools_agents[tool_name].append(agent_name)
520+
521+
tools: list[TemplateConfig.Tool] = []
522+
for tool_name, agent_names in tools_agents.items():
523+
tools.append(
524+
TemplateConfig.Tool(
525+
name=tool_name,
526+
agents=agent_names,
527+
)
528+
)
529+
530+
inputs: list[str] = []
531+
# TODO extract inputs from project
532+
# for input in frameworks.get_input_names():
533+
# inputs.append(input)
534+
535+
template = TemplateConfig(
536+
template_version=1,
537+
name=metadata.project_name,
538+
description=metadata.project_description,
539+
framework=framework,
540+
method="sequential", # TODO this needs to be stored in the project somewhere
541+
agents=agents,
542+
tasks=tasks,
543+
tools=tools,
544+
inputs=inputs,
545+
)
546+
547+
try:
548+
template.write_to_file(_path / output_filename)
549+
print(term_color(f"Template saved to: {_path / output_filename}", 'green'))
550+
except Exception as e:
551+
print(term_color(f"Failed to write template to file: {e}", 'red'))
552+
sys.exit(1)

agentstack/frameworks/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ def validate_project(self, path: Optional[Path] = None) -> None:
2828
"""
2929
...
3030

31+
def get_tool_names(self, path: Optional[Path] = None) -> list[str]:
32+
"""
33+
Get a list of tool names in the user's project.
34+
"""
35+
...
36+
3137
def add_tool(self, tool: ToolConfig, agent_name: str, path: Optional[Path] = None) -> None:
3238
"""
3339
Add a tool to an agent in the user's project.
@@ -46,6 +52,12 @@ def get_agent_names(self, path: Optional[Path] = None) -> list[str]:
4652
"""
4753
...
4854

55+
def get_agent_tool_names(self, agent_name: str, path: Optional[Path] = None) -> list[str]:
56+
"""
57+
Get a list of tool names in an agent in the user's project.
58+
"""
59+
...
60+
4961
def add_agent(self, agent: AgentConfig, path: Optional[Path] = None) -> None:
5062
"""
5163
Add an agent to the user's project.
@@ -102,6 +114,12 @@ def get_agent_names(framework: str, path: Optional[Path] = None) -> list[str]:
102114
"""
103115
return get_framework_module(framework).get_agent_names(path)
104116

117+
def get_agent_tool_names(framework: str, agent_name: str, path: Optional[Path] = None) -> list[str]:
118+
"""
119+
Get a list of tool names in the user's project.
120+
"""
121+
return get_framework_module(framework).get_agent_tool_names(agent_name, path)
122+
105123
def add_agent(framework: str, agent: AgentConfig, path: Optional[Path] = None):
106124
"""
107125
Add an agent to the user's project.

agentstack/frameworks/crewai.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Optional, Any
22
from pathlib import Path
33
import ast
44
from agentstack import ValidationError
@@ -259,6 +259,18 @@ def get_agent_names(path: Optional[Path] = None) -> list[str]:
259259
return [method.name for method in crew_file.get_agent_methods()]
260260

261261

262+
def get_agent_tool_names(agent_name: str, path: Optional[Path] = None) -> list[Any]:
263+
"""
264+
Get a list of tools used by an agent.
265+
"""
266+
if path is None:
267+
path = Path()
268+
with CrewFile(path / ENTRYPOINT) as crew_file:
269+
tools = crew_file.get_agent_tools(agent_name)
270+
print([node for node in tools.elts])
271+
return [asttools.get_node_value(node) for node in tools.elts]
272+
273+
262274
def add_agent(agent: AgentConfig, path: Optional[Path] = None) -> None:
263275
"""
264276
Add an agent method to the CrewAI entrypoint.

agentstack/generation/agent_generation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def add_agent(
2727
config.role = role or "Add your role here"
2828
config.goal = goal or "Add your goal here"
2929
config.backstory = backstory or "Add your backstory here"
30-
config.llm = llm or agentstack_config.default_model
30+
config.llm = llm or agentstack_config.default_model or ""
3131

3232
try:
3333
frameworks.add_agent(framework, agent, path)

agentstack/generation/asttools.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
functions that are useful for the specific tasks we need to accomplish.
1010
"""
1111

12-
from typing import TypeVar, Optional, Union, Iterable
12+
from typing import TypeVar, Optional, Union, Iterable, Any
1313
from pathlib import Path
1414
import ast
1515
import astor
@@ -175,3 +175,16 @@ def find_decorated_method_in_class(classdef: ast.ClassDef, decorator_name: str)
175175
def create_attribute(base_name: str, attr_name: str) -> ast.Attribute:
176176
"""Create an AST node for an attribute"""
177177
return ast.Attribute(value=ast.Name(id=base_name, ctx=ast.Load()), attr=attr_name, ctx=ast.Load())
178+
179+
180+
def get_node_value(node: Union[ast.expr, ast.Attribute, ast.Constant, ast.Str, ast.Num]) -> Any:
181+
if isinstance(node, ast.Constant):
182+
return node.value
183+
elif isinstance(node, ast.Attribute):
184+
prefix = get_node_value(node.value)
185+
if prefix:
186+
return '.'.join([prefix, node.attr])
187+
else:
188+
return node.attr
189+
else:
190+
return None

agentstack/generation/files.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
from typing import Optional, Union
2-
import os
2+
import os, sys
33
import json
44
from pathlib import Path
55
from pydantic import BaseModel
6+
7+
if sys.version_info >= (3, 11):
8+
import tomllib
9+
else:
10+
import tomli as tomllib
611
from agentstack.utils import get_version
712

813

914
DEFAULT_FRAMEWORK = "crewai"
1015
CONFIG_FILENAME = "agentstack.json"
1116
ENV_FILEMANE = ".env"
17+
PYPROJECT_FILENAME = "pyproject.toml"
1218

1319

1420
class ConfigFile(BaseModel):
@@ -140,3 +146,45 @@ def __enter__(self) -> 'EnvFile':
140146

141147
def __exit__(self, *args):
142148
self.write()
149+
150+
151+
class ProjectFile:
152+
"""
153+
Interface for interacting with pyproject.toml files inside of a project directory.
154+
This class is read-only and does not support writing changes back to the file.
155+
We expose project metadata as properties to support migration to other formats
156+
in the future.
157+
"""
158+
159+
_data: dict
160+
161+
def __init__(self, path: Union[str, Path, None] = None, filename: str = PYPROJECT_FILENAME):
162+
self._path = Path(path) if path else Path.cwd()
163+
self._filename = filename
164+
self.read()
165+
166+
@property
167+
def project_metadata(self) -> dict:
168+
try:
169+
return self._data['tool']['poetry']
170+
except KeyError:
171+
raise KeyError("No poetry metadata found in pyproject.toml.")
172+
173+
@property
174+
def project_name(self) -> str:
175+
return self.project_metadata.get('name', '')
176+
177+
@property
178+
def project_version(self) -> str:
179+
return self.project_metadata.get('version', '')
180+
181+
@property
182+
def project_description(self) -> str:
183+
return self.project_metadata.get('description', '')
184+
185+
def read(self):
186+
if os.path.exists(self._path / self._filename):
187+
with open(self._path / self._filename, 'rb') as f:
188+
self._data = tomllib.load(f)
189+
else:
190+
raise FileNotFoundError(f"File {self._path / self._filename} does not exist.")

agentstack/main.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
import os
33
import sys
44

5-
from agentstack.cli import init_project_builder, list_tools, configure_default_model, run_project
5+
from agentstack.cli import (
6+
init_project_builder,
7+
list_tools,
8+
configure_default_model,
9+
run_project,
10+
export_template,
11+
)
612
from agentstack.telemetry import track_cli_command
713
from agentstack.utils import get_version, get_framework
814
from agentstack import generation
@@ -83,6 +89,9 @@ def main():
8389
tools_remove_parser = tools_subparsers.add_parser("remove", aliases=["r"], help="Remove a tool")
8490
tools_remove_parser.add_argument("name", help="Name of the tool to remove")
8591

92+
export_parser = subparsers.add_parser('export', aliases=['e'], help='Export your agent as a template')
93+
export_parser.add_argument('filename', help='The name of the file to export to')
94+
8695
update = subparsers.add_parser('update', aliases=['u'], help='Check for updates')
8796

8897
# Parse arguments
@@ -128,6 +137,8 @@ def main():
128137
generation.remove_tool(args.name)
129138
else:
130139
tools_parser.print_help()
140+
elif args.command in ['export', 'e']:
141+
export_template(args.filename)
131142
elif args.command in ['update', 'u']:
132143
pass # Update check already done
133144
else:

0 commit comments

Comments
 (0)