diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d0c57fe..3f09f135 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,5 +35,16 @@ Adding tools is easy once you understand the project structure. A few things nee - The tools that are exported from this file should be listed in the tool's config json. 4. Manually test your tool integration by running `agentstack tools add ` and ensure it behaves as expected. +## Before creating your PR +Be sure that you are opening a PR using a branch other than `main` on your fork. This enables us +to pull your branch and make modifications to the PR with your permission that may be helpful. + +### Formatting +AgentStack uses Ruff formatter for consistent code formatting. To format your code, run: +```bash +pip install ruff +ruff format . +``` + ## Tests HAHAHAHAHAHAHA good one \ No newline at end of file diff --git a/agentstack/cli/agentstack_data.py b/agentstack/cli/agentstack_data.py index 4acd39c3..25dd88a3 100644 --- a/agentstack/cli/agentstack_data.py +++ b/agentstack/cli/agentstack_data.py @@ -7,19 +7,22 @@ class ProjectMetadata: - def __init__(self, - project_name: str = None, - project_slug: str = None, - description: str = "", - author_name: str = "", - version: str = "", - license: str = "", - year: int = datetime.now().year, - template: str = "none", - template_version: str = "0", - ): + def __init__( + self, + project_name: str = None, + project_slug: str = None, + description: str = "", + author_name: str = "", + version: str = "", + license: str = "", + 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 + self.project_slug = ( + clean_input(project_slug) if project_slug else self.project_name + ) self.description = description self.author_name = author_name self.version = version @@ -76,10 +79,11 @@ def to_json(self): class FrameworkData: - def __init__(self, - # name: Optional[Literal["crewai"]] = None - name: str = None # TODO: better framework handling, Literal or Enum - ): + def __init__( + self, + # name: Optional[Literal["crewai"]] = None + name: str = None, # TODO: better framework handling, Literal or Enum + ): self.name = name def to_dict(self): @@ -92,12 +96,13 @@ def to_json(self): class CookiecutterData: - def __init__(self, - project_metadata: ProjectMetadata, - structure: ProjectStructure, - # framework: Literal["crewai"], - framework: str, - ): + def __init__( + self, + project_metadata: ProjectMetadata, + structure: ProjectStructure, + # framework: Literal["crewai"], + framework: str, + ): self.project_metadata = project_metadata self.framework = framework self.structure = structure diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 16f3684c..dc447ce7 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -13,7 +13,12 @@ import importlib.resources from cookiecutter.main import cookiecutter -from .agentstack_data import FrameworkData, ProjectMetadata, ProjectStructure, CookiecutterData +from .agentstack_data import ( + FrameworkData, + ProjectMetadata, + ProjectStructure, + CookiecutterData, +) from agentstack.logger import log from agentstack.utils import get_package_path from agentstack.generation.files import ConfigFile @@ -30,7 +35,12 @@ 'anthropic/claude-3-opus', ] -def init_project_builder(slug_name: Optional[str] = None, template: Optional[str] = None, use_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 @@ -42,16 +52,23 @@ def init_project_builder(slug_name: Optional[str] = None, template: Optional[str template_data = None if template is not None: url_start = "https://" - if template[:len(url_start)] == url_start: + 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')) + 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: + 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) @@ -63,7 +80,7 @@ def init_project_builder(slug_name: Optional[str] = None, template: Optional[str "version": "0.0.1", "description": template_data['description'], "author": "Name ", - "license": "MIT" + "license": "MIT", } framework = template_data['framework'] design = { @@ -89,16 +106,12 @@ def init_project_builder(slug_name: Optional[str] = None, template: Optional[str "version": "0.0.1", "description": "New agentstack project", "author": "Name ", - "license": "MIT" + "license": "MIT", } framework = "crewai" # TODO: if --no-wizard, require a framework flag - design = { - 'agents': [], - 'tasks': [], - 'inputs': [] - } + design = {'agents': [], 'tasks': [], 'inputs': []} tools = [] @@ -109,12 +122,19 @@ def init_project_builder(slug_name: Optional[str] = None, template: Optional[str ) 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']) + generation.add_tool( + tool_data['name'], agents=tool_data['agents'], path=project_details['name'] + ) try: packaging.install(f'{AGENTSTACK_PACKAGE}[{framework}]', path=slug_name) except Exception as e: - print(term_color(f"Failed to install dependencies for {slug_name}. Please try again by running `agentstack update`", 'red')) + print( + term_color( + f"Failed to install dependencies for {slug_name}. Please try again by running `agentstack update`", + 'red', + ) + ) def welcome_message(): @@ -134,8 +154,8 @@ def configure_default_model(path: Optional[str] = None): """Set the default model""" agentstack_config = ConfigFile(path) if agentstack_config.default_model: - return # Default model already set - + return # Default model already set + print("Project does not have a default model configured.") other_msg = f"Other (enter a model name)" model = inquirer.list_input( @@ -143,10 +163,12 @@ def configure_default_model(path: Optional[str] = None): choices=PREFERRED_MODELS + [other_msg], ) - if model == other_msg: # If the user selects "Other", prompt for a model name - print(f'A list of available models is available at: "https://docs.litellm.ai/docs/providers"') + if model == other_msg: # If the user selects "Other", prompt for a model name + print( + f'A list of available models is available at: "https://docs.litellm.ai/docs/providers"' + ) model = inquirer.text(message="Enter the model name") - + with ConfigFile(path) as agentstack_config: agentstack_config.default_model = model @@ -172,7 +194,9 @@ def ask_framework() -> str: # choices=["CrewAI", "Autogen", "LiteLLM"], # ) - print("Congrats! Your project is ready to go! Quickly add features now or skip to do it later.\n\n") + print( + "Congrats! Your project is ready to go! Quickly add features now or skip to do it later.\n\n" + ) return framework @@ -183,10 +207,7 @@ def ask_design() -> dict: ) if not use_wizard: - return { - 'agents': [], - 'tasks': [] - } + return {'agents': [], 'tasks': []} os.system("cls" if os.name == "nt" else "clear") @@ -208,24 +229,36 @@ def ask_design() -> dict: agent_incomplete = True agent = None while agent_incomplete: - agent = inquirer.prompt([ - inquirer.Text("name", message="What's the name of this agent? (snake_case)"), - inquirer.Text("role", message="What role does this agent have?"), - inquirer.Text("goal", message="What is the goal of the agent?"), - inquirer.Text("backstory", message="Give your agent a backstory"), - # TODO: make a list - #2 - inquirer.Text('model', message="What LLM should this agent use? (any LiteLLM provider)", default="openai/gpt-4"), - # inquirer.List("model", message="What LLM should this agent use? (any LiteLLM provider)", choices=[ - # 'mixtral_llm', - # 'mixtral_llm', - # ]), - ]) + agent = inquirer.prompt( + [ + inquirer.Text( + "name", message="What's the name of this agent? (snake_case)" + ), + inquirer.Text("role", message="What role does this agent have?"), + inquirer.Text("goal", message="What is the goal of the agent?"), + inquirer.Text("backstory", message="Give your agent a backstory"), + # TODO: make a list - #2 + inquirer.Text( + 'model', + message="What LLM should this agent use? (any LiteLLM provider)", + default="openai/gpt-4", + ), + # inquirer.List("model", message="What LLM should this agent use? (any LiteLLM provider)", choices=[ + # 'mixtral_llm', + # 'mixtral_llm', + # ]), + ] + ) if not agent['name'] or agent['name'] == '': print(term_color("Error: Agent name is required - Try again", 'red')) agent_incomplete = True elif not is_snake_case(agent['name']): - print(term_color("Error: Agent name must be snake case - Try again", 'red')) + print( + term_color( + "Error: Agent name must be snake case - Try again", 'red' + ) + ) else: agent_incomplete = False @@ -251,19 +284,32 @@ def ask_design() -> dict: task_incomplete = True task = None while task_incomplete: - task = inquirer.prompt([ - inquirer.Text("name", message="What's the name of this task? (snake_case)"), - inquirer.Text("description", message="Describe the task in more detail"), - inquirer.Text("expected_output", - message="What do you expect the result to look like? (ex: A 5 bullet point summary of the email)"), - inquirer.List("agent", message="Which agent should be assigned this task?", - choices=[a['name'] for a in agents], ), - ]) + task = inquirer.prompt( + [ + inquirer.Text( + "name", message="What's the name of this task? (snake_case)" + ), + inquirer.Text( + "description", message="Describe the task in more detail" + ), + inquirer.Text( + "expected_output", + message="What do you expect the result to look like? (ex: A 5 bullet point summary of the email)", + ), + inquirer.List( + "agent", + message="Which agent should be assigned this task?", + choices=[a['name'] for a in agents], + ), + ] + ) if not task['name'] or task['name'] == '': print(term_color("Error: Task name is required - Try again", 'red')) elif not is_snake_case(task['name']): - print(term_color("Error: Task name must be snake case - Try again", 'red')) + print( + term_color("Error: Task name must be snake case - Try again", 'red') + ) else: task_incomplete = False @@ -297,16 +343,18 @@ def ask_tools() -> list: tools_data = open_json_file(tools_json_path) while adding_tools: - tool_type = inquirer.list_input( message="What category tool do you want to add?", - choices=list(tools_data.keys()) + ["~~ Stop adding tools ~~"] + choices=list(tools_data.keys()) + ["~~ Stop adding tools ~~"], ) - tools_in_cat = [f"{t['name']} - {t['url']}" for t in tools_data[tool_type] if t not in tools_to_add] + tools_in_cat = [ + f"{t['name']} - {t['url']}" + for t in tools_data[tool_type] + if t not in tools_to_add + ] tool_selection = inquirer.list_input( - message="Select your tool", - choices=tools_in_cat + message="Select your tool", choices=tools_in_cat ) tools_to_add.append(tool_selection.split(' - ')[0]) @@ -321,42 +369,59 @@ def ask_tools() -> list: def ask_project_details(slug_name: Optional[str] = None) -> dict: - name = inquirer.text(message="What's the name of your project (snake_case)", default=slug_name or '') + name = inquirer.text( + message="What's the name of your project (snake_case)", default=slug_name or '' + ) if not is_snake_case(name): print(term_color("Project name must be snake case", 'red')) return ask_project_details(slug_name) - questions = inquirer.prompt([ - inquirer.Text("version", message="What's the initial version", default="0.1.0"), - inquirer.Text("description", message="Enter a description for your project"), - inquirer.Text("author", message="Who's the author (your name)?"), - ]) + questions = inquirer.prompt( + [ + inquirer.Text( + "version", message="What's the initial version", default="0.1.0" + ), + inquirer.Text( + "description", message="Enter a description for your project" + ), + inquirer.Text("author", message="Who's the author (your name)?"), + ] + ) questions['name'] = name return questions -def insert_template(project_details: dict, framework_name: str, design: dict, template_data: Optional[dict] = None): +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="0.0.1", - license="MIT", - 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_metadata = ProjectMetadata( + project_name=project_details["name"], + description=project_details["description"], + author_name=project_details["author"], + version="0.0.1", + license="MIT", + 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"] project_structure.tasks = design["tasks"] project_structure.set_inputs(design["inputs"]) - cookiecutter_data = CookiecutterData(project_metadata=project_metadata, - structure=project_structure, - framework=framework_name.lower()) + cookiecutter_data = CookiecutterData( + project_metadata=project_metadata, + structure=project_structure, + framework=framework_name.lower(), + ) template_path = get_package_path() / f'templates/{framework.name}' with open(f"{template_path}/cookiecutter.json", "w") as json_file: @@ -365,10 +430,16 @@ def insert_template(project_details: dict, framework_name: str, design: dict, te # copy .env.example to .env shutil.copy( f'{template_path}/{"{{cookiecutter.project_metadata.project_slug}}"}/.env.example', - f'{template_path}/{"{{cookiecutter.project_metadata.project_slug}}"}/.env') + f'{template_path}/{"{{cookiecutter.project_metadata.project_slug}}"}/.env', + ) if os.path.isdir(project_details['name']): - print(term_color(f"Directory {template_path} already exists. Please check this and try again", "red")) + print( + term_color( + f"Directory {template_path} already exists. Please check this and try again", + "red", + ) + ) return cookiecutter(str(template_path), no_input=True, extra_context=None) @@ -381,7 +452,9 @@ def insert_template(project_details: dict, framework_name: str, design: dict, te # subprocess.check_output(["git", "init"]) # subprocess.check_output(["git", "add", "."]) except: - print("Failed to initialize git repository. Maybe you're already in one? Do this with: git init") + print( + "Failed to initialize git repository. Maybe you're already in one? Do this with: git init" + ) # TODO: check if poetry is installed and if so, run poetry install in the new directory # os.system("poetry install") @@ -416,4 +489,4 @@ def list_tools(): print(f": {tool.url if tool.url else 'AgentStack default tool'}") print("\n\n✨ Add a tool with: agentstack tools add ") - print(" https://docs.agentstack.sh/tools/core") \ No newline at end of file + print(" https://docs.agentstack.sh/tools/core") diff --git a/agentstack/generation/agent_generation.py b/agentstack/generation/agent_generation.py index bf64dd2e..be053cce 100644 --- a/agentstack/generation/agent_generation.py +++ b/agentstack/generation/agent_generation.py @@ -9,13 +9,13 @@ def generate_agent( - name, - role: Optional[str], - goal: Optional[str], - backstory: Optional[str], - llm: Optional[str] + name, + role: Optional[str], + goal: Optional[str], + backstory: Optional[str], + llm: Optional[str], ): - agentstack_config = ConfigFile() # TODO path + agentstack_config = ConfigFile() # TODO path if not role: role = 'Add your role here' if not goal: @@ -40,11 +40,11 @@ def generate_agent( def generate_crew_agent( - name, - role: Optional[str] = 'Add your role here', - goal: Optional[str] = 'Add your goal here', - backstory: Optional[str] = 'Add your backstory here', - llm: Optional[str] = 'openai/gpt-4o' + name, + role: Optional[str] = 'Add your role here', + goal: Optional[str] = 'Add your goal here', + backstory: Optional[str] = 'Add your backstory here', + llm: Optional[str] = 'openai/gpt-4o', ): config_path = os.path.join('src', 'config', 'agents.yaml') @@ -68,7 +68,9 @@ def generate_crew_agent( # Handle None values role_str = FoldedScalarString(role) if role else FoldedScalarString('') goals_str = FoldedScalarString(goal) if goal else FoldedScalarString('') - backstory_str = FoldedScalarString(backstory) if backstory else FoldedScalarString('') + backstory_str = ( + FoldedScalarString(backstory) if backstory else FoldedScalarString('') + ) model_str = llm if llm else '' # Add new agent details @@ -76,7 +78,7 @@ def generate_crew_agent( 'role': role_str, 'goal': goals_str, 'backstory': backstory_str, - 'llm': model_str + 'llm': model_str, } # Write back to the file without altering existing content @@ -94,7 +96,7 @@ def generate_crew_agent( " tools=[], # add tools here or use `agentstack tools add ", # TODO: Add any tools in agentstack.json " verbose=True", " )", - "" + "", ] insert_code_after_tag(file_path, tag, code_to_insert) @@ -102,4 +104,4 @@ def generate_crew_agent( 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 + return get_crew_components(framework, CrewComponent.AGENT, path)['agents'] diff --git a/agentstack/generation/files.py b/agentstack/generation/files.py index b1c226c3..7aff5448 100644 --- a/agentstack/generation/files.py +++ b/agentstack/generation/files.py @@ -9,20 +9,21 @@ CONFIG_FILENAME = "agentstack.json" ENV_FILEMANE = ".env" + class ConfigFile(BaseModel): """ Interface for interacting with the agentstack.json file inside a project directory. Handles both data validation and file I/O. - + `path` is the directory where the agentstack.json file is located. Defaults to the current working directory. - + Use it as a context manager to make and save edits: ```python with ConfigFile() as config: config.tools.append('tool_name') ``` - + Config Schema ------------- framework: str @@ -30,15 +31,16 @@ class ConfigFile(BaseModel): tools: list[str] A list of tools that are currently installed in the project. telemetry_opt_out: Optional[bool] - Whether the user has opted out of telemetry. + Whether the user has opted out of telemetry. default_model: Optional[str] The default model to use when generating agent configurations. """ + framework: Optional[str] = DEFAULT_FRAMEWORK tools: list[str] = [] telemetry_opt_out: Optional[bool] = None default_model: Optional[str] = None - + def __init__(self, path: Union[str, Path, None] = None): path = Path(path) if path else Path.cwd() if os.path.exists(path / CONFIG_FILENAME): @@ -46,46 +48,52 @@ def __init__(self, path: Union[str, Path, None] = None): super().__init__(**json.loads(f.read())) else: raise FileNotFoundError(f"File {path / CONFIG_FILENAME} does not exist.") - self._path = path # attribute needs to be set after init + self._path = path # attribute needs to be set after init def model_dump(self, *args, **kwargs) -> dict: # Ignore None values dump = super().model_dump(*args, **kwargs) return {key: value for key, value in dump.items() if value is not None} - + def write(self): with open(self._path / CONFIG_FILENAME, 'w') as f: f.write(json.dumps(self.model_dump(), indent=4)) - - def __enter__(self) -> 'ConfigFile': return self - def __exit__(self, *args): self.write() + + def __enter__(self) -> 'ConfigFile': + return self + + def __exit__(self, *args): + self.write() class EnvFile: """ Interface for interacting with the .env file inside a project directory. - Unlike the ConfigFile, we do not re-write the entire file on every change, + Unlike the ConfigFile, we do not re-write the entire file on every change, and instead just append new lines to the end of the file. This preseres comments and other formatting that the user may have added and prevents opportunities for data loss. - + `path` is the directory where the .env file is located. Defaults to the current working directory. `filename` is the name of the .env file, defaults to '.env'. - + Use it as a context manager to make and save edits: ```python with EnvFile() as env: env.append_if_new('ENV_VAR', 'value') ``` """ + variables: dict[str, str] - - def __init__(self, path: Union[str, Path, None] = None, filename: str = ENV_FILEMANE): + + def __init__( + self, path: Union[str, Path, None] = None, filename: str = ENV_FILEMANE + ): self._path = Path(path) if path else Path.cwd() self._filename = filename self.read() - + def __getitem__(self, key): return self.variables[key] @@ -93,15 +101,15 @@ def __setitem__(self, key, value): if key in self.variables: raise ValueError("EnvFile does not allow overwriting values.") self.append_if_new(key, value) - + def __contains__(self, key) -> bool: return key in self.variables - + def append_if_new(self, key, value): - if not key in self.variables: + if key not in self.variables: self.variables[key] = value self._new_variables[key] = value - + def read(self): def parse_line(line): key, value = line.split('=') @@ -109,16 +117,20 @@ def parse_line(line): if os.path.exists(self._path / self._filename): with open(self._path / self._filename, 'r') as f: - self.variables = dict([parse_line(line) for line in f.readlines() if '=' in line]) + self.variables = dict( + [parse_line(line) for line in f.readlines() if '=' in line] + ) else: self.variables = {} self._new_variables = {} - + def write(self): with open(self._path / self._filename, 'a') as f: for key, value in self._new_variables.items(): f.write(f"\n{key}={value}") - - def __enter__(self) -> 'EnvFile': return self - def __exit__(self, *args): self.write() + def __enter__(self) -> 'EnvFile': + return self + + def __exit__(self, *args): + self.write() diff --git a/agentstack/generation/gen_utils.py b/agentstack/generation/gen_utils.py index f9ac0e5f..80ebcf8f 100644 --- a/agentstack/generation/gen_utils.py +++ b/agentstack/generation/gen_utils.py @@ -16,8 +16,11 @@ def insert_code_after_tag(file_path, tag, code_to_insert, next_line=False): for index, line in enumerate(lines): if tag in line: # Insert the code block after the tag - indented_code = [(line[:len(line)-len(line.lstrip())] + code_line + '\n') for code_line in code_to_insert] - lines[index+1:index+1] = indented_code + indented_code = [ + (line[: len(line) - len(line.lstrip())] + code_line + '\n') + for code_line in code_to_insert + ] + lines[index + 1 : index + 1] = indented_code break else: raise ValueError(f"Tag '{tag}' not found in the file.") @@ -36,8 +39,10 @@ def insert_after_tasks(file_path, code_to_insert): last_task_end = None last_task_start = None for node in ast.walk(module): - if isinstance(node, ast.FunctionDef) and \ - any(isinstance(deco, ast.Name) and deco.id == 'task' for deco in node.decorator_list): + if isinstance(node, ast.FunctionDef) and any( + isinstance(deco, ast.Name) and deco.id == 'task' + for deco in node.decorator_list + ): last_task_end = node.end_lineno last_task_start = node.lineno @@ -86,9 +91,9 @@ class CrewComponent(str, Enum): def get_crew_components( - framework: str = 'crewai', - component_type: Optional[Union[CrewComponent, List[CrewComponent]]] = None, - path: str = '' + 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. @@ -116,10 +121,7 @@ def get_crew_components( # Parse the source into an AST tree = ast.parse(source) - components = { - 'agents': [], - 'tasks': [] - } + components = {'agents': [], 'tasks': []} # Find all function definitions with relevant decorators for node in ast.walk(tree): @@ -127,16 +129,21 @@ def get_crew_components( # 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': + 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': + 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 { + 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 d2fc6ebc..c8a3da35 100644 --- a/agentstack/generation/task_generation.py +++ b/agentstack/generation/task_generation.py @@ -8,10 +8,10 @@ def generate_task( - name, - description: Optional[str], - expected_output: Optional[str], - agent: Optional[str] + name, + description: Optional[str], + expected_output: Optional[str], + agent: Optional[str], ): if not description: description = 'Add your description here' @@ -35,10 +35,10 @@ def generate_task( def generate_crew_task( - name, - description: Optional[str], - expected_output: Optional[str], - agent: Optional[str] + name, + description: Optional[str], + expected_output: Optional[str], + agent: Optional[str], ): config_path = os.path.join('src', 'config', 'tasks.yaml') @@ -60,8 +60,14 @@ def generate_crew_task( data = {} # Handle None values - description_str = FoldedScalarString(description) if description else FoldedScalarString('') - expected_output_str = FoldedScalarString(expected_output) if expected_output else FoldedScalarString('') + description_str = ( + FoldedScalarString(description) if description else FoldedScalarString('') + ) + expected_output_str = ( + FoldedScalarString(expected_output) + if expected_output + else FoldedScalarString('') + ) agent_str = FoldedScalarString(agent) if agent else FoldedScalarString('') # Add new agent details @@ -83,7 +89,7 @@ def generate_crew_task( " return Task(", f" config=self.tasks_config['{name}'],", " )", - "" + "", ] insert_after_tasks(file_path, code_to_insert) @@ -91,4 +97,4 @@ def generate_crew_task( 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 + return get_crew_components(framework, CrewComponent.TASK, path)['tasks'] diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index ba3d30aa..20930683 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -28,6 +28,7 @@ 'crewai': 'src/crew.py', } + def get_framework_filename(framework: str, path: str = ''): if path: path = path.endswith('/') and path or path + '/' @@ -39,6 +40,7 @@ def get_framework_filename(framework: str, path: str = ''): print(term_color(f'Unknown framework: {framework}', 'red')) sys.exit(1) + class ToolConfig(BaseModel): name: str category: str @@ -76,6 +78,7 @@ def get_import_statement(self) -> str: def get_impl_file_path(self, framework: str) -> Path: return get_package_path() / f'templates/{framework}/tools/{self.name}_tool.py' + def get_all_tool_paths() -> list[Path]: paths = [] tools_dir = get_package_path() / 'tools' @@ -84,13 +87,18 @@ def get_all_tool_paths() -> list[Path]: paths.append(file) return paths + def get_all_tool_names() -> list[str]: return [path.stem for path in get_all_tool_paths()] + 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, agents: Optional[List[str]] = []): + +def add_tool( + tool_name: str, path: Optional[str] = None, agents: Optional[List[str]] = [] +): if path: path = path.endswith('/') and path or path + '/' else: @@ -108,11 +116,15 @@ def add_tool(tool_name: str, path: Optional[str] = None, agents: Optional[List[s if tool_data.packages: packaging.install(' '.join(tool_data.packages)) - shutil.copy(tool_file_path, f'{path}src/tools/{tool_name}_tool.py') # Move tool from package to project + 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=framework, tool_data=tool_data, path=path, agents=agents) # 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 + if tool_data.env: # add environment variables which don't exist with EnvFile(path) as env: for var, value in tool_data.env.items(): env.append_if_new(var, value) @@ -126,7 +138,11 @@ def add_tool(tool_name: str, path: Optional[str] = None, agents: Optional[List[s with agentstack_config as config: config.tools.append(tool_name) - print(term_color(f'🔨 Tool {tool_name} added to agentstack project successfully', 'green')) + print( + term_color( + f'🔨 Tool {tool_name} added to agentstack project successfully', 'green' + ) + ) if tool_data.cta: print(term_color(f'đŸĒŠ {tool_data.cta}', 'blue')) @@ -160,13 +176,19 @@ def remove_tool(tool_name: str, path: Optional[str] = None): 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')) + 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(), ] + code_to_insert = [ + tool_data.get_import_statement(), + ] insert_code_after_tag(file_path, tag, code_to_insert, next_line=True) @@ -180,65 +202,80 @@ def remove_tool_from_tools_init(tool_data: ToolConfig, path: str = ''): print(line, end='') -def add_tool_to_agent_definition(framework: str, tool_data: ToolConfig, path: str = '', agents: list[str] = []): +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. + 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') + 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 = ''): - modify_agent_tools(framework=framework, tool_data=tool_data, operation='remove', agents=None, path=path, base_name='tools') +def remove_tool_from_agent_definition( + framework: str, tool_data: ToolConfig, path: str = '' +): + 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() + 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() + value=ast.Name(id=base_name, ctx=ast.Load()), attr=tool_name, ctx=ast.Load() ), - ctx=ast.Load() + ctx=ast.Load(), ) def _create_tool_attributes( - tool_names: List[str], - base_name: str = 'tools' + 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' + 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 + _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: +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 @@ -256,8 +293,7 @@ def _is_tool_node_match(node: ast.AST, tool_name: str, base_name: str = 'tools') # 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_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 @@ -265,10 +301,10 @@ def _is_tool_node_match(node: ast.AST, tool_name: str, base_name: str = 'tools') def _process_tools_list( - current_tools: List[ast.AST], - tool_data: ToolConfig, - operation: str, - base_name: str = 'tools' + 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. @@ -282,30 +318,30 @@ def _process_tools_list( 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 - )) + 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) + 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' + 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. @@ -323,8 +359,9 @@ def _modify_agent_tools( 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): + 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 @@ -337,10 +374,7 @@ def _modify_agent_tools( if isinstance(kw.value, ast.List): # Process the tools list new_tools = _process_tools_list( - kw.value.elts, - tool_data, - operation, - base_name + kw.value.elts, tool_data, operation, base_name ) # Replace with new list @@ -350,12 +384,12 @@ def _modify_agent_tools( def modify_agent_tools( - framework: str, - tool_data: ToolConfig, - operation: str, - agents: Optional[List[str]] = None, - path: str = '', - base_name: str = '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. @@ -405,4 +439,4 @@ def visit_FunctionDef(self, node): final_lines.append(line + '\n') with open(filename, 'w', encoding='utf-8') as f: - f.write(''.join(final_lines)) \ No newline at end of file + f.write(''.join(final_lines)) diff --git a/agentstack/logger.py b/agentstack/logger.py index 680d3067..e41c4887 100644 --- a/agentstack/logger.py +++ b/agentstack/logger.py @@ -16,7 +16,9 @@ def get_logger(name, debug=False): handler = logging.StreamHandler(sys.stdout) handler.setLevel(log_level) - formatter = logging.Formatter("%(asctime)s - %(process)d - %(threadName)s - %(filename)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s") + formatter = logging.Formatter( + "%(asctime)s - %(process)d - %(threadName)s - %(filename)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s" + ) handler.setFormatter(formatter) if not logger.handlers: diff --git a/agentstack/main.py b/agentstack/main.py index 45bad63a..2c32bd20 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -10,73 +10,98 @@ import webbrowser + def main(): parser = argparse.ArgumentParser( description="AgentStack CLI - The easiest way to build an agent application" ) - parser.add_argument('-v', '--version', action='store_true', help="Show the version") + parser.add_argument("-v", "--version", action="store_true", help="Show the version") # Create top-level subparsers - subparsers = parser.add_subparsers(dest='command', help='Available commands') + subparsers = parser.add_subparsers(dest="command", help="Available commands") # 'docs' command - subparsers.add_parser('docs', help='Open Agentstack docs') + subparsers.add_parser("docs", help="Open Agentstack docs") # 'quickstart' command - subparsers.add_parser('quickstart', help='Open the quickstart guide') + subparsers.add_parser("quickstart", help="Open the quickstart guide") # 'templates' command - subparsers.add_parser('templates', help='View Agentstack templates') + 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('--wizard', '-w', action='store_true', help="Use the setup wizard") - init_parser.add_argument('--template', '-t', help="Agent template to use") + 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( + "--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') + _ = subparsers.add_parser("run", aliases=["r"], help="Run your agent") # 'generate' command - generate_parser = subparsers.add_parser('generate', aliases=['g'], help='Generate agents or tasks') + generate_parser = subparsers.add_parser( + "generate", aliases=["g"], help="Generate agents or tasks" + ) # Subparsers under 'generate' - generate_subparsers = generate_parser.add_subparsers(dest='generate_command', help='Generate agents or tasks') + generate_subparsers = generate_parser.add_subparsers( + dest="generate_command", help="Generate agents or tasks" + ) # 'agent' command under 'generate' - agent_parser = generate_subparsers.add_parser('agent', aliases=['a'], help='Generate an agent') - agent_parser.add_argument('name', help='Name of the agent') - agent_parser.add_argument('--role', '-r', help='Role of the agent') - agent_parser.add_argument('--goal', '-g', help='Goal of the agent') - agent_parser.add_argument('--backstory', '-b', help='Backstory of the agent') - agent_parser.add_argument('--llm', '-l', help='Language model to use') + agent_parser = generate_subparsers.add_parser( + "agent", aliases=["a"], help="Generate an agent" + ) + agent_parser.add_argument("name", help="Name of the agent") + agent_parser.add_argument("--role", "-r", help="Role of the agent") + agent_parser.add_argument("--goal", "-g", help="Goal of the agent") + agent_parser.add_argument("--backstory", "-b", help="Backstory of the agent") + agent_parser.add_argument("--llm", "-l", help="Language model to use") # 'task' command under 'generate' - task_parser = generate_subparsers.add_parser('task', aliases=['t'], help='Generate a task') - task_parser.add_argument('name', help='Name of the task') - task_parser.add_argument('--description', '-d', help='Description of the task') - task_parser.add_argument('--expected_output', '-e', help='Expected output of the task') - task_parser.add_argument('--agent', '-a', help='Agent associated with the task') + task_parser = generate_subparsers.add_parser( + "task", aliases=["t"], help="Generate a task" + ) + task_parser.add_argument("name", help="Name of the task") + task_parser.add_argument("--description", "-d", help="Description of the task") + task_parser.add_argument( + "--expected_output", "-e", help="Expected output of the task" + ) + task_parser.add_argument("--agent", "-a", help="Agent associated with the task") # 'tools' command - tools_parser = subparsers.add_parser('tools', aliases=['t'], help='Manage tools') + tools_parser = subparsers.add_parser("tools", aliases=["t"], help="Manage tools") # Subparsers under 'tools' - tools_subparsers = tools_parser.add_subparsers(dest='tools_command', help='Tools commands') + tools_subparsers = tools_parser.add_subparsers( + dest="tools_command", help="Tools commands" + ) # 'list' command under 'tools' - tools_list_parser = tools_subparsers.add_parser('list', aliases=['l'], help='List tools') + _ = tools_subparsers.add_parser("list", aliases=["l"], help="List tools") # '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') + 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') - tools_remove_parser.add_argument('name', help='Name of the tool to remove') + tools_remove_parser = tools_subparsers.add_parser( + "remove", aliases=["r"], help="Remove a tool" + ) + tools_remove_parser.add_argument("name", help="Name of the tool to remove") update = subparsers.add_parser('update', aliases=['u'], help='Check for updates') @@ -92,45 +117,49 @@ def main(): check_for_updates(update_requested=args.command in ('update', 'u')) # Handle commands - if args.command in ['docs']: - webbrowser.open('https://docs.agentstack.sh/') - elif args.command in ['quickstart']: - webbrowser.open('https://docs.agentstack.sh/quickstart') - elif args.command in ['templates']: - webbrowser.open('https://docs.agentstack.sh/quickstart') - elif args.command in ['init', 'i']: + if args.command in ["docs"]: + webbrowser.open("https://docs.agentstack.sh/") + elif args.command in ["quickstart"]: + webbrowser.open("https://docs.agentstack.sh/quickstart") + elif args.command in ["templates"]: + webbrowser.open("https://docs.agentstack.sh/quickstart") + elif args.command in ["init", "i"]: init_project_builder(args.slug_name, args.template, args.wizard) - elif args.command in ['run', 'r']: + elif args.command in ["run", "r"]: framework = get_framework() if framework == "crewai": - os.system('python src/main.py') - elif args.command in ['generate', 'g']: - if args.generate_command in ['agent', 'a']: + os.system("python src/main.py") + elif args.command in ["generate", "g"]: + if args.generate_command in ["agent", "a"]: if not args.llm: configure_default_model() - generation.generate_agent(args.name, args.role, args.goal, args.backstory, args.llm) - elif args.generate_command in ['task', 't']: - generation.generate_task(args.name, args.description, args.expected_output, args.agent) + generation.generate_agent( + args.name, args.role, args.goal, args.backstory, args.llm + ) + elif args.generate_command in ["task", "t"]: + generation.generate_task( + args.name, args.description, args.expected_output, args.agent + ) else: generate_parser.print_help() - elif args.command in ['tools', 't']: - if args.tools_command in ['list', 'l']: + elif args.command in ["tools", "t"]: + if args.tools_command in ["list", "l"]: list_tools() - elif args.tools_command in ['add', 'a']: + elif args.tools_command in ["add", "a"]: agents = [args.agent] if args.agent else None - agents = args.agents.split(',') if args.agents else agents + agents = args.agents.split(",") if args.agents else agents generation.add_tool(args.name, agents=agents) - elif args.tools_command in ['remove', 'r']: + elif args.tools_command in ["remove", "r"]: generation.remove_tool(args.name) else: tools_parser.print_help() elif args.command in ['update', 'u']: - pass # Update check already done + pass # Update check already done else: parser.print_help() -if __name__ == '__main__': +if __name__ == "__main__": try: main() except KeyboardInterrupt: diff --git a/agentstack/packaging.py b/agentstack/packaging.py index 3e8a6adb..fb0e3cb5 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -3,13 +3,16 @@ PACKAGING_CMD = "poetry" + def install(package: str, path: Optional[str] = None): if path: os.chdir(path) os.system(f"{PACKAGING_CMD} add {package}") + def remove(package: str): os.system(f"{PACKAGING_CMD} remove {package}") + def upgrade(package: str): os.system(f"{PACKAGING_CMD} add {package}") diff --git a/agentstack/telemetry.py b/agentstack/telemetry.py index db613c8c..1e8c3e48 100644 --- a/agentstack/telemetry.py +++ b/agentstack/telemetry.py @@ -32,6 +32,7 @@ TELEMETRY_URL = 'https://api.agentstack.sh/telemetry' + def collect_machine_telemetry(command: str): if command != "init" and get_telemetry_opt_out(): return @@ -43,7 +44,7 @@ def collect_machine_telemetry(command: str): 'os_version': platform.version(), 'cpu_count': psutil.cpu_count(logical=True), 'memory': psutil.virtual_memory().total, - 'agentstack_version': get_version() + 'agentstack_version': get_version(), } if command != "init": @@ -56,12 +57,14 @@ def collect_machine_telemetry(command: str): response = requests.get('https://ipinfo.io/json') if response.status_code == 200: location_data = response.json() - telemetry_data.update({ - 'ip': location_data.get('ip'), - 'city': location_data.get('city'), - 'region': location_data.get('region'), - 'country': location_data.get('country') - }) + telemetry_data.update( + { + 'ip': location_data.get('ip'), + 'city': location_data.get('city'), + 'region': location_data.get('region'), + 'country': location_data.get('country'), + } + ) except requests.RequestException as e: telemetry_data['location_error'] = str(e) @@ -72,5 +75,5 @@ def track_cli_command(command: str): try: data = collect_machine_telemetry(command) requests.post(TELEMETRY_URL, json={"command": command, **data}) - except: + except Exception: pass diff --git a/agentstack/update.py b/agentstack/update.py index 1b1bd55d..041d84ec 100644 --- a/agentstack/update.py +++ b/agentstack/update.py @@ -20,7 +20,7 @@ def _is_ci_environment(): 'TRAVIS', 'CIRCLECI', 'JENKINS_URL', - 'TEAMCITY_VERSION' + 'TEAMCITY_VERSION', ] return any(os.getenv(var) for var in ci_env_vars) @@ -45,7 +45,11 @@ def _is_ci_environment(): def get_latest_version(package: str) -> Version: """Get version information from PyPi to save a full package manager invocation""" import requests # defer import until we know we need it - response = requests.get(f"{ENDPOINT_URL}/{package}/", headers={"Accept": "application/vnd.pypi.simple.v1+json"}) + + response = requests.get( + f"{ENDPOINT_URL}/{package}/", + headers={"Accept": "application/vnd.pypi.simple.v1+json"}, + ) if response.status_code != 200: raise Exception(f"Failed to fetch package data from pypi.") data = response.json() @@ -116,14 +120,25 @@ def check_for_updates(update_requested: bool = False): installed_version: Version = parse_version(get_version(AGENTSTACK_PACKAGE)) if latest_version > installed_version: print('') # newline - if inquirer.confirm(f"New version of {AGENTSTACK_PACKAGE} available: {latest_version}! Do you want to install?"): + if inquirer.confirm( + f"New version of {AGENTSTACK_PACKAGE} available: {latest_version}! Do you want to install?" + ): packaging.upgrade(f'{AGENTSTACK_PACKAGE}[{get_framework()}]') - print(term_color(f"{AGENTSTACK_PACKAGE} updated. Re-run your command to use the latest version.", 'green')) + print( + term_color( + f"{AGENTSTACK_PACKAGE} updated. Re-run your command to use the latest version.", + 'green', + ) + ) sys.exit(0) else: - print(term_color("Skipping update. Run `agentstack update` to install the latest version.", 'blue')) + print( + term_color( + "Skipping update. Run `agentstack update` to install the latest version.", + 'blue', + ) + ) else: print(f"{AGENTSTACK_PACKAGE} is up to date ({installed_version})") record_update_check() - diff --git a/agentstack/utils.py b/agentstack/utils.py index 0d1bcea9..c6e7b274 100644 --- a/agentstack/utils.py +++ b/agentstack/utils.py @@ -8,6 +8,7 @@ from pathlib import Path import importlib.resources + def get_version(package: str = 'agentstack'): try: return version(package) @@ -18,12 +19,15 @@ def get_version(package: str = 'agentstack'): def verify_agentstack_project(path: Optional[str] = None): from agentstack.generation import ConfigFile + try: agentstack_config = ConfigFile(path) except FileNotFoundError: - print("\033[31mAgentStack Error: This does not appear to be an AgentStack project." - "\nPlease ensure you're at the root directory of your project and a file named agentstack.json exists. " - "If you're starting a new project, run `agentstack init`\033[0m") + print( + "\033[31mAgentStack Error: This does not appear to be an AgentStack project." + "\nPlease ensure you're at the root directory of your project and a file named agentstack.json exists. " + "If you're starting a new project, run `agentstack init`\033[0m" + ) sys.exit(1) @@ -36,6 +40,7 @@ def get_package_path() -> Path: def get_framework(path: Optional[str] = None) -> str: from agentstack.generation import ConfigFile + try: agentstack_config = ConfigFile(path) framework = agentstack_config.framework @@ -45,19 +50,25 @@ def get_framework(path: Optional[str] = None) -> str: return framework except FileNotFoundError: - print("\033[31mFile agentstack.json does not exist. Are you in the right directory?\033[0m") + print( + "\033[31mFile agentstack.json does not exist. Are you in the right directory?\033[0m" + ) sys.exit(1) def get_telemetry_opt_out(path: Optional[str] = None) -> str: from agentstack.generation import ConfigFile + try: agentstack_config = ConfigFile(path) return bool(agentstack_config.telemetry_opt_out) except FileNotFoundError: - print("\033[31mFile agentstack.json does not exist. Are you in the right directory?\033[0m") + print( + "\033[31mFile agentstack.json does not exist. Are you in the right directory?\033[0m" + ) sys.exit(1) + def camel_to_snake(name): s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() @@ -75,7 +86,12 @@ def open_json_file(path) -> dict: def clean_input(input_string): special_char_pattern = re.compile(r'[^a-zA-Z0-9\s_]') - return re.sub(special_char_pattern, '', input_string).lower().replace(' ', '_').replace('-', '_') + return ( + re.sub(special_char_pattern, '', input_string) + .lower() + .replace(' ', '_') + .replace('-', '_') + ) def term_color(text: str, color: str) -> str: @@ -86,7 +102,7 @@ def term_color(text: str, color: str) -> str: 'blue': '94', 'purple': '95', 'cyan': '96', - 'white': '97' + 'white': '97', } color_code = colors.get(color) if color_code: @@ -95,7 +111,5 @@ def term_color(text: str, color: str) -> str: return text - def is_snake_case(string: str): return bool(re.match('^[a-z0-9_]+$', string)) - diff --git a/examples/trip_planner/src/tools/__init__.py b/examples/trip_planner/src/tools/__init__.py index b0e4ead8..ce1fe5e4 100644 --- a/examples/trip_planner/src/tools/__init__.py +++ b/examples/trip_planner/src/tools/__init__.py @@ -5,12 +5,9 @@ from browserbase import Browserbase -from browserbase import Browserbase -from browserbase import Browserbase -from browserbase import Browserbase from .browserbase import Browserbase diff --git a/examples/web_researcher/src/tools/neon_tool.py b/examples/web_researcher/src/tools/neon_tool.py index a00e907d..d81cfb59 100644 --- a/examples/web_researcher/src/tools/neon_tool.py +++ b/examples/web_researcher/src/tools/neon_tool.py @@ -50,7 +50,7 @@ def execute_sql_ddl(connection_uri: str, command: str) -> str: return f"Failed to execute DDL command: {str(e)}" cur.close() conn.close() - return f"Command succeeded" + return "Command succeeded" @tool("Execute SQL DML") @@ -75,7 +75,7 @@ def run_sql_query(connection_uri: str, query: str) -> str: return f"Query result: {records}" except psycopg2.ProgrammingError: # For INSERT/UPDATE/DELETE operations - return f"Query executed successfully" + return "Query executed successfully" except Exception as e: conn.rollback() return f"Failed to execute SQL query: {str(e)}" diff --git a/pyproject.toml b/pyproject.toml index 4017a8f4..d97570b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,17 +30,40 @@ dependencies = [ ] [project.optional-dependencies] +dev = [ + "ruff", +] test = [ "tox>=4.23.2", ] crewai = [ - "crewai>=0.83.0", - "crewai-tools>=0.14.0" + "crewai==0.83.0", + "crewai-tools==0.14.0", ] + [tool.setuptools.package-data] agentstack = ["templates/**/*"] [project.scripts] agentstack = "agentstack.main:main" + +[tool.ruff] +exclude = [ + ".git", + ".env", + ".venv", + "venv", + "env", + "__pycache__", + "build", + "dist", + "*.egg-info", + "agentstack/templates/", + "examples", + "__init__.py" +] + +[tool.ruff.format] +quote-style = "preserve" \ No newline at end of file diff --git a/tests/test_cli_loads.py b/tests/test_cli_loads.py index 49bb15cd..ce166cb1 100644 --- a/tests/test_cli_loads.py +++ b/tests/test_cli_loads.py @@ -6,14 +6,16 @@ class TestAgentStackCLI(unittest.TestCase): - CLI_ENTRY = [sys.executable, "-m", "agentstack.main"] # Replace with your actual CLI entry point if different + CLI_ENTRY = [ + sys.executable, + "-m", + "agentstack.main", + ] # Replace with your actual CLI entry point if different def run_cli(self, *args): """Helper method to run the CLI with arguments.""" result = subprocess.run( - [*self.CLI_ENTRY, *args], - capture_output=True, - text=True + [*self.CLI_ENTRY, *args], capture_output=True, text=True ) return result diff --git a/tests/test_generation_files.py b/tests/test_generation_files.py index e2d80d7e..435bc745 100644 --- a/tests/test_generation_files.py +++ b/tests/test_generation_files.py @@ -1,35 +1,43 @@ -import os, sys +import os import unittest -import importlib.resources from pathlib import Path import shutil from agentstack.generation.files import ConfigFile, EnvFile -from agentstack.utils import verify_agentstack_project, get_framework, get_telemetry_opt_out +from agentstack.utils import ( + verify_agentstack_project, + get_framework, + get_telemetry_opt_out, +) BASE_PATH = Path(__file__).parent + class GenerationFilesTest(unittest.TestCase): def test_read_config(self): - config = ConfigFile(BASE_PATH / "fixtures") # + agentstack.json + config = ConfigFile(BASE_PATH / "fixtures") # + agentstack.json assert config.framework == "crewai" assert config.tools == ["tool1", "tool2"] assert config.telemetry_opt_out is None assert config.default_model is None - + def test_write_config(self): try: - os.makedirs(BASE_PATH/"tmp", exist_ok=True) - shutil.copy(BASE_PATH/"fixtures/agentstack.json", - BASE_PATH/"tmp/agentstack.json") - - with ConfigFile(BASE_PATH/"tmp") as config: + os.makedirs(BASE_PATH / "tmp", exist_ok=True) + shutil.copy( + BASE_PATH / "fixtures/agentstack.json", + BASE_PATH / "tmp/agentstack.json", + ) + + with ConfigFile(BASE_PATH / "tmp") as config: config.framework = "crewai" config.tools = ["tool1", "tool2"] config.telemetry_opt_out = True config.default_model = "openai/gpt-4o" - - tmp_data = open(BASE_PATH/"tmp/agentstack.json").read() - assert tmp_data == """{ + + tmp_data = open(BASE_PATH / "tmp/agentstack.json").read() + assert ( + tmp_data + == """{ "framework": "crewai", "tools": [ "tool1", @@ -38,56 +46,57 @@ def test_write_config(self): "telemetry_opt_out": true, "default_model": "openai/gpt-4o" }""" + ) except Exception as e: raise e finally: os.remove(BASE_PATH / "tmp/agentstack.json") - #os.rmdir(BASE_PATH / "tmp") + # os.rmdir(BASE_PATH / "tmp") def test_read_missing_config(self): - with self.assertRaises(FileNotFoundError) as context: - config = ConfigFile(BASE_PATH / "missing") + with self.assertRaises(FileNotFoundError) as _: + _ = ConfigFile(BASE_PATH / "missing") def test_verify_agentstack_project_valid(self): verify_agentstack_project(BASE_PATH / "fixtures") def test_verify_agentstack_project_invalid(self): - with self.assertRaises(SystemExit) as context: + with self.assertRaises(SystemExit) as _: verify_agentstack_project(BASE_PATH / "missing") - + def test_get_framework(self): assert get_framework(BASE_PATH / "fixtures") == "crewai" - with self.assertRaises(SystemExit) as context: + with self.assertRaises(SystemExit) as _: get_framework(BASE_PATH / "missing") - + def test_get_telemetry_opt_out(self): assert get_telemetry_opt_out(BASE_PATH / "fixtures") is False - with self.assertRaises(SystemExit) as context: + with self.assertRaises(SystemExit) as _: get_telemetry_opt_out(BASE_PATH / "missing") - + def test_read_env(self): env = EnvFile(BASE_PATH / "fixtures") assert env.variables == {"ENV_VAR1": "value1", "ENV_VAR2": "value2"} assert env["ENV_VAR1"] == "value1" assert env["ENV_VAR2"] == "value2" - with self.assertRaises(KeyError) as context: + with self.assertRaises(KeyError) as _: env["ENV_VAR3"] - + def test_write_env(self): try: - os.makedirs(BASE_PATH/"tmp", exist_ok=True) - shutil.copy(BASE_PATH/"fixtures/.env", - BASE_PATH/"tmp/.env") - - with EnvFile(BASE_PATH/"tmp") as env: - env.append_if_new("ENV_VAR1", "value100") # Should not be updated - env.append_if_new("ENV_VAR100", "value2") # Should be added - - tmp_data = open(BASE_PATH/"tmp/.env").read() - assert tmp_data == """\nENV_VAR1=value1\nENV_VAR2=value2\nENV_VAR100=value2""" + os.makedirs(BASE_PATH / "tmp", exist_ok=True) + shutil.copy(BASE_PATH / "fixtures/.env", BASE_PATH / "tmp/.env") + + with EnvFile(BASE_PATH / "tmp") as env: + env.append_if_new("ENV_VAR1", "value100") # Should not be updated + env.append_if_new("ENV_VAR100", "value2") # Should be added + + tmp_data = open(BASE_PATH / "tmp/.env").read() + assert ( + tmp_data == """\nENV_VAR1=value1\nENV_VAR2=value2\nENV_VAR100=value2""" + ) except Exception as e: raise e finally: os.remove(BASE_PATH / "tmp/.env") - #os.rmdir(BASE_PATH / "tmp") - + # os.rmdir(BASE_PATH / "tmp") diff --git a/tests/test_tool_config.py b/tests/test_tool_config.py index 90931c7b..20f820d6 100644 --- a/tests/test_tool_config.py +++ b/tests/test_tool_config.py @@ -1,12 +1,15 @@ import json -import os, sys import unittest -import importlib.resources from pathlib import Path -from agentstack.generation.tool_generation import get_all_tool_paths, get_all_tool_names, ToolConfig +from agentstack.generation.tool_generation import ( + get_all_tool_paths, + get_all_tool_names, + ToolConfig, +) BASE_PATH = Path(__file__).parent + class ToolConfigTest(unittest.TestCase): def test_minimal_json(self): config = ToolConfig.from_json(BASE_PATH / "fixtures/tool_config_min.json") @@ -20,7 +23,7 @@ def test_minimal_json(self): assert config.packages is None assert config.post_install is None assert config.post_remove is None - + def test_maximal_json(self): config = ToolConfig.from_json(BASE_PATH / "fixtures/tool_config_max.json") assert config.name == "tool_name" @@ -33,7 +36,7 @@ def test_maximal_json(self): assert config.packages == ["package1", "package2"] assert config.post_install == "install.sh" assert config.post_remove == "remove.sh" - + def test_all_json_configs_from_tool_name(self): for tool_name in get_all_tool_names(): config = ToolConfig.from_tool_name(tool_name) @@ -44,8 +47,10 @@ def test_all_json_configs_from_tool_path(self): for path in get_all_tool_paths(): try: config = ToolConfig.from_json(path) - except json.decoder.JSONDecodeError as e: - raise Exception(f"Failed to decode tool json at {path}. Does your tool config fit the required formatting? https://github.com/AgentOps-AI/AgentStack/blob/main/agentstack/tools/~README.md") + except json.decoder.JSONDecodeError: + raise Exception( + f"Failed to decode tool json at {path}. Does your tool config fit the required formatting? https://github.com/AgentOps-AI/AgentStack/blob/main/agentstack/tools/~README.md" + ) assert config.name == path.stem # We can assume that pydantic validation caught any other issues