diff --git a/0.2-changes.md b/0.2-changes.md new file mode 100644 index 00000000..beff03fa --- /dev/null +++ b/0.2-changes.md @@ -0,0 +1,85 @@ +# 0.2 Roadmap + +# Concepts +- Support only CrewAI for now +- Make the process of starting an agent project as easy as possible + +# How do we get there +- Understand current agent design +- What are the use cases? How can they be categorized? + +# CLI + +## Template Types + +### Chatbot +- Conversational +- Customer Support + +### Research Agent +- RAG +- Search +- Answers question + +### Creative Agent +- Coding Agent +- Writer Agent +- Image Gen + +### Copilot +- Understand a problem space +- Support decision-making +- Predict next steps + +## Templates +Are the templates part of cookiecutter, then structure is generated using the code-gen scripts? +Or do we manage a collection of independent cookiecutter templates. + +The options: + +### Codegen +This strategy involves using the existing and new codegen scripts to take the base project template and systematically +build off of it. + +This would require a templating structure that can handle complex agent designs. + +Pros: +- More easily extensible +- Helps prevent some templates lagging behind in updates + +Cons: +- More complex design. Requires more work on codegen and is a more difficult pattern to understand +- Contributions will be more challenging +- There may be some templates that this design may not account for + +### Cookiecutter +With this strategy, every template would be its own Cookiecutter template. + +Pros: +- Much simpler to understand +- No bounds on complexity or variation between templates + +Cons: +- Tech debt will easily grow + - Every update will require a change to every single template + - Requires robust processes for keeping every template updated + +## Decision +We'll move forward with the codegen method unless other information comes to light. + + +### `agentstack i ` +- Flags + - `--in-place` + - dont place into a subdirectory + - `--template ` + - generate the project according to a template + - can also consume a url + +### `agentstack tools` +- Stays the same + +### `agentstack generate` +- `agent` + - `--template ` +- diff --git a/agentstack/cli/agentstack_data.py b/agentstack/cli/agentstack_data.py index 1134dc4d..ca811423 100644 --- a/agentstack/cli/agentstack_data.py +++ b/agentstack/cli/agentstack_data.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Optional, Literal -from agentstack.utils import clean_input +from agentstack.utils import clean_input, get_version from agentstack.logger import log @@ -14,7 +14,9 @@ def __init__(self, author_name: str = "", version: str = "", license: str = "", - year: int = datetime.now().year + year: int = datetime.now().year, + template: str = "none", + template_version: str = "0", ): self.project_name = clean_input(project_name) if project_name else "myagent" self.project_slug = clean_input(project_slug) if project_slug else self.project_name @@ -23,6 +25,10 @@ def __init__(self, self.version = version self.license = license self.year = year + self.agentstack_version = get_version() + self.template = template + self.template_version = template_version + log.debug(f"ProjectMetadata: {self.to_dict()}") def to_dict(self): @@ -34,6 +40,9 @@ def to_dict(self): 'version': self.version, 'license': self.license, 'year': self.year, + 'agentstack_version': self.agentstack_version, + 'template': self.template, + 'template_version': self.template_version, } def to_json(self): diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 57de949b..9b560d16 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -1,8 +1,10 @@ import json import shutil +import sys import time from datetime import datetime from typing import Optional +import requests import itertools from art import text2art @@ -19,17 +21,64 @@ from ..utils import open_json_file, term_color, is_snake_case -def init_project_builder(slug_name: Optional[str] = None, skip_wizard: bool = False): +def init_project_builder(slug_name: Optional[str] = None, template: Optional[str] = None, use_wizard: bool = False): if slug_name and not is_snake_case(slug_name): print(term_color("Project name must be snake case", 'red')) return - if skip_wizard: + if template is not None and use_wizard: + print(term_color("Template and wizard flags cannot be used together", 'red')) + return + + template_data = None + if template is not None: + url_start = "https://" + if template[:len(url_start)] == url_start: + # template is a url + response = requests.get(template) + if response.status_code == 200: + template_data = response.json() + else: + print(term_color(f"Failed to fetch template data from {template}. Status code: {response.status_code}", 'red')) + sys.exit(1) + else: + with importlib.resources.path('agentstack.templates.proj_templates', f'{template}.json') as template_path: + if template_path is None: + print(term_color(f"No such template {template} found", 'red')) + sys.exit(1) + template_data = open_json_file(template_path) + + if template_data: project_details = { - "name": slug_name or "new_agentstack_project", - "version": "0.1.0", + "name": slug_name or template_data['name'], + "version": "0.0.1", + "description": template_data['description'], + "author": "Name ", + "license": "MIT" + } + framework = template_data['framework'] + design = { + 'agents': template_data['agents'], + 'tasks': template_data['tasks'] + } + + tools = template_data['tools'] + + elif use_wizard: + welcome_message() + project_details = ask_project_details(slug_name) + welcome_message() + framework = ask_framework() + design = ask_design() + tools = ask_tools() + + else: + welcome_message() + project_details = { + "name": slug_name or "agentstack_project", + "version": "0.0.1", "description": "New agentstack project", - "author": "", + "author": "Name ", "license": "MIT" } @@ -41,21 +90,15 @@ def init_project_builder(slug_name: Optional[str] = None, skip_wizard: bool = Fa } tools = [] - else: - welcome_message() - project_details = ask_project_details(slug_name) - welcome_message() - framework = ask_framework() - design = ask_design() - tools = ask_tools() log.debug( f"project_details: {project_details}" f"framework: {framework}" f"design: {design}" ) - insert_template(project_details, framework, design) - add_tools(tools, project_details['name']) + insert_template(project_details, framework, design, template_data) + for tool_data in tools: + generation.add_tool(tool_data['name'], agents=tool_data['agents'], path=project_details['name']) def welcome_message(): @@ -98,11 +141,9 @@ def ask_framework() -> str: def ask_design() -> dict: - # use_wizard = inquirer.confirm( - # message="Would you like to use the CLI wizard to set up agents and tasks?", - # ) - - use_wizard = False + use_wizard = inquirer.confirm( + message="Would you like to use the CLI wizard to set up agents and tasks?", + ) if not use_wizard: return { @@ -202,11 +243,9 @@ def ask_design() -> dict: def ask_tools() -> list: - # use_tools = inquirer.confirm( - # message="Do you want to add agent tools now? (you can do this later with `agentstack tools add `)", - # ) - - use_tools = False + use_tools = inquirer.confirm( + message="Do you want to add agent tools now? (you can do this later with `agentstack tools add `)", + ) if not use_tools: return [] @@ -262,14 +301,16 @@ def ask_project_details(slug_name: Optional[str] = None) -> dict: return questions -def insert_template(project_details: dict, framework_name: str, design: dict): +def insert_template(project_details: dict, framework_name: str, design: dict, template_data: Optional[dict] = None): framework = FrameworkData(framework_name.lower()) project_metadata = ProjectMetadata(project_name=project_details["name"], description=project_details["description"], author_name=project_details["author"], - version=project_details["version"], + version="0.0.1", license="MIT", - year=datetime.now().year) + year=datetime.now().year, + template=template_data['name'] if template_data else None, + template_version=template_data['template_version'] if template_data else None) project_structure = ProjectStructure() project_structure.agents = design["agents"] @@ -321,16 +362,11 @@ def insert_template(project_details: dict, framework_name: str, design: dict): ) -def add_tools(tools: list, project_name: str): - for tool in tools: - generation.add_tool(tool, project_name) - - def list_tools(): # Display the tools tools = get_all_tools() curr_category = None - + print("\n\nAvailable AgentStack Tools:") for category, tools in itertools.groupby(tools, lambda x: x.category): if curr_category != category: diff --git a/agentstack/generation/__init__.py b/agentstack/generation/__init__.py index f1073379..49d62c82 100644 --- a/agentstack/generation/__init__.py +++ b/agentstack/generation/__init__.py @@ -1,4 +1,4 @@ -from .agent_generation import generate_agent -from .task_generation import generate_task +from .agent_generation import generate_agent, get_agent_names +from .task_generation import generate_task, get_task_names from .tool_generation import add_tool, remove_tool from .files import ConfigFile, EnvFile, CONFIG_FILENAME \ No newline at end of file diff --git a/agentstack/generation/agent_generation.py b/agentstack/generation/agent_generation.py index 5f97138c..f13a5d9f 100644 --- a/agentstack/generation/agent_generation.py +++ b/agentstack/generation/agent_generation.py @@ -1,6 +1,6 @@ -from typing import Optional +from typing import Optional, List -from .gen_utils import insert_code_after_tag +from .gen_utils import insert_code_after_tag, get_crew_components, CrewComponent from agentstack.utils import verify_agentstack_project, get_framework import os from ruamel.yaml import YAML @@ -21,7 +21,7 @@ def generate_agent( if not backstory: backstory = 'Add your backstory here' if not llm: - llm = 'Add your llm here with format provider/model' + llm = 'openai/gpt-4o' verify_agentstack_project() @@ -45,7 +45,7 @@ def generate_crew_agent( role: Optional[str] = 'Add your role here', goal: Optional[str] = 'Add your goal here', backstory: Optional[str] = 'Add your backstory here', - llm: Optional[str] = 'Add your llm here with format provider/model' + llm: Optional[str] = 'openai/gpt-4o' ): config_path = os.path.join('src', 'config', 'agents.yaml') @@ -99,3 +99,8 @@ def generate_crew_agent( ] insert_code_after_tag(file_path, tag, code_to_insert) + + +def get_agent_names(framework: str = 'crewai', path: str = '') -> List[str]: + """Get only agent names from the crew file""" + return get_crew_components(framework, CrewComponent.AGENT, path)['agents'] \ No newline at end of file diff --git a/agentstack/generation/gen_utils.py b/agentstack/generation/gen_utils.py index 7256fbef..f9ac0e5f 100644 --- a/agentstack/generation/gen_utils.py +++ b/agentstack/generation/gen_utils.py @@ -1,4 +1,9 @@ import ast +import sys +from enum import Enum +from typing import Optional, Union, List + +from agentstack.utils import term_color def insert_code_after_tag(file_path, tag, code_to_insert, next_line=False): @@ -66,3 +71,72 @@ def string_in_file(file_path: str, str_to_match: str) -> bool: file_content = file.read() return str_to_match in file_content + +def _framework_filename(framework: str, path: str = ''): + if framework == 'crewai': + return f'{path}src/crew.py' + + print(term_color(f'Unknown framework: {framework}', 'red')) + sys.exit(1) + + +class CrewComponent(str, Enum): + AGENT = "agent" + TASK = "task" + + +def get_crew_components( + framework: str = 'crewai', + component_type: Optional[Union[CrewComponent, List[CrewComponent]]] = None, + path: str = '' +) -> dict[str, List[str]]: + """ + Get names of components (agents and/or tasks) defined in a crew file. + + Args: + framework: Name of the framework + component_type: Optional filter for specific component types. + Can be CrewComponentType.AGENT, CrewComponentType.TASK, + or a list of types. If None, returns all components. + path: Optional path to the framework file + + Returns: + Dictionary with 'agents' and 'tasks' keys containing lists of names + """ + filename = _framework_filename(framework, path) + + # Convert single component type to list for consistent handling + if isinstance(component_type, CrewComponent): + component_type = [component_type] + + # Read the source file + with open(filename, 'r') as f: + source = f.read() + + # Parse the source into an AST + tree = ast.parse(source) + + components = { + 'agents': [], + 'tasks': [] + } + + # Find all function definitions with relevant decorators + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef): + # Check decorators + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name): + if (component_type is None or CrewComponent.AGENT in component_type) \ + and decorator.id == 'agent': + components['agents'].append(node.name) + elif (component_type is None or CrewComponent.TASK in component_type) \ + and decorator.id == 'task': + components['tasks'].append(node.name) + + # If specific types were requested, only return those + if component_type: + return {k: v for k, v in components.items() + if CrewComponent(k[:-1]) in component_type} + + return components diff --git a/agentstack/generation/task_generation.py b/agentstack/generation/task_generation.py index 7976fe8d..d2fc6ebc 100644 --- a/agentstack/generation/task_generation.py +++ b/agentstack/generation/task_generation.py @@ -1,6 +1,6 @@ -from typing import Optional +from typing import Optional, List -from .gen_utils import insert_code_after_tag, insert_after_tasks +from .gen_utils import insert_after_tasks, get_crew_components, CrewComponent from ..utils import verify_agentstack_project, get_framework import os from ruamel.yaml import YAML @@ -87,3 +87,8 @@ def generate_crew_task( ] insert_after_tasks(file_path, code_to_insert) + + +def get_task_names(framework: str, path: str = '') -> List[str]: + """Get only task names from the crew file""" + return get_crew_components(framework, CrewComponent.TASK, path)['tasks'] \ No newline at end of file diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 8eb68692..c4e19fc0 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -3,8 +3,17 @@ import importlib.resources from pathlib import Path import json +import sys +from typing import Optional, List, Dict, Union + +from . import get_agent_names +from .gen_utils import insert_code_after_tag, string_in_file, _framework_filename +from ..utils import open_json_file, get_framework, term_color +import os import shutil import fileinput +import astor +import ast from pydantic import BaseModel, ValidationError from agentstack.utils import get_package_path @@ -16,7 +25,7 @@ TOOL_INIT_FILENAME = "src/tools/__init__.py" FRAMEWORK_FILENAMES: dict[str, str] = { - 'crewai': 'src/crew.py', + 'crewai': 'src/crew.py', } def get_framework_filename(framework: str, path: str = ''): @@ -41,7 +50,7 @@ class ToolConfig(BaseModel): packages: Optional[List[str]] = None post_install: Optional[str] = None post_remove: Optional[str] = None - + @classmethod def from_tool_name(cls, name: str) -> 'ToolConfig': path = get_package_path() / f'tools/{name}.json' @@ -60,10 +69,10 @@ def from_json(cls, path: Path) -> 'ToolConfig': for error in e.errors(): print(f"{' '.join(error['loc'])}: {error['msg']}") sys.exit(1) - + def get_import_statement(self) -> str: return f"from .{self.name}_tool import {', '.join(self.tools)}" - + def get_impl_file_path(self, framework: str) -> Path: return get_package_path() / f'templates/{framework}/tools/{self.name}_tool.py' @@ -81,27 +90,29 @@ def get_all_tool_names() -> list[str]: def get_all_tools() -> list[ToolConfig]: return [ToolConfig.from_json(path) for path in get_all_tool_paths()] -def add_tool(tool_name: str, path: Optional[str] = None): +def add_tool(tool_name: str, path: Optional[str] = None, agents: Optional[List[str]] = []): if path: path = path.endswith('/') and path or path + '/' else: path = './' - + framework = get_framework(path) agentstack_config = ConfigFile(path) - + if tool_name in agentstack_config.tools: print(term_color(f'Tool {tool_name} is already installed', 'red')) sys.exit(1) tool_data = ToolConfig.from_tool_name(tool_name) tool_file_path = tool_data.get_impl_file_path(framework) + + if tool_data.packages: os.system(f"poetry add {' '.join(tool_data.packages)}") # Install packages shutil.copy(tool_file_path, f'{path}src/tools/{tool_name}_tool.py') # Move tool from package to project add_tool_to_tools_init(tool_data, path) # Export tool from tools dir - add_tool_to_agent_definition(framework, tool_data, path) # Add tool to agent definition - + add_tool_to_agent_definition(framework=framework, tool_data=tool_data, path=path, agents=agents) # Add tool to agent definition + if tool_data.env: # add environment variables which don't exist with EnvFile(path) as env: for var, value in tool_data.env.items(): @@ -109,7 +120,7 @@ def add_tool(tool_name: str, path: Optional[str] = None): with EnvFile(path, filename=".env.example") as env: for var, value in tool_data.env.items(): env.append_if_new(var, value) - + if tool_data.post_install: os.system(tool_data.post_install) @@ -120,15 +131,16 @@ def add_tool(tool_name: str, path: Optional[str] = None): if tool_data.cta: print(term_color(f'🪩 {tool_data.cta}', 'blue')) + def remove_tool(tool_name: str, path: Optional[str] = None): if path: path = path.endswith('/') and path or path + '/' else: path = './' - + framework = get_framework() agentstack_config = ConfigFile(path) - + if not tool_name in agentstack_config.tools: print(term_color(f'Tool {tool_name} is not installed', 'red')) sys.exit(1) @@ -145,18 +157,20 @@ def remove_tool(tool_name: str, path: Optional[str] = None): if tool_data.post_remove: os.system(tool_data.post_remove) # We don't remove the .env variables to preserve user data. - + with agentstack_config as config: config.tools.remove(tool_name) - + print(term_color(f'🔨 Tool {tool_name}', 'green'), term_color('removed', 'red'), term_color('from agentstack project successfully', 'green')) + def add_tool_to_tools_init(tool_data: ToolConfig, path: str = ''): file_path = f'{path}{TOOL_INIT_FILENAME}' tag = '# tool import' code_to_insert = [tool_data.get_import_statement(), ] insert_code_after_tag(file_path, tag, code_to_insert, next_line=True) + def remove_tool_from_tools_init(tool_data: ToolConfig, path: str = ''): """Search for the import statement in the init and remove it.""" file_path = f'{path}{TOOL_INIT_FILENAME}' @@ -166,13 +180,215 @@ def remove_tool_from_tools_init(tool_data: ToolConfig, path: str = ''): if line.strip() != import_statement: print(line, end='') -def add_tool_to_agent_definition(framework: str, tool_data: ToolConfig, path: str = ''): - with fileinput.input(files=get_framework_filename(framework, path), inplace=True) as f: - for line in f: - print(line.replace('tools=[', f'tools=[{"*" if tool_data.tools_bundled else ""}tools.{", tools.".join(tool_data.tools)}, '), end='') + +def add_tool_to_agent_definition(framework: str, tool_data: ToolConfig, path: str = '', agents: list[str] = []): + """ + Add tools to specific agent definitions using AST transformation. + + Args: + framework: Name of the framework + tool_data: ToolConfig + agents: Optional list of agent names to modify. If None, modifies all agents. + path: Optional path to the framework file + """ + modify_agent_tools(framework=framework, tool_data=tool_data, operation='add', agents=agents, path=path, base_name='tools') + def remove_tool_from_agent_definition(framework: str, tool_data: ToolConfig, path: str = ''): - with fileinput.input(files=get_framework_filename(framework, path), inplace=True) as f: - for line in f: - print(line.replace(f'{", ".join([f"tools.{name}" for name in tool_data.tools])}, ', ''), end='') + modify_agent_tools(framework=framework, tool_data=tool_data, operation='remove', agents=None, path=path, base_name='tools') + + +def _create_tool_attribute(tool_name: str, base_name: str = 'tools') -> ast.Attribute: + """Create an AST node for a tool attribute""" + return ast.Attribute( + value=ast.Name(id=base_name, ctx=ast.Load()), + attr=tool_name, + ctx=ast.Load() + ) + +def _create_starred_tool(tool_name: str, base_name: str = 'tools') -> ast.Starred: + """Create an AST node for a starred tool expression""" + return ast.Starred( + value=ast.Attribute( + value=ast.Name(id=base_name, ctx=ast.Load()), + attr=tool_name, + ctx=ast.Load() + ), + ctx=ast.Load() + ) + + +def _create_tool_attributes( + tool_names: List[str], + base_name: str = 'tools' +) -> List[ast.Attribute]: + """Create AST nodes for multiple tool attributes""" + return [_create_tool_attribute(name, base_name) for name in tool_names] + + +def _create_tool_nodes( + tool_names: List[str], + is_bundled: bool = False, + base_name: str = 'tools' +) -> List[Union[ast.Attribute, ast.Starred]]: + """Create AST nodes for multiple tool attributes""" + return [ + _create_starred_tool(name, base_name) if is_bundled + else _create_tool_attribute(name, base_name) + for name in tool_names + ] + + +def _is_tool_node_match(node: ast.AST, tool_name: str, base_name: str = 'tools') -> bool: + """ + Check if an AST node matches a tool reference, regardless of whether it's starred + + Args: + node: AST node to check (can be Attribute or Starred) + tool_name: Name of the tool to match + base_name: Base module name (default: 'tools') + + Returns: + bool: True if the node matches the tool reference + """ + # If it's a Starred node, check its value + if isinstance(node, ast.Starred): + node = node.value + + # Extract the attribute name and base regardless of node type + if isinstance(node, ast.Attribute): + is_base_match = (isinstance(node.value, ast.Name) and + node.value.id == base_name) + is_name_match = node.attr == tool_name + return is_base_match and is_name_match + + return False + + +def _process_tools_list( + current_tools: List[ast.AST], + tool_data: ToolConfig, + operation: str, + base_name: str = 'tools' +) -> List[ast.AST]: + """ + Process a tools list according to the specified operation. + + Args: + current_tools: Current list of tool nodes + tool_data: Tool configuration + operation: Operation to perform ('add' or 'remove') + base_name: Base module name for tools + """ + if operation == 'add': + new_tools = current_tools.copy() + # Add new tools with bundling if specified + new_tools.extend(_create_tool_nodes( + tool_data.tools, + tool_data.tools_bundled, + base_name + )) + return new_tools + + elif operation == 'remove': + # Filter out tools that match any in the removal list + return [ + tool for tool in current_tools + if not any(_is_tool_node_match(tool, name, base_name) + for name in tool_data.tools) + ] + + raise ValueError(f"Unsupported operation: {operation}") + + +def _modify_agent_tools( + node: ast.FunctionDef, + tool_data: ToolConfig, + operation: str, + agents: Optional[List[str]] = None, + base_name: str = 'tools' +) -> ast.FunctionDef: + """ + Modify the tools list in an agent definition. + + Args: + node: AST node of the function to modify + tool_data: Tool configuration + operation: Operation to perform ('add' or 'remove') + agents: Optional list of agent names to modify + base_name: Base module name for tools + """ + # Skip if not in specified agents list + if agents is not None and agents != []: + if node.name not in agents: + return node + + # Check if this is an agent-decorated function + if not any(isinstance(d, ast.Name) and d.id == 'agent' + for d in node.decorator_list): + return node + + # Find the Return statement and modify tools + for item in node.body: + if isinstance(item, ast.Return): + agent_call = item.value + if isinstance(agent_call, ast.Call): + for kw in agent_call.keywords: + if kw.arg == 'tools': + if isinstance(kw.value, ast.List): + # Process the tools list + new_tools = _process_tools_list( + kw.value.elts, + tool_data, + operation, + base_name + ) + + # Replace with new list + kw.value = ast.List(elts=new_tools, ctx=ast.Load()) + + return node + + +def modify_agent_tools( + framework: str, + tool_data: ToolConfig, + operation: str, + agents: Optional[List[str]] = None, + path: str = '', + base_name: str = 'tools' +) -> None: + """ + Modify tools in agent definitions using AST transformation. + + Args: + framework: Name of the framework + tool_data: ToolConfig + operation: Operation to perform ('add' or 'remove') + agents: Optional list of agent names to modify + path: Optional path to the framework file + base_name: Base module name for tools (default: 'tools') + """ + if agents is not None: + valid_agents = get_agent_names(path=path) + for agent in agents: + if agent not in valid_agents: + print(term_color(f"Agent '{agent}' not found in the project.", 'red')) + sys.exit(1) + + filename = _framework_filename(framework, path) + + with open(filename, 'r') as f: + source = f.read() + + tree = ast.parse(source) + + class ModifierTransformer(ast.NodeTransformer): + def visit_FunctionDef(self, node): + return _modify_agent_tools(node, tool_data, operation, agents, base_name) + + modified_tree = ModifierTransformer().visit(tree) + modified_source = astor.to_source(modified_tree) + with open(filename, 'w') as f: + f.write(modified_source) \ No newline at end of file diff --git a/agentstack/main.py b/agentstack/main.py index e72871c6..14a448cf 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -25,10 +25,14 @@ def main(): # 'quickstart' command subparsers.add_parser('quickstart', help='Open the quickstart guide') + # 'templates' command + subparsers.add_parser('templates', help='View Agentstack templates') + # 'init' command init_parser = subparsers.add_parser('init', aliases=['i'], help='Initialize a directory for the project') init_parser.add_argument('slug_name', nargs='?', help="The directory name to place the project in") - init_parser.add_argument('--no-wizard', action='store_true', help="Skip wizard steps") + init_parser.add_argument('--wizard', '-w', action='store_true', help="Use the setup wizard") + init_parser.add_argument('--template', '-t', help="Agent template to use") # 'run' command run_parser = subparsers.add_parser('run', aliases=['r'], help='Run your agent') @@ -66,6 +70,8 @@ def main(): # 'add' command under 'tools' tools_add_parser = tools_subparsers.add_parser('add', aliases=['a'], help='Add a new tool') tools_add_parser.add_argument('name', help='Name of the tool to add') + tools_add_parser.add_argument('--agents', '-a', help='Name of agents to add this tool to, comma separated') + tools_add_parser.add_argument('--agent', help='Name of agent to add this tool to') # 'remove' command under 'tools' tools_remove_parser = tools_subparsers.add_parser('remove', aliases=['r'], help='Remove a tool') @@ -84,11 +90,13 @@ def main(): # Handle commands if args.command in ['docs']: webbrowser.open('https://docs.agentstack.sh/') - if args.command in ['quickstart']: + elif args.command in ['quickstart']: + webbrowser.open('https://docs.agentstack.sh/quickstart') + elif args.command in ['templates']: webbrowser.open('https://docs.agentstack.sh/quickstart') - if args.command in ['init', 'i']: - init_project_builder(args.slug_name, args.no_wizard) - if args.command in ['run', 'r']: + elif args.command in ['init', 'i']: + init_project_builder(args.slug_name, args.template, args.wizard) + elif args.command in ['run', 'r']: framework = get_framework() if framework == "crewai": os.system('python src/main.py') @@ -103,7 +111,9 @@ def main(): if args.tools_command in ['list', 'l']: list_tools() elif args.tools_command in ['add', 'a']: - generation.add_tool(args.name) + agents = [args.agent] if args.agent else None + agents = args.agents.split(',') if args.agents else agents + generation.add_tool(args.name, agents=agents) elif args.tools_command in ['remove', 'r']: generation.remove_tool(args.name) else: diff --git a/agentstack/telemetry.py b/agentstack/telemetry.py index 01fb7e09..db613c8c 100644 --- a/agentstack/telemetry.py +++ b/agentstack/telemetry.py @@ -33,7 +33,7 @@ TELEMETRY_URL = 'https://api.agentstack.sh/telemetry' def collect_machine_telemetry(command: str): - if get_telemetry_opt_out(): + if command != "init" and get_telemetry_opt_out(): return telemetry_data = { diff --git a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/agentstack.json b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/agentstack.json index ed01b193..5511a17a 100644 --- a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/agentstack.json +++ b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/agentstack.json @@ -1,3 +1,6 @@ { - "framework": "{{ cookiecutter.framework }}" + "framework": "{{ cookiecutter.framework }}", + "agentstack_version": "{{ cookiecutter.project_metadata.agentstack_version }}", + "template": "{{ cookiecutter.project_metadata.template }}", + "template_version": "{{ cookiecutter.project_metadata.template_version }}" } \ No newline at end of file diff --git a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py index 2d48645c..277e439c 100644 --- a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py +++ b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py @@ -13,7 +13,7 @@ def run(): Run the crew. """ inputs = { - 'topic': 'AI LLMs' + 'key': 'value' } {{cookiecutter.project_metadata.project_name|replace('-', '')|replace('_', '')|capitalize}}Crew().crew().kickoff(inputs=inputs) @@ -23,7 +23,7 @@ def train(): Train the crew for a given number of iterations. """ inputs = { - "topic": "AI LLMs" + 'key': 'value' } try: {{cookiecutter.project_metadata.project_name|replace('-', '')|replace('_', '')|capitalize}}Crew().crew().train(n_iterations=int(sys.argv[1]), filename=sys.argv[2], inputs=inputs) @@ -48,7 +48,7 @@ def test(): Test the crew execution and returns the results. """ inputs = { - "topic": "AI LLMs" + 'key': 'value' } try: {{cookiecutter.project_metadata.project_name|replace('-', '')|replace('_', '')|capitalize}}Crew().crew().test(n_iterations=int(sys.argv[1]), openai_model_name=sys.argv[2], inputs=inputs) diff --git a/agentstack/templates/proj_templates/chatbot.json b/agentstack/templates/proj_templates/chatbot.json new file mode 100644 index 00000000..1deef694 --- /dev/null +++ b/agentstack/templates/proj_templates/chatbot.json @@ -0,0 +1,16 @@ +{ + "name": "chatbot", + "tools": ["mem0"], + "agents": [{ + + }], + "tasks": [{ + + }], + "code_swap": [ + { + "direction": "out", + "file": + } + ] +} \ No newline at end of file diff --git a/agentstack/templates/proj_templates/research.json b/agentstack/templates/proj_templates/research.json new file mode 100644 index 00000000..b27b19c7 --- /dev/null +++ b/agentstack/templates/proj_templates/research.json @@ -0,0 +1,35 @@ +{ + "name": "research", + "description": "Starter project research agent", + "template_version": 1, + "framework": "crewai", + "agents": [{ + "name": "researcher", + "role": "Gather data using research tools.", + "goal": "Collect all relevant information asked for using the tools available to you. The data will be analyzed later, compile a result of all data that you believe to be relevant to the query.", + "backstory": "You are an expert researcher. You are given a query and are tasked with providing as much relevant data in a concise manner.", + "model": "openai/gpt-4o" + },{ + "name": "analyst", + "role": "Analyze gathered data.", + "goal": "Analyze and consolidate the data gathered from research to adequately answer the query provided in the task.", + "backstory": "You are an expert analyst. You are given a collection of research results and should use your knowledge to make conclusions on the data without making any assumptions that are not specifically supported by the data.", + "model": "openai/gpt-4o" + }], + "tasks": [{ + "name": "research", + "description": "use the tools at your disposal to thoroughly research the query: {query}", + "expected_output": "text output describing what what information was discovered, with labels defining where it was found. only output content found directly through research, do not modify those results in any way. Cite sources in the result.", + "agent": "researcher" + },{ + "name": "analyze", + "description": "Using the data gathered by the researcher, analyze the data and form a conclusion that reports key insights on that data pertaining to the query: {query}", + "expected_output": "A report in markdown format outlining the key insights of the research and accurately answering the query.", + "agent": "analyst" + }], + "tools": [{ + "name": "perplexity", + "agents": ["researcher"] + }], + "method": "sequential" +} \ No newline at end of file diff --git a/examples/web_researcher/.env.example b/examples/web_researcher/.env.example index 149da232..2575e4b9 100644 --- a/examples/web_researcher/.env.example +++ b/examples/web_researcher/.env.example @@ -4,4 +4,7 @@ OPENAI_API_KEY=... # Tools FIRECRAWL_API_KEY=... -NEON_API_KEY=... \ No newline at end of file +NEON_API_KEY=... +FTP_HOST=... +FTP_USER=... +FTP_PASSWORD=... \ No newline at end of file diff --git a/examples/web_researcher/src/crew.py b/examples/web_researcher/src/crew.py index 098e93f0..01cfac22 100644 --- a/examples/web_researcher/src/crew.py +++ b/examples/web_researcher/src/crew.py @@ -4,60 +4,39 @@ @CrewBase -class WebresearcherCrew(): +class WebresearcherCrew: """web_researcher crew""" - # Agent definitions @agent - def content_summarizer(self) -> Agent: - return Agent( - config=self.agents_config['content_summarizer'], - tools=[], # add tools here or use `agentstack tools add - verbose=True - ) + def content_summarizer(self) ->Agent: + return Agent(config=self.agents_config['content_summarizer'], tools + =[], verbose=True) @agent - def web_scraper(self) -> Agent: - return Agent( - config=self.agents_config['web_scraper'], - tools=[tools.web_scrape], # add tools here or use `agentstack tools add - verbose=True - ) + def web_scraper(self) ->Agent: + return Agent(config=self.agents_config['web_scraper'], tools=[tools + .web_scrape], verbose=True) @agent - def content_storer(self) -> Agent: - return Agent( - config=self.agents_config['content_storer'], - tools=[tools.create_database, tools.execute_sql_ddl, tools.run_sql_query], # add tools here or use `agentstack tools add - verbose=True - ) - - # Task definitions + def content_storer(self) ->Agent: + return Agent(config=self.agents_config['content_storer'], tools=[ + tools.create_database, tools.execute_sql_ddl, tools. + run_sql_query], verbose=True) + @task - def scrape_site(self) -> Task: - return Task( - config=self.tasks_config['scrape_site'], - ) + def scrape_site(self) ->Task: + return Task(config=self.tasks_config['scrape_site']) @task - def summarize(self) -> Task: - return Task( - config=self.tasks_config['summarize'], - ) + def summarize(self) ->Task: + return Task(config=self.tasks_config['summarize']) @task - def store(self) -> Task: - return Task( - config=self.tasks_config['store'], - ) + def store(self) ->Task: + return Task(config=self.tasks_config['store']) @crew - def crew(self) -> Crew: + def crew(self) ->Crew: """Creates the Test crew""" - return Crew( - agents=self.agents, # Automatically created by the @agent decorator - tasks=self.tasks, # Automatically created by the @task decorator - process=Process.sequential, - verbose=True, - # process=Process.hierarchical, # In case you wanna use that instead https://docs.crewai.com/how-to/Hierarchical/ - ) \ No newline at end of file + return Crew(agents=self.agents, tasks=self.tasks, process=Process. + sequential, verbose=True) diff --git a/examples/web_researcher/src/tools/__init__.py b/examples/web_researcher/src/tools/__init__.py index 83e7d9c9..fafc6f7e 100644 --- a/examples/web_researcher/src/tools/__init__.py +++ b/examples/web_researcher/src/tools/__init__.py @@ -1,6 +1,4 @@ # tool import - from .firecrawl_tool import web_scrape, web_crawl, retrieve_web_crawl - -from .neon_tool import create_database, execute_sql_ddl, run_sql_query \ No newline at end of file +from .neon_tool import create_database, execute_sql_ddl, run_sql_query diff --git a/pyproject.toml b/pyproject.toml index a8de616e..6b49c749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentstack" -version = "0.1.12" +version = "0.2.0" description = "The fastest way to build robust AI agents" authors = [ { name="Braelyn Boynton", email="bboynton97@gmail.com" } @@ -21,6 +21,8 @@ dependencies = [ "ruamel.yaml.base>=0.3.2", "cookiecutter==2.6.0", "psutil==5.9.0", + "astor==0.8.1", + "psutil==5.9.0", "pydantic>=2.10", ] diff --git a/tests/test_cli_loads.py b/tests/test_cli_loads.py index 98ff2318..49bb15cd 100644 --- a/tests/test_cli_loads.py +++ b/tests/test_cli_loads.py @@ -37,7 +37,7 @@ def test_init_command(self): if test_dir.exists(): shutil.rmtree(test_dir) - result = self.run_cli("init", str(test_dir), "--no-wizard") + result = self.run_cli("init", str(test_dir)) self.assertEqual(result.returncode, 0) self.assertTrue(test_dir.exists())